Thursday, May 22, 2008

Units DSL in Groovy

A while back I saw Guillaume Laforge's article about building a Groovy DSL for unit manipulations. I recently needed to implement something similar in a project, so I decided to take Guillaume's code and update it a bit. I wanted a nice way to package it up so I could quickly enable unit manipulation support on a particular class. I also added a pair of methods to make things more flexible. Here's my UnitDSL.groovy:

package org.psicat.model

import org.jscience.physics.amount.*
import javax.measure.unit.*

/**
* A helper class for setting up the Units DSL
*/
class UnitDSL {
private static boolean isEnabled = false;
private UnitDSL() { /* singleton */ }

/**
* Initialize the Units DSL.
*/
static enable() {
// only initialize once
if (isEnabled) return

// mark ourselves as initialized
isEnabled = true

// enable inheritance on EMC
ExpandoMetaClass.enableGlobally()

// transform number properties into an mount of a given unit represented by the property
Number.metaClass.getProperty = { String symbol -> Amount.valueOf(delegate, Unit.valueOf(symbol)) }

// define opeartor overloading, as JScience doesn\'t use the same operation names as Groovy
Amount.metaClass.static.valueOf = { Number number, String unit -> Amount.valueOf(number, Unit.valueOf(unit)) }
Amount.metaClass.multiply = { Number factor -> delegate.times(factor) }
Number.metaClass.multiply = { Amount amount -> amount.times(delegate) }
Number.metaClass.div = { Amount amount -> amount.inverse().times(delegate) }
Amount.metaClass.div = { Number factor -> delegate.divide(factor) }
Amount.metaClass.div = { Amount factor -> delegate.divide(factor) }
Amount.metaClass.power = { Number factor -> delegate.pow(factor) }
Amount.metaClass.negative = { -> delegate.opposite() }

// for unit conversions
Amount.metaClass.to = { Amount amount -> delegate.to(amount.unit) }
Amount.metaClass.to = { String unit -> delegate.to(Unit.valueOf(unit)) }
}

/**
* Add Units support to the specified class.
*/
static addUnitSupport(clazz) {
clazz.metaClass.setProperty = { String name, value ->
def metaProperty = clazz.metaClass.getMetaProperty(name)
if (metaProperty) {
if (metaProperty.type == Amount.class && value instanceof String) {
metaProperty.setProperty(delegate, Amount.valueOf(value))
} else {
metaProperty.setProperty(delegate, value)
}
}
}
}

/**
* Remove Units support from the specified class.
*/
static removeUnitSupport(clazz) {
GroovySystem.metaClassRegistry.removeMetaClass(clazz)
}
}


To enable the DSL, you have to call UnitDSL.enable(). This adds a few methods to the metaclasses on Number and Amount. The majority of the code in enable() is a straight cut and paste job from Guillaume's article.

I did add two methods. The first:

Amount.metaClass.static.valueOf = { Number number, String unit -> Amount.valueOf(number, Unit.valueOf(unit)) }


allows you to create an Amount using a Number and a String, e.g. Amount.valueOf(1.5, "cm").

The other new method:

Amount.metaClass.to = { String unit -> delegate.to(Unit.valueOf(unit)) }

allows conversions with the unit specified as a String, e.g. 1.5.m.to("cm")

The final enhancement I added was to create a addUnitSupport(clazz) method. This method overrides the setProperty() method of the passed class to support setting Amount properties as strings. All assignments in the following scenario are valid:

class Foo {
Amount bar
}

// test
UnitDSL.enable()
UnitDSL.addUnitSupport(Foo)

def foo = new Foo()
foo.bar = Amount.valueOf(3, SI.METER)
foo.bar = Amount.valueOf(3, "m")
foo.bar = Amount.valueOf("3m")
foo.bar = 3.m
foo.bar = "3m"


To use this code, you'll have to grab the latest JScience release.

6 comments:

Anonymous said...

Is a public enable() method really necessary? It seems like addUnitSupport() could take care of that for you (or possibly just enable as part of a static block in the UnitsDSL class).

Josh Reed said...

Thanks for the comment. I think the enable() method is just a matter of taste. It actually modifies the metaclass on Number and Amount, which is 90% of the DSL. addUnitSupport() is the other 10% by adding support for assigning strings to Amount properties on a class. If you didn't do it until addUnitSupport() was called, then you wouldn't be able to do '3.m' anywhere until you had added unit support to a specific class. A static block would work as well if you knew you were always going to need the unit manipulation functionality. I just am hesitant about changing runtime behavior without some explicit invocation when I know I won't always need the extra functionality.

Anonymous said...

Yourcodeisunfortunatelynotthread-safe.

Betweentheifstatementtochecktheenablementandthesettingofthevariableanotherthreadcouldrun...

Josh Reed said...

Your comment would be more useful had you used spaces.

The code does not need to be thread safe. The enable() method can run multiple times without issue. Each time it simply overwrites the previous metaclass methods with the new version. The isEnabled flag just tries to save you useless cycles. But you are correct in your assessment that under some conditions the enable() method may run more than once.

If you're worried about it, check out the other two comments on the post for other options.

Mars said...

aare you in antartida?

Josh Reed said...

Not anymore. I arrived home in December.