(minor) updated versions, normalized api around ranges
Some checks failed
/ Build and Test Library Snapshot (pull_request) Failing after 1m56s

This commit is contained in:
Pat Garrity 2026-01-11 10:03:58 -06:00
parent 56bc62c333
commit 01a5207d50
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
12 changed files with 208 additions and 221 deletions

View file

@ -62,16 +62,16 @@ those generators for `User`:
```scala
import gs.datagen.v0._
import gs.datagen.v0.generators.MinMax
import gs.datagen.v0.generators.Size
val nameGen: Gen[Name] =
Gen.string.alpha(4, 16).map(Name(_))
val dateOfBirthGen: Gen[LocalDate] =
Gen.date.beforeToday(
days = MinMax.Zero,
months = MinMax.nonNegative(0, 11),
years = MinMax.nonNegative(18, 80)
days = Size.Zero,
months = Size.between(0, 11),
years = Size.between(18, 80)
).map(DateOfBirth(_))
val karmaGen: Gen[Karma] =

View file

@ -1,4 +1,4 @@
val scala3: String = "3.7.2"
val scala3: String = "3.7.4"
ThisBuild / scalaVersion := scala3
ThisBuild / versionScheme := Some("semver-spec")
@ -22,7 +22,7 @@ val sharedSettings = Seq(
lazy val testSettings = Seq(
libraryDependencies ++= Seq(
"org.scalameta" %% "munit" % "1.1.0" % Test
"org.scalameta" %% "munit" % "1.2.1" % Test
)
)

View file

@ -14,8 +14,7 @@ import gs.datagen.v0.generators.GenOption
import gs.datagen.v0.generators.GenSet
import gs.datagen.v0.generators.GenString
import gs.datagen.v0.generators.GenUUID
import gs.datagen.v0.generators.MinMax
import gs.datagen.v0.generators.Size
import gs.datagen.v0.generators.Range
import gs.uuid.v0.UUID
import java.time.Clock
import java.time.Instant
@ -154,7 +153,7 @@ object Gen:
* The generator for the list elements.
*/
def list[A](
size: Size,
size: Range,
gen: Gen[A]
): Gen[List[A]] = new GenList[A](size, gen)
@ -169,7 +168,7 @@ object Gen:
def list[A](
fixedSize: Int,
gen: Gen[A]
): Gen[List[A]] = new GenList[A](Size.Fixed(fixedSize), gen)
): Gen[List[A]] = new GenList[A](Range.Fixed(fixedSize), gen)
/** Generator for a list of some size based on a generator for the list
* elements.
@ -185,7 +184,7 @@ object Gen:
minSize: Int,
maxSize: Int,
gen: Gen[A]
): Gen[List[A]] = new GenList[A](Size.Between(minSize, maxSize), gen)
): Gen[List[A]] = new GenList[A](Range.Between(minSize, maxSize), gen)
/** Generator for a set of some [[gs.datagen.v0.generators.Size]] based on a
* generator for the set elements.
@ -197,7 +196,7 @@ object Gen:
* The generator for the list elements.
*/
def set[A](
size: Size,
size: Range,
gen: Gen[A]
): Gen[Set[A]] = new GenSet[A](size, gen)
@ -213,7 +212,7 @@ object Gen:
def set[A](
fixedSize: Int,
gen: Gen[A]
): Gen[Set[A]] = new GenSet[A](Size.Fixed(fixedSize), gen)
): Gen[Set[A]] = new GenSet[A](Range.Fixed(fixedSize), gen)
/** Generator for a set of some size based on a generator for the set
* elements.
@ -229,7 +228,7 @@ object Gen:
minSize: Int,
maxSize: Int,
gen: Gen[A]
): Gen[Set[A]] = new GenSet[A](Size.Between(minSize, maxSize), gen)
): Gen[Set[A]] = new GenSet[A](Range.Between(minSize, maxSize), gen)
/** Generators which pick a single random element from a collection.
*/
@ -300,7 +299,7 @@ object Gen:
* The generator for values.
*/
def forKeysAndValues[K, V](
size: Size,
size: Range,
keyGen: Gen[K],
valueGen: Gen[V]
): Gen[Map[K, V]] =
@ -319,7 +318,7 @@ object Gen:
* The generator for key/value pairs.
*/
def forTuples[K, V](
size: Size,
size: Range,
keyValueGen: Gen[(K, V)]
): Gen[Map[K, V]] =
new GenMap.CoupledKeyValue[K, V](
@ -343,7 +342,7 @@ object Gen:
* The alphabet from which characters are selected.
*/
def alphabet(
size: Size,
size: Range,
alphabet: Alphabet
): Gen[String] =
new GenString(
@ -359,7 +358,7 @@ object Gen:
* The size constraints for the generated string.
*/
def lowercaseAlpha(
size: Size
size: Range
): Gen[String] =
new GenString(
alphabet = Alphabet.ASCII.LowerCaseAlpha,
@ -374,7 +373,7 @@ object Gen:
* The size constraints for the generated string.
*/
def uppercaseAlpha(
size: Size
size: Range
): Gen[String] =
new GenString(
alphabet = Alphabet.ASCII.UpperCaseAlpha,
@ -389,7 +388,7 @@ object Gen:
* The size constraints for the generated string.
*/
def alpha(
size: Size
size: Range
): Gen[String] =
new GenString(
alphabet = Alphabet.ASCII.Alpha,
@ -404,7 +403,7 @@ object Gen:
* The size constraints for the generated string.
*/
def alphaNumeric(
size: Size
size: Range
): Gen[String] =
new GenString(
alphabet = Alphabet.ASCII.AlphaNumeric,
@ -419,7 +418,7 @@ object Gen:
* The size constraints for the generated string.
*/
def lowercaseAlphaNumeric(
size: Size
size: Range
): Gen[String] =
new GenString(
alphabet = Alphabet.ASCII.LowerCaseAlphaNumeric,
@ -434,7 +433,7 @@ object Gen:
* The size constraints for the generated string.
*/
def uppercaseAlphaNumeric(
size: Size
size: Range
): Gen[String] =
new GenString(
alphabet = Alphabet.ASCII.UpperCaseAlphaNumeric,
@ -532,9 +531,9 @@ object Gen:
* {{{
* Gen.date.before(
* date = LocalDate.now(),
* days = MinMax.nonNegative(0, 10),
* months = MinMax.nonNegative(1, 11),
* years = MinMax.nonNegative(0, 30)
* days = Size.between(0, 10),
* months = Size.between(1, 11),
* years = Size.between(0, 30)
* )
* }}}
*
@ -549,9 +548,9 @@ object Gen:
*/
def before(
date: LocalDate,
days: MinMax.NonNegative,
months: MinMax.NonNegative = MinMax.Zero,
years: MinMax.NonNegative = MinMax.Zero
days: Range,
months: Range = Range.Zero,
years: Range = Range.Zero
): Gen[LocalDate] =
new GenLocalDate.Before(
pivot = date,
@ -576,9 +575,9 @@ object Gen:
*
* {{{
* Gen.date.beforeToday(
* days = MinMax.nonNegative(0, 10),
* months = MinMax.nonNegative(1, 11),
* years = MinMax.nonNegative(0, 30)
* days = Size.between(0, 10),
* months = Size.between(1, 11),
* years = Size.between(0, 30)
* )
* }}}
*
@ -592,9 +591,9 @@ object Gen:
* The range of years prior to today.
*/
def beforeToday(
days: MinMax.NonNegative,
months: MinMax.NonNegative = MinMax.Zero,
years: MinMax.NonNegative = MinMax.Zero,
days: Range,
months: Range = Range.Zero,
years: Range = Range.Zero,
clock: Clock = DefaultClock
): Gen[LocalDate] =
new GenLocalDate.Before(
@ -621,9 +620,9 @@ object Gen:
* {{{
* Gen.date.after(
* date = LocalDate.now(),
* days = MinMax.nonNegative(0, 10),
* months = MinMax.nonNegative(1, 11),
* years = MinMax.nonNegative(0, 30)
* days = Size.between(0, 10),
* months = Size.between(1, 11),
* years = Size.between(0, 30)
* )
* }}}
*
@ -638,9 +637,9 @@ object Gen:
*/
def after(
date: LocalDate,
days: MinMax.NonNegative,
months: MinMax.NonNegative = MinMax.Zero,
years: MinMax.NonNegative = MinMax.Zero
days: Range,
months: Range = Range.Zero,
years: Range = Range.Zero
): Gen[LocalDate] =
new GenLocalDate.After(
pivot = date,
@ -666,9 +665,9 @@ object Gen:
*
* {{{
* Gen.date.afterToday(
* days = MinMax.nonNegative(0, 10),
* months = MinMax.nonNegative(1, 11),
* years = MinMax.nonNegative(0, 30)
* days = Size.between(0, 10),
* months = Size.between(1, 11),
* years = Size.between(0, 30)
* )
* }}}
*
@ -682,9 +681,9 @@ object Gen:
* The range of years after today.
*/
def afterToday(
days: MinMax.NonNegative,
months: MinMax.NonNegative = MinMax.Zero,
years: MinMax.NonNegative = MinMax.Zero,
days: Range,
months: Range = Range.Zero,
years: Range = Range.Zero,
clock: Clock = DefaultClock
): Gen[LocalDate] =
new GenLocalDate.After(
@ -711,9 +710,9 @@ object Gen:
* {{{
* Gen.date.around(
* date = LocalDate.now(),
* days = MinMax.nonNegative(0, 10),
* months = MinMax.nonNegative(1, 11),
* years = MinMax.nonNegative(0, 30)
* days = Size.between(0, 10),
* months = Size.between(1, 11),
* years = Size.between(0, 30)
* )
* }}}
*
@ -728,9 +727,9 @@ object Gen:
*/
def around(
date: LocalDate,
days: MinMax,
months: MinMax = MinMax.Zero,
years: MinMax = MinMax.Zero
days: Range,
months: Range = Range.Zero,
years: Range = Range.Zero
): Gen[LocalDate] =
new GenLocalDate.Around(
pivot = date,
@ -755,9 +754,9 @@ object Gen:
*
* {{{
* Gen.date.around(
* days = MinMax.nonNegative(0, 10),
* months = MinMax.nonNegative(1, 11),
* years = MinMax.nonNegative(0, 30)
* days = Size.between(0, 10),
* months = Size.between(1, 11),
* years = Size.between(0, 30)
* )
* }}}
*
@ -771,9 +770,9 @@ object Gen:
* The range of years before or after today.
*/
def aroundToday(
days: MinMax,
months: MinMax = MinMax.Zero,
years: MinMax = MinMax.Zero,
days: Range,
months: Range = Range.Zero,
years: Range = Range.Zero,
clock: Clock = DefaultClock
): Gen[LocalDate] =
new GenLocalDate.Around(

View file

@ -3,9 +3,9 @@ package gs.datagen.v0.generators
import gs.datagen.v0.Gen
final class GenList[A](
val size: Size,
val range: Range,
val generator: Gen[A]
) extends Gen[List[A]]:
override def generate(input: Any): List[A] =
List.fill(size.next(Gen.rng()))(generator.generate(()))
List.fill(range.next(Gen.rng()))(generator.generate(()))

View file

@ -26,9 +26,9 @@ object GenLocalDate:
*/
final class Before(
val pivot: LocalDate,
val days: MinMax,
val months: MinMax,
val years: MinMax
val days: Range,
val months: Range,
val years: Range
) extends GenLocalDate:
/** @inheritDocs
@ -36,9 +36,9 @@ object GenLocalDate:
override def generate(input: Any): LocalDate =
val rng = Gen.rng()
pivot
.minusDays(days.select(rng).toInt)
.minusMonths(months.select(rng).toInt)
.minusYears(years.select(rng).toInt)
.minusDays(days.next(rng).toInt)
.minusMonths(months.next(rng).toInt)
.minusYears(years.next(rng).toInt)
/** Implementation of [[GenLocalDate]] that selects random dates after some
* given date.
@ -54,9 +54,9 @@ object GenLocalDate:
*/
final class After(
val pivot: LocalDate,
val days: MinMax,
val months: MinMax,
val years: MinMax
val days: Range,
val months: Range,
val years: Range
) extends GenLocalDate:
/** @inheritDocs
@ -64,9 +64,9 @@ object GenLocalDate:
override def generate(input: Any): LocalDate =
val rng = Gen.rng()
pivot
.plusDays(days.select(rng).toInt)
.plusMonths(months.select(rng).toInt)
.plusYears(years.select(rng).toInt)
.plusDays(days.next(rng).toInt)
.plusMonths(months.next(rng).toInt)
.plusYears(years.next(rng).toInt)
/** Implementation of [[GenLocalDate]] that selects random dates centered on
* some given date.
@ -82,9 +82,9 @@ object GenLocalDate:
*/
final class Around(
val pivot: LocalDate,
val days: MinMax,
val months: MinMax,
val years: MinMax
val days: Range,
val months: Range,
val years: Range
) extends GenLocalDate:
/** @inheritDocs
@ -109,10 +109,10 @@ object GenLocalDate:
*/
def plusOrMinusDays(
rng: Random,
range: MinMax
range: Range
): LocalDate =
if rng.nextBoolean() then base.plusDays(range.select(rng).toInt)
else base.minusDays(range.select(rng).toInt)
if rng.nextBoolean() then base.plusDays(range.next(rng).toInt)
else base.minusDays(range.next(rng).toInt)
/** Select a bounded random number of months before or after the base date.
*
@ -125,10 +125,10 @@ object GenLocalDate:
*/
def plusOrMinusMonths(
rng: Random,
range: MinMax
range: Range
): LocalDate =
if rng.nextBoolean() then base.plusMonths(range.select(rng).toInt)
else base.minusMonths(range.select(rng).toInt)
if rng.nextBoolean() then base.plusMonths(range.next(rng).toInt)
else base.minusMonths(range.next(rng).toInt)
/** Select a bounded random number of years before or after the base date.
*
@ -141,9 +141,9 @@ object GenLocalDate:
*/
def plusOrMinusYears(
rng: Random,
range: MinMax
range: Range
): LocalDate =
if rng.nextBoolean() then base.plusYears(range.select(rng).toInt)
else base.minusYears(range.select(rng).toInt)
if rng.nextBoolean() then base.plusYears(range.next(rng).toInt)
else base.minusYears(range.next(rng).toInt)
end GenLocalDate

View file

@ -7,23 +7,23 @@ abstract class GenMap[K, V] extends Gen[Map[K, V]]
object GenMap:
final class IndependentKeyValue[K, V](
val size: Size,
val range: Range,
val keyGen: Gen[K],
val valueGen: Gen[V]
) extends Gen[Map[K, V]]:
override def generate(input: Any): Map[K, V] =
List.fill(size.next(Gen.rng()))(generateTuple()).toMap
List.fill(range.next(Gen.rng()))(generateTuple()).toMap
private def generateTuple(): (K, V) =
keyGen.generate(()) -> valueGen.generate(())
final class CoupledKeyValue[K, V](
val size: Size,
val range: Range,
val keyValueGen: Gen[(K, V)]
) extends Gen[Map[K, V]]:
override def generate(input: Any): Map[K, V] =
List.fill(size.next(Gen.rng()))(keyValueGen.generate(())).toMap
List.fill(range.next(Gen.rng()))(keyValueGen.generate(())).toMap
end GenMap

View file

@ -3,9 +3,9 @@ package gs.datagen.v0.generators
import gs.datagen.v0.Gen
final class GenSet[A](
val size: Size,
val range: Range,
val generator: Gen[A]
) extends Gen[Set[A]]:
override def generate(input: Any): Set[A] =
Set.fill(size.next(Gen.rng()))(generator.generate(()))
Set.fill(range.next(Gen.rng()))(generator.generate(()))

View file

@ -4,7 +4,7 @@ import gs.datagen.v0.Gen
final class GenString(
val alphabet: Alphabet,
val size: Size
val size: Range
) extends Gen[String]:
override def generate(input: Any): String =

View file

@ -1,64 +0,0 @@
package gs.datagen.v0.generators
import java.util.Random
sealed trait MinMax:
def min: Int
def max: Int
def select(random: Random): Int =
if min == 0 && max == 0 then 0
else random.nextInt(min, max)
object MinMax:
val Zero: MinMax.NonNegative = NonNegative(0, 0)
def apply(
min: Int,
max: Int
): MinMax = Generic(min, max)
def nonNegative(
min: Int,
max: Int
): MinMax.NonNegative =
NonNegative(min, max)
final class Generic private (
val min: Int,
val max: Int
) extends MinMax
object Generic:
def apply(
min: Int,
max: Int
): Generic =
if min <= max then new Generic(min, max)
else new Generic(max, min)
end Generic
final class NonNegative private (
val min: Int,
val max: Int
) extends MinMax
object NonNegative:
def apply(
min: Int,
max: Int
): NonNegative =
if min < 0 || max < 0 then
throw new IllegalArgumentException(
"Only values >= 0 are allowed in this min/max pair."
)
else if min <= max then new NonNegative(min, max)
else new NonNegative(max, min)
end NonNegative
end MinMax

View file

@ -0,0 +1,116 @@
package gs.datagen.v0.generators
import java.util.Random
/** Represents a boundd value which must be greater than or equal to 0.
*/
abstract class Range:
def next(random: Random): Int
object Range:
given CanEqual[Range, Range] = CanEqual.derived
/** Fixed range with the value 0.
*/
val Zero: Range = fixed(0)
/** Fixed range with the value 1.
*/
val One: Range = fixed(1)
/** Fixed range that always produces a single value.
*
* @param value
* The fixed value of the range.
* @return
* Fixed range that always produces the given value.
*/
def fixed(value: Int): Range = Fixed(value)
/** Variable range that produces a value within some lower and upper bound.
*
* Ranges are _inclusive_ of the bounds.
*
* @param lower
* The lower bound of the range.
* @param upper
* The upper bound of the range.
* @return
* Variable range that produces a value inclusive to the given bounds.
*/
def between(
lower: Int,
upper: Int
): Range = Between(lower, upper)
final class Fixed private (
val value: Int
) extends Range:
override def next(random: Random): Int = value
override def equals(that: Any): Boolean =
that match
case other: Fixed => other.value == value
case _ => false
override def hashCode(): Int = value.hashCode()
override def toString(): String = value.toString()
object Fixed:
given CanEqual[Fixed, Fixed] = CanEqual.derived
def apply(
value: Int
): Range =
if value < 0 then
throw new IllegalArgumentException(
s"The fixed range value must be greater than or equal to 0. Received: '$value'"
)
else new Fixed(value)
end Fixed
final class Between private (
val lowerBound: Int,
val upperBound: Int
) extends Range:
override def next(random: Random): Int =
random.nextInt(lowerBound, upperBound + 1)
override def equals(that: Any): Boolean =
that match
case other: Between =>
other.lowerBound == lowerBound && other.upperBound == upperBound
case _ => false
override def hashCode(): Int =
java.util.Objects.hash(lowerBound, upperBound)
override def toString(): String =
s"[$lowerBound, $upperBound]"
object Between:
given CanEqual[Between, Between] = CanEqual.derived
def apply(
lowerBound: Int,
upperBound: Int
): Range =
if lowerBound < 0 then
throw new IllegalArgumentException(
s"The lower bound of a range must be greater than or equal to 0. Received: '$lowerBound'"
)
else if lowerBound > upperBound then
throw new IllegalArgumentException(
s"The lower bound of a range must less than the upper bound. Received lowerBound='$lowerBound' and upperBound='$upperBound'"
)
else new Between(lowerBound, upperBound)
end Between
end Range

View file

@ -1,64 +0,0 @@
package gs.datagen.v0.generators
import java.util.Random
/** Represents a variable size which must (usually) be greater than or equal to
* 0.
*/
abstract class Size:
def next(random: Random): Int
object Size:
def fixed(size: Int): Size = Fixed(size)
def between(
lower: Int,
upper: Int
): Size = Between(lower, upper)
final class Fixed private (
size: Int
) extends Size:
override def next(random: Random): Int = size
object Fixed:
def apply(
size: Int
): Size =
if size < 0 then
throw new IllegalArgumentException(
s"The size must be greater than or equal to 0. Received: '$size'"
)
else new Fixed(size)
end Fixed
final class Between private (
lowerBound: Int,
upperBound: Int
) extends Size:
override def next(random: Random): Int =
random.nextInt(lowerBound, upperBound + 1)
object Between:
def apply(
lowerBound: Int,
upperBound: Int
): Size =
if lowerBound < 0 then
throw new IllegalArgumentException(
s"The lower bound of a size must be greater than or equal to 0. Received: '$lowerBound'"
)
else if lowerBound > upperBound then
throw new IllegalArgumentException(
s"The lower bound of a size must less than the upper bound. Received lowerBound='$lowerBound' and upperBound='$upperBound'"
)
else new Between(lowerBound, upperBound)
end Between
end Size

View file

@ -1 +1 @@
sbt.version=1.11.6
sbt.version=1.12.0