diff --git a/src/main/scala/com/fortysevendeg/scalacheck/datetime/GenDateTime.scala b/src/main/scala/com/fortysevendeg/scalacheck/datetime/GenDateTime.scala
index 10a4deff..f16ae7bc 100644
--- a/src/main/scala/com/fortysevendeg/scalacheck/datetime/GenDateTime.scala
+++ b/src/main/scala/com/fortysevendeg/scalacheck/datetime/GenDateTime.scala
@@ -16,8 +16,8 @@ object GenDateTime {
* @param period An offset from dateTime
, serving as an upper bound for generated DateTime
s. Can be negative, denoting an offset before the provided DateTime
.
* @return A DateTime
generator for DateTime
s within the expected range.
*/
- def genDateTimeWithinRange[D, R](dateTime: D, range: R)(implicit scDateTime: ScalaCheckDateTimeInfra[D, R]): Gen[D] = {
+ def genDateTimeWithinRange[D, R](dateTime: D, range: R)(implicit scDateTime: ScalaCheckDateTimeInfra[D, R], granularity: Granularity[D]): Gen[D] = {
val diffMillis = scDateTime.getMillis(scDateTime.addRange(dateTime, range)) - scDateTime.getMillis(dateTime)
- Gen.choose(0L min diffMillis, 0L max diffMillis).map(millis => scDateTime.addMillis(dateTime, millis))
+ Gen.choose(0L min diffMillis, 0L max diffMillis).map(millis => granularity.normalize(scDateTime.addMillis(dateTime, millis)))
}
}
diff --git a/src/main/scala/com/fortysevendeg/scalacheck/datetime/Granularity.scala b/src/main/scala/com/fortysevendeg/scalacheck/datetime/Granularity.scala
new file mode 100644
index 00000000..4c9b0942
--- /dev/null
+++ b/src/main/scala/com/fortysevendeg/scalacheck/datetime/Granularity.scala
@@ -0,0 +1,13 @@
+package com.fortysevendeg.scalacheck.datetime
+
+trait Granularity[A] {
+ val normalize: A => A
+ val description: String
+}
+
+object Granularity {
+ implicit def identity[A]: Granularity[A] = new Granularity[A] {
+ val normalize = (a: A) => a
+ val description = "None"
+ }
+}
diff --git a/src/main/scala/com/fortysevendeg/scalacheck/datetime/instances/j8.scala b/src/main/scala/com/fortysevendeg/scalacheck/datetime/instances/jdk8.scala
similarity index 76%
rename from src/main/scala/com/fortysevendeg/scalacheck/datetime/instances/j8.scala
rename to src/main/scala/com/fortysevendeg/scalacheck/datetime/instances/jdk8.scala
index 37b08604..7f7ed39d 100644
--- a/src/main/scala/com/fortysevendeg/scalacheck/datetime/instances/j8.scala
+++ b/src/main/scala/com/fortysevendeg/scalacheck/datetime/instances/jdk8.scala
@@ -4,9 +4,9 @@ import com.fortysevendeg.scalacheck.datetime.typeclasses._
import java.time._
import java.time.temporal.ChronoUnit.MILLIS
-trait J8Instances {
+trait Jdk8Instances {
- implicit val j8ForDuration: ScalaCheckDateTimeInfra[ZonedDateTime, Duration] = new ScalaCheckDateTimeInfra[ZonedDateTime, Duration] {
+ implicit val jdk8ForDuration: ScalaCheckDateTimeInfra[ZonedDateTime, Duration] = new ScalaCheckDateTimeInfra[ZonedDateTime, Duration] {
def addRange(zonedDateTime: ZonedDateTime, duration: Duration): ZonedDateTime = zonedDateTime.plus(duration)
def addMillis(zonedDateTime: ZonedDateTime, millis: Long): ZonedDateTime = zonedDateTime.plus(millis, MILLIS)
def getMillis(zonedDateTime: ZonedDateTime): Long = zonedDateTime.toInstant.toEpochMilli
diff --git a/src/main/scala/com/fortysevendeg/scalacheck/datetime/instances/package.scala b/src/main/scala/com/fortysevendeg/scalacheck/datetime/instances/package.scala
index 7795bf6f..244e9f9a 100644
--- a/src/main/scala/com/fortysevendeg/scalacheck/datetime/instances/package.scala
+++ b/src/main/scala/com/fortysevendeg/scalacheck/datetime/instances/package.scala
@@ -2,5 +2,5 @@ package com.fortysevendeg.scalacheck.datetime
package object instances {
object joda extends JodaInstances
- object j8 extends J8Instances
+ object jdk8 extends Jdk8Instances
}
diff --git a/src/main/scala/com/fortysevendeg/scalacheck/datetime/j8/GenJ8.scala b/src/main/scala/com/fortysevendeg/scalacheck/datetime/jdk8/GenJdk8.scala
similarity index 65%
rename from src/main/scala/com/fortysevendeg/scalacheck/datetime/j8/GenJ8.scala
rename to src/main/scala/com/fortysevendeg/scalacheck/datetime/jdk8/GenJdk8.scala
index 54919a4a..5f9e8182 100644
--- a/src/main/scala/com/fortysevendeg/scalacheck/datetime/j8/GenJ8.scala
+++ b/src/main/scala/com/fortysevendeg/scalacheck/datetime/jdk8/GenJdk8.scala
@@ -1,4 +1,4 @@
-package com.fortysevendeg.scalacheck.datetime.j8
+package com.fortysevendeg.scalacheck.datetime.jdk8
import collection.JavaConverters._
@@ -8,9 +8,11 @@ import org.scalacheck.Arbitrary.arbitrary
import java.time._
import java.time.temporal.ChronoUnit.MILLIS
-object GenJ8 {
+import com.fortysevendeg.scalacheck.datetime.Granularity
- val genZonedDateTime: Gen[ZonedDateTime] = for {
+object GenJdk8 {
+
+ def genZonedDateTime(implicit granularity: Granularity[ZonedDateTime]): Gen[ZonedDateTime] = for {
year <- Gen.choose(-292278994, 292278994)
month <- Gen.choose(1, 12)
maxDaysInMonth = Month.of(month).length(Year.of(year).isLeap)
@@ -20,7 +22,7 @@ object GenJ8 {
second <- Gen.choose(0, 59)
nanoOfSecond <- Gen.choose(0, 999999999)
zoneId <- Gen.oneOf(ZoneId.getAvailableZoneIds.asScala.toList)
- } yield ZonedDateTime.of(year, month, dayOfMonth, hour, minute, second, nanoOfSecond, ZoneId.of(zoneId))
+ } yield granularity.normalize(ZonedDateTime.of(year, month, dayOfMonth, hour, minute, second, nanoOfSecond, ZoneId.of(zoneId)))
val genDuration: Gen[Duration] = Gen.choose(Long.MinValue, Long.MaxValue / 1000).map(l => Duration.of(l, MILLIS))
}
diff --git a/src/main/scala/com/fortysevendeg/scalacheck/datetime/jdk8/granularity/package.scala b/src/main/scala/com/fortysevendeg/scalacheck/datetime/jdk8/granularity/package.scala
new file mode 100644
index 00000000..ea3fcc60
--- /dev/null
+++ b/src/main/scala/com/fortysevendeg/scalacheck/datetime/jdk8/granularity/package.scala
@@ -0,0 +1,32 @@
+package com.fortysevendeg.scalacheck.datetime.jdk8
+
+import com.fortysevendeg.scalacheck.datetime.Granularity
+import java.time.ZonedDateTime
+
+package object granularity {
+
+ implicit val seconds: Granularity[ZonedDateTime] = new Granularity[ZonedDateTime] {
+ val normalize = (dt: ZonedDateTime) => dt.withNano(0)
+ val description = "Seconds"
+ }
+
+ implicit val minutes: Granularity[ZonedDateTime] = new Granularity[ZonedDateTime] {
+ val normalize = (dt: ZonedDateTime) => dt.withNano(0).withSecond(0)
+ val description = "Minutes"
+ }
+
+ implicit val hours: Granularity[ZonedDateTime] = new Granularity[ZonedDateTime] {
+ val normalize = (dt: ZonedDateTime) => dt.withNano(0).withSecond(0).withMinute(0)
+ val description = "Hours"
+ }
+
+ implicit val days: Granularity[ZonedDateTime] = new Granularity[ZonedDateTime] {
+ val normalize = (dt: ZonedDateTime) => dt.withNano(0).withSecond(0).withMinute(0).withHour(0)
+ val description = "Days"
+ }
+
+ implicit val years: Granularity[ZonedDateTime] = new Granularity[ZonedDateTime] {
+ val normalize = (dt: ZonedDateTime) => dt.withNano(0).withSecond(0).withMinute(0).withHour(0).withDayOfYear(1)
+ val description = "Years"
+ }
+}
diff --git a/src/main/scala/com/fortysevendeg/scalacheck/datetime/joda/GenJoda.scala b/src/main/scala/com/fortysevendeg/scalacheck/datetime/joda/GenJoda.scala
index bf408ff6..d86c4e45 100644
--- a/src/main/scala/com/fortysevendeg/scalacheck/datetime/joda/GenJoda.scala
+++ b/src/main/scala/com/fortysevendeg/scalacheck/datetime/joda/GenJoda.scala
@@ -1,5 +1,7 @@
package com.fortysevendeg.scalacheck.datetime.joda
+import com.fortysevendeg.scalacheck.datetime.Granularity
+
import org.scalacheck.Gen
import org.joda.time._
@@ -8,7 +10,7 @@ import org.joda.time._
*/
object GenJoda {
- /** A Years
period generator. */
+ /** A Years
period generator. */
val genYearsPeriod: Gen[Years] = Gen.choose(-292275054, 292278993).map(Years.ZERO.plus(_)) // Years.MIN_VALUE produces exception-throwing results
/** A Months
period generator. */
@@ -29,9 +31,7 @@ object GenJoda {
/** A Seconds
period generator. */
val genSecondsPeriod: Gen[Seconds] = Gen.choose(Seconds.MIN_VALUE.getSeconds, Seconds.MAX_VALUE.getSeconds).map(Seconds.ZERO.plus(_))
- /**
- * A Period
generator consisting of years, days, hours, minutes, seconds and millis.
- */
+ /** A Period
generator consisting of years, days, hours, minutes, seconds and millis. */
val genPeriod: Gen[Period] = for {
years <- genYearsPeriod
days <- Gen.choose(1, 365)
@@ -41,4 +41,15 @@ object GenJoda {
millis <- Gen.choose(0, 999)
} yield Period.years(years.getYears).withDays(days).withHours(hours).withMinutes(minutes).withSeconds(seconds).withMillis(millis)
+ /** A DateTime
generator. */
+ def genDateTime(implicit granularity: Granularity[DateTime]): Gen[DateTime] = for {
+ year <- Gen.choose(-292275055,292278994)
+ month <- Gen.choose(1, 12)
+ yearAndMonthDt = new DateTime(year, month, 1, 0, 0)
+ dayOfMonth <- Gen.choose(1, yearAndMonthDt.dayOfMonth.getMaximumValue)
+ hourOfDay <- Gen.choose(0, 23)
+ minuteOfHour <- Gen.choose(0, 59)
+ secondOfMinute <- Gen.choose(0, 59)
+ millisOfSecond <- Gen.choose(0, 999)
+ } yield granularity.normalize(new DateTime(year, month, dayOfMonth, hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond))
}
diff --git a/src/main/scala/com/fortysevendeg/scalacheck/datetime/joda/granularity/package.scala b/src/main/scala/com/fortysevendeg/scalacheck/datetime/joda/granularity/package.scala
new file mode 100644
index 00000000..78ca6628
--- /dev/null
+++ b/src/main/scala/com/fortysevendeg/scalacheck/datetime/joda/granularity/package.scala
@@ -0,0 +1,32 @@
+package com.fortysevendeg.scalacheck.datetime.joda
+
+import com.fortysevendeg.scalacheck.datetime.Granularity
+import org.joda.time.DateTime
+
+package object granularity {
+
+ implicit val seconds: Granularity[DateTime] = new Granularity[DateTime] {
+ val normalize = (dt: DateTime) => dt.withMillisOfSecond(0)
+ val description = "Seconds"
+ }
+
+ implicit val minutes: Granularity[DateTime] = new Granularity[DateTime] {
+ val normalize = (dt: DateTime) => dt.withMillisOfSecond(0).withSecondOfMinute(0)
+ val description = "Minutes"
+ }
+
+ implicit val hours: Granularity[DateTime] = new Granularity[DateTime] {
+ val normalize = (dt: DateTime) => dt.withMillisOfSecond(0).withSecondOfMinute(0).withMinuteOfHour(0)
+ val description = "Hours"
+ }
+
+ implicit val days: Granularity[DateTime] = new Granularity[DateTime] {
+ val normalize = (dt: DateTime) => dt.withMillisOfSecond(0).withSecondOfMinute(0).withMinuteOfHour(0).withHourOfDay(0)
+ val description = "Days"
+ }
+
+ implicit val years: Granularity[DateTime] = new Granularity[DateTime] {
+ val normalize = (dt: DateTime) => dt.withMillisOfSecond(0).withSecondOfMinute(0).withMinuteOfHour(0).withHourOfDay(0).withDayOfYear(1)
+ val description = "Years"
+ }
+}
diff --git a/src/test/scala/com/fortysevendeg/scalacheck/datetime/j8/GenJ8Properties.scala b/src/test/scala/com/fortysevendeg/scalacheck/datetime/j8/GenJ8Properties.scala
deleted file mode 100644
index 617fd6ee..00000000
--- a/src/test/scala/com/fortysevendeg/scalacheck/datetime/j8/GenJ8Properties.scala
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.fortysevendeg.scalacheck.datetime.j8
-
-import scala.util.Try
-
-import org.scalacheck._
-import org.scalacheck.Prop._
-
-import java.time._
-
-import com.fortysevendeg.scalacheck.datetime.GenDateTime._
-import com.fortysevendeg.scalacheck.datetime.instances.j8._
-
-import GenJ8._
-
-object GenJ8Properties extends Properties("Java 8 Generators") {
-
- property("genDuration creates valid durations") = forAll(genDuration) { _ => passed }
-
- property("genZonedDateTime created valid times") = forAll(genZonedDateTime) { _ => passed }
-
- // Guards against adding a duration to a datetime which cannot represent millis in a long, causing an exception.
- private[this] def tooLargeForAddingRanges(dateTime: ZonedDateTime, d: Duration): Boolean = {
- Try(dateTime.plus(d).toInstant().toEpochMilli()).isFailure
- }
-
- property("genDuration can be added to any date") = forAll(genZonedDateTime, genDuration) { (dt, dur) =>
- !tooLargeForAddingRanges(dt, dur) ==> {
- val attempted = Try(dt.plus(dur).toInstant().toEpochMilli())
- attempted.isSuccess :| attempted.toString
- }
- }
-
- property("genDateTimeWithinRange for Java 8 should generate ZonedDateTimes between the given date and the end of the specified Duration") = forAll(genZonedDateTime, genDuration) { (now, d) =>
- !tooLargeForAddingRanges(now, d) ==> {
- forAll(genDateTimeWithinRange(now, d)) { generated =>
- val durationBoundary = now.plus(d)
-
- val resultText = s"""Duration: $d
- |Now: $now
- |Generated: $generated
- |Period Boundary: $durationBoundary""".stripMargin
-
- val (lowerBound, upperBound) = if(durationBoundary.isAfter(now)) (now, durationBoundary) else (durationBoundary, now)
-
- val check = (lowerBound.isBefore(generated) || lowerBound.isEqual(generated)) &&
- (upperBound.isAfter(generated) || upperBound.isEqual(generated))
-
- check :| resultText
- }
- }
- }
-}
diff --git a/src/test/scala/com/fortysevendeg/scalacheck/datetime/jdk8/GenJdk8Properties.scala b/src/test/scala/com/fortysevendeg/scalacheck/datetime/jdk8/GenJdk8Properties.scala
new file mode 100644
index 00000000..3ce418b2
--- /dev/null
+++ b/src/test/scala/com/fortysevendeg/scalacheck/datetime/jdk8/GenJdk8Properties.scala
@@ -0,0 +1,93 @@
+package com.fortysevendeg.scalacheck.datetime.jdk8
+
+import scala.util.Try
+
+import org.scalacheck._
+import org.scalacheck.Prop._
+
+import java.time._
+
+import com.fortysevendeg.scalacheck.datetime.GenDateTime._
+import com.fortysevendeg.scalacheck.datetime.instances.jdk8._
+
+import GenJdk8._
+
+import com.fortysevendeg.scalacheck.datetime.Granularity
+
+object GenJdk8Properties extends Properties("Java 8 Generators") {
+
+ property("genDuration creates valid durations") = forAll(genDuration) { _ => passed }
+
+ property("genZonedDateTime created valid times (with no granularity)") = forAll(genZonedDateTime) { _ => passed }
+
+ val granularitiesAndPredicates: List[(Granularity[ZonedDateTime], ZonedDateTime => Boolean)] = {
+
+ import java.time.temporal.ChronoField._
+
+ def zeroNanos(dt: ZonedDateTime) = dt.get(NANO_OF_SECOND) == 0
+ def zeroSeconds(dt: ZonedDateTime) = zeroNanos(dt) && dt.get(SECOND_OF_MINUTE) == 0
+ def zeroMinutes(dt: ZonedDateTime) = zeroSeconds(dt) && dt.get(MINUTE_OF_HOUR) == 0
+ def zeroHours(dt: ZonedDateTime) = zeroMinutes(dt) && dt.get(HOUR_OF_DAY) == 0
+ def firstDay(dt: ZonedDateTime) = zeroHours(dt) && dt.get(DAY_OF_YEAR) == 1
+
+ List(
+ (granularity.seconds, zeroNanos _),
+ (granularity.minutes, zeroSeconds _),
+ (granularity.hours, zeroMinutes _),
+ (granularity.days, zeroHours _),
+ (granularity.years, firstDay _)
+ )
+ }
+
+ val granularitiesAndPredicatesWithDefault: List[(Granularity[ZonedDateTime], ZonedDateTime => Boolean)] = (Granularity.identity[ZonedDateTime], (_: ZonedDateTime) => true) :: granularitiesAndPredicates
+
+ property("genZonedDateTime with a granularity generates appropriate ZonedDateTimes") = forAll(Gen.oneOf(granularitiesAndPredicates)) { case (granularity, predicate) =>
+
+ implicit val generatedGranularity = granularity
+
+ forAll(genZonedDateTime) { dt =>
+ predicate(dt) :| s"${granularity.description}: $dt"
+ }
+ }
+
+ // Guards against adding a duration to a datetime which cannot represent millis in a long, causing an exception.
+ private[this] def tooLargeForAddingRanges(dateTime: ZonedDateTime, d: Duration): Boolean = {
+ Try(dateTime.plus(d).toInstant().toEpochMilli()).isFailure
+ }
+
+ property("genDuration can be added to any date") = forAll(genZonedDateTime, genDuration) { (dt, dur) =>
+ !tooLargeForAddingRanges(dt, dur) ==> {
+ val attempted = Try(dt.plus(dur).toInstant().toEpochMilli())
+ attempted.isSuccess :| attempted.toString
+ }
+ }
+
+ property("genDateTimeWithinRange for Java 8 should generate ZonedDateTimes between the given date and the end of the specified Duration") =
+ forAll(genZonedDateTime, genDuration, Gen.oneOf(granularitiesAndPredicatesWithDefault)) { case (now, d, (granularity, predicate)) =>
+ !tooLargeForAddingRanges(now, d) ==> {
+
+ implicit val generatedGranularity = granularity
+
+ forAll(genDateTimeWithinRange(now, d)) { generated =>
+ val durationBoundary = now.plus(d)
+
+ val resultText = s"""Duration: $d
+ |Now: $now
+ |Generated: $generated
+ |Period Boundary: $durationBoundary
+ |Granularity ${granularity.description}""".stripMargin
+
+ val (lowerBound, upperBound) = if(durationBoundary.isAfter(now)) (now, durationBoundary) else (durationBoundary, now)
+
+ val rangeCheck = (lowerBound.isBefore(generated) || lowerBound.isEqual(generated)) &&
+ (upperBound.isAfter(generated) || upperBound.isEqual(generated))
+
+ val granularityCheck = predicate(generated)
+
+ val prop = rangeCheck && granularityCheck
+
+ prop :| resultText
+ }
+ }
+ }
+}
diff --git a/src/test/scala/com/fortysevendeg/scalacheck/datetime/joda/GenJodaProperties.scala b/src/test/scala/com/fortysevendeg/scalacheck/datetime/joda/GenJodaProperties.scala
index b109e046..f7ab42ad 100644
--- a/src/test/scala/com/fortysevendeg/scalacheck/datetime/joda/GenJodaProperties.scala
+++ b/src/test/scala/com/fortysevendeg/scalacheck/datetime/joda/GenJodaProperties.scala
@@ -11,6 +11,8 @@ import com.fortysevendeg.scalacheck.datetime.instances.joda._
import GenJoda._
+import com.fortysevendeg.scalacheck.datetime.Granularity
+
object GenJodaProperties extends Properties("Joda Generators") {
/*
@@ -33,26 +35,63 @@ object GenJodaProperties extends Properties("Joda Generators") {
property("genPeriod creates valid periods containing a selection of other periods") = forAll(genPeriod) { _ => passed }
- property("genDateTimeWithinRange for Joda should generate DateTimes between the given date and the end of the specified Period") = forAll(genPeriod) { p =>
+ property("genDateTime creates valid DateTime instances (with no granularity)") = forAll(genDateTime) { _ => passed }
+
+ val granularitiesAndPredicates: List[(Granularity[DateTime], DateTime => Boolean)] = {
+
+ def zeroMillis(dt: DateTime) = dt.getMillisOfSecond == 0
+ def zeroSeconds(dt: DateTime) = zeroMillis(dt) && dt.getSecondOfMinute == 0
+ def zeroMinutes(dt: DateTime) = zeroSeconds(dt) && dt.getMinuteOfHour == 0
+ def zeroHours(dt: DateTime) = zeroMinutes(dt) && dt.getHourOfDay == 0
+ def firstDay(dt: DateTime) = zeroHours(dt) && dt.getDayOfYear == 1
+
+ List(
+ (granularity.seconds, zeroMillis _),
+ (granularity.minutes, zeroSeconds _),
+ (granularity.hours, zeroMinutes _),
+ (granularity.days, zeroHours _),
+ (granularity.years, firstDay _)
+ )
+ }
+
+ val granularitiesAndPredicatesWithDefault: List[(Granularity[DateTime], DateTime => Boolean)] = (Granularity.identity[DateTime], (_: DateTime) => true) :: granularitiesAndPredicates
+
+ property("genDateTime with a granularity generates appropriate DateTimes") = forAll(Gen.oneOf(granularitiesAndPredicates)) { case (granularity, predicate) =>
+ implicit val generatedGranularity = granularity
+
+ forAll(genDateTime) { dt =>
+ predicate(dt) :| s"${granularity.description}: $dt"
+ }
+ }
+
+ property("genDateTimeWithinRange for Joda should generate DateTimes between the given date and the end of the specified Period, with the relevant granularity") =
+ forAll(genPeriod, Gen.oneOf(granularitiesAndPredicatesWithDefault)) { case (period, (granularity, predicate)) =>
+
+ implicit val generatedGranularity = granularity
val now = new DateTime()
- forAll(genDateTimeWithinRange(now, p)) { generated =>
+ forAll(genDateTimeWithinRange(now, period)) { generated =>
// if period is negative, then periodBoundary will be before now
- val periodBoundary = now.plus(p)
+ val periodBoundary = now.plus(period)
- val resultText = s"""Period: ${PeriodFormat.getDefault().print(p)}
+ val resultText = s"""Period: ${PeriodFormat.getDefault().print(period)}
|Now: $now
|Generated: $generated
- |Period Boundary: $periodBoundary""".stripMargin
+ |Period Boundary: $periodBoundary
+ |Granularity: ${granularity.description}""".stripMargin
val (lowerBound, upperBound) = if(periodBoundary.isAfter(now)) (now, periodBoundary) else (periodBoundary, now)
- val check = (lowerBound.isBefore(generated) || lowerBound.isEqual(generated)) &&
- (upperBound.isAfter(generated) || upperBound.isEqual(generated))
+ val rangeCheck = (lowerBound.isBefore(generated) || lowerBound.isEqual(generated)) &&
+ (upperBound.isAfter(generated) || upperBound.isEqual(generated))
+
+ val granularityCheck = predicate(generated)
+
+ val prop = rangeCheck && granularityCheck
- check :| resultText
+ prop :| resultText
}
}
}