KMeasure is a purely Kotlin, multiplatform, (almost!) entirely compile-time units library with very minimal runtime overhead.
val distance = 6.kilo.meters
val time = 0.2.seconds
val volume = 1.gallons
val time: Time = 1.seconds // OK
val distance: Distance = 1.seconds // Compile Error! (Type mismatch: inferred type is Time but Distance was expected)
1.hours + 30.minutes == 90.minutes // true
val secondsInTwoMins = 2.minutes.inUnit(seconds) // 120.0
val degreesPerRadian = 1.radians.inUnit(degrees) // 57.29577951308232
val velocity: Velocity = 3.miles / 1.hours // (same as 3.ofUnit(miles/hours))
val work: Energy = 200.grams * velocity / 2.seconds * 10.feet
val volume: Volume = PI * (2.centi.meters * 2.centi.meters) * 1.deci.meters
val times: List<Time> = listOf(6.minutes, 10.minutes, 7.minutes, 9.minutes)
val averageSpeed: Velocity =
times
.map { 1.miles / it }
.average()
Make sure mavenCentral
is added as a repository somewhere in your build.gradle
:
repositories {
mavenCentral()
}
Then add KMeasure to your dependencies:
dependencies {
implementation("io.github.battery-staple:KMeasure:1.4.1")
}
All KMeasure values with a unit (like instances of Distance
or Time
) are represented
under the hood as a Quantity
:
value class Quantity<D : Dimension<*, *, *>>(val siValue: QuantityNumber) : Comparable<Quantity<D>> { /* body omitted */ }
As a value class, Quantities are represented at runtime simply as Double
s (with some exceptions: see docs).
This means that the following two snippets are identical at runtime and have therefore identical performance:
val time = 1.0.seconds + 5.0.seconds
val timeSeconds = 1.0 + 5.0
Quantity
also defines various operators, such as plus
, minus
, compareTo
, etc. to allow seamless arithmetic.
This isn't enough to mark the distinctions between types, however; that's what the type parameter D
,
of type Dimension
, is for.
A dimension is a type of unit, like distance, power, volume, pressure, et cetera.
All dimensions can be written as a product of six base dimensions:
mass, length, time, current, temperature, and luminous intensity.
Units can include these base dimensions multiple times: for example,
area is length squared (
(See here for a more complete explanation of dimensions.)
The vast majority of useful units can be represented with only the first four, so KMeasure only supports combinations of mass, length, time, and current for now.
Thus, Dimension
takes three type parameters:
class Dimension<M : BaseMassDimension, L : BaseLengthDimension, T : BaseTimeDimension> private constructor()
Each of the base dimensions has a set of subtypes. For example, here's BaseMassDimension
:
sealed interface BaseMassDimension
sealed interface MassN2 : BaseMassDimension
sealed interface MassN1 : BaseMassDimension
sealed interface Mass0 : BaseMassDimension
sealed interface Mass1 : BaseMassDimension
sealed interface Mass2 : BaseMassDimension
Each of these subtypes represents a "power" of mass. For example, Mass2
represents mass squared (N
represent negative powers—kotlin doesn't allow negative signs as part of identifiers.
MassN1
therefore represents the reciprocal of mass, or
With the three type parameters of Dimension
together, any combination of these base
dimensions—and therefore any1 dimension—can be represented.
Take force from before, which was Dimension<Mass1, Length1, TimeN2>
Now, the compiler will automatically distinguish between different dimensions, and won't allow operations (such as addition, subtraction, etc.) between Quantities with different Dimensions.
KMeasure also provides many typealiases so that you don't have to type out that long declaration for dimensions. Here are a few examples:
typealias MassDimension = Dimension<Mass1, Length0, Time0>
typealias MomentumDimension = Dimension<Mass1, Length1, TimeN1>
typealias ForceDimension = Dimension<Mass1, Length1, TimeN2>
typealias PressureDimension = Dimension<Mass1, LengthN1, TimeN2>
typealias EnergyDimension = Dimension<Mass1, Length2, TimeN2>
// etc...
Typealiases are also provided for Quantities so that you don't even have to write out Quantity<MassDimension>
;
you can just write Mass
. For example:
typealias Mass = Quantity<MassDimension>
typealias Momentum = Quantity<MomentumDimension>
typealias Force = Quantity<ForceDimension>
typealias Pressure = Quantity<PressureDimension>
typealias Energy = Quantity<EnergyDimension>
// etc...
At this point, only one thing is left: multiplication and division. These operations are different from addition and subtraction in that you can multiply and divide any unit by any other unit, and it'll return yet a different unit. For example, distance divided by time gives a velocity.
Unfortunately, there is no way to allow the Kotlin compiler to automatically determine what unit a multiplication or division should return. Instead, a function has to be defined for each possible multiplication and division—every possible combination of two units. Needless to say, this is a lot of functions, so they're autogenerated using a script.
Since each combination of every two dimensions needs to have two functions,
one for multiplication and one for division, the number of multiplication/division functions is
Thus, only certain dimensions are supported. Those that are supported were chosen to include all commonly-used mechanical units, so issues arising from this are likely to be rare. Certain very complex expressions may run into errors, but can usually be fixed with rearrangement through parenthesis.
Semantically, Scalar(1.0)
and 1.0
represent the same dimensionless value of 1.0.
In typical usage, it is recommended to use Double
s to represent dimensionless values,
but it is sometimes necessary to use Scalar
s, such as when a type is needed to implement
the Quantity<D>
interface. This may occur with classes or functions that take
an arbitrary unit as a generic type parameter.
Footnotes
-
You may have noticed that powers of Mass ony go up to 2 and down to -2; this was chosen deliberately as no commonly used units involve mass squared or anything of the like. More powers could be added, but at the cost of significantly increased code size—see Limitations. ↩