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 | |
| 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')
11 files changed, 1580 insertions, 502 deletions
diff --git a/opendc-common/src/main/kotlin/org/opendc/common/units/DataRate.kt b/opendc-common/src/main/kotlin/org/opendc/common/units/DataRate.kt index 2af45b7b..15f69ebf 100644 --- a/opendc-common/src/main/kotlin/org/opendc/common/units/DataRate.kt +++ b/opendc-common/src/main/kotlin/org/opendc/common/units/DataRate.kt @@ -20,13 +20,19 @@ * SOFTWARE. */ -@file:OptIn(InternalUse::class) +@file:OptIn(InternalUse::class, NonInlinableUnit::class) package org.opendc.common.units import kotlinx.serialization.Serializable 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.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 org.opendc.common.utils.ifNeg0thenPos0 import java.time.Duration @@ -40,8 +46,19 @@ public value class DataRate private constructor( // In bits/s. override val value: Double, ) : Unit<DataRate> { - @InternalUse - override fun new(value: Double): DataRate = DataRate(value.ifNeg0thenPos0()) + override fun toString(): String = fmtValue() + + public override fun fmtValue(fmt: String): String = + when (abs()) { + in zero..ofBps(100) -> "${String.format(fmt, tobps())} bps" + in ofbps(100)..ofKbps(100) -> "${String.format(fmt, toKbps())} Kbps" + in ofKbps(100)..ofMbps(100) -> "${String.format(fmt, toMbps())} Mbps" + else -> "${String.format(fmt, toGbps())} Gbps" + } + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Conversions to Double + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public fun tobps(): Double = value @@ -69,22 +86,118 @@ public value class DataRate private constructor( public fun toGBps(): Double = toGbps() / 8 - override fun toString(): String = fmtValue() + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Operation Override (to avoid boxing of value classes in byte code) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - public override fun fmtValue(fmt: String): String = - when (abs()) { - in ZERO..ofBps(100) -> "${String.format(fmt, tobps())} bps" - in ofbps(100)..ofKbps(100) -> "${String.format(fmt, toKbps())} Kbps" - in ofKbps(100)..ofMbps(100) -> "${String.format(fmt, toMbps())} Mbps" - else -> "${String.format(fmt, toGbps())} Gbps" + public override fun ifNeg0ThenPos0(): DataRate = DataRate(value.ifNeg0thenPos0()) + + public override operator fun plus(other: DataRate): DataRate = DataRate(value + other.value) + + public override operator fun minus(other: DataRate): DataRate = DataRate(value - other.value) + + public override operator fun div(scalar: Number): DataRate = DataRate(value / scalar.toDouble()) + + public override operator fun div(other: DataRate): Percentage = Percentage.ofRatio(value / other.value) + + public override operator fun times(scalar: Number): DataRate = DataRate(value * scalar.toDouble()) + + public override operator fun times(percentage: Percentage): DataRate = DataRate(value * percentage.value) + + public override operator fun unaryMinus(): DataRate = DataRate(-value) + + public override operator fun compareTo(other: DataRate): Int = this.value.compareTo(other.value) + + public override fun isZero(): Boolean = value == .0 + + public override fun approxZero(epsilon: Double): Boolean = value.approx(.0, epsilon = epsilon) + + public override fun approx( + other: DataRate, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this == other || this.value.approx(other.value, minEpsilon, epsilon) + + public override infix fun approx(other: DataRate): Boolean = approx(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLarger( + other: DataRate, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLarger(other.value, minEpsilon, epsilon) + + public override infix fun approxLarger(other: DataRate): Boolean = approxLarger(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLargerOrEq( + other: DataRate, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLargerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxLargerOrEq(other: DataRate): Boolean = approxLargerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmaller( + other: DataRate, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmaller(other.value, minEpsilon, epsilon) + + public override infix fun approxSmaller(other: DataRate): Boolean = approxSmaller(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmallerOrEq( + other: DataRate, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmallerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxSmallerOrEq(other: DataRate): Boolean = approxSmallerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override infix fun max(other: DataRate): DataRate = if (this.value > other.value) this else other + + public override infix fun min(other: DataRate): DataRate = if (this.value < other.value) this else other + + public override fun abs(): DataRate = DataRate(kotlin.math.abs(value)) + + public override fun roundToIfWithinEpsilon( + to: DataRate, + epsilon: Double, + ): DataRate = + if (this.value in (to.value - epsilon)..(to.value + epsilon)) { + to + } else { + this } - public operator fun times(time: Time): DataSize = DataSize.ofKiB(toKiBps() * time.toSec()) + public fun max( + a: DataRate, + b: DataRate, + ): DataRate = if (a.value > b.value) a else b + + public fun min( + a: DataRate, + b: DataRate, + ): DataRate = if (a.value < b.value) a else b + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Unit Specific Operations + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public operator fun times(timeDelta: TimeDelta): DataSize = DataSize.ofKiB(toKiBps() * timeDelta.toSec()) - public operator fun times(duration: Duration): DataSize = this * duration.toTime() + public operator fun times(duration: Duration): DataSize = this * duration.toTimeDelta() - public companion object { - @JvmStatic public val ZERO: DataRate = DataRate(.0) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Companion + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public companion object : UnitId<DataRate> { + @JvmStatic override val zero: DataRate = DataRate(.0) + + @JvmStatic override val max: DataRate = DataRate(Double.MAX_VALUE) + + @JvmStatic override val min: DataRate = DataRate(Double.MIN_VALUE) + + public operator fun Number.times(unit: DataRate): DataRate = unit * this @JvmStatic @JvmName("ofbps") @@ -142,9 +255,13 @@ public value class DataRate private constructor( @JvmName("ofGBps") public fun ofGBps(gBps: Number): DataRate = ofGbps(gBps.toDouble() * 8) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Serializer + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /** * Serializer for [DataRate] value class. It needs to be a compile - * time constant in order to be used as serializer automatically, + * time constant to be used as serializer automatically, * hence `object :` instead of class instantiation. * * ```json diff --git a/opendc-common/src/main/kotlin/org/opendc/common/units/DataSize.kt b/opendc-common/src/main/kotlin/org/opendc/common/units/DataSize.kt index e32d9e88..a17fda4c 100644 --- a/opendc-common/src/main/kotlin/org/opendc/common/units/DataSize.kt +++ b/opendc-common/src/main/kotlin/org/opendc/common/units/DataSize.kt @@ -20,14 +20,21 @@ * SOFTWARE. */ -@file:OptIn(InternalUse::class) +@file:OptIn(InternalUse::class, NonInlinableUnit::class) package org.opendc.common.units import kotlinx.serialization.Serializable 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.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 org.opendc.common.utils.fmt +import org.opendc.common.utils.ifNeg0thenPos0 import java.time.Duration /** @@ -40,8 +47,19 @@ public value class DataSize private constructor( // In MiB. override val value: Double, ) : Unit<DataSize> { - @InternalUse - override fun new(value: Double): DataSize = DataSize(value) + override fun toString(): String = fmtValue() + + override fun fmtValue(fmt: String): String = + when (abs()) { + in zero..ofBytes(100) -> "${toBytes().fmt(fmt)} Bytes" + in ofBytes(100)..ofKiB(100) -> "${toKiB().fmt(fmt)} KiB" + in ofKiB(100)..ofMiB(100) -> "${toMiB().fmt(fmt)} MiB" + else -> "${toGiB().fmt(fmt)} GiB" + } + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Conversions to Double + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public fun toBits(): Double = toKib() * 1024 @@ -83,22 +101,120 @@ public value class DataSize private constructor( public fun toTiB(): Double = toGiB() / 1024 - override fun toString(): String = fmtValue() + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Operation Override (to avoid boxing of value classes in byte code) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - override fun fmtValue(fmt: String): String = - when (abs()) { - in ZERO..ofBytes(100) -> "${toBytes().fmt(fmt)} Bytes" - in ofBytes(100)..ofKiB(100) -> "${toKiB().fmt(fmt)} KiB" - in ofKiB(100)..ofMiB(100) -> "${toMiB().fmt(fmt)} MiB" - else -> "${toGiB().fmt(fmt)} GiB" + public override fun ifNeg0ThenPos0(): DataSize = DataSize(value.ifNeg0thenPos0()) + + public override operator fun plus(other: DataSize): DataSize = DataSize(value + other.value) + + public override operator fun minus(other: DataSize): DataSize = DataSize(value - other.value) + + public override operator fun div(scalar: Number): DataSize = DataSize(value / scalar.toDouble()) + + public override operator fun div(other: DataSize): Percentage = Percentage.ofRatio(value / other.value) + + public override operator fun times(scalar: Number): DataSize = DataSize(value * scalar.toDouble()) + + public override operator fun times(percentage: Percentage): DataSize = DataSize(value * percentage.value) + + public override operator fun unaryMinus(): DataSize = DataSize(-value) + + public override operator fun compareTo(other: DataSize): Int = this.value.compareTo(other.value) + + public override fun isZero(): Boolean = value == .0 + + public override fun approxZero(epsilon: Double): Boolean = value.approx(.0, epsilon = epsilon) + + public override fun approx( + other: DataSize, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this == other || this.value.approx(other.value, minEpsilon, epsilon) + + public override infix fun approx(other: DataSize): Boolean = approx(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLarger( + other: DataSize, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLarger(other.value, minEpsilon, epsilon) + + public override infix fun approxLarger(other: DataSize): Boolean = approxLarger(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLargerOrEq( + other: DataSize, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLargerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxLargerOrEq(other: DataSize): Boolean = approxLargerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmaller( + other: DataSize, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmaller(other.value, minEpsilon, epsilon) + + public override infix fun approxSmaller(other: DataSize): Boolean = approxSmaller(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmallerOrEq( + other: DataSize, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmallerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxSmallerOrEq(other: DataSize): Boolean = approxSmallerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override infix fun max(other: DataSize): DataSize = if (this.value > other.value) this else other + + public override infix fun min(other: DataSize): DataSize = if (this.value < other.value) this else other + + public override fun abs(): DataSize = DataSize(kotlin.math.abs(value)) + + public override fun roundToIfWithinEpsilon( + to: DataSize, + epsilon: Double, + ): DataSize = + if (this.value in (to.value - epsilon)..(to.value + epsilon)) { + to + } else { + this } - public operator fun div(time: Time): DataRate = DataRate.ofKBps(this.toKiB() / time.toSec()) + public fun max( + a: DataSize, + b: DataSize, + ): DataSize = if (a.value > b.value) a else b + + public fun min( + a: DataSize, + b: DataSize, + ): DataSize = if (a.value < b.value) a else b + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Unit Specific Operations + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public operator fun div(timeDelta: TimeDelta): DataRate = DataRate.ofKBps(this.toKiB() / timeDelta.toSec()) - public operator fun div(duration: Duration): DataRate = this / duration.toTime() + public operator fun div(duration: Duration): DataRate = this / duration.toTimeDelta() - public companion object { - @JvmStatic public val ZERO: DataSize = DataSize(.0) + public operator fun div(dataRate: DataRate): TimeDelta = TimeDelta.ofSec(this.toKb() / dataRate.toKbps()) + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Companion + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public companion object : UnitId<DataSize> { + @JvmStatic override val zero: DataSize = DataSize(.0) + + @JvmStatic override val max: DataSize = DataSize(Double.MAX_VALUE) + + @JvmStatic override val min: DataSize = DataSize(Double.MIN_VALUE) + + public operator fun Number.times(unit: DataSize): DataSize = unit * this @JvmStatic @JvmName("ofBits") @@ -176,9 +292,13 @@ public value class DataSize private constructor( @JvmName("ofTiB") public fun ofTiB(tiB: Number): DataSize = ofGiB(tiB.toDouble() * 1024) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Serializer + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /** * Serializer for [DataSize] value class. It needs to be a compile - * time constant in order to be used as serializer automatically, + * time constant to be used as serializer automatically, * hence `object :` instead of class instantiation. * * ```json diff --git a/opendc-common/src/main/kotlin/org/opendc/common/units/Energy.kt b/opendc-common/src/main/kotlin/org/opendc/common/units/Energy.kt index 467192a0..f54fe515 100644 --- a/opendc-common/src/main/kotlin/org/opendc/common/units/Energy.kt +++ b/opendc-common/src/main/kotlin/org/opendc/common/units/Energy.kt @@ -20,13 +20,19 @@ * SOFTWARE. */ -@file:OptIn(InternalUse::class) +@file:OptIn(InternalUse::class, NonInlinableUnit::class) package org.opendc.common.units import kotlinx.serialization.Serializable 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.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 org.opendc.common.utils.fmt import org.opendc.common.utils.ifNeg0thenPos0 import java.time.Duration @@ -42,7 +48,18 @@ public value class Energy private constructor( // In Joule override val value: Double, ) : Unit<Energy> { - override fun new(value: Double): Energy = Energy(value.ifNeg0thenPos0()) + override fun toString(): String = fmtValue() + + override fun fmtValue(fmt: String): String = + if (value <= 1000.0) { + "${toJoule().fmt(fmt)} Joule" + } else { + "${toKJoule().fmt(fmt)} KJoule" + } + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Conversions to Double + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public fun toJoule(): Double = value @@ -52,22 +69,118 @@ public value class Energy private constructor( public fun toKWh(): Double = toWh() / 1000 - override fun toString(): String = fmtValue() + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Operation Override (to avoid boxing of value classes in byte code) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - override fun fmtValue(fmt: String): String = - if (value >= 1000.0) { - "${toJoule().fmt(fmt)} Joule" + public override fun ifNeg0ThenPos0(): Energy = Energy(value.ifNeg0thenPos0()) + + public override operator fun plus(other: Energy): Energy = Energy(value + other.value) + + public override operator fun minus(other: Energy): Energy = Energy(value - other.value) + + public override operator fun div(scalar: Number): Energy = Energy(value / scalar.toDouble()) + + public override operator fun div(other: Energy): Percentage = Percentage.ofRatio(value / other.value) + + public override operator fun times(scalar: Number): Energy = Energy(value * scalar.toDouble()) + + public override operator fun times(percentage: Percentage): Energy = Energy(value * percentage.value) + + public override operator fun unaryMinus(): Energy = Energy(-value) + + public override operator fun compareTo(other: Energy): Int = this.value.compareTo(other.value) + + public override fun isZero(): Boolean = value == .0 + + public override fun approxZero(epsilon: Double): Boolean = value.approx(.0, epsilon = epsilon) + + public override fun approx( + other: Energy, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this == other || this.value.approx(other.value, minEpsilon, epsilon) + + public override infix fun approx(other: Energy): Boolean = approx(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLarger( + other: Energy, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLarger(other.value, minEpsilon, epsilon) + + public override infix fun approxLarger(other: Energy): Boolean = approxLarger(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLargerOrEq( + other: Energy, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLargerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxLargerOrEq(other: Energy): Boolean = approxLargerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmaller( + other: Energy, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmaller(other.value, minEpsilon, epsilon) + + public override infix fun approxSmaller(other: Energy): Boolean = approxSmaller(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmallerOrEq( + other: Energy, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmallerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxSmallerOrEq(other: Energy): Boolean = approxSmallerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override infix fun max(other: Energy): Energy = if (this.value > other.value) this else other + + public override infix fun min(other: Energy): Energy = if (this.value < other.value) this else other + + public override fun abs(): Energy = Energy(kotlin.math.abs(value)) + + public override fun roundToIfWithinEpsilon( + to: Energy, + epsilon: Double, + ): Energy = + if (this.value in (to.value - epsilon)..(to.value + epsilon)) { + to } else { - "${toKJoule().fmt(fmt)} KJoule" + this } - public operator fun div(time: Time): Power = Power.ofWatts(toWh() / time.toHours()) + public fun max( + a: Energy, + b: Energy, + ): Energy = if (a.value > b.value) a else b - public operator fun div(duration: Duration): Power = this / duration.toTime() + public fun min( + a: Energy, + b: Energy, + ): Energy = if (a.value < b.value) a else b - public companion object { - @JvmStatic - public val ZERO: Energy = Energy(.0) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Unit Specific Operations + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public operator fun div(timeDelta: TimeDelta): Power = Power.ofWatts(toWh() / timeDelta.toHours()) + + public operator fun div(duration: Duration): Power = this / duration.toTimeDelta() + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Companion + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public companion object : UnitId<Energy> { + @JvmStatic override val zero: Energy = Energy(.0) + + @JvmStatic override val max: Energy = Energy(Double.MAX_VALUE) + + @JvmStatic override val min: Energy = Energy(Double.MIN_VALUE) + + public operator fun Number.times(unit: Frequency): Frequency = unit * this @JvmStatic @JvmName("ofJoule") @@ -87,9 +200,13 @@ public value class Energy private constructor( private val JOULES = Regex("\\s*(?:j|(?:joule|Joule)(?:|s))") + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Serializer + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /** * Serializer for [Energy] value class. It needs to be a compile - * time constant in order to be used as serializer automatically, + * time constant to be used as serializer automatically, * hence `object :` instead of class instantiation. * * ```json diff --git a/opendc-common/src/main/kotlin/org/opendc/common/units/Frequency.kt b/opendc-common/src/main/kotlin/org/opendc/common/units/Frequency.kt index df1b49f6..4ffb3992 100644 --- a/opendc-common/src/main/kotlin/org/opendc/common/units/Frequency.kt +++ b/opendc-common/src/main/kotlin/org/opendc/common/units/Frequency.kt @@ -20,13 +20,19 @@ * SOFTWARE. */ -@file:OptIn(InternalUse::class) +@file:OptIn(InternalUse::class, NonInlinableUnit::class) package org.opendc.common.units import kotlinx.serialization.Serializable 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.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 org.opendc.common.utils.fmt import org.opendc.common.utils.ifNeg0thenPos0 import java.time.Duration @@ -42,7 +48,19 @@ public value class Frequency private constructor( // As MHz. override val value: Double, ) : Unit<Frequency> { - override fun new(value: Double): Frequency = Frequency(value.ifNeg0thenPos0().also { check(it >= .0) }) + override fun toString(): String = fmtValue() + + override fun fmtValue(fmt: String): String = + when (abs()) { + in zero..ofHz(500) -> "${toHz().fmt(fmt)} Hz" + in ofHz(500)..ofKHz(500) -> "${toKHz().fmt(fmt)} KHz" + in ofKHz(500)..ofMHz(500) -> "${toMHz().fmt(fmt)} MHz" + else -> "${toGHz().fmt(fmt)} GHz" + } + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Conversions to Double + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public fun toHz(): Double = value * 1e6 @@ -52,22 +70,118 @@ public value class Frequency private constructor( public fun toGHz(): Double = value / 1e3 - override fun toString(): String = fmtValue() + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Operation Override (to avoid boxing of value classes in byte code) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - override fun fmtValue(fmt: String): String = - when (abs()) { - in ZERO..ofHz(500) -> "${toHz().fmt(fmt)} Hz" - in ofHz(500)..ofKHz(500) -> "${toKHz().fmt(fmt)} KHz" - in ofKHz(500)..ofMHz(500) -> "${toMHz().fmt(fmt)} MHz" - else -> "${toGHz().fmt(fmt)} GHz" + public override fun ifNeg0ThenPos0(): Frequency = Frequency(value.ifNeg0thenPos0()) + + public override operator fun plus(other: Frequency): Frequency = Frequency(value + other.value) + + public override operator fun minus(other: Frequency): Frequency = Frequency(value - other.value) + + public override operator fun div(scalar: Number): Frequency = Frequency(value / scalar.toDouble()) + + public override operator fun div(other: Frequency): Percentage = Percentage.ofRatio(value / other.value) + + public override operator fun times(scalar: Number): Frequency = Frequency(value * scalar.toDouble()) + + public override operator fun times(percentage: Percentage): Frequency = Frequency(value * percentage.value) + + public override operator fun unaryMinus(): Frequency = Frequency(-value) + + public override operator fun compareTo(other: Frequency): Int = this.value.compareTo(other.value) + + public override fun isZero(): Boolean = value == .0 + + public override fun approxZero(epsilon: Double): Boolean = value.approx(.0, epsilon = epsilon) + + public override fun approx( + other: Frequency, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this == other || this.value.approx(other.value, minEpsilon, epsilon) + + public override infix fun approx(other: Frequency): Boolean = approx(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLarger( + other: Frequency, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLarger(other.value, minEpsilon, epsilon) + + public override infix fun approxLarger(other: Frequency): Boolean = approxLarger(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLargerOrEq( + other: Frequency, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLargerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxLargerOrEq(other: Frequency): Boolean = approxLargerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmaller( + other: Frequency, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmaller(other.value, minEpsilon, epsilon) + + public override infix fun approxSmaller(other: Frequency): Boolean = approxSmaller(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmallerOrEq( + other: Frequency, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmallerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxSmallerOrEq(other: Frequency): Boolean = approxSmallerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override infix fun max(other: Frequency): Frequency = if (this.value > other.value) this else other + + public override infix fun min(other: Frequency): Frequency = if (this.value < other.value) this else other + + public override fun abs(): Frequency = Frequency(kotlin.math.abs(value)) + + public override fun roundToIfWithinEpsilon( + to: Frequency, + epsilon: Double, + ): Frequency = + if (this.value in (to.value - epsilon)..(to.value + epsilon)) { + to + } else { + this } - public operator fun times(time: Time): Double = toHz() * time.toSec() + public fun max( + a: Frequency, + b: Frequency, + ): Frequency = if (a.value > b.value) a else b + + public fun min( + a: Frequency, + b: Frequency, + ): Frequency = if (a.value < b.value) a else b + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Unit Specific Operations + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public operator fun times(timeDelta: TimeDelta): Double = toHz() * timeDelta.toSec() - public operator fun times(duration: Duration): Double = toHz() * duration.toTime().toSec() + public operator fun times(duration: Duration): Double = toHz() * duration.toTimeDelta().toSec() - public companion object { - @JvmStatic public val ZERO: Frequency = Frequency(.0) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Companion + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public companion object : UnitId<Frequency> { + @JvmStatic override val zero: Frequency = Frequency(.0) + + @JvmStatic override val max: Frequency = Frequency(Double.MAX_VALUE) + + @JvmStatic override val min: Frequency = Frequency(Double.MIN_VALUE) + + public operator fun Number.times(unit: Frequency): Frequency = unit * this @JvmStatic @JvmName("ofHz") @@ -87,9 +201,13 @@ public value class Frequency private constructor( private val HERTZ = Regex("\\s*(?:Hz|Hertz|hz|hertz)\\s*?") + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Serializer + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /** * Serializer for [Frequency] value class. It needs to be a compile - * time constant in order to be used as serializer automatically, + * time constant to be used as serializer automatically, * hence `object :` instead of class instantiation. * * ```json diff --git a/opendc-common/src/main/kotlin/org/opendc/common/units/Percentage.kt b/opendc-common/src/main/kotlin/org/opendc/common/units/Percentage.kt index 377fdecc..b9f8f7ae 100644 --- a/opendc-common/src/main/kotlin/org/opendc/common/units/Percentage.kt +++ b/opendc-common/src/main/kotlin/org/opendc/common/units/Percentage.kt @@ -20,38 +20,53 @@ * SOFTWARE. */ -@file:OptIn(InternalUse::class) +@file:OptIn(InternalUse::class, NonInlinableUnit::class) package org.opendc.common.units import kotlinx.serialization.Serializable -import mu.KotlinLogging import org.opendc.common.annotations.InternalUse +import org.opendc.common.utils.DFLT_MIN_EPS +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 org.opendc.common.utils.fmt import org.opendc.common.utils.ifNeg0thenPos0 import kotlin.text.RegexOption.IGNORE_CASE /** - * Represents a percentage. This interface has 2 value classes implementations. - * - * Using the interface instead of its implementation will likely result in worse - * performances compared to using the value-classes themselves, - * since the jvm will allocate an object for the interface. Therefore, it is suggested - * to use the interface as little as possible. Operations between the same implementation - * ([BoundedPercentage] + [BoundedPercentage]) will result in the same return type. - * - * [BoundedPercentage]s are adjusted to remain in range 0-100%, - * logging warning whenever an adjustment has been made. + * Represents a percentage. * * As all [Unit]s, offers the vast majority * of mathematical operations that one would perform on a simple [Double]. */ + +@JvmInline @Serializable(with = Percentage.Companion.PercentageSerializer::class) -public sealed interface Percentage : Unit<Percentage> { - override val value: Double +public value class Percentage( + override val value: Double, +) : Unit<Percentage> { + override fun toString(): String = fmtValue() + + /** + * ```kotlin + * // e.g. + * val perc: Percentage = Percentage.ofRatio(0.123456789) + * perc.fmtValue("%.4f") // "12.3456%" + * ``` + * + * @see[Unit.fmtValue] + */ + override fun fmtValue(fmt: String): String = "${toPercentageValue().fmt(fmt)}%" + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Conversions to Double + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** - * @return the value as a ratio (e.g. 50% -> 0.5) + * @return the value as a ratio (e.g., 50% -> 0.5) */ public fun toRatio(): Double = value @@ -60,75 +75,145 @@ public sealed interface Percentage : Unit<Percentage> { */ public fun toPercentageValue(): Double = value * 1e2 - /** - * @return *this* percentage converted to [BoundedPercentage]. - */ - public fun toBoundedPercentage(): BoundedPercentage + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Operation Override (to avoid boxing of value classes in byte code) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /** - * @return *this* percentage converted to [UnboundedPercentage]. - */ - public fun toUnboundedPercentage(): UnboundedPercentage + public override fun ifNeg0ThenPos0(): Percentage = Percentage(value.ifNeg0thenPos0()) - /** - * ```kotlin - * // e.g. - * val perc: Percentage = Percentage.ofRatio(0.123456789) - * perc.fmtValue("%.4f") // "12.3456%" - * ``` - * - * @see[Unit.fmtValue] - */ - override fun fmtValue(fmt: String): String = "${toPercentageValue().fmt(fmt)}%" + public override operator fun plus(other: Percentage): Percentage = Percentage(value + other.value) - public companion object { - @JvmStatic public val ZERO: Percentage = UnboundedPercentage(.0) + public override operator fun minus(other: Percentage): Percentage = Percentage(value - other.value) - @JvmStatic - @JvmName("ofRatio") - public fun ofRatio(ratio: Double): UnboundedPercentage = UnboundedPercentage(ratio) + public override operator fun div(scalar: Number): Percentage = Percentage(value / scalar.toDouble()) - @JvmStatic - @JvmName("ofRatioBounded") - public fun ofRatioBounded(ratio: Double): BoundedPercentage = BoundedPercentage(ratio) + public override operator fun div(other: Percentage): Percentage = Percentage.ofRatio(value / other.value) + + public override operator fun times(scalar: Number): Percentage = Percentage(value * scalar.toDouble()) + + public override operator fun times(percentage: Percentage): Percentage = Percentage(value * percentage.value) + + public override operator fun unaryMinus(): Percentage = Percentage(-value) + + public override operator fun compareTo(other: Percentage): Int = this.value.compareTo(other.value) + + public override fun isZero(): Boolean = value == .0 + + public override fun approxZero(epsilon: Double): Boolean = value.approx(.0, epsilon = epsilon) + + public override fun approx( + other: Percentage, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this == other || this.value.approx(other.value, minEpsilon, epsilon) + + public override infix fun approx(other: Percentage): Boolean = approx(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLarger( + other: Percentage, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLarger(other.value, minEpsilon, epsilon) + + public override infix fun approxLarger(other: Percentage): Boolean = approxLarger(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLargerOrEq( + other: Percentage, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLargerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxLargerOrEq(other: Percentage): Boolean = approxLargerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmaller( + other: Percentage, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmaller(other.value, minEpsilon, epsilon) + + public override infix fun approxSmaller(other: Percentage): Boolean = approxSmaller(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmallerOrEq( + other: Percentage, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmallerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxSmallerOrEq(other: Percentage): Boolean = approxSmallerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override infix fun max(other: Percentage): Percentage = if (this.value > other.value) this else other + + public override infix fun min(other: Percentage): Percentage = if (this.value < other.value) this else other + + public override fun abs(): Percentage = Percentage(kotlin.math.abs(value)) + + public override fun roundToIfWithinEpsilon( + to: Percentage, + epsilon: Double, + ): Percentage = + if (this.value in (to.value - epsilon)..(to.value + epsilon)) { + to + } else { + this + } + + public fun max( + a: Percentage, + b: Percentage, + ): Percentage = if (a.value > b.value) a else b + + public fun min( + a: Percentage, + b: Percentage, + ): Percentage = if (a.value < b.value) a else b + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Unit Specific Operations + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Companion + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public companion object : UnitId<Percentage> { + @JvmStatic override val zero: Percentage = Percentage(.0) + + @JvmStatic override val max: Percentage = Percentage(Double.MAX_VALUE) + + @JvmStatic override val min: Percentage = Percentage(Double.MIN_VALUE) + + public operator fun Number.times(unit: Percentage): Percentage = unit * this @JvmStatic - @JvmName("ofPercentage") - public fun ofPercentage(percentage: Number): UnboundedPercentage = UnboundedPercentage(percentage.toDouble() / 100) + @JvmName("ofRatio") + public fun ofRatio(ratio: Double): Percentage = Percentage(ratio) @JvmStatic - @JvmName("ofPercentageBounded") - public fun ofPercentageBounded(percentage: Double): BoundedPercentage = BoundedPercentage(percentage / 100) + @JvmName("ofPercentage") + public fun ofPercentage(percentage: Number): Percentage = Percentage(percentage.toDouble() / 100) /** * @return the percentage resulting from [this] / [other]. */ - public infix fun Number.percentageOf(other: Number): UnboundedPercentage = UnboundedPercentage(this.toDouble() / other.toDouble()) + public infix fun Number.percentageOf(other: Number): Percentage = Percentage(this.toDouble() / other.toDouble()) /** - * @return the *bounded* percentage resulting from [this] / [other]. + * @return the percentage resulting from [this] / [other], applicable on all [Unit]s of the same type. */ - public infix fun Number.boundedPercentageOf(other: Number): BoundedPercentage = - BoundedPercentage(this.toDouble() / other.toDouble()) + public infix fun <T : Unit<T>> T.percentageOf(other: T): Percentage = Percentage(this.value / other.value) - /** - * @return the percentage resulting from [this] / [other], applicable on all [Unit]s of same type. - */ - public infix fun <T : Unit<T>> T.percentageOf(other: T): UnboundedPercentage = UnboundedPercentage(this.value / other.value) - - /** - * @return the *bounded* percentage resulting from [this] / [other], applicable on all [Unit]s of same type. - */ - public infix fun <T : Unit<T>> T.boundedPercentageOf(other: T): BoundedPercentage = BoundedPercentage(this.value / other.value) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Serializer + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// private val PERCENTAGE = Regex("\\s*(?:percentage|Percentage|%)\\s*?") /** * Serializer for [Percentage] value class. It needs to be a compile - * time constant in order to be used as serializer automatically, + * time constant to be used as serializer automatically, * hence `object :` instead of class instantiation. * - * For implementation purposes it always deserialize an [UnboundedPercentage] as [Percentage]. + * For implementation purposes it always deserializes an [Percentage] as [Percentage]. * * ```json * // e.g. @@ -151,110 +236,3 @@ public sealed interface Percentage : Unit<Percentage> { ) } } - -/** - * Bounded implementation of [Percentage], meaning the - * percentage value is adjusted to always be in the range 0-100%, - * logging a warning whenever an adjustment has been made. - */ -@JvmInline -public value class BoundedPercentage - @InternalUse - internal constructor( - override val value: Double, - ) : Percentage { - override fun toBoundedPercentage(): BoundedPercentage = this - - override fun toUnboundedPercentage(): UnboundedPercentage = UnboundedPercentage(value) - - override fun new(value: Double): BoundedPercentage = BoundedPercentage(value.forceInRange().ifNeg0thenPos0()) - - override fun toString(): String = fmtValue() - - /** - * "Override" to return [BoundedPercentage] insteadof [Percentage]. - * @see[Unit.plus] - */ - public infix operator fun plus(other: BoundedPercentage): BoundedPercentage = BoundedPercentage(this.value + other.value) - - /** - * "Override" to return [BoundedPercentage] insteadof [Percentage]. - * @see[Unit.minus] - */ - public infix operator fun minus(other: BoundedPercentage): BoundedPercentage = BoundedPercentage(this.value - other.value) - - /** - * Override to return [BoundedPercentage] insteadof [Percentage]. - * @see[Unit.times] - */ - override operator fun times(scalar: Number): BoundedPercentage = BoundedPercentage(this.value * scalar.toDouble()) - - /** - * Override to return [BoundedPercentage] insteadof [Percentage]. - * @see[Unit.div] - */ - override operator fun div(scalar: Number): BoundedPercentage = BoundedPercentage(this.value / scalar.toDouble()) - - private fun Double.forceInRange( - from: Double = .0, - to: Double = 1.0, - ): Double = - if (this < from) { - LOG.warn("bounded percentage has been rounded up (from ${this * 1e2}% to ${from * 1e2}%") - from - } else if (this > to) { - LOG.warn("bounded percentage has been rounded down (from ${this * 1e2}% to ${to * 1e2}%") - to - } else { - this - } - - public companion object { - // TODO: replace with `by logger()` if pr #241 is approved - private val LOG = KotlinLogging.logger(name = this::class.java.enclosingClass.simpleName) - } - } - -/** - * Unbounded implementation of [Percentage], meaning the - * percentage value is allowed to be outside the range 0-100%. - */ -@JvmInline -public value class UnboundedPercentage - @InternalUse - internal constructor( - override val value: Double, - ) : Percentage { - override fun toBoundedPercentage(): BoundedPercentage = BoundedPercentage(value.ifNeg0thenPos0()) - - override fun toUnboundedPercentage(): UnboundedPercentage = this - - @InternalUse - override fun new(value: Double): UnboundedPercentage = UnboundedPercentage(value) - - override fun toString(): String = fmtValue() - - /** - * "Override" to return [UnboundedPercentage] insteadof [Percentage]. - * @see[Unit.plus] - */ - public infix operator fun plus(other: UnboundedPercentage): UnboundedPercentage = UnboundedPercentage(this.value + other.value) - - /** - * "Override" to return [UnboundedPercentage] insteadof [Percentage]. - * @see[Unit.minus] - */ - public infix operator fun minus(other: UnboundedPercentage): UnboundedPercentage = UnboundedPercentage(this.value - other.value) - - /** - * Override to return [UnboundedPercentage] insteadof [Percentage]. - * @see[Unit.times] - */ - override operator fun times(scalar: Number): UnboundedPercentage = UnboundedPercentage(this.value * scalar.toDouble()) - - /** - * Override to return [UnboundedPercentage] insteadof [Percentage]. - * @see[Unit.div] - */ - override operator fun div(scalar: Number): UnboundedPercentage = UnboundedPercentage(this.value / scalar.toDouble()) - } diff --git a/opendc-common/src/main/kotlin/org/opendc/common/units/Power.kt b/opendc-common/src/main/kotlin/org/opendc/common/units/Power.kt index fc9f6bf4..85fb95ae 100644 --- a/opendc-common/src/main/kotlin/org/opendc/common/units/Power.kt +++ b/opendc-common/src/main/kotlin/org/opendc/common/units/Power.kt @@ -20,13 +20,19 @@ * SOFTWARE. */ -@file:OptIn(InternalUse::class) +@file:OptIn(InternalUse::class, NonInlinableUnit::class) package org.opendc.common.units import kotlinx.serialization.Serializable 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.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 org.opendc.common.utils.fmt import org.opendc.common.utils.ifNeg0thenPos0 import java.time.Duration @@ -42,13 +48,6 @@ public value class Power private constructor( // In Watts. override val value: Double, ) : Unit<Power> { - @InternalUse - override fun new(value: Double): Power = Power(value.ifNeg0thenPos0()) - - public fun toWatts(): Double = value - - public fun toKWatts(): Double = value / 1000.0 - override fun toString(): String = fmtValue() override fun fmtValue(fmt: String): String = @@ -58,13 +57,126 @@ public value class Power private constructor( "${toWatts().fmt(fmt)} Watts" } - public operator fun times(time: Time): Energy = Energy.ofWh(toWatts() * time.toHours()) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Conversions to Double + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public fun toWatts(): Double = value - public operator fun times(duration: Duration): Energy = this * duration.toTime() + public fun toKWatts(): Double = value / 1000.0 - public companion object { - @JvmStatic - public val ZERO: Power = Power(.0) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Operation Override (to avoid boxing of value classes in byte code) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public override fun ifNeg0ThenPos0(): Power = Power(value.ifNeg0thenPos0()) + + public override operator fun plus(other: Power): Power = Power(value + other.value) + + public override operator fun minus(other: Power): Power = Power(value - other.value) + + public override operator fun div(scalar: Number): Power = Power(value / scalar.toDouble()) + + public override operator fun div(other: Power): Percentage = Percentage.ofRatio(value / other.value) + + public override operator fun times(scalar: Number): Power = Power(value * scalar.toDouble()) + + public override operator fun times(percentage: Percentage): Power = Power(value * percentage.value) + + public override operator fun unaryMinus(): Power = Power(-value) + + public override operator fun compareTo(other: Power): Int = this.value.compareTo(other.value) + + public override fun isZero(): Boolean = value == .0 + + public override fun approxZero(epsilon: Double): Boolean = value.approx(.0, epsilon = epsilon) + + public override fun approx( + other: Power, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this == other || this.value.approx(other.value, minEpsilon, epsilon) + + public override infix fun approx(other: Power): Boolean = approx(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLarger( + other: Power, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLarger(other.value, minEpsilon, epsilon) + + public override infix fun approxLarger(other: Power): Boolean = approxLarger(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLargerOrEq( + other: Power, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLargerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxLargerOrEq(other: Power): Boolean = approxLargerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmaller( + other: Power, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmaller(other.value, minEpsilon, epsilon) + + public override infix fun approxSmaller(other: Power): Boolean = approxSmaller(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmallerOrEq( + other: Power, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmallerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxSmallerOrEq(other: Power): Boolean = approxSmallerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override infix fun max(other: Power): Power = if (this.value > other.value) this else other + + public override infix fun min(other: Power): Power = if (this.value < other.value) this else other + + public override fun abs(): Power = Power(kotlin.math.abs(value)) + + public override fun roundToIfWithinEpsilon( + to: Power, + epsilon: Double, + ): Power = + if (this.value in (to.value - epsilon)..(to.value + epsilon)) { + to + } else { + this + } + + public fun max( + a: Power, + b: Power, + ): Power = if (a.value > b.value) a else b + + public fun min( + a: Power, + b: Power, + ): Power = if (a.value < b.value) a else b + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Unit Specific Operations + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public operator fun times(timeDelta: TimeDelta): Energy = Energy.ofWh(toWatts() * timeDelta.toHours()) + + public operator fun times(duration: Duration): Energy = this * duration.toTimeDelta() + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Companion + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public companion object : UnitId<Power> { + @JvmStatic override val zero: Power = Power(.0) + + @JvmStatic override val max: Power = Power(Double.MAX_VALUE) + + @JvmStatic override val min: Power = Power(Double.MIN_VALUE) + + public operator fun Number.times(unit: Power): Power = unit * this @JvmStatic @JvmName("ofWatts") @@ -74,9 +186,13 @@ public value class Power private constructor( @JvmName("ofKWatts") public fun ofKWatts(kWatts: Number): Power = Power(kWatts.toDouble() * 1000.0) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Serializer + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /** * Serializer for [Power] value class. It needs to be a compile - * time constant in order to be used as serializer automatically, + * time constant to be used as serializer automatically, * hence `object :` instead of class instantiation. * * ```json diff --git a/opendc-common/src/main/kotlin/org/opendc/common/units/Time.kt b/opendc-common/src/main/kotlin/org/opendc/common/units/Time.kt deleted file mode 100644 index 9d72ddfc..00000000 --- a/opendc-common/src/main/kotlin/org/opendc/common/units/Time.kt +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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 kotlinx.serialization.Serializable -import org.opendc.common.annotations.InternalUse -import org.opendc.common.utils.ifNeg0thenPos0 -import java.time.Duration -import java.time.Instant -import kotlin.text.RegexOption.IGNORE_CASE - -/** - * Represents time values. - * @see[Unit] - */ -@JvmInline -@Serializable(with = Time.Companion.TimeSerializer::class) -public value class Time private constructor( - // In milliseconds. - public override val value: Double, -) : Unit<Time> { - @InternalUse - override fun new(value: Double): Time = Time(value.ifNeg0thenPos0()) - - public fun toNs(): Double = value * 1e6 - - public fun toMicros(): Double = value * 1e3 - - public fun toMs(): Double = value - - public fun toMsLong(): Long = value.toLong() - - public fun toSec(): Double = value / 1000.0 - - public fun toMin(): Double = toSec() / 60 - - public fun toHours(): Double = toMin() / 60 - - public fun toInstantFromEpoch(): Instant = Instant.ofEpochMilli(value.toLong()) - - override fun toString(): String = fmtValue() - - /** - * @return the [Duration] [toString] result of this time value. - */ - override fun fmtValue(fmt: String): String = Duration.ofMillis(value.toLong()).toString() - - public operator fun times(power: Power): Energy = Energy.ofWh(toHours() * power.toWatts()) - - public operator fun times(dataRate: DataRate): DataSize = DataSize.ofKB(toSec() * dataRate.toKBps()) - - public companion object { - @JvmStatic public val ZERO: Time = Time(.0) - - @JvmStatic - @JvmName("ofNanos") - public fun ofNanos(nanos: Number): Time = Time(nanos.toDouble() / 1e6) - - @JvmStatic - @JvmName("ofMicros") - public fun ofMicros(micros: Number): Time = Time(micros.toDouble() / 1e3) - - @JvmStatic - @JvmName("ofMillis") - public fun ofMillis(ms: Number): Time = Time(ms.toDouble()) - - @JvmStatic - @JvmName("ofSec") - public fun ofSec(sec: Number): Time = Time(sec.toDouble() * 1000.0) - - @JvmStatic - @JvmName("ofMin") - public fun ofMin(min: Number): Time = Time(min.toDouble() * 60 * 1000.0) - - @JvmStatic - @JvmName("ofHours") - public fun ofHours(hours: Number): Time = Time(hours.toDouble() * 60 * 60 * 1000.0) - - @JvmStatic - @JvmName("ofDuration") - public fun ofDuration(duration: Duration): Time = duration.toTime() - - @JvmStatic - @JvmName("ofInstantFromEpoch") - public fun ofInstantFromEpoch(instant: Instant): Time = ofMillis(instant.toEpochMilli()) - - /** - * Serializer for [Time] value class. It needs to be a compile - * time constant in order to be used as serializer automatically, - * hence `object :` instead of class instantiation. - * - * ```json - * // e.g. - * "time": "10 hours" - * "time": " 30 minutes " - * "time": "1 ms" - * "time": "PT13H" - * // etc. - * ``` - */ - internal object TimeSerializer : UnitSerializer<Time>( - ifNumber = { - LOG.warn( - "deserialization of number with no unit of measure, assuming it is in milliseconds." + - "Keep in mind that you can also specify the value as '$it ms'", - ) - ofMillis(it.toDouble()) - }, - serializerFun = { this.encodeString(it.toString()) }, - ifMatches("$NUM_GROUP$NANO$SEC(?:|s)\\s*", IGNORE_CASE) { ofNanos(json.decNumFromStr(groupValues[1])) }, - ifMatches("$NUM_GROUP$MICRO$SEC(?:|s)\\s*", IGNORE_CASE) { ofMicros(json.decNumFromStr(groupValues[1])) }, - ifMatches("$NUM_GROUP$MILLI$SEC(?:|s)\\s*", IGNORE_CASE) { ofMillis(json.decNumFromStr(groupValues[1])) }, - ifMatches("$NUM_GROUP$SEC(?:|s)\\s*", IGNORE_CASE) { ofSec(json.decNumFromStr(groupValues[1])) }, - ifMatches("$NUM_GROUP$MIN(?:|s)\\s*") { ofMin(json.decNumFromStr(groupValues[1])) }, - ifMatches("$NUM_GROUP$HOUR(?:|s)\\s*") { ofHours(json.decNumFromStr(groupValues[1])) }, - ifNoExc { ofDuration(Duration.parse(this)) }, - ifNoExc { ofInstantFromEpoch(Instant.parse(this)) }, - ) - - /** - * @return [this] converted to a [Time] value, with the highest possible accuracy. - * - * @throws RuntimeException if [this] cannot be represented as nanos, millis, seconds, minutes or hours with a [Long]. - */ - public fun Duration.toTime(): Time { - fun tryNoThrow(block: () -> Time?) = - try { - block() - } catch (_: Exception) { - null - } - - return tryNoThrow { ofNanos(this.toNanos()) } - ?: tryNoThrow { ofMillis(this.toMillis()) } - ?: tryNoThrow { ofSec(this.toSeconds()) } - ?: tryNoThrow { ofMin(this.toMinutes()) } - ?: tryNoThrow { ofHours(this.toHours()) } - ?: throw RuntimeException( - "duration $this cannot be converted to ${Time::class.simpleName}, " + - "duration value overflow Long representation of nanos, millis, seconds, minutes and hours", - ) - } - } -} diff --git a/opendc-common/src/main/kotlin/org/opendc/common/units/TimeDelta.kt b/opendc-common/src/main/kotlin/org/opendc/common/units/TimeDelta.kt new file mode 100644 index 00000000..9f3857a8 --- /dev/null +++ b/opendc-common/src/main/kotlin/org/opendc/common/units/TimeDelta.kt @@ -0,0 +1,277 @@ +/* + * 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, NonInlinableUnit::class) + +package org.opendc.common.units + +import kotlinx.serialization.Serializable +import org.opendc.common.annotations.InternalUse +import org.opendc.common.utils.DFLT_MIN_EPS +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 org.opendc.common.utils.ifNeg0thenPos0 +import java.time.Duration +import java.time.Instant +import kotlin.text.RegexOption.IGNORE_CASE + +/** + * Represents time interval values. + * @see[Unit] + */ +@JvmInline +@Serializable(with = TimeDelta.Companion.TimeDeltaSerializer::class) +public value class TimeDelta private constructor( + // In milliseconds. + public override val value: Double, +) : Unit<TimeDelta> { + override fun toString(): String = fmtValue() + + override fun fmtValue(fmt: String): String = Duration.ofMillis(value.toLong()).toString() + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Conversions to Double + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public fun toNs(): Double = value * 1e6 + + public fun toMicros(): Double = value * 1e3 + + public fun toMs(): Double = value + + public fun toMsLong(): Long = value.toLong() + + public fun toSec(): Double = value / 1000.0 + + public fun toMin(): Double = toSec() / 60 + + public fun toHours(): Double = toMin() / 60 + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Operation Override (to avoid boxing of value classes in byte code) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public override fun ifNeg0ThenPos0(): TimeDelta = TimeDelta(value.ifNeg0thenPos0()) + + public override operator fun plus(other: TimeDelta): TimeDelta = TimeDelta(value + other.value) + + public override operator fun minus(other: TimeDelta): TimeDelta = TimeDelta(value - other.value) + + public override operator fun div(scalar: Number): TimeDelta = TimeDelta(value / scalar.toDouble()) + + public override operator fun div(other: TimeDelta): Percentage = Percentage.ofRatio(value / other.value) + + public override operator fun times(scalar: Number): TimeDelta = TimeDelta(value * scalar.toDouble()) + + public override operator fun times(percentage: Percentage): TimeDelta = TimeDelta(value * percentage.value) + + public override operator fun unaryMinus(): TimeDelta = TimeDelta(-value) + + public override operator fun compareTo(other: TimeDelta): Int = this.value.compareTo(other.value) + + public override fun isZero(): Boolean = value == .0 + + public override fun approxZero(epsilon: Double): Boolean = value.approx(.0, epsilon = epsilon) + + public override fun approx( + other: TimeDelta, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this == other || this.value.approx(other.value, minEpsilon, epsilon) + + public override infix fun approx(other: TimeDelta): Boolean = approx(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLarger( + other: TimeDelta, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLarger(other.value, minEpsilon, epsilon) + + public override infix fun approxLarger(other: TimeDelta): Boolean = approxLarger(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLargerOrEq( + other: TimeDelta, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLargerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxLargerOrEq(other: TimeDelta): Boolean = approxLargerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmaller( + other: TimeDelta, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmaller(other.value, minEpsilon, epsilon) + + public override infix fun approxSmaller(other: TimeDelta): Boolean = approxSmaller(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmallerOrEq( + other: TimeDelta, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmallerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxSmallerOrEq(other: TimeDelta): Boolean = approxSmallerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override infix fun max(other: TimeDelta): TimeDelta = if (this.value > other.value) this else other + + public override infix fun min(other: TimeDelta): TimeDelta = if (this.value < other.value) this else other + + public override fun abs(): TimeDelta = TimeDelta(kotlin.math.abs(value)) + + public override fun roundToIfWithinEpsilon( + to: TimeDelta, + epsilon: Double, + ): TimeDelta = + if (this.value in (to.value - epsilon)..(to.value + epsilon)) { + to + } else { + this + } + + public fun max( + a: TimeDelta, + b: TimeDelta, + ): TimeDelta = if (a.value > b.value) a else b + + public fun min( + a: TimeDelta, + b: TimeDelta, + ): TimeDelta = if (a.value < b.value) a else b + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Unit Specific Operations + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public fun toInstantFromEpoch(): Instant = Instant.ofEpochMilli(value.toLong()) + + public operator fun times(power: Power): Energy = Energy.ofWh(toHours() * power.toWatts()) + + public operator fun times(dataRate: DataRate): DataSize = DataSize.ofKB(toSec() * dataRate.toKBps()) + + public companion object : UnitId<TimeDelta> { + @JvmStatic override val zero: TimeDelta = TimeDelta(.0) + + @JvmStatic override val max: TimeDelta = TimeDelta(Double.MAX_VALUE) + + @JvmStatic override val min: TimeDelta = TimeDelta(Double.MIN_VALUE) + + public operator fun Number.times(unit: TimeDelta): TimeDelta = unit * this + + @JvmStatic + @JvmName("ofNanos") + public fun ofNanos(nanos: Number): TimeDelta = TimeDelta(nanos.toDouble() / 1e6) + + @JvmStatic + @JvmName("ofMicros") + public fun ofMicros(micros: Number): TimeDelta = TimeDelta(micros.toDouble() / 1e3) + + @JvmStatic + @JvmName("ofMillis") + public fun ofMillis(ms: Number): TimeDelta = TimeDelta(ms.toDouble()) + + @JvmStatic + @JvmName("ofSec") + public fun ofSec(sec: Number): TimeDelta = TimeDelta(sec.toDouble() * 1000.0) + + @JvmStatic + @JvmName("ofMin") + public fun ofMin(min: Number): TimeDelta = TimeDelta(min.toDouble() * 60 * 1000.0) + + @JvmStatic + @JvmName("ofHours") + public fun ofHours(hours: Number): TimeDelta = TimeDelta(hours.toDouble() * 60 * 60 * 1000.0) + + @JvmStatic + @JvmName("ofDuration") + public fun ofDuration(duration: Duration): TimeDelta = duration.toTimeDelta() + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Serializer + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Serializer for [TimeDelta] value class. It needs to be a compile + * time constant to be used as serializer automatically, + * hence `object :` instead of class instantiation. + * + * ```json + * // e.g. + * "timedelta": "10 hours" + * "timedelta": " 30 minutes " + * "timedelta": "1 ms" + * "timedelta": "PT13H" + * // etc. + * ``` + */ + internal object TimeDeltaSerializer : UnitSerializer<TimeDelta>( + ifNumber = { + LOG.warn( + "deserialization of number with no unit of measure, assuming it is in milliseconds." + + "Keep in mind that you can also specify the value as '$it ms'", + ) + ofMillis(it.toDouble()) + }, + serializerFun = { this.encodeString(it.toString()) }, + ifMatches("$NUM_GROUP$NANO$SEC(?:|s)\\s*", IGNORE_CASE) { ofNanos(json.decNumFromStr(groupValues[1])) }, + ifMatches("$NUM_GROUP$MICRO$SEC(?:|s)\\s*", IGNORE_CASE) { ofMicros(json.decNumFromStr(groupValues[1])) }, + ifMatches("$NUM_GROUP$MILLI$SEC(?:|s)\\s*", IGNORE_CASE) { ofMillis(json.decNumFromStr(groupValues[1])) }, + ifMatches("$NUM_GROUP$SEC(?:|s)\\s*", IGNORE_CASE) { ofSec(json.decNumFromStr(groupValues[1])) }, + ifMatches("$NUM_GROUP$MIN(?:|s)\\s*") { ofMin(json.decNumFromStr(groupValues[1])) }, + ifMatches("$NUM_GROUP$HOUR(?:|s)\\s*") { ofHours(json.decNumFromStr(groupValues[1])) }, + ifNoExc { ofDuration(Duration.parse(this)) }, + ifNoExc { + val instant = Instant.parse(this) + LOG.warn("`TimeDelta` value was expected but `Instant` string representation found. Converting to `TimeDelta` since Epoch") + + ofMillis(instant.toEpochMilli()) + }, + ) + + /** + * @return [this] converted to a [TimeDelta] value, with the highest possible accuracy. + * + * @throws RuntimeException if [this] cannot be represented as nanos, millis, seconds, minutes or hours with a [Long]. + */ + public fun Duration.toTimeDelta(): TimeDelta { + fun tryNoThrow(block: () -> TimeDelta?) = + try { + block() + } catch (_: Exception) { + null + } + + return tryNoThrow { ofNanos(this.toNanos()) } + ?: tryNoThrow { ofMillis(this.toMillis()) } + ?: tryNoThrow { ofSec(this.toSeconds()) } + ?: tryNoThrow { ofMin(this.toMinutes()) } + ?: tryNoThrow { ofHours(this.toHours()) } + ?: throw RuntimeException( + "duration $this cannot be converted to ${TimeDelta::class.simpleName}, " + + "duration value overflow Long representation of nanos, millis, seconds, minutes and hours", + ) + } + } +} diff --git a/opendc-common/src/main/kotlin/org/opendc/common/units/Timestamp.kt b/opendc-common/src/main/kotlin/org/opendc/common/units/Timestamp.kt new file mode 100644 index 00000000..2706e07c --- /dev/null +++ b/opendc-common/src/main/kotlin/org/opendc/common/units/Timestamp.kt @@ -0,0 +1,273 @@ +/* + * 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, NonInlinableUnit::class) + +package org.opendc.common.units + +import kotlinx.serialization.Serializable +import org.opendc.common.annotations.InternalUse +import org.opendc.common.units.TimeDelta.Companion.toTimeDelta +import org.opendc.common.utils.DFLT_MIN_EPS +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 org.opendc.common.utils.ifNeg0thenPos0 +import java.time.Duration +import java.time.Instant + +/** + * Represents timestamp values. + * @see[Unit] + */ +@JvmInline +@Serializable(with = Timestamp.Companion.TimeStampSerializer::class) +public value class Timestamp private constructor( + // In milliseconds since the Epoch. + public override val value: Double, +) : Unit<Timestamp> { + override fun toString(): String = fmtValue() + + /** + * @return the [Instant] [toString] result of this [Timestamp] value. + * @param[fmt] no ops. + */ + override fun fmtValue(fmt: String): String = toInstant().toString() + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Conversions to Double + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public fun toEpochNs(): Double = value * 1e6 + + public fun toEpochMicros(): Double = value * 1e3 + + public fun toEpochMs(): Double = value + + public fun toEpochSec(): Double = value / 1000.0 + + public fun toEpochMin(): Double = toEpochSec() / 60 + + public fun toEpochHours(): Double = toEpochMin() / 60 + + public fun toEpochDays(): Double = toEpochHours() / 24 + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Operation Override (to avoid boxing of value classes in byte code) + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public override fun ifNeg0ThenPos0(): Timestamp = Timestamp(value.ifNeg0thenPos0()) + + @UnintendedOperation + public override operator fun plus(other: Timestamp): Timestamp = throw UnitOperationException() + + @UnintendedOperation + public override operator fun minus(other: Timestamp): Timestamp = throw UnitOperationException() + + public override operator fun div(scalar: Number): Timestamp = Timestamp(value / scalar.toDouble()) + + public override operator fun div(other: Timestamp): Percentage = Percentage.ofRatio(value / other.value) + + public override operator fun times(scalar: Number): Timestamp = Timestamp(value * scalar.toDouble()) + + @UnintendedOperation + public override operator fun times(percentage: Percentage): Timestamp = throw UnitOperationException() + + @UnintendedOperation + public override operator fun unaryMinus(): Timestamp = throw UnitOperationException() + + public override operator fun compareTo(other: Timestamp): Int = this.value.compareTo(other.value) + + public override fun isZero(): Boolean = value == .0 + + public override fun approxZero(epsilon: Double): Boolean = value.approx(.0, epsilon = epsilon) + + public override fun approx( + other: Timestamp, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this == other || this.value.approx(other.value, minEpsilon, epsilon) + + public override infix fun approx(other: Timestamp): Boolean = approx(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLarger( + other: Timestamp, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLarger(other.value, minEpsilon, epsilon) + + public override infix fun approxLarger(other: Timestamp): Boolean = approxLarger(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxLargerOrEq( + other: Timestamp, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxLargerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxLargerOrEq(other: Timestamp): Boolean = approxLargerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmaller( + other: Timestamp, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmaller(other.value, minEpsilon, epsilon) + + public override infix fun approxSmaller(other: Timestamp): Boolean = approxSmaller(other, minEpsilon = DFLT_MIN_EPS) + + public override fun approxSmallerOrEq( + other: Timestamp, + minEpsilon: Double, + epsilon: Double, + ): Boolean = this.value.approxSmallerOrEq(other.value, minEpsilon, epsilon) + + public override infix fun approxSmallerOrEq(other: Timestamp): Boolean = approxSmallerOrEq(other, minEpsilon = DFLT_MIN_EPS) + + public override infix fun max(other: Timestamp): Timestamp = if (this.value > other.value) this else other + + public override infix fun min(other: Timestamp): Timestamp = if (this.value < other.value) this else other + + public override fun abs(): Timestamp = Timestamp(kotlin.math.abs(value)) + + public override fun roundToIfWithinEpsilon( + to: Timestamp, + epsilon: Double, + ): Timestamp = + if (this.value in (to.value - epsilon)..(to.value + epsilon)) { + to + } else { + this + } + + public fun max( + a: Timestamp, + b: Timestamp, + ): Timestamp = if (a.value > b.value) a else b + + public fun min( + a: Timestamp, + b: Timestamp, + ): Timestamp = if (a.value < b.value) a else b + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Unit Specific Operations + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public operator fun minus(timeDelta: TimeDelta): Timestamp = Timestamp(value - timeDelta.value) + + public operator fun plus(timeDelta: TimeDelta): Timestamp = Timestamp(value + timeDelta.value) + + /** + * @return the [TimeDelta] between *this* and [other]. Be aware that this is not the absolute value, it can be negative. + */ + public infix fun timeDelta(other: Timestamp): TimeDelta = this.toTimeDeltaFromEpoch() - other.toTimeDeltaFromEpoch() + + public fun toInstant(): Instant = + if (toEpochMs() > Long.MAX_VALUE) { + Instant.ofEpochSecond(toEpochSec().toLong()) + } else { + Instant.ofEpochMilli(toEpochMs().toLong()) + } + + public fun toTimeDeltaFromEpoch(): TimeDelta = TimeDelta.ofMillis(toEpochMs()) + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Companion + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public companion object : UnitId<Timestamp> { + @JvmStatic override val zero: Timestamp = Timestamp(.0) + + @JvmStatic override val max: Timestamp = Timestamp(Double.MAX_VALUE) + + @JvmStatic override val min: Timestamp = Timestamp(Double.MIN_VALUE) + + public operator fun Number.times(unit: Timestamp): Timestamp = unit * this + + @JvmStatic + @JvmName("ofEpochNs") + public fun ofEpochNs(nanos: Number): Timestamp = Timestamp(nanos.toDouble() / 1e6) + + @JvmStatic + @JvmName("ofEpochMicros") + public fun ofEpochMicros(micros: Number): Timestamp = Timestamp(micros.toDouble() / 1e3) + + @JvmStatic + @JvmName("ofEpochMs") + public fun ofEpochMs(ms: Number): Timestamp = Timestamp(ms.toDouble()) + + @JvmStatic + @JvmName("ofEpochSec") + public fun ofEpochSec(sec: Number): Timestamp = ofEpochMs(sec.toDouble() * 1000.0) + + @JvmStatic + @JvmName("ofEpochMin") + public fun ofEpochMin(sec: Number): Timestamp = ofEpochSec(sec.toDouble() * 60) + + @JvmStatic + @JvmName("ofEpochHours") + public fun ofEpochHours(sec: Number): Timestamp = ofEpochMin(sec.toDouble() * 60) + + @JvmStatic + @JvmName("ofInstant") + public fun ofInstant(instant: Instant): Timestamp = ofEpochMs(instant.toEpochMilli()) + + @JvmStatic + @JvmName("toTimestamp") + public fun Instant.toTimestamp(): Timestamp = ofEpochMs(toEpochMilli()) + + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Serializer + // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Serializer for [Timestamp] value class. It needs to be a compile + * time constant to be used as serializer automatically, + * hence `object :` instead of class instantiation. + * + * ```json + * // e.g. + * "timestamp": "10000" // 10,000 ms since Epoch + * "timestamp": "2001-09-09T01:48:19Z" + * // etc. + * ``` + */ + internal object TimeStampSerializer : UnitSerializer<Timestamp>( + ifNumber = { + LOG.warn( + "deserialization of number with no unit of measure, assuming it is in milliseconds since Epoch." + + "Keep in mind that you can also specify the value with timestamp representation (e.g. '2001-09-09T01:48:19Z')", + ) + ofEpochMs(it.toDouble()) + }, + serializerFun = { this.encodeString(it.toString()) }, + ifNoExc { ofInstant(Instant.parse(this)) }, + ifNoExc { + val duration = Duration.parse(this) + LOG.warn("timestamp value was expected but duration string representation found. Assuming it is a duration since Epoch.") + + ofEpochMs(duration.toTimeDelta().toMs()) + }, + ) + } +} 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 +} diff --git a/opendc-common/src/main/kotlin/org/opendc/common/units/UnitSerializer.kt b/opendc-common/src/main/kotlin/org/opendc/common/units/UnitSerializer.kt index aaf18498..45d2b3a4 100644 --- a/opendc-common/src/main/kotlin/org/opendc/common/units/UnitSerializer.kt +++ b/opendc-common/src/main/kotlin/org/opendc/common/units/UnitSerializer.kt @@ -20,6 +20,8 @@ * SOFTWARE. */ +@file:OptIn(NonInlinableUnit::class) + package org.opendc.common.units import kotlinx.serialization.KSerializer @@ -32,7 +34,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonTransformingSerializer -import mu.KotlinLogging +import org.opendc.common.logger.logger /** * Serializer for [T]. @@ -84,8 +86,7 @@ internal open class UnitSerializer<T : Unit<T>>( }, ) { companion object { - // TODO: replace with `by logger()` if pr #241 is approved - val LOG = KotlinLogging.logger(name = this::class.java.enclosingClass.simpleName) + val LOG by logger() val json = Json |
