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.LocalDateTime
andjava.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
Order
type class.
Add this line to your build.sbt
.
libraryDependencies += "ru.pavkin" %%% "dtc-core" % "2.6.0"
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.0"
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.0"
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.LocalDateTime
has an instance ofLocal
and a special UTC instance ofCapture
.java.time.ZonedDateTime
has 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:
MomentLocalDateTime
has aLocal
instance with and a special UTCCapture
instanceMomentZonedDateTime
has aZoned
instance (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 syntax
If 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._
andJSDate
dtc-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.
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
:
zonedDateTimeWithCrossZoneEquality
andzonedDateTimeWithStrictEquality
for JVMmomentZonedWithCrossZoneEquality
andmomentZonedWithStrictEquality
for moment
dtc.instances.zonedDateTime.zonedDateTimeDTC
was renamed tozonedDateTimeWithStrictEquality
dtc.instances.moment.momentZonedDTC
was 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:
TimePoint
for a generic instantTimePoint
+Capture
for a generic instant that can be constructed in a instant-preserving way.Zoned
for a zone-aware value for full control over zoning (no locals here from now on)Local
specifically for local (or UTC) instant, without zone information and control over it.
-
Now there's no
Zoned
instance 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
Zoned
toTimePoint
+Capture
.
-
Zoned.of[T](...)
was removed. UseCapture[T](...)
-
Lawless
was renamed toTimePoint
.
- Explicit implementation of
hashCode
was added to moment wrapper classes, which can lead to different behaviour inMap
s. - [Laws]
Zoned.constructorConsistency
law is gone. Proper laws forCapture
are work in progress.