diff --git a/pom.xml b/pom.xml index 036ba5c9..646798b9 100644 --- a/pom.xml +++ b/pom.xml @@ -549,6 +549,12 @@ ${pioneer.version} test + + org.assertj + assertj-core + ${assertj.version} + test + @@ -922,6 +928,7 @@ + 3.24.2 2.2.3 5.10.1 1.9.1 diff --git a/src/main/java/org/threeten/extra/HourMinute.java b/src/main/java/org/threeten/extra/HourMinute.java new file mode 100644 index 00000000..d25e83d2 --- /dev/null +++ b/src/main/java/org/threeten/extra/HourMinute.java @@ -0,0 +1,1090 @@ +/* + * 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 static java.time.temporal.ChronoField.AMPM_OF_DAY; +import static java.time.temporal.ChronoField.CLOCK_HOUR_OF_AMPM; +import static java.time.temporal.ChronoField.CLOCK_HOUR_OF_DAY; +import static java.time.temporal.ChronoField.HOUR_OF_AMPM; +import static java.time.temporal.ChronoField.HOUR_OF_DAY; +import static java.time.temporal.ChronoField.MINUTE_OF_DAY; +import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; +import static java.time.temporal.ChronoUnit.HALF_DAYS; +import static java.time.temporal.ChronoUnit.HOURS; +import static java.time.temporal.ChronoUnit.MINUTES; + +import java.io.Serializable; +import java.time.Clock; +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalAdjuster; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalField; +import java.time.temporal.TemporalQueries; +import java.time.temporal.TemporalQuery; +import java.time.temporal.TemporalUnit; +import java.time.temporal.UnsupportedTemporalTypeException; +import java.time.temporal.ValueRange; +import java.util.Objects; + +import org.joda.convert.FromString; +import org.joda.convert.ToString; + +/** + * An hour-minute, such as {@code 12:31}. + *

+ * This class is similar to {@link LocalTime} but has a precision of minutes. + * Seconds and nanoseconds cannot be represented by this class. + * + *

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 HourMinute + implements Temporal, TemporalAdjuster, Comparable, Serializable { + + /** + * The time of midnight at the start of the day, '00:00'. + */ + public static final HourMinute MIDNIGHT = new HourMinute(0, 0); + + /** + * Serialization version. + */ + private static final long serialVersionUID = -2532872925L; + /** + * Parser. + */ + private static final DateTimeFormatter PARSER = new DateTimeFormatterBuilder() + .appendValue(HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(MINUTE_OF_HOUR, 2) + .toFormatter(); + /** + * Hours per day. + */ + private static final int HOURS_PER_DAY = 24; + /** + * Minutes per hour. + */ + private static final int MINUTES_PER_HOUR = 60; + /** + * Minutes per day. + */ + private static final int MINUTES_PER_DAY = MINUTES_PER_HOUR * HOURS_PER_DAY; + + /** + * The hour-of-day. + */ + private final int hour; + /** + * The minute-of-hour. + */ + private final int minute; + + //----------------------------------------------------------------------- + /** + * Obtains the current hour-minute from the system clock in the default time-zone. + *

+ * This will query the {@link java.time.Clock#systemDefaultZone() system clock} in the default + * time-zone to obtain the current hour-minute. + * The zone and offset will be set based on the time-zone in the clock. + *

+ * Using this method will prevent the ability to use an alternate clock for testing + * because the clock is hard-coded. + * + * @return the current hour-minute using the system clock and default time-zone, not null + */ + public static HourMinute now() { + return now(Clock.systemDefaultZone()); + } + + /** + * Obtains the current hour-minute from the system clock in the specified time-zone. + *

+ * This will query the {@link Clock#system(java.time.ZoneId) system clock} to obtain the current hour-minute. + * Specifying the time-zone avoids dependence on the default time-zone. + *

+ * Using this method will prevent the ability to use an alternate clock for testing + * because the clock is hard-coded. + * + * @param zone the zone ID to use, not null + * @return the current hour-minute using the system clock, not null + */ + public static HourMinute now(ZoneId zone) { + return now(Clock.system(zone)); + } + + /** + * Obtains the current hour-minute from the specified clock. + *

+ * This will query the specified clock to obtain the current hour-minute. + * Using this method allows the use of an alternate clock for testing. + * The alternate clock may be introduced using {@link Clock dependency injection}. + * + * @param clock the clock to use, not null + * @return the current hour-minute, not null + */ + public static HourMinute now(Clock clock) { + final LocalTime now = LocalTime.now(clock); // called once + return HourMinute.of(now.getHour(), now.getMinute()); + } + + //----------------------------------------------------------------------- + /** + * Obtains an instance of {@code HourMinute} from a hour and minute. + * + * @param hour the hour to represent, from 0 to 23 + * @param minute the minute-of-hour to represent, from 1 to 2 + * @return the hour-minute, not null + * @throws DateTimeException if either field value is invalid + */ + public static HourMinute of(int hour, int minute) { + HOUR_OF_DAY.checkValidValue(hour); + MINUTE_OF_HOUR.checkValidValue(minute); + return new HourMinute(hour, minute); + } + + //----------------------------------------------------------------------- + /** + * Obtains an instance of {@code HourMinute} from a temporal object. + *

+ * This obtains a hour-minute based on the specified temporal. + * A {@code TemporalAccessor} represents an arbitrary set of date and time information, + * which this factory converts to an instance of {@code HourMinute}. + *

+ * The conversion extracts the {@link ChronoField#HOUR_OF_DAY HOUR_OF_DAY} and + * {@link ChronoField#MINUTE_OF_HOUR MINUTE_OF_HOUR} fields. + *

+ * This method matches the signature of the functional interface {@link TemporalQuery} + * allowing it to be used in queries via method reference, {@code HourMinute::from}. + * + * @param temporal the temporal object to convert, not null + * @return the hour-minute, not null + * @throws DateTimeException if unable to convert to a {@code HourMinute} + */ + public static HourMinute from(TemporalAccessor temporal) { + if (temporal instanceof HourMinute) { + return (HourMinute) temporal; + } + Objects.requireNonNull(temporal, "temporal"); + try { + // need to use getLong() as JDK Parsed class get() doesn't work properly + int hour = Math.toIntExact(temporal.getLong(HOUR_OF_DAY)); + int minute = Math.toIntExact(temporal.getLong(MINUTE_OF_HOUR)); + return of(hour, minute); + } catch (DateTimeException ex) { + throw new DateTimeException("Unable to obtain HourMinute from TemporalAccessor: " + + temporal + " of type " + temporal.getClass().getName(), ex); + } + } + + //----------------------------------------------------------------------- + /** + * Obtains an instance of {@code HourMinute} from a text string such as {@code 12:31}. + *

+ * The string must represent a valid hour-minute. + * The format must be {@code HH:mm}. + * + * @param text the text to parse such as "12:31", not null + * @return the parsed hour-minute, not null + * @throws DateTimeParseException if the text cannot be parsed + */ + @FromString + public static HourMinute parse(CharSequence text) { + return parse(text, PARSER); + } + + /** + * Obtains an instance of {@code HourMinute} from a text string using a specific formatter. + *

+ * The text is parsed using the formatter, returning a hour-minute. + * + * @param text the text to parse, not null + * @param formatter the formatter to use, not null + * @return the parsed hour-minute, not null + * @throws DateTimeParseException if the text cannot be parsed + */ + public static HourMinute parse(CharSequence text, DateTimeFormatter formatter) { + Objects.requireNonNull(formatter, "formatter"); + return formatter.parse(text, HourMinute::from); + } + + //----------------------------------------------------------------------- + /** + * Constructor. + * + * @param hour the hour to represent, validated from 0 to 23 + * @param minute the minute-of-hour to represent, validated from 0 to 59 + */ + private HourMinute(int hour, int minute) { + this.hour = hour; + this.minute = minute; + } + + /** + * Validates the input. + * + * @return the valid object, not null + */ + private Object readResolve() { + return of(hour, minute); + } + + /** + * Returns a copy of this hour-minute with the new hour and minute, checking + * to see if a new object is in fact required. + * + * @param newYear the hour to represent, validated from 0 to 23 + * @param newMinute the minute-of-hour to represent, validated from 0 to 59 + * @return the hour-minute, not null + */ + private HourMinute with(int newYear, int newMinute) { + if (hour == newYear && minute == newMinute) { + return this; + } + return new HourMinute(newYear, newMinute); + } + + //----------------------------------------------------------------------- + /** + * Checks if the specified field is supported. + *

+ * This checks if this hour-minute can be queried for the specified field. + * If false, then calling the {@link #range(TemporalField) range}, + * {@link #get(TemporalField) get} and {@link #with(TemporalField, long)} + * methods will throw an exception. + *

+ * If the field is a {@link ChronoField} then the query is implemented here. + * The supported fields are: + *

+ * All other {@code ChronoField} instances will return false. + *

+ * If the field is not a {@code ChronoField}, then the result of this method + * is obtained by invoking {@code TemporalField.isSupportedBy(TemporalAccessor)} + * passing {@code this} as the argument. + * Whether the field is supported is determined by the field. + * + * @param field the field to check, null returns false + * @return true if the field is supported on this hour-minute, false if not + */ + @Override + public boolean isSupported(TemporalField field) { + if (field instanceof ChronoField) { + return field == MINUTE_OF_HOUR || + field == MINUTE_OF_DAY || + field == HOUR_OF_AMPM || + field == CLOCK_HOUR_OF_AMPM || + field == HOUR_OF_DAY || + field == CLOCK_HOUR_OF_DAY || + field == AMPM_OF_DAY; + } + return field != null && field.isSupportedBy(this); + } + + /** + * Checks if the specified unit is supported. + *

+ * This checks if the specified unit can be added to, or subtracted from, this hour-minute. + * If false, then calling the {@link #plus(long, TemporalUnit)} and + * {@link #minus(long, TemporalUnit) minus} methods will throw an exception. + *

+ * If the unit is a {@link ChronoUnit} then the query is implemented here. + * The supported units are: + *

+ * All other {@code ChronoUnit} instances will return false. + *

+ * If the unit is not a {@code ChronoUnit}, then the result of this method + * is obtained by invoking {@code TemporalUnit.isSupportedBy(Temporal)} + * passing {@code this} as the argument. + * Whether the unit is supported is determined by the unit. + * + * @param unit the unit to check, null returns false + * @return true if the unit can be added/subtracted, false if not + */ + @Override + public boolean isSupported(TemporalUnit unit) { + if (unit instanceof ChronoUnit) { + return unit == MINUTES || unit == HOURS || unit == HALF_DAYS; + } + return unit != null && unit.isSupportedBy(this); + } + + //----------------------------------------------------------------------- + /** + * Gets the range of valid values for the specified field. + *

+ * The range object expresses the minimum and maximum valid values for a field. + * If it is not possible to return the range, because the field is not supported + * or for some other reason, an exception is thrown. + *

+ * If the field is a {@link ChronoField} then the query is implemented here. + * The {@link #isSupported(TemporalField) supported fields} will return + * appropriate range instances. + * All other {@code ChronoField} instances will throw an {@code UnsupportedTemporalTypeException}. + *

+ * If the field is not a {@code ChronoField}, then the result of this method + * is obtained by invoking {@code TemporalField.rangeRefinedBy(TemporalAccessor)} + * passing {@code this} as the argument. + * Whether the range can be obtained is determined by the field. + * + * @param field the field to query the range for, not null + * @return the range of valid values for the field, not null + * @throws DateTimeException if the range for the field cannot be obtained + * @throws UnsupportedTemporalTypeException if the field is not supported + */ + @Override + public ValueRange range(TemporalField field) { + return Temporal.super.range(field); + } + + /** + * Gets the value of the specified field from this hour-minute as an {@code int}. + *

+ * This queries this hour-minute for the value for the specified field. + * The returned value will always be within the valid range of values for the field. + * If it is not possible to return the value, because the field is not supported + * or for some other reason, an exception is thrown. + *

+ * If the field is a {@link ChronoField} then the query is implemented here. + * The {@link #isSupported(TemporalField) supported fields} will return valid + * values based on this hour-minute,. + * All other {@code ChronoField} instances will throw an {@code UnsupportedTemporalTypeException}. + *

+ * If the field is not a {@code ChronoField}, then the result of this method + * is obtained by invoking {@code TemporalField.getFrom(TemporalAccessor)} + * passing {@code this} as the argument. Whether the value can be obtained, + * and what the value represents, is determined by the field. + * + * @param field the field to get, not null + * @return the value for the field + * @throws DateTimeException if a value for the field cannot be obtained or + * the value is outside the range of valid values for the field + * @throws UnsupportedTemporalTypeException if the field is not supported or + * the range of values exceeds an {@code int} + * @throws ArithmeticException if numeric overflow occurs + */ + @Override + public int get(TemporalField field) { + if (field instanceof ChronoField) { + return get0(field); + } + return Temporal.super.get(field); + } + + /** + * Gets the value of the specified field from this hour-minute as a {@code long}. + *

+ * This queries this hour-minute for the value for the specified field. + * If it is not possible to return the value, because the field is not supported + * or for some other reason, an exception is thrown. + *

+ * If the field is a {@link ChronoField} then the query is implemented here. + * The {@link #isSupported(TemporalField) supported fields} will return valid + * values based on this hour-minute. + * All other {@code ChronoField} instances will throw an {@code UnsupportedTemporalTypeException}. + *

+ * If the field is not a {@code ChronoField}, then the result of this method + * is obtained by invoking {@code TemporalField.getFrom(TemporalAccessor)} + * passing {@code this} as the argument. Whether the value can be obtained, + * and what the value represents, is determined by the field. + * + * @param field the field to get, not null + * @return the value for the field + * @throws DateTimeException if a value for the field cannot be obtained + * @throws UnsupportedTemporalTypeException if the field is not supported + * @throws ArithmeticException if numeric overflow occurs + */ + @Override + public long getLong(TemporalField field) { + if (field instanceof ChronoField) { + return get0(field); + } + return field.getFrom(this); + } + + private int get0(TemporalField field) { + switch ((ChronoField) field) { + case MINUTE_OF_HOUR: + return minute; + case MINUTE_OF_DAY: + return hour * 60 + minute; + case HOUR_OF_AMPM: + return hour % 12; + case CLOCK_HOUR_OF_AMPM: + int ham = hour % 12; + return (ham % 12 == 0 ? 12 : ham); + case HOUR_OF_DAY: + return hour; + case CLOCK_HOUR_OF_DAY: + return (hour == 0 ? 24 : hour); + case AMPM_OF_DAY: + return hour / 12; + default: + throw new UnsupportedTemporalTypeException("Unsupported field: " + field); + } + } + + //----------------------------------------------------------------------- + /** + * Gets the hour field, from 0 to 23. + *

+ * This method returns the hour as an {@code int} from 0 to 23. + * + * @return the hour, from 0 to 23 + */ + public int getHour() { + return hour; + } + + /** + * Gets the minute-of-hour field from 0 to 59. + *

+ * This method returns the minute as an {@code int} from 0 to 59. + * + * @return the minute-of-hour, from 0 to 59 + */ + public int getMinute() { + return minute; + } + + //----------------------------------------------------------------------- + /** + * Returns an adjusted copy of this hour-minute. + *

+ * This returns a {@code HourMinute} based on this one, with the hour-minute adjusted. + * The adjustment takes place using the specified adjuster strategy object. + * Read the documentation of the adjuster to understand what adjustment will be made. + *

+ * The result of this method is obtained by invoking the + * {@link TemporalAdjuster#adjustInto(Temporal)} method on the + * specified adjuster passing {@code this} as the argument. + *

+ * This instance is immutable and unaffected by this method call. + * + * @param adjuster the adjuster to use, not null + * @return a {@code HourMinute} based on {@code this} with the adjustment made, not null + * @throws DateTimeException if the adjustment cannot be made + * @throws ArithmeticException if numeric overflow occurs + */ + @Override + public HourMinute with(TemporalAdjuster adjuster) { + return (HourMinute) adjuster.adjustInto(this); + } + + /** + * Returns a copy of this hour-minute with the specified field set to a new value. + *

+ * This returns a {@code HourMinute} based on this one, with the value + * for the specified field changed. + * This can be used to change any supported field, such as the hour or minute. + * If it is not possible to set the value, because the field is not supported or for + * some other reason, an exception is thrown. + *

+ * If the field is a {@link ChronoField} then the adjustment is implemented here. + * The supported fields behave as follows: + *

+ *

+ * In all cases, if the new value is outside the valid range of values for the field + * then a {@code DateTimeException} will be thrown. + *

+ * All other {@code ChronoField} instances will throw an {@code UnsupportedTemporalTypeException}. + *

+ * If the field is not a {@code ChronoField}, then the result of this method + * is obtained by invoking {@code TemporalField.adjustInto(Temporal, long)} + * passing {@code this} as the argument. In this case, the field determines + * whether and how to adjust the instant. + *

+ * This instance is immutable and unaffected by this method call. + * + * @param field the field to set in the result, not null + * @param newValue the new value of the field in the result + * @return a {@code HourMinute} based on {@code this} with the specified field set, not null + * @throws DateTimeException if the field cannot be set + * @throws UnsupportedTemporalTypeException if the field is not supported + * @throws ArithmeticException if numeric overflow occurs + */ + @Override + public HourMinute with(TemporalField field, long newValue) { + if (field instanceof ChronoField) { + ChronoField f = (ChronoField) field; + f.checkValidValue(newValue); + switch (f) { + case MINUTE_OF_HOUR: + return withMinute((int) newValue); + case MINUTE_OF_DAY: + return plusMinutes(newValue - (hour * MINUTES_PER_HOUR + minute)); + case HOUR_OF_AMPM: + return plusHours(newValue - (hour % 12)); + case CLOCK_HOUR_OF_AMPM: + return plusHours((newValue == 12 ? 0 : newValue) - (hour % 12)); + case HOUR_OF_DAY: + return withHour((int) newValue); + case CLOCK_HOUR_OF_DAY: + return withHour((int) (newValue == 24 ? 0 : newValue)); + case AMPM_OF_DAY: + return plusHours((newValue - (hour / 12)) * 12); + default: + throw new UnsupportedTemporalTypeException("Unsupported field: " + field); + } + } + return field.adjustInto(this, newValue); + } + + //----------------------------------------------------------------------- + /** + * Returns a copy of this {@code HourMinute} with the hour altered. + *

+ * This instance is immutable and unaffected by this method call. + * + * @param hour the hour to set in the returned hour-minute, from 0 to 23 + * @return a {@code HourMinute} based on this hour-minute with the requested hour, not null + * @throws DateTimeException if the hour value is invalid + */ + public HourMinute withHour(int hour) { + HOUR_OF_DAY.checkValidValue(hour); + return with(hour, minute); + } + + /** + * Returns a copy of this {@code HourMinute} with the minute-of-hour altered. + *

+ * This instance is immutable and unaffected by this method call. + * + * @param minute the minute-of-hour to set in the returned hour-minute, from 0 to 59 + * @return a {@code HourMinute} based on this hour-minute with the requested minute, not null + * @throws DateTimeException if the minute-of-hour value is invalid + */ + public HourMinute withMinute(int minute) { + MINUTE_OF_HOUR.checkValidValue(minute); + return with(hour, minute); + } + + //----------------------------------------------------------------------- + /** + * Returns a copy of this hour-minute with the specified amount added. + *

+ * This returns a {@code HourMinute} based on this one, with the specified amount added. + * The amount is typically {@link Period} but may be any other type implementing + * the {@link TemporalAmount} interface. + *

+ * The calculation is delegated to the amount object by calling + * {@link TemporalAmount#addTo(Temporal)}. The amount implementation is free + * to implement the addition in any way it wishes, however it typically + * calls back to {@link #plus(long, TemporalUnit)}. Consult the documentation + * of the amount implementation to determine if it can be successfully added. + *

+ * This instance is immutable and unaffected by this method call. + * + * @param amountToAdd the amount to add, not null + * @return a {@code HourMinute} based on this hour-minute with the addition made, not null + * @throws DateTimeException if the addition cannot be made + * @throws ArithmeticException if numeric overflow occurs + */ + @Override + public HourMinute plus(TemporalAmount amountToAdd) { + return (HourMinute) amountToAdd.addTo(this); + } + + /** + * Returns a copy of this hour-minute with the specified amount added. + *

+ * This returns a {@code HourMinute} based on this one, with the amount + * in terms of the unit added. If it is not possible to add the amount, because the + * unit is not supported or for some other reason, an exception is thrown. + *

+ * If the field is a {@link ChronoUnit} then the addition is implemented here. + * The supported fields behave as follows: + *

+ *

+ * All other {@code ChronoUnit} instances will throw an {@code UnsupportedTemporalTypeException}. + *

+ * If the field is not a {@code ChronoUnit}, then the result of this method + * is obtained by invoking {@code TemporalUnit.addTo(Temporal, long)} + * passing {@code this} as the argument. In this case, the unit determines + * whether and how to perform the addition. + *

+ * This instance is immutable and unaffected by this method call. + * + * @param amountToAdd the amount of the unit to add to the result, may be negative + * @param unit the unit of the amount to add, not null + * @return a {@code HourMinute} based on this hour-minute with the specified amount added, not null + * @throws DateTimeException if the addition cannot be made + * @throws UnsupportedTemporalTypeException if the unit is not supported + * @throws ArithmeticException if numeric overflow occurs + */ + @Override + public HourMinute plus(long amountToAdd, TemporalUnit unit) { + if (unit instanceof ChronoUnit) { + switch ((ChronoUnit) unit) { + case MINUTES: + return plusMinutes(amountToAdd); + case HOURS: + return plusHours(amountToAdd); + case HALF_DAYS: + return plusHours((amountToAdd % 2) * 12); + default: + throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit); + } + } + return unit.addTo(this, amountToAdd); + } + + /** + * Returns a copy of this {@code HourMinute} with the specified number of hours added. + *

+ * This adds the specified number of hours to this time, returning a new time. + * The calculation wraps around midnight. + *

+ * This instance is immutable and unaffected by this method call. + * + * @param hoursToAdd the hours to add, may be negative + * @return an {@code HourMinute} based on this time with the hours added, not null + */ + public HourMinute plusHours(long hoursToAdd) { + if (hoursToAdd == 0) { + return this; + } + int newHour = ((int) (hoursToAdd % HOURS_PER_DAY) + hour + HOURS_PER_DAY) % HOURS_PER_DAY; + return with(newHour, minute); + } + + /** + * Returns a copy of this {@code HourMinute} with the specified number of minutes added. + *

+ * This adds the specified number of minutes to this time, returning a new time. + * The calculation wraps around midnight. + *

+ * This instance is immutable and unaffected by this method call. + * + * @param minutesToAdd the minutes to add, may be negative + * @return an {@code HourMinute} based on this time with the minutes added, not null + */ + public HourMinute plusMinutes(long minutesToAdd) { + if (minutesToAdd == 0) { + return this; + } + int mofd = hour * MINUTES_PER_HOUR + minute; + int newMofd = ((int) (minutesToAdd % MINUTES_PER_DAY) + mofd + MINUTES_PER_DAY) % MINUTES_PER_DAY; + if (mofd == newMofd) { + return this; + } + int newHour = newMofd / MINUTES_PER_HOUR; + int newMinute = newMofd % MINUTES_PER_HOUR; + return with(newHour, newMinute); + } + + //----------------------------------------------------------------------- + /** + * Returns a copy of this hour-minute with the specified amount subtracted. + *

+ * This returns a {@code HourMinute} based on this one, with the specified amount subtracted. + * The amount is typically {@link Period} but may be any other type implementing + * the {@link TemporalAmount} interface. + *

+ * The calculation is delegated to the amount object by calling + * {@link TemporalAmount#subtractFrom(Temporal)}. The amount implementation is free + * to implement the subtraction in any way it wishes, however it typically + * calls back to {@link #minus(long, TemporalUnit)}. Consult the documentation + * of the amount implementation to determine if it can be successfully subtracted. + *

+ * This instance is immutable and unaffected by this method call. + * + * @param amountToSubtract the amount to subtract, not null + * @return a {@code HourMinute} based on this hour-minute with the subtraction made, not null + * @throws DateTimeException if the subtraction cannot be made + * @throws ArithmeticException if numeric overflow occurs + */ + @Override + public HourMinute minus(TemporalAmount amountToSubtract) { + return (HourMinute) amountToSubtract.subtractFrom(this); + } + + /** + * Returns a copy of this hour-minute with the specified amount subtracted. + *

+ * This returns a {@code HourMinute} based on this one, with the amount + * in terms of the unit subtracted. If it is not possible to subtract the amount, + * because the unit is not supported or for some other reason, an exception is thrown. + *

+ * This method is equivalent to {@link #plus(long, TemporalUnit)} with the amount negated. + * See that method for a full description of how addition, and thus subtraction, works. + *

+ * This instance is immutable and unaffected by this method call. + * + * @param amountToSubtract the amount of the unit to subtract from the result, may be negative + * @param unit the unit of the amount to subtract, not null + * @return a {@code HourMinute} based on this hour-minute with the specified amount subtracted, not null + * @throws DateTimeException if the subtraction cannot be made + * @throws UnsupportedTemporalTypeException if the unit is not supported + * @throws ArithmeticException if numeric overflow occurs + */ + @Override + public HourMinute minus(long amountToSubtract, TemporalUnit unit) { + return (amountToSubtract == Long.MIN_VALUE ? plus(Long.MAX_VALUE, unit).plus(1, unit) : plus(-amountToSubtract, unit)); + } + + /** + * Returns a copy of this hour-minute with the specified period in hours subtracted. + *

+ * This instance is immutable and unaffected by this method call. + * + * @param hoursToSubtract the hours to subtract, may be negative + * @return a {@code HourMinute} based on this hour-minute with the hours subtracted, not null + * @throws DateTimeException if the result exceeds the supported range + */ + public HourMinute minusHours(long hoursToSubtract) { + return (hoursToSubtract == Long.MIN_VALUE ? plusHours(Long.MAX_VALUE).plusHours(1) : plusHours(-hoursToSubtract)); + } + + /** + * Returns a copy of this hour-minute with the specified period in minutes subtracted. + *

+ * This instance is immutable and unaffected by this method call. + * + * @param minutesToSubtract the minutes to subtract, may be negative + * @return a {@code HourMinute} based on this hour-minute with the minutes subtracted, not null + * @throws DateTimeException if the result exceeds the supported range + */ + public HourMinute minusMinutes(long minutesToSubtract) { + return (minutesToSubtract == Long.MIN_VALUE ? plusMinutes(Long.MAX_VALUE).plusMinutes(1) : plusMinutes(-minutesToSubtract)); + } + + //----------------------------------------------------------------------- + /** + * Queries this hour-minute using the specified query. + *

+ * This queries this hour-minute using the specified query strategy object. + * The {@code TemporalQuery} object defines the logic to be used to + * obtain the result. Read the documentation of the query to understand + * what the result of this method will be. + *

+ * The result of this method is obtained by invoking the + * {@link TemporalQuery#queryFrom(TemporalAccessor)} method on the + * specified query passing {@code this} as the argument. + * + * @param the type of the result + * @param query the query to invoke, not null + * @return the query result, null may be returned (defined by the query) + * @throws DateTimeException if unable to query (defined by the query) + * @throws ArithmeticException if numeric overflow occurs (defined by the query) + */ + @SuppressWarnings("unchecked") + @Override + public R query(TemporalQuery query) { + if (query == TemporalQueries.localTime()) { + return (R) toLocalTime(); + } else if (query == TemporalQueries.precision()) { + return (R) MINUTES; + } + return Temporal.super.query(query); + } + + /** + * Adjusts the specified temporal object to have this hour-minute. + * Note that if the target has a second or nanosecond field, that is not altered by this method. + *

+ * This returns a temporal object of the same observable type as the input + * with the hour and minute changed to be the same as this. + *

+ * The adjustment is equivalent to using {@link Temporal#with(TemporalField, long)} + * passing {@link ChronoField#MINUTE_OF_DAY} as the field. + * Note that this does not affect any second/nanosecond field in the target. + *

+ * In most cases, it is clearer to reverse the calling pattern by using + * {@link Temporal#with(TemporalAdjuster)}: + *

+     *   // these two lines are equivalent, but the second approach is recommended
+     *   temporal = thisHourMinute.adjustInto(temporal);
+     *   temporal = temporal.with(thisHourMinute);
+     * 
+ *

+ * This instance is immutable and unaffected by this method call. + * + * @param temporal the target object to be adjusted, not null + * @return the adjusted object, not null + * @throws DateTimeException if unable to make the adjustment + * @throws ArithmeticException if numeric overflow occurs + */ + @Override + public Temporal adjustInto(Temporal temporal) { + return temporal.with(MINUTE_OF_DAY, hour * MINUTES_PER_HOUR + minute); + } + + /** + * Calculates the amount of time until another hour-minute in terms of the specified unit. + *

+ * This calculates the amount of time between two {@code HourMinute} + * objects in terms of a single {@code TemporalUnit}. + * The start and end points are {@code this} and the specified hour-minute. + * The result will be negative if the end is before the start. + * The {@code Temporal} passed to this method is converted to a + * {@code HourMinute} using {@link #from(TemporalAccessor)}. + * For example, the period in hours between two hour-minutes can be calculated + * using {@code startHourMinute.until(endHourMinute, YEARS)}. + *

+ * The calculation is implemented in this method for {@link ChronoUnit}. + * The units {@code MINUTES}, {@code HOURS} and {@code HALF_DAYS} are supported. + * Other {@code ChronoUnit} values will throw an exception. + *

+ * If the unit is not a {@code ChronoUnit}, then the result of this method + * is obtained by invoking {@code TemporalUnit.between(Temporal, Temporal)} + * passing {@code this} as the first argument and the converted input temporal + * as the second argument. + *

+ * This instance is immutable and unaffected by this method call. + * + * @param endExclusive the end date, exclusive, which is converted to a {@code HourMinute}, not null + * @param unit the unit to measure the amount in, not null + * @return the amount of time between this hour-minute and the end hour-minute + * @throws DateTimeException if the amount cannot be calculated, or the end + * temporal cannot be converted to a {@code HourMinute} + * @throws UnsupportedTemporalTypeException if the unit is not supported + * @throws ArithmeticException if numeric overflow occurs + */ + @Override + public long until(Temporal endExclusive, TemporalUnit unit) { + HourMinute end = HourMinute.from(endExclusive); + long minutesUntil = (end.hour * MINUTES_PER_HOUR + end.minute) - (hour * MINUTES_PER_HOUR + minute); // no overflow + if (unit instanceof ChronoUnit) { + switch ((ChronoUnit) unit) { + case MINUTES: + return minutesUntil; + case HOURS: + return minutesUntil / MINUTES_PER_HOUR; + case HALF_DAYS: + return minutesUntil / (12 * MINUTES_PER_HOUR); + default: + throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit); + } + } + return unit.between(this, end); + } + + /** + * Formats this hour-minute using the specified formatter. + *

+ * This hour-minute will be passed to the formatter to produce a string. + * + * @param formatter the formatter to use, not null + * @return the formatted hour-minute string, not null + * @throws DateTimeException if an error occurs during printing + */ + public String format(DateTimeFormatter formatter) { + Objects.requireNonNull(formatter, "formatter"); + return formatter.format(this); + } + + //----------------------------------------------------------------------- + /** + * Combines this time with a date to create a {@code LocalDateTime}. + *

+ * This returns a {@code LocalDateTime} formed from this time at the specified date. + * All possible combinations of date and time are valid. + * + * @param date the date to combine with, not null + * @return the local date-time formed from this time and the specified date, not null + */ + public LocalDateTime atDate(LocalDate date) { + return LocalDateTime.of(date, toLocalTime()); + } + + /** + * Combines this time with an offset to create an {@code OffsetTime}. + *

+ * This returns an {@code OffsetTime} formed from this time at the specified offset. + * All possible combinations of time and offset are valid. + * + * @param offset the offset to combine with, not null + * @return the offset time formed from this time and the specified offset, not null + */ + public OffsetTime atOffset(ZoneOffset offset) { + return OffsetTime.of(toLocalTime(), offset); + } + + //----------------------------------------------------------------------- + /** + * Returns the equivalent {@code LocalTime}. + *

+ * This returns a {@code LocalTime} formed from this hour and minute. + * + * @return the equivalent local time, not null + */ + public LocalTime toLocalTime() { + return LocalTime.of(hour, minute); + } + + //------------------------------------------------------------------------- + /** + * Compares this hour-minute to another + *

+ * The comparison is based first on the value of the hour, then on the value of the minute. + * It is "consistent with equals", as defined by {@link Comparable}. + * + * @param other the other hour-minute to compare to, not null + * @return the comparator value, negative if less, positive if greater + */ + @Override + public int compareTo(HourMinute other) { + int cmp = (hour - other.hour); + if (cmp == 0) { + cmp = (minute - other.minute); + } + return cmp; + } + + /** + * Is this hour-minute after the specified hour-minute. + * + * @param other the other hour-minute to compare to, not null + * @return true if this is after the specified hour-minute + */ + public boolean isAfter(HourMinute other) { + return compareTo(other) > 0; + } + + /** + * Is this hour-minute before the specified hour-minute. + * + * @param other the other hour-minute to compare to, not null + * @return true if this point is before the specified hour-minute + */ + public boolean isBefore(HourMinute other) { + return compareTo(other) < 0; + } + + //----------------------------------------------------------------------- + /** + * Checks if this hour-minute is equal to another hour-minute. + *

+ * The comparison is based on the time-line position of the hour-minute. + * + * @param obj the object to check, null returns false + * @return true if this is equal to the other hour-minute + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof HourMinute) { + HourMinute other = (HourMinute) obj; + return hour == other.hour && minute == other.minute; + } + return false; + } + + /** + * A hash code for this hour-minute. + * + * @return a suitable hash code + */ + @Override + public int hashCode() { + return hour * MINUTES_PER_HOUR + minute; + } + + //----------------------------------------------------------------------- + /** + * Outputs this hour-minute as a {@code String}, such as {@code 12:31}. + * + * @return a string representation of this hour-minute, not null + */ + @Override + @ToString + public String toString() { + return new StringBuilder(5) + .append(hour < 10 ? "0" : "").append(hour) + .append(minute < 10 ? ":0" : ":").append(minute) + .toString(); + } + +} diff --git a/src/test/java/org/threeten/extra/TestHourMinute.java b/src/test/java/org/threeten/extra/TestHourMinute.java new file mode 100644 index 00000000..37803a02 --- /dev/null +++ b/src/test/java/org/threeten/extra/TestHourMinute.java @@ -0,0 +1,715 @@ +/* + * 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 static java.time.temporal.ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH; +import static java.time.temporal.ChronoField.ALIGNED_DAY_OF_WEEK_IN_YEAR; +import static java.time.temporal.ChronoField.ALIGNED_WEEK_OF_MONTH; +import static java.time.temporal.ChronoField.ALIGNED_WEEK_OF_YEAR; +import static java.time.temporal.ChronoField.AMPM_OF_DAY; +import static java.time.temporal.ChronoField.CLOCK_HOUR_OF_AMPM; +import static java.time.temporal.ChronoField.CLOCK_HOUR_OF_DAY; +import static java.time.temporal.ChronoField.DAY_OF_MONTH; +import static java.time.temporal.ChronoField.DAY_OF_WEEK; +import static java.time.temporal.ChronoField.DAY_OF_YEAR; +import static java.time.temporal.ChronoField.EPOCH_DAY; +import static java.time.temporal.ChronoField.ERA; +import static java.time.temporal.ChronoField.HOUR_OF_AMPM; +import static java.time.temporal.ChronoField.HOUR_OF_DAY; +import static java.time.temporal.ChronoField.INSTANT_SECONDS; +import static java.time.temporal.ChronoField.MICRO_OF_DAY; +import static java.time.temporal.ChronoField.MICRO_OF_SECOND; +import static java.time.temporal.ChronoField.MILLI_OF_DAY; +import static java.time.temporal.ChronoField.MILLI_OF_SECOND; +import static java.time.temporal.ChronoField.MINUTE_OF_DAY; +import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; +import static java.time.temporal.ChronoField.MONTH_OF_YEAR; +import static java.time.temporal.ChronoField.NANO_OF_DAY; +import static java.time.temporal.ChronoField.NANO_OF_SECOND; +import static java.time.temporal.ChronoField.OFFSET_SECONDS; +import static java.time.temporal.ChronoField.PROLEPTIC_MONTH; +import static java.time.temporal.ChronoField.SECOND_OF_DAY; +import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; +import static java.time.temporal.ChronoField.YEAR; +import static java.time.temporal.ChronoField.YEAR_OF_ERA; +import static java.time.temporal.ChronoUnit.CENTURIES; +import static java.time.temporal.ChronoUnit.DAYS; +import static java.time.temporal.ChronoUnit.DECADES; +import static java.time.temporal.ChronoUnit.FOREVER; +import static java.time.temporal.ChronoUnit.HALF_DAYS; +import static java.time.temporal.ChronoUnit.HOURS; +import static java.time.temporal.ChronoUnit.MICROS; +import static java.time.temporal.ChronoUnit.MILLENNIA; +import static java.time.temporal.ChronoUnit.MILLIS; +import static java.time.temporal.ChronoUnit.MINUTES; +import static java.time.temporal.ChronoUnit.MONTHS; +import static java.time.temporal.ChronoUnit.NANOS; +import static java.time.temporal.ChronoUnit.SECONDS; +import static java.time.temporal.ChronoUnit.WEEKS; +import static java.time.temporal.ChronoUnit.YEARS; +import static java.time.temporal.IsoFields.DAY_OF_QUARTER; +import static java.time.temporal.IsoFields.QUARTER_OF_YEAR; +import static java.time.temporal.IsoFields.QUARTER_YEARS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.threeten.extra.TemporalFields.DAY_OF_HALF; +import static org.threeten.extra.TemporalFields.HALF_OF_YEAR; +import static org.threeten.extra.TemporalFields.HALF_YEARS; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalAdjuster; +import java.time.temporal.TemporalField; +import java.time.temporal.TemporalQueries; +import java.time.temporal.TemporalUnit; +import java.time.temporal.UnsupportedTemporalTypeException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test HourMinute. + */ +public class TestHourMinute { + + private static final HourMinute TEST = HourMinute.of(12, 31); + + //----------------------------------------------------------------------- + @Test + public void test_interfaces() { + assertThat(HourMinute.class) + .isAssignableTo(Serializable.class) + .isAssignableTo(Comparable.class) + .isAssignableTo(TemporalAdjuster.class) + .isAssignableTo(TemporalAccessor.class); + } + + @Test + public void test_serialization() throws IOException, ClassNotFoundException { + HourMinute test = HourMinute.of(12, 31); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(test); + } + try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { + assertThat(ois.readObject()).isEqualTo(test); + } + } + + //----------------------------------------------------------------------- + // of(int,int) / getters + //----------------------------------------------------------------------- + @Test + public void test_of_int_int() { + for (int hour = 0; hour <= 23; hour++) { + for (int minute = 0; minute <= 59; minute++) { + HourMinute test = HourMinute.of(hour, minute); + assertThat(test.getHour()).isEqualTo(hour); + assertThat(test.getMinute()).isEqualTo(minute); + assertThat(test) + .isEqualTo(HourMinute.of(hour, minute)) + .hasSameHashCodeAs(HourMinute.of(hour, minute)); + } + } + } + + @Test + public void test_of_int_int_invalid() { + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> HourMinute.of(-1, 0)); + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> HourMinute.of(24, 0)); + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> HourMinute.of(1, -1)); + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> HourMinute.of(1, 60)); + } + + //----------------------------------------------------------------------- + // from(TemporalAccessor) + //----------------------------------------------------------------------- + @Test + public void test_from_TemporalAccessor() { + for (int hour = 0; hour <= 23; hour++) { + for (int minute = 0; minute <= 59; minute++) { + HourMinute expected = HourMinute.of(hour, minute); + assertThat(HourMinute.from(expected)).isEqualTo(expected); + assertThat(HourMinute.from(LocalTime.of(hour, minute))).isEqualTo(expected); + assertThat(HourMinute.from(LocalDateTime.of(2020, 6, 3, hour, minute))).isEqualTo(expected); + } + } + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> HourMinute.from(LocalDate.EPOCH)); + assertThatNullPointerException().isThrownBy(() -> HourMinute.from((TemporalAccessor) null)); + } + + //----------------------------------------------------------------------- + // parse(CharSequence) + //----------------------------------------------------------------------- + static Object[][] data_parse_CharSequence() { + return new Object[][] { + {HourMinute.of(0, 0), "00:00"}, + {HourMinute.of(9, 9), "09:09"}, + {HourMinute.of(10, 10), "10:10"}, + {HourMinute.of(19, 59), "19:59"}, + {HourMinute.of(23, 59), "23:59"}, + }; + } + + @ParameterizedTest + @MethodSource("data_parse_CharSequence") + public void test_parse_CharSequence(HourMinute hourMin, String str) { + assertThat(HourMinute.parse(str)).isEqualTo(hourMin); + assertThat(hourMin).hasToString(str); + } + + @Test + public void test_parse_CharSequence_invalid() { + assertThatNullPointerException().isThrownBy(() -> HourMinute.parse((CharSequence) null)); + } + + //----------------------------------------------------------------------- + // parse(CharSequence,DateTimeFormatter) + //----------------------------------------------------------------------- + @Test + public void test_parse_CharSequenceDateTimeFormatter() { + assertThat(HourMinute.parse("23:30+01:00", DateTimeFormatter.ISO_OFFSET_TIME)).isEqualTo(HourMinute.of(23, 30)); + } + + @Test + public void test_parse_CharSequenceDateTimeFormatter_invalid() { + assertThatNullPointerException().isThrownBy(() -> HourMinute.parse((CharSequence) null, DateTimeFormatter.ISO_OFFSET_TIME)); + assertThatNullPointerException().isThrownBy(() -> HourMinute.parse("23:59", (DateTimeFormatter) null)); + } + + //----------------------------------------------------------------------- + // isSupported(TemporalField) + //----------------------------------------------------------------------- + @Test + public void test_isSupported_TemporalField() { + assertThat(TEST.isSupported((TemporalField) null)).isFalse(); + assertThat(TEST.isSupported(NANO_OF_SECOND)).isFalse(); + assertThat(TEST.isSupported(NANO_OF_DAY)).isFalse(); + assertThat(TEST.isSupported(MICRO_OF_SECOND)).isFalse(); + assertThat(TEST.isSupported(MICRO_OF_DAY)).isFalse(); + assertThat(TEST.isSupported(MILLI_OF_SECOND)).isFalse(); + assertThat(TEST.isSupported(MILLI_OF_DAY)).isFalse(); + assertThat(TEST.isSupported(SECOND_OF_MINUTE)).isFalse(); + assertThat(TEST.isSupported(SECOND_OF_DAY)).isFalse(); + assertThat(TEST.isSupported(MINUTE_OF_HOUR)).isTrue(); + assertThat(TEST.isSupported(MINUTE_OF_DAY)).isTrue(); + assertThat(TEST.isSupported(HOUR_OF_AMPM)).isTrue(); + assertThat(TEST.isSupported(CLOCK_HOUR_OF_AMPM)).isTrue(); + assertThat(TEST.isSupported(HOUR_OF_DAY)).isTrue(); + assertThat(TEST.isSupported(CLOCK_HOUR_OF_DAY)).isTrue(); + assertThat(TEST.isSupported(AMPM_OF_DAY)).isTrue(); + assertThat(TEST.isSupported(DAY_OF_WEEK)).isFalse(); + assertThat(TEST.isSupported(ALIGNED_DAY_OF_WEEK_IN_MONTH)).isFalse(); + assertThat(TEST.isSupported(ALIGNED_DAY_OF_WEEK_IN_YEAR)).isFalse(); + assertThat(TEST.isSupported(DAY_OF_MONTH)).isFalse(); + assertThat(TEST.isSupported(DAY_OF_YEAR)).isFalse(); + assertThat(TEST.isSupported(EPOCH_DAY)).isFalse(); + assertThat(TEST.isSupported(ALIGNED_WEEK_OF_MONTH)).isFalse(); + assertThat(TEST.isSupported(ALIGNED_WEEK_OF_YEAR)).isFalse(); + assertThat(TEST.isSupported(MONTH_OF_YEAR)).isFalse(); + assertThat(TEST.isSupported(PROLEPTIC_MONTH)).isFalse(); + assertThat(TEST.isSupported(YEAR_OF_ERA)).isFalse(); + assertThat(TEST.isSupported(YEAR)).isFalse(); + assertThat(TEST.isSupported(ERA)).isFalse(); + assertThat(TEST.isSupported(INSTANT_SECONDS)).isFalse(); + assertThat(TEST.isSupported(OFFSET_SECONDS)).isFalse(); + assertThat(TEST.isSupported(QUARTER_OF_YEAR)).isFalse(); + assertThat(TEST.isSupported(DAY_OF_QUARTER)).isFalse(); + assertThat(TEST.isSupported(HALF_OF_YEAR)).isFalse(); + assertThat(TEST.isSupported(DAY_OF_HALF)).isFalse(); + } + + //----------------------------------------------------------------------- + // isSupported(TemporalUnit) + //----------------------------------------------------------------------- + @Test + public void test_isSupported_TemporalUnit() { + assertThat(TEST.isSupported((TemporalUnit) null)).isFalse(); + assertThat(TEST.isSupported(NANOS)).isFalse(); + assertThat(TEST.isSupported(MICROS)).isFalse(); + assertThat(TEST.isSupported(MILLIS)).isFalse(); + assertThat(TEST.isSupported(SECONDS)).isFalse(); + assertThat(TEST.isSupported(MINUTES)).isTrue(); + assertThat(TEST.isSupported(HOURS)).isTrue(); + assertThat(TEST.isSupported(DAYS)).isFalse(); + assertThat(TEST.isSupported(WEEKS)).isFalse(); + assertThat(TEST.isSupported(MONTHS)).isFalse(); + assertThat(TEST.isSupported(YEARS)).isFalse(); + assertThat(TEST.isSupported(DECADES)).isFalse(); + assertThat(TEST.isSupported(CENTURIES)).isFalse(); + assertThat(TEST.isSupported(MILLENNIA)).isFalse(); + assertThat(TEST.isSupported(ERA)).isFalse(); + assertThat(TEST.isSupported(FOREVER)).isFalse(); + assertThat(TEST.isSupported(QUARTER_YEARS)).isFalse(); + assertThat(TEST.isSupported(HALF_YEARS)).isFalse(); + } + + //----------------------------------------------------------------------- + // range(TemporalField) + //----------------------------------------------------------------------- + @Test + public void test_range() { + assertThat(TEST.range(MINUTE_OF_HOUR)).isEqualTo(MINUTE_OF_HOUR.range()); + assertThat(TEST.range(MINUTE_OF_DAY)).isEqualTo(MINUTE_OF_DAY.range()); + assertThat(TEST.range(HOUR_OF_DAY)).isEqualTo(HOUR_OF_DAY.range()); + assertThat(TEST.range(CLOCK_HOUR_OF_DAY)).isEqualTo(CLOCK_HOUR_OF_DAY.range()); + assertThat(TEST.range(HOUR_OF_AMPM)).isEqualTo(HOUR_OF_AMPM.range()); + assertThat(TEST.range(CLOCK_HOUR_OF_AMPM)).isEqualTo(CLOCK_HOUR_OF_AMPM.range()); + assertThat(TEST.range(AMPM_OF_DAY)).isEqualTo(AMPM_OF_DAY.range()); + } + + @Test + public void test_range_invalid() { + assertThatExceptionOfType(UnsupportedTemporalTypeException.class).isThrownBy(() -> TEST.range(SECOND_OF_MINUTE)); + assertThatExceptionOfType(UnsupportedTemporalTypeException.class).isThrownBy(() -> TEST.range(NANO_OF_SECOND)); + assertThatNullPointerException().isThrownBy(() -> TEST.range((TemporalField) null)); + } + + //----------------------------------------------------------------------- + // get(TemporalField) + //----------------------------------------------------------------------- + @Test + public void test_get() { + assertThat(TEST.get(MINUTE_OF_HOUR)).isEqualTo(31); + assertThat(TEST.get(MINUTE_OF_DAY)).isEqualTo(12 * 60 + 31); + assertThat(TEST.get(HOUR_OF_DAY)).isEqualTo(12); + assertThat(TEST.get(CLOCK_HOUR_OF_DAY)).isEqualTo(12); + assertThat(TEST.get(HOUR_OF_AMPM)).isEqualTo(0); + assertThat(TEST.get(CLOCK_HOUR_OF_AMPM)).isEqualTo(12); + assertThat(TEST.get(AMPM_OF_DAY)).isEqualTo(1); + } + + @Test + public void test_get_invalid() { + assertThatExceptionOfType(UnsupportedTemporalTypeException.class).isThrownBy(() -> TEST.get(SECOND_OF_MINUTE)); + assertThatExceptionOfType(UnsupportedTemporalTypeException.class).isThrownBy(() -> TEST.get(NANO_OF_SECOND)); + assertThatNullPointerException().isThrownBy(() -> TEST.get((TemporalField) null)); + } + + //----------------------------------------------------------------------- + // getLong(TemporalField) + //----------------------------------------------------------------------- + @Test + public void test_getLong() { + assertThat(TEST.getLong(MINUTE_OF_HOUR)).isEqualTo(31); + assertThat(TEST.getLong(MINUTE_OF_DAY)).isEqualTo(12 * 60 + 31); + assertThat(TEST.getLong(HOUR_OF_DAY)).isEqualTo(12); + assertThat(TEST.getLong(CLOCK_HOUR_OF_DAY)).isEqualTo(12); + assertThat(TEST.getLong(HOUR_OF_AMPM)).isEqualTo(0); + assertThat(TEST.getLong(CLOCK_HOUR_OF_AMPM)).isEqualTo(12); + assertThat(TEST.getLong(AMPM_OF_DAY)).isEqualTo(1); + } + + @Test + public void test_getLong_invalid() { + assertThatExceptionOfType(UnsupportedTemporalTypeException.class).isThrownBy(() -> TEST.getLong(SECOND_OF_MINUTE)); + assertThatExceptionOfType(UnsupportedTemporalTypeException.class).isThrownBy(() -> TEST.getLong(NANO_OF_SECOND)); + assertThatNullPointerException().isThrownBy(() -> TEST.getLong((TemporalField) null)); + } + + //----------------------------------------------------------------------- + // with(TemporalAdjuster) + //----------------------------------------------------------------------- + @Test + public void test_with_TemporalAdjuster() { + assertThat(TEST.with(HourMinute.of(9, 10))).isEqualTo(HourMinute.of(9, 10)); + assertThat(TEST.with(AmPm.AM)).isEqualTo(HourMinute.of(0, 31)); + } + + @Test + public void test_with_TemporalAdjuster_invalid() { + assertThatExceptionOfType(UnsupportedTemporalTypeException.class).isThrownBy(() -> TEST.with(LocalTime.of(9, 10, 11))); + assertThatNullPointerException().isThrownBy(() -> TEST.with((TemporalAdjuster) null)); + } + + //----------------------------------------------------------------------- + // with(TemporalField, long) + //----------------------------------------------------------------------- + @Test + public void test_with_TemporalFieldlong() { + assertThat(TEST.with(MINUTE_OF_HOUR, 2)).isEqualTo(HourMinute.of(12, 2)); + assertThat(TEST.with(MINUTE_OF_DAY, 2)).isEqualTo(HourMinute.of(0, 2)); + assertThat(TEST.with(HOUR_OF_DAY, 2)).isEqualTo(HourMinute.of(2, 31)); + assertThat(TEST.with(CLOCK_HOUR_OF_DAY, 2)).isEqualTo(HourMinute.of(2, 31)); + assertThat(TEST.with(HOUR_OF_AMPM, 2)).isEqualTo(HourMinute.of(14, 31)); + assertThat(TEST.with(CLOCK_HOUR_OF_AMPM, 2)).isEqualTo(HourMinute.of(14, 31)); + assertThat(TEST.with(AMPM_OF_DAY, 0)).isEqualTo(HourMinute.of(0, 31)); + } + + @Test + public void test_with_TemporalFieldlong_invalid() { + assertThatExceptionOfType(UnsupportedTemporalTypeException.class).isThrownBy(() -> TEST.with(SECOND_OF_MINUTE, 1)); + assertThatExceptionOfType(UnsupportedTemporalTypeException.class).isThrownBy(() -> TEST.with(NANO_OF_SECOND, 1)); + assertThatNullPointerException().isThrownBy(() -> TEST.with((TemporalField) null, 1)); + } + + //----------------------------------------------------------------------- + // withHour(int) / withMinute(int) + //----------------------------------------------------------------------- + @Test + public void test_with_int() { + assertThat(TEST.withHour(9)).isEqualTo(HourMinute.of(9, 31)); + assertThat(TEST.withMinute(9)).isEqualTo(HourMinute.of(12, 9)); + } + + @Test + public void test_with_int_invalid() { + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> TEST.withHour(-1)); + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> TEST.withHour(24)); + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> TEST.withMinute(-1)); + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> TEST.withMinute(60)); + } + + //----------------------------------------------------------------------- + // plus(TemporalAmount) + //----------------------------------------------------------------------- + @Test + public void test_plus_TemporalAmount() { + assertThat(TEST.plus(Hours.of(3))).isEqualTo(HourMinute.of(15, 31)); + assertThat(TEST.plus(Minutes.of(3))).isEqualTo(HourMinute.of(12, 34)); + } + + @Test + public void test_plus_TemporalAmount_invalid() { + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> TEST.plus(Days.of(1))); + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> TEST.plus(Seconds.of(3))); + assertThatNullPointerException().isThrownBy(() -> TEST.plus(null)); + } + + //----------------------------------------------------------------------- + // plus(long,TemporalUnit) + //----------------------------------------------------------------------- + @Test + public void test_plus_longTemporalUnit() { + assertThat(TEST.plus(3, HOURS)).isEqualTo(HourMinute.of(15, 31)); + assertThat(TEST.plus(3, MINUTES)).isEqualTo(HourMinute.of(12, 34)); + assertThat(TEST.plus(1, HALF_DAYS)).isEqualTo(HourMinute.of(0, 31)); + } + + @Test + public void test_plus_longTemporalUnit_invalid() { + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> TEST.plus(1, DAYS)); + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> TEST.plus(1, SECONDS)); + assertThatNullPointerException().isThrownBy(() -> TEST.plus(1, null)); + } + + //----------------------------------------------------------------------- + // plusHours(int) / plusMinutes(int) + //----------------------------------------------------------------------- + @Test + public void test_plus_int() { + assertThat(TEST.plusHours(3)).isEqualTo(HourMinute.of(15, 31)); + assertThat(TEST.plusMinutes(3)).isEqualTo(HourMinute.of(12, 34)); + assertThat(TEST.plusHours(-15)).isEqualTo(HourMinute.of(21, 31)); + assertThat(TEST.plusHours(25)).isEqualTo(HourMinute.of(13, 31)); + } + + //----------------------------------------------------------------------- + // minus(TemporalAmount) + //----------------------------------------------------------------------- + @Test + public void test_minus_TemporalAmount() { + assertThat(TEST.minus(Hours.of(3))).isEqualTo(HourMinute.of(9, 31)); + assertThat(TEST.minus(Minutes.of(3))).isEqualTo(HourMinute.of(12, 28)); + } + + @Test + public void test_minus_TemporalAmount_invalid() { + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> TEST.minus(Days.of(1))); + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> TEST.minus(Seconds.of(3))); + assertThatNullPointerException().isThrownBy(() -> TEST.minus(null)); + } + + //----------------------------------------------------------------------- + // minus(long,TemporalUnit) + //----------------------------------------------------------------------- + @Test + public void test_minus_longTemporalUnit() { + assertThat(TEST.minus(3, HOURS)).isEqualTo(HourMinute.of(9, 31)); + assertThat(TEST.minus(3, MINUTES)).isEqualTo(HourMinute.of(12, 28)); + assertThat(TEST.minus(1, HALF_DAYS)).isEqualTo(HourMinute.of(0, 31)); + } + + @Test + public void test_minus_longTemporalUnit_invalid() { + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> TEST.minus(1, DAYS)); + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> TEST.minus(1, SECONDS)); + assertThatNullPointerException().isThrownBy(() -> TEST.minus(1, null)); + } + + //----------------------------------------------------------------------- + // minusHours(int) / minusMinutes(int) + //----------------------------------------------------------------------- + @Test + public void test_minus_int() { + assertThat(TEST.minusHours(3)).isEqualTo(HourMinute.of(9, 31)); + assertThat(TEST.minusMinutes(3)).isEqualTo(HourMinute.of(12, 28)); + assertThat(TEST.minusHours(-15)).isEqualTo(HourMinute.of(3, 31)); + assertThat(TEST.minusHours(25)).isEqualTo(HourMinute.of(11, 31)); + } + + //----------------------------------------------------------------------- + // query(TemporalQuery) + //----------------------------------------------------------------------- + @Test + public void test_query() { + assertThat(TEST.query(TemporalQueries.chronology())).isNull(); + assertThat(TEST.query(TemporalQueries.localDate())).isNull(); + assertThat(TEST.query(TemporalQueries.localTime())).isEqualTo(LocalTime.of(12, 31)); + assertThat(TEST.query(TemporalQueries.offset())).isNull(); + assertThat(TEST.query(TemporalQueries.precision())).isEqualTo(MINUTES); + assertThat(TEST.query(TemporalQueries.zone())).isNull(); + assertThat(TEST.query(TemporalQueries.zoneId())).isNull(); + } + + //----------------------------------------------------------------------- + // adjustInto(Temporal) + //----------------------------------------------------------------------- + @Test + public void test_adjustInto_Temporal() { + assertThat(TEST.adjustInto(LocalDateTime.of(2020, 6, 3, 2, 4, 6))).isEqualTo(LocalDateTime.of(2020, 6, 3, 12, 31, 6)); + } + + @Test + public void test_adjustInto_Temporal_invalid() { + assertThatNullPointerException().isThrownBy(() -> TEST.adjustInto((Temporal) null)); + } + + //----------------------------------------------------------------------- + // until(Temporal,TemporalUnit) + //----------------------------------------------------------------------- + static Object[][] data_until_TemporalTemporalUnit() { + return new Object[][] { + {HourMinute.of(12, 31), 0, 0}, + {HourMinute.of(12, 32), 0, 1}, + {HourMinute.of(13, 30), 0, 59}, + {HourMinute.of(13, 31), 1, 60}, + {HourMinute.of(13, 32), 1, 61}, + {HourMinute.of(12, 30), 0, -1}, + {HourMinute.of(11, 31), -1, -60}, + }; + } + + @ParameterizedTest + @MethodSource("data_until_TemporalTemporalUnit") + public void test_until_TemporalTemporalUnit(HourMinute target, int expectedHours, int expectedMins) { + assertThat(TEST.until(target, HOURS)).isEqualTo(expectedHours); + assertThat(TEST.until(target, MINUTES)).isEqualTo(expectedMins); + } + + @Test + public void test_until_TemporalTemporalUnit_invalid() { + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> TEST.until(HourMinute.of(0, 0), DAYS)); + assertThatExceptionOfType(DateTimeException.class).isThrownBy(() -> TEST.until(HourMinute.of(0, 0), SECONDS)); + assertThatNullPointerException().isThrownBy(() -> TEST.until(null, HOURS)); + assertThatNullPointerException().isThrownBy(() -> TEST.until(HourMinute.of(0, 0), null)); + } + + //----------------------------------------------------------------------- + // format(DateTimeFormatter) + //----------------------------------------------------------------------- + @Test + public void test_format() { + DateTimeFormatter f = new DateTimeFormatterBuilder() + .appendLiteral("Hour ") + .appendValue(HOUR_OF_DAY) + .appendLiteral(" Minute ") + .appendValue(MINUTE_OF_HOUR) + .toFormatter(); + assertThat(TEST.format(f)).isEqualTo("Hour 12 Minute 31"); + } + + @Test + public void test_format_null() { + assertThatNullPointerException().isThrownBy(() -> TEST.format(null)); + } + + //----------------------------------------------------------------------- + // atDate(LocalDate) + //----------------------------------------------------------------------- + @Test + public void test_atDate_LocalDate() { + LocalDate date = LocalDate.of(2020, 6, 3); + assertThat(TEST.atDate(date)).isEqualTo(LocalDateTime.of(date, LocalTime.of(12, 31))); + } + + @Test + public void test_atDate_LocalDate_invalid() { + assertThatNullPointerException().isThrownBy(() -> TEST.atDate(null)); + } + + //----------------------------------------------------------------------- + // atOffset(ZoneOffset) + //----------------------------------------------------------------------- + @Test + public void test_atOffset_ZoneOffset() { + ZoneOffset offset = ZoneOffset.ofHours(2); + assertThat(TEST.atOffset(offset)).isEqualTo(OffsetTime.of(LocalTime.of(12, 31), offset)); + } + + @Test + public void test_atOffset_ZoneOffset_invalid() { + assertThatNullPointerException().isThrownBy(() -> TEST.atOffset(null)); + } + + //----------------------------------------------------------------------- + // toLocalTime() + //----------------------------------------------------------------------- + @Test + public void test_toLocalTime() { + assertThat(TEST.toLocalTime()).isEqualTo(LocalTime.of(12, 31)); + } + + //----------------------------------------------------------------------- + // compareTo() + //----------------------------------------------------------------------- + @Test + public void test_compareTo() { + for (int hour1 = 0; hour1 <= 23; hour1++) { + for (int minute1 = 0; minute1 <= 59; minute1++) { + HourMinute a = HourMinute.of(hour1, minute1); + for (int hour2 = 0; hour2 <= 23; hour2++) { + for (int minute2 = 0; minute2 <= 59; minute2++) { + HourMinute b = HourMinute.of(hour2, minute2); + if (hour1 < hour2) { + assertThat(a).isLessThan(b); + assertThat(b).isGreaterThan(a); + assertThat(a.isAfter(b)).isFalse(); + assertThat(a.isBefore(b)).isTrue(); + assertThat(b.isAfter(a)).isTrue(); + assertThat(b.isBefore(a)).isFalse(); + } else if (hour1 > hour2) { + assertThat(a).isGreaterThan(b); + assertThat(b).isLessThan(a); + assertThat(a.isAfter(b)).isTrue(); + assertThat(a.isBefore(b)).isFalse(); + assertThat(b.isAfter(a)).isFalse(); + assertThat(b.isBefore(a)).isTrue(); + } else { + if (minute1 < minute2) { + assertEquals(true, a.compareTo(b) < 0); + assertEquals(true, b.compareTo(a) > 0); + assertEquals(false, a.isAfter(b)); + assertEquals(false, b.isBefore(a)); + assertEquals(true, b.isAfter(a)); + assertEquals(true, a.isBefore(b)); + } else if (minute1 > minute2) { + assertEquals(true, a.compareTo(b) > 0); + assertEquals(true, b.compareTo(a) < 0); + assertEquals(true, a.isAfter(b)); + assertEquals(true, b.isBefore(a)); + assertEquals(false, b.isAfter(a)); + assertEquals(false, a.isBefore(b)); + } else { + assertEquals(0, a.compareTo(b)); + assertEquals(0, b.compareTo(a)); + assertEquals(false, a.isAfter(b)); + assertEquals(false, b.isBefore(a)); + assertEquals(false, b.isAfter(a)); + assertEquals(false, a.isBefore(b)); + } + } + } + } + } + } + } + + @Test + public void test_compareTo_nullHourMinute() { + assertThatNullPointerException().isThrownBy(() -> TEST.compareTo(null)); + } + + //----------------------------------------------------------------------- + // equals() / hashCode() + //----------------------------------------------------------------------- + @Test + public void test_equals() { + for (int hour1 = 0; hour1 <= 23; hour1++) { + for (int minute1 = 0; minute1 <= 59; minute1++) { + HourMinute a = HourMinute.of(hour1, minute1); + for (int hour2 = 0; hour2 <= 23; hour2++) { + for (int minute2 = 0; minute2 <= 59; minute2++) { + HourMinute b = HourMinute.of(hour2, minute2); + if (hour1 == hour2 && minute1 == minute2) { + assertThat(a) + .isEqualTo(b) + .hasSameHashCodeAs(b); + } else { + assertThat(a).isNotEqualTo(b); + } + } + } + } + } + } + + @Test + public void test_equals_nullHourMinute() { + assertThat(TEST.equals(null)).isFalse(); + } + + @Test + public void test_equals_incorrectType() { + Object obj = "Incorrect type"; + assertThat(TEST.equals(obj)).isFalse(); + } + + //----------------------------------------------------------------------- + // toString() + //----------------------------------------------------------------------- + @Test + public void test_toString() { + assertThat(HourMinute.of(0, 0)).hasToString("00:00"); + assertThat(HourMinute.of(0, 5)).hasToString("00:05"); + assertThat(HourMinute.of(9, 0)).hasToString("09:00"); + assertThat(HourMinute.of(23, 59)).hasToString("23:59"); + } + +}