diff --git a/src/main/java/org/threeten/extra/RoundingMode.java b/src/main/java/org/threeten/extra/RoundingMode.java new file mode 100644 index 00000000..27237542 --- /dev/null +++ b/src/main/java/org/threeten/extra/RoundingMode.java @@ -0,0 +1,17 @@ +package org.threeten.extra; + +/** + * A time clock rounding mode. + *

+ * {@code RoundingMode} specifies a time clock rounding strategy. + *

+ * Rounding mode defines the following strategies: + *

  • DOWN - rounds the time towards the start of a fraction + *
  • HALF_UP - rounds towards the "nearest neighbor". UP, if the distance is of equal length + *
  • UP - rounds the time towards the end of a fraction + */ +public enum RoundingMode { + DOWN, + HALF_UP, + UP +} diff --git a/src/main/java/org/threeten/extra/Temporals.java b/src/main/java/org/threeten/extra/Temporals.java index 0b3f21c2..db5b7294 100644 --- a/src/main/java/org/threeten/extra/Temporals.java +++ b/src/main/java/org/threeten/extra/Temporals.java @@ -32,12 +32,16 @@ package org.threeten.extra; import static java.time.temporal.ChronoField.DAY_OF_WEEK; +import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; +import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; +import static java.time.temporal.ChronoField.NANO_OF_SECOND; import static java.time.temporal.ChronoUnit.DAYS; import static java.time.temporal.ChronoUnit.ERAS; import static java.time.temporal.ChronoUnit.FOREVER; import static java.time.temporal.ChronoUnit.WEEKS; import java.text.ParsePosition; +import java.time.Duration; import java.time.DateTimeException; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; @@ -59,6 +63,7 @@ *
  • adjusters that ignore Saturday/Sunday weekends *
  • conversion between {@code TimeUnit} and {@code ChronoUnit} *
  • converting an amount to another unit + *
  • adjusters that round time * * *

    Implementation Requirements:

    @@ -126,6 +131,66 @@ public static TemporalAdjuster previousWorkingDayOrSame() { return Adjuster.PREVIOUS_WORKING_OR_SAME; } + /** + * Returns an adjuster that offers time clock rounding. + *

    + * Time clock rounding divides the minutes of the hour into equal fractions of the same length. Every fraction has + * a start-inclusive and end-exclusive minute of the hour. A rounded result is either at the lower or upper end of + * one of these fractions. + *

    + * Time clock rounding views time as hour-minute. Therefore it always truncates to minutes. + * + * @param duration the fraction of the hour, must be a divisor of 60 + * @param roundingMode the time clock rounding mode + * @return time rounded to a fraction of the hour + */ + public static TemporalAdjuster roundTime(Duration duration, RoundingMode roundingMode) { + Objects.requireNonNull(duration, "duration"); + Objects.requireNonNull(roundingMode, "mode"); + + long minutes = duration.toMinutes(); + if (minutes < 1 || 60 % minutes != 0) { + throw new DateTimeException("duration is not a divisor of 60"); + } + + int divisor = (int) minutes; + + return temporal -> { + int minuteOfHour = temporal.get(MINUTE_OF_HOUR); + + temporal = temporal.with(SECOND_OF_MINUTE, 0) + .with(NANO_OF_SECOND, 0); + + if (minuteOfHour % divisor == 0) { + return temporal; + } + + int down = minuteOfHour / divisor * divisor; + int up = down + divisor; + RoundingMode mode = roundingMode; + + if (roundingMode == RoundingMode.HALF_UP) { + mode = (minuteOfHour - down) < (up - minuteOfHour) ? RoundingMode.DOWN : RoundingMode.UP; + } + + if (mode == RoundingMode.DOWN) { + temporal = temporal.with(MINUTE_OF_HOUR, down); + } + + if (mode == RoundingMode.UP) { + if (up == 60) { + if (temporal.isSupported(ChronoUnit.HOURS)) { + temporal = temporal.plus(1, ChronoUnit.HOURS); + } + up = 0; + } + temporal = temporal.with(MINUTE_OF_HOUR, up); + } + + return temporal; + }; + } + //----------------------------------------------------------------------- /** * Enum implementing the adjusters. diff --git a/src/test/java/org/threeten/extra/TestTemporals.java b/src/test/java/org/threeten/extra/TestTemporals.java index 62e2d99c..4d14c0e0 100644 --- a/src/test/java/org/threeten/extra/TestTemporals.java +++ b/src/test/java/org/threeten/extra/TestTemporals.java @@ -65,7 +65,9 @@ import java.io.ObjectOutputStream; import java.io.Serializable; import java.time.DateTimeException; +import java.time.LocalTime; import java.time.LocalDate; +import java.time.Duration; import java.time.Month; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; @@ -670,4 +672,53 @@ public void test_convertAmountInvalidUnsupported(TemporalUnit fromUnit, Temporal assertThrows(UnsupportedTemporalTypeException.class, () -> Temporals.convertAmount(1, fromUnit, resultUnit)); } + + //----------------------------------------------------------------------- + // roundTime() + //----------------------------------------------------------------------- + @Test + public void test_roundTime_dateTimeException() { + assertThrows(DateTimeException.class, () -> Temporals.roundTime(Duration.ofMinutes(7), RoundingMode.DOWN)); + } + + @Test + public void test_roundTime_down_localTime() { + TemporalAdjuster down = Temporals.roundTime(Duration.ofMinutes(6), RoundingMode.DOWN); + + assertEquals(LocalTime.of(14, 12), LocalTime.of(14, 12).with(down)); + assertEquals(LocalTime.of(14, 12), LocalTime.of(14, 13).with(down)); + assertEquals(LocalTime.of(14, 12), LocalTime.of(14, 14).with(down)); + assertEquals(LocalTime.of(14, 12), LocalTime.of(14, 15).with(down)); + assertEquals(LocalTime.of(14, 12), LocalTime.of(14, 16).with(down)); + assertEquals(LocalTime.of(14, 12), LocalTime.of(14, 17).with(down)); + assertEquals(LocalTime.of(14, 18), LocalTime.of(14, 18).with(down)); + + } + + @Test + public void test_roundTime_up_localTime() { + TemporalAdjuster up = Temporals.roundTime(Duration.ofMinutes(6), RoundingMode.UP); + + assertEquals(LocalTime.of(14, 12), LocalTime.of(14, 12, 7, 1).with(up)); + assertEquals(LocalTime.of(14, 18), LocalTime.of(14, 13, 7, 1).with(up)); + assertEquals(LocalTime.of(14, 18), LocalTime.of(14, 14).with(up)); + assertEquals(LocalTime.of(14, 18), LocalTime.of(14, 15).with(up)); + assertEquals(LocalTime.of(14, 18), LocalTime.of(14, 16).with(up)); + assertEquals(LocalTime.of(14, 18), LocalTime.of(14, 17).with(up)); + assertEquals(LocalTime.of(14, 18), LocalTime.of(14, 18).with(up)); + } + + @Test + public void test_roundTime_halfUp_localTime() { + TemporalAdjuster halfUp = Temporals.roundTime(Duration.ofMinutes(6), RoundingMode.HALF_UP); + + assertEquals(LocalTime.of(14, 54), LocalTime.of(14, 54, 7, 1).with(halfUp)); + assertEquals(LocalTime.of(14, 54), LocalTime.of(14, 55, 7, 1).with(halfUp)); + assertEquals(LocalTime.of(14, 54), LocalTime.of(14, 56).with(halfUp)); + assertEquals(LocalTime.of(15, 0), LocalTime.of(14, 57).with(halfUp)); + assertEquals(LocalTime.of(15, 0), LocalTime.of(14, 58).with(halfUp)); + assertEquals(LocalTime.of(15, 0), LocalTime.of(14, 59).with(halfUp)); + assertEquals(LocalTime.of(15, 0), LocalTime.of(15, 0).with(halfUp)); + } + }