Skylark

Measure

skylark-measure is a library dealing with unit-of-measure definition, arithmetic, simplification and conversions in a type-safe manner.

Configuration

libraryDependencies += "com.quantarray" %% "skylark-measure" % "0.17.0"

Simple usage

Many units of measure are defined for you.

  import com.quantarray.skylark.measure.implicits._
  import com.quantarray.skylark.measure.measures._

  kg
  lb
  Pa
  Hz

  // Special unit of measure representing a dimensionless, unitless measure
  Unit

Easily compose another unit of measure as stand-alone entities or in relation to an already-defined one.

  import com.quantarray.skylark.measure.implicits._
  import com.quantarray.skylark.measure.measures._

  // On a distant planet Gayana, new units of measure exist
  case object GA extends SystemOfUnits
  val flog = LengthMeasure("flog", GA)
  val kflog: LengthMeasure = Kilo * flog

Any unit of measure will have a set of basic properties that you would naturally expect to interrogate.

  import com.quantarray.skylark.measure.implicits._
  import com.quantarray.skylark.measure.measures._

  kg.name should be("kg")
  kg.dimension should be(Mass)
  kg.system should be(SI)
  kg.isStructuralAtom should be(true)
  kg.exponent should be(1.0)
  kg * s should be(ProductMeasure(kg, s))
  kg.inverse should be(ExponentialMeasure(kg, -1.0))
  kg to kg should be(Some(1))
  kg to lb should be(Some(2.204625))
  kg to g should be(Some(1000))
  kg to cg should be(Some(100000))
  kg to oz_metric should be(None) // Default conversion is not guaranteed to exist

You can compose more complex ones by multiplying, dividing, and exponentiating.

  import com.quantarray.skylark.measure.implicits._
  import com.quantarray.skylark.measure.measures._

  val N = kg * m / (sec ^ 2)

You can find our the conversion factor from one to another. No conversion factor may exist.

  import com.quantarray.skylark.measure.implicits._
  import com.quantarray.skylark.measure.measures._
  import org.scalatest.OptionValues._

  (kg to lb).value should be(2.204625)

When dealing with marshalling/encoding/serialization, you can store units of measure along with a numeric value as a plain string. With AnyMeasureParsers you can turn that string back into a measure.

  import com.quantarray.skylark.measure.implicits._
  import com.quantarray.skylark.measure.measures._
  import org.scalatest.OptionValues._

  val parser = AnyMeasureParsers(USD, bbl) // Declare the measure atom instances you expect to be present
  parser.parse("USD / bbl").value should equal(USD / bbl)

It's easy to compose numerical quantities with units of measure using a dot or postfix syntax or via full instantiation.

  import com.quantarray.skylark.measure.implicits._
  import com.quantarray.skylark.measure.quantities._

  val mass = 10.kg
  val length = 4 m
  val volume = 1000.0.bbl

  val pressure = Quantity(30, Pa)

You can perform the expected arithmetic operations on quantities.

  10.kg * 4.m should equal(40.0 * (kg * m))
  (4.oz_troy * 7.percent).to(oz_troy).value should equal(0.28.oz_troy)

  10.kg / 2.m should equal(5.0 * (kg / m))
  (10.USD / 2.percent).to(USD).value should equal(500 USD)

  (10.kg + 3.kg) should equal(Some(13 kg))
  (10.kg - 3.kg) should equal(Some(7 kg))
  (10.kg + 3.lb) should equal(Some(11.360775642116007 kg))
  (10.lb - 3.kg) should equal(Some(3.386125 lb))

  10.kg + (3.lb to kg) should equal(Some(11.360775642116007.kg))
  10.kg - (3.lb to kg) should equal(Some(8.639224357883993.kg))

Quantity conversions are supported via the same to operator. Basic converters are pre-defined. Conversions for product, ratio, and exponential measures are defined by converters and require their own CanConvert instances of their components' conversions.

  import com.quantarray.skylark.measure.implicits._
  import com.quantarray.skylark.measure.quantities._
  import org.scalatest.OptionValues._

  (1.ft to in).value should equal(12.0 in)
  (12.in to ft).value should equal(1.0 ft)

Ambiguity in choosing the right conversion factor due to a substance or its properties (e.g. specific gravity) can be resolved by importing right implicits.

  import com.quantarray.skylark.measure.implicits._
  import com.quantarray.skylark.measure.quantities._
  import org.scalatest.OptionValues._

  // Some general substance, like water
  (1.bbl to gal).value should equal(31.5.gal)

  import com.quantarray.skylark.measure.conversion.commodity.default._

  // Specific petroleum substance, having a special conversion
  (1.bbl to gal).value should equal(42.gal)

Typed Measure vs. untyped AnyMeasure

skylark-measure gives you the freedom and flexibility to work with measures of a defined dimension (e.g. MassMeasure) or AnyMeasure - a measure whose dimension can only be known at run time. The choice of which to work with depends on the individual API you would like to expose and enforce.

