summaryrefslogtreecommitdiff
path: root/opendc-common/src/main/kotlin/org
diff options
context:
space:
mode:
authorAlessio Leonardo Tomei <122273875+T0mexX@users.noreply.github.com>2025-03-18 10:31:21 +0100
committerGitHub <noreply@github.com>2025-03-18 10:31:21 +0100
commit46ba81a45f7cb10c7f870bbf6946a46207ee353c (patch)
treeef324e77ff6ba6d569084ff19efbc80162158bab /opendc-common/src/main/kotlin/org
parent582c45b6457bc9dc6fed57a843c87097db991d4a (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')
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/DataRate.kt149
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/DataSize.kt152
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/Energy.kt145
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/Frequency.kt148
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/Percentage.kt312
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/Power.kt146
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/Time.kt166
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/TimeDelta.kt277
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/Timestamp.kt273
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/Unit.kt307
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/UnitSerializer.kt7
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"
* ```
* &nbsp;
* ###### 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