Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Granularity #7

Merged
merged 6 commits into from
Jul 21, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ object GenDateTime {
* @param period An offset from <code>dateTime</code>, serving as an upper bound for generated <code>DateTime</code>s. Can be negative, denoting an offset <i>before</i> the provided <code>DateTime</code>.
* @return A <code>DateTime</code> generator for <code>DateTime</code>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)))
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ package com.fortysevendeg.scalacheck.datetime

package object instances {
object joda extends JodaInstances
object j8 extends J8Instances
object jdk8 extends Jdk8Instances
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.fortysevendeg.scalacheck.datetime.j8
package com.fortysevendeg.scalacheck.datetime.jdk8

import collection.JavaConverters._

Expand All @@ -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)
Expand All @@ -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))
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.fortysevendeg.scalacheck.datetime.joda

import com.fortysevendeg.scalacheck.datetime.Granularity

import org.scalacheck.Gen
import org.joda.time._

Expand All @@ -8,7 +10,7 @@ import org.joda.time._
*/
object GenJoda {

/** A <code>Years</code> period generator. */
/** A <code>Years</code> period generator. */
val genYearsPeriod: Gen[Years] = Gen.choose(-292275054, 292278993).map(Years.ZERO.plus(_)) // Years.MIN_VALUE produces exception-throwing results

/** A <code>Months</code> period generator. */
Expand All @@ -29,9 +31,7 @@ object GenJoda {
/** A <code>Seconds</code> period generator. */
val genSecondsPeriod: Gen[Seconds] = Gen.choose(Seconds.MIN_VALUE.getSeconds, Seconds.MAX_VALUE.getSeconds).map(Seconds.ZERO.plus(_))

/**
* A <code>Period</code> generator consisting of years, days, hours, minutes, seconds and millis.
*/
/** A <code>Period</code> generator consisting of years, days, hours, minutes, seconds and millis. */
val genPeriod: Gen[Period] = for {
years <- genYearsPeriod
days <- Gen.choose(1, 365)
Expand All @@ -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 <code>DateTime</code> 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))
}
Original file line number Diff line number Diff line change
@@ -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"
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
Loading