Units of measure in Kotlin
This project covers historical, fantasy, or whimsical units: Metric is uninteresting except that being based on base 10, it is not representable by binary computers (the French revolutionaries overlooked that). USD is provided as a practical example.
The project is a demonstration of the power (and limits) of generics in
Kotlin and in writing a clean DSL: see
Main.kt
.
It is also fun.
Try ./run
for a demonstration.
The build is vanilla Maven, with Batect offered as a means to reproduce locally what CI does.
$ ./mvnw clean verify
$ ./run
# Or:
$ ./batect build
$ ./batect demo
Test coverage is 100% for lines, branches, and instructions.
This library depends on
kotlin-rational
for
representing big rationals.
Presently there is no published dependency for kotlin-rational
.
To build KUnits, install locally from the kotlin-rational-2.2.0
tag.
This code targets JDK 17.
- From
Int
s:120.lines
- From
Long
s:300L.drams
- From
FixedBigRational
s:(12_345 over 4).seconds
- Idempotency:
+m1
- Negation:
-m1
- Addition:
4.dollars + 33.cents
- Subtraction:
(m1 into Hands) - m1
- Multiplication:
m2 * 4
- Division:
m2 / 4
- Between units of the same kind within a system:
m3 into Ounces
, or as shorthand,m1 / Barleycorns
- Into multiple other units of the same kind within a system:
m4.into(DollarCoins, HalfDollars, Quarters, Dimes, Nickels, Pennies)
, or as shorthand,m4 % looseChange
- Between units of the same kind between different systems:
1.smoots intoEnglish Inches
- Default formatting:
"${220.yards} IN $English IS ${220.yards intoFFF Furlongs} IN $FFF"
- Custom formatting:
"- $it (${it.format()})"
Kind
represents a kind of units (eg,Length
)System
represents a system of units (eg,English
)Units
represents units of measure (eg,MetasyntacticLengths
)Measure
represents quantities of units (eg,m1
)
Included for Measure
are the usual simple arithmetic operations.
The exemplar of quirkiness is traditional English units:
Unreal systems of units for testing:
Below is the source for the Martian system of units showing the minimal code needed for setting up a system of units:
// Define the Martian system of units, a singleton
object Martian : System<Martian>("Martian")
class Grok private constructor(value: FixedBigRational) :
// Grok is a measure of length in the Martian system
Measure<Length, Martian, Groks, Grok>(Groks, value) {
// Groks are units measured as multiples of one grok
companion object Groks : Units<Length, Martian, Groks, Grok>(
Length, Martian, "grok", ONE
) {
override fun new(quantity: FixedBigRational) = Grok(quantity)
override fun format(quantity: FixedBigRational) = "$quantity groks"
}
}
// Factory extension properties for creating some quantity of groks
val FixedBigRational.groks get() = Groks.new(this)
val Long.groks get() = (this over 1).groks
val Int.groks get() = (this over 1).groks
For convenience, systems of units may provide conversions into other systems:
infix fun <
V : Units<Length, Metasyntactic, V, N>,
N : Measure<Length, Metasyntactic, V, N>,
>
// Specialize converting Martian units of length to Metasyntactic ones
// Elsewhere, define the reflexive `intoMartian` to reverse the conversion
Measure<Length, Martian, *, *>.intoMetasyntactic(other: V) =
into(other) {
it * (1 over 3)
}
Typically, the base type for units of measure (MartialLengths
, above) is
sealed
as there is a known, fixed number of units.
However,
OtherDnDDenominations
is an example of extending a kind of units.
Also, see
ShoeSizes
for an
example of creating new kinds of units.
Generic signatures pervade types and function signatures. The standard ordering is:
K
"kind" — is this length, weight, etc.S
"system" ‐ is this English units, etc.U
"unit" ‐ what unit is this?M
"measure" ‐ how many units?
Syntactic sugar causes cancer of the semicolon.
— Alan J. Perlis
There are too many options for "nice" Kotlin syntactic sugar. The most "natural English" approach might be:
2.feet in Inches // *not* valid Kotlin
However, this is a compilation failure as the "in" needs to be "`in`" since
in
is a keyword in Kotlin.
Another might be:
2.feet to Inches
However, this overloads the universal to
function for creating Pair
s.
Or consider:
2.feet as Inches
Unfortunately, as
is an existing keyword for type casting.
The chosen compromise is an infix
into
function,
and a more general version for conversions into unit units of the same
kind in another system.
2.feet into Inches
Though infix functions do not chain nicely:
2.feet into Inches shouldBe 24.inches // what you expect
2.feet shouldBe 24.inches into Feet // does not compile
More readable might be:
(2.feet into Inches) shouldBe 24.inches // parentheses for readability
2.feet shouldBe (24.inches into Feet) // parentheses needed to compile
2.feet / Inches shouldBe 24.inches // operator binds more tightly than infix
2.feet shouldBe 24.inches / Feet // correct, but harder to read
And parentheses are required for correct binding order in some cases:
24.inches shouldBe (2.feet into Inches)
One may skip syntactic sugar altogether:
Feet(2).into(Inches)
At the cost of losing some pleasantness of Kotlin.
The trivial extension properties for converting Int
, Long
, and
FixedBigRational
into units could be inline
(as well as several others).
However, JaCoCo's Kotlin inline functions are not marked as
covered lowers test coverage,
and Kover's Feature request: Equivalent Maven
plugin does not support
Maven.
Following The Rules,
inline
is removed for now, until JaCoCo resolves this issue.
Incompatible unit conversions are inconsistent. The two cases are:
- Converting between units of different kinds (say, lengths and weights) in the same system of units
- Converting between units of the same kind (say, lengths) but in different systems of units
Behavior:
- Operations between incompatible units do not compile. This is by design. For example, you cannot convert feet into pounds.
// Does not compile: feet and pounds are different kinds of units
1.feet into Pounds
// Does not compile: both are lengths, but of different systems:
1.smoots into Inches
// This would both compile and run successfully:
1.smoots intoEnglish Inches