tmccarthy / intime

Scala integration for the java.time classes

GitHub

intime

CircleCI Maven Central

Libraries for integration between the java.time classes and common Scala libraries.

Add the following to your build.sbt file:

val intimeVersion = "1.0.0"

libraryDependencies += "au.id.tmm.intime" %% "intime-core"       % intimeVersion
libraryDependencies += "au.id.tmm.intime" %% "intime-cats"       % intimeVersion          // Cats integration
libraryDependencies += "au.id.tmm.intime" %% "intime-argonaut"   % intimeVersion          // Argonaut integration
libraryDependencies += "au.id.tmm.intime" %% "intime-scalacheck" % intimeVersion % "test" // Scalacheck integration


intime-core

intime-core adds integrations with the Scala standard library. Add it to your project with:

libraryDependencies += "au.id.tmm.intime" %% "intime-core" % "1.0.0"

Ordering instances for ordered classes

intime-core provides Ordering instances for all classes in the java.time package for which an ordering can be defined. This includes the most common classes like Instant and LocalDate.

import java.time._
import au.id.tmm.intime._

val dates: List[LocalDate] = List(
  LocalDate.of(2003, 3, 23),
  LocalDate.of(2015, 3, 29),
  LocalDate.of(2007, 4, 28),
)

dates.sorted // Sorts list of dates

PartialOrdering instances for Period

Period presents some problems when it comes to defining an Ordering instance, as any two instances cannot necessarily be compared (is 1 month longer or shorter than 30 days?). intime-core provides a PartialOrdering for each, handling those cases where an ordering can be computed.

Overloaded operators

intime-core provides overloaded operators for arithmetic and comparison operations on java.time classes:

import java.time._
import au.id.tmm.intime._

LocalDate.of(1999, 6, 20) + Period.ofDays(3) // 1999-06-23
Instant.EPOCH - Duration.ofSeconds(5)        // 1969-12-31T23:59:55Z
Period.ofDays(5) * 3                         // P15D
- Duration.ofHours(42)                       // PT-42H
Duration.ofDays(30) / 10                     // P3D
Instant.MAX > Instant.EPOCH                  // true 


intime-cats

intime-cats adds integrations with Cats. Add it to your project with:

libraryDependencies += "au.id.tmm.intime" %% "intime-cats" % "1.0.0"

All instances are tested with discipline.

Hash and Show instances

intime-cats provides Hash and Show instances for all classes in java.time.

import java.time._
import au.id.tmm.intime.cats._

import cats.syntax.show._
import cats.syntax.eq._

LocalDate.of(1999, 6, 20).show  // 1999-06-20
Instant.EPOCH === Instant.EPOCH // true

Order and PartialOrder instances

intime-cats uses the orderings in intime-core to define Cats Order instances (PartialOrder for Period).

import java.time._
import au.id.tmm.intime.cats._

import cats.syntax.partialOrder._

LocalDate.of(2003, 3, 23) < LocalDate.of(2015, 3, 29) // true

Period.ofYears(1) < Period.ofMonths(13)               // true
Period.ofDays(30) partialCompare Period.ofMonths(1)   // NaN

CommutativeGroup instances for Period and Duration

import java.time._
import au.id.tmm.intime.cats._

import cats.syntax.group._

Duration.ofDays(1) |+| Duration.ofHours(2) // P1DT2H
Duration.ofDays(1) |-| Duration.ofHours(2) // PT22H


intime-scalacheck

intime-scalacheck adds integrations with Scalacheck. Add it to your project with:

libraryDependencies += "au.id.tmm.intime" %% "intime-scalacheck" % "1.0.0" % "test"

Arbitrary instances

intime-scalacheck provides instances of Arbitrary for all classes in java.time. These can be used to generate arbitrary instances for property-based testing.

import java.time._
import au.id.tmm.intime.scalacheck._

import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks._

forAll { localDate: LocalDate =>
  assert(localDate.plusDays(1) isAfter localDate)
}

Generators for "sensible" datetime values

intime-scalacheck provides generators for "sensible" values of java.time classes. The generated values are all between 1900 and 2100, allowing property-based-tests that don't have to worry about peculiarities like year zero or durations that overflow.

import java.time._
import au.id.tmm.intime.scalacheck._

import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks._

forAll(genSensibleLocalDate) { localDate: LocalDate =>
  assert(localDate.getYear >= 1900)
}

Choose instances

intime-scalacheck provides instances of Choose, which let you define your own generators that produce values within a range.

import java.time._
import au.id.tmm.intime.scalacheck._

import org.scalacheck.Gen
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks._

val rangeGenerator: Gen[LocalDate] = Gen.choose(
  min = LocalDate.of(2019, 5, 30),
  max = LocalDate.of(2019, 7, 14),
)

forAll(rangeGenerator) { localDate: LocalDate =>
  assert(localDate.getYear == 2019)
}

Shrink instances

intime-scalacheck provides instances for Shrink, which Scalacheck will use to try to identify the smallest value for which a test fails. These are used automatically as long as you have the import:

import au.id.tmm.intime.scalacheck._


intime-argonaut

intime-argonaut provides integration with the Argonaut library for JSON handling. Add it to your project with:

libraryDependencies += "au.id.tmm.intime" %% "intime-argonaut" % "1.0.0"

Standard encoders and decoders

intime-argonaut defines EncodeJson and DecodeJson instances for all classes in the java.time package. They are encoded and decoded to JSON strings according to the most obvious format (see StandardCodecs.

import java.time._
import au.id.tmm.intime.argonaut._

import argonaut.Argonaut._

LocalDate.of(2019, 7, 14).asJson    // jString("2019-07-14")
jString("2019-07-14").as[LocalDate] // DecodeResult.ok(LocalDate.of(2019, 7, 14))

Custom encoders and decoders

intime-argonaut allows for the definition of custom EncodeJson and DecodeJson instances using instances of DateTimeFormatter.

import java.time._
import au.id.tmm.intime.argonaut._

import argonaut.Argonaut._

val formatter = DateTimeFormatter.ofPattern("MM-dd-uuuu")
implicit val customCodec = DateTimeFormatterCodecs.localDateCodecFrom(formatter)

LocalDate.of(2019, 7, 14).asJson    // jString("07-14-2019")
jString("07-14-2019").as[LocalDate] // DecodeResult.ok(LocalDate.of(2019, 7, 14))

Known issues

  • In Java 8, the standard codec for ZonedDateTime will fail to decode when the zone is GMT. This is fixed in Java 11.
  • In Java 8, the standard codec for Duration will drop the negative sign for durations between 0 and -1 seconds. This is fixed in Java 11.