/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (IsoRecurrence.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.Duration;
import net.time4j.IsoDateUnit;
import net.time4j.Moment;
import net.time4j.PlainDate;
import net.time4j.PlainTimestamp;
import net.time4j.ZonalDateTime;
import net.time4j.format.expert.ChronoFormatter;
import net.time4j.format.expert.Iso8601Format;
import net.time4j.tz.ZonalOffset;
import java.text.ParseException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static net.time4j.CalendarUnit.*;
import static net.time4j.ClockUnit.*;
import static java.util.Spliterator.*;
/**
* <p>Represents a sequence of recurrent finite intervals as defined by ISO-8601. </p>
*
* @author Meno Hochschild
* @since 3.22/4.18
*/
/*[deutsch]
* <p>Repräsentiert eine Sequenz von endlichen wiederkehrenden Intervallen wie in ISO-8601 definiert. </p>
*
* @author Meno Hochschild
* @since 3.22/4.18
*/
public class IsoRecurrence<I>
implements Iterable<I> {
//~ Statische Felder/Initialisierungen --------------------------------
private static final int INFINITE = -1;
private static final int TYPE_START_END = 0;
private static final int TYPE_START_DURATION = 1;
private static final int TYPE_DURATION_END = 2;
//~ Instanzvariablen --------------------------------------------------
private final int count;
private final int type;
//~ Konstruktoren -----------------------------------------------------
private IsoRecurrence(
int count,
int type
) {
super();
this.count = count;
this.type = type;
}
//~ Methoden ----------------------------------------------------------
/**
* <p>Creates a recurrent sequence of date intervals having given duration. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param start denotes the start of first interval (inclusive)
* @param duration represents the duration of every repeating interval
* @return sequence of recurrent closed date intervals
* @throws IllegalArgumentException if the count is negative or the duration is not positive
*/
/*[deutsch]
* <p>Erzeugt eine Sequenz von wiederkehrenden Datumsintervallen mit der angegebenen Dauer. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param start denotes the start of first interval (inclusive)
* @param duration represents the duration of every repeating interval
* @return sequence of recurrent closed date intervals
* @throws IllegalArgumentException if the count is negative or the duration is not positive
*/
public static IsoRecurrence<DateInterval> of(
int count,
PlainDate start,
Duration<? extends IsoDateUnit> duration
) {
check(count);
if (start == null) {
throw new NullPointerException("Missing start of recurrent interval.");
}
return new RecurrentDateIntervals(count, TYPE_START_DURATION, start, duration);
}
/**
* <p>Creates a recurrent backward sequence of date intervals having given duration. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param duration represents the negative duration of every repeating interval
* @param end denotes the end of first interval (inclusive)
* @return sequence of recurrent closed date intervals
* @throws IllegalArgumentException if the count is negative or the duration is not positive
*/
/*[deutsch]
* <p>Erzeugt eine Sequenz von wiederkehrenden rückwärts laufenden
* Datumsintervallen mit der angegebenen Dauer. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param duration represents the negative duration of every repeating interval
* @param end denotes the end of first interval (inclusive)
* @return sequence of recurrent closed date intervals
* @throws IllegalArgumentException if the count is negative or the duration is not positive
*/
public static IsoRecurrence<DateInterval> of(
int count,
Duration<? extends IsoDateUnit> duration,
PlainDate end
) {
check(count);
if (end == null) {
throw new NullPointerException("Missing end of recurrent interval.");
}
return new RecurrentDateIntervals(count, TYPE_DURATION_END, end, duration);
}
/**
* <p>Creates a recurrent sequence of date intervals having the duration
* of first interval in years, months and days. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param start denotes the start of first interval (inclusive)
* @param end denotes the end of first interval (inclusive)
* @return sequence of recurrent closed date intervals
* @throws IllegalArgumentException if the count is negative or if start is not before end
*/
/*[deutsch]
* <p>Erzeugt eine Sequenz von wiederkehrenden Datumsintervallen mit der Dauer des ersten Intervalls
* in Jahren, Monaten und Tagen. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param start denotes the start of first interval (inclusive)
* @param end denotes the end of first interval (inclusive)
* @return sequence of recurrent closed date intervals
* @throws IllegalArgumentException if the count is negative or if start is not before end
*/
public static IsoRecurrence<DateInterval> of(
int count,
PlainDate start,
PlainDate end
) {
check(count);
if (!end.isAfter(start)) {
throw new IllegalArgumentException("End is not after start.");
}
return new RecurrentDateIntervals(
count,
TYPE_START_END,
start,
Duration.inYearsMonthsDays().between(start, end.plus(1, DAYS)));
}
/**
* <p>Creates a recurrent sequence of timestamp intervals having given duration. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param start denotes the start of first interval (inclusive)
* @param duration represents the duration of every repeating interval
* @return sequence of recurrent half-open plain timestamp intervals
* @throws IllegalArgumentException if the count is negative or the duration is not positive
*/
/*[deutsch]
* <p>Erzeugt eine Sequenz von wiederkehrenden Zeit-Intervallen mit der angegebenen Dauer. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param start denotes the start of first interval (inclusive)
* @param duration represents the duration of every repeating interval
* @return sequence of recurrent half-open plain timestamp intervals
* @throws IllegalArgumentException if the count is negative or the duration is not positive
*/
public static IsoRecurrence<TimestampInterval> of(
int count,
PlainTimestamp start,
Duration<?> duration
) {
check(count);
if (start == null) {
throw new NullPointerException("Missing start of recurrent interval.");
}
return new RecurrentTimestampIntervals(count, TYPE_START_DURATION, start, duration);
}
/**
* <p>Creates a recurrent backward sequence of timestamp intervals having given duration. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param duration represents the negative duration of every repeating interval
* @param end denotes the end of first interval (exclusive)
* @return sequence of recurrent half-open plain timestamp intervals
* @throws IllegalArgumentException if the count is negative or the duration is not positive
*/
/*[deutsch]
* <p>Erzeugt eine Sequenz von wiederkehrenden rückwärts laufenden
* Zeit-Intervallen mit der angegebenen Dauer. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param duration represents the negative duration of every repeating interval
* @param end denotes the end of first interval (exclusive)
* @return sequence of recurrent half-open plain timestamp intervals
* @throws IllegalArgumentException if the count is negative or the duration is not positive
*/
public static IsoRecurrence<TimestampInterval> of(
int count,
Duration<?> duration,
PlainTimestamp end
) {
check(count);
if (end == null) {
throw new NullPointerException("Missing end of recurrent interval.");
}
return new RecurrentTimestampIntervals(count, TYPE_DURATION_END, end, duration);
}
/**
* <p>Creates a recurrent sequence of timestamp intervals having the duration
* of first timestamp interval in years, months, days, hours, minutes, seconds and nanoseconds. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param start denotes the start of first interval (inclusive)
* @param end denotes the end of first interval (exclusive)
* @return sequence of recurrent half-open plain timestamp intervals
* @throws IllegalArgumentException if the count is negative or if start is not before end
*/
/*[deutsch]
* <p>Erzeugt eine Sequenz von wiederkehrenden Zeit-Intervallen mit der Dauer des ersten Intervalls
* in Jahren, Monaten, Tagen, Stunden, Minuten, Sekunden und Nanosekunden. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param start denotes the start of first interval (inclusive)
* @param end denotes the end of first interval (exclusive)
* @return sequence of recurrent half-open plain timestamp intervals
* @throws IllegalArgumentException if the count is negative or if start is not before end
*/
public static IsoRecurrence<TimestampInterval> of(
int count,
PlainTimestamp start,
PlainTimestamp end
) {
check(count);
if (!end.isAfter(start)) {
throw new IllegalArgumentException("End is not after start.");
}
return new RecurrentTimestampIntervals(
count,
TYPE_START_END,
start,
Duration.in(YEARS, MONTHS, DAYS, HOURS, MINUTES, SECONDS, NANOS).between(start, end));
}
/**
* <p>Creates a recurrent sequence of moment intervals having given duration. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param start denotes the start of first interval (inclusive)
* @param duration represents the duration of every repeating interval
* @param offset time zone offset in full minutes
* @return sequence of recurrent half-open plain timestamp intervals
* @throws IllegalArgumentException if the count is negative or the duration is not positive
*/
/*[deutsch]
* <p>Erzeugt eine Sequenz von wiederkehrenden Moment-Intervallen mit der angegebenen Dauer. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param start denotes the start of first interval (inclusive)
* @param duration represents the duration of every repeating interval
* @param offset time zone offset in full minutes
* @return sequence of recurrent half-open plain timestamp intervals
* @throws IllegalArgumentException if the count is negative or the duration is not positive
*/
public static IsoRecurrence<MomentInterval> of(
int count,
Moment start,
Duration<?> duration,
ZonalOffset offset
) {
check(count);
return new RecurrentMomentIntervals(
count, TYPE_START_DURATION, start.toZonalTimestamp(offset), offset, duration);
}
/**
* <p>Creates a recurrent backward sequence of moment intervals having given duration. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param duration represents the negative duration of every repeating interval
* @param end denotes the end of first interval (exclusive)
* @param offset time zone offset in full minutes
* @return sequence of recurrent half-open plain timestamp intervals
* @throws IllegalArgumentException if the count is negative or the duration is not positive
*/
/*[deutsch]
* <p>Erzeugt eine Sequenz von wiederkehrenden rückwärts laufenden
* Moment-Intervallen mit der angegebenen Dauer. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param duration represents the negative duration of every repeating interval
* @param end denotes the end of first interval (exclusive)
* @param offset time zone offset in full minutes
* @return sequence of recurrent half-open plain timestamp intervals
* @throws IllegalArgumentException if the count is negative or the duration is not positive
*/
public static IsoRecurrence<MomentInterval> of(
int count,
Duration<?> duration,
Moment end,
ZonalOffset offset
) {
check(count);
return new RecurrentMomentIntervals(
count, TYPE_DURATION_END, end.toZonalTimestamp(offset), offset, duration);
}
/**
* <p>Creates a recurrent sequence of moment intervals having the duration
* of first timestamp interval in years, months, days, hours, minutes, seconds and nanoseconds. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param start denotes the start of first interval (inclusive)
* @param end denotes the end of first interval (exclusive)
* @param offset time zone offset in full minutes
* @return sequence of recurrent half-open moment intervals
* @throws IllegalArgumentException if the count is negative or if start is not before end
*/
/*[deutsch]
* <p>Erzeugt eine Sequenz von wiederkehrenden Moment-Intervallen mit der Dauer des ersten Intervalls
* in Jahren, Monaten, Tagen, Stunden, Minuten, Sekunden und Nanosekunden. </p>
*
* @param count the count of repeating intervals ({@code >= 0})
* @param start denotes the start of first interval (inclusive)
* @param end denotes the end of first interval (exclusive)
* @param offset time zone offset in full minutes
* @return sequence of recurrent half-open moment intervals
* @throws IllegalArgumentException if the count is negative or if start is not before end
*/
public static IsoRecurrence<MomentInterval> of(
int count,
Moment start,
Moment end,
ZonalOffset offset
) {
check(count);
if (!end.isAfter(start)) {
throw new IllegalArgumentException("End is not after start.");
}
PlainTimestamp s = start.toZonalTimestamp(offset);
PlainTimestamp e = end.toZonalTimestamp(offset);
return new RecurrentMomentIntervals(
count,
TYPE_START_END,
s,
offset,
Duration.in(YEARS, MONTHS, DAYS, HOURS, MINUTES, SECONDS, NANOS).between(s, e));
}
/**
* <p>Obtains the count of recurrent intervals. </p>
*
* @return non-negative count of recurrent intervals or {@code -1} if infinite
*/
/*[deutsch]
* <p>Ermittelt die Anzahl der wiederkehrenden Intervalle. </p>
*
* @return non-negative count of recurrent intervals or {@code -1} if infinite
*/
public int getCount() {
return this.count;
}
/**
* <p>Creates a copy with given modified count. </p>
*
* @param count non-negative count of recurrent intervals
* @return modified copy or this instance if not modified
* @throws IllegalArgumentException if the argument is negative
*/
/*[deutsch]
* <p>Erzeugt eine Kopie mit der angegebenen neuen Anzahl von wiederkehrenden Intervallen. </p>
*
* @param count non-negative count of recurrent intervals
* @return modified copy or this instance if not modified
* @throws IllegalArgumentException if the argument is negative
*/
public IsoRecurrence<I> withCount(int count) {
if (count == this.count) {
return this;
}
check(count);
return this.copyWithCount(count);
}
/**
* <p>Creates a copy with an unlimited count of recurrent intervals. </p>
*
* <p>This method mainly exists to satisfy the requirements of ISO-8601. However:
* <strong>Special care must be taken to avoid infinite loops or streams.</strong></p>
*
* @return modified copy or this instance if not modified
*/
/*[deutsch]
* <p>Erzeugt eine Kopie mit einer unbegrenzten Anzahl von wiederkehrenden Intervallen. </p>
*
* <p>Diese Methode existiert hauptsächlich, um die Anforderungen von ISO-8601 zu erfüllen.
* Aber: <strong>Besondere Vorsicht ist angebracht, um Endlosschleifen zu vermeiden.</strong></p>
*
* @return modified copy or this instance if not modified
*/
public IsoRecurrence<I> withInfiniteCount() {
if (this.count == INFINITE) {
return this;
}
return this.copyWithCount(INFINITE);
}
/**
* <p>Queries if the resulting interval stream describes a backwards running sequence. </p>
*
* @return boolean
*/
/*[deutsch]
* <p>Ermittelt, ob die resultierende Intervallsequenz rückläufig ist. </p>
*
* @return boolean
*/
public boolean isBackwards() {
return (this.type == TYPE_DURATION_END);
}
/**
* <p>Queries if the count of intervals is zero. </p>
*
* @return boolean
*/
/**
* <p>Ermittelt, ob die Anzahl der Intervalle null ist. </p>
*
* @return boolean
*/
public boolean isEmpty() {
return (this.count == 0);
}
/**
* <p>Queries if the count of intervals is unlimited. </p>
*
* @return boolean
*/
/**
* <p>Ermittelt, ob die Anzahl der Intervalle unbegrenzt ist. </p>
*
* @return boolean
*/
public boolean isInfinite() {
return (this.count == INFINITE);
}
/**
* <p>Parses a string like "R5/2016-04-01/2016-04-30" or "R5/2016-04-01/P1M"
* to a sequence of recurrent date intervals. </p>
*
* @param iso canonical representation of recurrent date intervals
* @return parsed sequence of recurrent closed date intervals
* @throws ParseException in any case of inconsistencies
*/
/*[deutsch]
* <p>Interpretiert einen Text wie "R5/2016-04-01/2016-04-30" oder "R5/2016-04-01/P1M"
* als eine Sequenz von wiederkehrenden Datumsintervallen. </p>
*
* @param iso canonical representation of recurrent date intervals
* @return parsed sequence of recurrent closed date intervals
* @throws ParseException in any case of inconsistencies
*/
public static IsoRecurrence<DateInterval> parseDateIntervals(String iso)
throws ParseException {
String[] parts = iso.split("/");
int count = parseCount(parts);
boolean infinite = false;
if (count == INFINITE) {
count = 0;
infinite = true;
}
IsoRecurrence<DateInterval> recurrence;
if (parts[2].charAt(0) == 'P') {
PlainDate start = Iso8601Format.parseDate(parts[1]);
Duration<? extends IsoDateUnit> duration = Duration.parseCalendarPeriod(parts[2]);
recurrence = IsoRecurrence.of(count, start, duration);
} else if (parts[1].charAt(0) == 'P') {
Duration<? extends IsoDateUnit> duration = Duration.parseCalendarPeriod(parts[1]);
PlainDate end = Iso8601Format.parseDate(parts[2]);
recurrence = IsoRecurrence.of(count, duration, end);
} else {
DateInterval interval = DateInterval.parseISO(iso.substring(getFirstSlash(iso) + 1));
PlainDate start = interval.getStart().getTemporal();
PlainDate end = interval.getEnd().getTemporal();
recurrence = IsoRecurrence.of(count, start, end);
}
if (infinite) {
recurrence = recurrence.withInfiniteCount();
}
return recurrence;
}
/**
* <p>Parses a string like "R5/2016-04-01T10:45/2016-04-30T23:59"
* to a sequence of recurrent timestamp intervals. </p>
*
* <p>Supported ISO-formats for timestamp parts are {@link Iso8601Format#BASIC_DATE_TIME}
* and {@link Iso8601Format#EXTENDED_DATE_TIME}. A duration component will be parsed
* by {@link Duration#parsePeriod(String)}. </p>
*
* @param iso canonical representation of recurrent intervals
* @return parsed sequence of recurrent half-open timestamp intervals
* @throws ParseException in any case of inconsistencies
*/
/*[deutsch]
* <p>Interpretiert einen Text wie "R5/2016-04-01T10:45/2016-04-30T23:59"
* als eine Sequenz von wiederkehrenden Zeit-Intervallen. </p>
*
* <p>Unterstützte ISO-Formate für Zeitstempelkomponenten sind
* {@link Iso8601Format#BASIC_DATE_TIME} und {@link Iso8601Format#EXTENDED_DATE_TIME}.
* Eine Dauerkomponente wird mittels {@link Duration#parsePeriod(String)}
* interpretiert. </p>
*
* @param iso canonical representation of recurrent intervals
* @return parsed sequence of recurrent half-open timestamp intervals
* @throws ParseException in any case of inconsistencies
*/
public static IsoRecurrence<TimestampInterval> parseTimestampIntervals(String iso)
throws ParseException {
String[] parts = iso.split("/");
int count = parseCount(parts);
boolean infinite = false;
if (count == INFINITE) {
count = 0;
infinite = true;
}
IsoRecurrence<TimestampInterval> recurrence;
if (parts[2].charAt(0) == 'P') {
boolean extended = isExtendedFormat(parts[1]);
PlainTimestamp start = timestampFormatter(extended).parse(parts[1]);
Duration<?> duration = Duration.parsePeriod(parts[2]);
recurrence = IsoRecurrence.of(count, start, duration);
} else if (parts[1].charAt(0) == 'P') {
Duration<?> duration = Duration.parsePeriod(parts[1]);
boolean extended = isExtendedFormat(parts[2]);
PlainTimestamp end = timestampFormatter(extended).parse(parts[2]);
recurrence = IsoRecurrence.of(count, duration, end);
} else {
TimestampInterval interval = TimestampInterval.parseISO(iso.substring(getFirstSlash(iso) + 1));
PlainTimestamp start = interval.getStart().getTemporal();
PlainTimestamp end = interval.getEnd().getTemporal();
recurrence = IsoRecurrence.of(count, start, end);
}
if (infinite) {
recurrence = recurrence.withInfiniteCount();
}
return recurrence;
}
/**
* <p>Parses a string like "R5/2016-04-01T10:45Z/30T23:59"
* to a sequence of recurrent moment intervals. </p>
*
* <p>Supported ISO-formats for moment parts are {@link Iso8601Format#BASIC_DATE_TIME_OFFSET}
* and {@link Iso8601Format#EXTENDED_DATE_TIME_OFFSET}. A duration component will be parsed
* by {@link Duration#parsePeriod(String)}. </p>
*
* @param iso canonical representation of recurrent intervals
* @return parsed sequence of recurrent half-open moment intervals
* @throws ParseException in any case of inconsistencies
*/
/*[deutsch]
* <p>Interpretiert einen Text wie "R5/2016-04-01T10:45Z/30T23:59"
* als eine Sequenz von wiederkehrenden Moment-Intervallen. </p>
*
* <p>Unterstützte ISO-Formate für Zeitstempelkomponenten sind
* {@link Iso8601Format#BASIC_DATE_TIME_OFFSET} und {@link Iso8601Format#EXTENDED_DATE_TIME_OFFSET}.
* Eine Dauerkomponente wird mittels {@link Duration#parsePeriod(String)} interpretiert. </p>
*
* @param iso canonical representation of recurrent intervals
* @return parsed sequence of recurrent half-open moment intervals
* @throws ParseException in any case of inconsistencies
*/
public static IsoRecurrence<MomentInterval> parseMomentIntervals(String iso)
throws ParseException {
String[] parts = iso.split("/");
int count = parseCount(parts);
boolean infinite = false;
if (count == INFINITE) {
count = 0;
infinite = true;
}
IsoRecurrence<MomentInterval> recurrence;
if (parts[2].charAt(0) == 'P') {
boolean extended = isExtendedFormat(parts[1]);
ZonalDateTime zdt = ZonalDateTime.parse(parts[1], momentFormatter(extended));
Duration<?> duration = Duration.parsePeriod(parts[2]);
recurrence = IsoRecurrence.of(count, zdt.toMoment(), duration, zdt.getOffset());
} else if (parts[1].charAt(0) == 'P') {
Duration<?> duration = Duration.parsePeriod(parts[1]);
boolean extended = isExtendedFormat(parts[2]);
ZonalDateTime zdt = ZonalDateTime.parse(parts[2], momentFormatter(extended));
recurrence = IsoRecurrence.of(count, duration, zdt.toMoment(), zdt.getOffset());
} else {
String remainder = iso.substring(getFirstSlash(iso) + 1);
MomentInterval interval = MomentInterval.parseISO(remainder);
Moment start = interval.getStart().getTemporal();
Moment end = interval.getEnd().getTemporal();
ZonalOffset offset = null;
int signIndex = -1;
for (int i = 1, n = remainder.length(); i < n; i++) {
char c = remainder.charAt(i);
if (c == 'Z') {
offset = ZonalOffset.UTC;
break;
} else if ((c == '-') || (c == '+')) {
signIndex = i;
} else if (c == '/') {
remainder = remainder.substring(signIndex, i);
if (remainder.charAt(3) != ':') {
remainder = remainder.substring(0, 3) + ":" + remainder.substring(3);
}
offset = ZonalOffset.parse(remainder);
break;
}
}
recurrence = IsoRecurrence.of(count, start, end, offset);
}
if (infinite) {
recurrence = recurrence.withInfiniteCount();
}
return recurrence;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj == null) {
return false;
} else if (this.getClass() == obj.getClass()) {
IsoRecurrence<?> that = IsoRecurrence.class.cast(obj);
return ((this.count == that.count) && (this.type == that.type));
} else {
return false;
}
}
@Override
public int hashCode() {
return this.count;
}
/**
* <p>Yields a representation in extended ISO-format. </p>
*
* <p>Examples: </p>
*
* <pre>
* System.out.println(
* IsoRecurrence.of(5, PlainDate.of(2016, 8, 12), Duration.of(3, CalendarUnit.WEEKS));
* // R5/2016-08-12/P3W
* System.out.println(
* IsoRecurrence.of(0, PlainTimestamp.of(2016, 8, 12, 10, 45), PlainTimestamp.of(2016, 8, 12, 12, 0)
* .withInfiniteCount());
* // R/2016-08-12T10:45/2016-08-12T12:00
* </pre>
*
* @return String
*/
/*[deutsch]
* <p>Liefert eine Darstellung im <i>extended</i> ISO-8601-Format. </p>
*
* <p>Beispiele: </p>
*
* <pre>
* System.out.println(
* IsoRecurrence.of(5, PlainDate.of(2016, 8, 12), Duration.of(3, CalendarUnit.WEEKS));
* // R5/2016-08-12/P3W
* System.out.println(
* IsoRecurrence.of(0, PlainTimestamp.of(2016, 8, 12, 10, 45), PlainTimestamp.of(2016, 8, 12, 12, 0)
* .withInfiniteCount());
* // R/2016-08-12T10:45/2016-08-12T12:00
* </pre>
*
* @return String
*/
@Override
public String toString() {
throw new AbstractMethodError();
}
@Override
public Iterator<I> iterator() {
throw new AbstractMethodError();
}
/**
* <p>Obtains an ordered stream of recurrent intervals. </p>
*
* @return Stream
* @since 4.18
* @see Spliterator#DISTINCT
* @see Spliterator#IMMUTABLE
* @see Spliterator#NONNULL
* @see Spliterator#ORDERED
* @see Spliterator#SIZED
* @see Spliterator#SUBSIZED
*/
/*[deutsch]
* <p>Erzeugt einen geordneten {@code Stream} von wiederkehrenden Intervallen. </p>
*
* @return Stream
* @since 4.18
* @see Spliterator#DISTINCT
* @see Spliterator#IMMUTABLE
* @see Spliterator#NONNULL
* @see Spliterator#ORDERED
* @see Spliterator#SIZED
* @see Spliterator#SUBSIZED
*/
public Stream<I> intervalStream() {
long size = (this.isInfinite() ? Long.MAX_VALUE : this.getCount());
int characteristics = DISTINCT | IMMUTABLE | NONNULL | ORDERED | SIZED | SUBSIZED;
Spliterator<I> spliterator = Spliterators.spliterator(this.iterator(), size, characteristics);
return StreamSupport.stream(spliterator, false);
}
IsoRecurrence<I> copyWithCount(int count) {
throw new AbstractMethodError();
}
int getType() {
return this.type;
}
private static void check(int count) {
if (count < 0) {
throw new IllegalArgumentException("Count of recurrent intervals must be postive or zero: " + count);
}
}
private static int parseCount(String[] parts)
throws ParseException {
if (parts.length != 3) {
throw new ParseException("Recurrent interval format must contain exactly 3 chars '/'.", 0);
} else if (parts[0].isEmpty() || parts[0].charAt(0) != 'R') {
throw new ParseException("Recurrent interval format must start with char 'R'.", 0);
}
int total = INFINITE;
for (int i = 1; i < parts[0].length(); i++) {
if (i == 1) {
total = 0;
}
int digit = (parts[0].charAt(i) - '0');
if (digit >= 0 && digit < 9) {
total = total * 10 + digit;
} else {
throw new ParseException("Digit 0-9 is missing.", i);
}
}
return total;
}
private static boolean isExtendedFormat(String iso) {
for (int i = 1, n = iso.length(); i < n; i++) {
char c = iso.charAt(i);
if (c == 'T') {
break;
} else if (c == '-') {
return true;
}
}
return false;
}
private static int getFirstSlash(String iso) {
for (int i = 0, n = iso.length(); i < n; i++) {
if (iso.charAt(i) == '/') {
return i;
}
}
return -1;
}
private static ChronoFormatter<PlainTimestamp> timestampFormatter(boolean extended) {
return (extended ? Iso8601Format.EXTENDED_DATE_TIME : Iso8601Format.BASIC_DATE_TIME);
}
private static ChronoFormatter<Moment> momentFormatter(boolean extended) {
return (extended ? Iso8601Format.EXTENDED_DATE_TIME_OFFSET : Iso8601Format.BASIC_DATE_TIME_OFFSET);
}
//~ Innere Klassen ----------------------------------------------------
private abstract static class ReadOnlyIterator<I, R extends IsoRecurrence<?>>
implements Iterator<I> {
//~ Instanzvariablen ----------------------------------------------
private int index = 0;
private R recurrence;
//~ Konstruktoren -------------------------------------------------
ReadOnlyIterator(R recurrence) {
super();
this.recurrence = recurrence;
}
//~ Methoden ------------------------------------------------------
@Override
public final boolean hasNext() {
int c = this.recurrence.getCount();
return ((c == INFINITE) || (this.index < c));
}
@Override
public final I next() {
int c = this.recurrence.getCount();
if ((c != INFINITE) && (this.index >= c)) {
throw new NoSuchElementException("After end of interval recurrence.");
}
I result = nextInterval();
this.index++;
return result;
}
@Override
public final void remove() {
throw new UnsupportedOperationException();
}
protected abstract I nextInterval();
}
private static class RecurrentDateIntervals
extends IsoRecurrence<DateInterval> {
//~ Statische Felder/Initialisierungen ----------------------------
//~ Instanzvariablen ----------------------------------------------
private final PlainDate ref;
private final Duration<? extends IsoDateUnit> duration;
//~ Konstruktoren -------------------------------------------------
private RecurrentDateIntervals(
int count,
int type,
PlainDate ref,
Duration<? extends IsoDateUnit> duration
) {
super(count, type);
this.ref = ref;
this.duration = duration;
if (!duration.isPositive()) {
throw new IllegalArgumentException("Duration must be positive: " + duration);
}
}
//~ Methoden ----------------------------------------------------------
@Override
public Iterator<DateInterval> iterator() {
return new ReadOnlyIterator<DateInterval, RecurrentDateIntervals>(this) {
private PlainDate current = RecurrentDateIntervals.this.ref;
@Override
protected DateInterval nextInterval() {
PlainDate next;
Boundary<PlainDate> s;
Boundary<PlainDate> e;
if (RecurrentDateIntervals.this.isBackwards()) {
next = this.current.minus(RecurrentDateIntervals.this.duration);
s = Boundary.ofClosed(next.plus(1, DAYS));
e = Boundary.ofClosed(this.current);
} else {
next = this.current.plus(RecurrentDateIntervals.this.duration);
s = Boundary.ofClosed(this.current);
e = Boundary.ofClosed(next.minus(1, DAYS));
}
this.current = next;
return DateIntervalFactory.INSTANCE.between(s, e);
}
};
}
@Override
public boolean equals(Object obj) {
if (super.equals(obj)) {
RecurrentDateIntervals that = (RecurrentDateIntervals) obj;
return (this.ref.equals(that.ref) && this.duration.equals(that.duration));
}
return false;
}
@Override
public int hashCode() {
return super.hashCode() + 31 * this.ref.hashCode() + 37 * this.duration.hashCode();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append('R');
int c = this.getCount();
if (c != INFINITE) {
sb.append(this.getCount());
}
sb.append('/');
switch (this.getType()) {
case TYPE_START_DURATION:
sb.append(this.ref);
sb.append('/');
sb.append(this.duration);
break;
case TYPE_DURATION_END:
sb.append(this.duration);
sb.append('/');
sb.append(this.ref);
break;
case TYPE_START_END:
sb.append(this.ref);
sb.append('/');
sb.append(this.ref.plus(this.duration).minus(1, DAYS));
break;
}
return sb.toString();
}
@Override
IsoRecurrence<DateInterval> copyWithCount(int count) {
return new RecurrentDateIntervals(count, this.getType(), this.ref, this.duration);
}
}
private static class RecurrentTimestampIntervals
extends IsoRecurrence<TimestampInterval> {
//~ Statische Felder/Initialisierungen ----------------------------
//~ Instanzvariablen ----------------------------------------------
private final PlainTimestamp ref;
private final Duration<?> duration;
//~ Konstruktoren -------------------------------------------------
private RecurrentTimestampIntervals(
int count,
int type,
PlainTimestamp ref,
Duration<?> duration
) {
super(count, type);
this.ref = ref;
this.duration = duration;
if (!duration.isPositive()) {
throw new IllegalArgumentException("Duration must be positive: " + duration);
}
}
//~ Methoden ----------------------------------------------------------
@Override
public Iterator<TimestampInterval> iterator() {
return new ReadOnlyIterator<TimestampInterval, RecurrentTimestampIntervals>(this) {
private PlainTimestamp current = RecurrentTimestampIntervals.this.ref;
@Override
protected TimestampInterval nextInterval() {
PlainTimestamp next;
Boundary<PlainTimestamp> s;
Boundary<PlainTimestamp> e;
if (RecurrentTimestampIntervals.this.isBackwards()) {
next = this.current.minus(RecurrentTimestampIntervals.this.duration);
s = Boundary.ofClosed(next);
e = Boundary.ofOpen(this.current);
} else {
next = this.current.plus(RecurrentTimestampIntervals.this.duration);
s = Boundary.ofClosed(this.current);
e = Boundary.ofOpen(next);
}
this.current = next;
return TimestampIntervalFactory.INSTANCE.between(s, e);
}
};
}
@Override
public boolean equals(Object obj) {
if (super.equals(obj)) {
RecurrentTimestampIntervals that = (RecurrentTimestampIntervals) obj;
return (this.ref.equals(that.ref) && this.duration.equals(that.duration));
}
return false;
}
@Override
public int hashCode() {
return super.hashCode() + 31 * this.ref.hashCode() + 37 * this.duration.hashCode();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append('R');
int c = this.getCount();
if (c != INFINITE) {
sb.append(this.getCount());
}
sb.append('/');
switch (this.getType()) {
case TYPE_START_DURATION:
sb.append(this.ref);
sb.append('/');
sb.append(this.duration);
break;
case TYPE_DURATION_END:
sb.append(this.duration);
sb.append('/');
sb.append(this.ref);
break;
case TYPE_START_END:
sb.append(this.ref);
sb.append('/');
sb.append(this.ref.plus(this.duration));
break;
}
return sb.toString();
}
@Override
IsoRecurrence<TimestampInterval> copyWithCount(int count) {
return new RecurrentTimestampIntervals(count, this.getType(), this.ref, this.duration);
}
}
private static class RecurrentMomentIntervals
extends IsoRecurrence<MomentInterval> {
//~ Statische Felder/Initialisierungen ----------------------------
//~ Instanzvariablen ----------------------------------------------
private final PlainTimestamp ref;
private final ZonalOffset offset;
private final Duration<?> duration;
//~ Konstruktoren -------------------------------------------------
private RecurrentMomentIntervals(
int count,
int type,
PlainTimestamp ref,
ZonalOffset offset,
Duration<?> duration
) {
super(count, type);
this.ref = ref;
this.offset = offset;
this.duration = duration;
if (!duration.isPositive()) {
throw new IllegalArgumentException("Duration must be positive: " + duration);
} else if ((offset.getIntegralAmount() % 60 != 0) || (offset.getFractionalAmount() != 0)) {
throw new IllegalArgumentException("Offset with seconds is invalid in ISO-8601: " + offset);
}
}
//~ Methoden ----------------------------------------------------------
@Override
public Iterator<MomentInterval> iterator() {
return new ReadOnlyIterator<MomentInterval, RecurrentMomentIntervals>(this) {
private PlainTimestamp current = RecurrentMomentIntervals.this.ref;
private ZonalOffset offset = RecurrentMomentIntervals.this.offset;
@Override
protected MomentInterval nextInterval() {
PlainTimestamp next;
Boundary<Moment> s;
Boundary<Moment> e;
if (RecurrentMomentIntervals.this.isBackwards()) {
next = this.current.minus(RecurrentMomentIntervals.this.duration);
s = Boundary.ofClosed(next.at(offset));
e = Boundary.ofOpen(this.current.at(offset));
} else {
next = this.current.plus(RecurrentMomentIntervals.this.duration);
s = Boundary.ofClosed(this.current.at(offset));
e = Boundary.ofOpen(next.at(offset));
}
this.current = next;
return MomentIntervalFactory.INSTANCE.between(s, e);
}
};
}
@Override
public boolean equals(Object obj) {
if (super.equals(obj)) {
RecurrentMomentIntervals that = (RecurrentMomentIntervals) obj;
return (
this.ref.equals(that.ref)
&& this.duration.equals(that.duration)
&& this.offset.equals(that.offset));
}
return false;
}
@Override
public int hashCode() {
return super.hashCode()
+ 7 * this.ref.hashCode()
+ 31 * this.offset.hashCode()
+ 37 * this.duration.hashCode();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append('R');
int c = this.getCount();
if (c != INFINITE) {
sb.append(this.getCount());
}
sb.append('/');
switch (this.getType()) {
case TYPE_START_DURATION:
sb.append(this.ref);
sb.append(this.getOffsetAsString());
sb.append('/');
sb.append(this.duration);
break;
case TYPE_DURATION_END:
sb.append(this.duration);
sb.append('/');
sb.append(this.ref);
sb.append(this.getOffsetAsString());
break;
case TYPE_START_END:
sb.append(this.ref);
sb.append(this.getOffsetAsString());
sb.append('/');
sb.append(this.ref.plus(this.duration));
sb.append(this.getOffsetAsString());
break;
}
return sb.toString();
}
@Override
IsoRecurrence<MomentInterval> copyWithCount(int count) {
return new RecurrentMomentIntervals(count, this.getType(), this.ref, this.offset, this.duration);
}
private String getOffsetAsString() {
if ((this.offset.getIntegralAmount() == 0) && (this.offset.getFractionalAmount() == 0)) {
return "Z";
}
return this.offset.toString();
}
}
}