/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (DayPartitionBuilder.java) is part of project Time4J.
*
* Time4J is free software: You can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* Time4J is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Time4J. If not, see <http://www.gnu.org/licenses/>.
* -----------------------------------------------------------------------
*/
package net.time4j.range;
import net.time4j.PlainDate;
import net.time4j.PlainTime;
import net.time4j.Weekday;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
/**
* <p>A mutable builder for creating day partition rules. </p>
*
* <p>This class enables the easy construction of daily shop opening times or weekly work time schedules.
* Example: </p>
*
* <pre>
* DayPartitionRule rule =
* new DayPartitionBuilder()
* .addExclusion(PlainDate.of(2016, 8, 27))
* .addWeekdayRule(MONDAY, FRIDAY, ClockInterval.between(PlainTime.of(9, 0), PlainTime.of(12, 30)))
* .addWeekdayRule(MONDAY, ClockInterval.between(PlainTime.of(14, 0), PlainTime.of(16, 0)))
* .addWeekdayRule(THURSDAY, ClockInterval.between(PlainTime.of(14, 0), PlainTime.of(19, 0)))
* .build();
*
* List<TimestampInterval> intervals = // determine all intervals for August and September in 2016
* DateInterval.between(PlainDate.of(2016, 8, 1), PlainDate.of(2016, 9, 30))
* .streamPartitioned(rule)
* .parallel()
* .collect(Collectors.toList());
* </pre>
*
* @author Meno Hochschild
* @see DayPartitionRule
* @see DateInterval#streamPartitioned(DayPartitionRule)
* @since 4.18
* @doctags.concurrency {mutable}
*/
/*[deutsch]
* <p>Dient der Erzeugung einer {@code DayPartitionRule}. </p>
*
* <p>Hiermit können tägliche Ladenöffnungszeiten oder wöchentliche Arbeitszeitschemata
* auf einfache Weise erstellt werden. Beispiel: </p>
*
* <pre>
* DayPartitionRule rule =
* new DayPartitionBuilder()
* .addExclusion(PlainDate.of(2016, 8, 27))
* .addWeekdayRule(MONDAY, FRIDAY, ClockInterval.between(PlainTime.of(9, 0), PlainTime.of(12, 30)))
* .addWeekdayRule(MONDAY, ClockInterval.between(PlainTime.of(14, 0), PlainTime.of(16, 0)))
* .addWeekdayRule(THURSDAY, ClockInterval.between(PlainTime.of(14, 0), PlainTime.of(19, 0)))
* .build();
*
* List<TimestampInterval> intervals = // ermittelt alle Intervalle für August und September 2016
* DateInterval.between(PlainDate.of(2016, 8, 1), PlainDate.of(2016, 9, 30))
* .streamPartitioned(rule)
* .parallel()
* .collect(Collectors.toList());
* </pre>
*
* @author Meno Hochschild
* @see DayPartitionRule
* @see DateInterval#streamPartitioned(DayPartitionRule)
* @since 4.18
* @doctags.concurrency {mutable}
*/
public class DayPartitionBuilder {
//~ Instanzvariablen --------------------------------------------------
private final Predicate<PlainDate> activeFilter;
private final Map<Weekday, List<ChronoInterval<PlainTime>>> weekdayRules;
private final Map<PlainDate, List<ChronoInterval<PlainTime>>> exceptionRules;
private final Set<PlainDate> exclusions;
//~ Konstruktoren -----------------------------------------------------
/**
* <p>Creates a new instance for building rules which are always active. </p>
*/
/*[deutsch]
* <p>Erzeugt eine neue Instanz zum Bauen von Regeln, die immer aktiv sind. </p>
*/
public DayPartitionBuilder() {
super();
this.activeFilter = (date) -> true;
this.weekdayRules = new EnumMap<>(Weekday.class);
this.exceptionRules = new HashMap<>();
this.exclusions = new HashSet<>();
}
/**
* <p>Creates a new instance with given filter. </p>
*
* <p>If only one rule is to be applied then setting an active filter has a similar effect as setting
* an exclusion date. However, if two or more rules with different filters are created then the new
* combined day-partition-rule (based on and-chaining) will not completely exclude certain dates
* only because of one partial rule with a special filter. </p>
*
* @param activeFilter determines when the rule to be created is active (should be stateless)
* @see DayPartitionRule#and(DayPartitionRule)
* @since 4.19
*/
/*[deutsch]
* <p>Erzeugt eine neue Instanz. </p>
*
* <p>Wenn es nur um eine Regel gilt, dann hat das Setzen eines Aktivfilters eine ähnliche Wirkung
* wie das Anwenden eines Ausschlußdatums. Allerdings gibt es einen Unterschied, wenn zwei oder
* mehr Regeln mit verschiedenen Filtern miteinander kombiniert werden. In letzterem Fall kennt die
* mit Hilfe von {@code and()}-Ausdrücken kombinierte Regel nicht automatisch ein Ausschlußdatum,
* wenn der Filter nur einer Teilregel ein Datum ausschließt. </p>
*
* @param activeFilter determines when the rule to be created is active (should be stateless)
* @see DayPartitionRule#and(DayPartitionRule)
* @since 4.19
*/
public DayPartitionBuilder(Predicate<PlainDate> activeFilter) {
super();
if (activeFilter == null) {
throw new NullPointerException("Missing active filter.");
}
this.activeFilter = activeFilter;
this.weekdayRules = new EnumMap<>(Weekday.class);
this.exceptionRules = new HashMap<>();
this.exclusions = new HashSet<>();
}
//~ Methoden ----------------------------------------------------------
/**
* <p>Adds a rule to partition any calendar date. </p>
*
* @param partition a clock interval
* @return this instance for method chaining
* @throws IllegalArgumentException if there is no canonical form of given interval (for example for [00:00/24:00])
* @see IsoInterval#toCanonical()
* @since 4.20
*/
/*[deutsch]
* <p>Fügt eine Regel hinzu, die irgendeinen Kalendertag passend zerlegt. </p>
*
* @param partition a clock interval
* @return this instance for method chaining
* @throws IllegalArgumentException if there is no canonical form of given interval (for example for [00:00/24:00])
* @see IsoInterval#toCanonical()
* @since 4.20
*/
public DayPartitionBuilder addDailyRule(ClockInterval partition) {
for (Weekday dayOfWeek : Weekday.values()) {
this.addWeekdayRule(dayOfWeek, partition);
}
return this;
}
/**
* <p>Adds a rule to partition a date dependending on its day of week. </p>
*
* <p>This method can be called multiple times for the same day of week in order to define
* more than one disjunct partition. </p>
*
* @param dayOfWeek controls the partitioning in the final day partition rule
* @param partition a clock interval
* @return this instance for method chaining
* @throws IllegalArgumentException if there is no canonical form of given interval (for example for [00:00/24:00])
* @see IsoInterval#toCanonical()
*/
/*[deutsch]
* <p>Fügt eine Regel hinzu, die einen Kalendertag in Abhängigkeit von seinem Wochentag zerlegt. </p>
*
* <p>Diese Methode kann mehrmals mit dem gleichen Wochentag aufgerufen werden, um mehr als einen
* Tagesabschnitt zu definieren. </p>
*
* @param dayOfWeek controls the partitioning in the final day partition rule
* @param partition a clock interval
* @return this instance for method chaining
* @throws IllegalArgumentException if there is no canonical form of given interval (for example for [00:00/24:00])
* @see IsoInterval#toCanonical()
*/
public DayPartitionBuilder addWeekdayRule(
Weekday dayOfWeek,
ClockInterval partition
) {
if (dayOfWeek == null) {
throw new NullPointerException("Missing day of week.");
}
try {
ClockInterval p = partition.toCanonical();
if (!p.isEmpty()) {
List<ChronoInterval<PlainTime>> ps = this.weekdayRules.get(dayOfWeek);
if (ps == null) {
ps = new ArrayList<>(5);
ps.add(p);
} else {
ps = IntervalCollection.onClockAxis().plus(ps).plus(p).withBlocks().getIntervals();
}
this.weekdayRules.put(dayOfWeek, ps);
}
return this;
} catch (IllegalStateException ise) {
throw new IllegalArgumentException(ise);
}
}
/**
* <p>Adds a rule to partition a date dependending on when its day of week falls into given range. </p>
*
* <p>This method can be called multiple times using the same arguments in order to define
* more than one disjunct partition. </p>
*
* @param from starting day of week
* @param to ending day of week
* @param partition a clock interval
* @return this instance for method chaining
* @throws IllegalArgumentException if there is no canonical form of given interval (for example for [00:00/24:00])
* @see IsoInterval#toCanonical()
* @since 4.20
*/
/*[deutsch]
* <p>Fügt eine Regel hinzu, die einen Kalendertag in Abhängigkeit davon zerlegt, ob
* dessen Wochentag in den angegebenen Wochentagsbereich fällt. </p>
*
* <p>Diese Methode kann mehrmals mit den gleichen Parametern aufgerufen werden, um mehr als einen
* Tagesabschnitt zu definieren. </p>
*
* @param from starting day of week
* @param to ending day of week (inclusive)
* @param partition a clock interval
* @return this instance for method chaining
* @throws IllegalArgumentException if there is no canonical form of given interval (for example for [00:00/24:00])
* @see IsoInterval#toCanonical()
* @since 4.20
*/
public DayPartitionBuilder addWeekdayRule(
Weekday from,
Weekday to,
ClockInterval partition
) {
if (to.equals(from)) {
return this.addWeekdayRule(from, partition);
}
Weekday current = from;
do {
this.addWeekdayRule(current, partition);
} while (!(current = current.next()).equals(to));
return this.addWeekdayRule(to, partition);
}
/**
* <p>Adds a rule to partition a date dependending on when its day of week falls into given range. </p>
*
* <p>This method can be called multiple times using the same arguments in order to define
* more than one disjunct partition. </p>
*
* @param spanOfWeekdays span of weekdays with start and end
* @param partition a clock interval
* @return this instance for method chaining
* @throws IllegalArgumentException if there is no canonical form of given interval (for example for [00:00/24:00])
* @see IsoInterval#toCanonical()
* @since 4.20
*/
/*[deutsch]
* <p>Fügt eine Regel hinzu, die einen Kalendertag in Abhängigkeit davon zerlegt, ob
* dessen Wochentag in den angegebenen Wochentagsbereich fällt. </p>
*
* <p>Diese Methode kann mehrmals mit den gleichen Parametern aufgerufen werden, um mehr als einen
* Tagesabschnitt zu definieren. </p>
*
* @param spanOfWeekdays span of weekdays with start and end
* @param partition a clock interval
* @return this instance for method chaining
* @throws IllegalArgumentException if there is no canonical form of given interval (for example for [00:00/24:00])
* @see IsoInterval#toCanonical()
* @since 4.20
*/
public DayPartitionBuilder addWeekdayRule(
SpanOfWeekdays spanOfWeekdays,
ClockInterval partition
) {
return this.addWeekdayRule(spanOfWeekdays.getStart(), spanOfWeekdays.getEnd(), partition);
}
/**
* <p>Adds a rule to partition a special calendar date. </p>
*
* <p>This method can be called multiple times for the same special day in order to define
* more than one disjunct partition. </p>
*
* @param specialDay controls the partitioning in the final day partition rule
* @param partition a clock interval
* @return this instance for method chaining
* @throws IllegalArgumentException if there is no canonical form of given interval (for example for [00:00/24:00])
* @see IsoInterval#toCanonical()
*/
/*[deutsch]
* <p>Fügt eine Regel hinzu, die einen speziellen Kalendertag zerlegt. </p>
*
* <p>Diese Methode kann mehrmals für das gleiche Datum aufgerufen werden, um mehr als einen
* Tagesabschnitt zu definieren. </p>
*
* @param specialDay controls the partitioning in the final day partition rule
* @param partition a clock interval
* @return this instance for method chaining
* @throws IllegalArgumentException if there is no canonical form of given interval (for example for [00:00/24:00])
* @see IsoInterval#toCanonical()
*/
public DayPartitionBuilder addSpecialRule(
PlainDate specialDay,
ClockInterval partition
) {
if (specialDay == null) {
throw new NullPointerException("Missing special calendar date.");
}
try {
ClockInterval p = partition.toCanonical();
if (!p.isEmpty()) {
List<ChronoInterval<PlainTime>> ps = this.exceptionRules.get(specialDay);
if (ps == null) {
ps = new ArrayList<>(5);
ps.add(p);
} else {
ps = IntervalCollection.onClockAxis().plus(ps).plus(p).withBlocks().getIntervals();
}
this.exceptionRules.put(specialDay, ps);
}
return this;
} catch (IllegalStateException ise) {
throw new IllegalArgumentException(ise);
}
}
/**
* <p>Adds an exclusion date. </p>
*
* @param date the calendar date to be excluded from creating day partitions
* @return this instance for method chaining
* @see DayPartitionRule#isExcluded(PlainDate)
* @see #addExclusion(Collection)
*/
/*[deutsch]
* <p>Fügt ein Ausschlußdatum hinzu. </p>
*
* @param date the calendar date to be excluded from creating day partitions
* @return this instance for method chaining
* @see DayPartitionRule#isExcluded(PlainDate)
* @see #addExclusion(Collection)
*/
public DayPartitionBuilder addExclusion(PlainDate date) {
if (date == null) {
throw new NullPointerException("Missing exclusion date.");
}
this.exclusions.add(date);
return this;
}
/**
* <p>Adds multiple exclusion dates. </p>
*
* @param dates collection of calendar dates to be excluded from creating day partitions
* @return this instance for method chaining
* @see DayPartitionRule#isExcluded(PlainDate)
* @see #addExclusion(PlainDate)
*/
/*[deutsch]
* <p>Fügt eine Menge von Ausschlußdatumsobjekten hinzu. </p>
*
* @param dates collection of calendar dates to be excluded from creating day partitions
* @return this instance for method chaining
* @see DayPartitionRule#isExcluded(PlainDate)
* @see #addExclusion(PlainDate)
*/
public DayPartitionBuilder addExclusion(Collection<PlainDate> dates) {
dates.stream().forEach(this::addExclusion);
return this;
}
/**
* <p>Creates a new day partition rule. </p>
*
* @return DayPartitionRule
*/
/*[deutsch]
* <p>Erzeugt eine neue Regel zur Zerlegung eines Tages in einen oder mehrere Tagesabschnitte. </p>
*
* @return DayPartitionRule
*/
public DayPartitionRule build() {
final Map<Weekday, List<ChronoInterval<PlainTime>>> wRules = new EnumMap<>(this.weekdayRules);
final Map<PlainDate, List<ChronoInterval<PlainTime>>> eRules = new HashMap<>(this.exceptionRules);
final Set<PlainDate> invalid = new HashSet<>(this.exclusions);
return new DayPartitionRule() {
@Override
public List<ChronoInterval<PlainTime>> getPartition(PlainDate date) {
if (!this.isExcluded(date) && activeFilter.test(date)) {
List<ChronoInterval<PlainTime>> partitions = eRules.get(date);
if (partitions == null) {
partitions = wRules.get(date.getDayOfWeek());
}
if (partitions != null) {
return Collections.unmodifiableList(partitions);
}
}
return Collections.emptyList();
}
@Override
public boolean isExcluded(PlainDate date) {
return invalid.contains(date);
}
};
}
}