Dan Wellman's Blog

Pimp My Library: 5.minutes with the Time And Money Library

When I was learning to use Ruby on Rails, I was impressed by how the framework made expressing units of time so readble: "5.minutes" or "2.weeks". This was achieved by adding methods to the Numeric class, using Ruby's Open Classes.

As a developer new to Scala, I thought it would be an interesting exercise to implement this feature using The Scala Way: implicit conversions. I'm using the Java Time and Money Library which demonstrates the principles from the Domain-Driven Design book. I wrote the library test-first using the expressive ScalaCheck testing framework, but have yet to use the library in a real application.

I'm posting my work so far in case:

I've found the Scala community to be a very helpful and smart bunch of folks - thank you for your advice!

The Code

package com.danielwellman.timeandmoneysugar

import com.domainlanguage.time.Duration


object RichDurationUnits {
  implicit def intToRichDurationUnits(value: Int) = new RichDurationUnits(value)
}

class RichDurationUnits(value: Int) {
  def milliseconds = Duration.milliseconds(value)
  def seconds = Duration.seconds(value)
  def minutes = Duration.minutes(value)
  def hours = Duration.hours(value)
  def days = Duration.days(value)
  def weeks = Duration.weeks(value)
  def months = Duration.months(value)
  def quarters = Duration.quarters(value)
  def years = Duration.years(value)
}

object RichDuration {
  implicit def intToRichDuration(value: Duration) = new RichDuration(value)
}

class RichDuration(value: Duration) {
  /**
   * Add two RichDurations using Duration's plus method.
   *
   * Note that it is a runtime error to add two Durations of unequal base units,
   * such as seconds and months.  Since Duration itself does not use types to
   * guard against this problem, this library doesn't either.
   */
  def +(other: Duration) = value.plus(other)
}

Usage Examples

Here's an example of using the enhancements to Integers:

scala> import com.danielwellman.timeandmoneysugar._
import com.danielwellman.timeandmoneysugar._

scala> import RichDurationUnits._
import RichDurationUnits._

scala> 5.minutes
res0: com.domainlanguage.time.Duration = 5 minutes

scala> 2 weeks
res1: com.domainlanguage.time.Duration = 14 days

Note also the example of "2 weeks" doesn't use a period at all, since the Scala compiler will try to insert a period for you.

Here's an example of using the + operator with Durations as well as the sugar for Integers:

scala> import RichDuration._
import RichDuration._

scala> 2.days + 5.days
res2: com.domainlanguage.time.Duration = 7 days

scala> 2.minutes + 2.weeks
res3: com.domainlanguage.time.Duration = 14 days, 2 minutes

scala> 2.minutes + 2.months
java.lang.IllegalArgumentException: 2 months is not convertible to: 2 minutes
	at com.domainlanguage.time.Duration.assertNotConvertible(Unknown Source)
	at com.domainlanguage.time.Duration.plus(Unknown Source)
	at com.danielwellman.timeandmoneysugar.RichDuration.$plus(RichDurationUnits.scala:37)
	at .(:11)
	at .()
	at RequestResult$.(:3)
...

The last addition exposes a feature of the Time And Money library; you can't sensibly add two minutes and two months. I think it should be possible to use the Scala type system to prevent addition of two unrelated units, but haven't tried to do that yet.

I have found that using spaces can lead to some confusion. For example:

scala> 5 minutes + 2 minutes
:11: error: timeandmoneysugar.this.RichDurationUnits.intToRichDurationUnits(5).minutes of 
  type com.domainlanguage.time.Duration does not take parameters
       5 minutes + 2 minutes
         ^

Here I believe the compiler is trying to parse the "+ 2 minutes" expression as an argument to the "minutes" method of RichDuration. In this case, you need to wrap some of the arguments to give the compiler a hint, like so:

scala> (5 minutes) + (2 minutes)
res8: com.domainlanguage.time.Duration = 7 minutes

References

Here are some articles I found useful while learning about implicit conversions: