Flexible, statically-checked experimental library for units of measurement.
Published under MIT License.
Available at Maven Central:
libraryDependencies += "io.github.karols" %% "units" % "0.2.0"
Reasons for creation and design goals.
Main features:
static, compile-time checking of units of measure
ability to define custom units of measure
unit arithmetic
automatic unit conversions
many out-of-the-box supported units and their conversions
support for affine spaces
bindings for several Scala and Java libraries
A quick comparison with the units of measure support in F# is here.
importio.github.karols.units._importio.github.karols.units.defining._typeUSD=DefineUnit[_U~:_S~:_D] typeEUR=DefineUnit[_E~:_U~:_R] implicitvalEUR_to_USD= one[EUR].contains(1.25)[USD]
importio.github.karols.units.SI._importio.github.karols.units.USCustomary._valpriceInUSA=200.of[USD/square[foot]]
valpriceInGermany=1500.of[EUR/square[metre]]
valarea=200.of[centimetre] *550.of[centimetre]
valcostInUSA= priceInUSA * area.convert[foot × foot]
valcostInGermany= priceInGermany * area.convert[metre × metre]
println(s"You can buy tiles in Germany for ${costInGermany.mkString}.")
println(s"You can buy tiles in USA for ${costInUSA.mkString}.")
if(costInUSA >~ costInGermany) {
println("Buy in Germany.")
} else {
println("Buy in USA.")
}prints:
You can buy tiles in Germany for 16500.0 EUR.
You can buy tiles in USA for 23680.602916761392 USD.
Buy in Germany.
You define a unit using DefineUnit type constructor with a type-level string as a parameter.
importio.github.karols.units._importio.github.karols.units.defining._typemetre=DefineUnit[_m]
typesecond=DefineUnit[_s]
typekilogram=DefineUnit[ _k ~: _g ]All units are subtypes of trait MUnit. This includes the type _1, which represents the dimensionless unit 1.
This also automatically generates implicit names for those units: "m", "s", and "kg" respectively.
You can define a derived unit with operators × and / (the ASCII alternative for × is ><) and type-level functions square and cube:
typenewton= (metre × kilogram) / (second × second)
typehertz= _1 / second
typem2= square[metre]
typem3= cube[metre]with implicit names "kg m s^(-2)", "s^(-1)", "m^2", and "m^3" respectively.
You can define a related unit with conversion ratio:
typecentimetre=DefineUnit[_c~:_m]
typekilometre=DefineUnit[_k~:_m]
implicitvalkm_to_m= one[kilometre].contains(1000)[metre]
implicitvalm_to_cm= one[metre].contains(100)[centimetre]This way, you have defined conversions m→km, km→m, m→cm, cm→m, m²→km², m³→km³, km²→m² etc.
Note that this does not define a conversion from kilometres to centimetres. You can do it this way:
implicitvalkm_to_cm= km_to_m >> m_to_cmYou can also quickly generate conversions for kg×m→kg×cm, m/s→cm/s, J/m→J/cm, N/m²→N/cm²:
implicitvalkgm_to_kgcm= m_to_cm.times[kilogram]
implicitvalmps_to_cmps= m_to_cm.dividedBy[second]
implicitvalJpm_to_Jpcm= m_to_cm.dividing[joule]
implicitvalNpm2_to_Npcm2= m_to_cm.pow2.dividing[newton]See sources for io.github.karols.units.SI and io.github.karols.units.USCustomary objects for more examples.
All code below assumes the following is imported:
importio.github.karols.units._You can create a value with a unit:
vallength=2.of[metre]This value is of type IntU[metre]. If you used a double literal, you would receive a DoubleU instance:
vallength2=2.0.of[metre]IntU and DoubleU are represented at runtime as an unboxed Long and Double respectively.
You can add and subtract values with the same units:
length + length2 // equals 4 mYou can multiply or divide two values:
length * length2 // equals 4 m^2You can also compare values with the same units:
valarea=3.of[metre × metre]
area > length * length2 //equals falseIf you want to extract the raw dimensionless numeric value from the value with a unit, you can use value method:
length.value // equals 2: LongOther useful methods include raising to powers and getting roots:
length.pow2 // 4 m^2
length.pow3 // 8 m^3
area.sqrt // 1.7... m27.of[cube[metre]].cbrt // 3.0 mNote that while using sqrt (respectively: cbrt) method will currently work for types with units to odd (respectively: not divisible by three) powers, but the resulting value will have some ill-defined type.
If you want to use an SI prefix without creating a separate unit for it, you can use more extension methods defined on numeric types:
20.kilo[metre] // equals 20000 m3.milli[second] // equals 0.003 sFinally, to print a value with a unit, you can use mkString method:
length.mkString // equals "4 m"300.of[metre/second].mkString // equals "300 m s^(-1)"It is currently not recommended though, it takes a lot of time to compile and generates awfully large classfiles. The recommended way is to use value method and append a unit symbol manually.
Section about using custom numeric types with units has been moved here.
You can convert a value to another unit, provided there is an implicit conversion ratio in scope:
vallength=2.of[metre]
length.convert[centimetre] // equals 200.0 cmIf you are converting an integer value from a unit to its subunit, you can get the results as an IntU:
length.convertToInt[centimetre] // equals 200 cmYou can apply the conversion ratio to a more complicated unit in order to replace one unit with another:
length.represent[metre, centimetre] // equals 200.0 cm
area.represent[metre, centimetre] // equals 300.0 cm×m1.represent[metre, centimetre] // equals 100.0 cm/mFinally, you can apply the conversion to all occurrences of a unit inside another. The only restriction is that the unit you are converting from is a basic unit, defined with DefineUnit:
area.representAll[metre, centimetre] // equals 30000.0 cm^21.representAll[metre, centimetre] // equals 1.02.of[metre/second].representAll[metre, centimetre] // equals 200.0 cm/s2.of[square[metre]/second].representAll[metre, centimetre] // equals 20000.0 cm^2/s30000000.of[kilogram/cube[metre]].representAll[metre, centimetre] // equals 30.0 kg/cm^3Units are automatically converted in comparisons when using operators ending with a tilde:
399.of[centimetre] >=~4.of[metre] // falseIf there is an integer conversion ratio between two units, then adding/subtracting two values with those units converts the sum/difference to the smaller unit:
14.of[centimetre] +3.of[metre] // equals 314 cm, not 3.14 mAffine space is a space of quantities which cannot be multiplied or added, only subtracted. The reason for that is that the zero is chosen arbitrarily. Affine spaces are used to represent temperatures, timestamps, Cartesian coordinates, potential energy, electric potential, and more. See this article for more info.
Example:
valfreezeC=0.at[CelsiusScale] // temperature at which water freezes0.of[celsius_deg] // zero difference of temperaturesAffine values have types DoubleA[A] and IntA[A], where A is an affine space.
The reason that you can't add affine values is that this operation makes no sense.
valfreezeF= freezeC.convert[FahrenheitScale] // equals 32°F
freezeC + freezeC // 0°C + 0°C == does not compile
freezeF + freezeF // 32°F + 32°F == does not compileIn the above case, if we assumed naïvely that we can add temperatures, the first sum would end up being 0°C, and the second one would be 64°F – clearly two different results.
You can add/subtract normal values to/from affine values:
valtemperatureIncreaseC=5.of[celsius_deg] // equals +5°CvaltemperatureIncreaseF= temperatureIncreaseC.convert[fahrenheit_deg] // equals +9°F
freezeC + temperatureIncreaseC // equals 5°C
freezeF + temperatureIncreaseF // equals 41°F == 5°C
freezeC - temperatureIncreaseC // equals -5°C
freezeF - temperatureIncreaseF // equals 23°F == -5°CYou can also calculate a difference between two affine values, which yields a normal value:
valboilC=100.at[CelsiusScale] // equals 100°C == 212°FvaldiffC= boilC -- freezeC // equals +100°C == +180°FThe operator name is double minus sign, because single minus sign was used for subtracting a normal value and Scala compiler cannot overload that method due to type erasure for value classes.
AffineSpace is defined as a pair containing a zero point and a unit.
importio.github.karols.units._importio.github.karols.units.defining._typecelsius_deg=DefineUnit[_deg~:_C]
sealedtraitCelsiusZerotypeCelsiusScale=DefineAffineSpace[CelsiusZero, celsius_deg]Unlike normal values, affine spaces currently require conversion functions in both directions.
implicitvalfromCelsiusToFahrenheit= convertAffineSpace[CelsiusScale,FahrenheitScale]{ x => x * (9/5.0) +32// (Double => Double)
}
implicitvalfromFahrenheitToCelsius= convertAffineSpace[FahrenheitScale,CelsiusScale]{ x => (x -32) * (5/9.0)
}If two affine spaces only differ by their unit, you can use changeUnit method to convert between them:
sealedtraitUnixEpochtypeUnixEpochSeconds=DefineAffineSpace[UnixEpoch, second]
typeUnixEpochMillis=DefineAffineSpace[UnixEpoch, millisecond]
valtimestamp=123456789.at[UnixEpochSeconds] // equals 123456789 s from Unix Epoch
timestamp.changeUnit[millisecond] // equals 123456789000 ms from Unix EpochAffine values can be compared with relational operators, similarly to normal values.
This section has been moved to a separate document.
This section has been moved to a separate document.
This section has been moved to a separate document.
This section has been moved to a separate document.
The implementation started with a pretty common type-level implementation of boolean and integers.
Type-level strings are either a cons of a char and a string, a single char, or an empty string. Each char is implemented currently as a triple of members of Z/5Z, so only 125 different characters are allowed.
A basic unit is implemented as a thin wrapper around a type-level string. That string is an identifier that is used for type equality comparison, and is also used for implicit name generation.
The main type for units (MUnit) is a linked list of pairs of basic units and non-zero integers, used to represent a type-level map.