// Copyright (C) 2006 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.ical.iter; import com.google.ical.util.DTBuilder; import com.google.ical.util.Predicate; import com.google.ical.util.TimeUtils; import com.google.ical.values.DateValue; import com.google.ical.values.DateValueImpl; import com.google.ical.values.TimeValue; import java.util.TimeZone; /** * an iterator over dates in an RRULE or EXRULE series. * * @author mikesamuel+svn@gmail.com (Mike Samuel) */ final class RRuleIteratorImpl implements RecurrenceIterator { /** * a function that determines when the recurrence ends. * Takes a date builder and yields shouldContinue:boolean. * The condition is applied <b>after</b> the date is converted to utc. */ private final Predicate<? super DateValue> condition_; /** * a function that applies the various period generators to generate an entire * date. * This may involve generating a set of dates and discarding all but those * that match the BYSETPOS rule. */ private final Generator instanceGenerator_; /** * a function that takes a builder and populates the year field. * Returns false if there aren't more years available. */ private final ThrottledGenerator yearGenerator_; /** * a function that takes a builder and populates the month field. * Returns false if there aren't more months available in the builder's year. */ private final Generator monthGenerator_; /** * a date that has been computed but not yet yielded to the user. */ private DateValue pendingUtc_; /** * used to build successive dates. * At the start of the building process, contains the last date generated. * Different periods are successively inserted into it. */ private DTBuilder builder_; /** true iff the recurrence has been exhausted. */ private boolean done_; /** the start date of the recurrence */ private final DateValue dtStart_; /** * false iff shorcutting advance would break the semantics of the iteration. * This may happen when, for example, the end condition requires that it see * every item. */ private final boolean canShortcutAdvance_; /** * the timezone that result dates should be converted <b>from</b>. * All date fields, parameters, and local variables in this class are in * the tzid_ timezone, unless they carry the Utc suffix. */ private final TimeZone tzid_; /** An iterator that generates dates from an RFC2445 Recurrence Rule */ RRuleIteratorImpl( DateValue dtStart, TimeZone tzid, Predicate<? super DateValue> condition, Generator instanceGenerator, ThrottledGenerator yearGenerator, Generator monthGenerator, Generator dayGenerator, Generator hourGenerator, Generator minuteGenerator, Generator secondGenerator, boolean canShortcutAdvance) { this.condition_ = condition; this.instanceGenerator_ = instanceGenerator; this.yearGenerator_ = yearGenerator; this.monthGenerator_ = monthGenerator; this.dtStart_ = dtStart; this.tzid_ = tzid; this.canShortcutAdvance_ = canShortcutAdvance; int initWorkLimit = 1000; // Initialize the builder and skip over any extraneous start instances DTBuilder builder = new DTBuilder(dtStart); this.builder_ = builder; // Apply the generators from largest field to smallest so we can start by // applying the smallest field iterator when asked to generate a date. try { Generator[] toInitialize; if (InstanceGenerators.skipSubDayGenerators( hourGenerator, minuteGenerator, secondGenerator)) { toInitialize = new Generator[] { yearGenerator, monthGenerator }; builder.hour = ((SingleValueGenerator) hourGenerator).getValue(); builder.minute = ((SingleValueGenerator) minuteGenerator).getValue(); builder.second = ((SingleValueGenerator) secondGenerator).getValue(); } else { toInitialize = new Generator[] { yearGenerator, monthGenerator, dayGenerator, hourGenerator, minuteGenerator, }; } for (int i = 0; i != toInitialize.length;) { if (toInitialize[i].generate(builder)) { ++i; } else { if (--i < 0) { // No years left. this.done_ = true; break; } } if (--initWorkLimit == 0) { this.done_ = true; break; } } } catch (Generator.IteratorShortCircuitingException ex) { this.done_ = true; } while (!this.done_) { this.pendingUtc_ = this.generateInstance(); if (null == this.pendingUtc_) { this.done_ = true; break; } else if (this.pendingUtc_.compareTo( TimeUtils.toUtc(dtStart, tzid)) >= 0) { // We only apply the condition to the ones past dtStart to avoid // counting useless instances if (!this.condition_.apply(this.pendingUtc_)) { this.done_ = true; this.pendingUtc_ = null; } break; } if (--initWorkLimit == 0) { this.done_ = true; break; } } } /** are there more dates in this recurrence? */ public boolean hasNext() { if (null == this.pendingUtc_) { this.fetchNext(); } return null != this.pendingUtc_; } /** fetch and return the next date in this recurrence. */ public DateValue next() { if (null == this.pendingUtc_) { this.fetchNext(); } DateValue next = this.pendingUtc_; this.pendingUtc_ = null; return next; } public void remove() { throw new UnsupportedOperationException(); } /** * Skip all instances of the recurrence before the given date, so that * the next call to {@link #next} will return a date on or after the given * date, assuming the recurrence includes such a date. */ public void advanceTo(DateValue dateUtc) { // Don't throw away a future pending date since the iterators will not // generate it again. if (this.pendingUtc_ != null && dateUtc.compareTo(this.pendingUtc_) <= 0) { return; } DateValue dateLocal = TimeUtils.fromUtc(dateUtc, tzid_); // Short-circuit if we're already past dateUtc. if (dateLocal.compareTo(this.builder_.toDate()) <= 0) { return; } this.pendingUtc_ = null; try { if (this.canShortcutAdvance_) { // skip years before date.year if (this.builder_.year < dateLocal.year()) { do { if (!this.yearGenerator_.generate(this.builder_)) { this.done_ = true; return; } } while (this.builder_.year < dateLocal.year()); while (!this.monthGenerator_.generate(this.builder_)) { if (!this.yearGenerator_.generate(this.builder_)) { this.done_ = true; return; } } } // skip months before date.year/date.month while (this.builder_.year == dateLocal.year() && this.builder_.month < dateLocal.month()) { while (!this.monthGenerator_.generate(this.builder_)) { // if there are more years available fetch one if (!this.yearGenerator_.generate(this.builder_)) { // otherwise the recurrence is exhausted this.done_ = true; return; } } } } // consume any remaining instances while (!this.done_) { DateValue dUtc = this.generateInstance(); if (null == dUtc) { this.done_ = true; } else { if (!this.condition_.apply(dUtc)) { this.done_ = true; } else if (dUtc.compareTo(dateUtc) >= 0) { this.pendingUtc_ = dUtc; break; } } } } catch (Generator.IteratorShortCircuitingException ex) { this.done_ = true; } } /** calculates and stored the next date in this recurrence. */ private void fetchNext() { if (null != this.pendingUtc_ || this.done_) { return; } DateValue dUtc = this.generateInstance(); // check the exit condition if (null != dUtc && this.condition_.apply(dUtc)) { this.pendingUtc_ = dUtc; this.yearGenerator_.workDone(); } else { this.done_ = true; } } private static final DateValue MIN_DATE = new DateValueImpl(Integer.MIN_VALUE, 1, 1); /** * make sure the iterator is monotonically increasing. * The local time is guaranteed to be monotonic, but because of daylight * savings shifts, the time in UTC may not be. */ private DateValue lastUtc_ = MIN_DATE; /** * @return a date value in UTC. */ private DateValue generateInstance() { try { do { if (!this.instanceGenerator_.generate(this.builder_)) { return null; } DateValue dUtc = this.dtStart_ instanceof TimeValue ? TimeUtils.toUtc(this.builder_.toDateTime(), this.tzid_) : this.builder_.toDate(); if (dUtc.compareTo(this.lastUtc_) > 0) { return dUtc; } } while (true); } catch (Generator.IteratorShortCircuitingException ex) { return null; } } }