time

At Evolution Gaming me and Artsiom work on internal scheduling application, that has a huge ScalaJS frontend. We have to deal with lots of complex date/time logic, both on backend and browser sides.

I quickly realised, that sharing same business logic code across platforms would be a massive advantage. But there was a problem: there were (and still is) no truly cross-platform date/time library for Scala/ScalaJS.

After a small research I settled with a type class-based solution that provides cross-platform
java.time.*-like values with full TimeZone/DST support. In this post we will:

take a quick look at the current state of date/time support in Scala/ScalaJS;

see how to get cross-platform date/time values today with the help of type classes.

Described approach works quite well in our application, so I extracted the core idea into a library, called DTC. If you’re a “Gimme the code!” kind of person, I welcome you to check out the repo.

Prerequisites

I assume, that reader is familiar with ScalaJS and how to set up a cross-platform project. Familiarity with type classes is also required.

The Goal

There’s no solution without a goal. Precise goal will also provide correct context for reasonings in this article. So let me state it.

My primary goal is to be able to write cross-platform code that operates on date/time values with full time zone support.

This also means that I will need implementation(s) that behave consistently across JVM and browser. We’re Scala programmers, so let’s choose JVM behaviour semantics as our second goal.

Current state of date/time in Scala and ScalaJS

So we have our goal, let’s see how we can achieve it.

In this section we’ll go over major date/time libraries and split them into 3 groups: JVM-only, JS-only and cross-platform.

JVM-only

We won’t spend too much time here, everything is quite good on JVM side: we have Joda and Java 8 time package. Both are established tools with rich API and time zone support.

But they can’t be used in ScalaJS code.

JS-only

We’re looking at JS libraries, because we can use them in ScalaJS through facades. When it comes to date/time calculations, there are effectively two options for JavaScript: plain JS
Date and MomentJS library.

There’re things that are common for both and make them quite problematic to use for our goal:

values are mutable;

semantics are different from JVM in many places. For example, you can call
date.setMonth(15) , and it will give you a same date in March next year!

There’s also a JS-Joda project, which is not so popular in JS world, but has much more value to JVM developers, because it brings Java8 time semantics to Javascript.

Cross-platform date/time libraries

This library is the future of cross-platform date/time code. It’s effectively Java 8 time, written from scratch for ScalaJS.

At the time of writing this post, scala-js-java-time already provides
LocalTime,
LocalDate,
Duration , and a handful of other really useful
java.time.* classes (full list here).

It means, that you can use these classes in cross-compiled code and you won’t get linking errors: in JVM runtime original
java.time.* classes will be used, and JS runtime will be backed by scala-js-java-time implementations.

Problem here, is that we need
LocalDateTime and
ZonedDateTime in ScalaJS. And they are not there yet.

Spoiler: we’ll be using scala-js-java-time in our final solution for the problem.

It’s in early development stages and also doesn’t have time zones in ScalaJS, but I still added it to the list, because developers took an interesting approach: they are converting original Joda code with ScalaGen.

So the resulting code really smells Java, but I’m still curious of what this can develop into.

Idea? No, the only option

The reason I’ve given the overview of currently available libraries is simple: it makes clear that there’s only one possible solution to the problem.

There’s no cross-platform library with full time zone support. And for JavaScript runtime there’s only MomentJS, that really fits our requirements. All this leaves us with nothing, except following approach:

We define some type class, that provides rich date/time API. It’s a glue that will allow us to write cross-platform code.

All code, that needs to operate date/time values, becomes polymorphic, like this:

1

caseclassTodoItem[T:DateTime](text:String,due:T)

We provide platform-dependent type class instances:
java.time.*-based for JVM and MomentJS-based for ScalaJS.

We define common behaviour laws to test the instances against. This will guarantee, that behaviour is consistent across platforms.

MomentJS API is powerful, but it has to be sandboxed and shaped to:

provide immutable values;

provide JVM-like behaviour;

There’s a limitation, that we can’t overcome without some manual implementation: both JS libraries don’t support nano seconds. So we’ll have to live with milliseconds precision.

We won’t go over all of these points in this article. DTC library does the heavy lifting for all of them. In following sections we’ll just glance over the core implementation and example.

DateTime type class

Let’s just take a small subset of
java.time.LocalDateTime API and lift it into a generic type class. We’ll use simulacrum, to avoid common boilerplate:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

importjava.time.{Duration,LocalDate,LocalTime}

importcats.kernel.Order

importsimulacrum.typeclass

@typeclasstraitDateTime[A]extendsOrder[A]{

defdate(x:A):LocalDate// extract date part

deftime(x:A):LocalTime// extract time part

// adding an arbitrary duration gives a value of the same type.

defplus(x:A,d:Duration):A

// many many other...

}

First of all, a total order for
DateTime values is defined. So we can extend
cats.kernel.Order and get all it’s goodies out of the box.

Second, thanks to scala-js-java-time, we can use
LocalTime and
LocalDate to represent parts of the value. Also, we can use
Duration for addition operations.

For now, let’s just view it as a glue for
LocalDateTime. We’ll get to time zone support a bit later.

Cross-compiled business logic

Having our new type class, let’s define some “complex” logic, that needs to be shared across JVM and browser.

1

2

3

4

5

6

7

8

9

10

11

importjava.time.Duration

importDateTime.syntax._

caseclassPeriod[T:DateTime](start:T,end:T){

defprolong(by:Duration):Period[T]=copy(end=end.plus(by))

defhasFullNumberOfDays:Boolean=start.time==end.time

defcrossesMidnight:Boolean=start.date!=end.date

}

With syntax extensions in place, the code looks quite nice.

More over, you can notice, that nothing here says, if time should be local or zoned. Code is polymorphic, and we can use different kinds of date/time values, depending on the context.

Now let’s get to the flesh and bones: type class instances.

Type class instances

Let’s start with JVM instance, as it’s going to be quite simple. Follow comments in code for details.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

importjava.time.temporal.ChronoUnit

importjava.time._

objectlocalDateTime{

implicitvallocalDateTimeInstance:DateTime[LocalDateTime]=

newDateTime[LocalDateTime]{

// this is required by cats.kernel.Order

defcompare(x:LocalDateTime,y:LocalDateTime):Int=x.compareTo(y)

defdate(x:LocalDateTime):LocalDate=x.toLocalDate

// clear any occasional nanos

deftime(x:LocalDateTime):LocalTime=

x.toLocalTime.truncatedTo(ChronoUnit.MILLIS)

defplus(x:LocalDateTime,d:Duration):LocalDateTime=

x.plus(d)

}

}

With MomentJS it’s going to be much more interesting, because we’ve obliged ourselves to provide values, that are comfortable to work with for a functional programmer.

To enforce immutability, we won’t expose any moment APIs directly. Instead, we’re going to wrap moment values in a simple object, that will be immutable:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

importjava.time.{Duration,LocalDate,LocalTime}

// import moment facade bindings

importmoment.{Date,Moment,Units}

classMomentLocalDateTime private(privateoverridevalunderlying:Date){

// create a copy of underlying value

privatedefcopy=Moment(underlying)

// produce a new value by applying a specified modifier

// to a copy of underlying mutable value

privatedefupdated(modifier:Date=>Date):MomentLocalDateTime=

newMomentLocalDateTime(modifier(copy))

defplusMillis(n:Long):MomentLocalDateTime=

updated(_.add(n.toDouble,Units.Millisecond))

// implementation for type class plus method

defplus(d:Duration):MomentLocalDateTime=plusMillis(d.toMillis)

defdayOfMonth:Int=underlying.date()

// JVM-like behaviour. JS has zero-based months

defmonth:Int=underlying.month()+1

defyear:Int=underlying.year()

defhour:Int=underlying.hour()

defminute:Int=underlying.minute()

defsecond:Int=underlying.second()

defmillisecond:Int=underlying.millisecond()

deftoLocalDate:LocalDate=LocalDate.of(year,month,dayOfMonth)

// LocalTime accepts nanos as a fraction of second,

// so we convert millis to nanos.

deftoLocalTime:LocalTime=

LocalTime.of(hour,minute,second,millisecond*1000000)

}

objectMomentLocalDateTime{

defcompare(x:MomentLocalDateTime,y:MomentLocalDateTime)=

Ordering.Double.compare(x.underlying.value(),y.underlying.value())

}

Several notable things here:

We make both constructor and underlying value private to make sure there’s no way to modify object internals. We’ll provide a custom constructor later.

Notice month value adjustment to provide JVM-like behaviour. You will see much more of such things in DTC, I even had to write a couple of methods from scratch.

To compare two moment values, we use their raw timestamps.

Now it’s trivial to define
DateTime instance for our
MomentLocalDateTime:

1

2

3

4

5

6

7

8

9

10

11

12

13

importjava.time.{Duration,LocalDate,LocalTime}

implicitvalmomentLocalDateTimeInstance:DateTime[MomentLocalDateTime]=

newDateTime[MomentLocalDateTime]{

defcompare(x:MomentLocalDateTime,y:MomentLocalDateTime):Int=

MomentDateTime.compare(x,y)

defdate(x:MomentLocalDateTime):LocalDate=x.toLocalDate

deftime(x:MomentLocalDateTime):LocalTime=x.toLocalTime

defplus(x:MomentLocalDateTime,d:Duration):MomentLocalDateTime=

x.plus(d)

}

Now have everything to run our generic domain logic on both platforms. I’ll leave it as an exercise for my dear reader.

Now let’s discuss some aspects of making this thing work for zoned values as well.

Time Zone support

Not much time is needed to realise, that we need separate type classes for local and zoned values. Reasons are:

They obey different laws. For example, you can’t expect a zoned value to have same local time after adding 24h to it.

They have different constructor APIs. Zoned value needs time zone parameter to be properly constructed.

Zoned values should provide additional APIs for zone conversions.

On the other side, most of querying, addition and modification APIs are common to both kinds of date/time values. And we would like to take advantage of that in cases we don’t really care about a kind of the value and wish to allow using both.

This leads us to following simple hierarchy:

LawlessDateTimeTC (which we initially called
DateTime) that contains common methods, specific to all date/time values.

LocalDateTimeTC and
ZonedDateTimeTC will extend
LawlessDateTimeTC and provide kind-specific methods (constructors, for example).

This
-TC suffix is ugly, but name clash in JVM code is worse :).

We will also have to provide a cross-compiled wrapper for time zone identifiers, because
java.time.ZoneId is not yet provided by scala-js-java-time, and we don’t really want to pass raw strings around.

Everything else is just an evolution of core idea. Full implementation and more examples are available in the DTC repo.

Note on polymorphism

A side-effect of this solution, is that all your code becomes polymorphic over the specific date/time type. While most of the time you’re going to use single kind of time (zoned or local), there are cases when polymorphism becomes useful.

For example, in an event-sourced system, you may require zoned values for most of the calculations within the domain, as well as commands. But, at the same time, it can be a good idea to store events to journal with UTC values instead.
With type class-based approach, you can use same data structures for both purposes, by just converting between type parameters of different kinds.

Conclusion

Though polymorphic code can look scary for some people, described approach give us following advantages:

Truly cross-platform code, that operates on rich date/time APIs with time zone support.

Polymorphism over specific kind of date/time values.

If you’re working with date/time values in Scala on a daily basis, please, give DTC a try and tell me what you think!