diff --git a/README.md b/README.md index e1fccc2..c2ec387 100644 --- a/README.md +++ b/README.md @@ -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] = diff --git a/build.sbt b/build.sbt index 2dc4a93..b3d0e16 100644 --- a/build.sbt +++ b/build.sbt @@ -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 ) ) diff --git a/modules/core/src/main/scala/gs/datagen/v0/gen.scala b/modules/core/src/main/scala/gs/datagen/v0/gen.scala index 1981f16..1998b02 100644 --- a/modules/core/src/main/scala/gs/datagen/v0/gen.scala +++ b/modules/core/src/main/scala/gs/datagen/v0/gen.scala @@ -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( diff --git a/modules/core/src/main/scala/gs/datagen/v0/generators/GenList.scala b/modules/core/src/main/scala/gs/datagen/v0/generators/GenList.scala index 554a167..662b346 100644 --- a/modules/core/src/main/scala/gs/datagen/v0/generators/GenList.scala +++ b/modules/core/src/main/scala/gs/datagen/v0/generators/GenList.scala @@ -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(())) diff --git a/modules/core/src/main/scala/gs/datagen/v0/generators/GenLocalDate.scala b/modules/core/src/main/scala/gs/datagen/v0/generators/GenLocalDate.scala index bfcf196..f52f5c9 100644 --- a/modules/core/src/main/scala/gs/datagen/v0/generators/GenLocalDate.scala +++ b/modules/core/src/main/scala/gs/datagen/v0/generators/GenLocalDate.scala @@ -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 diff --git a/modules/core/src/main/scala/gs/datagen/v0/generators/GenMap.scala b/modules/core/src/main/scala/gs/datagen/v0/generators/GenMap.scala index b013349..146df02 100644 --- a/modules/core/src/main/scala/gs/datagen/v0/generators/GenMap.scala +++ b/modules/core/src/main/scala/gs/datagen/v0/generators/GenMap.scala @@ -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 diff --git a/modules/core/src/main/scala/gs/datagen/v0/generators/GenSet.scala b/modules/core/src/main/scala/gs/datagen/v0/generators/GenSet.scala index c2183b1..446f459 100644 --- a/modules/core/src/main/scala/gs/datagen/v0/generators/GenSet.scala +++ b/modules/core/src/main/scala/gs/datagen/v0/generators/GenSet.scala @@ -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(())) diff --git a/modules/core/src/main/scala/gs/datagen/v0/generators/GenString.scala b/modules/core/src/main/scala/gs/datagen/v0/generators/GenString.scala index 83099df..e072afe 100644 --- a/modules/core/src/main/scala/gs/datagen/v0/generators/GenString.scala +++ b/modules/core/src/main/scala/gs/datagen/v0/generators/GenString.scala @@ -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 = diff --git a/modules/core/src/main/scala/gs/datagen/v0/generators/MinMax.scala b/modules/core/src/main/scala/gs/datagen/v0/generators/MinMax.scala deleted file mode 100644 index dc4d111..0000000 --- a/modules/core/src/main/scala/gs/datagen/v0/generators/MinMax.scala +++ /dev/null @@ -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 diff --git a/modules/core/src/main/scala/gs/datagen/v0/generators/Range.scala b/modules/core/src/main/scala/gs/datagen/v0/generators/Range.scala new file mode 100644 index 0000000..147fae1 --- /dev/null +++ b/modules/core/src/main/scala/gs/datagen/v0/generators/Range.scala @@ -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 diff --git a/modules/core/src/main/scala/gs/datagen/v0/generators/Size.scala b/modules/core/src/main/scala/gs/datagen/v0/generators/Size.scala deleted file mode 100644 index f9a07e3..0000000 --- a/modules/core/src/main/scala/gs/datagen/v0/generators/Size.scala +++ /dev/null @@ -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 diff --git a/project/build.properties b/project/build.properties index 5e6884d..30b7fd9 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.6 +sbt.version=1.12.0