DTC provides type classes for local and zoned datetime values, and type class instances for both JVM and ScalaJS.
It serves 2 main purposes:
- Allows to write generic polymorphic code, that operates on datetime values.
- Gives possibility to write universal datetime logic, that compiles both for JVM and ScalaJS.
Currently, there's no truly cross-platform datetime instance,
as scala-js-java-time does not yet provide
java.time.LocalDateTimeandjava.time.ZonedDateTime.
As a bonus, you get immutable datetime values for ScalaJS that behave like java.time._ counterparts.
DTC core depends on:
- scala-js-java-time to take advantage of
java.time._API parts, that are already available for ScalaJS - small cats-kernel module for
Ordertype class.
Add this line to your build.sbt.
libraryDependencies += "ru.pavkin" %%% "dtc-core" % "2.6.1"If you want to use momentjs instances for ScalaJS runtime (see JS instances), also add dtc-moment
module dependency to your scalajs subproject as well:
libraryDependencies += "ru.pavkin" %%% "dtc-moment" % "2.6.1"This will add momentjs to your JS and scala-js-momentjs to your scalaJS dependencies.
Some additional cats type class instances for DTC type classes ( like Invariant) are available via dtc-cats module:
libraryDependencies += "ru.pavkin" %%% "dtc-cats" % "2.6.1"This will bring in cats-core dependency.
Let's create a simple polymorphic class that works with local datetime values.
Say we want a period entity that operates on local datetime values and knows both it's start and end:
import java.time.Duration // this is provided crossplatformly by scala-js-java-time
import dtc.Local
import dtc.syntax.local._ // syntax extensions for Local instances
case class Period[T: Local](start: T, end: T) {
def prolong(by: Duration): Period[T] = copy(end = end.plus(by)) // syntax in action
def durationInSeconds: Long = start.secondsUntil(end) // syntax in action
def durationInMinutes: Long = start.minutesUntil(end) // syntax in action
}It is 100% cross-platform, so we can put it into a "shared" cross-project, to use later on both platforms.
Let's create two simple apps to demonstrate the concept.
First, let's start with JVM:
import java.time.LocalDateTime
import dtc.instances.localDateTime._ // provide implicit typeclass instance for java.time.LocalDateTime
import dtc.examples.Period
object Main extends App {
// with implicit typeclass instance in scope we can put LocalDateTime instances here.
val period = Period(LocalDateTime.now(), LocalDateTime.now().plusDays(1L))
println(period.durationInMinutes)
println(period.durationInSeconds)
println(period.hours.mkString("\n"))
}Next, nothing stops us from creating a JS app as well:
import java.time.Duration
import dtc.js.JSDate // this is special wrapper around plain JS date, that provides basic FP guarantees, e.g. immutability
import dtc.instances.jsDate._ // implicit Local instance for JSDate
import dtc.examples.Period
import scala.scalajs.js.annotation.JSExportTopLevel
object Main {
@JSExportTopLevel("dtc.examples.Main.main")
def main() = {
val period = Period(JSDate.now, JSDate.now.plus(Duration.ofDays(1L)))
println(period.durationInMinutes)
println(period.durationInSeconds)
println(period.hours.mkString("\n"))
}
}These examples demonstrate the core idea of the project. Read further to check out the list of available type classes and instances.
Disclaimer: although following entities are called type classes, there are not "pure". For example, they can throw exceptions for invalid method parameters. This is intentional:
Primary goal is to provide API that looks like java.time._ as much as it's possible.
DTC provides 4 type classes.
TimePoint extends cats.kernel.Order (api)
Base type class, that can be used for both local and zoned datetime instances.
All instances in DTC are extending TimePoint
Most of the APIs are same for any datetime value, so with this typeclass you get:
- all common methods and syntax except ones that are specific to local or zoned datetime values (e.g. constructors)
- you can use both zoned and local datetime instances to fill in the type parameter (not simultaneously, of course)
- almost no laws within the polymorphic code context :)
Local extends TimePoint (api)
Type class for values, that behave similarly to java.time.LocalDateTime. Instances hold local datetime laws.
Zoned extends TimePoint (api)
Type class for values, that behave similarly to java.time.ZonedDateTime. Instances hold zoned datetime laws.
Instance of Capture[T] means, that a specific instant can be represented by a value of type T.
Here an instant is represented as a product of (LocalDate, LocalTime, TimeZoneId).
While for a Zoned type it's clear, how to represent such instant (Zoned, actually, extends Capture to show that),
for Local it becomes tricky.
DTC provides only one instance of local Capture, which is a UTC instant representation with java.time.LocalDateTime.
Such behaviour allows to retain consistent value construction in polymorphic context:
LocalDateTime values, created with Capture will represent same instants as ZonedDateTime instances,
created from the same input.
Type class, that abstracts over the notion of "current" time. Provides API to get current date/time in a particular time zone.
In polymorphic context a Provider is the only way to get current time in DTC. This facilitates explicit DI of
current time, which leads to better design. For example, it allows to work with artificial time, controlled from the
outside (useful for tests).
To make your polymorphic code work on a specific platform, you'll need to supply typeclass instances for concrete datetime types you use.
DTC provides instances for both JVM and ScalaJS.
For JVM everything is straightforward:
java.time.LocalDateTimehas an instance ofLocaland a special UTC instance ofCapture.java.time.ZonedDateTimehas an instance ofZoned(which includesCapture).
To get the instances, just import respectively
import dtc.instances.localDateTime._
// or
import dtc.instances.zonedDateTime._Also both LocalDateTime and ZonedDateTime have an implicit instance of real system time Provider, available in:
import dtc.instances.providers._First of all, DTC does not provide instances for raw js values (neither Date nor moment).
They are mess to work with directly for two reasons:
- they are mutable
- they have totally different semantics, comparing to
java.time._.
Instead, DTC provides simple wrappers that delegate to underlying values (or even enrich the available API).
These wrappers provide immutability guarantees and adapt the behaviour to follow java.time._ semantics.
For ease of direct use, they reflect typeclass API as much as possible. Though, amount of actual direct use of them should be naturally limited, because, well... you can write polymorphic code instead!
JSDate wraps native ECMA-Script Date and provides instance for Local.
Instance can be imported like this:
import dtc.instances.jsDate._Javascript date has a very limited API which doesn't allow to handle time zones in a proper way.
So there's no Zoned and no Capture instance for it.
If you need a Zoned instance for your ScalaJS code, take a look at moment submodule, which is following next.
As on JVM, to get a real time Provider[JSDate], add this to your imports:
import dtc.instances.providers._These are based on popular MomentJS javascript library as well as ScalaJS facade for it.
To add them to your project, you'll need an explicit dependency on dtc-moment module:
libraryDependencies += "ru.pavkin" %%% "dtc-moment" % "2.0.0"Both classes wrap moment.Date and, as you can guess:
MomentLocalDateTimehas aLocalinstance with and a special UTCCaptureinstanceMomentZonedDateTimehas aZonedinstance (includesCapture)
You can get all instances in scope by adding:
import dtc.instances.moment._Provider instances for real time can be obtained here:
import dtc.instances.moment.providers._When writing polymorphic code with DTC, it's very convenient to use syntax extensions. They are similar to what Cats or Scalaz provide for their type classes.
Just add following to your imports:
import dtc.syntax.local._ // for Local syntax
// or
import dtc.syntax.zoned._ // for Zoned syntaxIf you need syntax for TimePoint or for both local and zoned type classes in the same file,
just import all syntax at once:
import dtc.syntax.all._Though, DTC provides basic API for datetime values comparison, it's more convenient and readable to use operators like
<, >= and so on.
To pull this off, you will need syntax extensions for cats.kernel.Order, that is extended by all DTC type classes.
Unfortunately, kernel doesn't have syntax extensions.
So, to get this syntax, you'll need to add dtc-cats module or define an explicit cats-core dependency in your
project:
libraryDependencies += "org.typelevel" %%% "cats-core" % "1.0.1"After that just add regular cats import to get Order syntax for datetime values:
import cats.syntax.order._See my article for original motivation and implementation overview.
DTC modules with published artifacts:
dtc-core- all type classes and instances forjava.time._andJSDatedtc-moment- momentjs instances (ScalaJS only)dtc-cats- additional cats instances for dtc type classes, like Invariant (adds cats-core dependency).dtc-laws- discipline laws to test your own instances
There's an open longstanding bug in MomentJS.
In some rare cases it gives incorrect diffs for monthsUntil method.
As of current version of DTC, this bug leaks into momentjs instances as well.
- Dependency updates.
- Cache
ZoneId.ofcalls. See PR
No major changes. Dependencies updated.
No major changes. Dependencies updated.
Support Scala.js 1.0.0
Scala 2.13 support has been provided
It appeared that variability of equality semantics was totally missed in previous versions.
Zoned values can be compared in two ways:
- Strict: identical instants are considered equal only if their time zones are equal as well.
- Relaxed: identical instants are considered equal regardless of time zones.
Since 2.0.0 dtc provides both kind of instances for java.time.ZonedDateTime and MomentZonedDateTime:
zonedDateTimeWithCrossZoneEqualityandzonedDateTimeWithStrictEqualityfor JVMmomentZonedWithCrossZoneEqualityandmomentZonedWithStrictEqualityfor moment
dtc.instances.zonedDateTime.zonedDateTimeDTCwas renamed tozonedDateTimeWithStrictEqualitydtc.instances.moment.momentZonedDTCwas renamed tomomentZonedWithCrossZoneEquality
This major release aims to fix a specific design flaw, which is impossibility to construct Local values from
zone-aware instants. Prior to 2.0, such option was an exclusive privilege of Zoned.
In practice this introduced limitations in correctly abstracting over UTC/Zoned time representations.
Inability to grasp this aspect from the beginning leaded to creation of arcane localDateTimeAsZoned instance.
This trick allowed to use LocalDateTime in Zoned context, which provided an ability to construct values
polymorphically.
Such abuse created it's own issues, in particular - ability to create LocalDateTime values in some Zoned contexts,
where it didn't make any sense.
Version 2.0 resolves this issue by extracting time creation functionality in a separate Capture typeclass.
Now it's possible to specify an exact polymorphic context you need:
TimePointfor a generic instantTimePoint+Capturefor a generic instant that can be constructed in a instant-preserving way.Zonedfor a zone-aware value for full control over zoning (no locals here from now on)Localspecifically for local (or UTC) instant, without zone information and control over it.
-
Now there's no
Zonedinstance forLocalDateTime.For each place you use it there're two options:
- It's zoned-only code, that doesn't make any sense in local context. In such case you have a better protection now from accidentally using it in an incorrect context.
- It's an abstraction over UTC/Zoned contexts.
In such case you should be able to replace the context bound from
ZonedtoTimePoint+Capture.
-
Zoned.of[T](...)was removed. UseCapture[T](...) -
Lawlesswas renamed toTimePoint.
- Explicit implementation of
hashCodewas added to moment wrapper classes, which can lead to different behaviour inMaps. - [Laws]
Zoned.constructorConsistencylaw is gone. Proper laws forCaptureare work in progress.
sbt release
sbt sonatypeBundleRelease
be sure you have configured ~/.sbt/1.0/sonatype.sbt with user / password obtained https://central.sonatype.com/account
credentials += Credentials("Sonatype Nexus Repository Manager",
"central.sonatype.com",
"user",
"password"