Skip to content

mdedetrich/soda-time

Repository files navigation

SodaTime, Port of JodaTime for Scala/Scala.js

Join the chat at https://gitter.im/mdedetrich/soda-time Build Status

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

Goals

  • Be completely API compatible with JodaTime for both Scala (JVM) and as much as possible for Scala.js. Please see notable changes for more info.

Artifacts

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"

What still needs to be done

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)

Why

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 with Scala.js's license. This means that to implement JSR-310 (i.e. java.time.*) for Scala.js, a complete cleanroom implementation of java.time.* needs to be done. Please see this ticket for more info.
  • Even with JSR-310 implemented, users who run on JDK-7 and less will still be stuck. Although there are backports for JSR-310, it is still unknown whether these backports will work in context of Scala.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's Date object behind the scenes). Javascript clients (including web browsers) are notorious for having quirks in how they implement the Javascript 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 from Date.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 uses JodaTime. 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

Differences

Breaking API differences

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 on Javascript
  • Error classes, such as org.joda.IllegalFieldValueException, only have a single primary constructor, rather than various constructors as the original JodaTime. This is because of a Scala limitation that does not allow you to have different super calls within different constructors. Since IllegalFieldValueException 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 making IllegalFieldValueException with custom error messages is non existent.

Other API changes

These are API changes which aren't breaking (i.e. usually the addition of certain utility methods)

  • Constructors for DateTime for the Javascript Date object, i.e. from Scala
val dateTime = new org.joda.DateTime(new js.Date())

And also from Javascript

var dateTime = new org.joda.DateTime(new Date());

Methodology

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 want SodaTime to simulate this
  • Following on, the code that is converted from JodaTime using ScalaGen 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

  1. Convert the Java code to Scala code using ScalaGen, one can also use javatoscala. ScalaGen is not perfect, and it has issues with the following (in order of being problematic)

    1. break/continue/break to label. Only break is supported in Scala, (and that is through an explicit import, scala.util.control.Breaks._). This is the only known change that ScalaGen 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 use scala.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
    2. ScalaGen will try to convert switch statement to match, 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 the default works in switch. Scala compiler will emit a warning to detect this, and its an easy fix (move the case _ => to the bottom of the match statement). There are however

      • Java switch uses break. 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 variable

        String 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

    3. Generally doesn't work with constructors/super/subclassing. This is mainly due to the fact that Scala doesn't support all of the Javas methdods of instantiating a new class. As an example, Scala has no API equivalent of supporting multiple different super calls in different constructors of the same class. When this occurs, we either use auxiallary constructors (named auxConstructor) 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 (see org.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 breaking API 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 the Java equivalent
    4. Side effect statements. The Java code written in JodaTime has some parts which is written like old C style, with the equivalent expressions either not existing in Scala, or being semantically different. Examples include

      • The following Java code

            if (c = getSomeChar()) {
                // Do stuff
            };

        Is legitimate, and the return type of that code is Char (assuming that getSomeChar() returns a Char and type of c is Char). The equivalent (which is what ScalaGen outputs) returns type Unit, which often means the code needs to be rewritten to something like

            if ({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 to

        someVar = 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
    5. Not creating var's when variable referenced is method a parameter. As an example, ScalaGen usually creates the following

            def 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)
            }
    6. 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 in Java, where as the Scala compiler will force you to use override if you are overriding the super member. Thankfully, IntelliJ has an inspection which picks this up automatically

    7. ScalaGen doesn't correctly generate for loops for Java code that uses the Consumer API. It will convert the Java for statement

      for (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
      }
    8. 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 between Long/Int, and vice versa). Manually specifying the type (i.e. var millis:Long) fixes this problem

  2. 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
  3. Split out the code into js and jvm 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 of Array internally for performance reasons
      • Changing java.util.concurrent.ConcurrentHashMap to standard java.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 because Scala.js uses a deep linking optimizer, and people are much more likely to use Scala collections than Java ones, saving room on theoretical outputted Javascript size. JVM implementation is untouched, as there is little point in converting it to Scala collections
    • Providing alternate types to the opaque types to be @JSExported. This is done just for the Javascript access to the API
    • Adding extra constructors for JS types (i.e. Javascripts array as js.Array as well as scala.Array)
    • Separate implementations of annotations that don't make sense on Javascript. i.e., for joda.convert.ToString, the JVM version is a Java source that looks like this (which is an exact copy of the source from JodaTime)
            @Target(ElementType.METHOD)
            @Retention(RetentionPolicy.RUNTIME)
            public @interface ToString {
            
            }

    The JS version looks like this

        import scala.annotation.StaticAnnotation
        
        class ToString extends StaticAnnotation

About

Scala/Scala.js port of JodaTime to Scala/Scala.js

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published