All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.fortuna.ical4j.model.Recur Maven / Gradle / Ivy

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 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 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