com.outerspacecat.icalendar.RecurrenceFunctions.java Source code

Java tutorial

Introduction

Here is the source code for com.outerspacecat.icalendar.RecurrenceFunctions.java

Source

/**
 * Copyright 2011 Caleb Richardson
 * 
 * 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.outerspacecat.icalendar;

import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.chrono.ChronoLocalDate;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalField;
import java.time.temporal.ValueRange;
import java.util.ArrayList;
import java.util.List;

/**
 * Defines utility methods for working with recurrences.
 * 
 * @author Caleb Richardson
 */
final class RecurrenceFunctions {
    private RecurrenceFunctions() {
    }

    /**
     * Returns a function for transforming instances of {@link LocalDate} into
     * instances of {@link FixedSchedule}.
     * 
     * @param start the start of the recurrence. Must be non {@code null} and must
     *        return non {@code null} for {@link FixedSchedule#getStartDate()}.
     * @return a function for transforming instances of {@link LocalDate} into
     *         instances of {@link FixedSchedule}. Never {@code null}.
     */
    static Function<LocalDate, FixedSchedule> dateTransformer(final FixedSchedule start) {
        Preconditions.checkNotNull(start, "schedule required");
        Preconditions.checkArgument(start.getStartDate() != null, "start date must be specified on start");

        return date -> start.adjustStartDate(date);
    }

    /**
     * Returns a function for transforming instances of {@link LocalDateTime} into
     * instances of {@link FixedSchedule}.
     * 
     * @param start the start of the recurrence. Must be non {@code null} and must
     *        return non {@code null} for
     *        {@link FixedSchedule#getZonedStartDateTime()}.
     * @param timeZone the time zone to use. Must be non {@code null}.
     * @return a function for transforming instances of {@link LocalDateTime} into
     *         instances of {@link FixedSchedule}. Never {@code null}.
     */
    static Function<LocalDateTime, FixedSchedule> dateTimeTransformer(final FixedSchedule start,
            final ZoneId timeZone) {
        Preconditions.checkNotNull(start, "FixedSchedule required");
        Preconditions.checkArgument(start.getZonedStartDateTime() != null,
                "zoned start date-time must be specified on start");
        Preconditions.checkNotNull(timeZone, "timeZone required");

        return date -> start.adjustZonedStartDateTime(date.atZone(timeZone));
    }

    /**
     * Returns a function for transforming instances of {@link PeriodType} into
     * instances of {@link FixedSchedule}.
     * 
     * @param fallbackTimeZone the time zone to use if a period returns
     *        {@code false} for {@link PeriodType#isUtc()}. Must be non
     *        {@code null}.
     * @return a function for transforming instances of {@link PeriodType} into
     *         instances of {@link FixedSchedule}. Never {@code null}.
     */
    static Function<PeriodType, FixedSchedule> periodTransformer(final ZoneId fallbackTimeZone) {
        Preconditions.checkNotNull(fallbackTimeZone, "fallbackTimeZone required");

        return new Function<PeriodType, FixedSchedule>() {
            @Override
            public FixedSchedule apply(final PeriodType input) {
                ZoneId timeZone = input.isUtc() ? ZoneOffset.UTC : fallbackTimeZone;
                if (input.getEnd() != null) {
                    return new FixedSchedule(input.getStart().atZone(timeZone), input.getEnd().atZone(timeZone));
                } else if (input.getDuration() != null) {
                    return new FixedSchedule(input.getStart().atZone(timeZone), input.getDuration());
                } else {
                    throw new IllegalStateException("no end or duration");
                }
            }
        };
    }

