Java tutorial
/** * 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; } }; } }