Type-safe dimensional analysis and unit conversion in Kotlin.

Overview

units-of-measure

Type-safe dimensional analysis and unit conversion in Kotlin.

demo image


Project Status

Stable and safe for production.

Building UOMs for the first time is quite slow on the latest Kotlin compiler versions (1.3.70+). To improve compile times, UOMs 7.0.0+ will be repackaged as an Λrrow Meta plugin.

Usage

Take a look at the project website for installation and usage: http://units.kunalsheth.info

You can also take a look at this sample project for a complete gradle setup and to learn about some of the more advanced features.


More sample code:

val mass1 = 3.kilo(Gram)
val mass2 = 14.Ounce
val sum = mass1 + mass2
// mass1 + 3.Days // will not compile

assert(sum in 7.5.Pound `±` 1.Ounce)
assert(sum in 3.3.kilo(Gram)..7.5.Pound) // this works too
// assert(sum in 7.4.Kilowatts..7.5.Pounds) // will not compile

val ratio = 2.Foot / 1.Metre
assert(ratio in 60.Percent `±` 5.Percent)
assert(ratio.Percent.toInt() in 55..65)


assert(1.kilo(Gram) == 1000.Gram)
assert(10.milli(Metre) == 1.centi(Metre))
assert(60000.milli(Second) == 1.Minute)


assert(420.Degree % 1.Turn in 60.Degree `±` 1.Degree)


val speed = 65.Mile / Hour
val time = 27.Minute
val distance = speed * time
val aBitFaster = distance / (time - 30.Second)

assert(distance == time * speed)
assert(distance in 29.Mile..30.Mile)
assert(distance in 30.Mile..29.Mile) // this works too
assert(aBitFaster in speed..(speed + 4.kilo(Metre) / Hour))


val threshold = 0.001.Foot / Second / Second
sequenceOf(0, 1, 4, 9, 16, 25).map { it.Foot }
        .derivative(::p)
        .derivative(::p)
        .zipWithNext { a, b -> a in b `±` threshold }
        .forEach { assert(it) }
// support for generic programming
fun <Q : Quan<Q>, DQDT : Quan<DQDT>> Sequence<Q>.derivative(p: (Q, `÷`, T) -> DQDT) = timeSeq()
        .zip(this)
        .zipWithNext { (x1, y1), (x2, y2) -> p(
                (y1 - y2), `÷`, (x1 - x2)
        ) }

Background

Type-safe dimensional analysis and unit conversion can be extremely beneficial to a team. From personal experience, using type-safe calculations result in:

  • Faster Development — IDE autocomplete provides meaningful predictions, rather than just listing every number in scope.
  • Cleaner Code — Variable names will be of a reasonable length now that unit information is documented by the type.
  • Higher Confidence — All unit/dimension related bugs will show up at compile time. Debugging is less difficult and time-consuming.

units-of-measure's novel, metaprogramming approach to the problem makes it:

  1. Incredibly Extendable — Adding new functionality is as simple as adding a line to your build file. No tedious "hand-coding" is required.
  2. Small — You only generate what you need. You are not forced to bundle every conceivable unit, quantity, and dimension with your app.
  3. Bug Resistant — Programming by hand is error prone and time-consuming. Code generation can ensure correctness.