    /**
     * Returns a function for transforming instances of
     * {@link ChronoFixedSchedule} into zero or more instances. If {@code field}
     * is not applicable to a fixed schedule then the generated list will be
     * empty. If a value is out of range then it will be ignored.
     * 
     * @param field the field to modify using each value in {@code values}. Must
     *        be non {@code null}.
     * @param values the values to alter supplied fixed schedules with. Must be
     *        non {@code null}, must contain one or more elements, and each
     *        element must be >= 0.
     * @return a function for transforming instances of
     *         {@link ChronoFixedSchedule} into zero or more instances. Never
     *         {@code null}.
     */
    static Function<ChronoFixedSchedule, List<ChronoFixedSchedule>> expander(final TemporalField field,
            final ImmutableSet<Integer> values) {
        Preconditions.checkNotNull(field, "field required");
        Preconditions.checkNotNull(values, "values required");
        Preconditions.checkArgument(!values.isEmpty(), "values must be non empty");
        for (Integer value : values)
            Preconditions.checkArgument(value >= 0, "each values element must be >=0");

        final ImmutableSortedSet<Integer> sortedValues = ImmutableSortedSet.copyOf(values);

        return new Function<ChronoFixedSchedule, List<ChronoFixedSchedule>>() {
            @Override
            public List<ChronoFixedSchedule> apply(final ChronoFixedSchedule obj) {
                Preconditions.checkNotNull(obj, "obj required");

                List<ChronoFixedSchedule> ret = new ArrayList<ChronoFixedSchedule>();

                if (obj.getStartDate() != null) {
                    if (obj.getStartDate().isSupported(field)) {
                        ValueRange range = obj.getStartDate().range(field);

                        for (int value : sortedValues) {
                            if (!range.isValidValue(value))
                                continue;

                            ret.add(obj.adjustStartDate(obj.getStartDate().with(field, value)));
                        }
                    }
                } else if (obj.getZonedStartDateTime() != null) {
                    if (obj.getZonedStartDateTime().isSupported(field)) {
                        ValueRange range = obj.getZonedStartDateTime().range(field);

                        for (int value : sortedValues) {
                            if (!range.isValidValue(value))
                                continue;

                            ret.add(obj.adjustZonedStartDateTime(obj.getZonedStartDateTime().with(field, value)));
                        }
                    }
                } else {
                    throw new IllegalStateException("no start date or zoned start date-time");
                }

                return ret;
            }
        };
    }

    /**
     * Returns a function for transforming instances of
     * {@link ChronoFixedSchedule} into zero or more instances. If {@code field}
     * is not applicable to a fixed schedule then the generated list will be
     * empty. If a value is out of range then it will be ignored.
     * 
     * @param field the field to modify using each value in {@code values}. Must
     *        be non {@code null}.
     * @param values the values to alter supplied fixed schedules with. Must be
     *        non {@code null} and must contain one or more elements. Elements may
     *        be negative, but must be non zero.
     * @return a function for transforming instances of
     *         {@link ChronoFixedSchedule} into zero or more instances. Never
     *         {@code null}.
     */
    static Function<ChronoFixedSchedule, List<ChronoFixedSchedule>> signedExpander(final TemporalField field,
            final ImmutableSet<Integer> values) {
        Preconditions.checkNotNull(field, "field required");
        Preconditions.checkNotNull(values, "values required");
        Preconditions.checkArgument(!values.isEmpty(), "values must be non empty");
        for (Integer value : values)
            Preconditions.checkArgument(value != 0, "each values element must be non zero");

        return new Function<ChronoFixedSchedule, List<ChronoFixedSchedule>>() {
            @Override
            public List<ChronoFixedSchedule> apply(final ChronoFixedSchedule obj) {
                Preconditions.checkNotNull(obj, "obj required");

                List<ChronoFixedSchedule> ret = new ArrayList<ChronoFixedSchedule>();

                if (obj.getStartDate() != null) {
                    if (obj.getStartDate().isSupported(field)) {
                        ValueRange range = obj.getStartDate().range(field);

                        long maximum = range.getMaximum();

                        ImmutableSortedSet<Long> sortedValues = ImmutableSortedSet.copyOf(
                                Iterables.transform(values, (input) -> input < 0 ? maximum + input + 1 : input));

                        for (Long value : sortedValues) {
                            if (!range.isValidValue(value))
                                continue;

                            ret.add(obj.adjustStartDate(obj.getStartDate().with(field, value)));
                        }
                    }
                } else if (obj.getZonedStartDateTime() != null) {
                    if (obj.getZonedStartDateTime().isSupported(field)) {
                        ValueRange range = obj.getStartDate().range(field);

                        long maximum = range.getMaximum();

                        ImmutableSortedSet<Long> sortedValues = ImmutableSortedSet.copyOf(
                                Iterables.transform(values, (input) -> input < 0 ? maximum + input + 1 : input));

                        for (Long value : sortedValues) {
                            if (!range.isValidValue(value))
                                continue;

                            ret.add(obj.adjustZonedStartDateTime(obj.getZonedStartDateTime().with(field, value)));
                        }
                    }
                } else {
                    throw new IllegalStateException("no start date or zoned start date-time");
                }

                return ret;
            }
        };
    }

