Intuitive, Type-safe Units
Measured provides a safe and simple way to work with units of measure. It uses the compiler to ensure correctness, and provides intuitive, mathematical operations to work with any units.
This means you can write more robust code that avoids implicit units. Time handling for example, is often done with implicit assumptions about milliseconds vs microseconds or seconds. Measured helps you avoid pitfalls like these.
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
import io.nacular.measured.units.Time.Companion.milliseconds
//sampleStart
interface Clock {
fun now(): Measure<Time>
}
fun handleUpdate(duration: Measure<Time>) {
// ...
reportTimeInMillis(duration `in` milliseconds)
}
fun update(clock: Clock) {
val startTime = clock.now()
//...
handleUpdate(clock.now() - startTime)
}
fun reportTimeInMillis(time: Double) {}
//sampleEnd
Complex units
Measured makes working with complex units easy. Simply use division and multiplication to create compound Measure
s. Convert between these safely and easily with the as
and in
methods.
import io.nacular.measured.units.Length.Companion.kilometers
import io.nacular.measured.units.Length.Companion.meters
import io.nacular.measured.units.Length.Companion.miles
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
import io.nacular.measured.units.Time.Companion.hours
import io.nacular.measured.units.Time.Companion.milliseconds
import io.nacular.measured.units.Time.Companion.minutes
import io.nacular.measured.units.Time.Companion.seconds
import io.nacular.measured.units.div
import io.nacular.measured.units.times
fun complexUnits() {
//sampleStart
val velocity = 5 * meters / seconds
val acceleration = 9 * meters / (seconds * seconds)
val time = 1 * minutes
// d = vt + ½at²
val distance = velocity * time + 1.0 / 2 * acceleration * time * time
println(distance ) // 16500 m
println(distance `as` kilometers) // 16.5 km
println(distance `as` miles ) // 10.25262467191601 mi
println(5 * miles / hours `as` meters / seconds) // 2.2352 m/s
//sampleEnd
}
The as
method converts a Measure
from its current Units
to another (of the same type). The result is another Measure
. While in
returns the magnitude of a Measure
in the given Units
.
Avoid raw values
Measure's support of math operators helps you avoid working with raw values directly.
import io.nacular.measured.units.Length
import io.nacular.measured.units.Length.Companion.kilometers
import io.nacular.measured.units.Length.Companion.meters
import io.nacular.measured.units.Length.Companion.miles
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
import io.nacular.measured.units.Time.Companion.hours
import io.nacular.measured.units.Time.Companion.milliseconds
import io.nacular.measured.units.Time.Companion.minutes
import io.nacular.measured.units.Time.Companion.seconds
import io.nacular.measured.units.UnitsRatio
import io.nacular.measured.units.Velocity
import io.nacular.measured.units.div
import io.nacular.measured.units.times
//sampleStart
val marathon = 26 * miles
val velocity = 3 * kilometers / hours
val timeToRunHalfMarathon = (marathon / 2) / velocity // 6.973824 hr
fun calculateTime(distance: Measure<Length>, velocity: Measure<Velocity>): Measure<Time> {
return distance / velocity
}
//sampleEnd
Extensible
You can easily add new conversions to existing units and they will work as expected.
import io.nacular.measured.units.Length
import io.nacular.measured.units.Length.Companion.meters
import io.nacular.measured.units.Length.Companion.miles
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time.Companion.hours
import io.nacular.measured.units.Time.Companion.seconds
import io.nacular.measured.units.Velocity
import io.nacular.measured.units.div
import io.nacular.measured.units.times
fun hands() {
//sampleStart
val hands = Length("hands", 0.1016) // define new Length unit
val l1 = 5 * hands
val l2 = l1 `as` meters // convert to Measure with new unit
val v: Measure<Velocity> = 100_000 * hands / hours
println("$l1 == $l2 or ${l1 `in` meters}") // 5.0 hands == 0.508 m or 0.508
println(v `as` hands / seconds) // 27.77777777777778 hands/s
println(v `as` miles / hours ) // 6.313131313131313 mi/hr
//sampleEnd
}
You can also define entirely new units with a set of conversions and have them interact with other units.
import Blits.Companion.blat
import Blits.Companion.blick
import Blits.Companion.bloop
import io.nacular.measured.units.InverseUnits
import io.nacular.measured.units.Length
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
import io.nacular.measured.units.Time.Companion.minutes
import io.nacular.measured.units.Time.Companion.seconds
import io.nacular.measured.units.Units
import io.nacular.measured.units.UnitsProduct
import io.nacular.measured.units.UnitsRatio
import io.nacular.measured.units.div
import io.nacular.measured.units.times
//sampleStart
// Define a custom Units type
class Blits(suffix: String, ratio: Double = 1.0): Units(suffix, ratio) {
operator fun div(other: Blits) = ratio / other.ratio
companion object {
// Various conversions
val bloop = Blits("bp" ) // the base unit
val blick = Blits("bk", 10.0)
val blat = Blits("cbt", 100.0)
}
}
// Some typealiases to help with readability
typealias BlitVelocity = UnitsRatio<Blits, Time>
typealias BlitAcceleration = UnitsRatio<Blits, UnitsProduct<Time, Time>>
val m1: Measure<BlitAcceleration> = 5 * blat / (seconds * seconds)
val m2: Measure<BlitVelocity> = m1 * 10 * minutes
val m3: Measure<InverseUnits<Time>> = m2 / (5 * blick)
//sampleEnd
Current Limitations
Measured uses Kotlin's type system to enable compile-time validation. This works really well in most cases, but there are things the type system currently does not support. For example, Units
and Measure
s are order-sensitive.
import io.nacular.measured.units.Angle
import io.nacular.measured.units.Angle.Companion.radians
import io.nacular.measured.units.Length
import io.nacular.measured.units.Length.Companion.kilometers
import io.nacular.measured.units.Length.Companion.meters
import io.nacular.measured.units.Length.Companion.miles
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
import io.nacular.measured.units.Time.Companion.hours
import io.nacular.measured.units.Time.Companion.milliseconds
import io.nacular.measured.units.Time.Companion.minutes
import io.nacular.measured.units.Time.Companion.seconds
import io.nacular.measured.units.UnitsProduct
import io.nacular.measured.units.UnitsRatio
import io.nacular.measured.units.Velocity
import io.nacular.measured.units.div
import io.nacular.measured.units.times
//sampleStart
val a: UnitsProduct<Angle, Time> = radians * seconds
val b: UnitsProduct<Time, Angle> = seconds * radians
//sampleEnd
Notice the types for a
and b
are different
This can be mitigated on a case by case basis with explicit extension functions that help with order. For example, you can ensure that kilograms
is sorted before meters
by providing the following extension.
import io.nacular.measured.units.Length
import io.nacular.measured.units.Length.Companion.meters
import io.nacular.measured.units.Mass
import io.nacular.measured.units.Mass.Companion.kilograms
import io.nacular.measured.units.Time.Companion.seconds
import io.nacular.measured.units.div
import io.nacular.measured.units.times
//sampleStart
// ensure Mass comes before Length when Length * Mass
operator fun Length.times(mass: Mass) = mass * this
val f1 = 1 * (kilograms * meters) / (seconds * seconds)
val f2 = 1 * (meters * kilograms) / (seconds * seconds)
// f1 and f2 now have the same type
//sampleEnd
You can also define an extension on Measure to avoid needing parentheses around kilograms and meters.
import io.nacular.measured.units.Length
import io.nacular.measured.units.Length.Companion.meters
import io.nacular.measured.units.Mass
import io.nacular.measured.units.Mass.Companion.kilograms
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time.Companion.seconds
import io.nacular.measured.units.div
import io.nacular.measured.units.times
//sampleStart
// ensure Mass comes before Length when Measure<Length> multiplied by Mass
operator fun Measure<Length>.times(mass: Mass) = amount * (units * mass)
//sampleEnd
Installation
Measured is a Kotlin Multi-platform library that targets a wide range of platforms. Simply add a dependency to your app's Gradle build file as follows to start using it.
repositories {
mavenCentral()
}
dependencies {
implementation("io.nacular.measured:measured:$VERSION")
}