From 60c91d645ab6190e65eb39d24f70aa92adc74738 Mon Sep 17 00:00:00 2001 From: David Strawn Date: Tue, 8 Dec 2020 18:16:00 -0700 Subject: [PATCH 1/7] Add java.time._ Instances For The JVM This commit adds Scalacheck instances for almost all of the types in the `java.time` package and sub-packages, for which meaningful definitions seem to exist. Almost all implemented types have `Gen.Choose` instances. `Shrink` instances were defined for the types that seemed to have a reasonably straight forward definition. And all implemented types have an `Arbitrary` instance. The instances were built against JRE 8, though I'm not aware of any changes between the JRE definitions and the current (JRE 15) definitions. The types are scoped to `org.scalacheck.time`, with the intent that `import org.scalacheck.time._` would bring them all into the implicit scope. It is likely worth discussing why these types are being added. Scalacheck instances for `java.time` types are hardly new. There are several libraries which provide some instances for some `java.time` types. There is however, no library of which I am aware, which provides a relatively complete implementation of these types, e.g. instances for most types, with full range support, and `Choose` instances. If such a library already exists, then I am more than happy to withdraw this PR. Further, `java.time` types have a relatively large user base in the Scala community. By hosting instances in Scalacheck proper, we increase exposure to these instances and lessen the likelihood that developers will attempt to re-invent the wheel, or just use stub instances, e.g. `Gen.const(LocalDate.now)`. While these types are currently scoped to the JVM only, it is arguable that they could be provided for ScalaJs and ScalaNative too. Both ScalaJs and ScalaNative have, or are working on, replacement API compatible implementations. However adding support in Scalacheck proper for this would require adding dependencies and is thus punted to a later discussion for now. --- .../scalacheck/time/JavaTimeInstances.scala | 556 ++++++++++++++++++ .../scala/org/scalacheck/time/package.scala | 3 + .../scala/org/scalacheck/time/CogenLaws.scala | 28 + .../scalacheck/time/GenSpecification.scala | 47 ++ .../scalacheck/time/ShrinkSpecification.scala | 12 + 5 files changed, 646 insertions(+) create mode 100644 jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala create mode 100644 jvm/src/main/scala/org/scalacheck/time/package.scala create mode 100644 jvm/src/test/scala/org/scalacheck/time/CogenLaws.scala create mode 100644 jvm/src/test/scala/org/scalacheck/time/GenSpecification.scala create mode 100644 jvm/src/test/scala/org/scalacheck/time/ShrinkSpecification.scala diff --git a/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala b/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala new file mode 100644 index 000000000..f302fc686 --- /dev/null +++ b/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala @@ -0,0 +1,556 @@ +package org.scalacheck.time + +import org.scalacheck._ +import java.time._ +import java.time.temporal._ +import org.scalacheck.Gen.Choose + +/** Instances for `java.time` types. */ +private[time] trait JavaTimeInstances { + + // temporal.ChronoUnit + // + // Arbitrary and Choose instances are already provided by Java enum support. + + implicit final lazy val cogenChronoUnit: Cogen[ChronoUnit] = + Cogen[Int].contramap(_.ordinal) + + // Duration + + // Java duration values are conceptually infinite, thus they do not expose + // Duration.MAX/Duration.MIN values, but in practice they are finite, + // restricted by their underlying representation a long and an int. + + private final lazy val minDuration: Duration = + Duration.ofSeconds(Long.MinValue) + + private final lazy val maxDuration: Duration = + Duration.ofSeconds(Long.MaxValue, 999999999L) + + implicit final lazy val chooseDuration: Choose[Duration] = + new Choose[Duration] { + override def choose(min: Duration, max: Duration): Gen[Duration] = { + min.compareTo(max) match { + case 0 => Gen.const(min) + case result if result > 0 => Gen.fail + case _ => + val minSeconds: Long = min.getSeconds + val maxSeconds: Long = max.getSeconds + Gen.choose(minSeconds, maxSeconds).flatMap{seconds => + val minNanos: Int = + if(seconds == minSeconds) { + min.getNano + } else { + 1 + } + val maxNanos: Int = + if (seconds == maxSeconds) { + max.getNano + } else { + 999999999 + } + Gen.choose(minNanos, maxNanos).map(nanos => + Duration.ofSeconds(seconds, nanos.toLong) + ) + } + } + } + } + + implicit final lazy val arbDuration: Arbitrary[Duration] = + Arbitrary(Gen.choose(minDuration, maxDuration)) + + implicit final lazy val cogenDuration: Cogen[Duration] = + Cogen[(Long, Int)].contramap(value => (value.getSeconds, value.getNano)) + + implicit final lazy val shrinkDuration: Shrink[Duration] = + Shrink[Duration]{value => + val q: Duration = value.dividedBy(2) + if (q == Duration.ZERO) { + Stream(Duration.ZERO) + } else { + q #:: q.negated #:: shrinkDuration.shrink(q) + } + } + + // Instant + + implicit final lazy val chooseInstant: Choose[Instant] = + new Choose[Instant] { + override def choose(min: Instant, max: Instant): Gen[Instant] = + min.compareTo(max) match { + case 0 => Gen.const(min) + case result if result > 0 => Gen.fail + case _ => + Gen.choose(min.getEpochSecond, max.getEpochSecond).flatMap{epochSecond => + val minNano: Int = + if (epochSecond == min.getEpochSecond) { + min.getNano + } else { + 1 + } + val maxNano: Int = + if (epochSecond == max.getEpochSecond) { + max.getNano + } else { + 999999999 + } + Gen.choose(minNano, maxNano).map(nanos => + Instant.ofEpochSecond(epochSecond, nanos.toLong) + ) + } + } + } + + implicit final lazy val arbInstant: Arbitrary[Instant] = + Arbitrary(Gen.choose(Instant.MIN, Instant.MAX)) + + implicit final lazy val cogenInstant: Cogen[Instant] = + Cogen[(Long, Int)].contramap(value => (value.getEpochSecond, value.getNano)) + + // Month + + implicit final lazy val chooseMonth: Choose[Month] = + Choose.xmap[Int, Month]( + value => Month.of((value % 12) + 1), + _.ordinal + ) + + implicit final lazy val cogenMonth: Cogen[Month] = + Cogen[Int].contramap(_.ordinal) + + // Year + + implicit final lazy val chooseYear: Choose[Year] = + new Choose[Year] { + override def choose(min: Year, max: Year): Gen[Year] = + min.compareTo(max) match { + case 0 => Gen.const(min) + case result if result > 0 => Gen.fail + case _ => + Gen.choose(min.getValue, max.getValue).map(value => Year.of(value)) + } + } + + implicit final lazy val arbYear: Arbitrary[Year] = + Arbitrary(Gen.choose(Year.of(Year.MIN_VALUE), Year.of(Year.MIN_VALUE))) + + implicit final lazy val cogenYear: Cogen[Year] = + Cogen[Int].contramap(_.getValue) + + // LocalDate + + implicit final lazy val chooseLocalDate: Choose[LocalDate] = + new Choose[LocalDate] { + override def choose(min: LocalDate, max: LocalDate): Gen[LocalDate] = + min.compareTo(max) match { + case 0 => Gen.const(min) + case result if result > 0 => Gen.fail + case _ => + Gen.choose(min.getYear, max.getYear).flatMap{year => + val minMonth: Month = + if (year == min.getYear) { + min.getMonth + } else { + Month.JANUARY + } + val maxMonth: Month = + if (year == max.getYear) { + max.getMonth + } else { + Month.DECEMBER + } + Gen.choose(minMonth, maxMonth).flatMap{month => + val minDay: Int = + if (year == min.getYear && month == minMonth) { + min.getDayOfMonth + } else { + 1 + } + val maxDay: Int = + if (year == max.getYear && month == max.getMonth) { + max.getDayOfMonth + } else { + // Calculation is proleptic. Historically inaccurate, but + // correct according to ISO-8601. + month.length(Year.isLeap(year.toLong)) + } + Gen.choose(minDay, maxDay).map(day => + LocalDate.of(year, month, day) + ) + } + } + } + } + + implicit final lazy val arbLocalDate: Arbitrary[LocalDate] = + Arbitrary(Gen.choose(LocalDate.MIN, LocalDate.MAX)) + + implicit final lazy val cogenLocalDate: Cogen[LocalDate] = + Cogen[(Int, Int, Int)].contramap(value => (value.getYear, value.getMonthValue, value.getDayOfMonth)) + + // LocalTime + + implicit final lazy val chooseLocalTime: Choose[LocalTime] = + new Choose[LocalTime] { + def choose(min: LocalTime, max: LocalTime): Gen[LocalTime] = + Gen.choose(min.toNanoOfDay, max.toNanoOfDay).map(nano => + LocalTime.ofNanoOfDay(nano) + ) + } + + implicit final lazy val arbLocalTime: Arbitrary[LocalTime] = + Arbitrary(Gen.choose(LocalTime.MIN, LocalTime.MAX)) + + implicit final lazy val cogenLocalTime: Cogen[LocalTime] = + Cogen[Long].contramap(_.toNanoOfDay) + + // LocalDateTime + + implicit final lazy val chooseLocalDateTime: Choose[LocalDateTime] = + new Choose[LocalDateTime] { + override def choose(min: LocalDateTime, max: LocalDateTime): Gen[LocalDateTime] = { + min.compareTo(max) match { + case 0 => Gen.const(min) + case result if result > 0 => Gen.fail + case _ => + val minLocalDate: LocalDate = + min.toLocalDate + val maxLocalDate: LocalDate = + max.toLocalDate + Gen.choose(minLocalDate, maxLocalDate).flatMap{localDate => + val minLocalTime: LocalTime = + if (localDate == minLocalDate) { + min.toLocalTime + } else { + LocalTime.MIN + } + val maxLocalTime: LocalTime = + if (localDate == maxLocalDate) { + max.toLocalTime + } else { + LocalTime.MAX + } + Gen.choose(minLocalTime, maxLocalTime).map(localTime => + LocalDateTime.of(localDate, localTime) + ) + } + } + } + } + + implicit final lazy val arbLocalDateTime: Arbitrary[LocalDateTime] = + Arbitrary(Gen.choose(LocalDateTime.MIN, LocalDateTime.MAX)) + + implicit final lazy val cogenLocalDateTime: Cogen[LocalDateTime] = + Cogen[(LocalDate, LocalTime)].contramap(value => (value.toLocalDate, value.toLocalTime)) + + // MonthDay + + implicit final lazy val chooseMonthDay: Choose[MonthDay] = + new Choose[MonthDay] { + override def choose(min: MonthDay, max: MonthDay): Gen[MonthDay] = + min.compareTo(max) match { + case 0 => Gen.const(min) + case result if result > 0 => Gen.fail + case _ => + val minMonth: Month = min.getMonth + val maxMonth: Month = max.getMonth + Gen.choose(minMonth, maxMonth).flatMap{month => + val minDayOfMonth: Int = + if (month == minMonth) { + min.getDayOfMonth + } else { + 1 + } + val maxDayOfMonth: Int = + if (month == maxMonth) { + max.getDayOfMonth + } else { + month.maxLength + } + Gen.choose(minDayOfMonth, maxDayOfMonth).map(dayOfMonth => + MonthDay.of(month, dayOfMonth) + ) + } + } + } + + implicit final lazy val arbMonthDay: Arbitrary[MonthDay] = + Arbitrary(Gen.choose(MonthDay.of(Month.JANUARY, 1), MonthDay.of(Month.DECEMBER, 31))) + + implicit final lazy val cogenMonthDay: Cogen[MonthDay] = + Cogen[(Month, Int)].contramap(value => (value.getMonth, value.getDayOfMonth)) + + // ZoneOffset + + /** ZoneOffset values have some unusual semantics when it comes to + * ordering. The short explanation is that `(ZoneOffset.MAX < + * ZoneOffset.MIN) == true`. This is because for any given `LocalDateTime`, + * that time applied to `ZoneOffset.MAX` will be an older moment in time + * than that same `LocalDateTime` applied to `ZoneOffset.MIN`. + * + * From the JavaDoc, + * + * "The offsets are compared in the order that they occur for the same time + * of day around the world. Thus, an offset of +10:00 comes before an + * offset of +09:00 and so on down to -18:00." + * + * This has the following surprising implication, + * + * {{{ + * scala> ZoneOffset.MIN + * val res0: java.time.ZoneOffset = -18:00 + * + * scala> ZoneOffset.MAX + * val res1: java.time.ZoneOffset = +18:00 + * + * scala> ZoneOffset.MIN.compareTo(ZoneOffset.MAX) + * val res3: Int = 129600 + * }}} + * + * This implementation is consistent with that comparison. + * + * @see [[https://docs.oracle.com/javase/8/docs/api/java/time/ZoneOffset.html#compareTo-java.time.ZoneOffset-]] + */ + implicit final lazy val chooseZoneOffset: Choose[ZoneOffset] = + new Choose[ZoneOffset] { + def choose(min: ZoneOffset, max: ZoneOffset): Gen[ZoneOffset] = + min.compareTo(max) match { + case 0 => Gen.const(min) + case result if result > 0 => Gen.fail + case _ => + // Looks flipped, but it is not. + Gen.choose(max.getTotalSeconds, min.getTotalSeconds).map(value => ZoneOffset.ofTotalSeconds(value)) + } + } + + implicit final lazy val arbZoneOffset: Arbitrary[ZoneOffset] = + Arbitrary( + Gen.oneOf( + Gen.oneOf(ZoneOffset.MAX, ZoneOffset.MIN, ZoneOffset.UTC), + Gen.choose(ZoneOffset.MAX, ZoneOffset.MIN) // These look flipped, but they are not. + ) + ) + + implicit final lazy val cogenZoneOffset: Cogen[ZoneOffset] = + Cogen[Int].contramap(_.getTotalSeconds) + + // ZoneId + + /** ''Technically'' the available zone ids can change at runtime, so we store + * an immutable snapshot in time here. We avoid going through the + * scala/java collection converters to avoid having to deal with the scala + * 2.13 changes and adding a dependency on the collection compatibility + * library. + */ + private final lazy val availableZoneIds: Set[ZoneId] = + ZoneId.getAvailableZoneIds.toArray(Array.empty[String]).toSet.map((value: String) => ZoneId.of(value)) + + // ZoneIds by themselves do not describe an offset from UTC (ZoneOffset + // does), so there isn't a meaningful way to define a choose as they can not + // be reasonably ordered. + + implicit final lazy val arbZoneId: Arbitrary[ZoneId] = + Arbitrary(Gen.oneOf(Gen.oneOf(availableZoneIds), arbZoneOffset.arbitrary)) + + implicit final lazy val cogenZoneId: Cogen[ZoneId] = + Cogen[String].contramap(_.toString) // This may seem contrived, and in a + // way it is, but ZoneId values + // _without_ offsets are basically + // just newtypes of String. + + // OffsetTime + + // This type can be particularly mind bending. Because OffsetTime values + // have no associated date, and because the Duration between OffsetTime.MIN + // and OffsetTime.MAX is 36 hours it can be difficult to write Choose for + // OffsetTime. One has to be careful to perturb both the LocalTime value and + // the ZoneOffset in such a way as to not accidentally create an OffsetTime + // value which is < one of the bounds. This is the reason that there are + // more helper functions for this type than others. It is an effort to keep + // clear what is going on. + + private def secondsUntilOffsetRollover(value: OffsetTime): Int = + value.getOffset().getTotalSeconds() + + private def shiftForwardByOffset(value: OffsetTime, seconds: Int): OffsetTime = + value.withOffsetSameLocal(ZoneOffset.ofTotalSeconds(value.getOffset().getTotalSeconds() - seconds)) + + private def genShiftOffsetTimeForward(min: OffsetTime, max: OffsetTime, shift: Duration): Gen[OffsetTime] = { + val shiftSeconds: Int = shift.getSeconds().toInt + val rolloverSeconds: Int = secondsUntilOffsetRollover(min) + val lub: Int = + if (shiftSeconds.compareTo(rolloverSeconds) < 0) { + shiftSeconds + } else { + rolloverSeconds + } + Gen.choose(0, lub).flatMap{offsetShift => + val shifted: OffsetTime = shiftForwardByOffset(min, offsetShift) + val localShiftMin: Duration = { + val durationFromMin: Duration = Duration.between(shifted, min) + val durationAfterMidnight: Duration = Duration.between(shifted.toLocalTime, LocalTime.MIN) + // For negative Duration values, larger absolute values are smaller, + // e.g. Duration.ofHours(-1).compareTo(Duration.ofHours(-2)) > 0. For + // this calculation we want the Duration with the smallest absolute + // value, e.g. the one which compares larger. + if(durationFromMin.compareTo(durationAfterMidnight) > 0) { + durationFromMin + } else { + durationAfterMidnight + } + } + val localShiftMax: Duration = { + val durationFromMax: Duration = Duration.between(shifted, max) + val durationFromMidnight: Duration = Duration.between(shifted.toLocalTime, LocalTime.MAX) + if (durationFromMax.compareTo(durationFromMidnight) < 0) { + durationFromMax + } else { + durationFromMidnight + } + } + Gen.choose(localShiftMin, localShiftMax).map(localShift => + shifted.plus(localShift) + ) + } + } + + implicit final lazy val chooseOffsetTime: Choose[OffsetTime] = { + val epochDate: LocalDate = LocalDate.ofEpochDay(0L) + new Choose[OffsetTime] { + override def choose(min: OffsetTime, max: OffsetTime): Gen[OffsetTime] = + min.compareTo(max) match { + case 0 => Gen.const(min) + case result if result > 0 => Gen.fail + case _ => + Gen.choose(Duration.ZERO, Duration.between(min, max)).flatMap{duration => + genShiftOffsetTimeForward(min, max, duration) + } + } + } + } + + implicit final lazy val arbOffsetTime: Arbitrary[OffsetTime] = + Arbitrary(Gen.choose(OffsetTime.MIN, OffsetTime.MAX)) + + implicit final lazy val cogenOffsetTime: Cogen[OffsetTime] = + Cogen[(LocalTime, ZoneOffset)].contramap(value => (value.toLocalTime, value.getOffset)) + + // OffsetDateTime + + implicit final lazy val chooseOffsetDateTime: Choose[OffsetDateTime] = + new Choose[OffsetDateTime] { + override def choose(min: OffsetDateTime, max: OffsetDateTime): Gen[OffsetDateTime] = + min.compareTo(max) match { + case 0 => Gen.const(min) + case result if result > 0 => Gen.fail + case _ => + Gen.choose(min.getOffset, max.getOffset).flatMap(offset => + Gen.choose(min.toInstant, max.toInstant).map(instant => + OffsetDateTime.ofInstant(instant, offset) + ) + ) + } + } + + implicit final lazy val arbOffsetDateTime: Arbitrary[OffsetDateTime] = + Arbitrary(Gen.choose(OffsetDateTime.MIN, OffsetDateTime.MAX)) + + implicit final lazy val cogenOffsetDateTime: Cogen[OffsetDateTime] = + Cogen[(LocalDateTime, ZoneOffset)].contramap(value => (value.toLocalDateTime, value.getOffset)) + + // Period + + implicit final lazy val arbPeriod: Arbitrary[Period] = + Arbitrary( + for { + years <- Arbitrary.arbitrary[Int] + months <- Arbitrary.arbitrary[Int] + days <- Arbitrary.arbitrary[Int] + } yield Period.of(years, months, days)) + + implicit final lazy val cogenPeriod: Cogen[Period] = + Cogen[(Int, Int, Int)].contramap(value => (value.getYears, value.getMonths, value.getDays)) + + implicit final lazy val shrinkPeriod: Shrink[Period] = + Shrink.xmap[(Int, Int, Int), Period]( + { + case (y, m, d) => Period.of(y, m, d) + }, + value => (value.getYears, value.getMonths, value.getDays) + ) + + // YearMonth + + implicit final lazy val chooseYearMonth: Choose[YearMonth] = + new Choose[YearMonth] { + def choose(min: YearMonth, max: YearMonth): Gen[YearMonth] = + min.compareTo(max) match { + case 0 => Gen.const(min) + case result if result > 0 => Gen.fail + case _ => + val minYear: Year = Year.of(min.getYear) + val maxYear: Year = Year.of(max.getYear) + Gen.choose(minYear, maxYear).flatMap{year => + val minMonth: Month = + if (minYear == year) { + min.getMonth + } else { + Month.JANUARY + } + val maxMonth: Month = + if (maxYear == year) { + max.getMonth + } else { + Month.DECEMBER + } + Gen.choose(minMonth, maxMonth).map(month => + YearMonth.of(year.getValue, month) + ) + } + } + } + + implicit final lazy val arbYearMonth: Arbitrary[YearMonth] = + Arbitrary(Gen.choose(YearMonth.of(Year.MIN_VALUE, Month.JANUARY), YearMonth.of(Year.MAX_VALUE, Month.DECEMBER))) + + implicit final lazy val cogenYearMonth: Cogen[YearMonth] = + Cogen[(Int, Month)].contramap(value => (value.getYear, value.getMonth)) + + // ZonedDateTime + + implicit final lazy val chooseZonedDateTime: Choose[ZonedDateTime] = + new Choose[ZonedDateTime] { + def choose(min: ZonedDateTime, max: ZonedDateTime): Gen[ZonedDateTime] = + min.compareTo(max) match { + case 0 => Gen.const(min) + case result if result > 0 => Gen.fail + case _ => + Gen.choose(min.getOffset, max.getOffset).flatMap(offset => + Gen.choose(min.toInstant, max.toInstant).map(instant => + ZonedDateTime.ofInstant(instant, offset) + ) + ) + } + } + + implicit final lazy val arbZonedDateTime: Arbitrary[ZonedDateTime] = + // The ZoneOffset's here look flipped by they are + // not. ZonedDateTime.of(LocalDateTime.MIN, ZoneOffset.MAX) is _older_ + // than ZonedDateTime.of(LocalDateTime, ZoneOffset.MIN). + Arbitrary(Gen.choose(ZonedDateTime.of(LocalDateTime.MIN, ZoneOffset.MAX), ZonedDateTime.of(LocalDateTime.MAX, ZoneOffset.MIN))) + + implicit final lazy val cogenZonedDateTime: Cogen[ZonedDateTime] = + Cogen[(LocalDateTime, ZoneOffset)].contramap(value => (value.toLocalDateTime, value.getOffset)) + + // DayOfWeek + + implicit final lazy val cogenDayOfWeek: Cogen[DayOfWeek] = + Cogen[Int].contramap(_.ordinal) + + // temporal.ChronoField + + implicit final lazy val cogenChronoField: Cogen[ChronoField] = + Cogen[Int].contramap(_.ordinal) +} diff --git a/jvm/src/main/scala/org/scalacheck/time/package.scala b/jvm/src/main/scala/org/scalacheck/time/package.scala new file mode 100644 index 000000000..51c187cd2 --- /dev/null +++ b/jvm/src/main/scala/org/scalacheck/time/package.scala @@ -0,0 +1,3 @@ +package org.scalacheck + +package object time extends time.JavaTimeInstances diff --git a/jvm/src/test/scala/org/scalacheck/time/CogenLaws.scala b/jvm/src/test/scala/org/scalacheck/time/CogenLaws.scala new file mode 100644 index 000000000..838e42952 --- /dev/null +++ b/jvm/src/test/scala/org/scalacheck/time/CogenLaws.scala @@ -0,0 +1,28 @@ +package org.scalacheck.time + +import java.time._ +import org.scalacheck.Gen._ +import org.scalacheck.Prop._ +import org.scalacheck.Shrink._ +import org.scalacheck._ +import scala.util._ + +object CogenLaws extends Properties("java.time CogenLaws") { + import CogenSpecification._ + + include(cogenLaws[Duration], "cogenDuration") + include(cogenLaws[Instant], "cogenInstant") + include(cogenLaws[Month], "cogenMonth") + include(cogenLaws[Year], "cogenYear") + include(cogenLaws[LocalTime], "cogenLocalTime") + include(cogenLaws[LocalDate], "cogenLocalDate") + include(cogenLaws[LocalDateTime], "cogenLocalDateTime") + include(cogenLaws[MonthDay], "cogenMonthDay") + include(cogenLaws[ZoneOffset], "cogenZoneOffset") + include(cogenLaws[OffsetTime], "cogenOffsetTime") + include(cogenLaws[OffsetDateTime], "cogenOffsetDateTime") + include(cogenLaws[YearMonth], "cogenYearMonth") + include(cogenLaws[ZonedDateTime], "cogenZonedDateTime") + include(cogenLaws[ZoneId], "cogenZoneId") + include(cogenLaws[Period], "cogenPeriod") +} diff --git a/jvm/src/test/scala/org/scalacheck/time/GenSpecification.scala b/jvm/src/test/scala/org/scalacheck/time/GenSpecification.scala new file mode 100644 index 000000000..6c0ded6d7 --- /dev/null +++ b/jvm/src/test/scala/org/scalacheck/time/GenSpecification.scala @@ -0,0 +1,47 @@ +package org.scalacheck.time + +import java.time._ +import org.scalacheck.Gen._ +import org.scalacheck.Prop._ +import org.scalacheck.Shrink._ +import org.scalacheck._ +import scala.util._ + +object GenSpecification extends Properties("java.time Gen"){ + + private[this] def chooseProp[A](implicit C: Choose[A], A: Arbitrary[A], O: Ordering[A]): Prop = { + import O.mkOrderingOps + forAll { (l: A, h: A) => + Try(choose(l, h)) match { + case Success(g) => forAll(g) { x => l <= x && x <= h } + case Failure(_) => Prop(l > h) + } + } + } + + property("choose-duration") = chooseProp[Duration] + + property("choose-instant") = chooseProp[Instant] + + property("choose-month") = chooseProp[Month] + + property("choose-year") = chooseProp[Year] + + property("choose-localTime") = chooseProp[LocalTime] + + property("choose-localDate") = chooseProp[LocalDate] + + property("choose-localDateTime") = chooseProp[LocalDateTime] + + property("choose-monthDay") = chooseProp[MonthDay] + + property("choose-zoneOffset") = chooseProp[ZoneOffset] + + property("choose-offsetTime") = chooseProp[OffsetTime] + + property("choose-offsetDateTime") = chooseProp[OffsetDateTime] + + property("choose-yearMonth") = chooseProp[YearMonth] + + property("choose-zonedDateTime") = chooseProp[ZonedDateTime] +} diff --git a/jvm/src/test/scala/org/scalacheck/time/ShrinkSpecification.scala b/jvm/src/test/scala/org/scalacheck/time/ShrinkSpecification.scala new file mode 100644 index 000000000..5c8f02184 --- /dev/null +++ b/jvm/src/test/scala/org/scalacheck/time/ShrinkSpecification.scala @@ -0,0 +1,12 @@ +package org.scalacheck.time + +import java.time._ +import org.scalacheck.Prop._ +import org.scalacheck.Shrink._ +import org.scalacheck._ + +object ShrinkSpecification extends Properties ("java.time Shrink"){ + property("shrink[Duration]") = forAll { (n: Duration) => + !shrink(n).contains(n) + } +} From f23a5697623b63046ed1570c94cb0167170c1a87 Mon Sep 17 00:00:00 2001 From: David Strawn Date: Fri, 18 Dec 2020 16:57:17 -0700 Subject: [PATCH 2/7] Add Orphan `Ordering` Instances In Tests For Scala <= 2.12 Scala <= 2.12 gets confused because some of the `java.time._` types implement `Comparable` on their super type. Thus this commit adds explicit orphaned `Ordering` instances for them, but only for Scala <= 2.12 and only in jvm tests. --- build.sbt | 4 ++++ .../org/scalacheck/time/OrphanInstances.scala | 6 ++++++ .../org/scalacheck/time/OrphanInstances.scala | 19 +++++++++++++++++++ .../scala/org/scalacheck/time/CogenLaws.scala | 3 --- .../scalacheck/time/GenSpecification.scala | 2 +- 5 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 jvm/src/test/scala-2.13+/org/scalacheck/time/OrphanInstances.scala create mode 100644 jvm/src/test/scala-2.13-/org/scalacheck/time/OrphanInstances.scala diff --git a/build.sbt b/build.sbt index 7e668c6fb..bac8f4245 100644 --- a/build.sbt +++ b/build.sbt @@ -265,6 +265,10 @@ lazy val jvm = project.in(file("jvm")) scalaMajorVersion.value == 13 || isDotty.value // ==> true // else ==> false }, + Test / unmanagedSourceDirectories += { + val s = if (scalaMajorVersion.value >= 13) "+" else "-" + baseDirectory.value / "src" / "test" / s"scala-2.13$s" + }, libraryDependencies += "org.scala-sbt" % "test-interface" % "1.0" ) diff --git a/jvm/src/test/scala-2.13+/org/scalacheck/time/OrphanInstances.scala b/jvm/src/test/scala-2.13+/org/scalacheck/time/OrphanInstances.scala new file mode 100644 index 000000000..a1555ac6a --- /dev/null +++ b/jvm/src/test/scala-2.13+/org/scalacheck/time/OrphanInstances.scala @@ -0,0 +1,6 @@ +package org.scalacheck.time + +/** This is unused on Scala >= 2.13. On Scala <= 2.12 it is used to help the + * compiler figure out some `Ordering` instances needed for testing. + */ +private[time] trait OrphanInstances {} diff --git a/jvm/src/test/scala-2.13-/org/scalacheck/time/OrphanInstances.scala b/jvm/src/test/scala-2.13-/org/scalacheck/time/OrphanInstances.scala new file mode 100644 index 000000000..654d8aa42 --- /dev/null +++ b/jvm/src/test/scala-2.13-/org/scalacheck/time/OrphanInstances.scala @@ -0,0 +1,19 @@ +package org.scalacheck.time + +import java.time._ +import java.time.chrono._ + +/** On Scala <= 2.12 it is used to help the compiler figure out some `Ordering` + * instances needed for testing. + */ +private[time] trait OrphanInstances { + + implicit final lazy val localDateOrdering: Ordering[LocalDate] = + Ordering.by((ld: LocalDate) => (ld: ChronoLocalDate)) + + implicit final lazy val localDateTimeOrdering: Ordering[LocalDateTime] = + Ordering.by((ldt: LocalDateTime) => (ldt: ChronoLocalDateTime[_])) + + implicit final lazy val zonedDateTimeOrdering: Ordering[ZonedDateTime] = + Ordering.by((zdt: ZonedDateTime) => (zdt: ChronoZonedDateTime[_])) +} diff --git a/jvm/src/test/scala/org/scalacheck/time/CogenLaws.scala b/jvm/src/test/scala/org/scalacheck/time/CogenLaws.scala index 838e42952..64019a1b0 100644 --- a/jvm/src/test/scala/org/scalacheck/time/CogenLaws.scala +++ b/jvm/src/test/scala/org/scalacheck/time/CogenLaws.scala @@ -1,9 +1,6 @@ package org.scalacheck.time import java.time._ -import org.scalacheck.Gen._ -import org.scalacheck.Prop._ -import org.scalacheck.Shrink._ import org.scalacheck._ import scala.util._ diff --git a/jvm/src/test/scala/org/scalacheck/time/GenSpecification.scala b/jvm/src/test/scala/org/scalacheck/time/GenSpecification.scala index 6c0ded6d7..199b7eeb4 100644 --- a/jvm/src/test/scala/org/scalacheck/time/GenSpecification.scala +++ b/jvm/src/test/scala/org/scalacheck/time/GenSpecification.scala @@ -7,7 +7,7 @@ import org.scalacheck.Shrink._ import org.scalacheck._ import scala.util._ -object GenSpecification extends Properties("java.time Gen"){ +object GenSpecification extends Properties("java.time Gen") with OrphanInstances { private[this] def chooseProp[A](implicit C: Choose[A], A: Arbitrary[A], O: Ordering[A]): Prop = { import O.mkOrderingOps From 6f1cd803cf672dfd71fa7edb6ef544600c443a5a Mon Sep 17 00:00:00 2001 From: David Strawn Date: Fri, 18 Dec 2020 17:06:14 -0700 Subject: [PATCH 3/7] Remove Unused `epochDate` Value --- .../main/scala/org/scalacheck/time/JavaTimeInstances.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala b/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala index f302fc686..f58339bff 100644 --- a/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala +++ b/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala @@ -416,8 +416,7 @@ private[time] trait JavaTimeInstances { } } - implicit final lazy val chooseOffsetTime: Choose[OffsetTime] = { - val epochDate: LocalDate = LocalDate.ofEpochDay(0L) + implicit final lazy val chooseOffsetTime: Choose[OffsetTime] = new Choose[OffsetTime] { override def choose(min: OffsetTime, max: OffsetTime): Gen[OffsetTime] = min.compareTo(max) match { @@ -429,7 +428,6 @@ private[time] trait JavaTimeInstances { } } } - } implicit final lazy val arbOffsetTime: Arbitrary[OffsetTime] = Arbitrary(Gen.choose(OffsetTime.MIN, OffsetTime.MAX)) From 80c58e8b02c6da2a044c200cad8929d5c5398ee4 Mon Sep 17 00:00:00 2001 From: David Strawn Date: Sat, 19 Dec 2020 17:20:12 -0700 Subject: [PATCH 4/7] Fix Typo In Comment --- .../org/scalacheck/time/JavaTimeInstances.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala b/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala index f58339bff..6d5283575 100644 --- a/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala +++ b/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala @@ -364,12 +364,12 @@ private[time] trait JavaTimeInstances { // This type can be particularly mind bending. Because OffsetTime values // have no associated date, and because the Duration between OffsetTime.MIN - // and OffsetTime.MAX is 36 hours it can be difficult to write Choose for - // OffsetTime. One has to be careful to perturb both the LocalTime value and - // the ZoneOffset in such a way as to not accidentally create an OffsetTime - // value which is < one of the bounds. This is the reason that there are - // more helper functions for this type than others. It is an effort to keep - // clear what is going on. + // and OffsetTime.MAX is 59 hours (and change) it can be difficult to write + // Choose for OffsetTime. One has to be careful to perturb both the + // LocalTime value and the ZoneOffset in such a way as to not accidentally + // create an OffsetTime value which is < one of the bounds. This is the + // reason that there are more helper functions for this type than others. It + // is an effort to keep clear what is going on. private def secondsUntilOffsetRollover(value: OffsetTime): Int = value.getOffset().getTotalSeconds() From 387336981042e39c8d3406db2208a62ab22e37a4 Mon Sep 17 00:00:00 2001 From: David Strawn Date: Sat, 19 Dec 2020 17:21:07 -0700 Subject: [PATCH 5/7] Fix Typo In Comment --- .../main/scala/org/scalacheck/time/JavaTimeInstances.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala b/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala index 6d5283575..4ad23cdbb 100644 --- a/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala +++ b/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala @@ -367,9 +367,9 @@ private[time] trait JavaTimeInstances { // and OffsetTime.MAX is 59 hours (and change) it can be difficult to write // Choose for OffsetTime. One has to be careful to perturb both the // LocalTime value and the ZoneOffset in such a way as to not accidentally - // create an OffsetTime value which is < one of the bounds. This is the - // reason that there are more helper functions for this type than others. It - // is an effort to keep clear what is going on. + // create an OffsetTime value which is < the min bound or > the max + // bound. This is the reason that there are more helper functions for this + // type than others. It is an effort to keep clear what is going on. private def secondsUntilOffsetRollover(value: OffsetTime): Int = value.getOffset().getTotalSeconds() From cff95da7a7e28039f797df84a0c49c0d88d4f40a Mon Sep 17 00:00:00 2001 From: David Strawn Date: Mon, 21 Dec 2020 08:52:31 -0700 Subject: [PATCH 6/7] Add Missing Test For `Shrink[Period]` --- .../test/scala/org/scalacheck/time/ShrinkSpecification.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jvm/src/test/scala/org/scalacheck/time/ShrinkSpecification.scala b/jvm/src/test/scala/org/scalacheck/time/ShrinkSpecification.scala index 5c8f02184..8a2dfc2cd 100644 --- a/jvm/src/test/scala/org/scalacheck/time/ShrinkSpecification.scala +++ b/jvm/src/test/scala/org/scalacheck/time/ShrinkSpecification.scala @@ -9,4 +9,8 @@ object ShrinkSpecification extends Properties ("java.time Shrink"){ property("shrink[Duration]") = forAll { (n: Duration) => !shrink(n).contains(n) } + + property("shrink[Period]") = forAll { (n: Period) => + !shrink(n).contains(n) + } } From 2ccd814a5a33e53ebd0c73480388407fb955a5cc Mon Sep 17 00:00:00 2001 From: David Strawn Date: Wed, 13 Jan 2021 10:45:09 -0700 Subject: [PATCH 7/7] Automatically Bring Java Time Instances Into Implicit Scope This commit breaks up the `JavaTimeInstances` trait into 4 separate traits, one for each typeclass. Each typeclass's companion object now extends the corresponding trait. This has the effect of automatically bringing the instances into implicit scope on the JVM. For ScalaJs and Scala Native, the traits are just empty stubs. --- .../scalacheck/time/JavaTimeArbitrary.scala | 4 + .../org/scalacheck/time/JavaTimeChoose.scala | 4 + .../org/scalacheck/time/JavaTimeCogen.scala | 4 + .../org/scalacheck/time/JavaTimeShrink.scala | 4 + .../scalacheck/time/JavaTimeArbitrary.scala | 118 +++++++++++ ...meInstances.scala => JavaTimeChoose.scala} | 186 ++---------------- .../org/scalacheck/time/JavaTimeCogen.scala | 102 ++++++++++ .../org/scalacheck/time/JavaTimeShrink.scala | 30 +++ .../scala/org/scalacheck/time/package.scala | 3 - .../scalacheck/time/JavaTimeArbitrary.scala | 4 + .../org/scalacheck/time/JavaTimeChoose.scala | 4 + .../org/scalacheck/time/JavaTimeCogen.scala | 4 + .../org/scalacheck/time/JavaTimeShrink.scala | 4 + src/main/scala/org/scalacheck/Arbitrary.scala | 2 +- src/main/scala/org/scalacheck/Cogen.scala | 2 +- src/main/scala/org/scalacheck/Gen.scala | 2 +- src/main/scala/org/scalacheck/Shrink.scala | 2 +- 17 files changed, 298 insertions(+), 181 deletions(-) create mode 100644 js/src/main/scala/org/scalacheck/time/JavaTimeArbitrary.scala create mode 100644 js/src/main/scala/org/scalacheck/time/JavaTimeChoose.scala create mode 100644 js/src/main/scala/org/scalacheck/time/JavaTimeCogen.scala create mode 100644 js/src/main/scala/org/scalacheck/time/JavaTimeShrink.scala create mode 100644 jvm/src/main/scala/org/scalacheck/time/JavaTimeArbitrary.scala rename jvm/src/main/scala/org/scalacheck/time/{JavaTimeInstances.scala => JavaTimeChoose.scala} (64%) create mode 100644 jvm/src/main/scala/org/scalacheck/time/JavaTimeCogen.scala create mode 100644 jvm/src/main/scala/org/scalacheck/time/JavaTimeShrink.scala delete mode 100644 jvm/src/main/scala/org/scalacheck/time/package.scala create mode 100644 native/src/main/scala/org/scalacheck/time/JavaTimeArbitrary.scala create mode 100644 native/src/main/scala/org/scalacheck/time/JavaTimeChoose.scala create mode 100644 native/src/main/scala/org/scalacheck/time/JavaTimeCogen.scala create mode 100644 native/src/main/scala/org/scalacheck/time/JavaTimeShrink.scala diff --git a/js/src/main/scala/org/scalacheck/time/JavaTimeArbitrary.scala b/js/src/main/scala/org/scalacheck/time/JavaTimeArbitrary.scala new file mode 100644 index 000000000..6743327e1 --- /dev/null +++ b/js/src/main/scala/org/scalacheck/time/JavaTimeArbitrary.scala @@ -0,0 +1,4 @@ +package org.scalacheck.time + +/** Stub trait since ScalaJs does not have native support for java.time types. */ +private[scalacheck] trait JavaTimeArbitrary diff --git a/js/src/main/scala/org/scalacheck/time/JavaTimeChoose.scala b/js/src/main/scala/org/scalacheck/time/JavaTimeChoose.scala new file mode 100644 index 000000000..d27e278ba --- /dev/null +++ b/js/src/main/scala/org/scalacheck/time/JavaTimeChoose.scala @@ -0,0 +1,4 @@ +package org.scalacheck.time + +/** Stub trait since ScalaJs does not have native support for java.time types. */ +private[scalacheck] trait JavaTimeChoose diff --git a/js/src/main/scala/org/scalacheck/time/JavaTimeCogen.scala b/js/src/main/scala/org/scalacheck/time/JavaTimeCogen.scala new file mode 100644 index 000000000..5d8cbfb9e --- /dev/null +++ b/js/src/main/scala/org/scalacheck/time/JavaTimeCogen.scala @@ -0,0 +1,4 @@ +package org.scalacheck.time + +/** Stub trait since ScalaJs does not have native support for java.time types. */ +private[scalacheck] trait JavaTimeCogen diff --git a/js/src/main/scala/org/scalacheck/time/JavaTimeShrink.scala b/js/src/main/scala/org/scalacheck/time/JavaTimeShrink.scala new file mode 100644 index 000000000..ca02c4738 --- /dev/null +++ b/js/src/main/scala/org/scalacheck/time/JavaTimeShrink.scala @@ -0,0 +1,4 @@ +package org.scalacheck.time + +/** Stub trait since ScalaJs does not have native support for java.time types. */ +private[scalacheck] trait JavaTimeShrink diff --git a/jvm/src/main/scala/org/scalacheck/time/JavaTimeArbitrary.scala b/jvm/src/main/scala/org/scalacheck/time/JavaTimeArbitrary.scala new file mode 100644 index 000000000..69d0f8db1 --- /dev/null +++ b/jvm/src/main/scala/org/scalacheck/time/JavaTimeArbitrary.scala @@ -0,0 +1,118 @@ +package org.scalacheck.time + +import org.scalacheck._ +import java.time._ + +/** [[Arbitrary]] instances for `java.time` types. + * + * @note [[Arbitrary]] instances for `java.time` types which are Java enum + * types are provided by ScalaCheck's general Java enum support. + */ +private[scalacheck] trait JavaTimeArbitrary { + + // Duration + + // Java duration values are conceptually infinite, thus they do not expose + // Duration.MAX/Duration.MIN values, but in practice they are finite, + // restricted by their underlying representation a long and an int. + + private final lazy val minJavaDuration: Duration = + Duration.ofSeconds(Long.MinValue) + + private final lazy val maxJavaDuration: Duration = + Duration.ofSeconds(Long.MaxValue, 999999999L) + + implicit final lazy val arbJavaDuration: Arbitrary[Duration] = + Arbitrary(Gen.choose(minJavaDuration, maxJavaDuration)) + + // Instant + + implicit final lazy val arbInstant: Arbitrary[Instant] = + Arbitrary(Gen.choose(Instant.MIN, Instant.MAX)) + + // Year + + implicit final lazy val arbYear: Arbitrary[Year] = + Arbitrary(Gen.choose(Year.of(Year.MIN_VALUE), Year.of(Year.MIN_VALUE))) + + // LocalDate + + implicit final lazy val arbLocalDate: Arbitrary[LocalDate] = + Arbitrary(Gen.choose(LocalDate.MIN, LocalDate.MAX)) + + // LocalTime + + implicit final lazy val arbLocalTime: Arbitrary[LocalTime] = + Arbitrary(Gen.choose(LocalTime.MIN, LocalTime.MAX)) + + // LocalDateTime + + implicit final lazy val arbLocalDateTime: Arbitrary[LocalDateTime] = + Arbitrary(Gen.choose(LocalDateTime.MIN, LocalDateTime.MAX)) + + // MonthDay + + implicit final lazy val arbMonthDay: Arbitrary[MonthDay] = + Arbitrary(Gen.choose(MonthDay.of(Month.JANUARY, 1), MonthDay.of(Month.DECEMBER, 31))) + + // ZoneOffset + + implicit final lazy val arbZoneOffset: Arbitrary[ZoneOffset] = + Arbitrary( + Gen.oneOf( + Gen.oneOf(ZoneOffset.MAX, ZoneOffset.MIN, ZoneOffset.UTC), + Gen.choose(ZoneOffset.MAX, ZoneOffset.MIN) // These look flipped, but they are not. + ) + ) + + // ZoneId + + /** ''Technically'' the available zone ids can change at runtime, so we store + * an immutable snapshot in time here. We avoid going through the + * scala/java collection converters to avoid having to deal with the scala + * 2.13 changes and adding a dependency on the collection compatibility + * library. + */ + private final lazy val availableZoneIds: Set[ZoneId] = + ZoneId.getAvailableZoneIds.toArray(Array.empty[String]).toSet.map((value: String) => ZoneId.of(value)) + + // ZoneIds by themselves do not describe an offset from UTC (ZoneOffset + // does), so there isn't a meaningful way to define a choose as they can not + // be reasonably ordered. + + implicit final lazy val arbZoneId: Arbitrary[ZoneId] = + Arbitrary(Gen.oneOf(Gen.oneOf(availableZoneIds), arbZoneOffset.arbitrary)) + + // OffsetTime + + implicit final lazy val arbOffsetTime: Arbitrary[OffsetTime] = + Arbitrary(Gen.choose(OffsetTime.MIN, OffsetTime.MAX)) + + // OffsetDateTime + + implicit final lazy val arbOffsetDateTime: Arbitrary[OffsetDateTime] = + Arbitrary(Gen.choose(OffsetDateTime.MIN, OffsetDateTime.MAX)) + + // Period + + implicit final lazy val arbPeriod: Arbitrary[Period] = + Arbitrary( + for { + years <- Arbitrary.arbitrary[Int] + months <- Arbitrary.arbitrary[Int] + days <- Arbitrary.arbitrary[Int] + } yield Period.of(years, months, days)) + + // YearMonth + + implicit final lazy val arbYearMonth: Arbitrary[YearMonth] = + Arbitrary(Gen.choose(YearMonth.of(Year.MIN_VALUE, Month.JANUARY), YearMonth.of(Year.MAX_VALUE, Month.DECEMBER))) + + // ZonedDateTime + + implicit final lazy val arbZonedDateTime: Arbitrary[ZonedDateTime] = + // The ZoneOffset's here look flipped but they are + // not. ZonedDateTime.of(LocalDateTime.MIN, ZoneOffset.MAX) is _older_ + // than ZonedDateTime.of(LocalDateTime, ZoneOffset.MIN). + Arbitrary(Gen.choose(ZonedDateTime.of(LocalDateTime.MIN, ZoneOffset.MAX), ZonedDateTime.of(LocalDateTime.MAX, ZoneOffset.MIN))) +} diff --git a/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala b/jvm/src/main/scala/org/scalacheck/time/JavaTimeChoose.scala similarity index 64% rename from jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala rename to jvm/src/main/scala/org/scalacheck/time/JavaTimeChoose.scala index 4ad23cdbb..eca9cb76f 100644 --- a/jvm/src/main/scala/org/scalacheck/time/JavaTimeInstances.scala +++ b/jvm/src/main/scala/org/scalacheck/time/JavaTimeChoose.scala @@ -2,32 +2,18 @@ package org.scalacheck.time import org.scalacheck._ import java.time._ -import java.time.temporal._ import org.scalacheck.Gen.Choose -/** Instances for `java.time` types. */ -private[time] trait JavaTimeInstances { - - // temporal.ChronoUnit - // - // Arbitrary and Choose instances are already provided by Java enum support. - - implicit final lazy val cogenChronoUnit: Cogen[ChronoUnit] = - Cogen[Int].contramap(_.ordinal) +/** [[Gen#Choose]] instances for `java.time` types. + * + * @note [[Gen#Choose]] instances for `java.time` types which are Java enum + * types are provided by ScalaCheck's general Java enum support. + */ +private[scalacheck] trait JavaTimeChoose { // Duration - // Java duration values are conceptually infinite, thus they do not expose - // Duration.MAX/Duration.MIN values, but in practice they are finite, - // restricted by their underlying representation a long and an int. - - private final lazy val minDuration: Duration = - Duration.ofSeconds(Long.MinValue) - - private final lazy val maxDuration: Duration = - Duration.ofSeconds(Long.MaxValue, 999999999L) - - implicit final lazy val chooseDuration: Choose[Duration] = + implicit final lazy val chooseJavaDuration: Choose[Duration] = new Choose[Duration] { override def choose(min: Duration, max: Duration): Gen[Duration] = { min.compareTo(max) match { @@ -57,22 +43,6 @@ private[time] trait JavaTimeInstances { } } - implicit final lazy val arbDuration: Arbitrary[Duration] = - Arbitrary(Gen.choose(minDuration, maxDuration)) - - implicit final lazy val cogenDuration: Cogen[Duration] = - Cogen[(Long, Int)].contramap(value => (value.getSeconds, value.getNano)) - - implicit final lazy val shrinkDuration: Shrink[Duration] = - Shrink[Duration]{value => - val q: Duration = value.dividedBy(2) - if (q == Duration.ZERO) { - Stream(Duration.ZERO) - } else { - q #:: q.negated #:: shrinkDuration.shrink(q) - } - } - // Instant implicit final lazy val chooseInstant: Choose[Instant] = @@ -102,12 +72,6 @@ private[time] trait JavaTimeInstances { } } - implicit final lazy val arbInstant: Arbitrary[Instant] = - Arbitrary(Gen.choose(Instant.MIN, Instant.MAX)) - - implicit final lazy val cogenInstant: Cogen[Instant] = - Cogen[(Long, Int)].contramap(value => (value.getEpochSecond, value.getNano)) - // Month implicit final lazy val chooseMonth: Choose[Month] = @@ -116,9 +80,6 @@ private[time] trait JavaTimeInstances { _.ordinal ) - implicit final lazy val cogenMonth: Cogen[Month] = - Cogen[Int].contramap(_.ordinal) - // Year implicit final lazy val chooseYear: Choose[Year] = @@ -132,12 +93,6 @@ private[time] trait JavaTimeInstances { } } - implicit final lazy val arbYear: Arbitrary[Year] = - Arbitrary(Gen.choose(Year.of(Year.MIN_VALUE), Year.of(Year.MIN_VALUE))) - - implicit final lazy val cogenYear: Cogen[Year] = - Cogen[Int].contramap(_.getValue) - // LocalDate implicit final lazy val chooseLocalDate: Choose[LocalDate] = @@ -183,28 +138,16 @@ private[time] trait JavaTimeInstances { } } - implicit final lazy val arbLocalDate: Arbitrary[LocalDate] = - Arbitrary(Gen.choose(LocalDate.MIN, LocalDate.MAX)) - - implicit final lazy val cogenLocalDate: Cogen[LocalDate] = - Cogen[(Int, Int, Int)].contramap(value => (value.getYear, value.getMonthValue, value.getDayOfMonth)) - // LocalTime implicit final lazy val chooseLocalTime: Choose[LocalTime] = new Choose[LocalTime] { - def choose(min: LocalTime, max: LocalTime): Gen[LocalTime] = + override def choose(min: LocalTime, max: LocalTime): Gen[LocalTime] = Gen.choose(min.toNanoOfDay, max.toNanoOfDay).map(nano => LocalTime.ofNanoOfDay(nano) ) } - implicit final lazy val arbLocalTime: Arbitrary[LocalTime] = - Arbitrary(Gen.choose(LocalTime.MIN, LocalTime.MAX)) - - implicit final lazy val cogenLocalTime: Cogen[LocalTime] = - Cogen[Long].contramap(_.toNanoOfDay) - // LocalDateTime implicit final lazy val chooseLocalDateTime: Choose[LocalDateTime] = @@ -239,12 +182,6 @@ private[time] trait JavaTimeInstances { } } - implicit final lazy val arbLocalDateTime: Arbitrary[LocalDateTime] = - Arbitrary(Gen.choose(LocalDateTime.MIN, LocalDateTime.MAX)) - - implicit final lazy val cogenLocalDateTime: Cogen[LocalDateTime] = - Cogen[(LocalDate, LocalTime)].contramap(value => (value.toLocalDate, value.toLocalTime)) - // MonthDay implicit final lazy val chooseMonthDay: Choose[MonthDay] = @@ -276,12 +213,6 @@ private[time] trait JavaTimeInstances { } } - implicit final lazy val arbMonthDay: Arbitrary[MonthDay] = - Arbitrary(Gen.choose(MonthDay.of(Month.JANUARY, 1), MonthDay.of(Month.DECEMBER, 31))) - - implicit final lazy val cogenMonthDay: Cogen[MonthDay] = - Cogen[(Month, Int)].contramap(value => (value.getMonth, value.getDayOfMonth)) - // ZoneOffset /** ZoneOffset values have some unusual semantics when it comes to @@ -296,7 +227,7 @@ private[time] trait JavaTimeInstances { * of day around the world. Thus, an offset of +10:00 comes before an * offset of +09:00 and so on down to -18:00." * - * This has the following surprising implication, + * This has the following implication, * * {{{ * scala> ZoneOffset.MIN @@ -315,7 +246,7 @@ private[time] trait JavaTimeInstances { */ implicit final lazy val chooseZoneOffset: Choose[ZoneOffset] = new Choose[ZoneOffset] { - def choose(min: ZoneOffset, max: ZoneOffset): Gen[ZoneOffset] = + override def choose(min: ZoneOffset, max: ZoneOffset): Gen[ZoneOffset] = min.compareTo(max) match { case 0 => Gen.const(min) case result if result > 0 => Gen.fail @@ -325,41 +256,6 @@ private[time] trait JavaTimeInstances { } } - implicit final lazy val arbZoneOffset: Arbitrary[ZoneOffset] = - Arbitrary( - Gen.oneOf( - Gen.oneOf(ZoneOffset.MAX, ZoneOffset.MIN, ZoneOffset.UTC), - Gen.choose(ZoneOffset.MAX, ZoneOffset.MIN) // These look flipped, but they are not. - ) - ) - - implicit final lazy val cogenZoneOffset: Cogen[ZoneOffset] = - Cogen[Int].contramap(_.getTotalSeconds) - - // ZoneId - - /** ''Technically'' the available zone ids can change at runtime, so we store - * an immutable snapshot in time here. We avoid going through the - * scala/java collection converters to avoid having to deal with the scala - * 2.13 changes and adding a dependency on the collection compatibility - * library. - */ - private final lazy val availableZoneIds: Set[ZoneId] = - ZoneId.getAvailableZoneIds.toArray(Array.empty[String]).toSet.map((value: String) => ZoneId.of(value)) - - // ZoneIds by themselves do not describe an offset from UTC (ZoneOffset - // does), so there isn't a meaningful way to define a choose as they can not - // be reasonably ordered. - - implicit final lazy val arbZoneId: Arbitrary[ZoneId] = - Arbitrary(Gen.oneOf(Gen.oneOf(availableZoneIds), arbZoneOffset.arbitrary)) - - implicit final lazy val cogenZoneId: Cogen[ZoneId] = - Cogen[String].contramap(_.toString) // This may seem contrived, and in a - // way it is, but ZoneId values - // _without_ offsets are basically - // just newtypes of String. - // OffsetTime // This type can be particularly mind bending. Because OffsetTime values @@ -429,12 +325,6 @@ private[time] trait JavaTimeInstances { } } - implicit final lazy val arbOffsetTime: Arbitrary[OffsetTime] = - Arbitrary(Gen.choose(OffsetTime.MIN, OffsetTime.MAX)) - - implicit final lazy val cogenOffsetTime: Cogen[OffsetTime] = - Cogen[(LocalTime, ZoneOffset)].contramap(value => (value.toLocalTime, value.getOffset)) - // OffsetDateTime implicit final lazy val chooseOffsetDateTime: Choose[OffsetDateTime] = @@ -452,38 +342,11 @@ private[time] trait JavaTimeInstances { } } - implicit final lazy val arbOffsetDateTime: Arbitrary[OffsetDateTime] = - Arbitrary(Gen.choose(OffsetDateTime.MIN, OffsetDateTime.MAX)) - - implicit final lazy val cogenOffsetDateTime: Cogen[OffsetDateTime] = - Cogen[(LocalDateTime, ZoneOffset)].contramap(value => (value.toLocalDateTime, value.getOffset)) - - // Period - - implicit final lazy val arbPeriod: Arbitrary[Period] = - Arbitrary( - for { - years <- Arbitrary.arbitrary[Int] - months <- Arbitrary.arbitrary[Int] - days <- Arbitrary.arbitrary[Int] - } yield Period.of(years, months, days)) - - implicit final lazy val cogenPeriod: Cogen[Period] = - Cogen[(Int, Int, Int)].contramap(value => (value.getYears, value.getMonths, value.getDays)) - - implicit final lazy val shrinkPeriod: Shrink[Period] = - Shrink.xmap[(Int, Int, Int), Period]( - { - case (y, m, d) => Period.of(y, m, d) - }, - value => (value.getYears, value.getMonths, value.getDays) - ) - // YearMonth implicit final lazy val chooseYearMonth: Choose[YearMonth] = new Choose[YearMonth] { - def choose(min: YearMonth, max: YearMonth): Gen[YearMonth] = + override def choose(min: YearMonth, max: YearMonth): Gen[YearMonth] = min.compareTo(max) match { case 0 => Gen.const(min) case result if result > 0 => Gen.fail @@ -510,17 +373,11 @@ private[time] trait JavaTimeInstances { } } - implicit final lazy val arbYearMonth: Arbitrary[YearMonth] = - Arbitrary(Gen.choose(YearMonth.of(Year.MIN_VALUE, Month.JANUARY), YearMonth.of(Year.MAX_VALUE, Month.DECEMBER))) - - implicit final lazy val cogenYearMonth: Cogen[YearMonth] = - Cogen[(Int, Month)].contramap(value => (value.getYear, value.getMonth)) - // ZonedDateTime implicit final lazy val chooseZonedDateTime: Choose[ZonedDateTime] = new Choose[ZonedDateTime] { - def choose(min: ZonedDateTime, max: ZonedDateTime): Gen[ZonedDateTime] = + override def choose(min: ZonedDateTime, max: ZonedDateTime): Gen[ZonedDateTime] = min.compareTo(max) match { case 0 => Gen.const(min) case result if result > 0 => Gen.fail @@ -532,23 +389,4 @@ private[time] trait JavaTimeInstances { ) } } - - implicit final lazy val arbZonedDateTime: Arbitrary[ZonedDateTime] = - // The ZoneOffset's here look flipped by they are - // not. ZonedDateTime.of(LocalDateTime.MIN, ZoneOffset.MAX) is _older_ - // than ZonedDateTime.of(LocalDateTime, ZoneOffset.MIN). - Arbitrary(Gen.choose(ZonedDateTime.of(LocalDateTime.MIN, ZoneOffset.MAX), ZonedDateTime.of(LocalDateTime.MAX, ZoneOffset.MIN))) - - implicit final lazy val cogenZonedDateTime: Cogen[ZonedDateTime] = - Cogen[(LocalDateTime, ZoneOffset)].contramap(value => (value.toLocalDateTime, value.getOffset)) - - // DayOfWeek - - implicit final lazy val cogenDayOfWeek: Cogen[DayOfWeek] = - Cogen[Int].contramap(_.ordinal) - - // temporal.ChronoField - - implicit final lazy val cogenChronoField: Cogen[ChronoField] = - Cogen[Int].contramap(_.ordinal) } diff --git a/jvm/src/main/scala/org/scalacheck/time/JavaTimeCogen.scala b/jvm/src/main/scala/org/scalacheck/time/JavaTimeCogen.scala new file mode 100644 index 000000000..026af5120 --- /dev/null +++ b/jvm/src/main/scala/org/scalacheck/time/JavaTimeCogen.scala @@ -0,0 +1,102 @@ +package org.scalacheck.time + +import org.scalacheck._ +import java.time._ +import java.time.temporal._ + +/** [[Cogen]] instances for `java.time` types. */ +private[scalacheck] trait JavaTimeCogen { + + // ChronoUnit + + implicit final lazy val cogenChronoUnit: Cogen[ChronoUnit] = + Cogen[Int].contramap(_.ordinal) + + // Duration + + implicit final lazy val cogenJavaDuration: Cogen[Duration] = + Cogen[(Long, Int)].contramap(value => (value.getSeconds, value.getNano)) + + // Instant + + implicit final lazy val cogenInstant: Cogen[Instant] = + Cogen[(Long, Int)].contramap(value => (value.getEpochSecond, value.getNano)) + + // Month + + implicit final lazy val cogenMonth: Cogen[Month] = + Cogen[Int].contramap(_.ordinal) + + // Year + + implicit final lazy val cogenYear: Cogen[Year] = + Cogen[Int].contramap(_.getValue) + + // LocalDate + + implicit final lazy val cogenLocalDate: Cogen[LocalDate] = + Cogen[(Int, Int, Int)].contramap(value => (value.getYear, value.getMonthValue, value.getDayOfMonth)) + + // LocalTime + + implicit final lazy val cogenLocalTime: Cogen[LocalTime] = + Cogen[Long].contramap(_.toNanoOfDay) + + // LocalDateTime + + implicit final lazy val cogenLocalDateTime: Cogen[LocalDateTime] = + Cogen[(LocalDate, LocalTime)].contramap(value => (value.toLocalDate, value.toLocalTime)) + + // MonthDay + + implicit final lazy val cogenMonthDay: Cogen[MonthDay] = + Cogen[(Month, Int)].contramap(value => (value.getMonth, value.getDayOfMonth)) + + // ZoneOffset + + implicit final lazy val cogenZoneOffset: Cogen[ZoneOffset] = + Cogen[Int].contramap(_.getTotalSeconds) + + // ZoneId + + implicit final lazy val cogenZoneId: Cogen[ZoneId] = + Cogen[String].contramap(_.toString) // This may seem contrived, and in a + // way it is, but ZoneId values + // _without_ offsets are basically + // just newtypes of String. + + // OffsetTime + + implicit final lazy val cogenOffsetTime: Cogen[OffsetTime] = + Cogen[(LocalTime, ZoneOffset)].contramap(value => (value.toLocalTime, value.getOffset)) + + // OffsetDateTime + + implicit final lazy val cogenOffsetDateTime: Cogen[OffsetDateTime] = + Cogen[(LocalDateTime, ZoneOffset)].contramap(value => (value.toLocalDateTime, value.getOffset)) + + // Period + + implicit final lazy val cogenPeriod: Cogen[Period] = + Cogen[(Int, Int, Int)].contramap(value => (value.getYears, value.getMonths, value.getDays)) + + // YearMonth + + implicit final lazy val cogenYearMonth: Cogen[YearMonth] = + Cogen[(Int, Month)].contramap(value => (value.getYear, value.getMonth)) + + // ZonedDateTime + + implicit final lazy val cogenZonedDateTime: Cogen[ZonedDateTime] = + Cogen[(LocalDateTime, ZoneOffset)].contramap(value => (value.toLocalDateTime, value.getOffset)) + + // DayOfWeek + + implicit final lazy val cogenDayOfWeek: Cogen[DayOfWeek] = + Cogen[Int].contramap(_.ordinal) + + // temporal.ChronoField + + implicit final lazy val cogenChronoField: Cogen[ChronoField] = + Cogen[Int].contramap(_.ordinal) +} diff --git a/jvm/src/main/scala/org/scalacheck/time/JavaTimeShrink.scala b/jvm/src/main/scala/org/scalacheck/time/JavaTimeShrink.scala new file mode 100644 index 000000000..610deba21 --- /dev/null +++ b/jvm/src/main/scala/org/scalacheck/time/JavaTimeShrink.scala @@ -0,0 +1,30 @@ +package org.scalacheck.time + +import org.scalacheck._ +import java.time._ + +/** [[Shrink]] instances for `java.time` types. */ +private[scalacheck] trait JavaTimeShrink { + + // Duration + + implicit final lazy val shrinkJavaDuration: Shrink[Duration] = + Shrink[Duration]{value => + val q: Duration = value.dividedBy(2) + if (q == Duration.ZERO) { + Stream(Duration.ZERO) + } else { + q #:: q.negated #:: shrinkJavaDuration.shrink(q) + } + } + + // Period + + implicit final lazy val shrinkPeriod: Shrink[Period] = + Shrink.xmap[(Int, Int, Int), Period]( + { + case (y, m, d) => Period.of(y, m, d) + }, + value => (value.getYears, value.getMonths, value.getDays) + ) +} diff --git a/jvm/src/main/scala/org/scalacheck/time/package.scala b/jvm/src/main/scala/org/scalacheck/time/package.scala deleted file mode 100644 index 51c187cd2..000000000 --- a/jvm/src/main/scala/org/scalacheck/time/package.scala +++ /dev/null @@ -1,3 +0,0 @@ -package org.scalacheck - -package object time extends time.JavaTimeInstances diff --git a/native/src/main/scala/org/scalacheck/time/JavaTimeArbitrary.scala b/native/src/main/scala/org/scalacheck/time/JavaTimeArbitrary.scala new file mode 100644 index 000000000..3fc80f03b --- /dev/null +++ b/native/src/main/scala/org/scalacheck/time/JavaTimeArbitrary.scala @@ -0,0 +1,4 @@ +package org.scalacheck.time + +/** Stub trait since Scala Native does not have native support for java.time types. */ +private[scalacheck] trait JavaTimeArbitrary diff --git a/native/src/main/scala/org/scalacheck/time/JavaTimeChoose.scala b/native/src/main/scala/org/scalacheck/time/JavaTimeChoose.scala new file mode 100644 index 000000000..0aa01c623 --- /dev/null +++ b/native/src/main/scala/org/scalacheck/time/JavaTimeChoose.scala @@ -0,0 +1,4 @@ +package org.scalacheck.time + +/** Stub trait since Scala Native does not have native support for java.time types. */ +private[scalacheck] trait JavaTimeChoose diff --git a/native/src/main/scala/org/scalacheck/time/JavaTimeCogen.scala b/native/src/main/scala/org/scalacheck/time/JavaTimeCogen.scala new file mode 100644 index 000000000..ddeb71f29 --- /dev/null +++ b/native/src/main/scala/org/scalacheck/time/JavaTimeCogen.scala @@ -0,0 +1,4 @@ +package org.scalacheck.time + +/** Stub trait since Scala Native does not have native support for java.time types. */ +private[scalacheck] trait JavaTimeCogen diff --git a/native/src/main/scala/org/scalacheck/time/JavaTimeShrink.scala b/native/src/main/scala/org/scalacheck/time/JavaTimeShrink.scala new file mode 100644 index 000000000..d28ca4323 --- /dev/null +++ b/native/src/main/scala/org/scalacheck/time/JavaTimeShrink.scala @@ -0,0 +1,4 @@ +package org.scalacheck.time + +/** Stub trait since Scala Native does not have native support for java.time types. */ +private[scalacheck] trait JavaTimeShrink diff --git a/src/main/scala/org/scalacheck/Arbitrary.scala b/src/main/scala/org/scalacheck/Arbitrary.scala index 5ccbd2828..0b2109077 100644 --- a/src/main/scala/org/scalacheck/Arbitrary.scala +++ b/src/main/scala/org/scalacheck/Arbitrary.scala @@ -59,7 +59,7 @@ sealed abstract class Arbitrary[T] extends Serializable { * generators. *