    /**
     * Returns a function for transforming instances of
     * {@link ChronoFixedSchedule} into zero or more instances. Used to apply
     * BYDAY values to a WEEKLY recurrence or a YEARLY recurrence with BYWEEKNO
     * values.
     * <p>
     * May not work for all chronologies.
     * 
     * @param weekStart the week start. Must be non {@code null}.
     * @param daysOfWeek the values to alter supplied fixed schedules with. Must
     *        be non {@code null}..
     * @return a function for transforming instances of {@link FixedSchedule} into
     *         zero or more instances. Never {@code null}.
     */
    static Function<ChronoFixedSchedule, List<ChronoFixedSchedule>> weeklyByDayExpander(final DayOfWeek weekStart,
            final ImmutableSet<DayOfWeek> daysOfWeek) {
        Preconditions.checkNotNull(weekStart, "weekStart required");
        Preconditions.checkNotNull(daysOfWeek, "daysOfWeek required");
        Preconditions.checkArgument(!daysOfWeek.isEmpty(), "jodaByDay must be non empty");

        return new Function<ChronoFixedSchedule, List<ChronoFixedSchedule>>() {
            @Override
            public List<ChronoFixedSchedule> apply(final ChronoFixedSchedule obj) {
                Preconditions.checkNotNull(obj, "obj required");

                List<ChronoFixedSchedule> ret = new ArrayList<ChronoFixedSchedule>();

                ChronoLocalDate startOfWeek = obj.getDateOfStart().minus(
                        (7 + obj.getDateOfStart().get(ChronoField.DAY_OF_WEEK) - weekStart.getValue()) % 7,
                        ChronoUnit.DAYS);

                for (int i = 0; i < 7; ++i) {
                    ChronoLocalDate adjustedStart = startOfWeek.plus(i, ChronoUnit.DAYS);

                    if (daysOfWeek.contains(DayOfWeek.of(adjustedStart.get(ChronoField.DAY_OF_WEEK))))
                        ret.add(obj.adjustDateOfStart(adjustedStart));
                }

                return ret;
            }
        };
    }

    /**
     * Returns a function for transforming instances of {@link FixedSchedule} into
     * zero or more instances. Used to apply BYDAY values to a MONTHLY recurrence.
     * <p>
     * May not work for all chronologies.
     * 
     * @param byDay the values to alter supplied fixed schedules with. Must be non
     *        {@code null} and must contain one or more elements.
     * @return a function for transforming instances of {@link FixedSchedule} into
     *         zero or more instances. Never {@code null}.
     */
    static Function<ChronoFixedSchedule, List<ChronoFixedSchedule>> monthlyByDayExpander(
            final ImmutableSet<DayOfWeekOccurrence> byDay) {
        Preconditions.checkNotNull(byDay, "byDay required");
        Preconditions.checkArgument(!byDay.isEmpty(), "byDay must be non empty");

        return new Function<ChronoFixedSchedule, List<ChronoFixedSchedule>>() {
            @Override
            public List<ChronoFixedSchedule> apply(final ChronoFixedSchedule obj) {
                Preconditions.checkNotNull(obj, "obj required");

                List<ChronoFixedSchedule> ret = new ArrayList<ChronoFixedSchedule>();

                long daysInMonth = obj.getDateOfStart().range(ChronoField.DAY_OF_MONTH).getMaximum();

                for (int dayOfMonth = 1; dayOfMonth <= daysInMonth; ++dayOfMonth) {
                    ChronoLocalDate adjustedStart = obj.getDateOfStart().with(ChronoField.DAY_OF_MONTH, dayOfMonth);

                    DayOfWeek dayOfWeek = DayOfWeek.of(adjustedStart.get(ChronoField.DAY_OF_WEEK));

                    int numericPrefix = dayOfMonth / 7 + (dayOfMonth % 7 == 0 ? 0 : 1);

                    int dayOfWeekOccurrencesInMonth = (int) (numericPrefix + ((daysInMonth - dayOfMonth) / 7));

                    if (byDay.contains(new DayOfWeekOccurrence(Optional.absent(), dayOfWeek))
                            || byDay.contains(
                                    new DayOfWeekOccurrence(Optional.fromNullable(numericPrefix), dayOfWeek))
                            || byDay.contains(new DayOfWeekOccurrence(
                                    Optional.fromNullable(numericPrefix - dayOfWeekOccurrencesInMonth - 1),
                                    dayOfWeek)))
                        ret.add(obj.adjustDateOfStart(adjustedStart));

                }

                return ret;
            }
        };
    }

