jfxtras.icalendarfx.properties.component.recurrence.RecurrenceRuleCache Maven / Gradle / Ivy
package jfxtras.icalendarfx.properties.component.recurrence;
import java.time.temporal.Temporal;
import java.util.Arrays;
import java.util.Iterator;
import java.util.stream.Stream;
import jfxtras.icalendarfx.components.VRepeatable;
import jfxtras.icalendarfx.properties.component.recurrence.rrule.RecurrenceRuleValue;
import jfxtras.icalendarfx.utilities.DateTimeUtilities;
/**
* Handles caching an array of Temporal date/time values to speed up producing a stream
* of recurrence instances for a recurrence rule (RRULE).
* RFC 5545 3.8.5.2, page 121
* The recurrence set is the complete set of recurrence instances for a calendar component.
*
* @author David Bal
*
*/
public class RecurrenceRuleCache
{
// Variables for start date or date/time cache used as starting Temporal for stream
private static final int CACHE_RANGE = 51; // number of values in cache
private static final int CACHE_SKIP = 21; // store every nth value in the cache
private int skipCounter = 0; // counter that increments up to CACHE_SKIP, indicates time to record a value, then resets to 0
public Temporal[] temporalCache; // the start date or date/time cache
private Temporal dateTimeStartLast; // last dateTimeStart, when changes indicates clearing the cache is necessary
private RecurrenceRuleValue rRuleLast; // last rRule, when changes indicates clearing the cache is necessary
public int cacheStart = 0; // start index where cache values are stored (starts in middle)
public int cacheEnd = 0; // end index where cache values are stored
private VRepeatable> component; // the VComponent
public RecurrenceRuleCache(VRepeatable> component)
{
this.component = component;
}
/**
* finds previous value in recurrence set before input parameter value
*
* @param value - start value
* @return - previous recurrence instance
*/
public Temporal previousValue(Temporal value)
{
final Temporal start;
if (cacheEnd == 0)
{
start = component.getDateTimeStart().getValue();
} else
{ // try to get start from cache
Temporal m = null;
for (int i=cacheEnd; i>cacheStart; i--)
{
if (DateTimeUtilities.isBefore(temporalCache[i], value))
{
m = temporalCache[i];
break;
}
}
start = (m != null) ? m : component.getDateTimeStart().getValue();
}
Iterator i = component.streamRecurrences(start).iterator();
// Iterator i = streamNoCache(start).iterator();
Temporal lastT = null;
while (i.hasNext())
{
Temporal t = i.next();
if (! DateTimeUtilities.isBefore(t, value)) break;
lastT = t;
}
return lastT;
}
/**
* Returns the previous start date/time value of the recurrence set on or before targetStart in the cache. If no value
* is present in the cache then the DTSTART value is returned. This value is guaranteed to be
* a valid recurrence date/time. It can be used as a starting point for calculating future
* recurrences more efficiently than using DTSTART.
*
* @param targetStart - target date/time to get previous recurrence.
* @return closest recurrence DTSTART value, without going over.
*/
public Temporal getClosestStart(Temporal targetStart)
{
final Temporal match;
final Temporal dateTimeStart;
final RecurrenceRuleValue recurrenceRule;
dateTimeStart = component.getDateTimeStart().getValue();
recurrenceRule = (component.getRecurrenceRule() != null) ? component.getRecurrenceRule().getValue() : null;
// adjust start to ensure its not before dateTimeStart
final Temporal start2 = (DateTimeUtilities.isBefore(targetStart, dateTimeStart)) ? dateTimeStart : targetStart;
final Temporal latestCacheValue;
if (recurrenceRule == null)
{ // if individual event
return null;
}
// check if cache needs to be cleared (changes to RRULE or DTSTART)
if ((dateTimeStartLast != null) && (rRuleLast != null))
{
boolean startChanged = ! dateTimeStart.equals(dateTimeStartLast);
boolean rRuleChanged = ! recurrenceRule.equals(rRuleLast);
if (startChanged || rRuleChanged)
{
temporalCache = null;
cacheStart = 0;
cacheEnd = 0;
skipCounter = 0;
dateTimeStartLast = dateTimeStart;
rRuleLast = recurrenceRule;
}
} else
{ // save current DTSTART and RRULE for next time
dateTimeStartLast = dateTimeStart;
rRuleLast = recurrenceRule;
}
// use cache if available to find matching start date or date/time
if (temporalCache != null)
{
// Reorder cache to maintain centered and sorted
final int len = temporalCache.length;
final Temporal[] p1;
final Temporal[] p2;
if (cacheEnd < cacheStart) // high values have wrapped from end to beginning
{
p1 = Arrays.copyOfRange(temporalCache, cacheStart, len);
p2 = Arrays.copyOfRange(temporalCache, 0, Math.min(cacheEnd+1,len));
} else if (cacheEnd > cacheStart) // low values have wrapped from beginning to end
{
p2 = Arrays.copyOfRange(temporalCache, cacheStart, len);
p1 = Arrays.copyOfRange(temporalCache, 0, Math.min(cacheEnd+1,len));
} else
{
p1 = null;
p2 = null;
}
if (p1 != null)
{ // copy elements to accommodate wrap and restore sort order
int p1Index = 0;
int p2Index = 0;
for (int i=0; i makeCache(Stream inStream)
{
Temporal earliestCacheValue = temporalCache[cacheStart];
Temporal latestCacheValue = temporalCache[cacheEnd];
// System.out.println("makeCache:" + earliestCacheValue + " " + latestCacheValue + " " + component.getRecurrences());
Stream outStream = inStream
.peek(t ->
{ // save new values in cache
if (component.getRecurrenceRule() != null)
{
if (DateTimeUtilities.isBefore(t, earliestCacheValue))
{
if (skipCounter == CACHE_SKIP)
{
cacheStart--;
if (cacheStart < 0) cacheStart = CACHE_RANGE - 1;
if (cacheStart == cacheEnd) cacheEnd--; // just overwrote oldest value - push cacheEnd down
temporalCache[cacheStart] = t;
skipCounter = 0;
} else skipCounter++;
}
if (DateTimeUtilities.isAfter(t, latestCacheValue))
{
if (skipCounter == CACHE_SKIP)
{
cacheEnd++;
if (cacheEnd == CACHE_RANGE) cacheEnd = 0;
if (cacheStart == cacheEnd) cacheStart++; // just overwrote oldest value - push cacheStart up
temporalCache[cacheEnd] = t;
skipCounter = 0;
} else skipCounter++;
}
// check if start or end needs to wrap
if (cacheEnd < 0) cacheEnd = CACHE_RANGE - 1;
if (cacheStart == CACHE_RANGE) cacheStart = 0;
}
});
return outStream;
}
// /** Stream of date/times that indicate the start of the event(s).
// * For a VEvent without RRULE the stream will contain only one date/time element.
// * A VEvent with a RRULE the stream contains more than one date/time element. It will be infinite
// * if COUNT or UNTIL is not present. The stream has an end when COUNT or UNTIL condition is met.
// * Starts on startDateTime, which must be a valid event date/time, not necessarily the
// * first date/time (DTSTART) in the sequence.
// *
// * @param start - starting date or date/time for which occurrence start date or date/time
// * are generated by the returned stream
// * @return stream of starting dates or date/times for occurrences after rangeStart
// */
// @Deprecated
// public Stream streamNoCache(Temporal start)
// {
// final Comparator temporalComparator;
// if (start instanceof LocalDate)
// {
// temporalComparator = (t1, t2) -> ((LocalDate) t1).compareTo((LocalDate) t2);
// } else if (start instanceof LocalDateTime)
// {
// temporalComparator = (t1, t2) -> ((LocalDateTime) t1).compareTo((LocalDateTime) t2);
// } else if (start instanceof ZonedDateTime)
// {
// temporalComparator = (t1, t2) -> ((ZonedDateTime) t1).compareTo((ZonedDateTime) t2);
// } else
// {
// throw new DateTimeException("Unsupported Temporal type:" + start.getClass().getSimpleName());
// }
// final Stream stream1;
// if (component.getRecurrenceRule() == null)
// { // if individual event
// stream1 = Arrays.asList(component.getDateTimeStart().getValue())
// .stream()
// .map(v -> (Temporal) v)
// .filter(d -> ! DateTimeUtilities.isBefore(d, start));
// } else
// { // if has recurrence rule
// stream1 = component.getRecurrenceRule().getValue().streamRecurrences(component.getDateTimeStart().getValue());
// }
// // If present, add recurrence list
// final Stream stream2;
// if (component.getRecurrences() == null)
// {
// stream2 = stream1;
// } else
// {
// stream2 = RecurrenceRuleCache.merge(
// stream1
// , component.getRecurrences()
// .stream()
// .flatMap(r -> r.getValue().stream())
// .map(v -> (Temporal) v)
// .sorted(temporalComparator)
// , temporalComparator);
// }
//
// final Stream stream3;
// if ((component instanceof VComponentDisplayable) && (((VComponentDisplayable>) component).getExceptions() != null))
// {
// /** Remove date/times in exDates set */
// List exceptions = ((VComponentDisplayable>) component).getExceptions()
// .stream()
// .flatMap(r -> r.getValue().stream())
// .map(v -> (Temporal) v)
// .sorted(temporalComparator)
// .collect(Collectors.toList());
// stream3 = stream2.filter(d -> ! exceptions.contains(d));
// } else
// {
// stream3 = stream2;
// }
// return stream3.filter(t -> ! DateTimeUtilities.isBefore(t, start));
// }
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy