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
JodaTimefor both Scala (JVM) and as much as possible forScala.js. Please see notable changes for more info.
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. Also its not yet published to a repo, best bet is to clone it locally and use publishLocal
- Take a look at sections of the converted code that used
continue/breakto a label. - Tests
- v2.8.x of the Joda-Time API (v2.7.x is what is currently being implemented)
-
java.util.Localeneeds to be implemented - Possible better solution for aux constructors
- Remove all mentions of file/io/streams from
Javascriptbased 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.jscan 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-310implemented, users who run onJDK-7and 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'sDateobject behind the scenes).Javascriptclients (including web browsers) are notorious for having quirks in how they implement theJavascriptDateobject (see here as an example).SodaTimewould 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.getMillisecondsis correct) JodaTimehas 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.SodaTimewas 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 aScalalimitation that does not allow you to have differentsupercalls within different constructors. SinceIllegalFieldValueExceptionextends a class we have no control over (java.lang.IllegalArgumentException) we had no choice but to do this. Luckily, the use case for users makingIllegalFieldValueExceptionwith 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
DateTimefor theJavascriptDateobject, 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
JodaTimeimplementation is itself is "correct". Note that JodaTime itself may not be correct, but if this is the case, then we wantSodaTimeto simulate this - Following on, the code that is converted from
JodaTimeusingScalaGenwill 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
Javacode toScalacode usingScalaGen, one can also use javatoscala.ScalaGenis not perfect, and it has issues with the following (in order of being problematic)-
break/continue/break to label. Only
breakis supported inScala, (and that is through an explicit import,scala.util.control.Breaks._). This is the only known change thatScalaGendoes which actually breaks business logic (at least on a semantic level) apart from switch statements.ScalaGenwill usually output commented code such as// breakor// continuein 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
namecontinueFlag) in code, along with a simpleif` 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
-
ScalaGenwill try to convertswitchstatement 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 thedefaultworks in switch.Scalacompiler will emit a warning to detect this, and its an easy fix (move thecase _ =>to the bottom of the match statement). There are however -
Javaswitch 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
Scaladoesn't support all of theJavas methdods of instantiating a new class. As an example,Scalahas no API equivalent of supporting multiple differentsupercalls 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.IllegalFieldValueExceptionfor 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
auxConstructorsin 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 breakingAPIchange, and use factory instant creation methods in the companion object. So far, this has only occurred for exception classes - Not onlining parameters properly.
JodaTimeuses the Java conversion of using constructors to set internal private mutable variables.ScalaGenusually 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 theJavaequivalent
-
Side effect statements. The
Javacode written inJodaTimehas 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
Javacodeif (c = getSomeChar()) { // Do stuff };
Is legitimate, and the return type of that code is
Char(assuming thatgetSomeChar()returns aCharand type ofcisChar). The equivalent (which is whatScalaGenoutputs) 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
Scalaequivalent, 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,ScalaGenusually 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.
ScalaGensometimes won't create overrides for functions which override super members. This isn't really the fault of the tool, since@Overrideis a convention inJava, where as theScalacompiler will force you to useoverrideif you are overriding the super member. Thankfully,IntelliJhas an inspection which picks this up automatically -
ScalaGendoesn't correctly generate for loops forJavacode that uses theConsumerAPI. It will convert theJavafor 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,
Scalagenby 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,
ScalaGensometimes unnecessarily puts statements in extra enclosing parenthesis, amongst other things
- Really quick and easy syntax fixes. As an example,
-
Split out the code into
jsandjvmif 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.Arrayinstead ofArrayinternally for performance reasons - Changing
java.util.concurrent.ConcurrentHashMapto standardjava.util.HashMapinternally (Javascripthas no concept of multithreading, so concurrent data structures are not required) - In general using Scala collections over Java ones for
Javascriptimplementation. This is becauseScala.jsuses a deep linking optimizer, and people are much more likely to use Scala collections than Java ones, saving room on theoretical outputtedJavascriptsize.JVMimplementation is untouched, as there is little point in converting it toScalacollections
- Using
- Providing alternate types to the
opaquetypes to be @JSExported. This is done just for theJavascriptaccess to the API - Adding extra constructors for JS types (i.e.
Javascriptsarray asjs.Arrayas well asscala.Array) - Separate implementations of annotations that don't make sense on
Javascript. i.e., forjoda.convert.ToString, theJVMversion is aJavasource 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