diff --git a/manual/src/docs/asciidoc/chapters/04-spring-data.adoc b/manual/src/docs/asciidoc/chapters/03-spring-data.adoc similarity index 100% rename from manual/src/docs/asciidoc/chapters/04-spring-data.adoc rename to manual/src/docs/asciidoc/chapters/03-spring-data.adoc diff --git a/manual/src/docs/asciidoc/chapters/03-random.adoc b/manual/src/docs/asciidoc/chapters/04-random.adoc similarity index 90% rename from manual/src/docs/asciidoc/chapters/03-random.adoc rename to manual/src/docs/asciidoc/chapters/04-random.adoc index 3b2d070..2cfc84b 100644 --- a/manual/src/docs/asciidoc/chapters/03-random.adoc +++ b/manual/src/docs/asciidoc/chapters/04-random.adoc @@ -179,3 +179,22 @@ include::../../../test/java/org/stubit/random/RandomDocTest.java[tag=aLocalDate] ---- include::../../../test/java/org/stubit/random/RandomDocTest.java[tag=aLocalDateInRange] ---- + +== Random Time + +The link:{javadoc-url}/random/org/stubit/random/RandomLocalTime.html[`RandomLocalTime` class] allows to generate random LocalTimes. + +[source,java,indent=0] +---- +include::../../../test/java/org/stubit/random/RandomDocTest.java[tag=aLocalTimeBetween] +---- + +[source,java,indent=0] +---- +include::../../../test/java/org/stubit/random/RandomDocTest.java[tag=aLocalTime] +---- + +[source,java,indent=0] +---- +include::../../../test/java/org/stubit/random/RandomDocTest.java[tag=aLocalTimeInRange] +---- diff --git a/manual/src/docs/asciidoc/index.adoc b/manual/src/docs/asciidoc/index.adoc index 9ecd193..9a7bc51 100644 --- a/manual/src/docs/asciidoc/index.adoc +++ b/manual/src/docs/asciidoc/index.adoc @@ -12,5 +12,5 @@ Michael Kutz include::chapters/00-introduction.adoc[leveloffset=1] include::chapters/01-getting-started.adoc[leveloffset=1] include::chapters/02-http.adoc[leveloffset=1] -include::chapters/03-random.adoc[leveloffset=1] -include::chapters/04-spring-data.adoc[leveloffset=1] +include::chapters/03-spring-data.adoc[leveloffset=1] +include::chapters/04-random.adoc[leveloffset=1] diff --git a/manual/src/test/java/org/stubit/random/RandomDocTest.java b/manual/src/test/java/org/stubit/random/RandomDocTest.java index 4327a25..aaed564 100644 --- a/manual/src/test/java/org/stubit/random/RandomDocTest.java +++ b/manual/src/test/java/org/stubit/random/RandomDocTest.java @@ -10,6 +10,9 @@ import static org.stubit.random.RandomLocalDate.aLocalDate; import static org.stubit.random.RandomLocalDate.aLocalDateBetween; import static org.stubit.random.RandomLocalDate.aLocalDateInRange; +import static org.stubit.random.RandomLocalTime.aLocalDateBetween; +import static org.stubit.random.RandomLocalTime.aLocalTime; +import static org.stubit.random.RandomLocalTime.aLocalTimeInRange; import static org.stubit.random.RandomNumber.aLong; import static org.stubit.random.RandomNumber.aLongBetween; import static org.stubit.random.RandomNumber.aNegativeInt; @@ -28,6 +31,7 @@ import java.time.DayOfWeek; import java.time.Duration; import java.time.LocalDate; +import java.time.LocalTime; import java.time.Month; import java.time.Year; import java.util.List; @@ -307,4 +311,48 @@ void randomLocalDateBuilder_examples() { assertThat(someTuesday1999.getYear()).isEqualTo(1999); // end::aLocalDate[] } + + @Test + void randomLocalTime_examples() { + // tag::aLocalTimeBetween[] + LocalTime someTimeBusinessHours = aLocalDateBetween(LocalTime.of(9, 0), LocalTime.of(17, 0, 0)); + assertThat(someTimeBusinessHours).isBetween(LocalTime.of(9, 0), LocalTime.of(17, 0)); + // end::aLocalTimeBetween[] + } + + @Test + void randomLocalTimeInRangeBuilder_examples() { + // tag::aLocalTimeInRange[] + LocalTime someLocalTime = aLocalTimeInRange().build(); + assertThat(someLocalTime).isBetween(LocalTime.MIN, LocalTime.MAX); + + LocalTime someFutureTime = aLocalTimeInRange().future().build(); + assertThat(someFutureTime).isAfterOrEqualTo(LocalTime.now()); + + LocalTime somePastTime = aLocalTimeInRange().past().build(); + assertThat(somePastTime).isBeforeOrEqualTo(LocalTime.now()); + + LocalTime someTimeAfterNoon = aLocalTimeInRange().after(LocalTime.NOON).build(); + assertThat(someTimeAfterNoon).isAfterOrEqualTo(LocalTime.NOON); + // end::aLocalTimeInRange[] + } + + @Test + void randomLocalTimeBuilder_examples() { + // tag::aLocalTime[] + LocalTime someLocalTime = aLocalTime().build(); + assertThat(someLocalTime).isBetween(LocalTime.MIN, LocalTime.MAX); + + LocalTime someTimeAt12 = aLocalTime().hour(12).build(); + assertThat(someTimeAt12.getHour()).isEqualTo(12); + + LocalTime someHalfTime = aLocalTime().minute(30).build(); + assertThat(someHalfTime.getMinute()).isEqualTo(30); + + LocalTime someTimeAlmostFull = aLocalTime().minute(59).second(59).nano(999_999_999).build(); + assertThat(someTimeAlmostFull.getMinute()).isEqualTo(59); + assertThat(someTimeAlmostFull.getSecond()).isEqualTo(59); + assertThat(someTimeAlmostFull.getNano()).isEqualTo(999_999_999); + // end::aLocalTime[] + } } diff --git a/modules/random/src/main/java/org/stubit/random/RandomLocalDate.java b/modules/random/src/main/java/org/stubit/random/RandomLocalDate.java index 1c900c7..2d41d2a 100644 --- a/modules/random/src/main/java/org/stubit/random/RandomLocalDate.java +++ b/modules/random/src/main/java/org/stubit/random/RandomLocalDate.java @@ -14,12 +14,12 @@ public class RandomLocalDate { private RandomLocalDate() {} /** - * @param afterInclusive the minimum value (inclusive) - * @param beforeInclusive the maximum value (inclusive) - * @return a random {@link LocalDate} between {@code afterInclusive} and {@code beforeInclusive} + * @param after the minimum value (inclusive) + * @param before the maximum value (inclusive) + * @return a random {@link LocalDate} between {@code after} and {@code before} */ - public static LocalDate aLocalDateBetween(LocalDate afterInclusive, LocalDate beforeInclusive) { - return aLocalDateInRange().after(afterInclusive).before(beforeInclusive).build(); + public static LocalDate aLocalDateBetween(LocalDate after, LocalDate before) { + return aLocalDateInRange().after(after).before(before).build(); } /** @@ -57,9 +57,9 @@ public static class LocalDateInRangeBuilder { private LocalDate after; private LocalDate before; - private LocalDateInRangeBuilder(LocalDate min, LocalDate max) { - this.after = min; - this.before = max; + private LocalDateInRangeBuilder(LocalDate after, LocalDate before) { + this.after = after; + this.before = before; } /** @@ -81,32 +81,32 @@ public LocalDateInRangeBuilder future() { } /** - * @param afterIncluding the minimum value (inclusive) + * @param after the minimum value (inclusive) * @return this - * @throws IllegalArgumentException if {@code afterIncluding} is after {@link #before} + * @throws IllegalArgumentException if {@code after} is after {@link #before} */ - public LocalDateInRangeBuilder after(LocalDate afterIncluding) { - if (afterIncluding.isAfter(before)) { + public LocalDateInRangeBuilder after(LocalDate after) { + if (after.isAfter(before)) { throw new IllegalArgumentException( "Can't set after to %s, as it must not be greater than before (%s)" - .formatted(afterIncluding, before)); + .formatted(after, before)); } - this.after = afterIncluding; + this.after = after; return this; } /** - * @param beforeIncluding the maximum value (inclusive) + * @param before the maximum value (inclusive) * @return this - * @throws IllegalArgumentException if {@code beforeIncluding} is before {@link #after} + * @throws IllegalArgumentException if {@code before} is before {@link #after} */ - public LocalDateInRangeBuilder before(LocalDate beforeIncluding) { - if (beforeIncluding.isBefore(after)) { + public LocalDateInRangeBuilder before(LocalDate before) { + if (before.isBefore(after)) { throw new IllegalArgumentException( "Can't set before to %s, as it must not be less than after (%s)" - .formatted(beforeIncluding, after)); + .formatted(before, after)); } - this.before = beforeIncluding; + this.before = before; return this; } diff --git a/modules/random/src/main/java/org/stubit/random/RandomLocalTime.java b/modules/random/src/main/java/org/stubit/random/RandomLocalTime.java new file mode 100644 index 0000000..7378e32 --- /dev/null +++ b/modules/random/src/main/java/org/stubit/random/RandomLocalTime.java @@ -0,0 +1,168 @@ +package org.stubit.random; + +import java.security.SecureRandom; +import java.time.LocalTime; +import org.jspecify.annotations.NullMarked; + +/** Generates random {@link LocalTime}s. */ +@NullMarked +public class RandomLocalTime { + + private RandomLocalTime() {} + + /** + * @param after the minimum value (inclusive) + * @param before the maximum value (inclusive) + * @return a random {@link LocalTime} between {@code after} and {@code before} + */ + public static LocalTime aLocalDateBetween(LocalTime after, LocalTime before) { + return aLocalTimeInRange().after(after).before(before).build(); + } + + /** + * @return a random {@link LocalTime} between {@link LocalTime#MIN} and now + */ + public static LocalTime aPastLocalTime() { + return aLocalTimeInRange().before(LocalTime.now()).build(); + } + + /** + * @return a random {@link LocalTime} between now and {@link LocalTime#MAX} + */ + public static LocalTime aFutureLocalTime() { + return aLocalTimeInRange().after(LocalTime.now()).build(); + } + + /** + * @return a {@link RandomLocalTime.LocalTimeInRangeBuilder} to configure the random {@link + * LocalTime} + */ + public static LocalTimeInRangeBuilder aLocalTimeInRange() { + return new LocalTimeInRangeBuilder(LocalTime.MIN, LocalTime.MAX); + } + + /** + * @return a {@link LocalTimeBuilder} to configure the random {@link LocalTime} + */ + public static LocalTimeBuilder aLocalTime() { + return new LocalTimeBuilder(); + } + + /** Builds a random {@link LocalTime} within a specified range. */ + public static class LocalTimeInRangeBuilder { + + private final SecureRandom secureRandom = new SecureRandom(); + private LocalTime after; + private LocalTime before; + + private LocalTimeInRangeBuilder(LocalTime after, LocalTime before) { + this.after = after; + this.before = before; + } + + /** + * Sets {@link #after} to {@link LocalTime#MIN} and {@link #before} to now. + * + * @return this + */ + public LocalTimeInRangeBuilder past() { + return after(LocalTime.MIN).before(LocalTime.now().minusSeconds(1)); + } + + /** + * Sets {@link #after} to now and {@link #before} to {@link LocalTime#MAX}. + * + * @return this + */ + public LocalTimeInRangeBuilder future() { + return after(LocalTime.now().plusSeconds(1)).before(LocalTime.MAX); + } + + /** + * @param after the minimum value (inclusive). + * @return this + */ + public LocalTimeInRangeBuilder after(LocalTime after) { + if (after.isAfter(before)) { + throw new IllegalArgumentException( + "Can't set after to %s, as it must not be greater than before (%s)" + .formatted(after, before)); + } + this.after = after; + return this; + } + + /** + * @param before the minimum value (inclusive) + * @return this builder + */ + public LocalTimeInRangeBuilder before(LocalTime before) { + if (before.isBefore(after)) { + throw new IllegalArgumentException( + "Can't set before to %s, as it must not be less than after (%s)" + .formatted(before, after)); + } + this.before = before; + return this; + } + + /** + * @return a random {@link LocalTime} between {@link #after} and {@link #before} + */ + public LocalTime build() { + long minDayNano = after.toNanoOfDay(); + long maxDayNano = before.toNanoOfDay() + 1; + long randomDayNano = minDayNano + secureRandom.nextLong(0, maxDayNano - minDayNano); + return LocalTime.ofNanoOfDay(randomDayNano); + } + } + + /** Builds a random {@link LocalTime} with specified field values (e.g. hour, minute). */ + public static class LocalTimeBuilder { + + private LocalTime localTime = RandomLocalTime.aLocalTimeInRange().build(); + + /** + * @param hour the hour for the random {@link LocalTime} + * @return this + */ + public LocalTimeBuilder hour(int hour) { + localTime = localTime.withHour(hour); + return this; + } + + /** + * @param minute the minute for the random {@link LocalTime} + * @return this + */ + public LocalTimeBuilder minute(int minute) { + localTime = localTime.withMinute(minute); + return this; + } + + /** + * @param second the second for the random {@link LocalTime} + * @return this + */ + public LocalTimeBuilder second(int second) { + localTime = localTime.withSecond(second); + return this; + } + + /** + * @param nano the nanosecond for the random {@link LocalTime} + * @return this + */ + public LocalTimeBuilder nano(int nano) { + localTime = localTime.withNano(nano); + return this; + } + + /** + * @return the random {@link LocalTime} + */ + public LocalTime build() { + return localTime; + } + } +} diff --git a/modules/random/src/test/java/org/stubit/random/RandomLocalTimeBuilderTest.java b/modules/random/src/test/java/org/stubit/random/RandomLocalTimeBuilderTest.java new file mode 100644 index 0000000..d3b05ed --- /dev/null +++ b/modules/random/src/test/java/org/stubit/random/RandomLocalTimeBuilderTest.java @@ -0,0 +1,108 @@ +package org.stubit.random; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import java.time.DateTimeException; +import java.time.LocalTime; +import org.junit.jupiter.api.Test; + +class RandomLocalTimeBuilderTest { + + @Test + void aLocalTime() { + assertThat(RandomLocalTime.aLocalTime().build()).isBetween(LocalTime.MIN, LocalTime.MAX); + } + + @Test + void hour() { + int expectedHour = 23; + LocalTime radomLocalTime = RandomLocalTime.aLocalTime().hour(expectedHour).build(); + assertThat(radomLocalTime.getHour()).isEqualTo(expectedHour); + } + + @Test + void hour_negative() { + var localTimeBuilder = RandomLocalTime.aLocalTime(); + assertThatExceptionOfType(DateTimeException.class) + .isThrownBy(() -> localTimeBuilder.hour(-1)) + .withMessage("Invalid value for HourOfDay (valid values 0 - 23): -1"); + } + + @Test + void hour_too_big() { + var localTimeBuilder = RandomLocalTime.aLocalTime(); + assertThatExceptionOfType(DateTimeException.class) + .isThrownBy(() -> localTimeBuilder.hour(24)) + .withMessage("Invalid value for HourOfDay (valid values 0 - 23): 24"); + } + + @Test + void minute() { + int expectedMinute = 59; + LocalTime radomLocalTime = RandomLocalTime.aLocalTime().minute(expectedMinute).build(); + assertThat(radomLocalTime.getMinute()).isEqualTo(expectedMinute); + } + + @Test + void minute_negative() { + var localTimeBuilder = RandomLocalTime.aLocalTime(); + assertThatExceptionOfType(DateTimeException.class) + .isThrownBy(() -> localTimeBuilder.minute(-1)) + .withMessage("Invalid value for MinuteOfHour (valid values 0 - 59): -1"); + } + + @Test + void minute_too_big() { + var localTimeBuilder = RandomLocalTime.aLocalTime(); + assertThatExceptionOfType(DateTimeException.class) + .isThrownBy(() -> localTimeBuilder.minute(60)) + .withMessage("Invalid value for MinuteOfHour (valid values 0 - 59): 60"); + } + + @Test + void second() { + int expectedSecond = 59; + LocalTime radomLocalTime = RandomLocalTime.aLocalTime().second(expectedSecond).build(); + assertThat(radomLocalTime.getSecond()).isEqualTo(expectedSecond); + } + + @Test + void second_negative() { + var localTimeBuilder = RandomLocalTime.aLocalTime(); + assertThatExceptionOfType(DateTimeException.class) + .isThrownBy(() -> localTimeBuilder.second(-1)) + .withMessage("Invalid value for SecondOfMinute (valid values 0 - 59): -1"); + } + + @Test + void second_too_big() { + var localTimeBuilder = RandomLocalTime.aLocalTime(); + assertThatExceptionOfType(DateTimeException.class) + .isThrownBy(() -> localTimeBuilder.second(60)) + .withMessage("Invalid value for SecondOfMinute (valid values 0 - 59): 60"); + } + + @Test + void nano() { + int expectedNano = 999999999; + LocalTime radomLocalTime = RandomLocalTime.aLocalTime().nano(expectedNano).build(); + assertThat(radomLocalTime.getNano()).isEqualTo(expectedNano); + } + + @Test + void nano_negative() { + var localTimeBuilder = RandomLocalTime.aLocalTime(); + assertThatExceptionOfType(DateTimeException.class) + .isThrownBy(() -> localTimeBuilder.nano(-1)) + .withMessage("Invalid value for NanoOfSecond (valid values 0 - 999999999): -1"); + } + + @Test + void nano_too_big() { + var localTimeBuilder = RandomLocalTime.aLocalTime(); + assertThatExceptionOfType(DateTimeException.class) + .isThrownBy(() -> localTimeBuilder.nano(1000000000)) + .withMessage("Invalid value for NanoOfSecond (valid values 0 - 999999999): 1000000000"); + } +} diff --git a/modules/random/src/test/java/org/stubit/random/RandomLocalTimeInRangeBuilderTest.java b/modules/random/src/test/java/org/stubit/random/RandomLocalTimeInRangeBuilderTest.java new file mode 100644 index 0000000..cc98b5a --- /dev/null +++ b/modules/random/src/test/java/org/stubit/random/RandomLocalTimeInRangeBuilderTest.java @@ -0,0 +1,68 @@ +package org.stubit.random; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import java.time.LocalTime; +import org.junit.jupiter.api.Test; + +class RandomLocalTimeInRangeBuilderTest { + + @Test + void aLocalTimeInRange() { + assertThat(RandomLocalTime.aLocalTimeInRange().build()).isBetween(LocalTime.MIN, LocalTime.MAX); + } + + @Test + void after() { + LocalTime min = LocalTime.of(12, 23, 45); + assertThat(RandomLocalTime.aLocalTimeInRange().after(min).build()).isAfterOrEqualTo(min); + } + + @Test + void after_before_VALUE() { + LocalTime min = LocalTime.MAX; + assertThat(RandomLocalTime.aLocalTimeInRange().after(min).build()).isEqualTo(min); + } + + @Test + void after_greater_than_before() { + LocalTime max = LocalTime.of(12, 23, 45); + LocalTime min = max.plusNanos(1); + assertThatIllegalArgumentException() + .isThrownBy(() -> RandomLocalTime.aLocalTimeInRange().before(max).after(min)) + .withMessage("Can't set after to %s, as it must not be greater than before (%s)", min, max); + } + + @Test + void before() { + LocalTime max = LocalTime.of(12, 23, 45); + assertThat(RandomLocalTime.aLocalTimeInRange().before(max).build()).isBeforeOrEqualTo(max); + } + + @Test + void before_after_VALUE() { + LocalTime max = LocalTime.MIN; + assertThat(RandomLocalTime.aLocalTimeInRange().before(max).build()).isEqualTo(max); + } + + @Test + void before_less_than_after() { + LocalTime after = LocalTime.of(12, 23, 45); + LocalTime before = after.minusNanos(1); + assertThatIllegalArgumentException() + .isThrownBy(() -> RandomLocalTime.aLocalTimeInRange().after(after).before(before)) + .withMessage( + "Can't set before to %s, as it must not be less than after (%s)", before, after); + } + + @Test + void future() { + assertThat(RandomLocalTime.aLocalTimeInRange().future().build()).isAfter(LocalTime.now()); + } + + @Test + void past() { + assertThat(RandomLocalTime.aLocalTimeInRange().past().build()).isBefore(LocalTime.now()); + } +} diff --git a/modules/random/src/test/java/org/stubit/random/RandomLocalTimeTest.java b/modules/random/src/test/java/org/stubit/random/RandomLocalTimeTest.java new file mode 100644 index 0000000..5bb3d02 --- /dev/null +++ b/modules/random/src/test/java/org/stubit/random/RandomLocalTimeTest.java @@ -0,0 +1,25 @@ +package org.stubit.random; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalTime; +import org.junit.jupiter.api.Test; + +class RandomLocalTimeTest { + + @Test + void aLocalDateBetween_min_equal_to_max() { + LocalTime min = LocalTime.now(); + assertThat(RandomLocalTime.aLocalDateBetween(min, min)).isEqualTo(min); + } + + @Test + void aFutureLocalTime() { + assertThat(RandomLocalTime.aFutureLocalTime()).isAfter(LocalTime.now()); + } + + @Test + void aPastLocalTime() { + assertThat(RandomLocalTime.aPastLocalTime()).isBefore(LocalTime.now()); + } +}