From 62317450ea39de3fc62439847de9c04cd98b9e2e Mon Sep 17 00:00:00 2001 From: Louis Deconinck Date: Sat, 12 Sep 2026 10:42:39 +0200 Subject: [PATCH 1/2] Deprecate TimeUnit parameters in TimeAndDate in favor of TemporalUnit (#1911) java.util.concurrent.TimeUnit cannot express units larger than days, so timeAndDate().past()/future() could not be used with e.g. years. The past()/future() overloads taking TimeUnit are now deprecated and replaced by equivalent overloads taking java.time.temporal.TemporalUnit, which also supports calendar-based units such as ChronoUnit.YEARS. --- docs/documentation/date-format.md | 4 +- .../datafaker/providers/base/TimeAndDate.java | 210 ++++++++++++++++-- .../providers/base/TimeAndDateTest.java | 81 +++++++ 3 files changed, 279 insertions(+), 16 deletions(-) diff --git a/docs/documentation/date-format.md b/docs/documentation/date-format.md index 1f39e2a2a..8c6dcb278 100644 --- a/docs/documentation/date-format.md +++ b/docs/documentation/date-format.md @@ -7,8 +7,8 @@ Since 1.2.0 Datafaker supports specifying of date formats for dates and timestam ```java Faker faker = new Faker(); - System.out.println(faker.timeAndDate().future(1, TimeUnit.HOURS, "yyyy MM.dd mm:hh:ss")); - System.out.println(faker.timeAndDate().past(1, TimeUnit.HOURS, "yyyy-MM-dd mm:hh:ss")); + System.out.println(faker.timeAndDate().future(1, ChronoUnit.HOURS, "yyyy MM.dd mm:hh:ss")); + System.out.println(faker.timeAndDate().past(1, ChronoUnit.HOURS, "yyyy-MM-dd mm:hh:ss")); System.out.println(faker.timeAndDate().birthday(1, 99, "yyyy/MM/dd")); ``` diff --git a/src/main/java/net/datafaker/providers/base/TimeAndDate.java b/src/main/java/net/datafaker/providers/base/TimeAndDate.java index 083ffaa47..9f6cff04e 100644 --- a/src/main/java/net/datafaker/providers/base/TimeAndDate.java +++ b/src/main/java/net/datafaker/providers/base/TimeAndDate.java @@ -8,10 +8,9 @@ import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalUnit; import java.util.concurrent.TimeUnit; -import static java.util.concurrent.TimeUnit.MILLISECONDS; - /** * A generator of random times and dates. *

@@ -34,7 +33,7 @@ protected TimeAndDate(BaseProviders faker) { */ public Instant future() { long FIFTY_YEARS = TimeUnit.DAYS.toMillis(18262); - return future(faker.number().numberBetween(1, FIFTY_YEARS), MILLISECONDS); + return future(faker.number().numberBetween(1, FIFTY_YEARS), ChronoUnit.MILLIS); } /** @@ -43,8 +42,22 @@ public Instant future() { * @param atMost at most this amount of time ahead from now exclusive. * @param unit the time unit. * @return a future date from now. + * @deprecated since 3.0.0. Use {@link #future(long, TemporalUnit)} instead. */ + @Deprecated(since = "3.0.0", forRemoval = true) public Instant future(long atMost, TimeUnit unit) { + return future(atMost, unit.toChronoUnit()); + } + + /** + * Generates a future date from now. + * + * @param atMost at most this amount of time ahead from now exclusive. + * @param unit the temporal unit, e.g. {@link ChronoUnit#HOURS}. + * @return a future date from now. + * @since 3.0.0 + */ + public Instant future(long atMost, TemporalUnit unit) { Instant aBitLaterThanNow = Instant.now().plusMillis(1); return future(atMost, unit, aBitLaterThanNow); } @@ -56,8 +69,23 @@ public Instant future(long atMost, TimeUnit unit) { * @param unit the time unit. * @param pattern date time pattern to convert to string. * @return a string representation of a future date from now. + * @deprecated since 3.0.0. Use {@link #future(long, TemporalUnit, String)} instead. */ + @Deprecated(since = "3.0.0", forRemoval = true) public String future(long atMost, TimeUnit unit, String pattern) { + return future(atMost, unit.toChronoUnit(), pattern); + } + + /** + * Generates and converts to string representation a future date from now. + * + * @param atMost at most this amount of time ahead from now exclusive. + * @param unit the temporal unit, e.g. {@link ChronoUnit#HOURS}. + * @param pattern date time pattern to convert to string. + * @return a string representation of a future date from now. + * @since 3.0.0 + */ + public String future(long atMost, TemporalUnit unit, String pattern) { return formatInstant(future(atMost, unit), pattern); } @@ -68,9 +96,24 @@ public String future(long atMost, TimeUnit unit, String pattern) { * @param minimum the minimum amount of time in the future from now. * @param unit the time unit. * @return a future date from now, with a minimum time. + * @deprecated since 3.0.0. Use {@link #future(long, long, TemporalUnit)} instead. */ + @Deprecated(since = "3.0.0", forRemoval = true) public Instant future(long atMost, long minimum, TimeUnit unit) { - Instant minimumDate = Instant.now().plus(minimum, unit.toChronoUnit()); + return future(atMost, minimum, unit.toChronoUnit()); + } + + /** + * Generates a future date from now, with a minimum time. + * + * @param atMost at most this amount of time ahead from now exclusive. + * @param minimum the minimum amount of time in the future from now. + * @param unit the temporal unit, e.g. {@link ChronoUnit#HOURS}. + * @return a future date from now, with a minimum time. + * @since 3.0.0 + */ + public Instant future(long atMost, long minimum, TemporalUnit unit) { + Instant minimumDate = Instant.now().atZone(ZoneId.systemDefault()).plus(minimum, unit).toInstant(); return future(atMost - minimum, unit, minimumDate); } @@ -83,8 +126,25 @@ public Instant future(long atMost, long minimum, TimeUnit unit) { * @param unit the time unit. * @param pattern date time pattern to convert to string. * @return a string representation of a future date from now, with a minimum time. + * @deprecated since 3.0.0. Use {@link #future(long, long, TemporalUnit, String)} instead. */ + @Deprecated(since = "3.0.0", forRemoval = true) public String future(long atMost, long minimum, TimeUnit unit, String pattern) { + return future(atMost, minimum, unit.toChronoUnit(), pattern); + } + + /** + * Generates and converts to string representation + * of a future date from now, with a minimum time. + * + * @param atMost at most this amount of time ahead from now exclusive. + * @param minimum the minimum amount of time in the future from now. + * @param unit the temporal unit, e.g. {@link ChronoUnit#HOURS}. + * @param pattern date time pattern to convert to string. + * @return a string representation of a future date from now, with a minimum time. + * @since 3.0.0 + */ + public String future(long atMost, long minimum, TemporalUnit unit, String pattern) { return formatInstant(future(atMost, minimum, unit), pattern); } @@ -95,11 +155,25 @@ public String future(long atMost, long minimum, TimeUnit unit, String pattern) { * @param unit the time unit. * @param referenceDate the future date relative to this date. * @return a future date relative to {@code referenceDate}. + * @deprecated since 3.0.0. Use {@link #future(long, TemporalUnit, Instant)} instead. */ + @Deprecated(since = "3.0.0", forRemoval = true) public Instant future(long atMost, TimeUnit unit, Instant referenceDate) { - long upperBoundMillis = unit.toMillis(atMost); - long futureMillis = referenceDate.toEpochMilli() + 1 + faker.random().nextLong(upperBoundMillis - 1); - return Instant.ofEpochMilli(futureMillis); + return future(atMost, unit.toChronoUnit(), referenceDate); + } + + /** + * Generates a future date relative to the {@code referenceDate}. + * + * @param atMost at most this amount of time ahead to the {@code referenceDate} exclusive. + * @param unit the temporal unit, e.g. {@link ChronoUnit#YEARS}. + * @param referenceDate the future date relative to this date. + * @return a future date relative to {@code referenceDate}. + * @since 3.0.0 + */ + public Instant future(long atMost, TemporalUnit unit, Instant referenceDate) { + Instant upperBound = referenceDate.atZone(ZoneId.systemDefault()).plus(atMost, unit).toInstant(); + return between(referenceDate.plusMillis(1), upperBound); } /** @@ -111,17 +185,34 @@ public Instant future(long atMost, TimeUnit unit, Instant referenceDate) { * @param referenceDate the future date relative to this date. * @param pattern date time pattern to convert to string. * @return a string representation of a future date relative to {@code referenceDate}. + * @deprecated since 3.0.0. Use {@link #future(long, TemporalUnit, Instant, String)} instead. */ + @Deprecated(since = "3.0.0", forRemoval = true) public String future(long atMost, TimeUnit unit, Instant referenceDate, String pattern) { + return future(atMost, unit.toChronoUnit(), referenceDate, pattern); + } + + /** + * Generates and converts to string representation + * a future date relative to the {@code referenceDate}. + * + * @param atMost at most this amount of time ahead to the {@code referenceDate} exclusive. + * @param unit the temporal unit, e.g. {@link ChronoUnit#YEARS}. + * @param referenceDate the future date relative to this date. + * @param pattern date time pattern to convert to string. + * @return a string representation of a future date relative to {@code referenceDate}. + * @since 3.0.0 + */ + public String future(long atMost, TemporalUnit unit, Instant referenceDate, String pattern) { return formatInstant(future(atMost, unit, referenceDate), pattern); } /** * Generates a past date from now. */ - public Instant past() { - long FIFTY_YEARS = TimeUnit.DAYS.toMillis(18262); - return past(faker.number().numberBetween(1, FIFTY_YEARS), MILLISECONDS); + public Instant past() { + long FIFTY_YEARS = TimeUnit.DAYS.toMillis(18262); + return past(faker.number().numberBetween(1, FIFTY_YEARS), ChronoUnit.MILLIS); } /** @@ -130,8 +221,22 @@ public Instant past() { * @param atMost at most this amount of time earlier from now exclusive. * @param unit the time unit. * @return a past date from now. + * @deprecated since 3.0.0. Use {@link #past(long, TemporalUnit)} instead. */ + @Deprecated(since = "3.0.0", forRemoval = true) public Instant past(long atMost, TimeUnit unit) { + return past(atMost, unit.toChronoUnit()); + } + + /** + * Generates a past date from now. + * + * @param atMost at most this amount of time earlier from now exclusive. + * @param unit the temporal unit, e.g. {@link ChronoUnit#DAYS}. + * @return a past date from now. + * @since 3.0.0 + */ + public Instant past(long atMost, TemporalUnit unit) { Instant aBitEarlierThanNow = Instant.now().minusMillis(1); return past(atMost, unit, aBitEarlierThanNow); } @@ -143,8 +248,23 @@ public Instant past(long atMost, TimeUnit unit) { * @param unit the time unit. * @param pattern date time pattern to convert to string. * @return a string representation of a past date from now. + * @deprecated since 3.0.0. Use {@link #past(long, TemporalUnit, String)} instead. */ + @Deprecated(since = "3.0.0", forRemoval = true) public String past(long atMost, TimeUnit unit, String pattern) { + return past(atMost, unit.toChronoUnit(), pattern); + } + + /** + * Generates a string representation of a past date from now. + * + * @param atMost at most this amount of time earlier from now exclusive. + * @param unit the temporal unit, e.g. {@link ChronoUnit#DAYS}. + * @param pattern date time pattern to convert to string. + * @return a string representation of a past date from now. + * @since 3.0.0 + */ + public String past(long atMost, TemporalUnit unit, String pattern) { return formatInstant(past(atMost, unit), pattern); } @@ -155,9 +275,24 @@ public String past(long atMost, TimeUnit unit, String pattern) { * @param minimum the minimum amount of time in the past from now. * @param unit the time unit. * @return a past date from now. + * @deprecated since 3.0.0. Use {@link #past(long, long, TemporalUnit)} instead. */ + @Deprecated(since = "3.0.0", forRemoval = true) public Instant past(long atMost, long minimum, TimeUnit unit) { - Instant minimumDate = Instant.now().minusMillis(unit.toMillis(minimum)); + return past(atMost, minimum, unit.toChronoUnit()); + } + + /** + * Generates a past date from now, with a minimum time. + * + * @param atMost at most this amount of time earlier from now exclusive. + * @param minimum the minimum amount of time in the past from now. + * @param unit the temporal unit, e.g. {@link ChronoUnit#DAYS}. + * @return a past date from now, with a minimum time. + * @since 3.0.0 + */ + public Instant past(long atMost, long minimum, TemporalUnit unit) { + Instant minimumDate = Instant.now().atZone(ZoneId.systemDefault()).minus(minimum, unit).toInstant(); return past(atMost - minimum, unit, minimumDate); } @@ -169,8 +304,24 @@ public Instant past(long atMost, long minimum, TimeUnit unit) { * @param unit the time unit. * @param pattern date time pattern to convert to string. * @return a string representation of a past date from now, with a minimum time. + * @deprecated since 3.0.0. Use {@link #past(long, long, TemporalUnit, String)} instead. */ + @Deprecated(since = "3.0.0", forRemoval = true) public String past(long atMost, long minimum, TimeUnit unit, String pattern) { + return past(atMost, minimum, unit.toChronoUnit(), pattern); + } + + /** + * Generates and converts to string representation a past date from now, with a minimum time. + * + * @param atMost at most this amount of time earlier from now exclusive. + * @param minimum the minimum amount of time in the past from now. + * @param unit the temporal unit, e.g. {@link ChronoUnit#DAYS}. + * @param pattern date time pattern to convert to string. + * @return a string representation of a past date from now, with a minimum time. + * @since 3.0.0 + */ + public String past(long atMost, long minimum, TemporalUnit unit, String pattern) { return formatInstant(past(atMost, minimum, unit), pattern); } @@ -181,12 +332,27 @@ public String past(long atMost, long minimum, TimeUnit unit, String pattern) { * @param unit the time unit. * @param referenceDate the past date relative to this date. * @return a past date relative to {@code referenceDate}. + * @deprecated since 3.0.0. Use {@link #past(long, TemporalUnit, Instant)} instead. */ + @Deprecated(since = "3.0.0", forRemoval = true) public Instant past(long atMost, TimeUnit unit, Instant referenceDate) { - long upperBoundMillis = unit.toMillis(atMost); - long pastMillis = referenceDate.toEpochMilli() - 1 - faker.random().nextLong(upperBoundMillis - 1); - return Instant.ofEpochMilli(pastMillis); + return past(atMost, unit.toChronoUnit(), referenceDate); + } + + /** + * Generates a past date relative to the {@code referenceDate}. + * + * @param atMost at most this amount of time past to the {@code referenceDate} exclusive. + * @param unit the temporal unit, e.g. {@link ChronoUnit#YEARS}. + * @param referenceDate the past date relative to this date. + * @return a past date relative to {@code referenceDate}. + * @since 3.0.0 + */ + public Instant past(long atMost, TemporalUnit unit, Instant referenceDate) { + Instant lowerBound = referenceDate.atZone(ZoneId.systemDefault()).minus(atMost, unit).toInstant(); + return between(lowerBound, referenceDate); } + /** * Generates a string representation of a past date relative to the {@code referenceDate}. * @@ -195,8 +361,24 @@ public Instant past(long atMost, TimeUnit unit, Instant referenceDate) { * @param referenceDate the past date relative to this date. * @param pattern date time pattern to convert to string. * @return a string representation of a past date relative to {@code referenceDate}. + * @deprecated since 3.0.0. Use {@link #past(long, TemporalUnit, Instant, String)} instead. */ + @Deprecated(since = "3.0.0", forRemoval = true) public String past(long atMost, TimeUnit unit, Instant referenceDate, String pattern) { + return past(atMost, unit.toChronoUnit(), referenceDate, pattern); + } + + /** + * Generates a string representation of a past date relative to the {@code referenceDate}. + * + * @param atMost at most this amount of time past to the {@code referenceDate} exclusive. + * @param unit the temporal unit, e.g. {@link ChronoUnit#YEARS}. + * @param referenceDate the past date relative to this date. + * @param pattern date time pattern to convert to string. + * @return a string representation of a past date relative to {@code referenceDate}. + * @since 3.0.0 + */ + public String past(long atMost, TemporalUnit unit, Instant referenceDate, String pattern) { return formatInstant(past(atMost, unit, referenceDate), pattern); } diff --git a/src/test/java/net/datafaker/providers/base/TimeAndDateTest.java b/src/test/java/net/datafaker/providers/base/TimeAndDateTest.java index 461ea2ab0..94301533a 100644 --- a/src/test/java/net/datafaker/providers/base/TimeAndDateTest.java +++ b/src/test/java/net/datafaker/providers/base/TimeAndDateTest.java @@ -13,6 +13,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.Period; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.concurrent.TimeUnit; @@ -23,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +@SuppressWarnings("removal") class TimeAndDateTest { private final Faker faker = new Faker(); private final TimeAndDate timeAndDate = faker.timeAndDate(); @@ -84,6 +86,68 @@ void testPastDateWithBounds() { assertThat(past.toEpochMilli()).isLessThan(now.toEpochMilli()); } + @RepeatedTest(100) + void testFutureDateWithBoundsTemporalUnit() { + Instant now = Instant.now(); + Instant future = timeAndDate.future(1, ChronoUnit.SECONDS); + assertThat(future).isBetween(now, Instant.now().plusSeconds(1)); + } + + @RepeatedTest(100) + void testFutureDateWithBoundsInYears() { + Instant now = Instant.now(); + Instant future = timeAndDate.future(10, ChronoUnit.YEARS); + Instant tenYearsLater = now.atZone(ZoneId.systemDefault()).plusYears(10).toInstant(); + assertThat(future).isBetween(now, tenYearsLater); + } + + @RepeatedTest(100) + void testFutureDateWithBoundsFromGivenMomentTemporalUnit() { + Instant moment = Instant.now().minus(10, ChronoUnit.DAYS); + Instant future = timeAndDate.future(15, MINUTES, moment); + assertThat(future).isBetween(moment, moment.plus(15, MINUTES)); + } + + @RepeatedTest(100) + void testFutureDateWithMinimumTemporalUnit() { + Instant now = Instant.now(); + Instant future = timeAndDate.future(5, 4, ChronoUnit.SECONDS); + assertThat(future) + .isBetween(now.plusMillis(3500), now.plusMillis(5500)); + } + + @RepeatedTest(100) + void testPastDateWithBoundsTemporalUnit() { + Instant now = Instant.now(); + Instant past = timeAndDate.past(100, ChronoUnit.SECONDS); + assertThat(past.toEpochMilli()).isLessThan(now.toEpochMilli()); + } + + @RepeatedTest(100) + void testPastDateWithBoundsInYears() { + Instant now = Instant.now(); + Instant past = timeAndDate.past(10, ChronoUnit.YEARS); + Instant tenYearsEarlier = now.atZone(ZoneId.systemDefault()).minusYears(10).toInstant(); + assertThat(past).isBetween(tenYearsEarlier, now); + } + + @RepeatedTest(100) + void testPastDateWithMinimumTemporalUnit() { + final long now = System.currentTimeMillis(); + Instant past = timeAndDate.past(5, 4, ChronoUnit.SECONDS); + assertThat(past.toEpochMilli()).isLessThan(now) + .isGreaterThan(now - 5500) + .isLessThan(now - 3500); + } + + @RepeatedTest(100) + void testPastDateWithReferenceDateTemporalUnit() { + Instant now = Instant.now(); + Instant past = timeAndDate.past(1, ChronoUnit.YEARS, now); + Instant oneYearEarlier = now.atZone(ZoneId.systemDefault()).minusYears(1).toInstant(); + assertThat(past).isBetween(oneYearEarlier, now); + } + @RepeatedTest(100) void testBetween() { Instant now = Instant.now(); @@ -156,6 +220,15 @@ void futureWithMask() { assertValidDate(timeAndDate.future(900, TimeUnit.DAYS, Instant.now(), pattern), pattern); } + @Test + void futureWithMaskTemporalUnit() { + String pattern = "yyyy MM.dd mm:hh:ss"; + assertValidDate(timeAndDate.future(1, ChronoUnit.HOURS, pattern), pattern); + assertValidDate(timeAndDate.future(20, 1, ChronoUnit.HOURS, pattern), pattern); + assertValidDate(timeAndDate.future(20, ChronoUnit.HOURS, Instant.now(), pattern), pattern); + assertValidDate(timeAndDate.future(2, ChronoUnit.YEARS, Instant.now(), pattern), pattern); + } + @Test void pastWithMask() { String pattern = "yyyy MM.dd mm:hh:ss"; @@ -164,6 +237,14 @@ void pastWithMask() { assertValidDate(timeAndDate.past(1, TimeUnit.DAYS, Instant.now(), pattern), pattern); } + @Test + void pastWithMaskTemporalUnit() { + String pattern = "yyyy MM.dd mm:hh:ss"; + assertValidDate(timeAndDate.past(1, ChronoUnit.DAYS, pattern), pattern); + assertValidDate(timeAndDate.past(20, 1, ChronoUnit.DAYS, pattern), pattern); + assertValidDate(timeAndDate.past(1, ChronoUnit.YEARS, Instant.now(), pattern), pattern); + } + @Test void periodTest() { Period maxPeriod = Period.of(3, 2, 1); From 93ff100159434b079788d406fd4224d6b0d766c2 Mon Sep 17 00:00:00 2001 From: Louis Deconinck Date: Sat, 12 Sep 2026 13:14:22 +0200 Subject: [PATCH 2/2] fix: keep past() lower bound exclusive in TemporalUnit overload between() includes its lower bound, so past() could return exactly referenceDate - atMost, tripping the strict > assertion in testPastDateWithReferenceDate. Mirror the original -1 - nextLong(bound-1) formula so the range stays (ref - atMost, ref) exclusive. Signed-off-by: Louis Deconinck --- src/main/java/net/datafaker/providers/base/TimeAndDate.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/datafaker/providers/base/TimeAndDate.java b/src/main/java/net/datafaker/providers/base/TimeAndDate.java index 9f6cff04e..68020f943 100644 --- a/src/main/java/net/datafaker/providers/base/TimeAndDate.java +++ b/src/main/java/net/datafaker/providers/base/TimeAndDate.java @@ -349,8 +349,10 @@ public Instant past(long atMost, TimeUnit unit, Instant referenceDate) { * @since 3.0.0 */ public Instant past(long atMost, TemporalUnit unit, Instant referenceDate) { - Instant lowerBound = referenceDate.atZone(ZoneId.systemDefault()).minus(atMost, unit).toInstant(); - return between(lowerBound, referenceDate); + Instant earliest = referenceDate.atZone(ZoneId.systemDefault()).minus(atMost, unit).toInstant(); + long boundMillis = referenceDate.toEpochMilli() - earliest.toEpochMilli(); + long pastMillis = referenceDate.toEpochMilli() - 1 - faker.random().nextLong(boundMillis - 1); + return Instant.ofEpochMilli(pastMillis); } /**