All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.io.nacular.measured.units.Units.kt Maven / Gradle / Ivy

There is a newer version: 0.4.1
Show newest version
package io.nacular.measured.units

import kotlin.jvm.JvmName
import kotlin.math.roundToInt

/**
 * Base class for all types that can represent a unit of measure.
 * A Units type can have multiple "members", each being some fraction of the base
 * unit for that type. Time for example, might have seconds as the base unit
 * and minute as a unit that is 60 times the base unit. This allows for a
 * set of different representations of the unit.
 *
 * @constructor
 * @param suffix to use when printing the unit in a human readable way
 * @param ratio of this unit relative to the base-unit
 */
abstract class Units(val suffix: String, val ratio: Double = 1.0) {
    /**
     * Whether there should be a space between the unit's name and the magnitude
     * of a value with that unit. Most units are displayed like this: 45 kg. But
     * some, like degrees are done w/o the space: 45°
     */
    protected open val spaceBetweenMagnitude = true

    internal fun measureSuffix() = if (spaceBetweenMagnitude) " $suffix" else suffix

    override fun toString() = suffix

    override fun hashCode(): Int {
        var result = suffix.hashCode()
        result = 31 * result + ratio.hashCode()
        result = 31 * result + spaceBetweenMagnitude.hashCode()
        return result
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Units) return false

        if (suffix != other.suffix) return false
        if (ratio  != other.ratio ) return false
        if (spaceBetweenMagnitude != other.spaceBetweenMagnitude) return false

        return true
    }
}

/**
 * Represents the product of two Units: A * B.
 *
 * @constructor
 * @param first unit being multiplied
 * @param second unit being multiplied
 */
class UnitsProduct(val first: A, val second: B): Units(if (first==second) "($first)^2" else "$first$second", first.ratio * second.ratio)

typealias Square = UnitsProduct

/**
 * Represents the ratio of two Units: A / B.
 *
 * @constructor
 * @param numerator unit being divided
 * @param denominator unit dividing numerator
 */
class UnitsRatio(val numerator: A, val denominator: B): Units("$numerator/$denominator", numerator.ratio / denominator.ratio) {
    /** The Inverse of this unit. */
    val reciprocal by lazy { UnitsRatio(denominator, numerator) }
}

/**
 * The inverse of a given Units.
 *
 * @constructor
 * @param unit this is the inverse of
 */
class InverseUnits(val unit: T): Units("1/${unit.suffix}", 1 / unit.ratio)

/**
 * Compares two units
 * @param other unit to compare
 * @return -1 if this unit is smaller, 1 if the other is smaller, and 0 if they are equal
 */
operator fun  A.compareTo(other: B): Int = ratio.compareTo(other.ratio)

/**
 * @return the smaller of the two Units
 */
fun  minOf(first: A, second: B) = if (first < second) first else second


/**
 * A quantity with a unit type
 */
class Measure(val amount: Double, val units: T): Comparable> {
    /**
     * Convert this type into another compatible type.
     * Type must share parent
     * (eg Mile into Kilometer, because they both are made from Distance)
     */
    infix fun  `as`(other: A): Measure = if (units == other) this else Measure(this `in` other, other)

    infix fun  `in`(other: A): Double = if (units == other) amount else  amount * (units.ratio / other.ratio)

    /**
     * Add another compatible quantity to this one
     */
    operator fun plus(other: Measure): Measure = minOf(units, other.units).let { Measure((this `in` it) + (other `in` it), it) }

    /**
     * Subtract a compatible quantity from this one
     */
    operator fun minus(other: Measure): Measure = minOf(units, other.units).let { Measure((this `in` it) - (other `in` it), it) }

    operator fun unaryMinus(): Measure = Measure(-amount, units)

    /**
     * Multiply this by a scalar value, used for things like "double this distance",
     * "1.5 times the speed", etc
     */
    operator fun times(other: Number): Measure = amount * other.toDouble() * units

    /**
     * Divide this by a scalar, used for things like "halve the speed"
     */
    operator fun div(other: Number): Measure = amount / other.toDouble() * units

    fun roundToInt(): Measure = amount.roundToInt() * units

    /**
     * Compare this value with another quantity - which must have the same type
     * Units are converted before comparison
     */
    override fun compareTo(other: Measure): Int = (this `as` other.units).amount.compareTo(other.amount)

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Measure<*>) return false
//        if (this.amount == 0.0 && other.amount == 0.0) return true TODO: Should this be true?

        val resultUnit = minOf(units, (other as Measure).units)

        val a = this  `in` resultUnit
        val b = other `in` resultUnit

        return a == b
    }

    override fun hashCode(): Int {
        return (amount * units.ratio).hashCode()
    }

    override fun toString(): String = "$amount${units.measureSuffix()}"
}

// region ================ Units * Units Math ============================

/** A * B              */                    operator fun  A.                                                    times(other: B               ): UnitsProduct                                 = UnitsProduct(this, other)
                          @JvmName("times7") operator fun            A.                                                    times(other: InverseUnits ): Double                                             = ratio / other.ratio
/** A * (1 / B)        */                    operator fun  A.                                                    times(other: InverseUnits ): UnitsRatio                                   = this / other.unit
/** A * (B / A)        */                    operator fun  A.                                                    times(other: UnitsRatio): Measure                                         = ratio / other.denominator.ratio * other.numerator
/** (A / B) * B        */ @JvmName("times1") operator fun                      UnitsRatio.                 times(other: B               ): Measure                                         = other.ratio / denominator.ratio * numerator
/** (A / (B * C)) * B  */ @JvmName("times2") operator fun            UnitsRatio>.times(other: B               ): Measure>                          = other.ratio / denominator.first.ratio * (numerator / denominator.second)
/** (A / (B * C)) * D) */ @JvmName("times3") operator fun  UnitsRatio>.times(other: D               ): UnitsRatio, UnitsProduct> = numerator * other / denominator
/** (A / B) * (A / B)  */ @JvmName("times4") operator fun                      UnitsRatio.                 times(other: UnitsRatio): UnitsRatio, UnitsProduct> = numerator * other.numerator / (denominator * other.denominator)
/** (A / B) * (B / A)) */ @JvmName("times5") operator fun                      UnitsRatio.                 times(other: UnitsRatio): Double                                             = numerator * other.numerator / (denominator * other.denominator)
/** (A / B) * (C / D)) */ @JvmName("times6") operator fun  UnitsRatio.                 times(other: UnitsRatio): UnitsRatio, UnitsProduct> = numerator * other.numerator / (denominator * other.denominator)
                          @JvmName("times8") operator fun            InverseUnits.                                      times(other: A               ): Double                                             = other.ratio / ratio
                          @JvmName("times9") operator fun  InverseUnits.                                      times(other: B               ): UnitsRatio                                   = other * this
// FIXME                                            operator fun            UnitsProduct.                         times(other: InverseUnits ): Measure                                         = units * other
// FIXME                                            operator fun            UnitsProduct.                         times(other: InverseUnits ): A                                                  = units * other
// FIXME                                            operator fun  UnitsProduct.                         times(other: UnitsRatio): UnitsRatio                                   = units * other
// FIXME                                            operator fun  UnitsProduct.                         times(other: UnitsRatio): UnitsRatio                                   = units * other
// FIXME                                            operator fun  UnitsProduct.                         times(other: UnitsRatio): UnitsRatio                                   = units * other
// FIXME                                            operator fun  UnitsProduct.                         times(other: UnitsRatio): UnitsRatio                                   = units * other
// FIXME                                            operator fun            UnitsProduct.                         times(other: UnitsRatio): UnitsRatio                                   = units * other
// FIXME                                            operator fun            UnitsProduct.                         times(other: UnitsRatio): UnitsRatio                                   = units * other

// endregion

// region ================ Units / Units Math ============================

// This cannot be defined given the next definition unfortunately
//operator fun  A.div(other: A) = this.ratio / other.ratio
/** A / B */                              operator fun  A.div  (other: B               ): UnitsRatio   = UnitsRatio(this, other)
/** A / (A / B) == A * (B / A) */         operator fun  A.div  (other: UnitsRatio): Measure         = this * other.reciprocal

/** (A * B) / A */       @JvmName("div1") operator fun            UnitsProduct.div(other: A                 ): Measure                = first.ratio  / other.ratio * second
/** (A * B) / B */       @JvmName("div2") operator fun            UnitsProduct.div(other: B                 ): Measure                = second.ratio / other.ratio * first
/** (A * A) / A */       @JvmName("div3") operator fun                      UnitsProduct.div(other: A                 ): Measure                = first.ratio  / other.ratio * second
/** (A * B) / (C * B) */ @JvmName("div1") operator fun  UnitsProduct.div(other: UnitsProduct): UnitsRatio          = first  / other.first
/** (A * B) / (C * A) */ @JvmName("div2") operator fun  UnitsProduct.div(other: UnitsProduct): UnitsRatio          = second / other.first
/** (A * B) / (B * C) */ @JvmName("div3") operator fun  UnitsProduct.div(other: UnitsProduct): UnitsRatio          = first  / other.second
/** (A * B) / (A * C) */ @JvmName("div4") operator fun  UnitsProduct.div(other: UnitsProduct): UnitsRatio          = second / other.second
/** (A * B) / (A * A) */ @JvmName("div5") operator fun            UnitsProduct.div(other: UnitsProduct): UnitsRatio          = second / other.second
/** (A * B) / (A * B) */ @JvmName("div7") operator fun            UnitsProduct.div(other: UnitsProduct): Double                    = ratio  / other.ratio
/** (A * B) / (B * A) */ @JvmName("div6") operator fun            UnitsProduct.div(other: UnitsProduct): Double                    = ratio  / other.ratio
/** (A * B) / (B * B) */ @JvmName("div8") operator fun            UnitsProduct.div(other: UnitsProduct): Measure> = second.ratio / other.second.ratio * (first / other.first)

@JvmName("div1")   operator fun                      UnitsRatio.                                  div(other: UnitsRatio): Double                                             = this * other.reciprocal
@JvmName("div2")   operator fun                      UnitsRatio.                                  div(other: UnitsRatio): UnitsRatio, UnitsProduct> = this * other.reciprocal
@JvmName("div3")   operator fun  UnitsRatio.                                  div(other: UnitsRatio): UnitsRatio, UnitsProduct> = this * other.reciprocal
                   operator fun                      UnitsRatio.                                  div(other: B               ): UnitsRatio>                  = numerator / (denominator * other)
@JvmName("div1")   operator fun                      UnitsRatio, UnitsProduct>.div(other: A               ): Measure>>         = numerator.first.ratio / other.ratio * (numerator.second / denominator)
@JvmName("div2")   operator fun            UnitsRatio, UnitsProduct>.div(other: A               ): Measure>>         = numerator.first.ratio / other.ratio * (numerator.second / denominator)
@JvmName("div3")   operator fun  UnitsRatio, UnitsProduct>.div(other: A               ): Measure>>         = numerator.first.ratio / other.ratio * (numerator.second / denominator)

// m/s * (s2/m) => s
operator fun  UnitsRatio.div(other: UnitsRatio>): Measure = numerator.ratio / other.numerator.ratio * (other.denominator / denominator)

// endregion

// region ================ Measure * Measure Math ========================

@JvmName("times1") operator fun                      Measure.                                times(other: Measure               ): Measure>                                 = amount * other.amount * (units * other.units)
@JvmName("times2") operator fun                      Measure.                                times(other: Measure>): Measure                                                  = amount * other.amount * (units * other.units)
@JvmName("times7") operator fun                      Measure.                                times(other: Measure> ): Measure>                                   = amount * other.amount * (units * other.units)
@JvmName("times3") operator fun                      Measure>.                 times(other: Measure               ): Measure                                                  = amount * other.amount * (units * other.units)
@JvmName("times4") operator fun                      Measure>.                 times(other: Measure>): Measure, UnitsProduct>> = amount * other.amount * (units * other.units)
@JvmName("times5") operator fun            Measure>>.times(other: Measure               ): Measure>                                   = amount * other.amount * (units * other.units)
@JvmName("times6") operator fun  Measure>>.times(other: Measure               ): Measure, UnitsProduct>> = amount * other.amount * (units * other.units)

// endregion

// TODO: Kapt code generation possible?
operator fun  Measure.rem(other: Measure): Double = amount % other.amount * (units.ratio % other.units.ratio)

// region ================ Measure / Measure Math ========================

@JvmName("div16") operator fun                      Measure.                 div(other: Measure                 ): Double                    = amount / other.amount * (units.ratio / other.units.ratio)
@JvmName("div16") operator fun            Measure.                 div(other: Measure                 ): Measure> = amount / other.amount * (units / other.units)
@JvmName("div1" ) operator fun            Measure>.div(other: Measure                 ): Measure                = amount / other.amount * (units / other.units)
@JvmName("div2" ) operator fun            Measure>.div(other: Measure                 ): Measure                = amount / other.amount * (units / other.units)
@JvmName("div3" ) operator fun  Measure>.div(other: Measure>): Measure> = amount / other.amount * (units / other.units)
@JvmName("div4" ) operator fun  Measure>.div(other: Measure>): Measure> = amount / other.amount * (units / other.units)
@JvmName("div5" ) operator fun  Measure>.div(other: Measure>): Measure> = amount / other.amount * (units / other.units)
@JvmName("div6" ) operator fun  Measure>.div(other: Measure>): Measure> = amount / other.amount * (units / other.units)
@JvmName("div7" ) operator fun            Measure>.div(other: Measure>): Measure> = amount / other.amount * (units / other.units)
@JvmName("div8" ) operator fun            Measure>.div(other: Measure>): Measure> = amount / other.amount * (units / other.units)

@JvmName("div9" ) operator fun                      Measure>.                                  div(other: Measure                       ): Measure>>                  = amount / other.amount * (units / other.units)
@JvmName("div10") operator fun  Measure>.                                  div(other: Measure>        ): Measure, UnitsProduct>> = amount / other.amount * (units / other.units)
@JvmName("div11") operator fun                      Measure>.                                  div(other: Measure>>): Measure                                                  = amount / other.amount * (units / other.units)
@JvmName("div12") operator fun                      Measure, UnitsProduct>>.div(other: Measure                       ): Measure>>                  = amount / other.amount * (units / other.units)
@JvmName("div13") operator fun            Measure, UnitsProduct>>.div(other: Measure                       ): Measure>>                  = amount / other.amount * (units / other.units)
@JvmName("div14") operator fun  Measure, UnitsProduct>>.div(other: Measure                       ): Measure>>                  = amount / other.amount * (units / other.units)
@JvmName("div15") operator fun                      Measure.                                                 div(other: Measure>        ): Measure                                                  = amount / other.amount * (units / other.units)

// endregion

// region ================ Measure * Units Math ============================

@JvmName("times16") operator fun                      Measure>.   times(other: A               ): Double                    = amount * (units * other)
@JvmName("times16") operator fun                      Measure.                 times(other: InverseUnits ): Double                    = amount * (units * other)
@JvmName("times16") operator fun            Measure>.   times(other: B               ): Measure> = amount * (units * other)
// FIXME @JvmName("times1" ) operator fun            Measure>.times(other: InverseUnits ): Measure                = amount * (units * other)
// FIXME @JvmName("times2" ) operator fun            Measure>.times(other: InverseUnits ): Measure                = amount * (units * other)
// FIXME @JvmName("times3" ) operator fun  Measure>.times(other: UnitsRatio): Measure> = amount * (units * other)
// FIXME @JvmName("times4" ) operator fun  Measure>.times(other: UnitsRatio): Measure> = amount * (units * other)
// FIXME @JvmName("times5" ) operator fun  Measure>.times(other: UnitsRatio): Measure> = amount * (units * other)
// FIXME @JvmName("times6" ) operator fun  Measure>.times(other: UnitsRatio): Measure> = amount * (units * other)
// FIXME @JvmName("times7" ) operator fun            Measure>.times(other: UnitsRatio): Measure> = amount * (units * other)
// FIXME @JvmName("times8" ) operator fun            Measure>.times(other: UnitsRatio): Measure> = amount * (units * other)

@JvmName("times1") operator fun                      Measure.                                times(other: B               ): Measure>                                 = amount * (units * other)
@JvmName("times2") operator fun                      Measure.                                times(other: UnitsRatio): Measure                                                  = amount * (units * other)
@JvmName("times7") operator fun                      Measure.                                times(other: InverseUnits ): Measure>                                   = amount * (units * other)
@JvmName("times3") operator fun                      Measure>.                 times(other: B               ): Measure                                                  = amount * (units * other)
@JvmName("times4") operator fun                      Measure>.                 times(other: UnitsRatio): Measure, UnitsProduct>> = amount * (units * other)
@JvmName("times5") operator fun            Measure>>.times(other: B               ): Measure>                                   = amount * (units * other)
@JvmName("times6") operator fun  Measure>>.times(other: D               ): Measure, UnitsProduct>> = amount * (units * other)

// endregion


// region ================ Measure / Units Math =========================

