The javadoc is available online.
+ +The source is available from subversion. See the project page for +instructions on how to download the source from subversion.
+ + +Requires ant to build with +junit for testing, and optionally +joda-time installed.
+ +Once you've got ant
+set up and installed,
+run ant default
to build the jars and documentation, and then
+read the documentation in the docs
directory. You can use the jar
+at jars/rfc2445.jar
in your program.
To build without joda-time, run ant rfc2445-no-joda
and it will
+produce a version of the jar without the joda-time compatibility classes.
Using the API is pretty easy. Pass in some ical and you get back +a date iterable, which can be used in a for-each loop thusly:
+ ++// A compatibility layer for joda-time +import com.google.ical.compat.jodatime.LocalDateIteratorFactory; +// A Joda time class that represents a day regardless of timezone +import org.joda.time.LocalDate; + +public class ThirteenFridaysTheThirteenth { + + /** print the first 13 Friday the 13ths in the 3rd millenium AD. */ + public static void main(String[] args) throws java.text.ParseException { + LocalDate start = new LocalDate(2001, 4, 13); + + // Every friday the thirteenth. + String ical = "RRULE:FREQ=MONTHLY" + + ";BYDAY=FR" // every Friday + + ";BYMONTHDAY=13" // that occurs on the 13th of the month + + ";COUNT=13"; // stop after 13 occurences + + // Print out each date in the series. + for (LocalDate date : + LocalDateIteratorFactory.createLocalDateIterable(ical, start, true)) { + System.out.println(date); + } + } + +} ++
See RFC 2445 for the recurrence rule +syntax and what it means, and the examples later in the same document.
+ +If you use java.util.Date
and java.util.Calendar
in
+your application instead of Joda-Time, you can use the
+com.google.ical.compat.javautil
package instead to provide
+Date objects.
If you make source changes, you can run ant runtests
to run
+build and run the tests.
Iterator<Date>
and Iterable<Date>
.
+ *
+ * @see RecurrenceIteratorFactory
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+public class DateIteratorFactory {
+
+ /**
+ * given a block of RRULE, EXRULE, RDATE, and EXDATE content lines, parse
+ * them into a single date iterator.
+ * @param rdata RRULE, EXRULE, RDATE, and EXDATE lines.
+ * @param start the first occurrence of the series.
+ * @param tzid the local timezone -- used to interpret start and any dates in
+ * RDATE and EXDATE lines that don't have TZID params.
+ * @param strict true if any failure to parse should result in a
+ * ParseException. false causes bad content lines to be logged and ignored.
+ */
+ public static DateIterator createDateIterator(
+ String rdata, Date start, TimeZone tzid, boolean strict)
+ throws ParseException {
+ return new RecurrenceIteratorWrapper(
+ RecurrenceIteratorFactory.createRecurrenceIterator(
+ rdata, dateToDateValue(start, true),
+ tzid, strict));
+ }
+
+ /**
+ * given a block of RRULE, EXRULE, RDATE, and EXDATE content lines, parse
+ * them into a single date iterable.
+ * @param rdata RRULE, EXRULE, RDATE, and EXDATE lines.
+ * @param start the first occurrence of the series.
+ * @param tzid the local timezone -- used to interpret start and any dates in
+ * RDATE and EXDATE lines that don't have TZID params.
+ * @param strict true if any failure to parse should result in a
+ * ParseException. false causes bad content lines to be logged and ignored.
+ */
+ public static DateIterable createDateIterable(
+ String rdata, Date start, TimeZone tzid, boolean strict)
+ throws ParseException {
+ return new RecurrenceIterableWrapper(
+ RecurrenceIteratorFactory.createRecurrenceIterable(
+ rdata, dateToDateValue(start, true),
+ tzid, strict));
+ }
+
+ /**
+ * creates a date iterator given a recurrence iterator from
+ * {@link com.google.ical.iter.RecurrenceIteratorFactory}.
+ */
+ public static DateIterator createDateIterator(RecurrenceIterator rit) {
+ return new RecurrenceIteratorWrapper(rit);
+ }
+
+ private static final class RecurrenceIterableWrapper
+ implements DateIterable {
+ private final RecurrenceIterable it;
+
+ public RecurrenceIterableWrapper(RecurrenceIterable it) { this.it = it; }
+
+ public DateIterator iterator() {
+ return new RecurrenceIteratorWrapper(it.iterator());
+ }
+ }
+
+ private static final class RecurrenceIteratorWrapper
+ implements DateIterator {
+ private final RecurrenceIterator it;
+ RecurrenceIteratorWrapper(RecurrenceIterator it) { this.it = it; }
+ public boolean hasNext() { return it.hasNext(); }
+ public Date next() { return dateValueToDate(it.next()); }
+ public void remove() { throw new UnsupportedOperationException(); }
+ public void advanceTo(Date d) {
+ // we need to treat midnight as a date value so that passing in
+ // dateValueToDate(A compatability layer that produces java Date
instances.
Iterator<DateTime>
and
+ * Iterable<DateTime>
.
+ *
+ * @see RecurrenceIteratorFactory
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+public class DateTimeIteratorFactory {
+
+ /**
+ * given a block of RRULE, EXRULE, RDATE, and EXDATE content lines, parse
+ * them into a single date time iterator.
+ * @param rdata RRULE, EXRULE, RDATE, and EXDATE lines.
+ * @param start the first occurrence of the series.
+ * @param tzid the local timezone -- used to interpret start and any dates in
+ * RDATE and EXDATE lines that don't have TZID params.
+ * @param strict true if any failure to parse should result in a
+ * ParseException. false causes bad content lines to be logged and ignored.
+ */
+ public static DateTimeIterator createDateTimeIterator(
+ String rdata, ReadableDateTime start, DateTimeZone tzid, boolean strict)
+ throws ParseException {
+ return new RecurrenceIteratorWrapper(
+ RecurrenceIteratorFactory.createRecurrenceIterator(
+ rdata, dateTimeToDateValue(start.toDateTime().withZone(tzid)),
+ TimeZoneConverter.toTimeZone(tzid), strict));
+ }
+
+ /**
+ * given a block of RRULE, EXRULE, RDATE, and EXDATE content lines, parse
+ * them into a single date time iterable.
+ * @param rdata RRULE, EXRULE, RDATE, and EXDATE lines.
+ * @param start the first occurrence of the series.
+ * @param tzid the local timezone -- used to interpret start and any dates in
+ * RDATE and EXDATE lines that don't have TZID params.
+ * @param strict true if any failure to parse should result in a
+ * ParseException. false causes bad content lines to be logged and ignored.
+ */
+ public static DateTimeIterable createDateTimeIterable(
+ String rdata, ReadableDateTime start, DateTimeZone tzid, boolean strict)
+ throws ParseException {
+ return new RecurrenceIterableWrapper(
+ RecurrenceIteratorFactory.createRecurrenceIterable(
+ rdata, dateTimeToDateValue(start.toDateTime().withZone(tzid)),
+ TimeZoneConverter.toTimeZone(tzid), strict));
+ }
+
+ /**
+ * creates a date-time iterator given a recurrence iterator from
+ * {@link com.google.ical.iter.RecurrenceIteratorFactory}.
+ */
+ public static DateTimeIterator createDateTimeIterator(
+ RecurrenceIterator rit) {
+ return new RecurrenceIteratorWrapper(rit);
+ }
+
+ private static final class RecurrenceIterableWrapper
+ implements DateTimeIterable {
+ private final RecurrenceIterable it;
+
+ public RecurrenceIterableWrapper(RecurrenceIterable it) { this.it = it; }
+
+ public DateTimeIterator iterator() {
+ return new RecurrenceIteratorWrapper(it.iterator());
+ }
+ }
+
+ private static final class RecurrenceIteratorWrapper
+ implements DateTimeIterator {
+ private final RecurrenceIterator it;
+ RecurrenceIteratorWrapper(RecurrenceIterator it) { this.it = it; }
+ public boolean hasNext() { return it.hasNext(); }
+ public DateTime next() { return dateValueToDateTime(it.next()); }
+ public void remove() { throw new UnsupportedOperationException(); }
+ public void advanceTo(ReadableDateTime d) {
+ DateTime dUtc = d.toDateTime().withZone(DateTimeZone.UTC);
+ it.advanceTo(dateTimeToDateValue(dUtc));
+ }
+ }
+
+ static DateTime dateValueToDateTime(DateValue dvUtc) {
+ if (dvUtc instanceof TimeValue) {
+ TimeValue tvUtc = (TimeValue) dvUtc;
+ return new DateTime(
+ dvUtc.year(),
+ dvUtc.month(), // java.util's dates are zero-indexed
+ dvUtc.day(),
+ tvUtc.hour(),
+ tvUtc.minute(),
+ tvUtc.second(),
+ 0,
+ DateTimeZone.UTC);
+ } else {
+ return new DateTime(
+ dvUtc.year(),
+ dvUtc.month(), // java.util's dates are zero-indexed
+ dvUtc.day(),
+ 0,
+ 0,
+ 0,
+ 0,
+ DateTimeZone.UTC);
+ }
+ }
+
+ static DateValue dateTimeToDateValue(ReadableDateTime dt) {
+ return new DateTimeValueImpl(
+ dt.getYear(), dt.getMonthOfYear(), dt.getDayOfMonth(),
+ dt.getHourOfDay(), dt.getMinuteOfHour(), dt.getSecondOfMinute());
+ }
+
+ private DateTimeIteratorFactory() {
+ // uninstantiable
+ }
+}
diff --git a/src/main/java/com/google/ical/compat/jodatime/LocalDateIterable.java b/src/main/java/com/google/ical/compat/jodatime/LocalDateIterable.java
new file mode 100644
index 0000000..4476ee3
--- /dev/null
+++ b/src/main/java/com/google/ical/compat/jodatime/LocalDateIterable.java
@@ -0,0 +1,28 @@
+// 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.compat.jodatime;
+
+import org.joda.time.LocalDate;
+
+/**
+ * an iterable over dates in order.
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+public interface LocalDateIterable extends IterableIterator<LocalDate>
and
+ * Iterable<LocalDate>
.
+ *
+ * @see RecurrenceIteratorFactory
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+public class LocalDateIteratorFactory {
+
+ /**
+ * given a block of RRULE, EXRULE, RDATE, and EXDATE content lines, parse
+ * them into a single local date iterator.
+ * @param rdata RRULE, EXRULE, RDATE, and EXDATE lines.
+ * @param start the first occurrence of the series.
+ * @param tzid the local timezone -- used to interpret any dates in RDATE and
+ * EXDATE lines that don't have TZID params.
+ * @param strict true if any failure to parse should result in a
+ * ParseException. false causes bad content lines to be logged and ignored.
+ */
+ public static LocalDateIterator createLocalDateIterator(
+ String rdata, LocalDate start, DateTimeZone tzid, boolean strict)
+ throws ParseException {
+ return new RecurrenceIteratorWrapper(
+ RecurrenceIteratorFactory.createRecurrenceIterator(
+ rdata, localDateToDateValue(start),
+ TimeZoneConverter.toTimeZone(tzid), strict));
+ }
+
+ /**
+ * given a block of RRULE, EXRULE, RDATE, and EXDATE content lines, parse
+ * them into a single local date iterator.
+ * @param rdata RRULE, EXRULE, RDATE, and EXDATE lines.
+ * @param start the first occurrence of the series.
+ * @param strict true if any failure to parse should result in a
+ * ParseException. false causes bad content lines to be logged and ignored.
+ */
+ public static LocalDateIterator createLocalDateIterator(
+ String rdata, LocalDate start, boolean strict)
+ throws ParseException {
+ return createLocalDateIterator(rdata, start, DateTimeZone.UTC, strict);
+ }
+
+ /**
+ * given a block of RRULE, EXRULE, RDATE, and EXDATE content lines, parse
+ * them into a single local date iterable.
+ * @param rdata RRULE, EXRULE, RDATE, and EXDATE lines.
+ * @param start the first occurrence of the series.
+ * @param tzid the local timezone -- used to interpret any dates in RDATE and
+ * EXDATE lines that don't have TZID params.
+ * @param strict true if any failure to parse should result in a
+ * ParseException. false causes bad content lines to be logged and ignored.
+ */
+ public static LocalDateIterable createLocalDateIterable(
+ String rdata, LocalDate start, DateTimeZone tzid, boolean strict)
+ throws ParseException {
+ return new RecurrenceIterableWrapper(
+ RecurrenceIteratorFactory.createRecurrenceIterable(
+ rdata, localDateToDateValue(start),
+ TimeZoneConverter.toTimeZone(tzid), strict));
+ }
+
+ /**
+ * given a block of RRULE, EXRULE, RDATE, and EXDATE content lines, parse
+ * them into a single local date iterable.
+ * @param rdata RRULE, EXRULE, RDATE, and EXDATE lines.
+ * @param start the first occurrence of the series.
+ * @param strict true if any failure to parse should result in a
+ * ParseException. false causes bad content lines to be logged and ignored.
+ */
+ public static LocalDateIterable createLocalDateIterable(
+ String rdata, LocalDate start, boolean strict)
+ throws ParseException {
+ return createLocalDateIterable(rdata, start, DateTimeZone.UTC, strict);
+ }
+
+ /**
+ * creates a local date iterator given a recurrence iterator from
+ * {@link com.google.ical.iter.RecurrenceIteratorFactory}.
+ */
+ public static LocalDateIterator createLocalDateIterator(
+ RecurrenceIterator rit) {
+ return new RecurrenceIteratorWrapper(rit);
+ }
+
+ private static final class RecurrenceIterableWrapper
+ implements LocalDateIterable {
+ private final RecurrenceIterable it;
+
+ public RecurrenceIterableWrapper(RecurrenceIterable it) { this.it = it; }
+
+ public LocalDateIterator iterator() {
+ return new RecurrenceIteratorWrapper(it.iterator());
+ }
+ }
+
+ private static final class RecurrenceIteratorWrapper
+ implements LocalDateIterator {
+ private final RecurrenceIterator it;
+ RecurrenceIteratorWrapper(RecurrenceIterator it) { this.it = it; }
+ public boolean hasNext() { return it.hasNext(); }
+ public LocalDate next() { return dateValueToLocalDate(it.next()); }
+ public void remove() { throw new UnsupportedOperationException(); }
+ public void advanceTo(LocalDate d) {
+ // we need to treat midnight as a date value so that passing in
+ // dateValueToDate(java.util.TimeZone
that supposedly is equivalent to
+ * the DateTimeZone
.
+ * Joda time's implementation simply uses the ID to look up the corresponding
+ * java.util.TimeZone
s which should not be used since they're
+ * frequently out-of-date re Brazilian timezones.
+ *
+ * See Sun bug 4328058.
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+final class TimeZoneConverter {
+
+ static final int MILLISECONDS_PER_SECOND = 1000;
+ static final int MILLISECONDS_PER_MINUTE = 60 * MILLISECONDS_PER_SECOND;
+ static final int MILLISECONDS_PER_HOUR = 60 * MILLISECONDS_PER_MINUTE;
+
+ /**
+ * return a java.util.Timezone
object that delegates to
+ * the given Joda DateTimeZone
.
+ */
+ public static TimeZone toTimeZone(final DateTimeZone dtz) {
+
+ TimeZone tz = new TimeZone() {
+ @Override
+ public void setRawOffset(int n) {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public boolean useDaylightTime() {
+ long firstTransition = 0L;
+ return firstTransition != dtz.nextTransition(firstTransition);
+ }
+ @Override
+ public boolean inDaylightTime(Date d) {
+ long t = d.getTime();
+ return dtz.getStandardOffset(t) != dtz.getOffset(t);
+ }
+ @Override
+ public int getRawOffset() {
+ return dtz.getStandardOffset(0);
+ }
+ @Override
+ public int getOffset(long instant) {
+ // This method is not abstract, but it normally calls through to the
+ // method below.
+ // It's optimized here since there's a direct equivalent in
+ // DateTimeZone.
+ // DateTimeZone and java.util.TimeZone use the same
+ // epoch so there's no translation of instant required.
+ return dtz.getOffset(instant);
+ }
+ @Override
+ public int getOffset(
+ int era, int year, int month, int day, int dayOfWeek,
+ int milliseconds) {
+ int millis = milliseconds; // milliseconds is day in standard time
+ int hour = millis / MILLISECONDS_PER_HOUR;
+ millis %= MILLISECONDS_PER_HOUR;
+ int minute = millis / MILLISECONDS_PER_MINUTE;
+ millis %= MILLISECONDS_PER_MINUTE;
+ int second = millis / MILLISECONDS_PER_SECOND;
+ millis %= MILLISECONDS_PER_SECOND;
+ if (era == GregorianCalendar.BC) { year = -(year - 1); }
+
+ // get the time in UTC in case a timezone has changed it's standard
+ // offset, e.g. rid of a half hour from UTC.
+ DateTime dt = null;
+ try {
+ dt = new DateTime(year, month + 1, day, hour, minute,
+ second, millis, dtz);
+ } catch (IllegalArgumentException ex) {
+ // Java does not complain if you try to convert a Date that does not
+ // exist due to the offset shifting forward, but Joda time does.
+ // Since we're trying to preserve the semantics of TimeZone, shift
+ // forward over the gap so that we're on a time that exists.
+ // This assumes that the DST correction is one hour long or less.
+ if (hour < 23) {
+ dt = new DateTime(year, month + 1, day, hour + 1, minute,
+ second, millis, dtz);
+ } else { // Some timezones shift at midnight.
+ Calendar c = new GregorianCalendar();
+ c.clear();
+ c.setTimeZone(TimeZone.getTimeZone("UTC"));
+ c.set(year, month, day, hour, minute, second);
+ c.add(Calendar.HOUR_OF_DAY, 1);
+ int year2 = c.get(Calendar.YEAR),
+ month2 = c.get(Calendar.MONTH),
+ day2 = c.get(Calendar.DAY_OF_MONTH),
+ hour2 = c.get(Calendar.HOUR_OF_DAY);
+ dt = new DateTime(year2, month2 + 1, day2, hour2, minute,
+ second, millis, dtz);
+ }
+ }
+ // since millis is in standard time, we construct the equivalent
+ // GMT+xyz timezone and use that to convert.
+ int offset = dtz.getStandardOffset(dt.getMillis());
+ DateTime stdDt = new DateTime(
+ year, month + 1, day, hour, minute,
+ second, millis, DateTimeZone.forOffsetMillis(offset));
+ return getOffset(stdDt.getMillis());
+ }
+
+ @Override
+ public String toString() {
+ return dtz.toString();
+ }
+
+ private static final long serialVersionUID = 58752546800455L;
+ };
+ // Now fix the tzids. DateTimeZone has a bad habit of returning
+ // "+06:00" when it should be "GMT+06:00"
+ String newTzid = cleanUpTzid(dtz.getID());
+ tz.setID(newTzid);
+ return tz;
+ }
+
+ /**
+ * If tzid is of the form [+-]hh:mm, we rewrite it to GMT[+-]hh:mm
+ * Otherwise return it unchanged.
+ */
+ static String cleanUpTzid(String tzid) {
+ if ("".equals(tzid)) { return "GMT"; }
+ switch (tzid.charAt(0)) {
+ case '+': case '-':
+ return "GMT" + tzid;
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9':
+ return "GMT+" + tzid;
+ default:
+ return tzid;
+ }
+ }
+
+ private TimeZoneConverter() {
+ // uninstantiable
+ }
+
+}
diff --git a/src/main/java/com/google/ical/compat/jodatime/package.html b/src/main/java/com/google/ical/compat/jodatime/package.html
new file mode 100644
index 0000000..99d97a9
--- /dev/null
+++ b/src/main/java/com/google/ical/compat/jodatime/package.html
@@ -0,0 +1,20 @@
+
Joda time compatability layer.
+ +RFC 2445 allows mixing of dates, date-times (and periods) within the same
+recurrence description. Joda Time makes a clear distinction between dates and
+times, so we provide two iterator factories, one for LocalDate
s and another for DateTime
s.
If you need to blur the distinction between
+LocalDateIterator
and
+DateTimeIterator
you can
+downcast them to Iterator<? extends ReadablePartial>
.
When we're pulling dates off the priority order, we need them to come off + * in a consistent order, so we need a total ordering on date values. + *
This means that a DateValue with no time must not be equal to a + * DateTimeValue at midnight. Since it obviously doesn't make sense for a + * DateValue to be after a DateTimeValue the same day at 23:59:59, we put the + * DateValue before 0 hours of the same day. + *
If we didn't have a total ordering, then it would be harder to correctly + * handle the case + *
+ * RDATE:20060607 + * EXDATE:20060607 + * EXDATE:20060607T000000Z + *+ * because we'd have two exdates that are equal according to the comparison, but + * only the first should match. + *
In the following example + *
+ * RDATE:20060607 + * RDATE:20060607T000000Z + * EXDATE:20060607 + *+ * the problem is worse because we may pull a candidate RDATE off the + * priority queue and then not know whether to consume the EXDATE or not. + *
Absent a total ordering, the following case could only be solved with + * lookahead and ugly logic. + *
+ * RDATE:20060607 + * RDATE:20060607T000000Z + * EXDATE:20060607 + * EXDATE:20060607T000000Z + *+ *
The conversion to GMT is also an implementation detail, so it's not clear + * which timezone we should consider midnight in, and a total ordering allows + * us to avoid timezone conversions during iteration.
+ * + * @author mikesamuel+svn@gmail.com (Mike Samuel) + */ +final class DateValueComparison { + + /** + * reduces a date to a value that can be easily compared to others, consistent + * with {@link com.google.ical.values.DateValueImpl#compareTo}. + */ + static long comparable(DateValue dv) { + long comp = (((((long) dv.year()) << 4) + dv.month()) << 5) + dv.day(); + if (dv instanceof TimeValue) { + TimeValue tv = (TimeValue) dv; + // We add 1 to comparable for timed values to make sure that timed + // events are distinct from all-day events, in keeping with + // DateValue.compareTo. + + // It would be odd if an all day exclusion matched a midnight event on + // the same day, but not one at another time of day. + return (((((comp << 5) + tv.hour()) << 6) + tv.minute()) << 6) + + tv.second() + 1; + } else { + return comp << 17; + } + } + + private DateValueComparison() { + // uninstantiable + } + +} diff --git a/src/main/java/com/google/ical/iter/Filters.java b/src/main/java/com/google/ical/iter/Filters.java new file mode 100644 index 0000000..53dbe2e --- /dev/null +++ b/src/main/java/com/google/ical/iter/Filters.java @@ -0,0 +1,159 @@ +// 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.values.DateValue; +import com.google.ical.values.Weekday; +import com.google.ical.values.WeekdayNum; +import com.google.ical.util.DTBuilder; +import com.google.ical.util.Predicate; +import com.google.ical.util.TimeUtils; + + +/** + * predicates used to filter out dates produced by a generator that do not + * pass some secondary criterion. For example, the recurrence rule + * FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13 should generate every friday the + * 13th. It is implemented as a generator that generates the 13th of every + * month -- a byMonthDay generator, and then the results of that are filtered + * by a byDayFilter that tests whether the date falls on Friday. + * + *A filter returns true to indicate the item is included in the + * recurrence.
+ * + * @author mikesamuel+svn@gmail.com (Mike Samuel) + */ +class Filters { + + /** + * constructs a day filter based on a BYDAY rule. + * @param days non null + * @param weeksInYear are the week numbers meant to be weeks in the + * current year, or weeks in the current month. + */ + static PredicateEach field generator takes as input the larger fields, and modifies its + * field, leaving the other fields unchanged. + * A year generator will update bldr.year, leaving the smaller fields unchanged, + * a month generator will update bldr.month, taking its cue from bldr.year, + * also leaving the smaller fields unchanged.
+ * + * @author mikesamuel+svn@gmail.com (Mike Samuel) + */ +abstract class Generator { + + /** + * @param bldr both input and output. non null. modified in place. + * @return true iff there are more instances of the generator's field to + * generate. If a generator is exhausted, generating a new value of a + * larger field may allow it to continue, so a month generator that runs + * out of months at 12, may start over at 1 if called with a bldr with + * a different year. + * @throws IteratorShortCircuitingException when an iterator reaches + * a threshold past which it cannot generate any more dates. This indicates + * that the entire iteration process should end. + */ + abstract boolean generate(DTBuilder bldr) + throws IteratorShortCircuitingException; + + /** + * thrown when an iteration process should be ended completely due to an + * artificial system limit. This allows us to make a distinction between + * normal exhaustion of iteration, and an artificial limit that may fall in + * a set, and so affect subsequent evaluation of BYSETPOS rules. + * + *Since this class is meant to be thrown as a flow control construct to + * indicate an artificial limit has been reached, not really an exceptional + * condition, and since its clients have no need of the stacktrace, we use a + * singleton to avoid forcing the JVM to unoptimize and decompile the + * RecurrenceIterator's inner loop.
+ */ + static class IteratorShortCircuitingException extends Exception { + private IteratorShortCircuitingException() { + super(); + setStackTrace(new StackTraceElement[0]); + } + + private static final IteratorShortCircuitingException INSTANCE = + new IteratorShortCircuitingException(); + + static IteratorShortCircuitingException instance() { return INSTANCE; } + } + + static { + // suffer the stack trace generation on class load of Generator, which will + // happen before any of the recuriter stuff could possibly have been JIT + // compiled. + IteratorShortCircuitingException.instance(); + } + +} diff --git a/src/main/java/com/google/ical/iter/Generators.java b/src/main/java/com/google/ical/iter/Generators.java new file mode 100644 index 0000000..e3c8976 --- /dev/null +++ b/src/main/java/com/google/ical/iter/Generators.java @@ -0,0 +1,551 @@ +// 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.TimeUtils; +import com.google.ical.values.DateValue; +import com.google.ical.values.DateValueImpl; +import com.google.ical.values.Weekday; +import com.google.ical.values.WeekdayNum; +import java.util.Arrays; + +/** + * factory for field generators. + * + * @author mikesamuel+svn@gmail.com (Mike Samuel) + */ +final class Generators { + + /** + * the maximum number of years generated between instances. + * See {@link ThrottledGenerator} for a description of the problem this + * solves. + * Note: this counts the maximum number of years generated, so for + * FREQ=YEARLY;INTERVAL=4 the generator would try 100 individual years over + * a span of 400 years before giving up and concluding that the rule generates + * no usable dates. + */ + private static final int MAX_YEARS_BETWEEN_INSTANCES = 100; + + /** + * constructs a generator that generates years successively counting from the + * first year passed in. + * @param interval number of years to advance each step. + * @param dtStart non null + * @return the year in dtStart the first time called and interval + last + * return value on subsequent calls. + */ + static ThrottledGenerator serialYearGenerator( + final int interval, final DateValue dtStart) { + return new ThrottledGenerator() { + /** the last year seen */ + int year = dtStart.year() - interval; + int throttle = MAX_YEARS_BETWEEN_INSTANCES; + + @Override + boolean generate(DTBuilder builder) + throws IteratorShortCircuitingException { + // make sure things halt even if the rrule is bad. + // Rules like + // FREQ=YEARLY;BYMONTHDAY=30;BYMONTH=2 + // should halt + + if (--throttle < 0) { + throw IteratorShortCircuitingException.instance(); + } + builder.year = year += interval; + return true; + } + + @Override + void workDone() { this.throttle = MAX_YEARS_BETWEEN_INSTANCES; } + + @Override + public String toString() { return "serialYearGenerator:" + interval; } + }; + } + + /** + * constructs a generator that generates months in the given builder's year + * successively counting from the first month passed in. + * @param interval number of months to advance each step. + * @param dtStart non null. + * @return the year in dtStart the first time called and interval + last + * return value on subsequent calls. + */ + static Generator serialMonthGenerator( + final int interval, final DateValue dtStart) { + return new Generator() { + int year = dtStart.year(); + int month = dtStart.month() - interval; + { + while (month < 1) { + month += 12; + --year; + } + } + @Override + boolean generate(DTBuilder builder) { + int nmonth; + if (year != builder.year) { + int monthsBetween = (builder.year - year) * 12 - (month - 1); + nmonth = ((interval - (monthsBetween % interval)) % interval) + 1; + if (nmonth > 12) { + // don't update year so that the difference calculation above is + // correct when this function is reentered with a different year + return false; + } + year = builder.year; + } else { + nmonth = month + interval; + if (nmonth > 12) { + return false; + } + } + month = builder.month = nmonth; + return true; + } + + @Override + public String toString() { return "serialMonthGenerator:" + interval; } + }; + } + + /** + * constructs a generator that generates every day in the current month that + * is an integer multiple of interval days from dtStart. + */ + static Generator serialDayGenerator( + final int interval, final DateValue dtStart) { + return new Generator() { + int year, month, date; + /** ndays in the last month encountered */ + int nDays; + + { + // step back one interval + DTBuilder dtStartMinus1B = new DTBuilder(dtStart); + dtStartMinus1B.day -= interval; + DateValue dtStartMinus1 = dtStartMinus1B.toDate(); + year = dtStartMinus1.year(); + month = dtStartMinus1.month(); + date = dtStartMinus1.day(); + nDays = TimeUtils.monthLength(year, month); + } + + @Override + boolean generate(DTBuilder builder) { + int ndate; + if (year == builder.year && month == builder.month) { + ndate = date + interval; + if (ndate > nDays) { + return false; + } + } else { + nDays = TimeUtils.monthLength(builder.year, builder.month); + if (interval != 1) { + // Calculate the number of days between the first of the new + // month andthe old date and extend it to make it an integer + // multiple of interval + int daysBetween = TimeUtils.daysBetween( + new DateValueImpl(builder.year, builder.month, 1), + new DateValueImpl(year, month, date)); + ndate = ((interval - (daysBetween % interval)) % interval) + 1; + if (ndate > nDays) { + // need to early out without updating year or month so that the + // next time we enter, with a different month, the daysBetween + // call above compares against the proper last date + return false; + } + } else { + ndate = 1; + } + year = builder.year; + month = builder.month; + } + date = builder.day = ndate; + return true; + } + + @Override + public String toString() { return "serialDayGenerator:" + interval; } + }; + } + + /** + * constructs a generator that yields the specified years in increasing order. + */ + static Generator byYearGenerator(int[] years, final DateValue dtStart) { + final int[] uyears = Util.uniquify(years); + + // index into years + return new Generator() { + int i; + { + while (i < uyears.length && dtStart.year() > uyears[i]) { ++i; } + } + + @Override + boolean generate(DTBuilder builder) { + if (i >= uyears.length) { return false; } + builder.year = uyears[i++]; + return true; + } + + @Override + public String toString() { return "byYearGenerator"; } + }; + } + + /** + * constructs a generator that yields the specified months in increasing order + * for each year. + * @param months values in [1-12] + * @param dtStart non null + */ + static Generator byMonthGenerator(int[] months, final DateValue dtStart) { + final int[] umonths = Util.uniquify(months); + + return new Generator() { + int i; + int year = dtStart.year(); + + @Override + boolean generate(DTBuilder builder) { + if (year != builder.year) { + i = 0; + year = builder.year; + } + if (i >= umonths.length) { return false; } + builder.month = umonths[i++]; + return true; + } + + @Override + public String toString() { return "byMonthGenerator"; } + }; + } + + /** + * constructs a function that yields the specified dates + * (possibly relative to end of month) in increasing order + * for each month seen. + * @param dates elements in [-53,53] != 0 + * @param dtStart non null + */ + static Generator byMonthDayGenerator(int[] dates, final DateValue dtStart) { + final int[] udates = Util.uniquify(dates); + + return new Generator() { + int year = dtStart.year(); + int month = dtStart.month(); + /** list of generated dates for the current month */ + int[] posDates; + /** index of next date to return */ + int i = 0; + + { + convertDatesToAbsolute(); + } + + private void convertDatesToAbsolute() { + IntSet posDates = new IntSet(); + int nDays = TimeUtils.monthLength(year, month); + for (int j = 0; j < udates.length; ++j) { + int date = udates[j]; + if (date < 0) { + date += nDays + 1; + } + if (date >= 1 && date <= nDays) { + posDates.add(date); + } + } + this.posDates = posDates.toIntArray(); + } + + @Override + boolean generate(DTBuilder builder) { + if (year != builder.year || month != builder.month) { + year = builder.year; + month = builder.month; + + convertDatesToAbsolute(); + + i = 0; + } + if (i >= posDates.length) { return false; } + builder.day = posDates[i++]; + return true; + } + + @Override + public String toString() { return "byMonthDayGenerator"; } + }; + } + + /** + * constructs a day generator based on a BYDAY rule. + * + * @param days day of week, number pairs, + * e.g. SU,3MO means every sunday and the 3rd monday. + * @param weeksInYear are the week numbers meant to be weeks in the + * current year, or weeks in the current month. + * @param dtStart non null + */ + static Generator byDayGenerator( + WeekdayNum[] days, final boolean weeksInYear, final DateValue dtStart) { + final WeekdayNum[] udays = days.clone(); + + return new Generator() { + int year = dtStart.year(); + int month = dtStart.month(); + /** list of generated dates for the current month */ + int[] dates; + /** index of next date to return */ + int i = 0; + + { generateDates(); } + + void generateDates() { + int nDays; + Weekday dow0; + int nDaysInMonth = TimeUtils.monthLength(year, month); + // index of the first day of the month in the month or year + int d0; + + if (weeksInYear) { + nDays = TimeUtils.yearLength(year); + dow0 = Weekday.firstDayOfWeekInMonth(year, 1); + d0 = TimeUtils.dayOfYear(year, month, 1); + } else { + nDays = nDaysInMonth; + dow0 = Weekday.firstDayOfWeekInMonth(year, month); + d0 = 0; + } + + // an index not greater than the first week of the month in the month + // or year + int w0 = d0 / 7; + + // iterate through days and resolve each [week, day of week] pair to a + // day of the month + IntSet udates = new IntSet(); + for (int j = 0; j < udays.length; ++j) { + WeekdayNum day = udays[j]; + if (0 != day.num) { + int date = Util.dayNumToDate( + dow0, nDays, day.num, day.wday, d0, nDaysInMonth); + if (0 != date) { udates.add(date); } + } else { + int wn = w0 + 6; + for (int w = w0; w <= wn; ++w) { + int date = Util.dayNumToDate( + dow0, nDays, w, day.wday, d0, nDaysInMonth); + if (0 != date) { udates.add(date); } + } + } + } + dates = udates.toIntArray(); + } + + @Override + boolean generate(DTBuilder builder) { + if (year != builder.year || month != builder.month) { + year = builder.year; + month = builder.month; + + generateDates(); + // start at the beginning of the month + i = 0; + } + if (i >= dates.length) { return false; } + builder.day = dates[i++]; + return true; + } + + @Override + public String toString() { + return "byDayGenerator:" + Arrays.toString(udays); + } + }; + } + + /** + * constructs a generator that yields each day in the current month that falls + * in one of the given weeks of the year. + * @param weekNos (elements in [-53,53] != 0) week numbers + * @param wkst (in RRULE_WDAY_*) day of the week that the week starts on. + * @param dtStart non null + */ + static Generator byWeekNoGenerator( + int[] weekNos, final Weekday wkst, final DateValue dtStart) { + final int[] uWeekNos = Util.uniquify(weekNos); + + return new Generator() { + int year = dtStart.year(); + int month = dtStart.month(); + /** number of weeks in the last year seen */ + int weeksInYear; + /** dates generated anew for each month seen */ + int[] dates; + /** index into dates */ + int i = 0; + + /** + * day of the year of the start of week 1 of the current year. + * Since week 1 may start on the previous year, this may be negative. + */ + int doyOfStartOfWeek1; + + { + checkYear(); + checkMonth(); + } + + void checkYear() { + // if the first day of jan is wkst, then there are 7. + // if the first day of jan is wkst + 1, then there are 6 + // if the first day of jan is wkst + 6, then there is 1 + Weekday dowJan1 = Weekday.firstDayOfWeekInMonth(year, 1); + int nDaysInFirstWeek = + 7 - ((7 + dowJan1.javaDayNum - wkst.javaDayNum) % 7); + // number of days not in any week + int nOrphanedDays = 0; + // according to RFC 2445 + // Week number one of the calendar year is the first week which + // contains at least four (4) days in that calendar year. + if (nDaysInFirstWeek < 4) { + nOrphanedDays = nDaysInFirstWeek; + nDaysInFirstWeek = 7; + } + + // calculate the day of year (possibly negative) of the start of the + // first week in the year. This day must be of wkst. + doyOfStartOfWeek1 = nDaysInFirstWeek - 7 + nOrphanedDays; + + weeksInYear = (TimeUtils.yearLength(year) - nOrphanedDays + 6) / 7; + } + + void checkMonth() { + // the day of the year of the 1st day in the month + int doyOfMonth1 = TimeUtils.dayOfYear(year, month, 1); + // the week of the year of the 1st day of the month. approximate. + int weekOfMonth = ((doyOfMonth1 - doyOfStartOfWeek1) / 7) + 1; + // number of days in the month + int nDays = TimeUtils.monthLength(year, month); + + // generate the dates in the month + IntSet udates = new IntSet(); + for (int j = 0; j < uWeekNos.length; j++) { + int weekNo = uWeekNos[j]; + if (weekNo < 0) { + weekNo += weeksInYear + 1; + } + if (weekNo >= weekOfMonth - 1 && weekNo <= weekOfMonth + 6) { + for (int d = 0; d < 7; ++d) { + int date = + ((weekNo - 1) * 7 + d + doyOfStartOfWeek1 - doyOfMonth1) + 1; + if (date >= 1 && date <= nDays) { + udates.add(date); + } + } + } + } + dates = udates.toIntArray(); + } + + @Override + boolean generate(DTBuilder builder) { + + // this is a bit odd, since we're generating days within the given + // weeks of the year within the month/year from builder + if (year != builder.year || month != builder.month) { + if (year != builder.year) { + year = builder.year; + checkYear(); + } + month = builder.month; + checkMonth(); + + i = 0; + } + + if (i >= dates.length) { return false; } + builder.day = dates[i++]; + return true; + } + + @Override + public String toString() { return "byWeekNoGenerator"; } + }; + } + + /** + * constructs a day generator that generates dates in the current month that + * fall on one of the given days of the year. + * @param yearDays elements in [-366,366] != 0 + */ + static Generator byYearDayGenerator(int[] yearDays, final DateValue dtStart) { + final int[] uYearDays = Util.uniquify(yearDays); + + return new Generator() { + int year = dtStart.year(); + int month = dtStart.month(); + int[] dates; + int i = 0; + + { checkMonth(); } + + void checkMonth() { + // now, calculate the first week of the month + int doyOfMonth1 = TimeUtils.dayOfYear(year, month, 1); + int nDays = TimeUtils.monthLength(year, month); + int nYearDays = TimeUtils.yearLength(year); + IntSet udates = new IntSet(); + for (int j = 0; j < uYearDays.length; j++) { + int yearDay = uYearDays[j]; + if (yearDay < 0) { yearDay += nYearDays + 1; } + int date = yearDay - doyOfMonth1; + if (date >= 1 && date <= nDays) { udates.add(date); } + } + dates = udates.toIntArray(); + } + + @Override + boolean generate(DTBuilder builder) { + if (year != builder.year || month != builder.month) { + year = builder.year; + month = builder.month; + + checkMonth(); + + i = 0; + } + if (i >= dates.length) { return false; } + builder.day = dates[i++]; + return true; + } + + @Override + public String toString() { return "byYearDayGenerator"; } + }; + } + + private Generators() { + // uninstantiable + } + +} diff --git a/src/main/java/com/google/ical/iter/InstanceGenerators.java b/src/main/java/com/google/ical/iter/InstanceGenerators.java new file mode 100644 index 0000000..4aded7c --- /dev/null +++ b/src/main/java/com/google/ical/iter/InstanceGenerators.java @@ -0,0 +1,271 @@ +// 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.Frequency; +import com.google.ical.values.Weekday; +import com.google.ical.values.DateValue; + +import java.util.ArrayList; +import java.util.List; + +/** + * factory for generators that operate on groups of generators to generate full + * dates. + * + * @author mikesamuel+svn@gmail.com (Mike Samuel) + */ +class InstanceGenerators { + + /** + * a collector that yields each date in the period without doing any set + * collecting. + */ + static Generator serialInstanceGenerator( + final Predicate super DateValue> filter, + final Generator yearGenerator, final Generator monthGenerator, + final Generator dayGenerator) { + return new Generator() { + @Override + public boolean generate(DTBuilder builder) + throws IteratorShortCircuitingException { + // cascade through periods to compute the next date + do { + // until we run out of days in the current month + while (!dayGenerator.generate(builder)) { + // until we run out of months in the current year + while (!monthGenerator.generate(builder)) { + // if there are more years available fetch one + if (!yearGenerator.generate(builder)) { + // otherwise the recurrence is exhausted + return false; + } + } + } + // apply filters to generated dates + } while (!filter.apply(builder.toDate())); + + return true; + } + }; + } + + static Generator bySetPosInstanceGenerator( + int[] setPos, final Frequency freq, final Weekday wkst, + final Predicate super DateValue> filter, + final Generator yearGenerator, final Generator monthGenerator, + final Generator dayGenerator) { + final int[] uSetPos = Util.uniquify(setPos); + + final Generator serialInstanceGenerator = + serialInstanceGenerator( + filter, yearGenerator, monthGenerator, dayGenerator); + + final boolean allPositive; + final int maxPos; + if (false) { + int mp = 0; + boolean ap = true; + for (int i = setPos.length; --i >= 0;) { + if (setPos[i] < 0) { + ap = false; + break; + } + mp = Math.max(setPos[i], mp); + } + maxPos = mp; + allPositive = ap; + } else { + // TODO(msamuel): does this work? + maxPos = uSetPos[uSetPos.length - 1]; + allPositive = uSetPos[0] > 0; + } + + return new Generator() { + DateValue pushback = null; + /** + * Is this the first instance we generate? + * We need to know so that we don't clobber dtStart. + */ + boolean first = true; + /** Do we need to halt iteration once the current set has been used? */ + boolean done = false; + + /** The elements in the current set, filtered by set pos */ + Listremove
operation.
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+public interface RecurrenceIterator extends Iterator!hasNext()
, then behavior is undefined.
+ *
+ * @return a DateValue that is strictly later than any date previously
+ * returned by this iterator.
+ */
+ DateValue next();
+
+ /**
+ * skips all dates in the series before the given date.
+ *
+ * @param newStartUtc non null.
+ */
+ void advanceTo(DateValue newStartUtc);
+
+ /**
+ * unsupported.
+ * @throws UnsupportedOperationException always
+ */
+ void remove();
+}
diff --git a/src/main/java/com/google/ical/iter/RecurrenceIteratorFactory.java b/src/main/java/com/google/ical/iter/RecurrenceIteratorFactory.java
new file mode 100644
index 0000000..4b41e4d
--- /dev/null
+++ b/src/main/java/com/google/ical/iter/RecurrenceIteratorFactory.java
@@ -0,0 +1,487 @@
+// 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.values.DateTimeValue;
+import com.google.ical.values.DateTimeValueImpl;
+import com.google.ical.values.DateValue;
+import com.google.ical.values.Frequency;
+import com.google.ical.values.IcalObject;
+import com.google.ical.values.RDateList;
+import com.google.ical.values.RRule;
+import com.google.ical.values.TimeValue;
+import com.google.ical.values.Weekday;
+import com.google.ical.values.WeekdayNum;
+import com.google.ical.util.Predicate;
+import com.google.ical.util.Predicates;
+import com.google.ical.util.TimeUtils;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+/**
+ * for calculating the occurrences of an individual RFC 2445 RRULE or groups of
+ * RRULES, RDATES, EXRULES, and EXDATES.
+ *
+ * + * Filter - a function that returns true iff the given date matches the subrule. + *
+ * Condition - returns true if the given date is past the end of the recurrence. + * + *
All the functions that represent rule parts are stateful.
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+public class RecurrenceIteratorFactory {
+
+ private static final Logger LOGGER = Logger.getLogger(
+ RecurrenceIteratorFactory.class.getName());
+
+ /**
+ * given a block of RRULE, EXRULE, RDATE, and EXDATE content lines, parse
+ * them into a single recurrence iterator.
+ * @param rdata ical text.
+ * @param dtStart the date of the first occurrence in timezone tzid, which is
+ * used to fill in optional fields in the RRULE, such as the day of the
+ * month for a monthly repetition when no ther day specified.
+ * Note: this may not be the first date in the series since an EXRULE or
+ * EXDATE might force it to be skipped, but there will be no earlier date
+ * generated by this ruleset.
+ * @param strict true if any failure to parse should result in a
+ * ParseException. false causes bad content lines to be logged and ignored.
+ */
+ public static RecurrenceIterator createRecurrenceIterator(
+ String rdata, DateValue dtStart, TimeZone tzid, boolean strict)
+ throws ParseException {
+ return createRecurrenceIterable(rdata, dtStart, tzid, strict).iterator();
+ }
+
+ public static RecurrenceIterable createRecurrenceIterable(
+ String rdata, final DateValue dtStart, final TimeZone tzid,
+ final boolean strict)
+ throws ParseException {
+ final IcalObject[] contentLines = parseContentLines(rdata, tzid, strict);
+
+ return new RecurrenceIterable() {
+ public RecurrenceIterator iterator() {
+ List If a rule does prove productive though, it can be alerted to the fact by
+ * the {@link #workDone} method, so that any throttle can be reset.
+ * recurrence rule implementation as described at
+RFC 2445 section 4.3.10.
+ Purpose: This value type is used to identify properties that contain
+ a recurrence rule specification.
+
+ Formal Definition: The value type is defined by the following
+ notation:
+
+ Description: If the property permits, multiple "recur" values are
+ specified by a COMMA character (US-ASCII decimal 44) separated list
+ of values. The value type is a structured value consisting of a list
+ of one or more recurrence grammar parts. Each rule part is defined by
+ a NAME=VALUE pair. The rule parts are separated from each other by
+ the SEMICOLON character (US-ASCII decimal 59). The rule parts are not
+ ordered in any particular sequence. Individual rule parts MUST only
+ be specified once.
+
+ The FREQ rule part identifies the type of recurrence rule. This rule
+ part MUST be specified in the recurrence rule. Valid values include
+ SECONDLY, to specify repeating events based on an interval of a
+ second or more; MINUTELY, to specify repeating events based on an
+ interval of a minute or more; HOURLY, to specify repeating events
+ based on an interval of an hour or more; DAILY, to specify repeating
+ events based on an interval of a day or more; WEEKLY, to specify
+ repeating events based on an interval of a week or more; MONTHLY, to
+ specify repeating events based on an interval of a month or more; and
+ YEARLY, to specify repeating events based on an interval of a year or
+ more.
+
+ The INTERVAL rule part contains a positive integer representing how
+ often the recurrence rule repeats. The default value is "1", meaning
+ every second for a SECONDLY rule, or every minute for a MINUTELY
+ rule, every hour for an HOURLY rule, every day for a DAILY rule,
+ every week for a WEEKLY rule, every month for a MONTHLY rule and
+ every year for a YEARLY rule.
+
+ The UNTIL rule part defines a date-time value which bounds the
+ recurrence rule in an inclusive manner. If the value specified by
+ UNTIL is synchronized with the specified recurrence, this date or
+ date-time becomes the last instance of the recurrence. If specified
+ as a date-time value, then it MUST be specified in an UTC time
+ format. If not present, and the COUNT rule part is also not present,
+ the RRULE is considered to repeat forever.
+
+ The COUNT rule part defines the number of occurrences at which to
+ range-bound the recurrence. The "DTSTART" property value, if
+ specified, counts as the first occurrence.
+
+ The BYSECOND rule part specifies a COMMA character (US-ASCII decimal
+ 44) separated list of seconds within a minute. Valid values are 0 to
+ 59. The BYMINUTE rule part specifies a COMMA character (US-ASCII
+ decimal 44) separated list of minutes within an hour. Valid values
+ are 0 to 59. The BYHOUR rule part specifies a COMMA character (US-
+ ASCII decimal 44) separated list of hours of the day. Valid values
+ are 0 to 23.
+
+ The BYDAY rule part specifies a COMMA character (US-ASCII decimal 44)
+ separated list of days of the week; MO indicates Monday; TU indicates
+ Tuesday; WE indicates Wednesday; TH indicates Thursday; FR indicates
+ Friday; SA indicates Saturday; SU indicates Sunday.
+
+ Each BYDAY value can also be preceded by a positive (+n) or negative
+ (-n) integer. If present, this indicates the nth occurrence of the
+ specific day within the MONTHLY or YEARLY RRULE. For example, within
+ a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday
+ within the month, whereas -1MO represents the last Monday of the
+ month. If an integer modifier is not present, it means all days of
+ this type within the specified frequency. For example, within a
+ MONTHLY rule, MO represents all Mondays within the month.
+
+ The BYMONTHDAY rule part specifies a COMMA character (ASCII decimal
+ 44) separated list of days of the month. Valid values are 1 to 31 or
+ -31 to -1. For example, -10 represents the tenth to the last day of
+ the month.
+
+ The BYYEARDAY rule part specifies a COMMA character (US-ASCII decimal
+ 44) separated list of days of the year. Valid values are 1 to 366 or
+ -366 to -1. For example, -1 represents the last day of the year
+ (December 31st) and -306 represents the 306th to the last day of the
+ year (March 1st).
+
+ The BYWEEKNO rule part specifies a COMMA character (US-ASCII decimal
+ 44) separated list of ordinals specifying weeks of the year. Valid
+ values are 1 to 53 or -53 to -1. This corresponds to weeks according
+ to week numbering as defined in [ISO 8601]. A week is defined as a
+ seven day period, starting on the day of the week defined to be the
+ week start (see WKST). Week number one of the calendar year is the
+ first week which contains at least four (4) days in that calendar
+ year. This rule part is only valid for YEARLY rules. For example, 3
+ represents the third week of the year.
+
+ The WKST rule part specifies the day on which the workweek starts.
+ Valid values are MO, TU, WE, TH, FR, SA and SU. This is significant
+ when a WEEKLY RRULE has an interval greater than 1, and a BYDAY rule
+ part is specified. This is also significant when in a YEARLY RRULE
+ when a BYWEEKNO rule part is specified. The default value is MO.
+
+ The BYSETPOS rule part specifies a COMMA character (US-ASCII decimal
+ 44) separated list of values which corresponds to the nth occurrence
+ within the set of events specified by the rule. Valid values are 1 to
+ 366 or -366 to -1. It MUST only be used in conjunction with another
+ BYxxx rule part. For example "the last work day of the month" could
+ be represented as:
+
+ Each BYSETPOS value can include a positive (+n) or negative (-n)
+ integer. If present, this indicates the nth occurrence of the
+ specific occurrence within the set of events specified by the rule.
+
+ If BYxxx rule part values are found which are beyond the available
+ scope (ie, BYMONTHDAY=30 in February), they are simply ignored.
+
+ Information, not contained in the rule, necessary to determine the
+ various recurrence instance start time and dates are derived from the
+ Start Time (DTSTART) entry attribute. For example,
+ "FREQ=YEARLY;BYMONTH=1" doesn't specify a specific day within the
+ month or a time. This information would be the same as what is
+ specified for DTSTART.
+
+ BYxxx rule parts modify the recurrence in some manner. BYxxx rule
+ parts for a period of time which is the same or greater than the
+ frequency generally reduce or limit the number of occurrences of the
+ recurrence generated. For example, "FREQ=DAILY;BYMONTH=1" reduces the
+ number of recurrence instances from all days (if BYMONTH tag is not
+ present) to all days in January. BYxxx rule parts for a period of
+ time less than the frequency generally increase or expand the number
+ of occurrences of the recurrence. For example,
+ "FREQ=YEARLY;BYMONTH=1,2" increases the number of days within the
+ yearly recurrence set from 1 (if BYMONTH tag is not present) to 2.
+
+ If multiple BYxxx rule parts are specified, then after evaluating the
+ specified FREQ and INTERVAL rule parts, the BYxxx rule parts are
+ applied to the current set of evaluated occurrences in the following
+ order: BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY, BYHOUR,
+ BYMINUTE, BYSECOND and BYSETPOS; then COUNT and UNTIL are evaluated.
+
+ Here is an example of evaluating multiple BYxxx rule parts.
+
+ First, the "INTERVAL=2" would be applied to "FREQ=YEARLY" to arrive
+ at "every other year". Then, "BYMONTH=1" would be applied to arrive
+ at "every January, every other year". Then, "BYDAY=SU" would be
+ applied to arrive at "every Sunday in January, every other year".
+ Then, "BYHOUR=8,9" would be applied to arrive at "every Sunday in
+ January at 8 AM and 9 AM, every other year". Then, "BYMINUTE=30"
+ would be applied to arrive at "every Sunday in January at 8:30 AM and
+ 9:30 AM, every other year". Then, lacking information from RRULE, the
+ second is derived from DTSTART, to end up in "every Sunday in January
+ at 8:30:00 AM and 9:30:00 AM, every other year". Similarly, if the
+ BYMINUTE, BYHOUR, BYDAY, BYMONTHDAY or BYMONTH rule part were
+ missing, the appropriate minute, hour, day or month would have been
+ retrieved from the "DTSTART" property.
+
+ No additional content value encoding (i.e., BACKSLASH character
+ encoding) is defined for this value type.
+
+ Example: The following is a rule which specifies 10 meetings which
+ occur every other day:
+
+ There are other examples specified in the "RRULE" specification.
+
+
\ No newline at end of file
diff --git a/src/main/java/com/google/ical/util/DTBuilder.java b/src/main/java/com/google/ical/util/DTBuilder.java
new file mode 100644
index 0000000..0880c0a
--- /dev/null
+++ b/src/main/java/com/google/ical/util/DTBuilder.java
@@ -0,0 +1,197 @@
+// 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.util;
+
+import com.google.ical.values.DateTimeValue;
+import com.google.ical.values.DateTimeValueImpl;
+import com.google.ical.values.DateValue;
+import com.google.ical.values.DateValueImpl;
+import com.google.ical.util.TimeUtils;
+import com.google.ical.values.TimeValue;
+
+/**
+ * a mutable buffer that can be used to build {@link DateValue}s and
+ * {@link DateTimeValue}s.
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+public class DTBuilder {
+
+ /** in AD. 0 -> 1BC. */
+ public int year;
+ /** one indexed. */
+ public int month;
+ /** one indexed */
+ public int day;
+ /** zero indexed in 24 hour time. */
+ public int hour;
+ /** zero indexed */
+ public int minute;
+ /** zero indexed */
+ public int second;
+
+ public DTBuilder() {
+ }
+
+ public DTBuilder(int year, int month, int day,
+ int hour, int minute, int second) {
+ this.year = year;
+ this.month = month;
+ this.day = day;
+ this.hour = hour;
+ this.minute = minute;
+ this.second = second;
+ }
+
+ public DTBuilder(int year, int month, int day) {
+ this.year = year;
+ this.month = month;
+ this.day = day;
+ }
+
+ public DTBuilder(DateValue dv) {
+ this.year = dv.year();
+ this.month = dv.month();
+ this.day = dv.day();
+ if (dv instanceof TimeValue) {
+ TimeValue tv = (TimeValue) dv;
+ this.hour = tv.hour();
+ this.minute = tv.minute();
+ this.second = tv.second();
+ }
+ }
+
+ /**
+ * produces a normalized date time, using zero for the time fields if none
+ * were provided.
+ * @return not null
+ */
+ public DateTimeValue toDateTime() {
+ normalize();
+ return new DateTimeValueImpl(year, month, day, hour, minute, second);
+ }
+
+ /**
+ * produces a normalized date.
+ * @return not null
+ */
+ public DateValue toDate() {
+ normalize();
+ return new DateValueImpl(year, month, day);
+ }
+
+ /**
+ * behavior undefined unless normalized.
+ * If you're not sure whether it's appropriate to use this method, use
+ * According to section 4.3.10 of RFC 2445: The iCalendar object is organized into individual lines of text, called
+ * content lines. Content lines are delimited by a line break, which is a CRLF
+ * sequence (US-ASCII decimal 13, followed by US-ASCII decimal 10).
+ *
+ * Lines of text SHOULD NOT be longer than 75 octets, excluding the line
+ * break. Long content lines SHOULD be split into a multiple line
+ * representations using a line "folding" technique. That is, a long line can
+ * be split between any two characters by inserting a CRLF immediately
+ * followed by a single linear white space character (i.e., SPACE, US-ASCII
+ * decimal 32 or HTAB, US-ASCII decimal 9). Any sequence of CRLF followed
+ * immediately by a single linear white space character is ignored (i.e.,
+ * removed) when processing the content type.
+ */
+ public static String unfoldIcal(String foldedContentLines) {
+ return IGNORABLE_ICAL_WHITESPACE.matcher(foldedContentLines).replaceAll("");
+ }
+ private static final Pattern IGNORABLE_ICAL_WHITESPACE =
+ Pattern.compile("(?:\\r\\n?|\\n)[ \t]");
+
+ private IcalParseUtil() { }
+}
diff --git a/src/main/java/com/google/ical/values/IcalSchema.java b/src/main/java/com/google/ical/values/IcalSchema.java
new file mode 100644
index 0000000..2c76df7
--- /dev/null
+++ b/src/main/java/com/google/ical/values/IcalSchema.java
@@ -0,0 +1,204 @@
+// 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.values;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * ical objects are made up of parameters (key=value pairs) and the contents
+ * are often one or more value types or key=value pairs.
+ * This schema encapsulates rules that can be applied to parse each part before
+ * inserting the results into the {@link IcalObject}.
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+class IcalSchema {
+
+ /** rules for decoding parameter values */
+ private Map See RFC 2445 sections 4.8.5.1 and 4.8.5.3.
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+public class RDateList extends AbstractIcalObject {
+
+ private TimeZone tzid;
+ private DateValue[] datesUtc;
+ private IcalValueType valueType;
+
+ public RDateList(String icalString, TimeZone tzid) throws ParseException {
+ setTzid(tzid);
+ parse(icalString, RRuleSchema.instance());
+ }
+
+ public RDateList(TimeZone tzid) {
+ setTzid(tzid);
+ setName("RDATE");
+ datesUtc = new DateValue[0];
+ }
+
+ public TimeZone getTzid() { return this.tzid; }
+ public void setTzid(TimeZone tzid) {
+ assert null != tzid;
+ this.tzid = tzid;
+ }
+
+ public DateValue[] getDatesUtc() {
+ return null != this.datesUtc ? this.datesUtc.clone() : null;
+ }
+ public void setDatesUtc(DateValue[] datesUtc) {
+ this.datesUtc = datesUtc.clone();
+ if (datesUtc.length > 0) {
+ setValueType((datesUtc[0] instanceof TimeValue)
+ ? IcalValueType.DATE_TIME
+ : IcalValueType.DATE);
+ }
+ }
+
+ /**
+ * the type of the values contained by this list as reported by the ical
+ * "VALUE" parameter, typically DATE or DATE-TIME.
+ */
+ public IcalValueType getValueType() {
+ return valueType;
+ }
+
+ public void setValueType(IcalValueType valueType) {
+ this.valueType = valueType;
+ }
+
+ /** returns a String containing ical content lines. */
+ public String toIcal() {
+ StringBuilder buf = new StringBuilder();
+ buf.append(this.getName().toUpperCase());
+ buf.append(";TZID=\"").append(tzid.getID()).append('"');
+ buf.append(";VALUE=").append(valueType.toIcal());
+ if (hasExtParams()) {
+ for (Map.Entry value types for dates, date ranges, periods of time, etc.inclusions - exclusions
.
+ * Exclusions trump inclusions, and {@link DateValue dates} and
+ * {@link DateTimeValue date-times} never match one another.
+ * @param included non null.
+ * @param excluded non null.
+ * @return non null.
+ */
+ public static RecurrenceIterator except(
+ RecurrenceIterator included, RecurrenceIterator excluded) {
+ return new CompoundIteratorImpl(
+ Collections.
+ *
+ * from hanging an iterator.
+ * RRULE:FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=30
+ * 4.3.10 Recurrence Rule
+Value Name: RECUR
+
+ recur = "FREQ"=freq *(
+
+ ; either UNTIL or COUNT may appear in a 'recur',
+ ; but UNTIL and COUNT MUST NOT occur in the same 'recur'
+
+ ( ";" "UNTIL" "=" enddate ) /
+ ( ";" "COUNT" "=" 1*DIGIT ) /
+
+ ; the rest of these keywords are optional,
+ ; but MUST NOT occur more than once
+
+ ( ";" "INTERVAL" "=" 1*DIGIT ) /
+ ( ";" "BYSECOND" "=" byseclist ) /
+ ( ";" "BYMINUTE" "=" byminlist ) /
+ ( ";" "BYHOUR" "=" byhrlist ) /
+ ( ";" "BYDAY" "=" bywdaylist ) /
+ ( ";" "BYMONTHDAY" "=" bymodaylist ) /
+ ( ";" "BYYEARDAY" "=" byyrdaylist ) /
+ ( ";" "BYWEEKNO" "=" bywknolist ) /
+ ( ";" "BYMONTH" "=" bymolist ) /
+ ( ";" "BYSETPOS" "=" bysplist ) /
+ ( ";" "WKST" "=" weekday ) /
+
+ ( ";" x-name "=" text )
+ )
+
+ freq = "SECONDLY" / "MINUTELY" / "HOURLY" / "DAILY"
+ / "WEEKLY" / "MONTHLY" / "YEARLY"
+
+ enddate = date
+ enddate =/ date-time ;An UTC value
+
+ byseclist = seconds / ( seconds *("," seconds) )
+
+ seconds = 1DIGIT / 2DIGIT ;0 to 59
+
+ byminlist = minutes / ( minutes *("," minutes) )
+
+ minutes = 1DIGIT / 2DIGIT ;0 to 59
+
+ byhrlist = hour / ( hour *("," hour) )
+
+ hour = 1DIGIT / 2DIGIT ;0 to 23
+
+ bywdaylist = weekdaynum / ( weekdaynum *("," weekdaynum) )
+
+ weekdaynum = [([plus] ordwk / minus ordwk)] weekday
+
+ plus = "+"
+
+ minus = "-"
+
+ ordwk = 1DIGIT / 2DIGIT ;1 to 53
+
+ weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA"
+ ;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
+ ;FRIDAY, SATURDAY and SUNDAY days of the week.
+
+ bymodaylist = monthdaynum / ( monthdaynum *("," monthdaynum) )
+
+ monthdaynum = ([plus] ordmoday) / (minus ordmoday)
+
+ ordmoday = 1DIGIT / 2DIGIT ;1 to 31
+
+ byyrdaylist = yeardaynum / ( yeardaynum *("," yeardaynum) )
+
+ yeardaynum = ([plus] ordyrday) / (minus ordyrday)
+
+ ordyrday = 1DIGIT / 2DIGIT / 3DIGIT ;1 to 366
+
+ bywknolist = weeknum / ( weeknum *("," weeknum) )
+
+ weeknum = ([plus] ordwk) / (minus ordwk)
+
+ bymolist = monthnum / ( monthnum *("," monthnum) )
+
+ monthnum = 1DIGIT / 2DIGIT ;1 to 12
+
+ bysplist = setposday / ( setposday *("," setposday) )
+
+ setposday = yeardaynum
+
+
+ Note: Assuming a Monday week start, week 53 can only occur when
+ Thursday is January 1 or if it is a leap year and Wednesday is
+ January 1.
+
+ The BYMONTH rule part specifies a COMMA character (US-ASCII decimal
+ 44) separated list of months of the year. Valid values are 1 to 12.
+
+ RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1
+
+
+ DTSTART;TZID=US-Eastern:19970105T083000
+ RRULE:FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;
+ BYMINUTE=30
+
+
+ FREQ=DAILY;COUNT=10;INTERVAL=2
+
+toDateValue().compareTo(dv)
instead.
+ */
+ public int compareTo(DateValue dv) {
+ long dvComparable =
+ (((((long) dv.year()) << 4) + dv.month()) << 5) + dv.day();
+ long dtbComparable =
+ ((((long) year << 4) + month << 5)) + day;
+ if (dv instanceof TimeValue) {
+ TimeValue tv = (TimeValue) dv;
+ dvComparable = (((((dvComparable << 5) + tv.hour()) << 6) + tv.minute())
+ << 6) + tv.second();
+ dtbComparable = (((((dtbComparable << 5) + hour) << 6) + minute)
+ << 6) + second;
+ }
+ long delta = dtbComparable - dvComparable;
+ return delta < 0 ? -1 : delta == 0 ? 0 : 1;
+ }
+
+ /**
+ * makes sure that the fields are in the proper ranges, by e.g. converting
+ * 32 January to 1 February, or month 0 to December of the year before.
+ */
+ public void normalize() {
+ this.normalizeTime();
+ this.normalizeDate();
+ }
+
+ @Override
+ public String toString() {
+ return year + "-" + month + "-" + day + " " + hour + ":" + minute + ":"
+ + second;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof DTBuilder)) { return false; }
+ DTBuilder that = (DTBuilder) o;
+ return this.year == that.year
+ && this.month == that.month
+ && this.day == that.day
+ && this.hour == that.hour
+ && this.minute == that.minute
+ && this.second == that.second;
+ }
+
+ @Override
+ public int hashCode() {
+ return
+ ((((((((year << 4) + month << 5) + day) << 5) + hour) << 6) + minute)
+ << 6) + second;
+ }
+
+ private void normalizeTime() {
+ int addMinutes = ((second < 0) ? (second - 59) : second) / 60;
+ second -= addMinutes * 60;
+ minute += addMinutes;
+ int addHours = ((minute < 0) ? (minute - 59) : minute) / 60;
+ minute -= addHours * 60;
+ hour += addHours;
+ int addDays = ((hour < 0) ? (hour - 23) : hour) / 24;
+ hour -= addDays * 24;
+ day += addDays;
+ }
+ private void normalizeDate() {
+ while (day <= 0) {
+ int days = TimeUtils.yearLength(month > 2 ? year : year - 1);
+ day += days;
+ --year;
+ }
+ if (month <= 0) {
+ int years = month / 12 - 1;
+ year += years;
+ month -= 12 * years;
+ } else if (month > 12) {
+ int years = (month - 1) / 12;
+ year += years;
+ month -= 12 * years;
+ }
+ while (true) {
+ if (month == 1) {
+ int yearLength = TimeUtils.yearLength(year);
+ if (day > yearLength) {
+ ++year;
+ day -= yearLength;
+ }
+ }
+ int monthLength = TimeUtils.monthLength(year, month);
+ if (day > monthLength) {
+ day -= monthLength;
+ if (++month > 12) {
+ month -= 12;
+ ++year;
+ }
+ } else {
+ break;
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/ical/util/Predicate.java b/src/main/java/com/google/ical/util/Predicate.java
new file mode 100644
index 0000000..8d2a018
--- /dev/null
+++ b/src/main/java/com/google/ical/util/Predicate.java
@@ -0,0 +1,18 @@
+// CopyrightGoogle Inc. All rights reserved.
+
+package com.google.ical.util;
+
+/**
+ * A function with a boolean return value useful for filtering.
+ */
+public interface Predicate
+ * The FREQ rule part identifies the type of recurrence rule. This rule
+ * part MUST be specified in the recurrence rule. Valid values include
+ * SECONDLY, to specify repeating events based on an interval of a
+ * second or more; MINUTELY, to specify repeating events based on an
+ * interval of a minute or more; HOURLY, to specify repeating events
+ * based on an interval of an hour or more; DAILY, to specify repeating
+ * events based on an interval of a day or more; WEEKLY, to specify
+ * repeating events based on an interval of a week or more; MONTHLY, to
+ * specify repeating events based on an interval of a month or more; and
+ * YEARLY, to specify repeating events based on an interval of a year or
+ * more.
+ *
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+public enum Frequency {
+ // in order of increasing length
+ SECONDLY,
+ MINUTELY,
+ HOURLY,
+ DAILY,
+ WEEKLY,
+ MONTHLY,
+ YEARLY,
+ ;
+}
diff --git a/src/main/java/com/google/ical/values/IcalObject.java b/src/main/java/com/google/ical/values/IcalObject.java
new file mode 100644
index 0000000..bcf44f8
--- /dev/null
+++ b/src/main/java/com/google/ical/values/IcalObject.java
@@ -0,0 +1,36 @@
+// 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.values;
+
+import java.util.Map;
+
+/**
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+public interface IcalObject {
+
+ /** the name of the content line, such as EXRULE, SUMMARY, ORGANIZER. */
+ String getName();
+ /** returns a String containing ical content lines. */
+ String toIcal();
+ /**
+ * map of extension parameters such as X-Google-Foo=bar.
+ * The iteration order is the order they appear in the
+ * content line.
+ */
+ Map4.1 Content Lines
+ *
+ *
+ * 4.2.20 Value Data Types
+ *
+ * Parameter Name: VALUE
+ *
+ * Purpose: To explicitly specify the data type format for a property value.
+ *
+ * Format Definition: The "VALUE" property parameter is defined by the
+ * following notation:
+ *
+ * valuetypeparam = "VALUE" "=" valuetype
+ *
+ * valuetype = ("BINARY"
+ * / "BOOLEAN"
+ * / "CAL-ADDRESS"
+ * / "DATE"
+ * / "DATE-TIME"
+ * / "DURATION"
+ * / "FLOAT"
+ * / "INTEGER"
+ * / "PERIOD"
+ * / "RECUR"
+ * / "TEXT"
+ * / "TIME"
+ * / "URI"
+ * / "UTC-OFFSET"
+ * / x-name
+ * ; Some experimental iCalendar data type.
+ * / iana-token)
+ * ; Some other IANA registered iCalendar data type.
+ *
+ *
+ * Description: The parameter specifies the data type and format of the property
+ * value. The property values MUST be of a single value type. For example, a
+ * "RDATE" property cannot have a combination of DATE- TIME and TIME value
+ * types.
+ *
+ * If the property's value is the default value type, then this parameter need
+ * not be specified. However, if the property's default value type is overridden
+ * by some other allowable value type, then this parameter MUST be specified.
+ *
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+public enum IcalValueType {
+
+ BINARY,
+ BOOLEAN,
+ CAL_ADDRESS,
+ DATE,
+ DATE_TIME,
+ DURATION,
+ FLOAT,
+ INTEGER,
+ PERIOD,
+ RECUR,
+ TEXT,
+ TIME,
+ URI,
+ UTC_OFFSET,
+
+ X_NAME,
+ OTHER,
+ ;
+
+ public static IcalValueType fromIcal(String icalValue) {
+ return valueOf(icalValue.toUpperCase().replace('-', '_'));
+ }
+
+ public String toIcal() { return name().replace('_', '-'); }
+}
diff --git a/src/main/java/com/google/ical/values/PeriodValue.java b/src/main/java/com/google/ical/values/PeriodValue.java
new file mode 100644
index 0000000..e07bfcd
--- /dev/null
+++ b/src/main/java/com/google/ical/values/PeriodValue.java
@@ -0,0 +1,54 @@
+// 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.values;
+
+
+/**
+ * a half-open range of {@link DateValue}s. The start is inclusive, and the
+ * end is exclusive. The end must be on or after the start. When the start and
+ * end are the same, the period is zero width, i.e. contains zero seconds.
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+public interface PeriodValue {
+
+ /**
+ * the start of the period.
+ * @return non null.
+ */
+ DateValue start();
+
+ /**
+ * the end of the period.
+ * The end must be >= {@link #start()}, and
+ * (start() instanceof {@link TimeValue}) ==
+ * (end() instanceof TimeValue).
+ * @return non null.
+ */
+ DateValue end();
+
+ /**
+ * true iff this period overlaps the given period.
+ * @param pv not null.
+ */
+ boolean intersects(PeriodValue pv);
+
+ /**
+ * true iff this period completely contains the given period.
+ * @param pv not null.
+ */
+ boolean contains(PeriodValue pv);
+
+} // PeriodValue
diff --git a/src/main/java/com/google/ical/values/PeriodValueImpl.java b/src/main/java/com/google/ical/values/PeriodValueImpl.java
new file mode 100644
index 0000000..2f31979
--- /dev/null
+++ b/src/main/java/com/google/ical/values/PeriodValueImpl.java
@@ -0,0 +1,104 @@
+// 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.values;
+
+import com.google.ical.util.TimeUtils;
+
+/**
+ * a half-open range of {@link DateValue}s.
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+public class PeriodValueImpl implements PeriodValue {
+
+ private DateValue start, end;
+
+ /**
+ * returns a period with the given start and end dates.
+ * @param start non null.
+ * @param end on or after start. May/must have a time if the start has a
+ * time.
+ */
+ public static PeriodValue create(DateValue start, DateValue end) {
+ return new PeriodValueImpl(start, end);
+ }
+
+ /**
+ * returns a period with the given start date and duration.
+ * @param start non null.
+ * @param dur a positive duration represented as a DateValue.
+ */
+ public static PeriodValue createFromDuration(DateValue start, DateValue dur) {
+ DateValue end = TimeUtils.add(start, dur);
+ if (end instanceof TimeValue && !(start instanceof TimeValue)) {
+ start = TimeUtils.dayStart(start);
+ }
+ return new PeriodValueImpl(start, end);
+ }
+
+ protected PeriodValueImpl(DateValue start, DateValue end) {
+ if (start.compareTo(end) > 0) {
+ throw new IllegalArgumentException
+ ("Start (" + start + ") must precede end (" + end + ")");
+ }
+ if ((start instanceof TimeValue) ^ (end instanceof TimeValue)) {
+ throw new IllegalArgumentException
+ ("Start (" + start + ") and end (" + end +
+ ") must both have times or neither have times.");
+ }
+ this.start = start;
+ this.end = end;
+ }
+
+ public DateValue start() { return start; }
+
+ public DateValue end() { return end; }
+
+ /** true iff this period overlaps the given period. */
+ public boolean intersects(PeriodValue pv) {
+ DateValue sa = this.start,
+ ea = this.end,
+ sb = pv.start(),
+ eb = pv.end();
+
+ return sa.compareTo(eb) < 0 && sb.compareTo(ea) < 0;
+ }
+
+ /** true iff this period completely contains the given period. */
+ public boolean contains(PeriodValue pv) {
+ DateValue sa = this.start,
+ ea = this.end,
+ sb = pv.start(),
+ eb = pv.end();
+
+ return !(sb.compareTo(sa) < 0 || ea.compareTo(eb) < 0);
+ }
+
+ @Override public boolean equals(Object o) {
+ if (!(o instanceof PeriodValue)) { return false; }
+ PeriodValue that = (PeriodValue) o;
+ return this.start().equals(that.start())
+ && this.end().equals(that.end());
+ }
+
+ @Override public int hashCode() {
+ return start.hashCode() ^ (31 * end.hashCode());
+ }
+
+ @Override public String toString() {
+ return start().toString() + "/" + end().toString();
+ }
+
+} // PeriodValueImpl
diff --git a/src/main/java/com/google/ical/values/RDateList.java b/src/main/java/com/google/ical/values/RDateList.java
new file mode 100644
index 0000000..c139f47
--- /dev/null
+++ b/src/main/java/com/google/ical/values/RDateList.java
@@ -0,0 +1,101 @@
+// 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.values;
+
+import java.text.ParseException;
+import java.util.Map;
+import java.util.TimeZone;
+
+/**
+ * a list of dates, as from an RDATE or EXDATE ical property.
+ *
+ * DAY_OF_WEEK
values.
+ */
+ public final int jsDayNum;
+ /**
+ * agrees with the java weekday values as found in java.util.Calendar.
+ */
+ public final int javaDayNum;
+
+ Weekday(int wDayNum) {
+ this.jsDayNum = wDayNum;
+ this.javaDayNum = 1 + wDayNum;
+ }
+
+ private static Weekday[] VALUES = new Weekday[7];
+
+ static {
+ System.arraycopy(values(), 0, VALUES, 0, 7);
+ }
+
+ public static Weekday valueOf(DateValue dv) {
+ int dayIndex =
+ TimeUtils.fixedFromGregorian(dv.year(), dv.month(), dv.day()) % 7;
+ if (dayIndex < 0) { dayIndex += 7; }
+ return VALUES[dayIndex];
+ }
+
+ public static Weekday firstDayOfWeekInMonth(int year, int month) {
+ int result = TimeUtils.fixedFromGregorian(year, month, 1) % 7;
+ return VALUES[(result >= 0) ? result : result + 7];
+ }
+
+ public Weekday successor() {
+ return VALUES[(ordinal() + 1) % 7];
+ }
+
+ public Weekday predecessor() {
+ return VALUES[(ordinal() - 1 + 7) % 7];
+ }
+
+}
diff --git a/src/main/java/com/google/ical/values/WeekdayNum.java b/src/main/java/com/google/ical/values/WeekdayNum.java
new file mode 100644
index 0000000..aa7b23f
--- /dev/null
+++ b/src/main/java/com/google/ical/values/WeekdayNum.java
@@ -0,0 +1,70 @@
+// 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.values;
+
+/**
+ * represents a day of the week in a month or year such as the
+ * third monday of the month. A negative num indicates it counts from the
+ * end of the month or year.
+ *
+ *
+ * Each BYDAY value can also be preceded by a positive (+n) or negative
+ * (-n) integer. If present, this indicates the nth occurrence of the
+ * specific day within the MONTHLY or YEARLY RRULE. For example, within
+ * a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday
+ * within the month, whereas -1MO represents the last Monday of the
+ * month. If an integer modifier is not present, it means all days of
+ * this type within the specified frequency. For example, within a
+ * MONTHLY rule, MO represents all Mondays within the month.
+ *
+ *
+ * @author mikesamuel+svn@gmail.com (Mike Samuel)
+ */
+public class WeekdayNum {
+ public final int num;
+ public final Weekday wday;
+
+ /**
+ * @param num in -53,53
+ * @param wday non null.
+ */
+ public WeekdayNum(int num, Weekday wday) {
+ if (!(-53 <= num && 53 >= num && null != wday)) {
+ throw new IllegalArgumentException();
+ }
+ this.num = num;
+ this.wday = wday;
+ }
+ public String toIcal() {
+ return (0 != this.num)
+ ? String.valueOf(this.num) + this.wday
+ : this.wday.toString();
+ }
+ @Override
+ public String toString() {
+ return toIcal();
+ }
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof WeekdayNum)) { return false; }
+ WeekdayNum wdn = (WeekdayNum) o;
+ return this.num == wdn.num && this.wday == wdn.wday;
+ }
+
+ @Override
+ public int hashCode() {
+ return num ^ (53 * wday.hashCode());
+ }
+}
diff --git a/src/main/java/com/google/ical/values/package.html b/src/main/java/com/google/ical/values/package.html
new file mode 100644
index 0000000..1da2bc7
--- /dev/null
+++ b/src/main/java/com/google/ical/values/package.html
@@ -0,0 +1,3 @@
+
+
- *
- * This is a long overdue rewrite of a class of the same name that first
- * appeared in the swing table demos in 1997.
- *
- * @author Philip Milne
- * @author Brendon McLean
- * @author Dan van Enckevort
- * @author Parwinder Sekhon
- * @version 2.0 02/27/04
- */
-
-public class TableSorter extends AbstractTableModel {
- protected TableModel tableModel;
-
- public static final int DESCENDING = -1;
- public static final int NOT_SORTED = 0;
- public static final int ASCENDING = 1;
-
- private static Directive EMPTY_DIRECTIVE = new Directive ( -1, NOT_SORTED );
-
- public static final Comparator COMPARABLE_COMAPRATOR = new Comparator () {
- public int compare ( Object o1, Object o2 ) {
- return ( (Comparable) o1 ).compareTo ( o2 );
- }
- };
- public static final Comparator LEXICAL_COMPARATOR = new Comparator () {
- public int compare ( Object o1, Object o2 ) {
- return o1.toString ().compareTo ( o2.toString () );
- }
- };
-
- private Row[] viewToModel;
- private int[] modelToView;
-
- private JTableHeader tableHeader;
- private MouseListener mouseListener;
- private TableModelListener tableModelListener;
- private Map columnComparators = new HashMap ();
- private List sortingColumns = new ArrayList ();
-
- public TableSorter() {
- this.mouseListener = new MouseHandler ();
- this.tableModelListener = new TableModelHandler ();
- }
-
- public TableSorter(TableModel tableModel) {
- this ();
- setTableModel ( tableModel );
- }
-
- public TableSorter(TableModel tableModel, JTableHeader tableHeader) {
- this ();
- setTableHeader ( tableHeader );
- setTableModel ( tableModel );
- }
-
- private void clearSortingState () {
- viewToModel = null;
- modelToView = null;
- }
-
- public TableModel getTableModel () {
- return tableModel;
- }
-
- public void setTableModel ( TableModel tableModel ) {
- if ( this.tableModel != null ) {
- this.tableModel.removeTableModelListener ( tableModelListener );
- }
-
- this.tableModel = tableModel;
- if ( this.tableModel != null ) {
- this.tableModel.addTableModelListener ( tableModelListener );
- }
-
- clearSortingState ();
- fireTableStructureChanged ();
- }
-
- public JTableHeader getTableHeader () {
- return tableHeader;
- }
-
- public void setTableHeader ( JTableHeader tableHeader ) {
- if ( this.tableHeader != null ) {
- this.tableHeader.removeMouseListener ( mouseListener );
- TableCellRenderer defaultRenderer = this.tableHeader
- .getDefaultRenderer ();
- if ( defaultRenderer instanceof SortableHeaderRenderer ) {
- this.tableHeader
- .setDefaultRenderer ( ( (SortableHeaderRenderer) defaultRenderer ).tableCellRenderer );
- }
- }
- this.tableHeader = tableHeader;
- if ( this.tableHeader != null ) {
- this.tableHeader.addMouseListener ( mouseListener );
- this.tableHeader.setDefaultRenderer ( new SortableHeaderRenderer (
- this.tableHeader.getDefaultRenderer () ) );
- }
- }
-
- public boolean isSorting () {
- return sortingColumns.size () != 0;
- }
-
- private Directive getDirective ( int column ) {
- for ( int i = 0; i < sortingColumns.size (); i++ ) {
- Directive directive = (Directive) sortingColumns.get ( i );
- if ( directive.column == column ) {
- return directive;
- }
- }
- return EMPTY_DIRECTIVE;
- }
-
- public int getSortingStatus ( int column ) {
- return getDirective ( column ).direction;
- }
-
- private void sortingStatusChanged () {
- clearSortingState ();
- fireTableDataChanged ();
- if ( tableHeader != null ) {
- tableHeader.repaint ();
- }
- }
-
- public void setSortingStatus ( int column, int status ) {
- Directive directive = getDirective ( column );
- if ( directive != EMPTY_DIRECTIVE ) {
- sortingColumns.remove ( directive );
- }
- if ( status != NOT_SORTED ) {
- sortingColumns.add ( new Directive ( column, status ) );
- }
- sortingStatusChanged ();
- }
-
- protected Icon getHeaderRendererIcon ( int column, int size ) {
- Directive directive = getDirective ( column );
- if ( directive == EMPTY_DIRECTIVE ) {
- return null;
- }
- return new Arrow ( directive.direction == DESCENDING, size, sortingColumns
- .indexOf ( directive ) );
- }
-
- private void cancelSorting () {
- sortingColumns.clear ();
- sortingStatusChanged ();
- }
-
- public void setColumnComparator ( Class type, Comparator comparator ) {
- if ( comparator == null ) {
- columnComparators.remove ( type );
- } else {
- columnComparators.put ( type, comparator );
- }
- }
-
- protected Comparator getComparator ( int column ) {
- Class columnType = tableModel.getColumnClass ( column );
- Comparator comparator = (Comparator) columnComparators.get ( columnType );
- if ( comparator != null ) {
- return comparator;
- }
- if ( Comparable.class.isAssignableFrom ( columnType ) ) {
- return COMPARABLE_COMAPRATOR;
- }
- return LEXICAL_COMPARATOR;
- }
-
- private Row[] getViewToModel () {
- if ( viewToModel == null ) {
- int tableModelRowCount = tableModel.getRowCount ();
- viewToModel = new Row[tableModelRowCount];
- for ( int row = 0; row < tableModelRowCount; row++ ) {
- viewToModel[row] = new Row ( row );
- }
-
- if ( isSorting () ) {
- Arrays.sort ( viewToModel );
- }
- }
- return viewToModel;
- }
-
- public int modelIndex ( int viewIndex ) {
- return getViewToModel ()[viewIndex].modelIndex;
- }
-
- private int[] getModelToView () {
- if ( modelToView == null ) {
- int n = getViewToModel ().length;
- modelToView = new int[n];
- for ( int i = 0; i < n; i++ ) {
- modelToView[modelIndex ( i )] = i;
- }
- }
- return modelToView;
- }
-
- // TableModel interface methods
-
- public int getRowCount () {
- return ( tableModel == null ) ? 0 : tableModel.getRowCount ();
- }
-
- public int getColumnCount () {
- return ( tableModel == null ) ? 0 : tableModel.getColumnCount ();
- }
-
- public String getColumnName ( int column ) {
- return tableModel.getColumnName ( column );
- }
-
- public Class getColumnClass ( int column ) {
- return tableModel.getColumnClass ( column );
- }
-
- public boolean isCellEditable ( int row, int column ) {
- return tableModel.isCellEditable ( modelIndex ( row ), column );
- }
-
- public Object getValueAt ( int row, int column ) {
- return tableModel.getValueAt ( modelIndex ( row ), column );
- }
-
- public void setValueAt ( Object aValue, int row, int column ) {
- tableModel.setValueAt ( aValue, modelIndex ( row ), column );
- }
-
- // Helper classes
-
- private class Row implements Comparable {
- private int modelIndex;
-
- public Row(int index) {
- this.modelIndex = index;
- }
-
- public int compareTo ( Object o ) {
- int row1 = modelIndex;
- int row2 = ( (Row) o ).modelIndex;
-
- for ( Iterator it = sortingColumns.iterator (); it.hasNext (); ) {
- Directive directive = (Directive) it.next ();
- int column = directive.column;
- Object o1 = tableModel.getValueAt ( row1, column );
- Object o2 = tableModel.getValueAt ( row2, column );
-
- int comparison = 0;
- // Define null less than everything, except null.
- if ( o1 == null && o2 == null ) {
- comparison = 0;
- } else if ( o1 == null ) {
- comparison = -1;
- } else if ( o2 == null ) {
- comparison = 1;
- } else {
- comparison = getComparator ( column ).compare ( o1, o2 );
- }
- if ( comparison != 0 ) {
- return directive.direction == DESCENDING ? -comparison : comparison;
- }
- }
- return 0;
- }
- }
-
- private class TableModelHandler implements TableModelListener {
- public void tableChanged ( TableModelEvent e ) {
- // If we're not sorting by anything, just pass the event along.
- if ( !isSorting () ) {
- clearSortingState ();
- fireTableChanged ( e );
- return;
- }
-
- // If the table structure has changed, cancel the sorting; the
- // sorting columns may have been either moved or deleted from
- // the model.
- if ( e.getFirstRow () == TableModelEvent.HEADER_ROW ) {
- cancelSorting ();
- fireTableChanged ( e );
- return;
- }
-
- // We can map a cell event through to the view without widening
- // when the following conditions apply:
- //
- // a) all the changes are on one row (e.getFirstRow() == e.getLastRow())
- // and,
- // b) all the changes are in one column (column !=
- // TableModelEvent.ALL_COLUMNS) and,
- // c) we are not sorting on that column (getSortingStatus(column) ==
- // NOT_SORTED) and,
- // d) a reverse lookup will not trigger a sort (modelToView != null)
- //
- // Note: INSERT and DELETE events fail this test as they have column ==
- // ALL_COLUMNS.
- //
- // The last check, for (modelToView != null) is to see if modelToView
- // is already allocated. If we don't do this check; sorting can become
- // a performance bottleneck for applications where cells
- // change rapidly in different parts of the table. If cells
- // change alternately in the sorting column and then outside of
- // it this class can end up re-sorting on alternate cell updates -
- // which can be a performance problem for large tables. The last
- // clause avoids this problem.
- int column = e.getColumn ();
- if ( e.getFirstRow () == e.getLastRow ()
- && column != TableModelEvent.ALL_COLUMNS
- && getSortingStatus ( column ) == NOT_SORTED && modelToView != null ) {
- int viewIndex = getModelToView ()[e.getFirstRow ()];
- fireTableChanged ( new TableModelEvent ( TableSorter.this, viewIndex,
- viewIndex, column, e.getType () ) );
- return;
- }
-
- // Something has happened to the data that may have invalidated the row
- // order.
- clearSortingState ();
- fireTableDataChanged ();
- return;
- }
- }
-
- private class MouseHandler extends MouseAdapter {
- public void mouseClicked ( MouseEvent e ) {
- JTableHeader h = (JTableHeader) e.getSource ();
- TableColumnModel columnModel = h.getColumnModel ();
- int viewColumn = columnModel.getColumnIndexAtX ( e.getX () );
- int column = columnModel.getColumn ( viewColumn ).getModelIndex ();
- if ( column != -1 ) {
- int status = getSortingStatus ( column );
- if ( !e.isControlDown () ) {
- cancelSorting ();
- }
- // Cycle the sorting states through {NOT_SORTED, ASCENDING, DESCENDING}
- // or
- // {NOT_SORTED, DESCENDING, ASCENDING} depending on whether shift is
- // pressed.
- status = status + ( e.isShiftDown () ? -1 : 1 );
- status = ( status + 4 ) % 3 - 1; // signed mod, returning {-1, 0, 1}
- setSortingStatus ( column, status );
- }
- }
- }
-
- private static class Arrow implements Icon {
- private boolean descending;
- private int size;
- private int priority;
-
- public Arrow(boolean descending, int size, int priority) {
- this.descending = descending;
- this.size = size;
- this.priority = priority;
- }
-
- public void paintIcon ( Component c, Graphics g, int x, int y ) {
- Color color = c == null ? Color.GRAY : c.getBackground ();
- // In a compound sort, make each succesive triangle 20%
- // smaller than the previous one.
- int dx = (int) ( size / 2 * Math.pow ( 0.8, priority ) );
- int dy = descending ? dx : -dx;
- // Align icon (roughly) with font baseline.
- y = y + 5 * size / 6 + ( descending ? -dy : 0 );
- int shift = descending ? 1 : -1;
- g.translate ( x, y );
-
- // Right diagonal.
- g.setColor ( color.darker () );
- g.drawLine ( dx / 2, dy, 0, 0 );
- g.drawLine ( dx / 2, dy + shift, 0, shift );
-
- // Left diagonal.
- g.setColor ( color.brighter () );
- g.drawLine ( dx / 2, dy, dx, 0 );
- g.drawLine ( dx / 2, dy + shift, dx, shift );
-
- // Horizontal line.
- if ( descending ) {
- g.setColor ( color.darker ().darker () );
- } else {
- g.setColor ( color.brighter ().brighter () );
- }
- g.drawLine ( dx, 0, 0, 0 );
-
- g.setColor ( color );
- g.translate ( -x, -y );
- }
-
- public int getIconWidth () {
- return size;
- }
-
- public int getIconHeight () {
- return size;
- }
- }
-
- private class SortableHeaderRenderer implements TableCellRenderer {
- private TableCellRenderer tableCellRenderer;
-
- public SortableHeaderRenderer(TableCellRenderer tableCellRenderer) {
- this.tableCellRenderer = tableCellRenderer;
- }
-
- public Component getTableCellRendererComponent ( JTable table,
- Object value, boolean isSelected, boolean hasFocus, int row, int column ) {
- Component c = tableCellRenderer.getTableCellRendererComponent ( table,
- value, isSelected, hasFocus, row, column );
- if ( c instanceof JLabel ) {
- JLabel l = (JLabel) c;
- l.setHorizontalTextPosition ( JLabel.LEFT );
- int modelColumn = table.convertColumnIndexToModel ( column );
- l.setIcon ( getHeaderRendererIcon ( modelColumn, l.getFont ()
- .getSize () ) );
- }
- return c;
- }
- }
-
- private static class Directive {
- private int column;
- private int direction;
-
- public Directive(int column, int direction) {
- this.column = column;
- this.direction = direction;
- }
- }
-}