*/ -object Arbitrary extends ArbitraryLowPriority with ArbitraryArities { +object Arbitrary extends ArbitraryLowPriority with ArbitraryArities with time.JavaTimeArbitrary { /** Arbitrary instance of the Function0 type. */ implicit def arbFunction0[T](implicit a: Arbitrary[T]): Arbitrary[() => T] = diff --git a/src/main/scala/org/scalacheck/Cogen.scala b/src/main/scala/org/scalacheck/Cogen.scala index 28a4e6819..6d0c4ba7b 100644 --- a/src/main/scala/org/scalacheck/Cogen.scala +++ b/src/main/scala/org/scalacheck/Cogen.scala @@ -28,7 +28,7 @@ sealed trait Cogen[T] extends Serializable { Cogen((seed: Seed, s: S) => perturb(seed, f(s))) } -object Cogen extends CogenArities with CogenLowPriority with CogenVersionSpecific { +object Cogen extends CogenArities with CogenLowPriority with CogenVersionSpecific with time.JavaTimeCogen { def apply[T](implicit ev: Cogen[T]): Cogen[T] = ev diff --git a/src/main/scala/org/scalacheck/Gen.scala b/src/main/scala/org/scalacheck/Gen.scala index d10b34aac..74fc2b53b 100644 --- a/src/main/scala/org/scalacheck/Gen.scala +++ b/src/main/scala/org/scalacheck/Gen.scala @@ -347,7 +347,7 @@ object Gen extends GenArities with GenVersionSpecific { } /** Provides implicit [[org.scalacheck.Gen.Choose]] instances */ - object Choose { + object Choose extends time.JavaTimeChoose { class IllegalBoundsError[A](low: A, high: A) extends IllegalArgumentException(s"invalid bounds: low=$low, high=$high") diff --git a/src/main/scala/org/scalacheck/Shrink.scala b/src/main/scala/org/scalacheck/Shrink.scala index 4b84b2591..0c0c4f354 100644 --- a/src/main/scala/org/scalacheck/Shrink.scala +++ b/src/main/scala/org/scalacheck/Shrink.scala @@ -29,7 +29,7 @@ trait ShrinkLowPriority { implicit def shrinkAny[T]: Shrink[T] = Shrink(_ => Stream.empty) } -object Shrink extends ShrinkLowPriority with ShrinkVersionSpecific { +object Shrink extends ShrinkLowPriority with ShrinkVersionSpecific with time.JavaTimeShrink { import Stream.{cons, empty} import scala.collection._