Todo List

  • Make it work.
  • Generate implicit relationships as well.
  • Make annotations easier to write and manage.
  • Add support for unit conversions.
  • Add docs. (http://units.kunalsheth.info)
  • Add metric prefixes.
  • Multiplatform.
  • Stronger support for generic use (Quantity<This, IntegralOfThis, DerivativeOfThis>)
  • * and ÷ singleton types for even safer proof-passing.
  • Publish on Gradle Plugin Portal.
  • Document serialization functionality.
  • Optimize for faster compilation and runtime.
  • Benchmark performance hit in contrast to primitives. (Can someone help me with this?)
Comments
  • Can this be used on android?

    Can this be used on android?

    This looks like a very fascinating project, and I would like to try it out in an android app that I have to do a lot of unit conversion in. The configuration on the wiki, unfortunately, does not work for android.

    I get

    Could not get unknown property 'sourceSets' for root project

    Is it possible to adjust the config to work on android?

    opened by bj0 24
  • kilo syntax

    kilo syntax

    In the demo there is code: val mass = 3.kilo(Gram), but When I try to do this, I get the following compile error:

    Unresolved reference. None of the following candidates is applicable because of receiver type mismatch: public val M /* = L0M1T0I0Theta0N0J0 /.Gram: Double defined in info.kunalsheth.units.generated public val Number.Gram: M / = L0M1T0I0Theta0N0J0 */ defined in info.kunalsheth.units.generated

    I'm not sure why it can't pick between those 2 extension methods.

    same for: val speed = 65.Mile / Hour

    It cannot figure out Hour

    This is in a unit test in a kotlin/jvm library

    opened by bj0 6
  • Proof Passing 2.0

    Proof Passing 2.0

    “Proof passing” allows dimensional relationships to be used as bounds for UOM type parameters by exploiting Kotlin’s type inference and method overloading capabilities.

    In this PR, I make proof passing even safer by forcing the correct operation (*, /) to be selected, eliminating chance of deliberate human error.

    In the past, it was technically possible that, given an inadequately bounded set of type parameters, division and multiplication operations may be interchanged when passed via method reference into a higher-order function. Though there have been no reports of this happening in practice.

    opened by kunalsheth 2
  • uom module fails first build with: Declaration annotated with '@OptionalExpectation' can only be used in common module sources

    uom module fails first build with: Declaration annotated with '@OptionalExpectation' can only be used in common module sources

    Since converting to a multiproject module, I am getting the following error when trying to build from a clean directory (./gradlew clean):

    Declaration annotated with '@OptionalExpectation' can only be used in common module sources

    I believe this is caused by the @JsName annotations.

    It only happens on the first invocation of ./gradlew build, if I re-run ./gradlew build a second time everything compiles and runs fine.

    This isn't a problem for development/testing on my workstation, but it is a problem for an automated build server. Have you seen this issue or have any ideas?

    My module's build.gradle is:

    apply plugin: 'org.jetbrains.kotlin.multiplatform'
    apply plugin: 'info.kunalsheth.units.gradle'
    
    repositories.mavenCentral()
    
    kotlin {
        jvm()
    
        sourceSets {
            commonMain {
                kotlin.srcDirs += generateUnitsOfMeasure.generatedSrcDir
                dependencies { implementation kotlin('stdlib-common') }
            }
            jvmMain {
                dependsOn commonMain
                dependencies {
                    implementation kotlin('stdlib')
                }
            }
            jvmTest {
                dependencies {
                    implementation kotlin('test')
                    implementation kotlin('test-junit')
                    implementation "com.google.truth:truth:0.45"
                }
            }
            all {
                languageSettings.enableLanguageFeature('InlineClasses')
            }
        }
        targets.all {
            compilations.all { compileKotlinTask.dependsOn(generateUnitsOfMeasure) }
        }
    }
    
    // workaround for https://youtrack.jetbrains.com/issue/KT-27170
    configurations {
        compileClasspath
    }
    
    apply from: 'units-of-measure.gradle'
    
    
    opened by bj0 1
  • The number of seconds in a minute is not fixed

    The number of seconds in a minute is not fixed

    https://github.com/kunalsheth/units-of-measure/blob/63cc2baad35384d8a3bdf3dbbf29e6082683a9d1/demo/src/main/kotlin/info/kunalsheth/units/sample/Sample.kt#L25

    A minute can be 61 or 59 seconds. Just thought you should know. https://en.wikipedia.org/wiki/Minute

    opened by ragmondo 1
  • Question about angles and radians

    Question about angles and radians

    Hello there!

    This is a cool library and I hope to see it developed more or at least have more community support on it!

    I had a question about angles and radians within the library, and maybe I'm overthinking this. I have a function that accepts an angle, but internally the calculations are done in radians. I'm seeing if I could convert my code to use your library.

    How would you write a function that does something like this into using your library? I've already got it compiled and working otherwise.

    fun doMath(degrees: Double) {
        val radians = degrees * (Math.PI / 180.0)
        // Math that uses radians later
    }
    

    I was having a hard time figuring out what types I'd need to use from the library to replace the Double for the angle. It's assumed to be degrees currently.

    opened by evan-swinney 0
  • Generated code not compiling

    Generated code not compiling

    This library looks super cool and I wanted to try it out, but am coming across a problem in the generated code, where the compiler is complaining that '@JvmName' annotation is not applicable to this declaration.

    There are too many errors to list all of them, but they are all of a similar nature. An example line where the compiler is complaining is:

    @JvmName("L0A0M0T0I0Theta0N0J0_Divide_L0A0M0T0I0Theta0N0J0_generic")
    operator fun `Dimensionless`.div(that: Quan<`Dimensionless`>) = `Dimensionless`(this.siValue / that.siValue)
    

    Setup

    src/main/kotlin/Main.kt

    import info.kunalsheth.units.generated.*
    
    fun main() {
        println(1.Metre)
    }
    

    build.gradle

    plugins {
        id 'org.jetbrains.kotlin.jvm' version '1.4.21'
        id 'info.kunalsheth.units' version '6.0.2'
    }
    
    repositories {
        mavenCentral()
    }
    
    sourceSets {
        main {
            java {
                srcDirs 'src/main/kotlin'
                srcDirs 'build/uom'
            }
        }
    }
    
    opened by veyndan 2
  • Celsius, Fahrenheit, and Conversion.

    Celsius, Fahrenheit, and Conversion.

    I believe the issue of converting between Kelvin and Celsius/Fahrenheit is a perfectly solvable one, and I offer the overall approach I used to handle this:

    // Static Properties of a Quantity
    // e.g.
    // object Velocity : Quantifiable<Velocity> { 
    //     override dimension = Dimension(L = 1, T = -1)
    //     override val baseUnit = Meter/Second
    // }
    interface Quantifiable<Q: Quantifiable<Q>> {
        val dimension: Dimension
        val baseUnit: BaseUnit<Q>
    }
    
    // An instance of a Quantifiable
    // e.g. 11.Kelvin == Quantity(11, Kelvin)
    // calculates amount in the base unit of the quantifiable for binary operations involving other quantities of the same quantifiable. e.g. 12.Kelvin + 25.Celsius == 12.Kelvin + (25.Celsius convertTo Kelvin)
    class Quantity<Q : Quantifiable<Q>>(val amount: Double, val unit: MeasureUnit<Q> = baseUnit) : Quantifiable<Q> by unit.quantifiable {
        val baseAmount: Double by lazy { unit.convertToBase(amount) }
        operator fun compareTo(other: Quantity<Q>): Int = baseAmount.compareTo(other.baseAmount)
        operator fun plus(other: Quantity<Q>): Quantity<Q> = unit.convertFromBase(other.baseAmount + baseAmount)
        ...
    }
    
    // The units themselves hold the functions for converting to/from base unit.
    // In this way, Celsius is 'defined' as Kelvin - 273.15, as seen below.
    interface MeasureUnit<Q : Quantifiable<Q>> { // e.g. Kelvin
        val quantifiable: Q
        fun convertToBase(amount: Double): Double
        fun convertFromBase(baseAmount: Double): Double
    }
    
    // The base unit doesn't need to convert amounts to base. it's already base.
    abstract class BaseUnit<Q : Quantifiable<Q>>(override val quantifiable: Q) : MeasureUnit<Q : Quantifiable<Q>> {
        override fun convertToBase(amount: Double) = amount
        override fun convertFromBase(baseAmount: Double) = baseAmount
    }
    
    // The following instances can be val instead of object. They were object for me because they were members of a sealed class
    object Temperature : Quantifiable<Temperature> {
        override val dimension = Dimension(Θ = 1)
        override val baseUnit = Kelvin
    }
    
    object Kelvin : BaseUnit<Temperature> by BaseUnit(Temperature)
    object Celsius : MeasureUnit<Temperature> {
        override val quantifiable = Temperature
        override fun convertToBase(amount: Double) = amount + 273.15
        override fun convertFromBase(baseAmount: Double) = amount - 273.15
    }
    
    enhancement 
    opened by Hullaballoonatic 1
  • IntelliJ can't seem to find the UOM code (i.e. everything is red)

    IntelliJ can't seem to find the UOM code (i.e. everything is red)

    Make sure your Gradle project is properly imported into IntelliJ:

    1. IntelliJ Menu Bar: View > Tool Windows > Gradle
    2. Gradle Tool Window: Sync
    3. Gradle Tool Window: [project name] > Tasks > unitsofmeasure > generateUnitsOfMeasure
    4. Gradle Tool Window: Sync
    5. open the generated UnitsOfMeasure.kt file in build/uoms directory of your units-of-measure Gradle module.
    6. wait for IntelliJ to finish lagging
    7. restart IntelliJ if necessary
    opened by kunalsheth 0
  • How do I define Celsius and Fahrenheit?

    How do I define Celsius and Fahrenheit?

    UPDATE: DO NOT DO THIS. units-of-measure is incompatible with Celsius and Fahrenheit. Attempting to define Celsius and Fahrenheit will cause weird and incorrect behavior.

    Celsius and Fahrenheit would have to be manually programmed in on the user's end. Celsius and Fahrenheit aren't "units" in the theoretical sense because they have different zeros.

    ~To add Celsius and Fahrenheit, your code would look like the following:~

    inline val Number.Celsius get() = `Θ`(toDouble() + 273.15)
    inline val `Θ`.Celsius get() = siValue - 273.15
    object `Celsius` : UomConverter<`Θ`>,
            Quan<`Θ`> by box(1.Celsius) {
        override val unitName = "Celsius"
        override fun invoke(x: Double) = x.Celsius
        override fun invoke(x: `Θ`) = x.Celsius
    }
    
    inline val Number.Fahrenheit get() = `Θ`((toDouble() - 32) * 5 / 9 + 273.15)
    inline val `Θ`.Fahrenheit get() = (siValue - 273.15) * 9 / 5 + 32
    object Fahrenheit : UomConverter<`Θ`>,
            Quan<`Θ`> by box(1.Fahrenheit) {
        override val unitName = "Fahrenheit"
        override fun invoke(x: Double) = x.Fahrenheit
        override fun invoke(x: `Θ`) = x.Fahrenheit
    }
    
    opened by kunalsheth 2
Type-safe arguments for JetPack Navigation Compose using Kotlinx.Serialization

Navigation Compose Typed Compile-time type-safe arguments for JetPack Navigation Compose library. Based on KotlinX.Serialization. Major features: Comp

Kiwi.com 32 Jan 4, 2023
ShapeShift️ - A Kotlin library for intelligent object mapping and conversion between objects

ShapeShift️ A Kotlin library for intelligent object mapping and conversion between objects. Documentation Installation Maven Gradle Groovy DSL Kotlin

KRUD 127 Dec 7, 2022
AppConversorMoedas - The currency conversion using an API to bring the data up to date

LAB - Criando um app de conversor moedas/cambio com Kotlin. O curso pode ser ace

Davi Braga 0 Jan 1, 2022
Sample application to demonstrate Multi-module Clean MVVM Architecture and usage of Android Hilt, Kotlin Flow, Navigation Graph, Unit tests etc.

MoneyHeist-Chars Sample application to demonstrate Multi-module Clean MVVM Architecture and usage of Android Hilt, Kotlin Flow, Navigation Graph, Room

Hisham 20 Nov 19, 2022
Architecture With MVI using Kotlin, Coroutines, Retrofit and Unit test

Architecture With MVI using Kotlin, Coroutines, Retrofit and Unit test MVI (Model-View-Intent) streamlines the process of creating and developing appl

Ahmed Karam 4 Aug 18, 2022
A Kotlin multiplatform unit testing library inspired by / similar to Google Truth.

Truthish A testing API inspired by Google Truth but rewritten in Kotlin from the ground up, so it can be used in Kotlin multiplatform projects. For ex

Varabyte 70 Nov 2, 2022
It is far easier to design a class to be thread-safe than to retrofit it for thread safety later

"It is far easier to design a class to be thread-safe than to retrofit it for thread safety later." (Brian Goetz - Java concurrency: Publisher: Addiso

Nguyễn Trường Thịnh 3 Oct 26, 2022
A pluggable sealed API result type for modeling Retrofit responses.

A pluggable sealed API result type for modeling Retrofit responses.

Slack 645 Dec 19, 2022
Basic app to use different type of observables StateFlow, Flow, SharedFlow, LiveData, State, Channel...

stateflow-flow-sharedflow-livedata Basic app to use different type of observables StateFlow, Flow, SharedFlow, LiveData, State, Channel... StateFlow,

Raheem 5 Dec 21, 2022
Create an application with Kotlin/JVM and Kotlin/JS, and explore features around code sharing, serialization, server- and client

Practical Kotlin Multiplatform on the Web 본 저장소는 코틀린 멀티플랫폼 기반 웹 프로그래밍 워크숍(강좌)을 위해 작성된 템플릿 프로젝트가 있는 곳입니다. 워크숍 과정에서 코틀린 멀티플랫폼을 기반으로 프론트엔드(front-end)는 Ko

SpringRunner 14 Nov 5, 2022
Create an application with Kotlin/JVM and Kotlin/JS, and explore features around code sharing, serialization, server- and client

Building a Full Stack Web App with Kotlin Multiplatform 본 저장소는 INFCON 2022에서 코틀린 멀티플랫폼 기반 웹 프로그래밍 핸즈온랩을 위해 작성된 템플릿 프로젝트가 있는 곳입니다. 핸즈온 과정에서 코틀린 멀티플랫폼을

Arawn Park 19 Sep 8, 2022
Run Kotlin/JS libraries in Kotlin/JVM and Kotlin/Native programs

Zipline This library streamlines using Kotlin/JS libraries from Kotlin/JVM and Kotlin/Native programs. It makes it possible to do continuous deploymen

Cash App 1.5k Dec 30, 2022
Real life Kotlin Multiplatform project with an iOS application developed in Swift with SwiftUI, an Android application developed in Kotlin with Jetpack Compose and a backed in Kotlin hosted on AppEngine.

Conferences4Hall Real life Kotlin Multiplatform project with an iOS application developed in Swift with SwiftUI, an Android application developed in K

Gérard Paligot 98 Dec 15, 2022
A somewhat copy past of Jetbrain's code from the kotlin plugin repo to make it humanly possible to test Intellij IDEA kotlin plugins that work on kotlin

A somewhat copy past of Jetbrain's code from the kotlin plugin repo to make it humanly possible to test Intellij IDEA kotlin plugins that work on kotlin

common sense OSS 0 Jan 20, 2022
A tiny Kotlin multiplatform library that assists in saving and restoring objects to and from disk using kotlinx.coroutines, kotlinx.serialisation and okio

Store A tiny Kotlin multiplatform library that assists in saving and restoring objects to and from disk using kotlinx.coroutines, kotlinx.serialisatio

Isuru Rajapakse 98 Jan 3, 2023
Bego Chat is chat application in Kotlin and Firebase with the following features: last seen , user status like typing ,online and last seen with MVVM pattern and clean architecture

Compose ChatApp(Bego Chat) Bego Chat is Compose chat application in Kotlin and Firebase with the following features: sending all file types and abilit

Ahmed EL Bagory 5 Dec 20, 2022
LifecycleMvp 1.2 0.0 Kotlin is MVP architecture implementation with Android Architecture Components and Kotlin language features

MinSDK 14+ Download Gradle Add to project level build.gradle allprojects { repositories { ... maven { url 'https://jitpack.io' }

Robert 20 Nov 9, 2021
Opinionated Redux-like implementation backed by Kotlin Coroutines and Kotlin Multiplatform Mobile

CoRed CoRed is Redux-like implementation that maintains the benefits of Redux's core idea without the boilerplate. No more action types, action creato

Kittinun Vantasin 28 Dec 10, 2022
👋 A common toolkit (utils) ⚒️ built to help you further reduce Kotlin boilerplate code and improve development efficiency. Do you think 'kotlin-stdlib' or 'android-ktx' is not sweet enough? You need this! 🍭

Toolkit [ ?? Work in progress ⛏ ?? ??️ ?? ] Snapshot version: repositories { maven("https://s01.oss.sonatype.org/content/repositories/snapshots") }

凛 35 Jul 23, 2022