In the situation where you know you must receive a MassMeasure, you would encode exactly as natural logic or physics would dictate. There is, hence, no chance someone can pass a quantity in units of LuminousFluxMeasure, for example, where a quantity in units of MassMeasure is expected (unless one finds compiler errors aesthetically pleasing).

  type VelocityMeasure = RatioMeasure[LengthMeasure, TimeMeasure]

  type MomentumMeasure = ProductMeasure[MassMeasure, VelocityMeasure]

  type Mass = Quantity[Double, MassMeasure]

  type Velocity = Quantity[Double, VelocityMeasure]

  type Momentum = Quantity[Double, MomentumMeasure]

  def momentum(mass: Mass, velocity: Velocity): Momentum = mass * velocity

In other situations, where knowledge of a measure's dimension is uncertain, one would rely on AnyMeasure. Operations on AnyMeasure yields another AnyMeasure and thus have less strict type requirements than a type derived from Measure. You can always match on AnyMeasure to check or assert a certain shape.

Overriding default behavior

The default arithmetic, conversion, and simplification operations are defined for both dimensional measure types and AnyMeasure.

Arithmetic

skylark-measure relies on the presence of implicit type classes CanMultiplyMeasure, CanDivideMeasure, and CanExponentiateMeasure to perform arithmetic operations.

By default

  • m1 * m2 returns ProductMeasure(m1, m2);
  • m1 / m2 returns RatioMeasure(m1, m2);
  • m ^ n return ExponentialMeasure(m, n).

One can, however, override the return type by proving a custom implicit class that derives from one of the three Can*Measure traits.

For example, say when one does b / s (bits per second), one wants to work with a custom BitRateMeasure instead of the default RatioMeasure(bit, s).

One would then need to define the custom object like InformationTimeCanDivide:

  object custom extends com.quantarray.skylark.measure.arithmetic.SafeArithmeticImplicits
  {

    implicit object InformationTimeCanDivide extends CanDivide[InformationMeasure, TimeMeasure, BitRateMeasure]
    {
      override def divide(numerator: InformationMeasure, denominator: TimeMeasure): BitRateMeasure = BitRateMeasure(numerator, denominator)
    }

  }

Conversion

skylark-measure relies on the presence of implicit type class CanConvert and helper Converter type to convert between measures of different type.

  object VolumeToExponentialLengthConverter extends Converter[VolumeMeasure, ExponentialLengthMeasure]
  {
    import com.quantarray.skylark.measure.measures._

    override def apply(from: VolumeMeasure, to: ExponentialLengthMeasure): Option[Double] = Conversion(from, to) match
    {
      case `bbl` ⤇ `gal` => Some(42.0)
    }
  }

  trait BaseCommodityConversionImplicits
  {

    implicit val volumeToExponentialLengthCanConvert = new CanConvert[VolumeMeasure, ExponentialLengthMeasure]
    {
      override def convert: Converter[VolumeMeasure, ExponentialLengthMeasure] = VolumeToExponentialLengthConverter
    }

  }

Simplification

  type EnergyPriceTimesFXMeasure = ProductMeasure[EnergyPrice, FX]

  implicit val canSimplifyEnergyPriceTimesCurrencyPrice = new CanSimplifyMeasure[EnergyPriceTimesFXMeasure, Option[EnergyPrice]]
  {
    override def simplify(inflated: EnergyPriceTimesFXMeasure): Option[EnergyPrice] =
    {
      if (inflated.multiplicand.numerator == inflated.multiplier.denominator)
      {
        Some(RatioMeasure(inflated.multiplier.numerator, inflated.multiplicand.denominator))
      }
      else
      {
        None
      }
    }
  }

Package organization

Types

// Types
import com.quantarray.skylark.measure._

Measures

// Typed dimensional measures
import com.quantarray.skylark.measure.measures._
// Untyped AnyMeasure(s) (use these if it's more convenient to match on the measure's shape at run time)
import com.quantarray.skylark.measure.any.measures._

Quantities

// Typed dimensional quantities
import com.quantarray.skylark.measure.quantities._
// Quantity[N, AnyMeasure] (use these if it's more convenient to match on the quantity measure's shape at run time)
import com.quantarray.skylark.measure.any.quantities._

Arithmetic

// Typed safe arithmetic (no exceptions thrown)
import com.quantarray.skylark.measure.arithmetic.safe._

// Untyped safe arithmetic
import com.quantarray.skylark.measure.any.arithmetic.safe._

Simplification

// Typed default simplification
import com.quantarray.skylark.measure.simplification.default._

// Untyped default simplification
import com.quantarray.skylark.measure.any.simplification.default._

Conversion

// Typed default conversion
import com.quantarray.skylark.measure.conversion.default._

// Untyped default conversion
import com.quantarray.skylark.measure.any.conversion.default._

Arithmetic, simplification, and conversion all at once

// Typed Safe arithmetic, default simplification, and default conversion
import com.quantarray.skylark.measure.implicits._

// Untyped safe arithmetic, default simplification, and default conversion
import com.quantarray.skylark.measure.any.implicits._