Typesafe units of measure in Kotlin.
This project covers historical, fantasy, or whimsical units: Metric units are boring except that being based on base 10, they are not representable by binary computers (the French revolutionaries overlooked that). And this project is fun.
USD currency
is provided as a practical example of base 10 units.
English units are a good example of conversions and rational fractions.
This library shows that typing and generics improve the experience for others
in any domain—focusing on units of measure—but the lesson is
general when providing code for others.
See
Main.kt for examples that caller does not code
for generics.
The "cow mug" for the project does not represent anything unless you're Scottish which I am not. Still ... "ku-nits" is a bit like "coo-nits": fleas on cows12. Working with less common units is like fleas on cows: managed and to avoid unless necessary. It is not so cavalier when uncommon units of measure are central. Hopefully this project demonstrates a sensible, idiomatic means for unit conversions, and helps you manage irritations, and learn more about Kotlin generics. 🐮
After cloning the project, try ./run for a demonstration.
The build is vanilla Maven, and includes a ./mvnw (wrapper)
script.
$ ./mvnw clean verify
$ ./run # a demo
# Or:
$ earthly +build
$ earthly +run # a demo
Note
You will need an OWASP API key exported to your environment as
OWASP_NVD_API_KEY.
This is for running security checks on dependencies as part of the build.
Alternatively, use the -Dowasp.skip=true flag to ./mvn; there is no
equivalent for skipping these checks for the Earthly build.
Test coverage is 100% for lines, branches, and instructions. Checkout CI builds to see what happens.
- D&D — currency denominations
- Discworld —
- Ankh-Morpork
- Lancre
- English — currency denominations, lengths, times, volumes of wine, weights
- FFF — areas, lengths, times, weights
- MIT — lengths
- USD — currenncy denominations
Kunits depends on an older version of kotlin-rational to represent
"big" rationals (infinite precision fractions limited only by your computing
environment).
All example unit conversions in this project are "small" precision (ratios).
Conversions among units relies on rational (finite) ratios.
Presently there is no published dependency for kotlin-rational (a project of
this author).
To build KUnits, install locally from the
kotlin-rational-2.2.0
tag.
This code targets JDK 21.
- From
Ints:120.lines - From
Longs:300L.drams - From
FixedBigRationals:(12_345 over 4).seconds
There are also aliases for some units such as
1.twopence is identical to 1.tuppence
and with an alias for "tu'penny".
- 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:
m5.into(DollarCoins, HalfDollars, Quarters, Dimes, Nickels, Pennies), or as shorthand,m5 % usLooseChange - 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()})"
Kindrepresents a kind of units (eg,Length)Systemrepresents a system of units (eg,English)Unitsrepresents units of measure (eg,MetasyntacticLengths)Measurerepresents quantities of units (eg,m1)
Included for Measure are the usual simple arithmetic operations.
The exemplar of quirkiness is traditional English units:
- English units of denomination (money)
- English units of length
- English units of time
- English units of volume
- English units of weight
See also the English denominations for an example custom formatting function,
formatTraditional() (eg, "4/2/4" for 4 pounds, 2 shillings, and 4 pence).
Among the challenges with the English (British) systems of units is that coinages available in historic periods do not always align with expression of value. For example, the crown is a coin worth 5 shillings, however, it is notated as "5s" (5 shillings) rather as number of crowns as it was simply a coin, not a basis in the notation of value. The same is true for many or most historic coinage systems though the English (British) system is most prominent.
An example of a historic English coin not represented is the gold penny (20 pence in its time).
[NOTE!] No attempt is made to distinguish English and British systems of measurements. The intermingled history of the British Isles is complex, and coinage changed dramatically in place and time (such as UK decimalisation in 1971). A complete system would provide a location/date-dependent calendar of coinage which is beyond the scope of this project. I do the best I can; suggestions welcome.
[NOTE!] Further, values of several coins changed over time, and coins were issued with the same value as earlier coins while being used alongside each other (changing value and availability of silver and gold; changes in rulership issuing coins; etc), and England/Britain/UK redominated several times.
This project usually uses a latter value for coins, or the most used value of coinage, based on Internet reading; I am no historian or numismatist, but I enjoy the challenge of representing the mishmash of this coinage in software. Repeating: I do the best I can; suggestions welcome.
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:
object Martian : System<Martian>("Martian")
infix fun <
V : Units<Length, Metasyntactic, V, N>,
N : Measure<Length, Metasyntactic, V, N>
> Measure<Length, Martian, *, *>.intoMetasyntactic(
other: V
) = into(other) {
it * (1 over 3)
}
class Grok private constructor(value: FixedBigRational) :
Measure<Length, Martian, Groks, Grok>(Groks, value) {
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"
}
}
val FixedBigRational.groks get() = Groks.new(this)
val Long.groks get() = (this over 1).groks
val Int.groks get() = (this over 1).groksFor convenience, systems of units may provide conversions into other systems:
infix fun <
V : Units<Length, Martian, V, N>,
N : Measure<Length, Martian, V, N>
> MetasyntacticLength<*, *>.intoMartian(
other: V
) = into(other) {
it * (3 over 1)
}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
ShoeSize 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. This library uses math/bit operators when sensible, and backs off where it conflicts with the existing Kotlin standard library.
See Operators.kt.
Simple math operators with Measure arithmetic (possily with conversion of
right-hand sides to the units of the left):
+a— idempotency-a— negationa + b— additiona - b— subtractiona * b— multiplicationa / b— divisiona % b— modulo -- an exact modulus including remainders using a largest-to-smallest ("greedy") approach
The most "natural English" approach might be:
2.feet in Inches // *not* valid KotlinHowever, 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 InchesHowever, this overloads the standard library to function for creating Pairs
(very much needed when declaring maps).
Or consider:
2.feet as InchesUnfortunately, 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 InchesThough 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 compileMore 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 readAnd 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- 10 Little-Known Units of Time
- Avoirdupois system
- British Denominations
- Carolingian monetary system
- Chart showing the relationships of distance measures
- Currency for Ankh-Morpork
- English units
- English Weights & Measures
- FFF system
- _Florin (English coin)
- Great Recoinage of 1816
- Hogshead
- How Quids, Bobs, Florins, Tanners, and Joeys Got Their Names
- Imperial units
- Less common coinage — D&D
- List of British banknotes and coins
- Medieval money
- metasyntactic variable
- Physikal
- Smoot
- Understanding old British money - pounds, shillings and pence
- Units of Measurement - API
- Units & Systems of Units
