Libra is a dimensional analysis library based on shapeless, spire and singleton-ops. It contains out of the box support for SI units for all numeric types.

To Use

Libra supports JDK 8, scala versions 2.11.11 and 2.12.4.

Add this to your build.sbt:

libraryDependencies += "com.github.to-ithaca" %% "libra" % "0.3.0"
scalaOrganization := "org.typelevel"
scalacOptions += "-Yliteral-types"

This adds Typelevel Scala.

TL;DR

Example usage:

import spire.implicits._
// import spire.implicits._

import libra._, libra.si._
// import libra._
// import libra.si._

(3.m + 2.m).show
// res0: String = 5 m [L]

(3.m * 2.m).show
// res1: String = 6 m^2 [L^2]

(1.0.km.to[Metre] + 2.0.m + 3.0.mm.to[Metre]).show
// res2: String = 1002.003 m [L]

(3.0.s.to[Millisecond] / 3.0.ms).show
// res3: String = 1000.0  []

3.m + 2.kg // This should fail
// <console>:22: error: These quantities can't be added!
// Most likely they have different dimensions.  If not, make sure that there's an implicit AdditiveSemigroup in scope.
// Left: libra.Quantity[Int,libra.Term[libra.si.package.Length,libra.si.package.Metre,libra.Fraction[Int(1),Int(1)]] :: shapeless.HNil]
// Right: libra.Quantity[Int,libra.Term[libra.si.package.Mass,libra.si.package.Kilogram,libra.Fraction[Int(1),Int(1)]] :: shapeless.HNil]
//        3.m + 2.kg // This should fail
//            ^

Why?

When we deal with numeric quantities, we often resort to Int, Double or Float types. These are incommunicative and error prone.

val distance = 3.0 // 3 m
// distance: Double = 3.0

val time = 2.0 // 2 s
// time: Double = 2.0

val speed = distance + time // Oh no!
// speed: Double = 5.0

There’s a mistake in our formula, but we won’t know without a decent set of tests.

Libra provides a Quantity which wraps base numeric types. It supports compile time dimensional analysis.

import spire.implicits._
// import spire.implicits._

import libra._, libra.si._
// import libra._
// import libra.si._

val distance = 3.0.m
// distance: libra.QuantityOf[Double,libra.si.package.Length,libra.si.package.Metre] = Quantity(3.0)

val time = 2.0.s
// time: libra.QuantityOf[Double,libra.si.package.Time,libra.si.package.Second] = Quantity(2.0)

distance + time
// <console>:33: error: These quantities can't be added!
// Most likely they have different dimensions.  If not, make sure that there's an implicit AdditiveSemigroup in scope.
// Left: libra.Quantity[Double,libra.Term[libra.si.package.Length,libra.si.package.Metre,libra.Fraction[Int(1),Int(1)]] :: shapeless.HNil]
// Right: libra.Quantity[Double,libra.Term[libra.si.package.Time,libra.si.package.Second,libra.Fraction[Int(1),Int(1)]] :: shapeless.HNil]
//        distance + time
//                 ^

(distance / time).show // Yay!
// res6: String = 1.5 m s^-1 [L T^-1]

Do I have to use Typelevel Scala?

No, althout it does make the syntax nicer. Libra uses Typelevel Scala for literal types in type position. This hasn’t been added to Lightbend Scala yet. You can use shapeless witnesses to get the same behaviour. Take a look at the code snippets in this example project.

Credits

Libra makes heavy use of Typelevel Scala, shapeless, spire and singleton-ops. It wouldn’t be possible without these projects, their authors and contributors, so if you like Libra, please check them out.

Libra also uses:

Alternatives

If Libra isn’t quite your cup of tea, definitely take a look at Squants. It’s also a great library for dimensional analysis, with a slightly different scope.