net.fortuna.ical4j.model.Recur Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ical4j Show documentation
Show all versions of ical4j Show documentation
A Java library for reading and writing iCalendar (*.ics) files
The newest version!
/**
* Copyright (c) 2012, Ben Fortuna
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* o Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* o Neither the name of Ben Fortuna nor the names of any other contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.fortuna.ical4j.model;
import net.fortuna.ical4j.transform.recurrence.*;
import net.fortuna.ical4j.util.CompatibilityHints;
import net.fortuna.ical4j.util.Configurator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.Serializable;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.chrono.Chronology;
import java.time.temporal.*;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
/**
* $Id$ [18-Apr-2004]
*
*
* 3.3.10. Recurrence Rule
*
* Value Name: RECUR
*
* Purpose: This value type is used to identify properties that contain
* a recurrence rule specification.
*
* Format Definition: This value type is defined by the following
* notation:
*
* recur = recur-rule-part *( ";" recur-rule-part )
* ;
* ; The rule parts are not ordered in any
* ; particular sequence.
* ;
* ; The FREQ rule part is REQUIRED,
* ; but MUST NOT occur more than once.
* ;
* ; The UNTIL or COUNT rule parts are OPTIONAL,
* ; but they MUST NOT occur in the same 'recur'.
* ;
*
* ; The other rule parts are OPTIONAL,
* ; but MUST NOT occur more than once.
*
* recur-rule-part = ( "FREQ" "=" freq )
* / ( "UNTIL" "=" enddate )
* / ( "COUNT" "=" 1*DIGIT )
* / ( "INTERVAL" "=" 1*DIGIT )
* / ( "BYSECOND" "=" byseclist )
* / ( "BYMINUTE" "=" byminlist )
* / ( "BYHOUR" "=" byhrlist )
* / ( "BYDAY" "=" bywdaylist )
* / ( "BYMONTHDAY" "=" bymodaylist )
* / ( "BYYEARDAY" "=" byyrdaylist )
* / ( "BYWEEKNO" "=" bywknolist )
* / ( "BYMONTH" "=" bymolist )
* / ( "BYSETPOS" "=" bysplist )
* / ( "WKST" "=" weekday )
*
* freq = "SECONDLY" / "MINUTELY" / "HOURLY" / "DAILY"
* / "WEEKLY" / "MONTHLY" / "YEARLY"
*
* enddate = date / date-time
*
* byseclist = ( seconds *("," seconds) )
*
* seconds = 1*2DIGIT ;0 to 60
*
* byminlist = ( minutes *("," minutes) )
*
* minutes = 1*2DIGIT ;0 to 59
*
* byhrlist = ( hour *("," hour) )
*
* hour = 1*2DIGIT ;0 to 23
*
* bywdaylist = ( weekdaynum *("," weekdaynum) )
*
* weekdaynum = [[plus / minus] ordwk] weekday
*
* plus = "+"
*
* minus = "-"
*
* ordwk = 1*2DIGIT ;1 to 53
*
* weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA"
* ;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
* ;FRIDAY, and SATURDAY days of the week.
*
* bymodaylist = ( monthdaynum *("," monthdaynum) )
*
* monthdaynum = [plus / minus] ordmoday
*
* ordmoday = 1*2DIGIT ;1 to 31
*
* byyrdaylist = ( yeardaynum *("," yeardaynum) )
*
* yeardaynum = [plus / minus] ordyrday
*
* ordyrday = 1*3DIGIT ;1 to 366
*
* bywknolist = ( weeknum *("," weeknum) )
*
* weeknum = [plus / minus] ordwk
*
* bymolist = ( monthnum *("," monthnum) )
*
* monthnum = 1*2DIGIT ;1 to 12
*
* bysplist = ( setposday *("," setposday) )
*
* setposday = yeardaynum
*
*
* @author Ben Fortuna
* @version 2.0
*/
public class Recur implements Serializable {
private static final long serialVersionUID = -7333226591784095142L;
private static final String FREQ = "FREQ";
private static final String UNTIL = "UNTIL";
private static final String COUNT = "COUNT";
private static final String INTERVAL = "INTERVAL";
private static final String BYSECOND = "BYSECOND";
private static final String BYMINUTE = "BYMINUTE";
private static final String BYHOUR = "BYHOUR";
private static final String BYDAY = "BYDAY";
private static final String BYMONTHDAY = "BYMONTHDAY";
private static final String BYYEARDAY = "BYYEARDAY";
private static final String BYWEEKNO = "BYWEEKNO";
private static final String BYMONTH = "BYMONTH";
private static final String BYSETPOS = "BYSETPOS";
private static final String WKST = "WKST";
private static final String RSCALE = "RSCALE";
private static final String SKIP = "SKIP";
public enum RScale {
JAPANESE("Japanese"),
BUDDHIST("ThaiBuddhist"),
ROC("Minguo"),
ISLAMIC("islamic"),
ISO8601("ISO"),
CHINESE("ISO"),
ETHIOPIC("Ethiopic"),
HEBREW("ISO"),
GREGORIAN("ISO");
private final String chronology;
RScale(String chronology) {
this.chronology = chronology;
}
public String getChronology() {
return chronology;
}
}
public enum Skip {
OMIT, BACKWARD, FORWARD
}
/**
* Second frequency resolution.
* @deprecated use {@link Frequency} instead.
*/
@Deprecated
public static final String SECONDLY = "SECONDLY";
/**
* Minute frequency resolution.
* @deprecated use {@link Frequency} instead.
*/
@Deprecated
public static final String MINUTELY = "MINUTELY";
/**
* Hour frequency resolution.
* @deprecated use {@link Frequency} instead.
*/
@Deprecated
public static final String HOURLY = "HOURLY";
/**
* Day frequency resolution.
* @deprecated use {@link Frequency} instead.
*/
@Deprecated
public static final String DAILY = "DAILY";
/**
* Week frequency resolution.
* @deprecated use {@link Frequency} instead.
*/
@Deprecated
public static final String WEEKLY = "WEEKLY";
/**
* Month frequency resolution.
* @deprecated use {@link Frequency} instead.
*/
@Deprecated
public static final String MONTHLY = "MONTHLY";
/**
* Year frequency resolution.
* @deprecated use {@link Frequency} instead.
*/
@Deprecated
public static final String YEARLY = "YEARLY";
/**
* When calculating dates matching this recur ({@code getDates()} or {@code getNextDate}),
* this property defines the maximum number of attempt to find a matching date by
* incrementing the seed.
* The default value is 1000. A value of -1 corresponds to no maximum.
*/
public static final String KEY_MAX_INCREMENT_COUNT = "net.fortuna.ical4j.recur.maxincrementcount";
private static final int maxIncrementCount;
static {
maxIncrementCount = Configurator.getIntProperty(KEY_MAX_INCREMENT_COUNT).orElse(1_000);
}
private transient Logger log = LoggerFactory.getLogger(Recur.class);
private static final Comparator CANDIDATE_SORTER = TemporalComparator.INSTANCE;
private Frequency frequency;
private Skip skip;
private TemporalAdapter until;
private RScale rscale;
private Integer count;
private Integer interval;
private List secondList = new NumberList(ChronoField.SECOND_OF_MINUTE.range(), false);
private List minuteList = new NumberList(ChronoField.MINUTE_OF_HOUR.range(), false);
private List hourList = new NumberList(ChronoField.HOUR_OF_DAY.range(), false);
private final List dayList = new WeekDayList();
private List monthDayList = new NumberList(ChronoField.DAY_OF_MONTH.range(), true);
private List yearDayList = new NumberList(ChronoField.DAY_OF_YEAR.range(), true);
private List weekNoList = new NumberList(WeekFields.ISO.weekOfWeekBasedYear().range(), true);
private List monthList = new MonthList(ChronoField.MONTH_OF_YEAR.range());
private List setPosList = new NumberList(ChronoField.DAY_OF_YEAR.range(), true);
private WeekDay weekStartDay;
private final Map experimentalValues = new HashMap();
// Temporal field we increment based on frequency.
private TemporalUnit calIncField;
/**
* Default constructor.
*/
private Recur() {
}
/**
* Constructs a new instance from the specified string value.
*
* @param aValue a string representation of a recurrence.
*/
public Recur(final String aValue) {
this(aValue, CompatibilityHints.isHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING));
}
/**
* Constructs a new recurrence from the specified string value.
* @param aValue a string representation of a recurrence.
* @param experimentalTokensAllowed allow unrecognised tokens in the recurrence
*/
public Recur(final String aValue, boolean experimentalTokensAllowed) {
var chronology = Chronology.ofLocale(Locale.getDefault());
Iterator tokens = Arrays.asList(aValue.split("[;=]")).iterator();
while (tokens.hasNext()) {
final var token = tokens.next();
if (FREQ.equals(token)) {
frequency = Frequency.valueOf(nextToken(tokens, token));
} else if (SKIP.equals(token)) {
skip = Skip.valueOf(nextToken(tokens, token));
} else if (RSCALE.equals(token)) {
rscale = RScale.valueOf(nextToken(tokens, token));
chronology = Chronology.of(rscale.getChronology());
} else if (UNTIL.equals(token)) {
final String untilString = nextToken(tokens, token);
until = TemporalAdapter.parse(untilString);
} else if (COUNT.equals(token)) {
count = Integer.parseInt(nextToken(tokens, token));
} else if (INTERVAL.equals(token)) {
interval = Integer.parseInt(nextToken(tokens, token));
} else if (BYSECOND.equals(token)) {
secondList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.SECOND_OF_MINUTE), false);
} else if (BYMINUTE.equals(token)) {
minuteList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.MINUTE_OF_HOUR), false);
} else if (BYHOUR.equals(token)) {
hourList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.HOUR_OF_DAY), false);
} else if (BYDAY.equals(token)) {
dayList.addAll(new WeekDayList(nextToken(tokens, token)));
} else if (BYMONTHDAY.equals(token)) {
monthDayList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.DAY_OF_MONTH), true);
} else if (BYYEARDAY.equals(token)) {
yearDayList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.DAY_OF_YEAR), true);
} else if (BYWEEKNO.equals(token)) {
weekNoList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.ALIGNED_WEEK_OF_YEAR), true);
} else if (BYMONTH.equals(token)) {
monthList = new MonthList(nextToken(tokens, token), chronology.range(ChronoField.MONTH_OF_YEAR));
} else if (BYSETPOS.equals(token)) {
setPosList = new NumberList(nextToken(tokens, token), chronology.range(ChronoField.DAY_OF_YEAR), true);
} else if (WKST.equals(token)) {
weekStartDay = WeekDay.getWeekDay(WeekDay.Day.valueOf(nextToken(tokens, token)));
} else {
if (experimentalTokensAllowed) {
// assume experimental value..
experimentalValues.put(token, nextToken(tokens, token));
} else {
throw new IllegalArgumentException(String.format("Invalid recurrence rule part: %s=%s",
token, nextToken(tokens, token)));
}
}
}
validateFrequency();
}
private String nextToken(Iterator tokens, String lastToken) {
try {
return tokens.next();
} catch (NoSuchElementException e) {
throw new IllegalArgumentException("Missing expected token, last token: " + lastToken);
}
}
/**
* @param frequency a recurrence frequency string
* @param until maximum recurrence date
*/
@Deprecated
public Recur(final String frequency, final T until) {
this(Frequency.valueOf(frequency), until);
}
public Recur(final Frequency frequency) {
this.frequency = frequency;
validateFrequency();
}
/**
* @param frequency a recurrence frequency string
* @param until maximum recurrence date
*/
public Recur(final Frequency frequency, final T until) {
this.frequency = frequency;
this.until = new TemporalAdapter(until);
validateFrequency();
}
/**
* @param frequency a recurrence frequency string
* @param count maximum recurrence count
*/
@Deprecated
public Recur(final String frequency, final int count) {
this(Frequency.valueOf(frequency), count);
}
/**
* @param frequency a recurrence frequency string
* @param count maximum recurrence count
*/
public Recur(final Frequency frequency, final int count) {
this.frequency = frequency;
this.count = count;
validateFrequency();
}
private Frequency deriveFilterType() {
if (frequency == Frequency.DAILY || !getYearDayList().isEmpty() || !getMonthDayList().isEmpty()) {
return Frequency.DAILY;
} else if (frequency == Frequency.WEEKLY || !getWeekNoList().isEmpty()) {
return Frequency.WEEKLY;
} else if (frequency == Frequency.MONTHLY || !getMonthList().isEmpty()) {
return Frequency.MONTHLY;
} else {
return frequency;
}
}
/**
* Accessor for the configured BYDAY list.
* NOTE: Any changes to the returned list will have no effect on the recurrence rule processing.
*
* @return Returns the dayList.
*/
public final List getDayList() {
return dayList;
}
/**
* Accessor for the configured BYHOUR list.
* NOTE: Any changes to the returned list will have no effect on the recurrence rule processing.
*
* @return Returns the hourList.
*/
public final List getHourList() {
return hourList;
}
/**
* Accessor for the configured BYMINUTE list.
* NOTE: Any changes to the returned list will have no effect on the recurrence rule processing.
*
* @return Returns the minuteList.
*/
public final List getMinuteList() {
return minuteList;
}
/**
* Accessor for the configured BYMONTHDAY list.
* NOTE: Any changes to the returned list will have no effect on the recurrence rule processing.
*
* @return Returns the monthDayList.
*/
public final List getMonthDayList() {
return monthDayList;
}
/**
* Accessor for the configured BYMONTH list.
* NOTE: Any changes to the returned list will have no effect on the recurrence rule processing.
*
* @return Returns the monthList.
*/
public final List getMonthList() {
return monthList;
}
/**
* Accessor for the configured BYSECOND list.
* NOTE: Any changes to the returned list will have no effect on the recurrence rule processing.
*
* @return Returns the secondList.
*/
public final List getSecondList() {
return secondList;
}
/**
* Accessor for the configured BYSETPOS list.
* NOTE: Any changes to the returned list will have no effect on the recurrence rule processing.
*
* @return Returns the setPosList.
*/
public final List getSetPosList() {
return setPosList;
}
/**
* Accessor for the configured BYWEEKNO list.
* NOTE: Any changes to the returned list will have no effect on the recurrence rule processing.
*
* @return Returns the weekNoList.
*/
public final List getWeekNoList() {
return weekNoList;
}
/**
* Accessor for the configured BYYEARDAY list.
* NOTE: Any changes to the returned list will have no effect on the recurrence rule processing.
*
* @return Returns the yearDayList.
*/
public final List getYearDayList() {
return yearDayList;
}
/**
* @return Returns the count or -1 if the rule does not have a count.
*/
public final int getCount() {
return Optional.ofNullable(count).orElse(-1);
}
/**
* @return Returns the experimentalValues.
*/
public final Map getExperimentalValues() {
return experimentalValues;
}
/**
* @return Returns the frequency.
*/
public final Frequency getFrequency() {
return frequency;
}
/**
*
* @return leap month skip behaviour.
*/
public Skip getSkip() {
return skip;
}
/**
* @return Returns the interval or -1 if the rule does not have an interval defined.
*/
public final int getInterval() {
return Optional.ofNullable(interval).orElse(-1);
}
/**
* @return Returns the until or null if there is none.
*/
public final T getUntil() {
return until != null ? until.getTemporal() : null;
}
/**
* @return Returns the weekStartDay or null if there is none.
*/
public final WeekDay getWeekStartDay() {
return weekStartDay;
}
/**
* @param weekStartDay The weekStartDay to set.
* @deprecated will be removed in a future version to support immutable pattern.
*/
@Deprecated
public final void setWeekStartDay(final WeekDay weekStartDay) {
this.weekStartDay = weekStartDay;
if (frequency != null) {
// May have to update calIncField
validateFrequency();
}
}
/**
* {@inheritDoc}
*/
@Override
public final String toString() {
final var b = new StringBuilder();
if (rscale != null) {
b.append(RSCALE).append('=').append(rscale).append(';');
}
b.append(FREQ).append('=').append(frequency);
if (weekStartDay != null) {
b.append(';').append(WKST).append('=').append(weekStartDay);
}
if (until != null) {
// Note: UNTIL should always be in UTC time.
b.append(';').append(UNTIL).append('=').append(until);
}
if (count != null) {
b.append(';').append(COUNT).append('=').append(count);
}
if (interval != null) {
b.append(';').append(INTERVAL).append('=').append(interval);
}
if (!monthList.isEmpty()) {
b.append(';').append(BYMONTH).append('=').append(monthList);
}
if (!weekNoList.isEmpty()) {
b.append(';').append(BYWEEKNO).append('=').append(NumberList.toString(weekNoList));
}
if (!yearDayList.isEmpty()) {
b.append(';').append(BYYEARDAY).append('=').append(NumberList.toString(yearDayList));
}
if (!monthDayList.isEmpty()) {
b.append(';').append(BYMONTHDAY).append('=').append(NumberList.toString(monthDayList));
}
if (!dayList.isEmpty()) {
b.append(';').append(BYDAY).append('=').append(WeekDayList.toString(dayList));
}
if (!hourList.isEmpty()) {
b.append(';').append(BYHOUR).append('=').append(NumberList.toString(hourList));
}
if (!minuteList.isEmpty()) {
b.append(';').append(BYMINUTE).append('=').append(NumberList.toString(minuteList));
}
if (!secondList.isEmpty()) {
b.append(';').append(BYSECOND).append('=').append(NumberList.toString(secondList));
}
if (!setPosList.isEmpty()) {
b.append(';').append(BYSETPOS).append('=').append(NumberList.toString(setPosList));
}
if (skip != null) {
b.append(';').append(SKIP).append('=').append(skip);
}
return b.toString();
}
/**
* Returns a list of start dates in the specified period represented by this recur. Any date fields not specified by
* this recur are retained from the period start, and as such you should ensure the period start is initialised
* correctly.
*
* @param periodStart the start of the period
* @param periodEnd the end of the period
* @return a list of dates
*/
public final List getDates(final T periodStart, final T periodEnd) {
return getDates(periodStart, periodStart, periodEnd, -1);
}
/**
* Convenience method for retrieving recurrences in a specified period.
*
* @param seed a seed date for generating recurrence instances
* @param period the period of returned recurrence dates
* @return a list of dates
*/
public final List getDates(final T seed, final Period extends Temporal> period) {
return getDates(seed, period.getStart(), period.getEnd(), -1);
}
/**
* Returns a list of start dates in the specified period represented by this recur. This method includes a base date
* argument, which indicates the start of the fist occurrence of this recurrence. The base date is used to inject
* default values to return a set of dates in the correct format. For example, if the search start date (start) is
* Wed, Mar 23, 12:19PM, but the recurrence is Mon - Fri, 9:00AM - 5:00PM, the start dates returned should all be at
* 9:00AM, and not 12:19PM.
*
* @param seed the start date of this Recurrence's first instance
* @param periodStart the start of the period
* @param periodEnd the end of the period
* @return a list of dates represented by this recur instance
*/
public final List getDates(final T seed, final Temporal periodStart, final Temporal periodEnd) {
return getDates(seed, periodStart, periodEnd, -1);
}
/**
* Returns a list of start dates in the specified period represented by this recur. This method includes a base date
* argument, which indicates the start of the fist occurrence of this recurrence. The base date is used to inject
* default values to return a set of dates in the correct format. For example, if the search start date (start) is
* Wed, Mar 23, 12:19PM, but the recurrence is Mon - Fri, 9:00AM - 5:00PM, the start dates returned should all be at
* 9:00AM, and not 12:19PM.
*
* @param seed the start date of this Recurrence's first instance
* @param periodStart the start of the period
* @param periodEnd the end of the period
* @param maxCount limits the number of instances returned. Up to one years
* worth extra may be returned. Less than 0 means no limit
* @return a list of dates represented by this recur instance
*/
public final List getDates(final T seed, final Temporal periodStart, final Temporal periodEnd, final int maxCount) {
final List dates = getDatesAsStream(seed, periodStart, periodEnd, maxCount).collect(Collectors.toList());
// sort final list..
if (!TemporalAdapter.isDateTimePrecision(seed)) {
dates.sort(new TemporalComparator(ChronoUnit.DAYS));
} else {
dates.sort(CANDIDATE_SORTER);
}
return dates;
}
public final Stream getDatesAsStream(final T seed, final Temporal periodStart, final Temporal periodEnd,
int maxCount) {
Spliterator spliterator = new DateSpliterator(seed, periodStart, periodEnd, maxCount);
return StreamSupport.stream(spliterator, false);
}
/**
* Returns the next date of this recurrence given a seed date
* and start date. The seed date indicates the start of the fist
* occurrence of this recurrence. The start date is the
* starting date to search for the next recurrence. Return null
* if there is no occurrence date after start date.
*
* @param seed the start date of this Recurrence's first instance
* @param startDate the date to start the search
* @return the next date in the recurrence series after startDate
*/
public final T getNextDate(final T seed, final T startDate) {
T candidateSeed = seed;
int incrementMultiplier = 1;
// optimize the start time for selecting candidates
// (only applicable where a COUNT is not specified)
if (count == null) {
while (TemporalAdapter.isBefore(candidateSeed, startDate.minus(Math.max(getInterval(), 1), calIncField))) {
candidateSeed = increment(seed, incrementMultiplier++);
}
}
int invalidCandidateCount = 0;
int noCandidateIncrementCount = 0;
T candidate = candidateSeed;
while (true) {
if (getUntil() != null && TemporalAdapter.isAfter(candidate, getUntil())) {
break;
}
if (getCount() > 0 && invalidCandidateCount >= getCount()) {
break;
}
final List candidates = getCandidates(seed, candidateSeed);
if (!candidates.isEmpty()) {
noCandidateIncrementCount = 0;
for (T candidate1 : candidates) {
candidate = candidate1;
// don't count candidates that occur before the seed date..
if (!TemporalAdapter.isBefore(candidate, seed)) {
// Candidate must be after startDate because
// we want the NEXT occurrence
if (!TemporalAdapter.isAfter(candidate, startDate)) {
invalidCandidateCount++;
} else if (getCount() > 0 && invalidCandidateCount >= getCount()) {
break;
} else if (!(getUntil() != null && TemporalAdapter.isAfter(candidate, getUntil()))) {
return candidate;
}
}
}
} else {
noCandidateIncrementCount++;
if ((maxIncrementCount > 0) && (noCandidateIncrementCount > maxIncrementCount)) {
break;
}
}
candidateSeed = increment(seed, incrementMultiplier++);
}
return null;
}
/**
* Increments the specified temporal according to the frequency and interval specified in this recurrence rule.
*
* @param cal a {@link Temporal} value to increment
*/
private T increment(final T cal, int multiplier) {
// initialise interval..
final int calInterval = Math.max(getInterval(), 1) * multiplier;
//noinspection unchecked
return (T) cal.plus(calInterval, calIncField);
}
/**
* Returns a list of possible dates generated from the applicable BY* rules, using the specified date as a seed.
*
* @param date the seed date
* @return a List of Temporal of the same type as the seed date
*/
private List getCandidates(final T rootSeed, final T date) {
List dates = new ArrayList<>();
dates.add(date);
if (!monthList.isEmpty()) {
dates = new ByMonthRule(monthList, frequency, skip).apply(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYMONTH processing: " + dates);
}
}
if (!weekNoList.isEmpty()) {
dates = new ByWeekNoRule(weekNoList, frequency, WeekDay.getDayOfWeek(weekStartDay)).apply(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYWEEKNO processing: " + dates);
}
}
if (!yearDayList.isEmpty()) {
dates = new ByYearDayRule(yearDayList, frequency).apply(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYYEARDAY processing: " + dates);
}
}
if (!monthDayList.isEmpty()) {
dates = new ByMonthDayRule(monthDayList, frequency, skip).apply(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYMONTHDAY processing: " + dates);
}
} else if ((frequency == Frequency.MONTHLY && dayList.isEmpty()) ||
(frequency == Frequency.YEARLY && yearDayList.isEmpty()
&& weekNoList.isEmpty() && dayList.isEmpty())) {
List implicitMonthDayList = new NumberList(ChronoField.DAY_OF_MONTH.range(), false);
// where seed doesn't provide timezone rules derive using system default timezone..
implicitMonthDayList.add(new TemporalAdapter<>(rootSeed).toLocalTime().getDayOfMonth());
ByMonthDayRule implicitRule = new ByMonthDayRule<>(implicitMonthDayList, frequency, skip);
dates = implicitRule.apply(dates);
}
if (!dayList.isEmpty()) {
dates = new ByDayRule(dayList, deriveFilterType(), WeekDay.getDayOfWeek(weekStartDay)).apply(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYDAY processing: " + dates);
}
} else if (frequency == Frequency.WEEKLY || (frequency == Frequency.YEARLY && yearDayList.isEmpty()
&& !weekNoList.isEmpty() && monthDayList.isEmpty())) {
ByDayRule implicitRule = new ByDayRule<>(rootSeed, deriveFilterType(), WeekDay.getDayOfWeek(getWeekStartDay()));
dates = implicitRule.apply(dates);
}
if (!hourList.isEmpty()) {
dates = new ByHourRule(hourList, frequency).apply(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYHOUR processing: " + dates);
}
}
if (!minuteList.isEmpty()) {
dates = new ByMinuteRule(minuteList, frequency).apply(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYMINUTE processing: " + dates);
}
}
if (!secondList.isEmpty()) {
dates = new BySecondRule(secondList, frequency).apply(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYSECOND processing: " + dates);
}
}
if (!setPosList.isEmpty()) {
dates = new BySetPosRule(setPosList).apply(dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after SETPOS processing: " + dates);
}
}
dates.sort(CANDIDATE_SORTER);
return dates;
}
private void validateFrequency() {
if (frequency == null) {
throw new IllegalArgumentException("A recurrence rule MUST contain a FREQ rule part.");
}
if (Frequency.SECONDLY.equals(getFrequency())) {
calIncField = ChronoUnit.SECONDS;
} else if (Frequency.MINUTELY.equals(getFrequency())) {
calIncField = ChronoUnit.MINUTES;
} else if (Frequency.HOURLY.equals(getFrequency())) {
calIncField = ChronoUnit.HOURS;
} else if (Frequency.DAILY.equals(getFrequency())) {
calIncField = ChronoUnit.DAYS;
} else if (Frequency.WEEKLY.equals(getFrequency())) {
calIncField = ChronoUnit.WEEKS;
} else if (Frequency.MONTHLY.equals(getFrequency())) {
calIncField = ChronoUnit.MONTHS;
} else if (Frequency.YEARLY.equals(getFrequency())) {
if (getWeekNoList().isEmpty()) {
calIncField = ChronoUnit.YEARS;
} else {
calIncField = weekBasedYears(WeekDay.getDayOfWeek(getWeekStartDay()));
}
} else {
throw new IllegalArgumentException("Invalid FREQ rule part '"
+ frequency + "' in recurrence rule");
}
}
private static TemporalUnit weekBasedYears(DayOfWeek weekStartDay) {
WeekFields weekFields;
if (weekStartDay == null) {
weekFields = WeekFields.of(DayOfWeek.MONDAY, 4);
} else {
weekFields = WeekFields.of(weekStartDay, 4);
}
return new TemporalUnit() {
@Override
public long between(Temporal one, Temporal other) {
throw new UnsupportedOperationException();
}
@Override
public R addTo(R one, long other) {
TemporalField field = weekFields.weekBasedYear();
long newValue = one.get(field) + other;
// 'one.with(field, newValue)' would be neater here, but 'with' does not work for
// ZonedDateTime as WeekFields' field.adjustInto returns a LocalDate,
// so we need to manually 'adjustInto' and use the result as TemporalAdjuster:
Temporal result = field.adjustInto(one, newValue);
if (TemporalAdjuster.class.isAssignableFrom(result.getClass())) {
return (R) one.with((TemporalAdjuster) result);
} else {
return (R) one;
}
}
@Override
public boolean isTimeBased() {
return false;
}
@Override
public boolean isDateBased() {
return true;
}
@Override
public boolean isDurationEstimated() {
return true;
}
@Override
public Duration getDuration() {
return WeekFields.WEEK_BASED_YEARS.getDuration();
}
};
}
/**
* @param count The count to set.
* @deprecated will be removed in a future version to support immutable pattern.
*/
@Deprecated
public final void setCount(final int count) {
this.count = count;
this.until = null;
}
/**
* @param frequency The frequency to set.
* @deprecated will be removed in a future version to support immutable pattern.
*/
@Deprecated
public final void setFrequency(final String frequency) {
this.frequency = Frequency.valueOf(frequency);
validateFrequency();
}
/**
* @param interval The interval to set.
* @deprecated will be removed in a future version to support immutable pattern.
*/
@Deprecated
public final void setInterval(final int interval) {
this.interval = interval;
}
/**
* @param until The until to set.
* @deprecated will be removed in a future version to support immutable pattern.
*/
@Deprecated
public final void setUntil(final T until) {
this.until = new TemporalAdapter<>(until);
this.count = -1;
}
/**
* @param stream
* @throws IOException
* @throws ClassNotFoundException
*/
private void readObject(final java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
log = LoggerFactory.getLogger(Recur.class);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Recur recur = (Recur) o;
return frequency == recur.frequency && skip == recur.skip && Objects.equals(until, recur.until) &&
rscale == recur.rscale && Objects.equals(count, recur.count) && Objects.equals(interval, recur.interval) &&
Objects.equals(secondList, recur.secondList) && Objects.equals(minuteList, recur.minuteList) &&
Objects.equals(hourList, recur.hourList) && Objects.equals(dayList, recur.dayList) &&
Objects.equals(monthDayList, recur.monthDayList) && Objects.equals(yearDayList, recur.yearDayList) &&
Objects.equals(weekNoList, recur.weekNoList) && Objects.equals(monthList, recur.monthList) &&
Objects.equals(setPosList, recur.setPosList) && weekStartDay == recur.weekStartDay;
}
@Override
public int hashCode() {
return Objects.hash(frequency, skip, until, rscale, count, interval, secondList, minuteList, hourList, dayList,
monthDayList, yearDayList, weekNoList, monthList, setPosList, weekStartDay);
}
/**
* Support for building Recur instances.
*/
public static class Builder {
private Frequency frequency;
private Skip skip;
private T until;
private RScale rscale;
private Integer count;
private Integer interval;
private List secondList;
private List minuteList;
private List hourList;
private List dayList;
private List monthDayList;
private List yearDayList;
private List weekNoList;
private List monthList;
private List setPosList;
private WeekDay weekStartDay;
public Builder() {
}
/**
* Initialise builder using an existing recurrence.
* @param recur a non-null recurrence.
*/
public Builder(Recur recur) {
Objects.requireNonNull(recur);
this.frequency = recur.frequency;
this.rscale = recur.rscale;
this.skip = recur.skip;
this.until = recur.until != null ? recur.until.getTemporal() : null;
this.count = recur.count;
this.interval = recur.interval;
this.secondList = recur.secondList;
this.minuteList = recur.minuteList;
this.hourList = recur.hourList;
this.dayList = recur.dayList;
this.monthDayList = recur.monthDayList;
this.yearDayList = recur.yearDayList;
this.weekNoList = recur.weekNoList;
this.monthList = recur.monthList;
this.setPosList = recur.setPosList;
this.weekStartDay = recur.weekStartDay;
}
public Builder frequency(Frequency frequency) {
this.frequency = frequency;
return this;
}
public Builder skip(Skip skip) {
this.skip = skip;
return this;
}
public Builder until(T until) {
this.until = until;
return this;
}
public Builder rscale(RScale rscale) {
this.rscale = rscale;
return this;
}
public Builder count(Integer count) {
this.count = count;
return this;
}
public Builder interval(Integer interval) {
this.interval = interval;
return this;
}
public Builder secondList(Integer...seconds) {
return secondList(Arrays.asList(seconds));
}
public Builder secondList(List secondList) {
this.secondList = secondList;
return this;
}
public Builder minuteList(Integer...minutes) {
return minuteList(Arrays.asList(minutes));
}
public Builder minuteList(List minuteList) {
this.minuteList = minuteList;
return this;
}
public Builder hourList(Integer...hours) {
return hourList(Arrays.asList(hours));
}
public Builder hourList(List hourList) {
this.hourList = hourList;
return this;
}
public Builder dayList(WeekDay...days) {
return dayList(new WeekDayList(days));
}
public Builder dayList(List dayList) {
this.dayList = dayList;
return this;
}
public Builder monthDayList(Integer...monthDays) {
return monthDayList(Arrays.asList(monthDays));
}
public Builder monthDayList(List monthDayList) {
this.monthDayList = monthDayList;
return this;
}
public Builder yearDayList(Integer...yearDays) {
return yearDayList(Arrays.asList(yearDays));
}
public Builder yearDayList(List yearDayList) {
this.yearDayList = yearDayList;
return this;
}
public Builder weekNoList(Integer...weekNos) {
return weekNoList(Arrays.asList(weekNos));
}
public Builder weekNoList(List weekNoList) {
this.weekNoList = weekNoList;
return this;
}
public Builder monthList(Month...months) {
return monthList(Arrays.asList(months));
}
public Builder monthList(List monthList) {
this.monthList = monthList;
return this;
}
public Builder setPosList(Integer...setPos) {
return setPosList(Arrays.asList(setPos));
}
public Builder setPosList(List setPosList) {
this.setPosList = setPosList;
return this;
}
public Builder weekStartDay(WeekDay weekStartDay) {
this.weekStartDay = weekStartDay;
return this;
}
public Recur build() {
var chronology = rscale != null ? Chronology.of(rscale.getChronology())
: Chronology.ofLocale(Locale.getDefault());
Recur recur = new Recur<>();
recur.frequency = frequency;
recur.rscale = rscale;
recur.skip = skip;
if (until != null) {
recur.until = new TemporalAdapter(until);
}
recur.count = count;
recur.interval = interval;
if (secondList != null) {
recur.secondList = new NumberList(secondList, chronology.range(ChronoField.SECOND_OF_MINUTE), false);
}
if (minuteList != null) {
recur.minuteList = new NumberList(minuteList, chronology.range(ChronoField.MINUTE_OF_HOUR), false);
}
if (hourList != null) {
recur.hourList = new NumberList(hourList, chronology.range(ChronoField.HOUR_OF_DAY), false);
}
if (dayList != null) {
recur.dayList.addAll(dayList);
}
if (monthDayList != null) {
recur.monthDayList = new NumberList(monthDayList, chronology.range(ChronoField.DAY_OF_MONTH), true);
}
if (yearDayList != null) {
recur.yearDayList = new NumberList(yearDayList, chronology.range(ChronoField.DAY_OF_YEAR), true);
}
if (weekNoList != null) {
recur.weekNoList = new NumberList(weekNoList, chronology.range(ChronoField.ALIGNED_WEEK_OF_YEAR), true);
}
if (monthList != null) {
recur.monthList = new MonthList(monthList, chronology.range(ChronoField.MONTH_OF_YEAR));
}
if (setPosList != null) {
recur.setPosList = new NumberList(setPosList, chronology.range(ChronoField.DAY_OF_YEAR), true);
}
recur.weekStartDay = weekStartDay;
recur.validateFrequency();
return recur;
}
}
private class DateSpliterator extends Spliterators.AbstractSpliterator {
final T seed;
final Temporal periodStart;
final Temporal periodEnd;
final int maxCount;
final List dates;
T candidateSeed;
int incrementMultiplier = 1;
T lastCandidate = null;
Iterator candidates = null;
final HashSet invalidCandidates = new HashSet<>();
int noCandidateIncrementCount = 0;
public DateSpliterator(T seed, Temporal periodStart, Temporal periodEnd, int maxCount) {
super(maxCount, 0);
this.seed = seed;
this.periodStart = periodStart;
this.periodEnd = periodEnd;
this.maxCount = maxCount;
dates = new ArrayList<>();
candidateSeed = seed;
// optimize the start time for selecting candidates
// (only applicable where a COUNT is not specified)
if (count == null) {
T incremented = increment(seed, incrementMultiplier);
while (TemporalAdapter.isBefore(incremented, periodStart.minus(Math.max(getInterval(), 1), calIncField))) {
candidateSeed = incremented;
incrementMultiplier++;
if (candidateSeed == null) {
break;
}
incremented = increment(seed, incrementMultiplier);
}
}
}
@Override
public boolean tryAdvance(Consumer super T> action) {
boolean advance = maxCount < 0 || dates.size() < maxCount;
if (advance) {
if (getUntil() != null && lastCandidate != null && TemporalAdapter.isAfter(lastCandidate, getUntil())) {
advance = false;
} else if (periodEnd != null && lastCandidate != null && TemporalAdapter.isAfter(lastCandidate, periodEnd)) {
advance = false;
} else if (getCount() >= 1 && (dates.size() + invalidCandidates.size()) >= getCount()) {
advance = false;
}
}
if (advance) {
// generate new candidate list..
while (candidates == null || !candidates.hasNext()) {
// rootSeed = date used for the seed for the RRule at the
// start of the first period.
// candidateSeed = date used for the start of
// the current period.
candidates = getCandidates(seed, candidateSeed).iterator();
if (!candidates.hasNext()) {
noCandidateIncrementCount++;
if ((maxIncrementCount > 0) && (noCandidateIncrementCount > maxIncrementCount)) {
advance = false;
break;
}
} else {
noCandidateIncrementCount = 0;
}
candidateSeed = increment(seed, incrementMultiplier++);
}
}
if (advance) {
// iterate current candidate list..
lastCandidate = candidates.next();
// don't count candidates that occur before the seed date..
if (!TemporalAdapter.isBefore(lastCandidate, seed)) {
// candidates exclusive of periodEnd..
if (TemporalAdapter.isBefore(lastCandidate, periodStart) || TemporalAdapter.isAfter(lastCandidate, periodEnd)) {
invalidCandidates.add(lastCandidate);
} else if (!TemporalAdapter.isBefore(lastCandidate, periodStart) && !TemporalAdapter.isAfter(lastCandidate, periodEnd)
&& (getUntil() == null || !TemporalAdapter.isAfter(lastCandidate, getUntil()))) {
dates.add(lastCandidate);
action.accept(lastCandidate);
}
}
}
return advance;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy