diff options
Diffstat (limited to 'opendc-common/src/main/kotlin/org/opendc/common/units/Unit.kt')
| -rw-r--r-- | opendc-common/src/main/kotlin/org/opendc/common/units/Unit.kt | 386 |
1 files changed, 386 insertions, 0 deletions
diff --git a/opendc-common/src/main/kotlin/org/opendc/common/units/Unit.kt b/opendc-common/src/main/kotlin/org/opendc/common/units/Unit.kt new file mode 100644 index 00000000..8bcbb148 --- /dev/null +++ b/opendc-common/src/main/kotlin/org/opendc/common/units/Unit.kt @@ -0,0 +1,386 @@ +/* + * Copyright (c) 2024 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +@file:OptIn(InternalUse::class) + +package org.opendc.common.units + +import org.opendc.common.annotations.InternalUse +import org.opendc.common.units.Time.Companion.toTime +import org.opendc.common.utils.DFLT_MIN_EPS +import org.opendc.common.utils.adaptiveEps +import org.opendc.common.utils.approx +import org.opendc.common.utils.approxLarger +import org.opendc.common.utils.approxLargerOrEq +import org.opendc.common.utils.approxSmaller +import org.opendc.common.utils.approxSmallerOrEq +import java.time.Duration +import kotlin.experimental.ExperimentalTypeInference + +/** + * Value classes can extend this interface to represent + * unit of measure with much higher type safety than [Double] (*If used from kotlin*) + * and approximately same performances. + * ```kotlin + * // e.g. + * @JvmInline value class DataRate(override val value) : Unit<DataRate> { } + * ``` + * This interface provides most of the utility functions and + * mathematical operations that are available to [Double] (including threshold comparison methods), + * but applicable to [T] (also with scalar multiplication and division), + * and operations between different unit of measures. + * + * ``` + * // e.g. sum of data-rates + * val a: DataRate = DataRate.ofMibps(100) + * val b: DataRate = DataRate.ofGibps(1) + * val c: DataRate = a + b + * c.fmt("%.3f") // "1.097 Gibps" + * + * // e.g. data-rate times scalar + * val e: DataRate = a * 2 + * e.fmt() // "200 Mibps" + * + * // e.g. threshold comparison + * if (e approx a) { ... } + * + * // e.g. operations between different unit of measures + * val a: DataRate = DataRate.ofMBps(1) + * val b: Time = Time.ofSec(3) + * val c: DataSize = a * b + * c.fmt() // "3MB" + * ``` + * + * ###### Java interoperability + * Functions that concern inline classes are not callable from java by default (at least for now). + * Hence, the JvmName annotation is needed for java interoperability. **Only methods that allow java + * to interact with kotlin code concerning inline classes should be made accessible to java.** + * Java will never be able to invoke instance methods, only static ones. + * + * Java sees value classes as the standard data type they represent (in this case double). + * Meaning there is no type safety from java, nevertheless functions can be invoked + * to provide methods the correct unit value (and for improved understandability). + * + * ```kotlin + * // kotlin + * @JvmStatic @JvmName("function") + * fun function(time: Time) { } + * ``` + * ```java + * // java + * double time = Time.ofHours(2); + * function(time) + * // or + * function(Time.ofHours(2)) + * ``` + * + * @param[T] the unit of measure that is represented (e.g. [DataRate]) + */ +public sealed interface Unit<T : Unit<T>> : Comparable<T> { + /** + * The actual value of this unit of measure used for computation and comparisons. + * + * What magnitude this value represents (e.g. Kbps, Mbps etc.) is up to the interface implementation, + * and it does not interfere with the operations, hence this property should be reserved for internal use. + */ + @InternalUse + public val value: Double + + /** + * @return the sum with [other] as [T]. + */ + public operator fun plus(other: T): T = new(value + other.value) + + /** + * @return the subtraction of [other] from *this* as [T]. + */ + public operator fun minus(other: T): T = new(value - other.value) + + /** + * @return *this* divided by scalar [scalar] as [T]. + */ + public operator fun div(scalar: Number): T = new(value / scalar.toDouble()) + + /** + * @return *this* divided by [other] as [Double]. + */ + public operator fun div(other: T): Double = value / other.value + + /** + * @return *this* multiplied by scalar [scalar] as [T]. + */ + public operator fun times(scalar: Number): T = new(value * scalar.toDouble()) + + /** + * @return *this* negated. + */ + public operator fun unaryMinus(): T = new(-value) + + public override operator fun compareTo(other: T): Int = this.value.compareTo(other.value) + + /** + * @return `true` if *this* is equal to 0 (using `==` operator). + */ + public fun isZero(): Boolean = value == .0 || value == -.0 + + /** + * @return `true` if *this* is approximately equal to 0. + * @see[Double.approx] + */ + public fun approxZero(epsilon: Double = DFLT_MIN_EPS): Boolean = value.approx(.0, epsilon = epsilon) + + /** + * @see[Double.approx] + */ + public fun approx( + other: T, + minEpsilon: Double = DFLT_MIN_EPS, + epsilon: Double = adaptiveEps(this.value, other.value, minEpsilon), + ): Boolean = this == other || this.value.approx(other.value, minEpsilon, epsilon) + + /** + * @see[Double.approx] + */ + public infix fun approx(other: T): Boolean = approx(other, minEpsilon = DFLT_MIN_EPS) + + /** + * @see[Double.approxLarger] + */ + public fun approxLarger( + other: T, + minEpsilon: Double = DFLT_MIN_EPS, + epsilon: Double = adaptiveEps(this.value, other.value, minEpsilon), + ): Boolean = this.value.approxLarger(other.value, minEpsilon, epsilon) + + /** + * @see[Double.approxLarger] + */ + public infix fun approxLarger(other: T): Boolean = approxLarger(other, minEpsilon = DFLT_MIN_EPS) + + /** + * @see[Double.approxLargerOrEq] + */ + public fun approxLargerOrEq( + other: T, + minEpsilon: Double = DFLT_MIN_EPS, + epsilon: Double = adaptiveEps(this.value, other.value, minEpsilon), + ): Boolean = this.value.approxLargerOrEq(other.value, minEpsilon, epsilon) + + /** + * @see[Double.approxLargerOrEq] + */ + public infix fun approxLargerOrEq(other: T): Boolean = approxLargerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + /** + * @see[Double.approxSmaller] + */ + public fun approxSmaller( + other: T, + minEpsilon: Double = DFLT_MIN_EPS, + epsilon: Double = adaptiveEps(this.value, other.value, minEpsilon), + ): Boolean = this.value.approxSmaller(other.value, minEpsilon, epsilon) + + /** + * @see[Double.approxSmaller] + */ + public infix fun approxSmaller(other: T): Boolean = approxSmaller(other, minEpsilon = DFLT_MIN_EPS) + + /** + * @see[Double.approxSmallerOrEq] + */ + public fun approxSmallerOrEq( + other: T, + minEpsilon: Double = DFLT_MIN_EPS, + epsilon: Double = adaptiveEps(this.value, other.value, minEpsilon), + ): Boolean = this.value.approxSmallerOrEq(other.value, minEpsilon, epsilon) + + /** + * @see[Double.approxSmallerOrEq] + */ + public infix fun approxSmallerOrEq(other: T): Boolean = approxSmallerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + /** + * @return the max value between *this* and [other]. + */ + @Suppress("UNCHECKED_CAST") + public infix fun max(other: T): T = if (this.value > other.value) this as T else other + + /** + * @return the minimum value between *this* and [other]. + */ + @Suppress("UNCHECKED_CAST") + public infix fun min(other: T): T = if (this.value < other.value) this as T else other + + /** + * @return the absolute value of *this*. + */ + public fun abs(): T = new(kotlin.math.abs(value)) + + /** + * @return *this* approximated to [to] if within `0 - epsilon` and `0 + epsilon`. + */ + @Suppress("UNCHECKED_CAST") + public fun roundToIfWithinEpsilon( + to: T, + epsilon: Double = DFLT_MIN_EPS, + ): T = + if (this.value in (to.value - epsilon)..(to.value + epsilon)) { + to + } else { + this as T + } + + /** + * The "constructor" of [T] that this interface uses to + * instantiate new [T] when performing operations. + */ + @InternalUse + public fun new(value: Double): T + + /** + * Returns the formatted string representation of the unit of measure (e.g. "1.2 Gbps") + * with the formatter [fmt] applied to the value part of the resulting string. + * + * ```kotlin + * val dr = DataRate.ofGbps(1.234567) + * dr.fmtValue() // "1.234567 Gbps" + * dr.fmtValue("%.2f") // "1.23 Gbps" + * ``` + */ + public fun fmtValue(fmt: String = "%f"): String + + public companion object { + /** + * @return [unit] multiplied by scalar [this]. + */ + public operator fun <T : Unit<T>> Number.times(unit: T): T = unit * this + + /** + * @return minimum value between [a] and [b]. + */ + public fun <T : Unit<T>> min( + a: T, + b: T, + ): T = if (a.value < b.value) a else b + + /** + * @return minimum value between [units]. + */ + public fun <T : Unit<T>> minOf(vararg units: T): T = units.minBy { it.value } + + /** + * @return maximum value between [a] and [b]. + */ + public fun <T : Unit<T>> max( + a: T, + b: T, + ): T = if (a.value > b.value) a else b + + /** + * @return maximum value between [units]. + */ + public fun <T : Unit<T>> maxOf(vararg units: T): T = units.maxBy { it.value } + + // maxBy and minBy need to be defined in implementations. + + // Operations whose 'this' is a `Unit` are defined here. + // Operations whose 'this' is not a `Unit` are defined in their classes + // and not as extension function so that they do not need to be imported + + public operator fun Duration.times(dataRate: DataRate): DataSize = toTime() * dataRate + + public operator fun Duration.times(power: Power): Energy = toTime() * power + + public operator fun Number.div(time: Time): Frequency = Frequency.ofHz(this.toDouble() / time.toSec()) + + public operator fun Number.div(duration: Duration): Frequency = this / duration.toTime() + + // Defined here so that they can overload the same method name, instead of having a different name forEach unit. + // You can not overload `sumOf` and using that name results in not being able to use the overloads for unit and for number in the same file. + + // A reified version that does not need overloads can be also be defined, with a switch statement on the reified unit type for the base value. + // Then, if a unit is not included in the switch, a runtime error occurs, not compile time. + + @OptIn(ExperimentalTypeInference::class) + @OverloadResolutionByLambdaReturnType + @JvmName("sumOfDataRate") + public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> DataRate): DataRate { + var sum: DataRate = DataRate.ZERO + forEach { sum += selector(it) } + return sum + } + + @OptIn(ExperimentalTypeInference::class) + @OverloadResolutionByLambdaReturnType + @JvmName("sumOfDataSize") + public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> DataSize): DataSize { + var sum: DataSize = DataSize.ZERO + forEach { sum += selector(it) } + return sum + } + + @OptIn(ExperimentalTypeInference::class) + @OverloadResolutionByLambdaReturnType + @JvmName("sumOfEnergy") + public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> Energy): Energy { + var sum: Energy = Energy.ZERO + forEach { sum += selector(it) } + return sum + } + + @OptIn(ExperimentalTypeInference::class) + @OverloadResolutionByLambdaReturnType + @JvmName("sumOfPower") + public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> Power): Power { + var sum: Power = Power.ZERO + forEach { sum += selector(it) } + return sum + } + + @OptIn(ExperimentalTypeInference::class) + @OverloadResolutionByLambdaReturnType + @JvmName("sumOfTime") + public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> Time): Time { + var sum: Time = Time.ZERO + forEach { sum += selector(it) } + return sum + } + + @OptIn(ExperimentalTypeInference::class) + @OverloadResolutionByLambdaReturnType + @JvmName("sumOfFrequency") + public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> Frequency): Frequency { + var sum: Frequency = Frequency.ZERO + forEach { sum += selector(it) } + return sum + } + + @OptIn(ExperimentalTypeInference::class) + @OverloadResolutionByLambdaReturnType + @JvmName("sumOfPercentage") + public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> Percentage): Percentage { + var sum: Percentage = Percentage.ZERO + forEach { sum += selector(it) } + return sum + } + } +} |