    /**
     * Returns a function for transforming instances of {@link FixedSchedule} into
     * zero or more instances. Used to apply BYWEEKNO values to a YEARLY
     * recurrence.
     * <p>
     * May not work for all chronologies.
     * 
     * @param byWeekNo the values to alter supplied fixed schedules with. Must be
     *        non {@code null}, must contain one or more elements, and each
     *        element must be >= -53 and <= 53, but not 0.
     * @return a function for transforming instances of {@link FixedSchedule} into
     *         zero or more instances. Never {@code null}.
     */
    static Function<ChronoFixedSchedule, List<ChronoFixedSchedule>> yearlyByWeekNoExpander(
            final DayOfWeek weekStart, final ImmutableSet<Integer> byWeekNo) {
        Preconditions.checkNotNull(weekStart, "weekStart required");
        Preconditions.checkNotNull(byWeekNo, "byWeekNo required");
        Preconditions.checkArgument(!byWeekNo.isEmpty(), "byWeekNo must be non empty");
        for (Integer value : byWeekNo)
            Preconditions.checkArgument(value >= -53 && value <= 53 && value != 0,
                    "each byWeekNo element must be >= 53 and <= 53, but not 0");

        return new Function<ChronoFixedSchedule, List<ChronoFixedSchedule>>() {
            @Override
            public List<ChronoFixedSchedule> apply(final ChronoFixedSchedule obj) {
                Preconditions.checkNotNull(obj, "obj required");

                List<ChronoFixedSchedule> ret = new ArrayList<ChronoFixedSchedule>();

                ChronoLocalDate firstDayOfYear = obj.getDateOfStart().with(ChronoField.DAY_OF_YEAR, 1);

                // offset to the closest day on WEEKSTART before or on the
                // 1st
                int daysToStartOfFirstWeek = -1
                        * ((7 + firstDayOfYear.get(ChronoField.DAY_OF_WEEK) - weekStart.getValue()) % 7);

                // the first week must have 4 or more days in the original
                // year
                if (daysToStartOfFirstWeek < -3)
                    daysToStartOfFirstWeek += 7;

                ChronoLocalDate startOfFirstWeekOfYear = firstDayOfYear.plus(daysToStartOfFirstWeek,
                        ChronoUnit.DAYS);

                long daysInYear = obj.getDateOfStart().range(ChronoField.DAY_OF_YEAR).getMaximum();
                long weeksInYear = (daysInYear - daysToStartOfFirstWeek) / 7;

                // 4 or more days left over counts as one last week
                if ((daysInYear - daysToStartOfFirstWeek) % 7 >= 4)
                    ++weeksInYear;

                final long maximum = weeksInYear;

                ImmutableSortedSet<Long> sortedValues = ImmutableSortedSet
                        .copyOf(Iterables.transform(byWeekNo, (input) -> input < 0 ? maximum + input + 1 : input));

                for (Long value : sortedValues) {
                    if (value < 1 || value > maximum)
                        continue;

                    ChronoLocalDate startOfWeek = startOfFirstWeekOfYear.plus((value - 1) * 7, ChronoUnit.DAYS);

                    ret.add(obj.adjustDateOfStart(startOfWeek));
                }

                return ret;
            }
        };
    }

    /**
     * Returns a function for transforming instances of {@link FixedSchedule} into
     * zero or more instances. Used to apply BYDAY values to a YEARLY recurrence.
     * <p>
     * May not work for all chronologies.
     * 
     * @param byDay the values to alter supplied fixed schedules with. Must be non
     *        {@code null} and must contain one or more elements.
     * @return a function for transforming instances of {@link FixedSchedule} into
     *         zero or more instances. Never {@code null}.
     */
    static Function<ChronoFixedSchedule, List<ChronoFixedSchedule>> yearlyByDayExpander(
            final ImmutableSet<DayOfWeekOccurrence> byDay) {
        Preconditions.checkNotNull(byDay, "byDay required");
        Preconditions.checkArgument(!byDay.isEmpty(), "byDay must be non empty");

        return new Function<ChronoFixedSchedule, List<ChronoFixedSchedule>>() {
            @Override
            public List<ChronoFixedSchedule> apply(final ChronoFixedSchedule obj) {
                Preconditions.checkNotNull(obj, "obj required");

                List<ChronoFixedSchedule> ret = new ArrayList<ChronoFixedSchedule>();

                int daysInYear = (int) (obj.getDateOfStart().range(ChronoField.DAY_OF_YEAR).getMaximum());

                for (int dayOfYear = 1; dayOfYear <= daysInYear; ++dayOfYear) {
                    ChronoLocalDate adjustedStart = obj.getDateOfStart().with(ChronoField.DAY_OF_YEAR, dayOfYear);

                    DayOfWeek dayOfWeek = DayOfWeek.of(adjustedStart.get(ChronoField.DAY_OF_WEEK));

                    int numericPrefix = dayOfYear / 7 + (dayOfYear % 7 == 0 ? 0 : 1);

                    int dayOfWeekOccurrencesInYear = (int) (numericPrefix + ((daysInYear - dayOfYear) / 7));

                    if (byDay.contains(new DayOfWeekOccurrence(Optional.absent(), dayOfWeek))
                            || byDay.contains(
                                    new DayOfWeekOccurrence(Optional.fromNullable(numericPrefix), dayOfWeek))
                            || byDay.contains(new DayOfWeekOccurrence(
                                    Optional.fromNullable(numericPrefix - dayOfWeekOccurrencesInYear - 1),
                                    dayOfWeek)))
                        ret.add(obj.adjustDateOfStart(adjustedStart));
                }

                return ret;
            }
        };
    }
}