diff --git a/src/main/java/org/thejavaguy/javafaker/Faker.java b/src/main/java/org/thejavaguy/javafaker/Faker.java index a997e95..488a95a 100644 --- a/src/main/java/org/thejavaguy/javafaker/Faker.java +++ b/src/main/java/org/thejavaguy/javafaker/Faker.java @@ -109,6 +109,7 @@ public class Faker { private final Mountain mountain; private final Shipping shipping; private final Subscription subscription; + private final Temporal temporal; private final Vehicle vehicle; public Faker() { @@ -229,6 +230,7 @@ public Faker(FakeValuesService fakeValuesService, RandomService random) { this.mountain = new Mountain(this); this.shipping = new Shipping(this); this.subscription = new Subscription(this); + this.temporal = new Temporal(this); this.vehicle = new Vehicle(this); } @@ -748,6 +750,16 @@ public Subscription subscription() { return subscription; } + /** + * Returns a Temporal instance for generating date/time fake data using the modern {@code + * java.time} API. + * + * @return a {@link Temporal} instance + */ + public Temporal temporal() { + return temporal; + } + public String resolve(String key) { return this.fakeValuesService.resolve(key, this, this); } diff --git a/src/main/java/org/thejavaguy/javafaker/Temporal.java b/src/main/java/org/thejavaguy/javafaker/Temporal.java new file mode 100644 index 0000000..c52f633 --- /dev/null +++ b/src/main/java/org/thejavaguy/javafaker/Temporal.java @@ -0,0 +1,351 @@ +package org.thejavaguy.javafaker; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Period; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Generates random date/time values using the Java 8+ {@code java.time} API. + * + *

This is the modern counterpart to {@link DateAndTime}, which uses legacy {@code + * java.util.Date}/{@code Calendar}. All methods in this class return {@code java.time} types + * exclusively. + * + * @since 1.x.x + */ +public class Temporal { + + private static final int DEFAULT_MIN_AGE = 18; + private static final int DEFAULT_MAX_AGE = 65; + + private final Faker faker; + private final List zoneIds; + + protected Temporal(Faker faker) { + this.faker = faker; + Set ids = ZoneId.getAvailableZoneIds(); + this.zoneIds = new ArrayList<>(ids); + } + + // ── Instant methods ───────────────────────────────────────────────── + + /** + * Generates a future {@link Instant} from now. + * + *

A 1-second slack is added to avoid returning an instant in the past due to execution delay. + * + * @param atMost at most this amount of time ahead from now (exclusive) + * @param unit the time unit + * @return a future instant from now + */ + public Instant futureInstant(long atMost, ChronoUnit unit) { + Instant ref = Instant.now().plusSeconds(1); + return futureInstant(atMost, unit, ref); + } + + /** + * Generates a future {@link Instant} from now, with a minimum offset. + * + * @param atMost at most this amount of time ahead from now (exclusive) + * @param minimum minimum amount of time in the future from now + * @param unit the time unit + * @return a future instant from now, at least {@code minimum} ahead + */ + public Instant futureInstant(long atMost, long minimum, ChronoUnit unit) { + Instant ref = Instant.now().plus(minimum, unit); + return futureInstant(atMost - minimum, unit, ref); + } + + /** + * Generates a future {@link Instant} relative to a reference instant. + * + * @param atMost at most this amount of time ahead of {@code referenceInstant} (exclusive) + * @param unit the time unit + * @param referenceInstant the base instant + * @return a future instant relative to {@code referenceInstant} + */ + public Instant futureInstant(long atMost, ChronoUnit unit, Instant referenceInstant) { + long upperBoundMillis = Duration.of(atMost, unit).toMillis(); + long offsetMillis = 1 + faker.random().nextLong(upperBoundMillis - 1); + return referenceInstant.plusMillis(offsetMillis); + } + + /** + * Generates a past {@link Instant} from now. + * + *

A 1-second slack is subtracted to avoid returning an instant in the future due to execution + * delay. + * + * @param atMost at most this amount of time before now (exclusive) + * @param unit the time unit + * @return a past instant from now + */ + public Instant pastInstant(long atMost, ChronoUnit unit) { + Instant ref = Instant.now().minusSeconds(1); + return pastInstant(atMost, unit, ref); + } + + /** + * Generates a past {@link Instant} from now, with a minimum offset. + * + * @param atMost at most this amount of time before now (exclusive) + * @param minimum minimum amount of time in the past from now + * @param unit the time unit + * @return a past instant from now, at least {@code minimum} ago + */ + public Instant pastInstant(long atMost, long minimum, ChronoUnit unit) { + Instant ref = Instant.now().minus(minimum, unit); + return pastInstant(atMost - minimum, unit, ref); + } + + /** + * Generates a past {@link Instant} relative to a reference instant. + * + * @param atMost at most this amount of time before {@code referenceInstant} (exclusive) + * @param unit the time unit + * @param referenceInstant the base instant + * @return a past instant relative to {@code referenceInstant} + */ + public Instant pastInstant(long atMost, ChronoUnit unit, Instant referenceInstant) { + long upperBoundMillis = Duration.of(atMost, unit).toMillis(); + long offsetMillis = 1 + faker.random().nextLong(upperBoundMillis - 1); + return referenceInstant.minusMillis(offsetMillis); + } + + /** + * Generates a random {@link Instant} between two instants. + * + * @param from the lower bound (inclusive) + * @param to the upper bound (exclusive) + * @return a random instant between {@code from} and {@code to} + * @throws IllegalArgumentException if {@code to} is before {@code from} + */ + public Instant instantBetween(Instant from, Instant to) { + if (to.isBefore(from)) { + throw new IllegalArgumentException( + "Invalid instant range, the upper bound is before the lower bound."); + } + if (from.equals(to)) { + return from; + } + long offsetMillis = faker.random().nextLong(to.toEpochMilli() - from.toEpochMilli()); + return from.plusMillis(offsetMillis); + } + + // ── LocalDateTime methods ─────────────────────────────────────────── + + /** + * Generates a future {@link LocalDateTime} from now (UTC). + * + * @param atMost at most this amount of time ahead from now (exclusive) + * @param unit the time unit + * @return a future local date-time + */ + public LocalDateTime futureLocalDateTime(long atMost, ChronoUnit unit) { + return LocalDateTime.ofInstant(futureInstant(atMost, unit), ZoneOffset.UTC); + } + + /** + * Generates a future {@link LocalDateTime} from now (UTC), with a minimum offset. + * + * @param atMost at most this amount of time ahead from now (exclusive) + * @param minimum minimum amount of time in the future from now + * @param unit the time unit + * @return a future local date-time, at least {@code minimum} ahead + */ + public LocalDateTime futureLocalDateTime(long atMost, long minimum, ChronoUnit unit) { + return LocalDateTime.ofInstant(futureInstant(atMost, minimum, unit), ZoneOffset.UTC); + } + + /** + * Generates a past {@link LocalDateTime} from now (UTC). + * + * @param atMost at most this amount of time before now (exclusive) + * @param unit the time unit + * @return a past local date-time + */ + public LocalDateTime pastLocalDateTime(long atMost, ChronoUnit unit) { + return LocalDateTime.ofInstant(pastInstant(atMost, unit), ZoneOffset.UTC); + } + + /** + * Generates a past {@link LocalDateTime} from now (UTC), with a minimum offset. + * + * @param atMost at most this amount of time before now (exclusive) + * @param minimum minimum amount of time in the past from now + * @param unit the time unit + * @return a past local date-time, at least {@code minimum} ago + */ + public LocalDateTime pastLocalDateTime(long atMost, long minimum, ChronoUnit unit) { + return LocalDateTime.ofInstant(pastInstant(atMost, minimum, unit), ZoneOffset.UTC); + } + + /** + * Generates a random {@link LocalDateTime} between two local date-times. + * + *

Both bounds are interpreted as UTC for the internal calculation. + * + * @param from the lower bound (inclusive) + * @param to the upper bound (exclusive) + * @return a random local date-time between {@code from} and {@code to} + * @throws IllegalArgumentException if {@code to} is before {@code from} + */ + public LocalDateTime localDateTimeBetween(LocalDateTime from, LocalDateTime to) { + Instant fromInstant = from.toInstant(ZoneOffset.UTC); + Instant toInstant = to.toInstant(ZoneOffset.UTC); + return LocalDateTime.ofInstant(instantBetween(fromInstant, toInstant), ZoneOffset.UTC); + } + + // ── LocalDate methods ─────────────────────────────────────────────── + + /** + * Generates a future {@link LocalDate} from today. + * + * @param atMost at most this many days ahead from today (exclusive) + * @return a future local date + */ + public LocalDate futureLocalDate(int atMost) { + int offset = 1 + faker.random().nextInt(atMost); + return LocalDate.now().plusDays(offset); + } + + /** + * Generates a future {@link LocalDate} from today, with a minimum offset. + * + * @param atMost at most this many days ahead from today (exclusive) + * @param minimum minimum number of days in the future from today + * @return a future local date, at least {@code minimum} days ahead + */ + public LocalDate futureLocalDate(int atMost, int minimum) { + int range = atMost - minimum; + int offset = minimum + 1 + faker.random().nextInt(range); + return LocalDate.now().plusDays(offset); + } + + /** + * Generates a past {@link LocalDate} from today. + * + * @param atMost at most this many days before today (exclusive) + * @return a past local date + */ + public LocalDate pastLocalDate(int atMost) { + int offset = 1 + faker.random().nextInt(atMost); + return LocalDate.now().minusDays(offset); + } + + /** + * Generates a past {@link LocalDate} from today, with a minimum offset. + * + * @param atMost at most this many days before today (exclusive) + * @param minimum minimum number of days in the past from today + * @return a past local date, at least {@code minimum} days ago + */ + public LocalDate pastLocalDate(int atMost, int minimum) { + int range = atMost - minimum; + int offset = minimum + 1 + faker.random().nextInt(range); + return LocalDate.now().minusDays(offset); + } + + /** + * Generates a random {@link LocalDate} between two dates. + * + * @param from the lower bound (inclusive) + * @param to the upper bound (exclusive) + * @return a random local date between {@code from} and {@code to} + * @throws IllegalArgumentException if {@code to} is before {@code from} + */ + public LocalDate localDateBetween(LocalDate from, LocalDate to) { + if (to.isBefore(from)) { + throw new IllegalArgumentException( + "Invalid date range, the upper bound date is before the lower bound."); + } + if (from.equals(to)) { + return from; + } + long daysBetween = from.until(to, ChronoUnit.DAYS); + long offset = faker.random().nextLong(daysBetween); + return from.plusDays(offset); + } + + /** + * Generates a random birthday {@link LocalDate} between 18 and 65 years ago. + * + * @return a random birthday local date + */ + public LocalDate birthdayLocalDate() { + return birthdayLocalDate(DEFAULT_MIN_AGE, DEFAULT_MAX_AGE); + } + + /** + * Generates a random birthday {@link LocalDate} between two ages. + * + * @param minAge the minimum age in years + * @param maxAge the maximum age in years + * @return a random birthday local date between {@code minAge} and {@code maxAge} years ago + * @throws IllegalArgumentException if {@code maxAge} is less than {@code minAge} + */ + public LocalDate birthdayLocalDate(int minAge, int maxAge) { + if (maxAge < minAge) { + throw new IllegalArgumentException("Invalid age range, maxAge is less than minAge."); + } + LocalDate today = LocalDate.now(); + LocalDate from = today.minusYears(maxAge); + LocalDate to = today.minusYears(minAge); + return localDateBetween(from, to); + } + + // ── Utility methods ───────────────────────────────────────────────── + + /** + * Generates a random {@link LocalTime}. + * + * @return a random local time (hour 0–23, minute 0–59, second 0–59) + */ + public LocalTime localTime() { + int hour = faker.random().nextInt(24); + int minute = faker.random().nextInt(60); + int second = faker.random().nextInt(60); + return LocalTime.of(hour, minute, second); + } + + /** + * Generates a random {@link Duration} between 1 second and the given maximum. + * + * @param maxSeconds the maximum duration in seconds (exclusive) + * @return a random duration + */ + public Duration duration(long maxSeconds) { + long seconds = 1 + faker.random().nextLong(maxSeconds - 1); + return Duration.ofSeconds(seconds); + } + + /** + * Generates a random {@link Period} between 1 day and the given maximum days. + * + * @param maxDays the maximum period in days (exclusive) + * @return a random period + */ + public Period period(int maxDays) { + int days = 1 + faker.random().nextInt(maxDays - 1); + return Period.ofDays(days); + } + + /** + * Returns a random IANA {@link ZoneId}. + * + * @return a random zone ID, e.g., "America/New_York", "Europe/London" + */ + public ZoneId zoneId() { + String id = zoneIds.get(faker.random().nextInt(zoneIds.size())); + return ZoneId.of(id); + } +} diff --git a/src/test/java/org/thejavaguy/javafaker/TemporalTest.java b/src/test/java/org/thejavaguy/javafaker/TemporalTest.java new file mode 100644 index 0000000..eff69aa --- /dev/null +++ b/src/test/java/org/thejavaguy/javafaker/TemporalTest.java @@ -0,0 +1,253 @@ +package org.thejavaguy.javafaker; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Period; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import org.junit.jupiter.api.Test; + +public class TemporalTest extends AbstractFakerTest { + + // ── Instant: future ───────────────────────────────────────────────── + + @Test + void futureInstant() { + Instant now = Instant.now(); + Instant result = faker.temporal().futureInstant(1, ChronoUnit.HOURS); + assertThat(result).isAfter(now); + assertThat(result).isBefore(now.plus(1, ChronoUnit.HOURS).plusSeconds(2)); + } + + @Test + void futureInstantWithMinimum() { + Instant now = Instant.now(); + Instant result = faker.temporal().futureInstant(10, 5, ChronoUnit.MINUTES); + assertThat(result).isAfter(now.plus(5, ChronoUnit.MINUTES).minusSeconds(1)); + assertThat(result).isBefore(now.plus(10, ChronoUnit.MINUTES).plusSeconds(2)); + } + + @Test + void futureInstantRelativeToReference() { + Instant reference = Instant.parse("2020-01-01T00:00:00Z"); + Instant result = faker.temporal().futureInstant(1, ChronoUnit.DAYS, reference); + assertThat(result).isAfter(reference); + assertThat(result).isBefore(reference.plus(1, ChronoUnit.DAYS)); + } + + // ── Instant: past ─────────────────────────────────────────────────── + + @Test + void pastInstant() { + Instant now = Instant.now(); + Instant result = faker.temporal().pastInstant(1, ChronoUnit.HOURS); + assertThat(result).isBefore(now); + assertThat(result).isAfter(now.minus(1, ChronoUnit.HOURS).minusSeconds(2)); + } + + @Test + void pastInstantWithMinimum() { + Instant now = Instant.now(); + Instant result = faker.temporal().pastInstant(10, 5, ChronoUnit.MINUTES); + assertThat(result).isBefore(now.minus(5, ChronoUnit.MINUTES).plusSeconds(1)); + assertThat(result).isAfter(now.minus(10, ChronoUnit.MINUTES).minusSeconds(2)); + } + + @Test + void pastInstantRelativeToReference() { + Instant reference = Instant.parse("2020-01-01T00:00:00Z"); + Instant result = faker.temporal().pastInstant(1, ChronoUnit.DAYS, reference); + assertThat(result).isBefore(reference); + assertThat(result).isAfter(reference.minus(1, ChronoUnit.DAYS)); + } + + // ── Instant: between ──────────────────────────────────────────────── + + @Test + void instantBetween() { + Instant from = Instant.parse("2020-01-01T00:00:00Z"); + Instant to = Instant.parse("2020-12-31T23:59:59Z"); + Instant result = faker.temporal().instantBetween(from, to); + assertThat(result).isAfterOrEqualTo(from); + assertThat(result).isBefore(to); + } + + @Test + void instantBetweenSameInstant() { + Instant instant = Instant.parse("2020-06-15T12:00:00Z"); + assertThat(faker.temporal().instantBetween(instant, instant)).isEqualTo(instant); + } + + @Test + void instantBetweenInvalidRange() { + Instant from = Instant.parse("2020-12-31T00:00:00Z"); + Instant to = Instant.parse("2020-01-01T00:00:00Z"); + assertThatThrownBy(() -> faker.temporal().instantBetween(from, to)) + .isInstanceOf(IllegalArgumentException.class); + } + + // ── LocalDateTime: future ─────────────────────────────────────────── + + @Test + void futureLocalDateTime() { + LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC); + LocalDateTime result = faker.temporal().futureLocalDateTime(1, ChronoUnit.HOURS); + assertThat(result).isAfter(now); + } + + @Test + void futureLocalDateTimeWithMinimum() { + LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC); + LocalDateTime result = faker.temporal().futureLocalDateTime(10, 5, ChronoUnit.MINUTES); + assertThat(result).isAfter(now.plusMinutes(4)); + } + + // ── LocalDateTime: past ───────────────────────────────────────────── + + @Test + void pastLocalDateTime() { + LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC); + LocalDateTime result = faker.temporal().pastLocalDateTime(1, ChronoUnit.HOURS); + assertThat(result).isBefore(now); + } + + @Test + void pastLocalDateTimeWithMinimum() { + LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC); + LocalDateTime result = faker.temporal().pastLocalDateTime(10, 5, ChronoUnit.MINUTES); + assertThat(result).isBefore(now.minusMinutes(4)); + } + + // ── LocalDateTime: between ────────────────────────────────────────── + + @Test + void localDateTimeBetween() { + LocalDateTime from = LocalDateTime.of(2020, 1, 1, 0, 0); + LocalDateTime to = LocalDateTime.of(2020, 12, 31, 23, 59); + LocalDateTime result = faker.temporal().localDateTimeBetween(from, to); + assertThat(result).isAfterOrEqualTo(from); + assertThat(result).isBefore(to); + } + + // ── LocalDate: future ─────────────────────────────────────────────── + + @Test + void futureLocalDate() { + LocalDate today = LocalDate.now(); + LocalDate result = faker.temporal().futureLocalDate(30); + assertThat(result).isAfter(today); + assertThat(result).isBeforeOrEqualTo(today.plusDays(30)); + } + + @Test + void futureLocalDateWithMinimum() { + LocalDate today = LocalDate.now(); + LocalDate result = faker.temporal().futureLocalDate(30, 10); + assertThat(result).isAfter(today.plusDays(10)); + assertThat(result).isBeforeOrEqualTo(today.plusDays(30)); + } + + // ── LocalDate: past ───────────────────────────────────────────────── + + @Test + void pastLocalDate() { + LocalDate today = LocalDate.now(); + LocalDate result = faker.temporal().pastLocalDate(30); + assertThat(result).isBefore(today); + assertThat(result).isAfterOrEqualTo(today.minusDays(30)); + } + + @Test + void pastLocalDateWithMinimum() { + LocalDate today = LocalDate.now(); + LocalDate result = faker.temporal().pastLocalDate(30, 10); + assertThat(result).isBefore(today.minusDays(10)); + assertThat(result).isAfterOrEqualTo(today.minusDays(30)); + } + + // ── LocalDate: between ────────────────────────────────────────────── + + @Test + void localDateBetween() { + LocalDate from = LocalDate.of(2020, 1, 1); + LocalDate to = LocalDate.of(2020, 12, 31); + LocalDate result = faker.temporal().localDateBetween(from, to); + assertThat(result).isAfterOrEqualTo(from); + assertThat(result).isBefore(to); + } + + @Test + void localDateBetweenSameDate() { + LocalDate date = LocalDate.of(2020, 6, 15); + assertThat(faker.temporal().localDateBetween(date, date)).isEqualTo(date); + } + + @Test + void localDateBetweenInvalidRange() { + LocalDate from = LocalDate.of(2020, 12, 31); + LocalDate to = LocalDate.of(2020, 1, 1); + assertThatThrownBy(() -> faker.temporal().localDateBetween(from, to)) + .isInstanceOf(IllegalArgumentException.class); + } + + // ── LocalDate: birthday ───────────────────────────────────────────── + + @Test + void birthdayLocalDate() { + LocalDate today = LocalDate.now(); + LocalDate result = faker.temporal().birthdayLocalDate(); + assertThat(result).isAfterOrEqualTo(today.minusYears(65)); + assertThat(result).isBefore(today.minusYears(18)); + } + + @Test + void birthdayLocalDateWithAgeRange() { + LocalDate today = LocalDate.now(); + LocalDate result = faker.temporal().birthdayLocalDate(21, 30); + assertThat(result).isAfterOrEqualTo(today.minusYears(30)); + assertThat(result).isBefore(today.minusYears(21)); + } + + @Test + void birthdayLocalDateInvalidRange() { + assertThatThrownBy(() -> faker.temporal().birthdayLocalDate(30, 20)) + .isInstanceOf(IllegalArgumentException.class); + } + + // ── Utility methods ───────────────────────────────────────────────── + + @Test + void localTime() { + LocalTime result = faker.temporal().localTime(); + assertThat(result).isNotNull(); + assertThat(result.getHour()).isBetween(0, 23); + assertThat(result.getMinute()).isBetween(0, 59); + assertThat(result.getSecond()).isBetween(0, 59); + } + + @Test + void duration() { + Duration result = faker.temporal().duration(3600); + assertThat(result.getSeconds()).isBetween(1L, 3599L); + } + + @Test + void period() { + Period result = faker.temporal().period(365); + assertThat(result.getDays()).isBetween(1, 364); + } + + @Test + void zoneId() { + ZoneId result = faker.temporal().zoneId(); + assertThat(result).isNotNull(); + assertThat(ZoneId.getAvailableZoneIds()).contains(result.getId()); + } +}