@JvmName("div16") operator fun                      Measure.                 div(other: A                 ): Double                    = amount * (units.ratio / other.ratio)
@JvmName("div16") operator fun            Measure.                 div(other: B                 ): Measure> = amount * (units / other)
@JvmName("div1" ) operator fun            Measure>.div(other: A                 ): Measure                = amount * (units / other)
@JvmName("div2" ) operator fun            Measure>.div(other: B                 ): Measure                = amount * (units / other)
@JvmName("div3" ) operator fun  Measure>.div(other: UnitsProduct): Measure> = amount * (units / other)
@JvmName("div4" ) operator fun  Measure>.div(other: UnitsProduct): Measure> = amount * (units / other)
@JvmName("div5" ) operator fun  Measure>.div(other: UnitsProduct): Measure> = amount * (units / other)
@JvmName("div6" ) operator fun  Measure>.div(other: UnitsProduct): Measure> = amount * (units / other)
@JvmName("div7" ) operator fun            Measure>.div(other: UnitsProduct): Measure> = amount * (units / other)
@JvmName("div8" ) operator fun            Measure>.div(other: UnitsProduct): Measure> = amount * (units / other)

@JvmName("div9" ) operator fun                      Measure>.                                  div(other: B                       ): Measure>>                  = amount * (units / other)
@JvmName("div10") operator fun  Measure>.                                  div(other: UnitsRatio        ): Measure, UnitsProduct>> = amount * (units / other)
@JvmName("div11") operator fun                      Measure>.                                  div(other: UnitsRatio>): Measure                                                  = amount * (units / other)
@JvmName("div12") operator fun                      Measure, UnitsProduct>>.div(other: A                       ): Measure>>                  = amount * (units / other)
@JvmName("div13") operator fun            Measure, UnitsProduct>>.div(other: A                       ): Measure>>                  = amount * (units / other)
@JvmName("div14") operator fun  Measure, UnitsProduct>>.div(other: A                       ): Measure>>                  = amount * (units / other)
@JvmName("div15") operator fun                      Measure.                                                 div(other: UnitsRatio        ): Measure                                                  = amount * (units / other)

// endregion

// region ================ Number - Measure Math =========================

private infix fun  Number.into(unit: T): Measure = Measure(this.toDouble(), unit)

operator fun  Number.times(unit: T): Measure               = this into unit
operator fun  Number.div  (unit: T): Measure> = this into InverseUnits(unit)
operator fun  Number.times(measure: Measure): Measure = measure * this

operator fun  T.times (value: Number): Measure = value into this
operator fun  T.invoke(value: Number): Measure = value into this

// endregion

// region ================ Measure Math ==================================

/**
 * @return the absolute value of [measure], retaining its units.
 * @see absoluteValue extension property for [Measure]
 */
fun  abs(measure: Measure) = kotlin.math.abs  (measure.amount) * measure.units

/**
 * Rounds the [measure] to the closest integer, retaining its units.
 */
fun  round(measure: Measure) = kotlin.math.round(measure.amount) * measure.units

/**
 * Rounds the [measure] to the next, larger integer, retaining its units.
 */
fun  ceil(measure: Measure) = kotlin.math.ceil (measure.amount) * measure.units

/**
 * Rounds the [measure] to the previous, smaller integer, retaining its units.
 */
fun  floor(measure: Measure) = kotlin.math.floor(measure.amount) * measure.units

/**
 * Returns a [Measure] that is rounded to he closest multiple of [toNearest], and has the
 * the units of [toNearest].
 *
 * ```
 *
 * val length = 25 * inches
 *
 * round(length, toNearest = 1   * feet  ) // 2.0 feet
 * round(length, toNearest = 0.1 * meters) // 0.6 m
 * ```
 *
 * @see toNearest extension property of [Measure]
 */
fun  round(measure: Measure, toNearest: Measure): Measure = when (toNearest.amount) {
    0.0  -> measure
    else -> kotlin.math.round(measure / toNearest) * toNearest
}

/**
 * Returns a [Measure] that is rounded to he closest multiple of [toNearest], and has the
 * the units of [toNearest].
 *
 * ```
 *
 * val length = 25 * inches
 *
 * length toNearest 1   * feet   // 2.0 feet
 * length toNearest 0.1 * meters // 0.6 m
 * ```
 */
infix fun  Measure.toNearest(value: Measure): Measure = round(this, toNearest = value)

/**
 * @return the absolute value of this measure, retaining its units.
 */
inline val  Measure.absoluteValue: Measure get() = abs(this)

/**
 * @return the sign of this value:
 *   - `-1.0` when it is negative
 *   - `0.0` when it is zero
 *   - `1.0` when it is positive
 */
val  Measure.sign: Double get() = kotlin.math.sign(amount)

// endregion

// region ================ Units Aliases =================================

typealias Velocity     = UnitsRatio
typealias Acceleration = UnitsRatio>

// endregion