Skip to main content
measured

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 Measures. 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 Measures 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
caution

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") }

Contact

  • Please see issues to share bugs you find, make feature requests, or just get help with your questions.
  • Don't hesitate to ⭐️ star if you find this project useful.