From 386a432b6df51a953161c38c4c50e5cd8d9c86b4 Mon Sep 17 00:00:00 2001 From: Aleksander Sciborek Date: Thu, 15 Nov 2018 21:21:19 +0100 Subject: [PATCH] [LANG-1407] implement DurationUtils with methods for rounding value of Duration in a given unit of time --- .../commons/lang3/time/DurationUtils.java | 286 ++++++++++++++++++ .../commons/lang3/time/DurationUtilsTest.java | 271 +++++++++++++++++ 2 files changed, 557 insertions(+) create mode 100644 src/main/java/org/apache/commons/lang3/time/DurationUtils.java create mode 100644 src/test/java/org/apache/commons/lang3/time/DurationUtilsTest.java diff --git a/src/main/java/org/apache/commons/lang3/time/DurationUtils.java b/src/main/java/org/apache/commons/lang3/time/DurationUtils.java new file mode 100644 index 00000000000..2a7be472e31 --- /dev/null +++ b/src/main/java/org/apache/commons/lang3/time/DurationUtils.java @@ -0,0 +1,286 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.lang3.time; + +import java.time.Duration; + +/** + *

A suite of utilities surround the use of the {@link java.time.Duration} object

+ *

DurationUtils contains methods to rounding duration value

+ * + * @since 3.9 + */ +public class DurationUtils { + + /** + * Number of nanoseconds in a standard day + * + * @since 3.9 + */ + public static final long NANOS_PER_DAY = 86_400_000_000_000L; + + /** + * Number of nanoseconds in a standard hour + * + * @since 3.9 + */ + public static final long NANOS_PER_HOUR = 3_600_000_000_000L; + + /** + * Number of nanoseconds in a standard minute + * + * @since 3.9 + */ + public static final long NANOS_PER_MINUTE = 60_000_000_000L; + + /** + * Number of nanoseconds in a standard second + * + * @since 3.9 + */ + public static final long NANOS_PER_SECOND = 1_000_000_000L; + + /** + * Number of nanoseconds in a standard millisecond + * + * @since 3.9 + */ + public static final long NANOS_PER_MILLISECOND = 1_000_000L; + + /** + *

{@code DurationUtils} instances should NOT be constructed in + * standard programming.

+ * + *

This constructor is public to permit tools that require a JavaBean + * instance to operate

+ */ + public DurationUtils() { + super(); + } + + /** + *

Rounds up the quantity of days from the given duration

+ * + *

For the duration of 24h it should return 1(day) and the duration of 25 h it should return 2(days)

+ * + * @param duration the given duration from which the quantity of days will be calculated + * @return the rounded up quantity of days + * @throws IllegalArgumentException if the given duration is null + * @since 3.9 + */ + public static long roundUpDaysQuantity(final Duration duration) { + if (duration == null) { + throw new IllegalArgumentException("the duration must not be null"); + } + boolean shouldRoundUpDaysQuantity = (duration.toNanos() % NANOS_PER_DAY > 0); + if (shouldRoundUpDaysQuantity) { + return duration.toDays() + 1; + } + return duration.toDays(); + } + + /** + *

Rounds the quantity of days from the given duration

+ * + *

For the duration of 36h it should return 2 (days) and for the duration of 36h 59 minutes it should return 1(day)

+ * + * @param duration the given duration from which the quantity of days will be calculated + * @return the rounded quantity of days + * @throws IllegalArgumentException if the given duration is null + * @since 3.9 + */ + public static long roundDaysQuantity(final Duration duration) { + if (duration == null) { + throw new IllegalArgumentException("the duration must not be null"); + } + boolean shouldRoundDaysQuantity = ((duration.toNanos() % NANOS_PER_DAY) >= (NANOS_PER_DAY / 2)); + if (shouldRoundDaysQuantity) { + return duration.toDays() + 1; + } + return duration.toDays(); + } + + /** + *

Rounds up the quantity of hours from the given duration

+ * + *

For the duration of 1h it should return 1(hour) and the duration of 1h 10 min it should return 2(hours)

+ * + * @param duration the given duration from which the quantity of hours will be calculated + * @return the rounded up quantity of hours + * @throws IllegalArgumentException if the given duration is null + * @since 3.9 + */ + public static long roundUpHoursQuantity(final Duration duration) { + if (duration == null) { + throw new IllegalArgumentException("the duration must not be null"); + } + boolean shouldRoundUpHoursQuantity = (duration.toNanos() % NANOS_PER_HOUR > 0); + if (shouldRoundUpHoursQuantity) { + return duration.toHours() + 1; + } + return duration.toHours(); + } + + /** + *

Rounds the quantity of hours from the given duration

+ * + *

For the duration of 1h 30min it should return 2 (hours) and for the duration of 1h 29 min it should return 1(hour)

+ * + * @param duration the given duration from which the quantity of hours will be calculated + * @return the rounded quantity of hours + * @throws IllegalArgumentException if the given duration is null + * @since 3.9 + */ + public static long roundHoursQuantity(final Duration duration) { + if (duration == null) { + throw new IllegalArgumentException("the duration must not be null"); + } + boolean shouldRoundHoursQuantity = ((duration.toNanos() % NANOS_PER_HOUR) >= (NANOS_PER_HOUR / 2)); + if (shouldRoundHoursQuantity) { + return duration.toHours() + 1; + } + return duration.toHours(); + } + + /** + *

Rounds up the quantity of minutes from the given duration

+ * + *

For the duration of 10 minutes it should return 10(minutes) and the duration of 10 minutes 10 seconds it should return 11(minutes)

+ * + * @param duration the given duration from which the quantity of minutes will be calculated + * @return the rounded up quantity of minutes + * @throws IllegalArgumentException if the given duration is null + * @since 3.9 + */ + public static long roundUpMinutesQuantity(final Duration duration) { + if (duration == null) { + throw new IllegalArgumentException("the duration must not be null"); + } + boolean shouldRoundUpMinutes = (duration.toNanos() % NANOS_PER_MINUTE > 0); + if (shouldRoundUpMinutes) { + return duration.toMinutes() + 1; + } + return duration.toMinutes(); + } + + /** + *

Rounds the quantity of minutes from the given duration

+ * + *

For the duration of 1 minute 30 seconds it should return 2(minutes) and for the duration of 1 minute 29 seconds it should return 1(minute)

+ * + * @param duration the given duration from which the quantity of minutes will be calculated + * @return the rounded quantity of days + * @throws IllegalArgumentException if the given duration is null + * @since 3.9 + */ + public static long roundMinutesQuantity(final Duration duration) { + if (duration == null) { + throw new IllegalArgumentException("the duration must not be null"); + } + boolean shouldRoundMinutes = ((duration.toNanos() % NANOS_PER_MINUTE) >= (NANOS_PER_MINUTE / 2)); + if (shouldRoundMinutes) { + return duration.toMinutes() + 1; + } + return duration.toMinutes(); + } + + /** + *

Rounds up the quantity of seconds from the given duration

+ * + *

For the duration of 10 seconds it should return 10(seconds) and the duration of 1 second 10 milliseconds it should return 2(seconds)

+ * + * @param duration the given duration from which the quantity of seconds will be calculated + * @return the rounded up quantity of seconds + * @throws IllegalArgumentException if the given duration is null + * @since 3.9 + */ + public static long roundUpSecondsQuantity(final Duration duration) { + if (duration == null) { + throw new IllegalArgumentException("the duration must not be null"); + } + boolean shouldRoundUpSeconds = (duration.toNanos() % NANOS_PER_SECOND > 0); + if (shouldRoundUpSeconds) { + return duration.getSeconds() + 1; + } + return duration.getSeconds(); + } + + /** + *

Rounds the quantity of seconds from the given duration

+ * + *

For the duration of 10 seconds and 499 milliseconds it should return 10(seconds) + * and the duration of 1 second 500 milliseconds it should return 2(seconds)

+ * + * @param duration the given duration from which the quantity of seconds will be calculated + * @return the rounded quantity of seconds + * @throws IllegalArgumentException if the given duration is null + * @since 3.9 + */ + public static long roundSecondsQuantity(final Duration duration) { + if (duration == null) { + throw new IllegalArgumentException("the duration must not be null"); + } + boolean shouldRoundSeconds = ((duration.toNanos() % NANOS_PER_SECOND) >= (NANOS_PER_SECOND / 2)); + if (shouldRoundSeconds) { + return duration.getSeconds() + 1; + } + return duration.getSeconds(); + } + + /** + *

Rounds up the quantity of milliseconds from the given duration

+ * + *

For the duration of 10 milliseconds it should return 10(milliseconds) and the duration of 1 milliseconds 10 nanoseconds it should return 2(milliseconds)

+ * + * @param duration the given duration from which the quantity of milliseconds will be calculated + * @return the rounded up quantity of milliseconds + * @throws IllegalArgumentException if the given duration is null + * @since 3.9 + */ + public static long roundUpMillisecondsQuantity(final Duration duration) { + if (duration == null) { + throw new IllegalArgumentException("the duration must not be null"); + } + boolean shouldRoundUp = (duration.toNanos() % NANOS_PER_MILLISECOND > 0); + if (shouldRoundUp) { + return duration.toMillis() + 1; + } + return duration.toMillis(); + } + + /** + *

Rounds the quantity of milliseconds from the given duration

+ * + *

For the duration of 10 milliseconds 499 999 nanoseconds it should return 10(milliseconds) and the duration of 10 milliseconds and 500 000 nanoseconds it should return 11(milliseconds)

+ * + * @param duration the given duration from which the quantity of milliseconds will be calculated + * @return the rounded quantity of milliseconds + * @throws IllegalArgumentException if the given duration is null + * @since 3.9 + */ + public static long roundMillisecondsQuantity(final Duration duration) { + if (duration == null) { + throw new IllegalArgumentException("the duration must not be null"); + } + boolean shouldRound = ((duration.toNanos() % NANOS_PER_MILLISECOND) >= (NANOS_PER_MILLISECOND / 2)); + if (shouldRound) { + return duration.toMillis() + 1; + } + return duration.toMillis(); + } + +} diff --git a/src/test/java/org/apache/commons/lang3/time/DurationUtilsTest.java b/src/test/java/org/apache/commons/lang3/time/DurationUtilsTest.java new file mode 100644 index 00000000000..cd3eb406021 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/time/DurationUtilsTest.java @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.lang3.time; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +public class DurationUtilsTest { + + @Nested + class DaysQuantityRoundingTest { + + @DisplayName("roundUpDaysQuantity should throw the IllegalArgumentException for the null duration") + @Test + void roundUpDaysQuantityShouldThrowIllegalArgumentExceptionForNullDuration() { + Assertions.assertThrows(IllegalArgumentException.class, () -> DurationUtils.roundUpDaysQuantity(null)); + } + + @DisplayName("the quantity of days from the Duration 3 days and 1 second should be rounded up") + @Test + void durationOf3DaysAnd1SecondShouldBeRoundedUp() { + Duration duration = Duration.ofDays(3).plusSeconds(1); + long daysQuantity = DurationUtils.roundUpDaysQuantity(duration); + Assertions.assertEquals(4, daysQuantity); + } + + @DisplayName("the quantity of days from the Duration 3 days should not be rounded up") + @Test + void durationOfExactly3DaysShouldNotBeRoundedUp() { + Duration duration = Duration.ofDays(3); + long daysQuantity = DurationUtils.roundUpDaysQuantity(duration); + Assertions.assertEquals(3, daysQuantity); + } + + @DisplayName("roundDaysQuantity should throw the IllegalArgumentException for the null duration") + @Test + void roundDaysQuantityShouldThrowIllegalArgumentExceptionForNullDuration() { + Assertions.assertThrows(IllegalArgumentException.class, () -> DurationUtils.roundDaysQuantity(null)); + } + + @DisplayName("the quantity of days from the Duration 3 days 12 hours should be rounded") + @Test + void durationOf3AndHalfDaysShouldBeRounded() { + Duration duration = Duration.ofDays(3).plusHours(12); + long daysQuantity = DurationUtils.roundDaysQuantity(duration); + Assertions.assertEquals(4, daysQuantity); + } + + @DisplayName("the quantity of days from the Duration 3 days 11 hours 59 minutes should not be rounded") + @Test + void durationOf3Days11Hours59MinutesShouldNotBeRounded() { + Duration duration = Duration.ofDays(3).plusHours(11).plusMinutes(59); + long daysQuantity = DurationUtils.roundDaysQuantity(duration); + Assertions.assertEquals(3, daysQuantity); + } + + } + + @Nested + class HoursQuantityRoundingTest { + + @DisplayName("roundUpHoursQuantity should throw the IllegalArgumentException for the null duration") + @Test + void roundUpHoursQuantityShouldThrowIllegalArgumentExceptionForNullDuration() { + Assertions.assertThrows(IllegalArgumentException.class, () -> DurationUtils.roundUpHoursQuantity(null)); + } + + @DisplayName("the quantity of hours from the Duration 2 hours and 1 second should be rounded up") + @Test + void durationOf2HoursAnd1SecondShouldBeRoundedUp() { + Duration duration = Duration.ofHours(2).plusSeconds(1); + long hoursQuantity = DurationUtils.roundUpHoursQuantity(duration); + Assertions.assertEquals(3, hoursQuantity); + } + + @DisplayName("the quantity of hours from the Duration 3 hours should not be rounded up") + @Test + void durationOfExactly3HoursShouldNotBeRoundedUp() { + Duration duration = Duration.ofHours(3); + long hoursQuantity = DurationUtils.roundUpHoursQuantity(duration); + Assertions.assertEquals(3, hoursQuantity); + } + + @DisplayName("roundUpHoursQuantity should throw the IllegalArgumentException for the null duration") + @Test + void roundHoursQuantityShouldThrowIllegalArgumentExceptionForNullDuration() { + Assertions.assertThrows(IllegalArgumentException.class, () -> DurationUtils.roundHoursQuantity(null)); + } + + @DisplayName("the quantity of hours from the Duration 2 hours 30 minutes should be rounded") + @Test + void durationOf3AndHalfDaysShouldBeRounded() { + Duration duration = Duration.ofHours(2).plusMinutes(30); + long hoursQuantity = DurationUtils.roundHoursQuantity(duration); + Assertions.assertEquals(3, hoursQuantity); + } + + @DisplayName("the quantity of hours from the Duration 3 hours 29 minutes 59 seconds should not be rounded") + @Test + void durationOf3Days11Hours59MinutesShouldNotBeRounded() { + Duration duration = Duration.ofHours(3).plusMinutes(29).plusSeconds(59); + long hoursQuantity = DurationUtils.roundHoursQuantity(duration); + Assertions.assertEquals(3, hoursQuantity); + } + } + + @Nested + class MinutesQuantityRoundingTest { + + @DisplayName("roundUpMinutesQuantity should throw the IllegalArgumentException for the null duration") + @Test + void roundUpMinutesQuantityShouldThrowIllegalArgumentExceptionForNullDuration() { + Assertions.assertThrows(IllegalArgumentException.class, () -> DurationUtils.roundUpMinutesQuantity(null)); + } + + @DisplayName("the quantity of minutes from the Duration 10 minutes and 1 second should be rounded up") + @Test + void durationOf10MinutesAnd1SecondShouldBeRoundedUp() { + Duration duration = Duration.ofMinutes(10).plusSeconds(1); + long minutesQuantity = DurationUtils.roundUpMinutesQuantity(duration); + Assertions.assertEquals(11, minutesQuantity); + } + + @DisplayName("the quantity of minutes from the Duration 10 minutes should not be rounded up") + @Test + void durationOfExactly10MinutesShouldNotBeRoundedUp() { + Duration duration = Duration.ofMinutes(10); + long minutesQuantity = DurationUtils.roundUpMinutesQuantity(duration); + Assertions.assertEquals(10, minutesQuantity); + } + + @DisplayName("roundMinutesQuantity should throw the IllegalArgumentException for the null duration") + @Test + void roundMinutesQuantityShouldThrowIllegalArgumentExceptionForNullDuration() { + Assertions.assertThrows(IllegalArgumentException.class, () -> DurationUtils.roundMinutesQuantity(null)); + } + + @DisplayName("the quantity of minutes from the Duration 10 minutes and 30 second should be rounded") + @Test + void durationOf10MinutesAnd30SecondShouldBeRoundedUp() { + Duration duration = Duration.ofMinutes(10).plusSeconds(30); + long minutesQuantity = DurationUtils.roundMinutesQuantity(duration); + Assertions.assertEquals(11, minutesQuantity); + } + + @DisplayName("the quantity of minutes from the Duration 10 minutes and 29 seconds should not be rounded") + @Test + void durationOfExactly10MinutesAnd29SecondsShouldNotBeRounded() { + Duration duration = Duration.ofMinutes(10).plusSeconds(29); + long minutesQuantity = DurationUtils.roundMinutesQuantity(duration); + Assertions.assertEquals(10, minutesQuantity); + } + + } + + @Nested + class SecondsQuantityRoundingTest { + + @DisplayName("roundUpSecondsQuantity should throw the IllegalArgumentException for the null duration") + @Test + void roundUpMinutesQuantityShouldThrowIllegalArgumentExceptionForNullDuration() { + Assertions.assertThrows(IllegalArgumentException.class, () -> DurationUtils.roundUpSecondsQuantity(null)); + } + + @DisplayName("the quantity of seconds from the Duration 10 seconds and 1 millisecond should be rounded up") + @Test + void durationOf10SecondsAnd1MillisecondShouldBeRoundedUp() { + Duration duration = Duration.ofSeconds(10).plusMillis(1); + long secondsQuantity = DurationUtils.roundUpSecondsQuantity(duration); + Assertions.assertEquals(11, secondsQuantity); + } + + @DisplayName("the quantity of seconds from the Duration 10 minutes should not be rounded up") + @Test + void durationOfExactly10SecondsShouldNotBeRoundedUp() { + Duration duration = Duration.ofSeconds(10); + long secondsQuantity = DurationUtils.roundUpSecondsQuantity(duration); + Assertions.assertEquals(10, secondsQuantity); + } + + @DisplayName("roundSecondsQuantity should throw the IllegalArgumentException for the null duration") + @Test + void roundSecondsQuantityShouldThrowIllegalArgumentExceptionForNullDuration() { + Assertions.assertThrows(IllegalArgumentException.class, () -> DurationUtils.roundSecondsQuantity(null)); + } + + @DisplayName("the quantity of seconds from the Duration 10 seconds and 500 milliseconds should be rounded") + @Test + void durationOf10SecondsAnd500MillisecondShouldBeRounded() { + Duration duration = Duration.ofSeconds(10).plusMillis(500); + long secondsQuantity = DurationUtils.roundSecondsQuantity(duration); + Assertions.assertEquals(11, secondsQuantity); + } + + @DisplayName("the quantity of seconds from the Duration 10 seconds and 499 milliseconds should not be rounded") + @Test + void durationOfExactly10MinutesAnd29SecondsShouldNotBeRounded() { + Duration duration = Duration.ofSeconds(10).plusMillis(499); + long secondsQuantity = DurationUtils.roundSecondsQuantity(duration); + Assertions.assertEquals(10, secondsQuantity); + } + + } + + @Nested + class MillisecondsQuantityRoundingTest { + + @DisplayName("roundUpMillisecondsQuantity should throw the IllegalArgumentException for the null duration") + @Test + void roundUpMillisecondsQuantityShouldThrowIllegalArgumentExceptionForNullDuration() { + Assertions.assertThrows(IllegalArgumentException.class, () -> DurationUtils.roundUpMillisecondsQuantity(null)); + } + + @DisplayName("the quantity of milliseconds from the Duration 10 milliseconds and 1 nanoseconds should be rounded up") + @Test + void durationOf10SecondsAnd1MillisecondShouldBeRoundedUp() { + Duration duration = Duration.ofMillis(10).plusNanos(1); + long millisecondsQuantity = DurationUtils.roundUpMillisecondsQuantity(duration); + Assertions.assertEquals(11, millisecondsQuantity); + } + + @DisplayName("the quantity of milliseconds from the Duration 10 milliseconds should not be rounded up") + @Test + void durationOfExactly10SecondsShouldNotBeRoundedUp() { + Duration duration = Duration.ofMillis(10); + long millisecondsQuantity = DurationUtils.roundUpMillisecondsQuantity(duration); + Assertions.assertEquals(10, millisecondsQuantity); + } + + @DisplayName("roundMillisecondsQuantity should throw the IllegalArgumentException for the null duration") + @Test + void roundSecondsQuantityShouldThrowIllegalArgumentExceptionForNullDuration() { + Assertions.assertThrows(IllegalArgumentException.class, () -> DurationUtils.roundMillisecondsQuantity(null)); + } + + @DisplayName("the quantity of milliseconds from the Duration 10 milliseconds and 500 000 nanoseconds should be rounded") + @Test + void durationOf10SecondsAnd500MillisecondShouldBeRounded() { + Duration duration = Duration.ofMillis(10).plusNanos(500_000); + long millisecondsQuantity = DurationUtils.roundMillisecondsQuantity(duration); + Assertions.assertEquals(11, millisecondsQuantity); + } + + @DisplayName("the quantity of seconds from the Duration 10 seconds and 499 999 nanoseconds should not be rounded") + @Test + void durationOfExactly10MinutesAnd29SecondsShouldNotBeRounded() { + Duration duration = Duration.ofMillis(10).plusNanos(499_999); + long millisecondsQuantity = DurationUtils.roundMillisecondsQuantity(duration); + Assertions.assertEquals(10, millisecondsQuantity); + } + } + +}