All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.threeten.extra.LocalDateRange Maven / Gradle / Ivy

Go to download

Additional functionality that enhances JSR-310 dates and times in Java SE 8 and later

There is a newer version: 1.8.0
Show newest version
/*
 * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  * Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  * Neither the name of JSR-310 nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.threeten.extra;

import java.io.Serializable;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAdjuster;
import java.util.Comparator;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.joda.convert.FromString;
import org.joda.convert.ToString;

/**
 * A range of local dates.
 * 

* A {@code LocalDateRange} represents a range of dates, from a start date to an end date. * Instances can be constructed from either a half-open or a closed range of dates. * Internally, the class stores the start and end dates, with the start inclusive and the end exclusive. * The end date is always greater than or equal to the start date. *

* The constants {@code LocalDate.MIN} and {@code LocalDate.MAX} can be used * to indicate an unbounded far-past or far-future. Note that there is no difference * between a half-open and a closed range when the end is {@code LocalDate.MAX}. * Empty ranges are allowed. *

* No range can end at {@code LocalDate.MIN} or {@code LocalDate.MIN.plusDays(1)}. * No range can start at {@code LocalDate.MAX} or {@code LocalDate.MAX.minusDays(1)}. * No empty range can exist at {@code LocalDate.MIN} or {@code LocalDate.MAX}. *

* Date ranges are not comparable. To compare the length of two ranges, it is * generally recommended to compare the number of days they contain. * *

Implementation Requirements:

* This class is immutable and thread-safe. *

* This class must be treated as a value type. Do not synchronize, rely on the * identity hash code or use the distinction between equals() and ==. */ public final class LocalDateRange implements Serializable { /** * The day after the MIN date. */ private static final LocalDate MINP1 = LocalDate.MIN.plusDays(1); /** * The day before the MAX date. */ private static final LocalDate MAXM1 = LocalDate.MAX.minusDays(1); /** * A range over the whole time-line. */ public static final LocalDateRange ALL = new LocalDateRange(LocalDate.MIN, LocalDate.MAX); /** * Serialization version. */ private static final long serialVersionUID = 3358656715467L; /** * The start date (inclusive). */ private final LocalDate start; /** * The end date (exclusive). */ private final LocalDate end; //----------------------------------------------------------------------- /** * Obtains a half-open range of dates, including the start and excluding the end. *

* The range includes the start date and excludes the end date, unless the end is {@code LocalDate.MAX}. * The end date must be equal to or after the start date. * This definition permits an empty range located at a specific date. *

* The constants {@code LocalDate.MIN} and {@code LocalDate.MAX} can be used * to indicate an unbounded far-past or far-future. *

* The start inclusive date must not be {@code LocalDate.MAX} or {@code LocalDate.MAX.minusDays(1)}. * The end inclusive date must not be {@code LocalDate.MIN} or {@code LocalDate.MIN.plusDays(1)}. * No empty range can exist at {@code LocalDate.MIN} or {@code LocalDate.MAX}. * * @param startInclusive the inclusive start date, not null * @param endExclusive the exclusive end date, not null * @return the half-open range, not null * @throws DateTimeException if the end is before the start, * or the start date is {@code LocalDate.MAX} or {@code LocalDate.MAX.minusDays(1)}, * or the end date is {@code LocalDate.MIN} or {@code LocalDate.MIN.plusDays(1)} */ public static LocalDateRange of(LocalDate startInclusive, LocalDate endExclusive) { Objects.requireNonNull(startInclusive, "startInclusive"); Objects.requireNonNull(endExclusive, "endExclusive"); return new LocalDateRange(startInclusive, endExclusive); } /** * Obtains a closed range of dates, including the start and end. *

* The range includes the start date and the end date. * The end date must be equal to or after the start date. *

* The constants {@code LocalDate.MIN} and {@code LocalDate.MAX} can be used * to indicate an unbounded far-past or far-future. In addition, an end date of * {@code LocalDate.MAX.minusDays(1)} will also create an unbounded far-future range. *

* The start inclusive date must not be {@code LocalDate.MAX} or {@code LocalDate.MAX.minusDays(1)}. * The end inclusive date must not be {@code LocalDate.MIN}. * * @param startInclusive the inclusive start date, not null * @param endInclusive the inclusive end date, not null * @return the closed range * @throws DateTimeException if the end is before the start, * or the start date is {@code LocalDate.MAX} or {@code LocalDate.MAX.minusDays(1)}, * or the end date is {@code LocalDate.MIN} */ public static LocalDateRange ofClosed(LocalDate startInclusive, LocalDate endInclusive) { Objects.requireNonNull(startInclusive, "startInclusive"); Objects.requireNonNull(endInclusive, "endInclusive"); if (endInclusive.isBefore(startInclusive)) { throw new DateTimeException("Start date must be on or before end date"); } LocalDate end = (endInclusive.equals(LocalDate.MAX) ? LocalDate.MAX : endInclusive.plusDays(1)); return new LocalDateRange(startInclusive, end); } /** * Obtains an instance of {@code LocalDateRange} from the start and a period. *

* The end date is calculated as the start plus the duration. * The period must not be negative. *

* The constant {@code LocalDate.MIN} can be used to indicate an unbounded far-past. *

* The period must not be zero or one day when the start date is {@code LocalDate.MIN}. * * @param startInclusive the inclusive start date, not null * @param period the period from the start to the end, not null * @return the range, not null * @throws DateTimeException if the end is before the start, * or if the period addition cannot be made * @throws ArithmeticException if numeric overflow occurs when adding the period */ public static LocalDateRange of(LocalDate startInclusive, Period period) { Objects.requireNonNull(startInclusive, "startInclusive"); Objects.requireNonNull(period, "period"); if (period.isNegative()) { throw new DateTimeException("Period must not be zero or negative"); } return new LocalDateRange(startInclusive, startInclusive.plus(period)); } /** * Obtains an empty date range located at the specified date. *

* The empty range has zero length and contains no other dates or ranges. * An empty range cannot be located at {@code LocalDate.MIN}, {@code LocalDate.MIN.plusDays(1)}, * {@code LocalDate.MAX} or {@code LocalDate.MAX.minusDays(1)}. * * @param date the date where the empty range is located, not null * @return the empty range, not null * @throws DateTimeException if the date is {@code LocalDate.MIN}, {@code LocalDate.MIN.plusDays(1)}, * {@code LocalDate.MAX} or {@code LocalDate.MAX.minusDays(1)} */ public static LocalDateRange ofEmpty(LocalDate date) { Objects.requireNonNull(date, "date"); return new LocalDateRange(date, date); } /** * Obtains a range that is unbounded at the start and end. * * @return the range, with an unbounded start and unbounded end */ public static LocalDateRange ofUnbounded() { return ALL; } /** * Obtains a range up to, but not including, the specified end date. *

* The range includes all dates from the unbounded start, denoted by {@code LocalDate.MIN}, to the end date. * The end date is exclusive and cannot be {@code LocalDate.MIN} or {@code LocalDate.MIN.plusDays(1)}. * * @param endExclusive the exclusive end date, {@code LocalDate.MAX} treated as unbounded, not null * @return the range, with an unbounded start * @throws DateTimeException if the end date is {@code LocalDate.MIN} or {@code LocalDate.MIN.plusDays(1)} */ public static LocalDateRange ofUnboundedStart(LocalDate endExclusive) { return LocalDateRange.of(LocalDate.MIN, endExclusive); } /** * Obtains a range from and including the specified start date. *

* The range includes all dates from the start date to the unbounded end, denoted by {@code LocalDate.MAX}. * The start date is inclusive and cannot be {@code LocalDate.MAX} or {@code LocalDate.MAX.minusDays(1)}. * * @param startInclusive the inclusive start date, {@code LocalDate.MIN} treated as unbounded, not null * @return the range, with an unbounded end * @throws DateTimeException if the start date is {@code LocalDate.MAX} or {@code LocalDate.MAX.minusDays(1)} */ public static LocalDateRange ofUnboundedEnd(LocalDate startInclusive) { return LocalDateRange.of(startInclusive, LocalDate.MAX); } //----------------------------------------------------------------------- /** * Obtains an instance of {@code LocalDateRange} from a text string such as * {@code 2007-12-03/2007-12-04}, where the end date is exclusive. *

* The string must consist of one of the following three formats: *

    *
  • a representations of an {@link LocalDate}, followed by a forward slash, * followed by a representation of a {@link LocalDate} *
  • a representation of an {@link LocalDate}, followed by a forward slash, * followed by a representation of a {@link Period} *
  • a representation of a {@link Period}, followed by a forward slash, * followed by a representation of an {@link LocalDate} *
* * @param text the text to parse, not null * @return the parsed range, not null * @throws DateTimeParseException if the text cannot be parsed */ @FromString public static LocalDateRange parse(CharSequence text) { Objects.requireNonNull(text, "text"); for (int i = 0; i < text.length(); i++) { if (text.charAt(i) == '/') { char firstChar = text.charAt(0); if (firstChar == 'P' || firstChar == 'p') { // period followed by date Period duration = Period.parse(text.subSequence(0, i)); LocalDate end = LocalDate.parse(text.subSequence(i + 1, text.length())); return LocalDateRange.of(end.minus(duration), end); } else { // date followed by date or period LocalDate start = LocalDate.parse(text.subSequence(0, i)); if (i + 1 < text.length()) { char c = text.charAt(i + 1); if (c == 'P' || c == 'p') { Period duration = Period.parse(text.subSequence(i + 1, text.length())); return LocalDateRange.of(start, start.plus(duration)); } } LocalDate end = LocalDate.parse(text.subSequence(i + 1, text.length())); return LocalDateRange.of(start, end); } } } throw new DateTimeParseException("LocalDateRange cannot be parsed, no forward slash found", text, 0); } //----------------------------------------------------------------------- /** * Constructor. * * @param startInclusive the start date, inclusive, validated not null * @param endExclusive the end date, exclusive, validated not null */ private LocalDateRange(LocalDate startInclusive, LocalDate endExclusive) { if (endExclusive.isBefore(startInclusive)) { throw new DateTimeException("End date must be on or after start date"); } if (startInclusive.equals(MAXM1)) { throw new DateTimeException("Range must not start at LocalDate.MAX.minusDays(1)"); } if (endExclusive.equals(MINP1)) { throw new DateTimeException("Range must not end at LocalDate.MIN.plusDays(1)"); } if (endExclusive.equals(LocalDate.MIN) || startInclusive.equals(LocalDate.MAX)) { throw new DateTimeException("Empty range must not be at LocalDate.MIN or LocalDate.MAX"); } this.start = startInclusive; this.end = endExclusive; } //----------------------------------------------------------------------- /** * Gets the start date of this range, inclusive. *

* This will return {@code LocalDate#MIN} if the range is unbounded at the start. * In this case, the range includes all dates into the far-past. *

* This never returns {@code LocalDate.MAX} or {@code LocalDate.MAX.minusDays(1)}. * * @return the start date */ public LocalDate getStart() { return start; } /** * Gets the end date of this range, exclusive. *

* This will return {@code LocalDate.MAX} if the range is unbounded at the end. * In this case, the range includes all dates into the far-future. *

* This never returns {@code LocalDate.MIN} or {@code LocalDate.MIN.plusDays(1)}. * * @return the end date, exclusive */ public LocalDate getEnd() { return end; } /** * Gets the end date of this range, inclusive. *

* This will return {@code LocalDate.MAX} if the range is unbounded at the end. * In this case, the range includes all dates into the far-future. *

* This returns the date before the end date. *

* This never returns {@code LocalDate.MIN}. * * @return the end date, inclusive */ public LocalDate getEndInclusive() { if (isUnboundedEnd()) { return LocalDate.MAX; } return end.minusDays(1); } //----------------------------------------------------------------------- /** * Checks if the range is empty. *

* An empty range occurs when the start date equals the end date. *

* An empty range is never unbounded. * * @return true if the range is empty */ public boolean isEmpty() { return start.equals(end); } /** * Checks if the start of the range is unbounded. *

* An unbounded range is never empty. * * @return true if start is unbounded */ public boolean isUnboundedStart() { return start.equals(LocalDate.MIN); } /** * Checks if the end of the range is unbounded. *

* An unbounded range is never empty. * * @return true if end is unbounded */ public boolean isUnboundedEnd() { return end.equals(LocalDate.MAX); } //----------------------------------------------------------------------- /** * Returns a copy of this range with the start date adjusted. *

* This returns a new instance with the start date altered. * Since {@code LocalDate} implements {@code TemporalAdjuster} any * local date can simply be passed in. *

* For example, to adjust the start to one week earlier: *

     *  range = range.withStart(date -> date.minus(1, ChronoUnit.WEEKS));
     * 
* * @param adjuster the adjuster to use, not null * @return a copy of this range with the start date adjusted * @throws DateTimeException if the new start date is after the current end date */ public LocalDateRange withStart(TemporalAdjuster adjuster) { return LocalDateRange.of(start.with(adjuster), end); } /** * Returns a copy of this range with the end date adjusted. *

* This returns a new instance with the exclusive end date altered. * Since {@code LocalDate} implements {@code TemporalAdjuster} any * local date can simply be passed in. *

* For example, to adjust the end to one week later: *

     *  range = range.withEnd(date -> date.plus(1, ChronoUnit.WEEKS));
     * 
* * @param adjuster the adjuster to use, not null * @return a copy of this range with the end date adjusted * @throws DateTimeException if the new end date is before the current start date */ public LocalDateRange withEnd(TemporalAdjuster adjuster) { return LocalDateRange.of(start, end.with(adjuster)); } //----------------------------------------------------------------------- /** * Checks if this range contains the specified date. *

* This checks if the specified date is within the bounds of this range. * If this range is empty then this method always returns false. * Else if this range has an unbounded start then {@code contains(LocalDate#MIN)} returns true. * Else if this range has an unbounded end then {@code contains(LocalDate#MAX)} returns true. * * @param date the date to check for, not null * @return true if this range contains the date */ public boolean contains(LocalDate date) { Objects.requireNonNull(date, "date"); return start.compareTo(date) <= 0 && (date.compareTo(end) < 0 || isUnboundedEnd()); } /** * Checks if this range encloses the specified range. *

* This checks if the bounds of the specified range are within the bounds of this range. * An empty range encloses itself. * * @param other the other range to check for, not null * @return true if this range contains all dates in the other range */ public boolean encloses(LocalDateRange other) { Objects.requireNonNull(other, "other"); return start.compareTo(other.start) <= 0 && other.end.compareTo(end) <= 0; } /** * Checks if this range abuts the specified range. *

* The result is true if the end of this range is the start of the other, or vice versa. * An empty range does not abut itself. * * @param other the other range, not null * @return true if this range abuts the other range */ public boolean abuts(LocalDateRange other) { Objects.requireNonNull(other, "other"); return end.equals(other.start) ^ start.equals(other.end); } /** * Checks if this range is connected to the specified range. *

* The result is true if the two ranges have an enclosed range in common, even if that range is empty. * An empty range is connected to itself. *

* This is equivalent to {@code (overlaps(other) || abuts(other))}. * * @param other the other range, not null * @return true if this range is connected to the other range */ public boolean isConnected(LocalDateRange other) { Objects.requireNonNull(other, "other"); return this.equals(other) || (start.compareTo(other.end) <= 0 && other.start.compareTo(end) <= 0); } /** * Checks if this range overlaps the specified range. *

* The result is true if the the two ranges share some part of the time-line. * An empty range overlaps itself. *

* This is equivalent to {@code (isConnected(other) && !abuts(other))}. * * @param other the time range to compare to, null means a zero length range now * @return true if the time ranges overlap */ public boolean overlaps(LocalDateRange other) { Objects.requireNonNull(other, "other"); return other.equals(this) || (start.compareTo(other.end) < 0 && other.start.compareTo(end) < 0); } //----------------------------------------------------------------------- /** * Calculates the range that is the intersection of this range and the specified range. *

* This finds the intersection of two ranges. * This throws an exception if the two ranges are not {@linkplain #isConnected(LocalDateRange) connected}. * * @param other the other range to check for, not null * @return the range that is the intersection of the two ranges * @throws DateTimeException if the ranges do not connect */ public LocalDateRange intersection(LocalDateRange other) { Objects.requireNonNull(other, "other"); if (isConnected(other) == false) { throw new DateTimeException("Ranges do not connect: " + this + " and " + other); } int cmpStart = start.compareTo(other.start); int cmpEnd = end.compareTo(other.end); if (cmpStart >= 0 && cmpEnd <= 0) { return this; } else if (cmpStart <= 0 && cmpEnd >= 0) { return other; } else { LocalDate newStart = (cmpStart >= 0 ? start : other.start); LocalDate newEnd = (cmpEnd <= 0 ? end : other.end); return LocalDateRange.of(newStart, newEnd); } } /** * Calculates the range that is the union of this range and the specified range. *

* This finds the union of two ranges. * This throws an exception if the two ranges are not {@linkplain #isConnected(LocalDateRange) connected}. * * @param other the other range to check for, not null * @return the range that is the union of the two ranges * @throws DateTimeException if the ranges do not connect */ public LocalDateRange union(LocalDateRange other) { Objects.requireNonNull(other, "other"); if (isConnected(other) == false) { throw new DateTimeException("Ranges do not connect: " + this + " and " + other); } int cmpStart = start.compareTo(other.start); int cmpEnd = end.compareTo(other.end); if (cmpStart >= 0 && cmpEnd <= 0) { return other; } else if (cmpStart <= 0 && cmpEnd >= 0) { return this; } else { LocalDate newStart = (cmpStart >= 0 ? other.start : start); LocalDate newEnd = (cmpEnd <= 0 ? other.end : end); return LocalDateRange.of(newStart, newEnd); } } /** * Calculates the smallest range that encloses this range and the specified range. *

* The result of this method will {@linkplain #encloses(LocalDateRange) enclose} * this range and the specified range. * * @param other the other range to check for, not null * @return the range that spans the two ranges */ public LocalDateRange span(LocalDateRange other) { Objects.requireNonNull(other, "other"); int cmpStart = start.compareTo(other.start); int cmpEnd = end.compareTo(other.end); LocalDate newStart = (cmpStart >= 0 ? other.start : start); LocalDate newEnd = (cmpEnd <= 0 ? other.end : end); return LocalDateRange.of(newStart, newEnd); } //----------------------------------------------------------------------- /** * Streams the set of dates included in the range. *

* This returns a stream consisting of each date in the range. * The stream is ordered. * * @return the stream of dates from the start to the end */ public Stream stream() { long count = end.toEpochDay() - start.toEpochDay() + (isUnboundedEnd() ? 1 : 0); Spliterator spliterator = new Spliterators.AbstractSpliterator( count, Spliterator.IMMUTABLE | Spliterator.NONNULL | Spliterator.DISTINCT | Spliterator.ORDERED | Spliterator.SORTED | Spliterator.SIZED | Spliterator.SUBSIZED) { private LocalDate current = start; @Override public boolean tryAdvance(Consumer action) { if (current != null) { if (current.isBefore(end)) { action.accept(current); current = current.plusDays(1); return true; } if (current.equals(LocalDate.MAX)) { action.accept(LocalDate.MAX); current = null; return true; } } return false; } @Override public Comparator getComparator() { return null; } }; return StreamSupport.stream(spliterator, false); } //----------------------------------------------------------------------- /** * Checks if this range is after the specified date. *

* The result is true if every date in this range is after the specified date. * An empty range behaves as though it is a date for comparison purposes. * * @param date the other date to compare to, not null * @return true if the start of this range is after the specified date */ public boolean isAfter(LocalDate date) { return start.compareTo(date) > 0; } /** * Checks if this range is before the specified date. *

* The result is true if every date in this range is before the specified date. * An empty range behaves as though it is a date for comparison purposes. * * @param date the other date to compare to, not null * @return true if the start of this range is before the specified date */ public boolean isBefore(LocalDate date) { return end.compareTo(date) <= 0 && start.compareTo(date) < 0; } //----------------------------------------------------------------------- /** * Checks if this range is after the specified range. *

* The result is true if every date in this range is after every date in the specified range. * An empty range behaves as though it is a date for comparison purposes. * * @param other the other range to compare to, not null * @return true if every date in this range is after every date in the other range */ public boolean isAfter(LocalDateRange other) { return start.compareTo(other.end) >= 0 && !other.equals(this); } /** * Checks if this range is before the specified range. *

* The result is true if every date in this range is before every date in the specified range. * An empty range behaves as though it is a date for comparison purposes. * * @param range the other range to compare to, not null * @return true if every date in this range is before every date in the other range */ public boolean isBefore(LocalDateRange range) { return end.compareTo(range.start) <= 0 && !range.equals(this); } //----------------------------------------------------------------------- /** * Obtains the length of this range in days. *

* This returns the number of days between the start and end dates. * If the range is too large, the length will be {@code Integer.MAX_VALUE}. * Unbounded ranges return {@code Integer.MAX_VALUE}. * * @return the length in days, Integer.MAX_VALUE if unbounded or too large */ public int lengthInDays() { if (isUnboundedStart() || isUnboundedEnd()) { return Integer.MAX_VALUE; } long length = end.toEpochDay() - start.toEpochDay(); return length > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) length; } /** * Obtains the length of this range as a period. *

* This returns the {@link Period} between the start and end dates. * Unbounded ranges throw {@link ArithmeticException}. * * @return the period of the range * @throws ArithmeticException if the calculation exceeds the capacity of {@code Period}, * or the range is unbounded */ public Period toPeriod() { if (isUnboundedStart() || isUnboundedEnd()) { throw new ArithmeticException("Unbounded range cannot be converted to a Period"); } return Period.between(start, end); } //----------------------------------------------------------------------- /** * Checks if this range is equal to another range. *

* Compares this {@code LocalDateRange} with another ensuring that the two dates are the same. * Only objects of type {@code LocalDateRange} are compared, other types return false. * * @param obj the object to check, null returns false * @return true if this is equal to the other range */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof LocalDateRange) { LocalDateRange other = (LocalDateRange) obj; return start.equals(other.start) && end.equals(other.end); } return false; } /** * A hash code for this range. * * @return a suitable hash code */ @Override public int hashCode() { return start.hashCode() ^ end.hashCode(); } //----------------------------------------------------------------------- /** * Outputs this range as a {@code String}, such as {@code 2007-12-03/2007-12-04}. *

* The output will be the ISO-8601 format formed by combining the * {@code toString()} methods of the two dates, separated by a forward slash. * * @return a string representation of this date, not null */ @Override @ToString public String toString() { return start.toString() + '/' + end.toString(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy