SodaTime is a port of JodaTime to Scala, so that it can be compiled with Scala.js
.
The intention is to have a cross compiled, high quality Date/Time library that can be used across all JVM's, as well as
Scala.js
- Be completely API compatible with
JodaTime
for both Scala (JVM) and as much as possible forScala.js
. Please see notable changes for more info.
Current SNAPSHOT
artifacts are deployed to SonaType Snapshots, so you should be able to use the current version using
"org.mdedetrich" %% "soda-time" % "0.0.1-SNAPSHOT"
This is still ALPHA quality, Joda Time is a big library, and there is stuff that still needs to be done. Definitely try it out, but I wouldn't recommend using it in production.
- Take a look at sections of the converted code that used
continue
/break
to a label. - Tests
- v2.8.x of the Joda-Time API (v2.7.x is what is currently being implemented)
-
java.util.Locale
needs to be implemented - Possible better solution for aux constructors
- Remove all mentions of file/io/streams from
Javascript
based API - Adding utility methods for
Javascript
(i.e. java Date constructors)
The state of Date/Time libraries for Java
is pretty grim (in fact its grim for most languages that haven't been made in the
past decade). Because of this, JodaTime was created, which provided a high quality
implementation of Date/Time library for Java.
In regards to Scala, most people just use JodaTime, and the few who have completely
migrated to JDK-8
use JSR-310
. Unfortunately, this poses a problem for people intending to use Scala.js
(and possible any other future backend for Scala).
Scala.js
can only compile Scala source code, not Java source code/JVM bytecode. This means it won't work with JodaTime- The
JSR-310
(designed to supersede JodaTime) has a GPL style license, which is incompatible withScala.js
's license. This means that to implementJSR-310
(i.e.java.time.*
) forScala.js
, a complete cleanroom implementation ofjava.time.*
needs to be done. Please see this ticket for more info. - Even with
JSR-310
implemented, users who run onJDK-7
and less will still be stuck. Although there are backports forJSR-310
, it is still unknown whether these backports will work in context ofScala.js
(needs to be confirmed) - Implementing a correct Date/Time library is really hard (see this video for more info)
Due to all of the above, the only real solution (at least for the short/medium term) is to have a cross compatile version of
JodaTime that will work on Java/Scala/Scala.js
). The ultimate solution would be to provide
a clean version of a Date/Time library code Scala (something along the lines scala.time.*
) that would be in the Scala stdlib
.
However due to the difficulty of coding a correct Date/Time library (as mentioned before), plus other reasons, we shouldn't be
expecting this any time soon.
There are other reasons (outside of compatibility/cross platform) as to why you might want to use SodaTime.
- Its a clean implementation of Date/Time for javascript (unlike, for example, moment.js, which uses
Javascript
'sDate
object behind the scenes).Javascript
clients (including web browsers) are notorious for having quirks in how they implement theJavascript
Date
object (see here as an example).SodaTime
would not have an issue in this regard since it provides a correct implementation of Date/Time across all browsers (assuming that UTC timestamp retrieved fromDate.getMilliseconds
is correct) JodaTime
has been battle tested for 13 years, and its the defacto standard for Date/Time on Java. This means its well tested. Any Java developer who is worth their salt usesJodaTime
.SodaTime
was mainly ported using the Scalagen library, which means that the majority of critical business logic code has been ported correctly and automatically. Please see methodology for more info- It has a very good design (even when used in
Scala
), due to it putting high emphasis on using immutable types
There are difference for SodaTime
on Scala.js, mainly due dealing with Javascript. In regards to API, there are some breaking changes, which are noted below
- Methods that deal with file operations (i.e.
java.io.File
) are not exported, as they make no sense onJavascript
- Error classes, such as
org.joda.IllegalFieldValueException
, only have a single primary constructor, rather than various constructors as the originalJodaTime
. This is because of aScala
limitation that does not allow you to have differentsuper
calls within different constructors. SinceIllegalFieldValueException
extends a class we have no control over (java.lang.IllegalArgumentException
) we had no choice but to do this. Luckily, the use case for users makingIllegalFieldValueException
with custom error messages is non existent.
These are API changes which aren't breaking (i.e. usually the addition of certain utility methods)
- Constructors for
DateTime
for theJavascript
Date
object, i.e. fromScala
val dateTime = new org.joda.DateTime(new js.Date())
And also from Javascript
var dateTime = new org.joda.DateTime(new Date());
The main goal for SodaTime
is to provide a correct implementation of JodaTime
for Scala
/Scala.js
. At the same time, JodaTime
itself is a massive library,
so providing a clean, idiomatic Scala implementation of JodaTime
is unrealistic and unwise. Such effort should be used in creating a new Scala implementation
of a Date/Time library.
Hence to verify the correctness of SodaTime, we rely on the following principles
- The
JodaTime
implementation is itself is "correct". Note that JodaTime itself may not be correct, but if this is the case, then we wantSodaTime
to simulate this - Following on, the code that is converted from
JodaTime
usingScalaGen
will be mainly correct in regards to business logic (see below for more details) - Converting the test cases from
JodaTime
, and implementing our own.
With this in mind, we generally want to leverage as many tools/methods to obtain this goal, described below
-
Convert the
Java
code toScala
code usingScalaGen
, one can also use javatoscala.ScalaGen
is not perfect, and it has issues with the following (in order of being problematic)-
break/continue/break to label. Only
break
is supported inScala
, (and that is through an explicit import,scala.util.control.Breaks._
). This is the only known change thatScalaGen
does which actually breaks business logic (at least on a semantic level) apart from switch statements.ScalaGen
will usually output commented code such as// break
or// continue
in this case . To fix this, the following is done- If its only a
break
, we do usescala.util.control.Break
- If its a continue, we simulate the behaviour using a flag
name
continueFlag) in code, along with a simple
if` condition. - If its a break to label, we have to manually rewrite the code. we have to carefully rewrite the code
- If its only a
-
ScalaGen
will try to convertswitch
statement tomatch
, however it usually doesn't fully work for because of the existence/ non existence of break. Here are the following issues with this-
It Typically places the Scala equivalent of
default
(case _ =>
) statement at the top of the match block, which is obviously semantically different to how thedefault
works in switch.Scala
compiler will emit a warning to detect this, and its an easy fix (move thecase _ =>
to the bottom of the match statement). There are however -
Java
switch usesbreak
. Depending on what the switch statement does, this may or may not be semantically equivalent. As an example, the following code attempts to mutate a variableString seconds; switch (getSeconds()) { default: seconds = "0"; break; case 1: seconds = "one"; break; }
The equivalent can be converted to
val seconds = getSeconds() match { case 1 => "1" case _ => "0" }
Sometimes its not so straight forward, especially when there is a combination of side effects/mutation. Generally speaking, generated match statements should be inspected
-
-
Generally doesn't work with constructors/super/subclassing. This is mainly due to the fact that
Scala
doesn't support all of theJava
s methdods of instantiating a new class. As an example,Scala
has no API equivalent of supporting multiple differentsuper
calls in different constructors of the same class. When this occurs, we either use auxiallary constructors (namedauxConstructor
) if we have control of the classes that is being extended, else we resort making an API incompatible change which involves manually creating constructors in a companion object (seeorg.joda.IllegalFieldValueException
for more info). In this sitaution, the following this done- Attempt to manually rewrite the constructors. If there is a common super constructor, we can avoid the before mentioned limitation
- If the above isn't the case, we create an empty constructor (which doesn't initialize any state), and then we make
auxConstructors
in the super class to simulate the same behaviour. - If we don't have control over the super class (i.e the case with
org.joda.IllegalFieldValueException
), we have to make a breakingAPI
change, and use factory instant creation methods in the companion object. So far, this has only occurred for exception classes - Not onlining parameters properly.
JodaTime
uses the Java conversion of using constructors to set internal private mutable variables.ScalaGen
usually tries to move these online private variables into the constructor, which combined with the general super/constructor issues, causes problems. This code is often rewritten to resemble theJava
equivalent
-
Side effect statements. The
Java
code written inJodaTime
has some parts which is written like old C style, with the equivalent expressions either not existing inScala
, or being semantically different. Examples include-
The following
Java
codeif (c = getSomeChar()) { // Do stuff };
Is legitimate, and the return type of that code is
Char
(assuming thatgetSomeChar()
returns aChar
and type ofc
isChar
). The equivalent (which is whatScalaGen
outputs) returns typeUnit
, which often means the code needs to be rewritten to something likeif ({c = getSomeChar();c}) { // Do stuff }
-
Another example is double assignment, which is often used in
C
, i.e.someVar = anotherVar = yetAnotherVar;
This has no
Scala
equivalent, so its rewritten tosomeVar = anotherVar anotherVar = yetAnotherVar
-
There is also the shorthand increment operation, which often doesn't work in
Scala
. i.e.Int counter = 0; counter += 1;
In scala this is done like so
var counter = 0 counter = counter + 1
-
-
Not creating
var
's when variable referenced is method a parameter. As an example,ScalaGen
usually creates the followingdef toDateTime(zone: DateTimeZone): DateTime = { zone = DateTimeUtils.getZone(zone) if (getZone == zone) { return this } super.toDateTime(zone) }
Which wont compile, so this is what is often done
override def toDateTime(zone: DateTimeZone): DateTime = { var _zone = zone _zone = DateTimeUtils.getZone(_zone) if (getZone == _zone) { return this } super.toDateTime(_zone) }
-
Not adding overrides.
ScalaGen
sometimes won't create overrides for functions which override super members. This isn't really the fault of the tool, since@Override
is a convention inJava
, where as theScala
compiler will force you to useoverride
if you are overriding the super member. Thankfully,IntelliJ
has an inspection which picks this up automatically -
ScalaGen
doesn't correctly generate for loops forJava
code that uses theConsumer
API. It will convert theJava
for statementfor (item : someCollection) { \\ Do stuff };
Into this
for (item <- someCollection) { \\ Do stuff }
The scala converted code will show as correct in some tools (i.e. Intellij), but it won't compile. Scala doesn't (yet) have interopt for the Java consumer API. This means the above code will be typically changed to
val iterator = someCollection.iterator while(iterator.hasNext) { // Do stuff here }
-
Manually annotate types,
Scalagen
by default doesn't annotate converted variable declarations types. This usually isn't an issue, but there are instances of implicitly converted types which don't work out (i.e.
conversions betweenLong
/Int
, and vice versa). Manually specifying the type (i.e.var millis:Long
) fixes this problem
-
-
At this point, the code will usually compile, so we do the following things
- Really quick and easy syntax fixes. As an example,
ScalaGen
sometimes unnecessarily puts statements in extra enclosing parenthesis, amongst other things
- Really quick and easy syntax fixes. As an example,
-
Split out the code into
js
andjvm
if it makes sense to do so. The following are reasons why you would do this- Replace internal collections used in business logic to ones that make sense and/or ones that have JS equivalent (performance). Examples
includes
- Using
js.Array
instead ofArray
internally for performance reasons - Changing
java.util.concurrent.ConcurrentHashMap
to standardjava.util.HashMap
internally (Javascript
has no concept of multithreading, so concurrent data structures are not required) - In general using Scala collections over Java ones for
Javascript
implementation. This is becauseScala.js
uses a deep linking optimizer, and people are much more likely to use Scala collections than Java ones, saving room on theoretical outputtedJavascript
size.JVM
implementation is untouched, as there is little point in converting it toScala
collections
- Using
- Providing alternate types to the
opaque
types to be @JSExported. This is done just for theJavascript
access to the API - Adding extra constructors for JS types (i.e.
Javascripts
array asjs.Array
as well asscala.Array
) - Separate implementations of annotations that don't make sense on
Javascript
. i.e., forjoda.convert.ToString
, theJVM
version is aJava
source that looks like this (which is an exact copy of the source fromJodaTime
)
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ToString { }
The JS version looks like this
import scala.annotation.StaticAnnotation class ToString extends StaticAnnotation
- Replace internal collections used in business logic to ones that make sense and/or ones that have JS equivalent (performance). Examples
includes