/* * ----------------------------------------------------------------------- * Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/> * ----------------------------------------------------------------------- * This file (DateInterval.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.CalendarUnit; import net.time4j.Duration; import net.time4j.PlainDate; import net.time4j.PlainTime; import net.time4j.PlainTimestamp; import net.time4j.Weekcycle; import net.time4j.Weekday; import net.time4j.Weekmodel; import net.time4j.base.GregorianMath; import net.time4j.engine.AttributeQuery; import net.time4j.engine.ChronoDisplay; import net.time4j.engine.ChronoElement; import net.time4j.engine.ChronoEntity; import net.time4j.engine.EpochDays; import net.time4j.engine.TimeSpan; import net.time4j.format.Attributes; import net.time4j.format.expert.ChronoFormatter; import net.time4j.format.expert.ChronoParser; import net.time4j.format.expert.ChronoPrinter; import net.time4j.format.expert.Iso8601Format; import net.time4j.format.expert.IsoDateStyle; import net.time4j.format.expert.ParseLog; import net.time4j.format.expert.PatternType; import net.time4j.format.expert.SignPolicy; import net.time4j.tz.GapResolver; import net.time4j.tz.OverlapResolver; import net.time4j.tz.TZID; import net.time4j.tz.Timezone; import net.time4j.tz.TransitionStrategy; import java.io.IOException; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.Serializable; import java.text.ParseException; import java.time.LocalDate; import java.util.Comparator; import java.util.Locale; import java.util.Spliterator; import java.util.function.Consumer; import java.util.stream.LongStream; import java.util.stream.Stream; import java.util.stream.StreamSupport; import static net.time4j.PlainDate.*; import static net.time4j.range.IntervalEdge.CLOSED; /** * <p>Defines a date interval. </p> * * @author Meno Hochschild * @since 2.0 * @doctags.concurrency {immutable} */ /*[deutsch] * <p>Definiert ein Datumsintervall. </p> * * @author Meno Hochschild * @since 2.0 * @doctags.concurrency {immutable} */ public final class DateInterval extends IsoInterval<PlainDate, DateInterval> implements Serializable { //~ Statische Felder/Initialisierungen -------------------------------- private static final long serialVersionUID = 8074261825266036014L; private static final Comparator<ChronoInterval<PlainDate>> COMPARATOR = new IntervalComparator<>(true, PlainDate.axis()); private static final ChronoPrinter<PlainDate> REDUCED_DD = ChronoFormatter.ofDatePattern("dd", PatternType.CLDR, Locale.ROOT); private static final ChronoPrinter<PlainDate> REDUCED_MMDD = ChronoFormatter.ofDatePattern("MMdd", PatternType.CLDR, Locale.ROOT); private static final ChronoPrinter<PlainDate> REDUCED_MM_DD = ChronoFormatter.ofDatePattern("MM-dd", PatternType.CLDR, Locale.ROOT); private static final ChronoPrinter<PlainDate> REDUCED_E = ChronoFormatter.ofDatePattern("e", PatternType.CLDR, Locale.ROOT); private static final ChronoPrinter<PlainDate> REDUCED_W_WWE = ChronoFormatter.ofDatePattern("'W'wwe", PatternType.CLDR, Locale.ROOT); private static final ChronoPrinter<PlainDate> REDUCED_W_WW_E = ChronoFormatter.ofDatePattern("'W'ww-e", PatternType.CLDR, Locale.ROOT); private static final ChronoPrinter<PlainDate> REDUCED_DDD = ChronoFormatter.ofDatePattern("DDD", PatternType.CLDR, Locale.ROOT); //~ Konstruktoren ----------------------------------------------------- // package-private DateInterval( Boundary<PlainDate> start, Boundary<PlainDate> end ) { super(start, end); } //~ Methoden ---------------------------------------------------------- /** * <p>Defines a comparator which sorts intervals first * by start boundary and then by length. </p> * * @return Comparator * @since 2.0 */ /*[deutsch] * <p>Definiert ein Vergleichsobjekt, das Intervalle zuerst nach dem * Start und dann nach der Länge sortiert. </p> * * @return Comparator * @since 2.0 */ public static Comparator<ChronoInterval<PlainDate>> comparator() { return COMPARATOR; } /** * <p>Creates a closed interval between given dates. </p> * * @param start date of lower boundary (inclusive) * @param end date of upper boundary (inclusive) * @return new date interval * @throws IllegalArgumentException if start is after end * @since 2.0 */ /*[deutsch] * <p>Erzeugt ein geschlossenes Intervall zwischen den angegebenen * Datumswerten. </p> * * @param start date of lower boundary (inclusive) * @param end date of upper boundary (inclusive) * @return new date interval * @throws IllegalArgumentException if start is after end * @since 2.0 */ public static DateInterval between( PlainDate start, PlainDate end ) { return new DateInterval( Boundary.of(CLOSED, start), Boundary.of(CLOSED, end)); } /** * <p>Creates a closed interval between given dates. </p> * * @param start date of lower boundary (inclusive) * @param end date of upper boundary (inclusive) * @return new date interval * @throws IllegalArgumentException if start is after end * @see #between(PlainDate, PlainDate) * @since 4.11 */ /*[deutsch] * <p>Erzeugt ein geschlossenes Intervall zwischen den angegebenen Datumswerten. </p> * * @param start date of lower boundary (inclusive) * @param end date of upper boundary (inclusive) * @return new date interval * @throws IllegalArgumentException if start is after end * @see #between(PlainDate, PlainDate) * @since 4.11 */ public static DateInterval between( LocalDate start, LocalDate end ) { return DateInterval.between(PlainDate.from(start), PlainDate.from(end)); } /** * <p>Creates an infinite interval since given start date. </p> * * @param start date of lower boundary (inclusive) * @return new date interval * @since 2.0 */ /*[deutsch] * <p>Erzeugt ein unbegrenztes Intervall ab dem angegebenen * Startdatum. </p> * * @param start date of lower boundary (inclusive) * @return new date interval * @since 2.0 */ public static DateInterval since(PlainDate start) { Boundary<PlainDate> future = Boundary.infiniteFuture(); return new DateInterval(Boundary.of(CLOSED, start), future); } /** * <p>Creates an infinite interval since given start date. </p> * * @param start date of lower boundary (inclusive) * @return new date interval * @see #since(PlainDate) * @since 4.11 */ /*[deutsch] * <p>Erzeugt ein unbegrenztes Intervall ab dem angegebenen Startdatum. </p> * * @param start date of lower boundary (inclusive) * @return new date interval * @see #since(PlainDate) * @since 4.11 */ public static DateInterval since(LocalDate start) { return DateInterval.since(PlainDate.from(start)); } /** * <p>Creates an infinite interval until given end date. </p> * * @param end date of upper boundary (inclusive) * @return new date interval * @since 2.0 */ /*[deutsch] * <p>Erzeugt ein unbegrenztes Intervall bis zum angegebenen * Endedatum. </p> * * @param end date of upper boundary (inclusive) * @return new date interval * @since 2.0 */ public static DateInterval until(PlainDate end) { Boundary<PlainDate> past = Boundary.infinitePast(); return new DateInterval(past, Boundary.of(CLOSED, end)); } /** * <p>Creates an infinite interval until given end date. </p> * * @param end date of upper boundary (inclusive) * @return new date interval * @see #until(PlainDate) * @since 4.11 */ /*[deutsch] * <p>Erzeugt ein unbegrenztes Intervall bis zum angegebenen * Endedatum. </p> * * @param end date of upper boundary (inclusive) * @return new date interval * @see #until(PlainDate) * @since 4.11 */ public static DateInterval until(LocalDate end) { return DateInterval.until(PlainDate.from(end)); } /** * <p>Creates a closed interval including only given date. </p> * * @param date single contained date * @return new date interval * @since 2.0 */ /*[deutsch] * <p>Erzeugt ein geschlossenes Intervall, das nur das angegebene * Datum enthält. </p> * * @param date single contained date * @return new date interval * @since 2.0 */ public static DateInterval atomic(PlainDate date) { return between(date, date); } /** * <p>Creates a closed interval including only given date. </p> * * @param date single contained date * @return new date interval * @see #atomic(PlainDate) * @since 4.11 */ /*[deutsch] * <p>Erzeugt ein geschlossenes Intervall, das nur das angegebene * Datum enthält. </p> * * @param date single contained date * @return new date interval * @see #atomic(PlainDate) * @since 4.11 */ public static DateInterval atomic(LocalDate date) { return DateInterval.atomic(PlainDate.from(date)); } /** * <p>Yields the start date. </p> * * @return start date or {@code null} if infinite * @see Boundary#isInfinite() * @since 4.11 */ /*[deutsch] * <p>Liefert das Startdatum. </p> * * @return start date or {@code null} if infinite * @see Boundary#isInfinite() * @since 4.11 */ public PlainDate getStartAsCalendarDate() { return this.getStart().getTemporal(); } /** * <p>Yields the start date. </p> * * @return start date or {@code null} if infinite * @see Boundary#isInfinite() * @since 4.11 */ /*[deutsch] * <p>Liefert das Startdatum. </p> * * @return start date or {@code null} if infinite * @see Boundary#isInfinite() * @since 4.11 */ public LocalDate getStartAsLocalDate() { PlainDate date = this.getStartAsCalendarDate(); return ((date == null) ? null : date.toTemporalAccessor()); } /** * <p>Yields the end date. </p> * * @return end date or {@code null} if infinite * @see Boundary#isInfinite() * @since 4.11 */ /*[deutsch] * <p>Liefert das Endedatum. </p> * * @return end date or {@code null} if infinite * @see Boundary#isInfinite() * @since 4.11 */ public PlainDate getEndAsCalendarDate() { return this.getEnd().getTemporal(); } /** * <p>Yields the end date. </p> * * @return end date or {@code null} if infinite * @see Boundary#isInfinite() * @since 4.11 */ /*[deutsch] * <p>Liefert das Endedatum. </p> * * @return end date or {@code null} if infinite * @see Boundary#isInfinite() * @since 4.11 */ public LocalDate getEndAsLocalDate() { PlainDate date = this.getEndAsCalendarDate(); return ((date == null) ? null : date.toTemporalAccessor()); } /** * <p>Converts this instance to a timestamp interval with * dates from midnight to midnight. </p> * * <p>The resulting interval is half-open if this interval is finite. </p> * * @return timestamp interval (from midnight to midnight) * @since 2.0 */ /*[deutsch] * <p>Wandelt diese Instanz in ein Zeitstempelintervall * mit Datumswerten von Mitternacht zu Mitternacht um. </p> * * <p>Das Ergebnisintervall ist halb-offen, wenn dieses Intervall * endlich ist. </p> * * @return timestamp interval (from midnight to midnight) * @since 2.0 */ public TimestampInterval toFullDays() { Boundary<PlainTimestamp> b1; Boundary<PlainTimestamp> b2; if (this.getStart().isInfinite()) { b1 = Boundary.infinitePast(); } else { PlainDate d1 = this.getStart().getTemporal(); PlainTimestamp t1; if (this.getStart().isOpen()) { t1 = d1.at(PlainTime.midnightAtEndOfDay()); } else { t1 = d1.atStartOfDay(); } b1 = Boundary.of(IntervalEdge.CLOSED, t1); } if (this.getEnd().isInfinite()) { b2 = Boundary.infiniteFuture(); } else { PlainDate d2 = this.getEnd().getTemporal(); PlainTimestamp t2; if (this.getEnd().isOpen()) { t2 = d2.atStartOfDay(); } else { t2 = d2.at(PlainTime.midnightAtEndOfDay()); } b2 = Boundary.of(IntervalEdge.OPEN, t2); } return new TimestampInterval(b1, b2); } /** * <p>Converts this instance to a moment interval with date boundaries mapped * to the midnight cycle in given time zone. </p> * * <p>The resulting interval is half-open if this interval is finite. Note that sometimes * the moments of result intervals can deviate from midnight if midnight does not exist * due to daylight saving effects. The exact behaviour can be controlled by a suitable * transition strategy. </p> * * @param tz timezone * @return global timestamp intervall interpreted in given timezone * @see Timezone#with(TransitionStrategy) * @since 3.22/4.18 * @deprecated Use {@link #inTimezone(TZID)} instead */ /*[deutsch] * <p>Kombiniert dieses Datumsintervall mit der angegebenen * Zeitzone zu einem globalen UTC-Intervall, indem die Momente * den Mitternachtszyklus abbilden. </p> * * <p>Das Ergebnisintervall ist halb-offen, wenn dieses Intervall endlich ist. Hinweis: * Manchmal sind die Momentgrenzen von Mitternacht verschieden, nämlich dann, wenn * wegen Sommerzeitumstellungen Mitternacht nicht vorhanden ist. Das exakte Verhalten * kann durch eine geeignete {@code TransitionStrategy} gesteuert werden. </p> * * @param tz timezone * @return global timestamp intervall interpreted in given timezone * @see Timezone#with(TransitionStrategy) * @since 3.22/4.18 * @deprecated Use {@link #inTimezone(TZID)} instead */ @Deprecated public MomentInterval in(Timezone tz) { return this.toFullDays().in(tz); } /** * <p>Converts this instance to a moment interval with date boundaries mapped * to the midnight cycle in given time zone. </p> * * <p>The resulting interval is half-open if this interval is finite. Note that sometimes * the moments of result intervals can deviate from midnight if midnight does not exist * due to daylight saving effects. </p> * * @param tzid timezone identifier * @return global timestamp intervall interpreted in given timezone * @see GapResolver#NEXT_VALID_TIME * @see OverlapResolver#EARLIER_OFFSET * @since 3.23/4.19 */ /*[deutsch] * <p>Kombiniert dieses Datumsintervall mit der angegebenen * Zeitzone zu einem globalen UTC-Intervall, indem die Momente * den Mitternachtszyklus abbilden. </p> * * <p>Das Ergebnisintervall ist halb-offen, wenn dieses Intervall endlich ist. Hinweis: * Manchmal sind die Momentgrenzen von Mitternacht verschieden, nämlich dann, wenn * wegen Sommerzeitumstellungen Mitternacht nicht vorhanden ist. </p> * * @param tzid timezone identifier * @return global timestamp intervall interpreted in given timezone * @see GapResolver#NEXT_VALID_TIME * @see OverlapResolver#EARLIER_OFFSET * @since 3.23/4.19 */ public MomentInterval inTimezone(TZID tzid) { return this.toFullDays().in( Timezone.of(tzid).with(GapResolver.NEXT_VALID_TIME.and(OverlapResolver.EARLIER_OFFSET))); } /** * <p>Yields the length of this interval in days. </p> * * @return duration in days as long primitive * @throws UnsupportedOperationException if this interval is infinite * @since 2.0 * @see #getDurationInYearsMonthsDays() * @see #getDuration(CalendarUnit[]) getDuration(CalendarUnit...) */ /*[deutsch] * <p>Liefert die Länge dieses Intervalls in Tagen. </p> * * @return duration in days as long primitive * @throws UnsupportedOperationException if this interval is infinite * @since 2.0 * @see #getDurationInYearsMonthsDays() * @see #getDuration(CalendarUnit[]) getDuration(CalendarUnit...) */ public long getLengthInDays() { if (this.isFinite()) { long days = CalendarUnit.DAYS.between( this.getStart().getTemporal(), this.getEnd().getTemporal()); if (this.getStart().isOpen()) { days--; } if (this.getEnd().isClosed()) { days++; } return days; } else { throw new UnsupportedOperationException( "An infinite interval has no finite duration."); } } /** * <p>Yields the length of this interval in years, months and days. </p> * * @return duration in years, months and days * @throws UnsupportedOperationException if this interval is infinite * @since 2.0 * @see #getLengthInDays() * @see #getDuration(CalendarUnit[]) getDuration(CalendarUnit...) */ /*[deutsch] * <p>Liefert die Länge dieses Intervalls in Jahren, Monaten und * Tagen. </p> * * @return duration in years, months and days * @throws UnsupportedOperationException if this interval is infinite * @since 2.0 * @see #getLengthInDays() * @see #getDuration(CalendarUnit[]) getDuration(CalendarUnit...) */ public Duration<CalendarUnit> getDurationInYearsMonthsDays() { PlainDate date = this.getTemporalOfOpenEnd(); boolean max = (date == null); if (max) { // max reached date = this.getEnd().getTemporal(); } Duration<CalendarUnit> result = Duration.inYearsMonthsDays().between( this.getTemporalOfClosedStart(), date); if (max) { return result.plus(1, CalendarUnit.DAYS); } return result; } /** * <p>Yields the length of this interval in given calendrical units. </p> * * @param units calendrical units as calculation base * @return duration in given units * @throws UnsupportedOperationException if this interval is infinite * @since 2.0 * @see #getLengthInDays() * @see #getDurationInYearsMonthsDays() */ /*[deutsch] * <p>Liefert die Länge dieses Intervalls in den angegebenen * kalendarischen Zeiteinheiten. </p> * * @param units calendrical units as calculation base * @return duration in given units * @throws UnsupportedOperationException if this interval is infinite * @since 2.0 * @see #getLengthInDays() * @see #getDurationInYearsMonthsDays() */ public Duration<CalendarUnit> getDuration(CalendarUnit... units) { PlainDate date = this.getTemporalOfOpenEnd(); boolean max = (date == null); if (max) { // max reached date = this.getEnd().getTemporal(); } Duration<CalendarUnit> result = Duration.in(units).between( this.getTemporalOfClosedStart(), date); if (max) { return result.plus(1, CalendarUnit.DAYS); } return result; } /** * <p>Moves this interval along the time axis by given units. </p> * * @param amount amount of units * @param unit time unit for moving * @return moved copy of this interval */ public DateInterval move( long amount, CalendarUnit unit ) { if (amount == 0) { return this; } Boundary<PlainDate> s; Boundary<PlainDate> e; if (this.getStart().isInfinite()) { s = Boundary.infinitePast(); } else { s = Boundary.of( this.getStart().getEdge(), this.getStart().getTemporal().plus(amount, unit)); } if (this.getEnd().isInfinite()) { e = Boundary.infiniteFuture(); } else { e = Boundary.of( this.getEnd().getEdge(), this.getEnd().getTemporal().plus(amount, unit)); } return new DateInterval(s, e); } /** * <p>Obtains a stream iterating over every calendar date of the canonical form of this interval. </p> * * @return daily stream * @throws IllegalStateException if this interval is infinite or if there is no canonical form * @see #toCanonical() * @see #streamDaily(PlainDate, PlainDate) * @since 4.18 */ /*[deutsch] * <p>Erzeugt einen {@code Stream}, der über jedes Kalenderdatum der kanonischen Form * dieses Intervalls geht. </p> * * @return daily stream * @throws IllegalStateException if this interval is infinite or if there is no canonical form * @see #toCanonical() * @see #streamDaily(PlainDate, PlainDate) * @since 4.18 */ public Stream<PlainDate> streamDaily() { if (this.isEmpty()) { return Stream.empty(); } DateInterval interval = this.toCanonical(); PlainDate start = interval.getStartAsCalendarDate(); PlainDate end = interval.getEndAsCalendarDate(); if ((start == null) || (end == null)) { throw new IllegalStateException("Streaming is not supported for infinite intervals."); } return StreamSupport.stream(new DailySpliterator(start, end), false); } /** * <p>Obtains a stream iterating over every calendar date between given interval boundaries. </p> * * <p>This static method avoids the costs of constructing an instance of {@code DateInterval}. </p> * * @param start start boundary - inclusive * @param end end boundary - inclusive * @throws IllegalArgumentException if start is after end * @return daily stream * @see #streamDaily() * @since 4.18 */ /*[deutsch] * <p>Erzeugt einen {@code Stream}, der über jedes Kalenderdatum zwischen den angegebenen * Intervallgrenzen geht. </p> * * <p>Diese statische Methode vermeidet die Kosten der Intervallerzeugung. </p> * * @param start start boundary - inclusive * @param end end boundary - inclusive * @throws IllegalArgumentException if start is after end * @return daily stream * @see #streamDaily() * @since 4.18 */ public static Stream<PlainDate> streamDaily( PlainDate start, PlainDate end ) { long s = start.getDaysSinceEpochUTC(); long e = end.getDaysSinceEpochUTC(); if (s > e) { throw new IllegalArgumentException("Start after end: " + start + "/" + end); } return StreamSupport.stream(new DailySpliterator(start, s, e), false); } /** * <p>Obtains a stream iterating over every calendar date which is the result of addition of given duration * to start until the end of this interval is reached. </p> * * @param duration duration which has to be added to the start multiple times * @throws IllegalArgumentException if the duration is not positive * @throws IllegalStateException if this interval is infinite or if there is no canonical form * @return stream consisting of distinct dates which are the result of adding the duration to the start * @see #toCanonical() * @see #stream(Duration, PlainDate, PlainDate) * @since 4.18 */ /*[deutsch] * <p>Erzeugt einen {@code Stream}, der jeweils ein Kalenderdatum als Vielfaches der Dauer angewandt auf * den Start und bis zum Ende dieses Intervalls geht. </p> * * @param duration duration which has to be added to the start multiple times * @throws IllegalArgumentException if the duration is not positive * @throws IllegalStateException if this interval is infinite or if there is no canonical form * @return stream consisting of distinct dates which are the result of adding the duration to the start * @see #toCanonical() * @see #stream(Duration, PlainDate, PlainDate) * @since 4.18 */ public Stream<PlainDate> stream(Duration<CalendarUnit> duration) { if (this.isEmpty() && duration.isPositive()) { return Stream.empty(); } DateInterval interval = this.toCanonical(); PlainDate start = interval.getStartAsCalendarDate(); PlainDate end = interval.getEndAsCalendarDate(); if ((start == null) || (end == null)) { throw new IllegalStateException("Streaming is not supported for infinite intervals."); } return DateInterval.stream(duration, start, end); } /** * <p>Obtains a stream iterating over every calendar date which is the result of addition of given duration * to start until the end is reached. </p> * * <p>This static method avoids the costs of constructing an instance of {@code DateInterval}. </p> * * @param duration duration which has to be added to the start multiple times * @param start start boundary - inclusive * @param end end boundary - inclusive * @throws IllegalArgumentException if start is after end or if the duration is not positive * @return stream consisting of distinct dates which are the result of adding the duration to the start * @since 4.18 */ /*[deutsch] * <p>Erzeugt einen {@code Stream}, der jeweils ein Kalenderdatum als Vielfaches der Dauer angewandt auf * den Start und bis zum Ende geht. </p> * * <p>Diese statische Methode vermeidet die Kosten der Intervallerzeugung. </p> * * @param duration duration which has to be added to the start multiple times * @param start start boundary - inclusive * @param end end boundary - inclusive * @throws IllegalArgumentException if start is after end or if the duration is not positive * @return stream consisting of distinct dates which are the result of adding the duration to the start * @since 4.18 */ public static Stream<PlainDate> stream( Duration<CalendarUnit> duration, PlainDate start, PlainDate end ) { if (!duration.isPositive()) { throw new IllegalArgumentException("Duration must be positive: " + duration); } long months = 0; long days = 0; for (TimeSpan.Item<CalendarUnit> item : duration.getTotalLength()) { long amount = item.getAmount(); switch (item.getUnit()) { case MILLENNIA: months = Math.addExact(months, Math.multiplyExact(1000 * 12, amount)); break; case CENTURIES: months = Math.addExact(months, Math.multiplyExact(100 * 12, amount)); break; case DECADES: months = Math.addExact(months, Math.multiplyExact(10 * 12, amount)); break; case YEARS: months = Math.addExact(months, Math.multiplyExact(12, amount)); break; case QUARTERS: months = Math.addExact(months, Math.multiplyExact(3, amount)); break; case MONTHS: months = Math.addExact(months, amount); break; case WEEKS: days = Math.addExact(days, Math.multiplyExact(7, amount)); break; case DAYS: days = Math.addExact(days, amount); break; default: throw new UnsupportedOperationException(item.getUnit().name()); } } final long eMonths = months; final long eDays = days; if ((eMonths == 0) && (eDays == 1)) { return DateInterval.streamDaily(start, end); } long s = start.getDaysSinceEpochUTC(); long e = end.getDaysSinceEpochUTC(); if (s > e) { throw new IllegalArgumentException("Start after end: " + start + "/" + end); } long n = 1 + ((e - s) / (Math.addExact(Math.multiplyExact(eMonths, 31), eDays))); // first estimate PlainDate date; long size; do { size = n; long m = Math.multiplyExact(eMonths, n); long d = Math.multiplyExact(eDays, n); date = start.plus(m, CalendarUnit.MONTHS).plus(d, CalendarUnit.DAYS); n++; } while (!date.isAfter(end)); if (size == 1) { return Stream.of(start); // short-cut } return LongStream.range(0, size).mapToObj( index -> start.plus(eMonths * index, CalendarUnit.MONTHS).plus(eDays * index, CalendarUnit.DAYS)); } /** * <p>Obtains a stream iterating over every calendar date which is the result of addition of given duration * in week-based units to start until the end of this interval is reached. </p> * * @param weekBasedYears duration component of week-based years * @param isoWeeks duration component of calendar weeks (from Monday to Sunday) * @param days duration component of ordinary calendar days * @throws IllegalStateException if this interval is infinite or if there is no canonical form * @throws IllegalArgumentException if there is any negative duration component or if there is * no positive duration component at all * @return stream consisting of distinct dates which are the result of adding the duration to the start * @see #toCanonical() * @see Weekcycle#YEARS * @since 4.18 */ /*[deutsch] * <p>Erzeugt einen {@code Stream}, der jeweils ein Kalenderdatum als Vielfaches der Dauer in * wochenbasierten Zeiteinheiten angewandt auf den Start und bis zum Ende dieses Intervalls geht. </p> * * @param weekBasedYears duration component of week-based years * @param isoWeeks duration component of calendar weeks (from Monday to Sunday) * @param days duration component of ordinary calendar days * @throws IllegalStateException if this interval is infinite or if there is no canonical form * @throws IllegalArgumentException if there is any negative duration component or if there is * no positive duration component at all * @return stream consisting of distinct dates which are the result of adding the duration to the start * @see #toCanonical() * @see Weekcycle#YEARS * @since 4.18 */ public Stream<PlainDate> streamWeekBased( int weekBasedYears, int isoWeeks, int days ) { if ((weekBasedYears < 0) || (isoWeeks < 0) || (days < 0)) { throw new IllegalArgumentException("Found illegal negative duration component."); } final long effYears = weekBasedYears; final long effDays = 7L * isoWeeks + days; if ((weekBasedYears == 0) && (effDays == 0)) { throw new IllegalArgumentException("Cannot create stream with empty duration."); } if (this.isEmpty()) { return Stream.empty(); } DateInterval interval = this.toCanonical(); PlainDate start = interval.getStartAsCalendarDate(); PlainDate end = interval.getEndAsCalendarDate(); if ((start == null) || (end == null)) { throw new IllegalStateException("Streaming is not supported for infinite intervals."); } if ((effYears == 0) && (effDays == 1)) { return DateInterval.streamDaily(start, end); } long s = start.getDaysSinceEpochUTC(); long e = end.getDaysSinceEpochUTC(); long n = 1 + ((e - s) / (Math.addExact(Math.multiplyExact(effYears, 371), effDays))); // first estimate PlainDate date; long size; do { size = n; long y = Math.multiplyExact(effYears, n); long d = Math.multiplyExact(effDays, n); date = start.plus(y, Weekcycle.YEARS).plus(d, CalendarUnit.DAYS); n++; } while (!date.isAfter(end)); if (size == 1) { return Stream.of(start); // short-cut } return LongStream.range(0, size).mapToObj( index -> start.plus(effYears * index, Weekcycle.YEARS).plus(effDays * index, CalendarUnit.DAYS)); } /** * <p>Creates a partitioning stream of timestamp intervals where every day of this interval is partitioned * according to given partitioning rule. </p> * * <p>This method enables the easy construction of daily shop opening times or weekly work time schedules. </p> * * @param rule day partition rule * @return stream of timestamp intervals * @see #streamPartitioned(DayPartitionRule, TZID) * @since 4.18 */ /*[deutsch] * <p>Erzeugt einen {@code Stream}, der jeden Tag dieses Intervalls entsprechend der angegebenen Regel * in einzelne Tagesabschnitte zerlegt. </p> * * <p>Hiermit können tägliche Ladenöffnungszeiten oder wöchentliche Arbeitszeitschemata * auf einfache Weise erstellt werden. </p> * * @param rule day partition rule * @return stream of timestamp intervals * @see #streamPartitioned(DayPartitionRule, TZID) * @since 4.18 */ public Stream<TimestampInterval> streamPartitioned(DayPartitionRule rule) { return this.streamDaily().flatMap( date -> rule.getPartition(date).stream().map( partition -> TimestampInterval.between( date.at(partition.getStart().getTemporal()), date.at(partition.getEnd().getTemporal()) ) ) ); } /** * <p>Creates a partitioning stream of moment intervals where every day of this interval is partitioned * according to given partitioning rule. </p> * * <p>This method enables the easy construction of daily shop opening times or weekly work time schedules. </p> * * @param rule day partition rule * @param tzid timezone identifier * @return stream of moment intervals * @throws IllegalArgumentException if given timezone cannot be loaded * @see #streamPartitioned(DayPartitionRule) * @see GapResolver#NEXT_VALID_TIME * @see OverlapResolver#EARLIER_OFFSET * @since 4.19 */ /*[deutsch] * <p>Erzeugt einen {@code Stream}, der jeden Tag dieses Intervalls entsprechend der angegebenen Regel * in einzelne Tagesabschnitte zerlegt und als Momentintervalle darstellt. </p> * * <p>Hiermit können tägliche Ladenöffnungszeiten oder wöchentliche Arbeitszeitschemata * auf einfache Weise erstellt werden. </p> * * @param rule day partition rule * @param tzid timezone identifier * @return stream of moment intervals * @throws IllegalArgumentException if given timezone cannot be loaded * @see #streamPartitioned(DayPartitionRule) * @see GapResolver#NEXT_VALID_TIME * @see OverlapResolver#EARLIER_OFFSET * @since 4.19 */ public Stream<MomentInterval> streamPartitioned( DayPartitionRule rule, TZID tzid ) { final Timezone tz = Timezone.of(tzid).with(GapResolver.NEXT_VALID_TIME.and(OverlapResolver.EARLIER_OFFSET)); return this.streamPartitioned(rule) .map(interval -> interval.in(tz)) .filter(interval -> !interval.isEmpty()); } /** * <p>Prints the canonical form of this interval in given ISO-8601 style. </p> * * @param dateStyle controlling the date format of output * @param infinityStyle controlling the format of infinite boundaries * @return String * @throws IllegalStateException if there is no canonical form * or given infinity style prevents infinite intervals * @see #toCanonical() * @since 4.18 */ /*[deutsch] * <p>Formatiert die kanonische Form dieses Intervalls im angegebenen ISO-8601-Stil. </p> * * @param dateStyle controlling the date format of output * @param infinityStyle controlling the format of infinite boundaries * @return String * @throws IllegalStateException if there is no canonical form * or given infinity style prevents infinite intervals * @see #toCanonical() * @since 4.18 */ public String formatISO( IsoDateStyle dateStyle, InfinityStyle infinityStyle ) { DateInterval interval = this.toCanonical(); StringBuilder buffer = new StringBuilder(21); ChronoPrinter<PlainDate> printer = Iso8601Format.ofDate(dateStyle); if (interval.getStart().isInfinite()) { buffer.append(infinityStyle.displayPast(printer, PlainDate.axis())); } else { printer.print(interval.getStartAsCalendarDate(), buffer); } buffer.append('/'); if (interval.getEnd().isInfinite()) { buffer.append(infinityStyle.displayFuture(printer, PlainDate.axis())); } else { printer.print(interval.getEndAsCalendarDate(), buffer); } return buffer.toString(); } /** * <p>Prints the canonical form of this interval in given reduced ISO-8601 style. </p> * * <p>The term "reduced" means that higher-order elements like the year can be * left out in the end component if it is equal to the value of the start component. Example: </p> * * <pre> * DateInterval interval = * DateInterval.between( * PlainDate.of(2016, 2, 29), * PlainDate.of(2016, 3, 13)); * System.out.println(interval.formatReduced(IsoDateStyle.EXTENDED_CALENDAR_DATE)); * // Output: 2016-02-29/03-13 * </pre> * * @param dateStyle controlling the date format of output * @param infinityStyle controlling the format of infinite boundaries * @return String * @throws IllegalStateException if there is no canonical form * or given infinity style prevents infinite intervals * @see #toCanonical() * @since 4.18 */ /*[deutsch] * <p>Formatiert die kanonische Form dieses Intervalls im angegebenen reduzierten ISO-8601-Stil. </p> * * <p>Der Begriff "reduziert" bedeutet, daß höherwertige Elemente wie das Jahr * in der Endkomponente weggelassen werden, wenn ihr Wert gleich dem Wert der Startkomponente ist. * Beispiel: </p> * * <pre> * DateInterval interval = * DateInterval.between( * PlainDate.of(2016, 2, 29), * PlainDate.of(2016, 3, 13)); * System.out.println(interval.formatReduced(IsoDateStyle.EXTENDED_CALENDAR_DATE)); * // Output: 2016-02-29/03-13 * </pre> * * @param dateStyle controlling the date format of output * @param infinityStyle controlling the format of infinite boundaries * @return String * @throws IllegalStateException if there is no canonical form * or given infinity style prevents infinite intervals * @see #toCanonical() * @since 4.18 */ public String formatReduced( IsoDateStyle dateStyle, InfinityStyle infinityStyle ) { DateInterval interval = this.toCanonical(); StringBuilder buffer = new StringBuilder(21); ChronoPrinter<PlainDate> printer = Iso8601Format.ofDate(dateStyle); PlainDate start = interval.getStartAsCalendarDate(); if (interval.getStart().isInfinite()) { buffer.append(infinityStyle.displayPast(printer, PlainDate.axis())); } else { printer.print(start, buffer); } buffer.append('/'); PlainDate end = interval.getEndAsCalendarDate(); if (interval.isFinite()) { getEndPrinter(dateStyle, start, end).print(end, buffer); } else if (interval.getEnd().isInfinite()) { buffer.append(infinityStyle.displayFuture(printer, PlainDate.axis())); } else { printer.print(end, buffer); } return buffer.toString(); } /** * <p>Interpretes given text as interval using a localized interval pattern. </p> * * <p>If given parser does not contain a reference to a locale then the interval pattern * "{0}/{1}" will be used. </p> * * @param text text to be parsed * @param parser format object for parsing start and end components * @return parsed interval * @throws IndexOutOfBoundsException if given text is empty * @throws ParseException if the text is not parseable * @since 3.9/4.6 * @see #parse(String, ChronoParser, String) * @see net.time4j.format.FormatPatternProvider#getIntervalPattern(Locale) */ /*[deutsch] * <p>Interpretiert den angegebenen Text als Intervall mit Hilfe eines lokalisierten * Intervallmusters. </p> * * <p>Falls der angegebene Formatierer keine Referenz zu einer Sprach- und Ländereinstellung hat, wird * das Intervallmuster "{0}/{1}" verwendet. </p> * * @param text text to be parsed * @param parser format object for parsing start and end components * @return parsed interval * @throws IndexOutOfBoundsException if given text is empty * @throws ParseException if the text is not parseable * @since 3.9/4.6 * @see #parse(String, ChronoParser, String) * @see net.time4j.format.FormatPatternProvider#getIntervalPattern(Locale) */ public static DateInterval parse( String text, ChronoParser<PlainDate> parser ) throws ParseException { return parse(text, parser, IsoInterval.getIntervalPattern(parser)); } /** * <p>Interpretes given text as interval using given interval pattern. </p> * * <p>Starting with version v4.18, it is also possible to use an or-pattern logic. Example: </p> * * <pre> * String multiPattern = "{0} - {1}|since {0}|until {1}"; * ChronoParser<PlainDate> parser = * ChronoFormatter.ofDatePattern("MMMM d / uuuu", PatternType.CLDR, Locale.US); * * DateInterval between = * DateInterval.parse("July 20 / 2015 - December 31 / 2015", parser, multiPattern); * System.out.println(between); // [2015-07-20/2015-12-31] * * DateInterval since = * DateInterval.parse("since July 20 / 2015", parser, multiPattern); * System.out.println(since); // [2015-07-20/+∞) * * DateInterval until = * DateInterval.parse("until December 31 / 2015", parser, multiPattern); * System.out.println(until); // (-∞/2015-12-31] * </pre> * * @param text text to be parsed * @param parser format object for parsing start and end components * @param intervalPattern interval pattern containing placeholders {0} and {1} (for start and end) * @return parsed interval * @throws IndexOutOfBoundsException if given text is empty * @throws ParseException if the text is not parseable * @since 3.9/4.6 */ /*[deutsch] * <p>Interpretiert den angegebenen Text als Intervall mit Hilfe des angegebenen * Intervallmusters. </p> * * <p>Beginnend mit der Version v4.18 ist es auch möglich, eine Oder-Logik im Muster zu verwenden. * Beispiel: </p> * * <pre> * String multiPattern = "{0} - {1}|since {0}|until {1}"; * ChronoParser<PlainDate> parser = * ChronoFormatter.ofDatePattern("MMMM d / uuuu", PatternType.CLDR, Locale.US); * * DateInterval between = * DateInterval.parse("July 20 / 2015 - December 31 / 2015", parser, multiPattern); * System.out.println(between); // [2015-07-20/2015-12-31] * * DateInterval since = * DateInterval.parse("since July 20 / 2015", parser, multiPattern); * System.out.println(since); // [2015-07-20/+∞) * * DateInterval until = * DateInterval.parse("until December 31 / 2015", parser, multiPattern); * System.out.println(until); // (-∞/2015-12-31] * </pre> * * @param text text to be parsed * @param parser format object for parsing start and end components * @param intervalPattern interval pattern containing placeholders {0} and {1} (for start and end) * @return parsed interval * @throws IndexOutOfBoundsException if given text is empty * @throws ParseException if the text is not parseable * @since 3.9/4.6 */ public static DateInterval parse( String text, ChronoParser<PlainDate> parser, String intervalPattern ) throws ParseException { return IntervalParser.parsePattern(text, DateIntervalFactory.INSTANCE, parser, intervalPattern); } /** * <p>Interpretes given text as interval. </p> * * <p>This method can also accept a hyphen as alternative to solidus as separator * between start and end component unless the start component is a period. * Infinity symbols are understood. </p> * * @param text text to be parsed * @param parser format object for parsing start and end components * @param policy strategy for parsing interval boundaries * @return result * @throws ParseException if parsing does not work * @throws IndexOutOfBoundsException if the start position is at end of text or even behind * @since 4.18 */ /*[deutsch] * <p>Interpretiert den angegebenen Text als Intervall. </p> * * <p>Diese Methode kann auch einen Bindestrich als Alternative zum Schrägstrich als Trennzeichen * zwischen Start- und Endkomponente interpretieren, es sei denn, die Startkomponente ist eine Periode. * Unendlichkeitssymbole werden verstanden. </p> * * @param text text to be parsed * @param parser format object for parsing start and end components * @param policy strategy for parsing interval boundaries * @return result * @throws ParseException if parsing does not work * @throws IndexOutOfBoundsException if the start position is at end of text or even behind * @since 4.18 */ public static DateInterval parse( CharSequence text, ChronoParser<PlainDate> parser, BracketPolicy policy ) throws ParseException { ParseLog plog = new ParseLog(); DateInterval interval = IntervalParser.of( DateIntervalFactory.INSTANCE, parser, policy ).parse(text, plog, parser.getAttributes()); if ((interval == null) || plog.isError()) { throw new ParseException(plog.getErrorMessage(), plog.getErrorIndex()); } else if ( (plog.getPosition() < text.length()) && !parser.getAttributes().get(Attributes.TRAILING_CHARACTERS, Boolean.FALSE).booleanValue() ) { throw new ParseException("Trailing characters found: " + text, plog.getPosition()); } else { return interval; } } /** * <p>Interpretes given text as interval. </p> * * <p>Similar to {@link #parse(CharSequence, ChronoParser, char, ChronoParser, BracketPolicy, ParseLog)}. * Since version v3.9/4.6 this method can also accept a hyphen as alternative to solidus as separator * between start and end component unless the start component is a period. </p> * * @param text text to be parsed * @param parser format object for parsing start and end components * @param policy strategy for parsing interval boundaries * @param status parser information (always as new instance) * @return result or {@code null} if parsing does not work * @throws IndexOutOfBoundsException if the start position is at end of text or even behind * @since 2.0 * @deprecated Use one of other parse methods accepting a bracket policy instead */ /*[deutsch] * <p>Interpretiert den angegebenen Text als Intervall. </p> * * <p>Ähnlich wie {@link #parse(CharSequence, ChronoParser, char, ChronoParser, BracketPolicy, ParseLog)}. * Seit der Version v3.9/4.6 kann diese Methode auch einen Bindestrich als Alternative zum Schrägstrich * als Trennzeichen zwischen Start- und Endkomponente, es sei denn, die Startkomponente ist eine Periode. </p> * * @param text text to be parsed * @param parser format object for parsing start and end components * @param policy strategy for parsing interval boundaries * @param status parser information (always as new instance) * @return result or {@code null} if parsing does not work * @throws IndexOutOfBoundsException if the start position is at end of text or even behind * @since 2.0 * @deprecated Use one of other parse methods accepting a bracket policy instead */ @Deprecated public static DateInterval parse( CharSequence text, ChronoParser<PlainDate> parser, BracketPolicy policy, ParseLog status ) { return IntervalParser.of( DateIntervalFactory.INSTANCE, parser, policy ).parse(text, status, parser.getAttributes()); } /** * <p>Interpretes given text as interval. </p> * * <p>This method is mainly intended for parsing technical interval formats similar to ISO-8601 * which are not localized. Infinity symbols are understood. </p> * * @param text text to be parsed * @param startFormat format object for parsing start component * @param separator char separating start and end component * @param endFormat format object for parsing end component * @param policy strategy for parsing interval boundaries * @param status parser information (always as new instance) * @return result or {@code null} if parsing does not work * @throws IndexOutOfBoundsException if the start position is at end of text or even behind * @since 3.9/4.6 */ /*[deutsch] * <p>Interpretiert den angegebenen Text als Intervall. </p> * * <p>Diese Methode ist vor allem für technische nicht-lokalisierte Intervallformate ähnlich * wie in ISO-8601 definiert gedacht. Unendlichkeitssymbole werden verstanden. </p> * * @param text text to be parsed * @param startFormat format object for parsing start component * @param separator char separating start and end component * @param endFormat format object for parsing end component * @param policy strategy for parsing interval boundaries * @param status parser information (always as new instance) * @return result or {@code null} if parsing does not work * @throws IndexOutOfBoundsException if the start position is at end of text or even behind * @since 3.9/4.6 */ public static DateInterval parse( CharSequence text, ChronoParser<PlainDate> startFormat, char separator, ChronoParser<PlainDate> endFormat, BracketPolicy policy, ParseLog status ) { return IntervalParser.of( DateIntervalFactory.INSTANCE, startFormat, endFormat, policy, separator ).parse(text, status, startFormat.getAttributes()); } /** * <p>Interpretes given ISO-conforming text as interval. </p> * * <p>All styles are supported, namely calendar dates, ordinal dates * and week dates, either in basic or in extended format. Mixed date * styles for start and end are not allowed however. Furthermore, one * of start or end can also be represented by a period string. If not * then the end component may exist in an abbreviated form as * documented in ISO-8601-paper leaving out higher-order elements * like the calendar year (which will be overtaken from the start * component instead). </p> * * <p>The infinity symbols "-" (past and future), * "-∞" (past), "+∞" (future), * "-999999999-01-01" und "+999999999-12-31" * can also be parsed as extension although strictly spoken ISO-8601 * does not know or specify infinite intervals. Examples for supported * formats: </p> * * <pre> * System.out.println( * DateInterval.parseISO("2012-01-01/2014-06-20")); * // output: [2012-01-01/2014-06-20] * * System.out.println(DateInterval.parseISO("2012-01-01/08-11")); * // output: [2012-01-01/2012-08-11] * * System.out.println(DateInterval.parseISO("2012-W01-1/W06-4")); * // output: [2012-01-02/2012-02-09] * * System.out.println(DateInterval.parseISO("2012-001/366")); * // output: [2012-01-01/2012-12-31] * * System.out.println(DateInterval.parseISO("2012-001/+∞")); * // output: [2012-01-01/+∞) * </pre> * * <p>This method dynamically creates an appropriate interval format for reduced forms. * If performance is more important then a static fixed formatter might be considered. </p> * * @param text text to be parsed * @return parsed interval * @throws IndexOutOfBoundsException if given text is empty * @throws ParseException if the text is not parseable * @since 2.0 * @see BracketPolicy#SHOW_NEVER */ /*[deutsch] * <p>Interpretiert den angegebenen ISO-konformen Text als Intervall. </p> * * <p>Alle Stile werden unterstützt, nämlich Kalendardatum, * Ordinaldatum und Wochendatum, sowohl im Basisformat als auch im * erweiterten Format. Gemischte Datumsstile von Start und Ende * sind jedoch nicht erlaubt. Außerdem darf eine der beiden * Komponenten Start und Ende als P-String vorliegen. Wenn nicht, dann * darf die Endkomponente auch in einer abgekürzten Schreibweise * angegeben werden, in der weniger präzise Elemente wie das * Kalenderjahr ausgelassen und von der Startkomponente übernommen * werden. </p> * * <p>Die Unendlichkeitssymbole "-" (sowohl Vergangenheit als auch Zukunft), * "-∞" (Vergangenheit), "+∞" (Zukunft), * "-999999999-01-01" und "+999999999-12-31" werden ebenfalls * interpretiert, obwohl ISO-8601 keine unendlichen Intervalle kennt. Beispiele * für unterstützte Formate: </p> * * <pre> * System.out.println( * DateInterval.parseISO("2012-01-01/2014-06-20")); * // Ausgabe: [2012-01-01/2014-06-20] * * System.out.println(DateInterval.parseISO("2012-01-01/08-11")); * // Ausgabe: [2012-01-01/2012-08-11] * * System.out.println(DateInterval.parseISO("2012-W01-1/W06-4")); * // Ausgabe: [2012-01-02/2012-02-09] * * System.out.println(DateInterval.parseISO("2012-001/366")); * // Ausgabe: [2012-01-01/2012-12-31] * * System.out.println(DateInterval.parseISO("2012-001/+∞")); * // output: [2012-01-01/+∞) * </pre> * * <p>Intern wird das notwendige Intervallformat für reduzierte Formen dynamisch ermittelt. Ist * das Antwortzeitverhalten wichtiger, sollte einem statisch initialisierten konstanten Format der * Vorzug gegeben werden. </p> * * @param text text to be parsed * @return parsed interval * @throws IndexOutOfBoundsException if given text is empty * @throws ParseException if the text is not parseable * @since 2.0 * @see BracketPolicy#SHOW_NEVER */ public static DateInterval parseISO(String text) throws ParseException { if (text.isEmpty()) { throw new IndexOutOfBoundsException("Empty text."); } // prescan for format analysis int start = 0; int n = Math.min(text.length(), 48); boolean sameFormat = true; int componentLength = 0; for (int i = 1; i < n; i++) { if (text.charAt(i) == '/') { if (i + 1 == n) { throw new ParseException("Missing end component.", n); } else if ( (text.charAt(0) == 'P') || ((text.charAt(0) == '-') && (i == 1 || text.charAt(1) == '\u221E')) ) { start = i + 1; componentLength = n - i - 1; } else if ( (text.charAt(i + 1) == 'P') || ((text.charAt(i + 1) == '-') && (i + 2 == n)) || ((text.charAt(i + 1) == '+') && (i + 2 < n) && (text.charAt(i + 2) == '\u221E')) ) { componentLength = i; } else { sameFormat = (2 * i + 1 == n); componentLength = i; } break; } } int literals = 0; boolean ordinalStyle = false; boolean weekStyle = false; for (int i = start + 1; i < n; i++) { char c = text.charAt(i); if (c == '-') { literals++; } else if (c == 'W') { weekStyle = true; break; } else if (c == '/') { break; } } boolean extended = (literals > 0); char c = text.charAt(start); componentLength -= 4; if ((c == '+') || (c == '-')) { componentLength -= 2; } if (!weekStyle) { ordinalStyle = ( (literals == 1) || ((literals == 0) && (componentLength == 3))); } // start format ChronoFormatter<PlainDate> startFormat; if (extended) { if (ordinalStyle) { startFormat = Iso8601Format.EXTENDED_ORDINAL_DATE; } else if (weekStyle) { startFormat = Iso8601Format.EXTENDED_WEEK_DATE; } else { startFormat = Iso8601Format.EXTENDED_CALENDAR_DATE; } } else { if (ordinalStyle) { startFormat = Iso8601Format.BASIC_ORDINAL_DATE; } else if (weekStyle) { startFormat = Iso8601Format.BASIC_WEEK_DATE; } else { startFormat = Iso8601Format.BASIC_CALENDAR_DATE; } } // prepare component parsers ChronoFormatter<PlainDate> endFormat = (sameFormat ? startFormat : null); // null means reduced iso format // create interval Parser parser = new Parser(startFormat, endFormat, extended, weekStyle, ordinalStyle); return parser.parse(text); } static ChronoPrinter<PlainDate> getEndPrinter( IsoDateStyle style, PlainDate start, PlainDate end ) { ChronoPrinter<PlainDate> endPrinter = Iso8601Format.ofDate(style); int year1 = start.getYear(); int year2 = end.getYear(); switch (style) { case BASIC_CALENDAR_DATE: if (year1 == year2) { endPrinter = ((start.getMonth() == end.getMonth()) ? REDUCED_DD : REDUCED_MMDD); } break; case BASIC_ORDINAL_DATE: if (year1 == year2) { endPrinter = REDUCED_DDD; } break; case BASIC_WEEK_DATE: year1 = start.getInt(PlainDate.YEAR_OF_WEEKDATE); year2 = end.getInt(PlainDate.YEAR_OF_WEEKDATE); if (year1 == year2) { if (start.getInt(Weekmodel.ISO.weekOfYear()) == end.getInt(Weekmodel.ISO.weekOfYear())) { endPrinter = REDUCED_E; } else { endPrinter = REDUCED_W_WWE; } } break; case EXTENDED_CALENDAR_DATE: if (year1 == year2) { endPrinter = ((start.getMonth() == end.getMonth()) ? REDUCED_DD : REDUCED_MM_DD); } break; case EXTENDED_ORDINAL_DATE: if (year1 == year2) { endPrinter = REDUCED_DDD; } break; case EXTENDED_WEEK_DATE: year1 = start.getInt(PlainDate.YEAR_OF_WEEKDATE); year2 = end.getInt(PlainDate.YEAR_OF_WEEKDATE); if (year1 == year2) { if (start.getInt(Weekmodel.ISO.weekOfYear()) == end.getInt(Weekmodel.ISO.weekOfYear())) { endPrinter = REDUCED_E; } else { endPrinter = REDUCED_W_WW_E; } } break; default: throw new UnsupportedOperationException(style.name()); } return endPrinter; } @Override IntervalFactory<PlainDate, DateInterval> getFactory() { return DateIntervalFactory.INSTANCE; } @Override DateInterval getContext() { return this; } /** * @serialData Uses * <a href="../../../serialized-form.html#net.time4j.range.SPX"> * a dedicated serialization form</a> as proxy. The first byte * contains the type-ID {@code 32} in the six most significant * bits. The next bytes represent the start and the end * boundary. * * Schematic algorithm: * * <pre> int header = 32; header <<= 2; out.writeByte(header); writeBoundary(getStart(), out); writeBoundary(getEnd(), out); private static void writeBoundary( Boundary<?> boundary, ObjectOutput out ) throws IOException { if (boundary.equals(Boundary.infinitePast())) { out.writeByte(1); } else if (boundary.equals(Boundary.infiniteFuture())) { out.writeByte(2); } else { out.writeByte(boundary.isOpen() ? 4 : 0); out.writeObject(boundary.getTemporal()); } } </pre> * * @return replacement object in serialization graph */ private Object writeReplace() { return new SPX(this, SPX.DATE_TYPE); } /** * @serialData Blocks because a serialization proxy is required. * @param in object input stream * @throws InvalidObjectException (always) */ private void readObject(ObjectInputStream in) throws IOException { throw new InvalidObjectException("Serialization proxy required."); } //~ Innere Klassen ---------------------------------------------------- private static class Parser extends IntervalParser<PlainDate, DateInterval> { //~ Instanzvariablen ---------------------------------------------- private final boolean extended; private final boolean weekStyle; private final boolean ordinalStyle; //~ Konstruktoren ------------------------------------------------- Parser( ChronoParser<PlainDate> startFormat, ChronoParser<PlainDate> endFormat, // optional boolean extended, boolean weekStyle, boolean ordinalStyle ) { super(DateIntervalFactory.INSTANCE, startFormat, endFormat, BracketPolicy.SHOW_NEVER, '/'); this.extended = extended; this.weekStyle = weekStyle; this.ordinalStyle = ordinalStyle; } //~ Methoden ------------------------------------------------------ @Override protected PlainDate parseReducedEnd( CharSequence text, PlainDate start, ParseLog lowerLog, ParseLog upperLog, AttributeQuery attrs ) { ChronoFormatter<PlainDate> reducedParser = this.createEndFormat( PlainDate.axis().preformat(start, attrs), lowerLog.getRawValues()); return reducedParser.parse(text, upperLog); } private ChronoFormatter<PlainDate> createEndFormat( ChronoDisplay defaultSupplier, ChronoEntity<?> rawData ) { ChronoFormatter.Builder<PlainDate> builder = ChronoFormatter.setUp(PlainDate.class, Locale.ROOT); ChronoElement<Integer> year = (this.weekStyle ? YEAR_OF_WEEKDATE : YEAR); if (this.extended) { int p = (this.ordinalStyle ? 3 : 5); builder.startSection(Attributes.PROTECTED_CHARACTERS, p); builder.addCustomized( year, NoopPrinter.NOOP, (this.weekStyle ? YearParser.YEAR_OF_WEEKDATE : YearParser.YEAR)); } else { int p = (this.ordinalStyle ? 3 : 4); builder.startSection(Attributes.PROTECTED_CHARACTERS, p); builder.addInteger(year, 4, 9, SignPolicy.SHOW_WHEN_BIG_NUMBER); } builder.endSection(); if (this.weekStyle) { builder.startSection(Attributes.PROTECTED_CHARACTERS, 1); builder.addCustomized( Weekmodel.ISO.weekOfYear(), NoopPrinter.NOOP, this.extended ? FixedNumParser.EXTENDED_WEEK_OF_YEAR : FixedNumParser.BASIC_WEEK_OF_YEAR); builder.endSection(); builder.addFixedNumerical(DAY_OF_WEEK, 1); } else if (this.ordinalStyle) { builder.addFixedInteger(DAY_OF_YEAR, 3); } else { builder.startSection(Attributes.PROTECTED_CHARACTERS, 2); if (this.extended) { builder.addCustomized( MONTH_AS_NUMBER, NoopPrinter.NOOP, FixedNumParser.CALENDAR_MONTH); } else { builder.addFixedInteger(MONTH_AS_NUMBER, 2); } builder.endSection(); builder.addFixedInteger(DAY_OF_MONTH, 2); } for (ChronoElement<?> key : DateIntervalFactory.INSTANCE.stdElements(rawData)) { setDefault(builder, key, defaultSupplier); } return builder.build(); } // wilcard capture private static <V> void setDefault( ChronoFormatter.Builder<PlainDate> builder, ChronoElement<V> element, ChronoDisplay defaultSupplier ) { builder.setDefault(element, defaultSupplier.get(element)); } } private static class DailySpliterator implements Spliterator<PlainDate> { //~ Instanzvariablen ---------------------------------------------- private long startEpoch; // always inclusive private final long endEpoch; // closed range private PlainDate current; //~ Konstruktoren ------------------------------------------------- DailySpliterator( PlainDate start, PlainDate end ) { this(start, start.getDaysSinceEpochUTC(), end.getDaysSinceEpochUTC()); } private DailySpliterator( PlainDate start, long startEpoch, long endEpoch ) { super(); this.startEpoch = startEpoch; this.endEpoch = endEpoch; this.current = ( (startEpoch > endEpoch) ? null : start.with(PlainDate.DAY_OF_WEEK, Weekday.valueOf((int) Math.floorMod(startEpoch + 5, 7) + 1))); } //~ Methoden ------------------------------------------------------ @Override public boolean tryAdvance(Consumer<? super PlainDate> action) { if (this.current == null) { return false; } action.accept(this.current); if (this.startEpoch == this.endEpoch) { this.current = null; } else { this.current = this.current.plus(1, CalendarUnit.DAYS); } this.startEpoch++; return true; } @Override public void forEachRemaining(Consumer<? super PlainDate> action) { if (this.current == null) { return; } PlainDate date = this.current; for (long index = this.startEpoch, n = this.endEpoch; index <= n; index++) { action.accept(date); if (index < n) { date = date.plus(1, CalendarUnit.DAYS); } } this.current = null; this.startEpoch = this.endEpoch + 1; } @Override public Spliterator<PlainDate> trySplit() { if (this.current == null) { return null; // end of traversal } long sum = (this.endEpoch - this.startEpoch - 3); if (sum < 7) { return null; // no split } long dateEpoch = (sum >>> 1) + this.startEpoch; PlainDate date = PlainDate.of(dateEpoch, EpochDays.UTC); int year = date.getYear(); int month = date.getMonth(); final int dom = date.getDayOfMonth(); final boolean allowHalfMonths = (this.estimateSize() < 180) && (dom <= 15); if (allowHalfMonths) { dateEpoch += (15 - dom); // split at midth of month } else { dateEpoch += (GregorianMath.getLengthOfMonth(year, month) - dom); // split at end of month } if (dateEpoch > this.endEpoch - 7) { return null; // no split } Spliterator<PlainDate> split = new DailySpliterator(this.current, this.startEpoch, dateEpoch); Weekday newWD = this.current.getDayOfWeek().roll((int) (dateEpoch - this.startEpoch + 1)); this.startEpoch = dateEpoch + 1; if (allowHalfMonths) { this.current = PlainDate.of(year, month, 16); } else { month++; if (month == 13) { year++; month = 1; } this.current = PlainDate.of(year, month, 1); } this.current = this.current.with(PlainDate.DAY_OF_WEEK, newWD); // trigger day-of-week-optimization return split; } @Override public long estimateSize() { return (this.endEpoch - this.startEpoch + 1); } @Override public int characteristics() { return DISTINCT | IMMUTABLE | NONNULL | ORDERED | SORTED | SIZED | SUBSIZED; } @Override public Comparator<? super PlainDate> getComparator() { return null; } } }