diff options
| author | Alessio Leonardo Tomei <122273875+T0mexX@users.noreply.github.com> | 2025-03-18 10:31:21 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-03-18 10:31:21 +0100 |
| commit | 46ba81a45f7cb10c7f870bbf6946a46207ee353c (patch) | |
| tree | ef324e77ff6ba6d569084ff19efbc80162158bab /opendc-common/src/main/kotlin/org/opendc/common/units/Unit.kt | |
| parent | 582c45b6457bc9dc6fed57a843c87097db991d4a (diff) | |
Unit System v2 (#243)
* Separated `Time` unit into `TimeDelta` and `TimeStamp` + small fixes
Addition and subtruction between `Timestamp`s is not allowed, but any
other `Unit` operation/comparison is. `TimeDelta`s can be
added/subtructed to/form `Timestamp`s.
Deserialization of `Timestamp`:
- `Number` -> interpreted as millis from Epoch
- `Instant` (string representation) -> converted to Timestamp
- `Duration` (string representation) -> interpreted as duration since
Epoch (warn msg is logged)
Deserialization of `TimeDelta` is the same as `Time` was before, with the
diference that when an `Instant` is converted to an timedelta since Epoch
a warning message is logged.
* Unit System v2
- Merged `BoundedPercentage` and `UnboundedPercentage`
- Overrided all operation defined in `Unit` in all subclasses to avoid
as much as possible value classes being boxed in bytecode. If units are used as generics
(hence also functions defined in Unit<T>) they are boxed (as double would if used as generic).
- All units companions now subclass `UnitId`, and can be used as keys
(e.g `Map<UnitId, idk>`), while offering `max` `min` and `zero`
methods.
- Division between the same unit now returns `Percentage`
- Added `Iterable<T>.averageOfUnitOrNull(selector (T) -> <specific unit>)`
- `ifNeg0ThenPos0()` now is optional and not invoked on every
constructor
- Now methods in `Unit<T>` are all abstract, forcing override and avoid
boxing in some cases
- Added `@UnintendedOperation` and `UnitOperationException` for methods
that must be defined but are not intended for use (e.g. `Timestamp` +
`Timestamp`)
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 | 307 |
1 files changed, 217 insertions, 90 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 index 8bcbb148..17b46024 100644 --- a/opendc-common/src/main/kotlin/org/opendc/common/units/Unit.kt +++ b/opendc-common/src/main/kotlin/org/opendc/common/units/Unit.kt @@ -20,12 +20,12 @@ * SOFTWARE. */ -@file:OptIn(InternalUse::class) +@file:OptIn(InternalUse::class, NonInlinableUnit::class) package org.opendc.common.units import org.opendc.common.annotations.InternalUse -import org.opendc.common.units.Time.Companion.toTime +import org.opendc.common.units.TimeDelta.Companion.toTimeDelta import org.opendc.common.utils.DFLT_MIN_EPS import org.opendc.common.utils.adaptiveEps import org.opendc.common.utils.approx @@ -47,7 +47,7 @@ import kotlin.experimental.ExperimentalTypeInference * 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. + * and operations between different unit of measures (e.g., DataRate * TimeDelta = DataSize, and many others). * * ``` * // e.g. sum of data-rates @@ -65,32 +65,32 @@ import kotlin.experimental.ExperimentalTypeInference * * // e.g. operations between different unit of measures * val a: DataRate = DataRate.ofMBps(1) - * val b: Time = Time.ofSec(3) + * val b: TimeDelta = TimeDelta.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 + * Hence, the JvmName annotation is necessary 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). + * 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 with the correct unit value (and for improved understandability). * * ```kotlin * // kotlin * @JvmStatic @JvmName("function") - * fun function(time: Time) { } + * fun function(time: TimeDelta) { } * ``` * ```java * // java - * double time = Time.ofHours(2); + * double time = TimeDelta.ofHours(2); * function(time) * // or - * function(Time.ofHours(2)) + * function(TimeDelta.ofHours(2)) * ``` * * @param[T] the unit of measure that is represented (e.g. [DataRate]) @@ -99,162 +99,180 @@ 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. + * 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 + @NonInlinableUnit public val value: Double /** + * If `this` is -0.0 it is converted to +0.0. + */ + @NonInlinableUnit + public fun ifNeg0ThenPos0(): T + + /** * @return the sum with [other] as [T]. */ - public operator fun plus(other: T): T = new(value + other.value) + @NonInlinableUnit + public operator fun plus(other: T): T /** * @return the subtraction of [other] from *this* as [T]. */ - public operator fun minus(other: T): T = new(value - other.value) + @NonInlinableUnit + public operator fun minus(other: T): T /** * @return *this* divided by scalar [scalar] as [T]. */ - public operator fun div(scalar: Number): T = new(value / scalar.toDouble()) + @NonInlinableUnit + public operator fun div(scalar: Number): T /** * @return *this* divided by [other] as [Double]. */ - public operator fun div(other: T): Double = value / other.value + @NonInlinableUnit + public operator fun div(other: T): Percentage /** * @return *this* multiplied by scalar [scalar] as [T]. */ - public operator fun times(scalar: Number): T = new(value * scalar.toDouble()) + @NonInlinableUnit + public operator fun times(scalar: Number): T /** - * @return *this* negated. + * @return *this* multiplied by percentage [Percentage] as [T]. */ - public operator fun unaryMinus(): T = new(-value) + @NonInlinableUnit + public operator fun times(percentage: Percentage): T - public override operator fun compareTo(other: T): Int = this.value.compareTo(other.value) + /** + * @return *this* negated. + */ + @NonInlinableUnit + public operator fun unaryMinus(): T /** * @return `true` if *this* is equal to 0 (using `==` operator). */ - public fun isZero(): Boolean = value == .0 || value == -.0 + @NonInlinableUnit + public fun isZero(): Boolean /** * @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) + @NonInlinableUnit + public fun approxZero(epsilon: Double = DFLT_MIN_EPS): Boolean /** * @see[Double.approx] */ + @NonInlinableUnit 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) + ): Boolean /** * @see[Double.approx] */ - public infix fun approx(other: T): Boolean = approx(other, minEpsilon = DFLT_MIN_EPS) + @NonInlinableUnit + public infix fun approx(other: T): Boolean /** * @see[Double.approxLarger] */ + @NonInlinableUnit 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) + ): Boolean /** * @see[Double.approxLarger] */ - public infix fun approxLarger(other: T): Boolean = approxLarger(other, minEpsilon = DFLT_MIN_EPS) + @NonInlinableUnit + public infix fun approxLarger(other: T): Boolean /** * @see[Double.approxLargerOrEq] */ + @NonInlinableUnit 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) + ): Boolean /** * @see[Double.approxLargerOrEq] */ - public infix fun approxLargerOrEq(other: T): Boolean = approxLargerOrEq(other, minEpsilon = DFLT_MIN_EPS) + @NonInlinableUnit + public infix fun approxLargerOrEq(other: T): Boolean /** * @see[Double.approxSmaller] */ + @NonInlinableUnit 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) + ): Boolean /** * @see[Double.approxSmaller] */ - public infix fun approxSmaller(other: T): Boolean = approxSmaller(other, minEpsilon = DFLT_MIN_EPS) + @NonInlinableUnit + public infix fun approxSmaller(other: T): Boolean /** * @see[Double.approxSmallerOrEq] */ + @NonInlinableUnit 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) + ): Boolean /** * @see[Double.approxSmallerOrEq] */ - public infix fun approxSmallerOrEq(other: T): Boolean = approxSmallerOrEq(other, minEpsilon = DFLT_MIN_EPS) + @NonInlinableUnit + public infix fun approxSmallerOrEq(other: T): Boolean /** * @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 + @NonInlinableUnit + public infix fun max(other: T): T /** * @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 + @NonInlinableUnit + public infix fun min(other: T): T /** * @return the absolute value of *this*. */ - public fun abs(): T = new(kotlin.math.abs(value)) + @NonInlinableUnit + public fun abs(): T /** * @return *this* approximated to [to] if within `0 - epsilon` and `0 + epsilon`. */ - @Suppress("UNCHECKED_CAST") + @NonInlinableUnit 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 + ): T /** * Returns the formatted string representation of the unit of measure (e.g. "1.2 Gbps") @@ -272,59 +290,42 @@ public sealed interface Unit<T : Unit<T>> : Comparable<T> { /** * @return [unit] multiplied by scalar [this]. */ + @NonInlinableUnit 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 + @NonInlinableUnit + public inline fun <reified T : Unit<T>> minOf(vararg units: T): T = units.minBy { it.value } /** * @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. + @NonInlinableUnit + public inline fun <reified T : Unit<T>> maxOf(vararg units: T): T = units.maxBy { it.value } - // 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 = toTimeDelta() * dataRate - public operator fun Duration.times(dataRate: DataRate): DataSize = toTime() * dataRate + public operator fun Duration.times(power: Power): Energy = toTimeDelta() * power - public operator fun Duration.times(power: Power): Energy = toTime() * power + public operator fun Number.div(timeDelta: TimeDelta): Frequency = Frequency.ofHz(this.toDouble() / timeDelta.toSec()) - 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() + public operator fun Number.div(duration: Duration): Frequency = this / duration.toTimeDelta() // 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. + // You cannot 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. + // A reified version that does not need overloads can 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 + var sum: DataRate = DataRate.zero forEach { sum += selector(it) } return sum } @@ -333,7 +334,7 @@ public sealed interface Unit<T : Unit<T>> : Comparable<T> { @OverloadResolutionByLambdaReturnType @JvmName("sumOfDataSize") public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> DataSize): DataSize { - var sum: DataSize = DataSize.ZERO + var sum: DataSize = DataSize.zero forEach { sum += selector(it) } return sum } @@ -342,7 +343,7 @@ public sealed interface Unit<T : Unit<T>> : Comparable<T> { @OverloadResolutionByLambdaReturnType @JvmName("sumOfEnergy") public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> Energy): Energy { - var sum: Energy = Energy.ZERO + var sum: Energy = Energy.zero forEach { sum += selector(it) } return sum } @@ -351,7 +352,7 @@ public sealed interface Unit<T : Unit<T>> : Comparable<T> { @OverloadResolutionByLambdaReturnType @JvmName("sumOfPower") public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> Power): Power { - var sum: Power = Power.ZERO + var sum: Power = Power.zero forEach { sum += selector(it) } return sum } @@ -359,8 +360,8 @@ public sealed interface Unit<T : Unit<T>> : Comparable<T> { @OptIn(ExperimentalTypeInference::class) @OverloadResolutionByLambdaReturnType @JvmName("sumOfTime") - public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> Time): Time { - var sum: Time = Time.ZERO + public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> TimeDelta): TimeDelta { + var sum: TimeDelta = TimeDelta.zero forEach { sum += selector(it) } return sum } @@ -369,7 +370,7 @@ public sealed interface Unit<T : Unit<T>> : Comparable<T> { @OverloadResolutionByLambdaReturnType @JvmName("sumOfFrequency") public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> Frequency): Frequency { - var sum: Frequency = Frequency.ZERO + var sum: Frequency = Frequency.zero forEach { sum += selector(it) } return sum } @@ -378,9 +379,135 @@ public sealed interface Unit<T : Unit<T>> : Comparable<T> { @OverloadResolutionByLambdaReturnType @JvmName("sumOfPercentage") public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> Percentage): Percentage { - var sum: Percentage = Percentage.ZERO + var sum: Percentage = Percentage.zero forEach { sum += selector(it) } return sum } + + @OptIn(ExperimentalTypeInference::class) + @OverloadResolutionByLambdaReturnType + @JvmName("averageOfDataRateOrNull") + public inline fun <T> Iterable<T>.averageOfUnitOrNull(selector: (T) -> DataRate): DataRate? { + if (!iterator().hasNext()) return null + var sum: DataRate = DataRate.zero + var count = 0 + forEach { + sum += selector(it) + count++ + } + return sum / count + } + + @OptIn(ExperimentalTypeInference::class) + @OverloadResolutionByLambdaReturnType + @JvmName("averageOfDataSizeOrNull") + public inline fun <T> Iterable<T>.averageOfUnitOrNull(selector: (T) -> DataSize): DataSize? { + if (!iterator().hasNext()) return null + var sum: DataSize = DataSize.zero + var count = 0 + forEach { + sum += selector(it) + count++ + } + return sum / count + } + + @OptIn(ExperimentalTypeInference::class) + @OverloadResolutionByLambdaReturnType + @JvmName("averageOfEnergyOrNull") + public inline fun <T> Iterable<T>.averageOfUnitOrNull(selector: (T) -> Energy): Energy? { + if (!iterator().hasNext()) return null + var sum: Energy = Energy.zero + var count = 0 + forEach { + sum += selector(it) + count++ + } + return sum / count + } + + @OptIn(ExperimentalTypeInference::class) + @OverloadResolutionByLambdaReturnType + @JvmName("averageOfPowerOrNull") + public inline fun <T> Iterable<T>.averageOfUnitOrNull(selector: (T) -> Power): Power? { + if (!iterator().hasNext()) return null + var sum: Power = Power.zero + var count = 0 + forEach { + sum += selector(it) + count++ + } + return sum / count + } + + @OptIn(ExperimentalTypeInference::class) + @OverloadResolutionByLambdaReturnType + @JvmName("averageOfTimeOrNull") + public inline fun <T> Iterable<T>.averageOfUnitOrNull(selector: (T) -> TimeDelta): TimeDelta? { + if (!iterator().hasNext()) return null + var sum: TimeDelta = TimeDelta.zero + var count = 0 + forEach { + sum += selector(it) + count++ + } + return sum / count + } + + @OptIn(ExperimentalTypeInference::class) + @OverloadResolutionByLambdaReturnType + @JvmName("averageOfFrequencyOrNull") + public inline fun <T> Iterable<T>.averageOfUnitOrNull(selector: (T) -> Frequency): Frequency? { + if (!iterator().hasNext()) return null + var sum: Frequency = Frequency.zero + var count = 0 + forEach { + sum += selector(it) + count++ + } + return sum / count + } + + @OptIn(ExperimentalTypeInference::class) + @OverloadResolutionByLambdaReturnType + @JvmName("averageOfPercentageOrNull") + public inline fun <T> Iterable<T>.averageOfUnitOrNull(selector: (T) -> Percentage): Percentage? { + if (!iterator().hasNext()) return null + var sum: Percentage = Percentage.zero + var count = 0 + forEach { + sum += selector(it) + count++ + } + return sum / count + } } } + +@RequiresOptIn( + message = + "Unit value class cannot be JVM inlined if this symbol is used " + + "(and if value class is used as generic type, but that holds for `double` as well)", + level = RequiresOptIn.Level.WARNING, +) +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.CONSTRUCTOR) +public annotation class NonInlinableUnit + +@RequiresOptIn( + message = + "This operation is not intended for this unit, but it needs to be define. " + + "Invoking this method will result in an exception. ", + level = RequiresOptIn.Level.WARNING, +) +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.CONSTRUCTOR) +public annotation class UnintendedOperation + +public class UnitOperationException(override val message: String? = null) : Exception() + +public interface UnitId<T : Unit<T>> { + public val zero: T + public val max: T + public val min: T +} |
