diff --git a/modules/effect/src/main/scala/src/gs/std/v0/effect/ClockDateTimeProvider.scala b/modules/effect/src/main/scala/src/gs/std/v0/effect/ClockDateTimeProvider.scala new file mode 100644 index 0000000..3074720 --- /dev/null +++ b/modules/effect/src/main/scala/src/gs/std/v0/effect/ClockDateTimeProvider.scala @@ -0,0 +1,87 @@ +package gs.std.v0.effect + +import cats.effect.Sync +import cats.syntax.all.* +import java.time.Clock +import java.time.Instant +import java.time.LocalDate +import java.time.OffsetDateTime +import java.time.ZonedDateTime +import java.time.ZoneId + +/** Implementation of [[DateTimeProvider]] based on a specific + * `java.time.Clock`. All dates and times are calculated using this clock. + * + * @param clock + * The clock used to calculate dates and times. + */ +final class ClockDateTimeProvider[F[_]: Sync]( + val clock: Clock +) extends DateTimeProvider[F]: + + /** @inheritDocs + */ + override def timestamp(): F[Instant] = + Sync[F].delay(Instant.now(clock)) + + /** @inheritDocs + */ + override def nowInstant(): F[Instant] = + Sync[F].delay(Instant.now(clock)) + + /** @inheritDocs + */ + override def nowZoned(): F[ZonedDateTime] = + Sync[F].delay(ZonedDateTime.now(clock)) + + /** @inheritDocs + */ + override def nowOffset(): F[OffsetDateTime] = + Sync[F].delay(OffsetDateTime.now(clock)) + + /** @inheritDocs + */ + override def today(): F[LocalDate] = + Sync[F].delay(LocalDate.now(clock)) + + /** @inheritDocs + */ + override def yesterday(): F[LocalDate] = + today().map(_.minusDays(1L)) + + /** @inheritDocs + */ + override def tomorrow(): F[LocalDate] = + today().map(_.plusDays(1L)) + +object ClockDateTimeProvider: + + /** Instantiate a new [[ClockDateTimeProvider]]. + * + * @param clock + * The clock to use for all date/time calculations. + * @return + * The new [[DateTimeProvider]] instance. + */ + def apply[F[_]: Sync](clock: Clock): DateTimeProvider[F] = + new ClockDateTimeProvider[F](clock) + + /** @return + * A new [[ClockDateTimeProvider]] based on the system's default zone using + * the system's clock. + */ + def system[F[_]: Sync]: DateTimeProvider[F] = + new ClockDateTimeProvider[F](Clock.systemDefaultZone()) + + /** Use a system clock aligned to the given zone. + * + * @param zoneId + * The identifier of the zone to target. + * @return + * A new [[ClockDateTimeProvider]] based on the specified zone using the + * system's clock. + */ + def forZone[F[_]: Sync](zoneId: ZoneId): DateTimeProvider[F] = + new ClockDateTimeProvider[F](Clock.system(zoneId)) + +end ClockDateTimeProvider diff --git a/modules/effect/src/main/scala/src/gs/std/v0/effect/DateTimeProvider.scala b/modules/effect/src/main/scala/src/gs/std/v0/effect/DateTimeProvider.scala new file mode 100644 index 0000000..e95066b --- /dev/null +++ b/modules/effect/src/main/scala/src/gs/std/v0/effect/DateTimeProvider.scala @@ -0,0 +1,44 @@ +package gs.std.v0.effect + +import java.time.Instant +import java.time.LocalDate +import java.time.OffsetDateTime +import java.time.ZonedDateTime + +/** Provides date and time values. + */ +trait DateTimeProvider[F[_]]: + /** @return + * The current timestamp (`java.time.Instant`). + */ + def timestamp(): F[Instant] + + /** @return + * The current `java.time.Instant`. + */ + def nowInstant(): F[Instant] + + /** @return + * The current date and time in the configured zone. + */ + def nowZoned(): F[ZonedDateTime] + + /** @return + * The current date and time in the configured offset. + */ + def nowOffset(): F[OffsetDateTime] + + /** @return + * The current date. + */ + def today(): F[LocalDate] + + /** @return + * The date before today. + */ + def yesterday(): F[LocalDate] + + /** @return + * The date after today. + */ + def tomorrow(): F[LocalDate] diff --git a/modules/effect/src/main/scala/src/gs/std/v0/effect/RNG.scala b/modules/effect/src/main/scala/src/gs/std/v0/effect/RNG.scala deleted file mode 100644 index 34e9dd1..0000000 --- a/modules/effect/src/main/scala/src/gs/std/v0/effect/RNG.scala +++ /dev/null @@ -1,168 +0,0 @@ -package gs.std.v0.effect - -import cats.Applicative -import cats.effect.Sync -import fs2.Stream -import java.security.SecureRandom -import java.util.Random - -trait RNG[F[_]]: - def nextInt(): F[Int] - def nextInt(bound: Int): F[Int] - - def nextInt( - origin: Int, - bound: Int - ): F[Int] - - def nextLong(): F[Long] - def nextLong(bound: Long): F[Long] - - def nextLong( - origin: Long, - bound: Long - ): F[Long] - - def nextDouble(): F[Double] - def nextDouble(bound: Double): F[Double] - - def nextDouble( - origin: Double, - bound: Double - ): F[Double] - - def nextBoolean(): F[Boolean] - - def nextInts( - origin: Int, - bound: Int - ): Stream[F, Int] - - def nextLongs( - origin: Long, - bound: Long - ): Stream[F, Long] - - def nextDoubles( - origin: Double, - bound: Double - ): Stream[F, Double] - -object RNG: - - def secure[F[_]: Sync]: RNG[F] = - new JavaRandom[F](new SecureRandom()) - - final class JavaRandom[F[_]: Sync](random: Random) extends RNG[F]: - - override def nextInt(): F[Int] = - Sync[F].delay(random.nextInt()) - - override def nextInt(bound: Int): F[Int] = - Sync[F].delay(random.nextInt(bound)) - - override def nextInt( - origin: Int, - bound: Int - ): F[Int] = - Sync[F].delay(random.nextInt(origin, bound)) - - override def nextLong(): F[Long] = - Sync[F].delay(random.nextLong()) - - override def nextLong(bound: Long): F[Long] = - Sync[F].delay(random.nextLong(bound)) - - override def nextLong( - origin: Long, - bound: Long - ): F[Long] = - Sync[F].delay(random.nextLong(origin, bound)) - - override def nextDouble(): F[Double] = - Sync[F].delay(random.nextDouble()) - - override def nextDouble(bound: Double): F[Double] = - Sync[F].delay(random.nextDouble(bound)) - - override def nextDouble( - origin: Double, - bound: Double - ): F[Double] = - Sync[F].delay(random.nextDouble(origin, bound)) - - override def nextBoolean(): F[Boolean] = - Sync[F].delay(random.nextBoolean()) - - override def nextInts( - origin: Int, - bound: Int - ): Stream[F, Int] = - Stream.repeatEval(nextInt(origin, bound)) - - override def nextLongs( - origin: Long, - bound: Long - ): Stream[F, Long] = - Stream.repeatEval(nextLong(origin, bound)) - - override def nextDoubles( - origin: Double, - bound: Double - ): Stream[F, Double] = - Stream.repeatEval(nextDouble(origin, bound)) - - end JavaRandom - - final class Zero[F[_]: Applicative] extends RNG[F]: - - override def nextInt(): F[Int] = Applicative[F].pure(0) - - override def nextInt(bound: Int): F[Int] = Applicative[F].pure(0) - - override def nextInt( - origin: Int, - bound: Int - ): F[Int] = Applicative[F].pure(0) - - override def nextLong(): F[Long] = Applicative[F].pure(0) - - override def nextLong(bound: Long): F[Long] = Applicative[F].pure(0) - - override def nextLong( - origin: Long, - bound: Long - ): F[Long] = Applicative[F].pure(0) - - override def nextDouble(): F[Double] = Applicative[F].pure(0.0) - - override def nextDouble(bound: Double): F[Double] = Applicative[F].pure(0.0) - - override def nextDouble( - origin: Double, - bound: Double - ): F[Double] = Applicative[F].pure(0.0) - - override def nextBoolean(): F[Boolean] = Applicative[F].pure(false) - - override def nextInts( - origin: Int, - bound: Int - ): Stream[F, Int] = - Stream.repeatEval(Applicative[F].pure(0)) - - override def nextLongs( - origin: Long, - bound: Long - ): Stream[F, Long] = - Stream.repeatEval(Applicative[F].pure(0)) - - override def nextDoubles( - origin: Double, - bound: Double - ): Stream[F, Double] = - Stream.repeatEval(Applicative[F].pure(0.0)) - - end Zero - -end RNG diff --git a/modules/effect/src/main/scala/src/gs/std/v0/effect/Rng.scala b/modules/effect/src/main/scala/src/gs/std/v0/effect/Rng.scala new file mode 100644 index 0000000..813fce8 --- /dev/null +++ b/modules/effect/src/main/scala/src/gs/std/v0/effect/Rng.scala @@ -0,0 +1,388 @@ +package gs.std.v0.effect + +import cats.Applicative +import cats.effect.Sync +import cats.syntax.all.* +import fs2.Stream +import java.security.SecureRandom +import java.util.Random + +/** Random number generator. + */ +trait Rng[F[_]]: + + /** Explicitly set or update the seed using the given 8 bytes. + * + * @param seed + * The seed value to assign or augment. + * @return + * This [[Rng]] instance. + */ + def updateSeed(seed: Long): F[Rng[F]] + + /** @return + * The next (unbounded) random `Int`. + */ + def nextInt(): F[Int] + + /** Calculate a number bounded inclusively within 0 and some bound. + * + * @param bound + * The bound. + * @return + * The calculated number. + */ + def nextInt(bound: Int): F[Int] + + /** Calculate a number bounded inclusively within some origin and some bound. + * + * @param origin + * The origin. + * @param bound + * The bound. + * @return + * The calculated number. + */ + def nextInt( + origin: Int, + bound: Int + ): F[Int] + + /** @return + * The next (unbounded) random `Long`. + */ + def nextLong(): F[Long] + + /** Calculate a number bounded inclusively within 0 and some bound. + * + * @param bound + * The bound. + * @return + * The calculated number. + */ + def nextLong(bound: Long): F[Long] + + /** Calculate a number bounded inclusively within some origin and some bound. + * + * @param origin + * The origin. + * @param bound + * The bound. + * @return + * The calculated number. + */ + def nextLong( + origin: Long, + bound: Long + ): F[Long] + + /** @return + * The next (unbounded) random `Double`. + */ + def nextDouble(): F[Double] + + /** Calculate a number bounded inclusively within 0 and some bound. + * + * @param bound + * The bound. + * @return + * The calculated number. + */ + def nextDouble(bound: Double): F[Double] + + /** Calculate a number bounded inclusively within some origin and some bound. + * + * @param origin + * The origin. + * @param bound + * The bound. + * @return + * The calculated number. + */ + def nextDouble( + origin: Double, + bound: Double + ): F[Double] + + /** @return + * The next randomly selected `true` or `false` value. + */ + def nextBoolean(): F[Boolean] + + /** Produce an infinite stream of `Int` bounded inclusively within some origin + * and some bound. + * + * @param origin + * The origin. + * @param bound + * The bound. + * @return + * The infinite stream of values. + */ + def nextInts( + origin: Int, + bound: Int + ): Stream[F, Int] + + /** Produce an infinite stream of `Long` bounded inclusively within some + * origin and some bound. + * + * @param origin + * The origin. + * @param bound + * The bound. + * @return + * The infinite stream of values. + */ + def nextLongs( + origin: Long, + bound: Long + ): Stream[F, Long] + + /** Produce an infinite stream of `Double` bounded inclusively within some + * origin and some bound. + * + * @param origin + * The origin. + * @param bound + * The bound. + * @return + * The infinite stream of values. + */ + def nextDoubles( + origin: Double, + bound: Double + ): Stream[F, Double] + +object Rng: + + /** Instantiate [[Rng]] using a new `java.util.Random` instance. + * + * For secure random number generation, please refer to the `secure[F]` + * function. + * + * @param random + * The `java.util.Random` instance. + * @return + * The new [[Rng]]. + */ + def default[F[_]: Sync]: Rng[F] = + default[F](new Random()) + + /** Instantiate [[Rng]] using the given random number generator. + * + * @param random + * The `java.util.Random` instance. + * @return + * The new [[Rng]]. + */ + def default[F[_]: Sync](random: Random): Rng[F] = + new JavaRandom[F](random) + + /** Instantiate [[Rng]] using a new `java.security.SecureRandom` instance. + * + * @param random + * The `java.security.SecureRandom` instance. + * @return + * The new [[Rng]]. + */ + def secure[F[_]: Sync]: Rng[F] = + secure[F](new SecureRandom()) + + /** Instantiate [[Rng]] using the given secure random number generator. + * + * @param random + * The `java.security.SecureRandom` instance. + * @return + * The new [[Rng]]. + */ + def secure[F[_]: Sync](random: SecureRandom): Rng[F] = + new JavaRandom[F](random) + + /** @return + * New instance of [[Rng]] that always returns `0` or `false`. + */ + def zero[F[_]: Applicative]: Rng[F] = + new Zero[F] + + /** Implementation of [[Rng]] based on any given `java.util.Random` instance. + * + * @param random + * The underlying random number generator. + */ + final class JavaRandom[F[_]: Sync](random: Random) extends Rng[F]: + + /** @inheritDocs + */ + override def updateSeed(seed: Long): F[Rng[F]] = + Sync[F].delay(random.setSeed(seed)).as(this) + + /** @inheritDocs + */ + override def nextInt(): F[Int] = + Sync[F].delay(random.nextInt()) + + /** @inheritDocs + */ + override def nextInt(bound: Int): F[Int] = + Sync[F].delay(random.nextInt(bound)) + + /** @inheritDocs + */ + override def nextInt( + origin: Int, + bound: Int + ): F[Int] = + Sync[F].delay(random.nextInt(origin, bound)) + + /** @inheritDocs + */ + override def nextLong(): F[Long] = + Sync[F].delay(random.nextLong()) + + /** @inheritDocs + */ + override def nextLong(bound: Long): F[Long] = + Sync[F].delay(random.nextLong(bound)) + + /** @inheritDocs + */ + override def nextLong( + origin: Long, + bound: Long + ): F[Long] = + Sync[F].delay(random.nextLong(origin, bound)) + + /** @inheritDocs + */ + override def nextDouble(): F[Double] = + Sync[F].delay(random.nextDouble()) + + /** @inheritDocs + */ + override def nextDouble(bound: Double): F[Double] = + Sync[F].delay(random.nextDouble(bound)) + + /** @inheritDocs + */ + override def nextDouble( + origin: Double, + bound: Double + ): F[Double] = + Sync[F].delay(random.nextDouble(origin, bound)) + + /** @inheritDocs + */ + override def nextBoolean(): F[Boolean] = + Sync[F].delay(random.nextBoolean()) + + /** @inheritDocs + */ + override def nextInts( + origin: Int, + bound: Int + ): Stream[F, Int] = + Stream.repeatEval(nextInt(origin, bound)) + + /** @inheritDocs + */ + override def nextLongs( + origin: Long, + bound: Long + ): Stream[F, Long] = + Stream.repeatEval(nextLong(origin, bound)) + + /** @inheritDocs + */ + override def nextDoubles( + origin: Double, + bound: Double + ): Stream[F, Double] = + Stream.repeatEval(nextDouble(origin, bound)) + + end JavaRandom + + /** Implementation of [[Rng]] that always returns `0` or `false`. + */ + final class Zero[F[_]: Applicative] extends Rng[F]: + + /** @inheritDocs + */ + override def updateSeed(seed: Long): F[Rng[F]] = Applicative[F].pure(this) + + /** @inheritDocs + */ + override def nextInt(): F[Int] = Applicative[F].pure(0) + + /** @inheritDocs + */ + override def nextInt(bound: Int): F[Int] = Applicative[F].pure(0) + + /** @inheritDocs + */ + override def nextInt( + origin: Int, + bound: Int + ): F[Int] = Applicative[F].pure(0) + + /** @inheritDocs + */ + override def nextLong(): F[Long] = Applicative[F].pure(0) + + /** @inheritDocs + */ + override def nextLong(bound: Long): F[Long] = Applicative[F].pure(0) + + /** @inheritDocs + */ + override def nextLong( + origin: Long, + bound: Long + ): F[Long] = Applicative[F].pure(0) + + /** @inheritDocs + */ + override def nextDouble(): F[Double] = Applicative[F].pure(0.0) + + /** @inheritDocs + */ + override def nextDouble(bound: Double): F[Double] = Applicative[F].pure(0.0) + + /** @inheritDocs + */ + override def nextDouble( + origin: Double, + bound: Double + ): F[Double] = Applicative[F].pure(0.0) + + /** @inheritDocs + */ + override def nextBoolean(): F[Boolean] = Applicative[F].pure(false) + + /** @inheritDocs + */ + override def nextInts( + origin: Int, + bound: Int + ): Stream[F, Int] = + Stream.repeatEval(Applicative[F].pure(0)) + + /** @inheritDocs + */ + override def nextLongs( + origin: Long, + bound: Long + ): Stream[F, Long] = + Stream.repeatEval(Applicative[F].pure(0)) + + /** @inheritDocs + */ + override def nextDoubles( + origin: Double, + bound: Double + ): Stream[F, Double] = + Stream.repeatEval(Applicative[F].pure(0.0)) + + end Zero + +end Rng