summaryrefslogtreecommitdiff
path: root/opendc-common/src
diff options
context:
space:
mode:
Diffstat (limited to 'opendc-common/src')
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/annotations/InternalUse.kt28
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/DataRate.kt183
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/DataSize.kt220
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/Energy.kt118
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/Frequency.kt115
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/Percentage.kt260
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/Power.kt102
-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/Unit.kt386
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/units/UnitSerializer.kt215
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/utils/DoubleUtils.kt225
11 files changed, 2018 insertions, 0 deletions
diff --git a/opendc-common/src/main/kotlin/org/opendc/common/annotations/InternalUse.kt b/opendc-common/src/main/kotlin/org/opendc/common/annotations/InternalUse.kt
new file mode 100644
index 00000000..e32aa811
--- /dev/null
+++ b/opendc-common/src/main/kotlin/org/opendc/common/annotations/InternalUse.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+package org.opendc.common.annotations
+
+@RequiresOptIn(message = "This symbol is for internal use only")
+@Retention(AnnotationRetention.BINARY)
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.CONSTRUCTOR)
+public annotation class InternalUse
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
new file mode 100644
index 00000000..2af45b7b
--- /dev/null
+++ b/opendc-common/src/main/kotlin/org/opendc/common/units/DataRate.kt
@@ -0,0 +1,183 @@
+/*
+ * 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.units.Time.Companion.toTime
+import org.opendc.common.utils.ifNeg0thenPos0
+import java.time.Duration
+
+/**
+ * Represents data-rate values.
+ * @see[Unit]
+ */
+@JvmInline
+@Serializable(with = DataRate.Companion.DataRateSerializer::class)
+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())
+
+ public fun tobps(): Double = value
+
+ public fun toKibps(): Double = value / 1024
+
+ public fun toKbps(): Double = value / 1e3
+
+ public fun toKiBps(): Double = toKibps() / 8
+
+ public fun toKBps(): Double = toKbps() / 8
+
+ public fun toMibps(): Double = toKibps() / 1024
+
+ public fun toMbps(): Double = toKbps() / 1e3
+
+ public fun toMiBps(): Double = toMibps() / 8
+
+ public fun toMBps(): Double = toMbps() / 8
+
+ public fun toGibps(): Double = toMibps() / 1024
+
+ public fun toGbps(): Double = toMbps() / 1e3
+
+ public fun toGiBps(): Double = toGibps() / 8
+
+ public fun toGBps(): Double = toGbps() / 8
+
+ 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"
+ }
+
+ public operator fun times(time: Time): DataSize = DataSize.ofKiB(toKiBps() * time.toSec())
+
+ public operator fun times(duration: Duration): DataSize = this * duration.toTime()
+
+ public companion object {
+ @JvmStatic public val ZERO: DataRate = DataRate(.0)
+
+ @JvmStatic
+ @JvmName("ofbps")
+ public fun ofbps(bps: Number): DataRate = DataRate(bps.toDouble())
+
+ @JvmStatic
+ @JvmName("ofBps")
+ public fun ofBps(Bps: Number): DataRate = ofbps(Bps.toDouble() * 8)
+
+ @JvmStatic
+ @JvmName("ofKibps")
+ public fun ofKibps(kibps: Number): DataRate = ofbps(kibps.toDouble() * 1024)
+
+ @JvmStatic
+ @JvmName("ofKbps")
+ public fun ofKbps(kbps: Number): DataRate = ofbps(kbps.toDouble() * 1e3)
+
+ @JvmStatic
+ @JvmName("ofKiBps")
+ public fun ofKiBps(kiBps: Number): DataRate = ofKibps(kiBps.toDouble() * 8)
+
+ @JvmStatic
+ @JvmName("ofKBps")
+ public fun ofKBps(kBps: Number): DataRate = ofKbps(kBps.toDouble() * 8)
+
+ @JvmStatic
+ @JvmName("ofMibps")
+ public fun ofMibps(mibps: Number): DataRate = ofKibps(mibps.toDouble() * 1024)
+
+ @JvmStatic
+ @JvmName("ofMbps")
+ public fun ofMbps(mbps: Number): DataRate = ofKbps(mbps.toDouble() * 1e3)
+
+ @JvmStatic
+ @JvmName("ofMiBps")
+ public fun ofMiBps(miBps: Number): DataRate = ofMibps(miBps.toDouble() * 8)
+
+ @JvmStatic
+ @JvmName("ofMBps")
+ public fun ofMBps(mBps: Number): DataRate = ofMbps(mBps.toDouble() * 8)
+
+ @JvmStatic
+ @JvmName("ofGibps")
+ public fun ofGibps(gibps: Number): DataRate = ofMibps(gibps.toDouble() * 1024)
+
+ @JvmStatic
+ @JvmName("ofGbps")
+ public fun ofGbps(gbps: Number): DataRate = ofMbps(gbps.toDouble() * 1e3)
+
+ @JvmStatic
+ @JvmName("ofGiBps")
+ public fun ofGiBps(giBps: Number): DataRate = ofGibps(giBps.toDouble() * 8)
+
+ @JvmStatic
+ @JvmName("ofGBps")
+ public fun ofGBps(gBps: Number): DataRate = ofGbps(gBps.toDouble() * 8)
+
+ /**
+ * Serializer for [DataRate] 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.
+ * "data-rate": "1 Gbps"
+ * "data-rate": "10KBps"
+ * "data-rate": " 0.3 GBps "
+ * // etc.
+ * ```
+ */
+ internal object DataRateSerializer : UnitSerializer<DataRate>(
+ ifNumber = {
+ LOG.warn(
+ "deserialization of number with no unit of measure, assuming it is in Kibps." +
+ "Keep in mind that you can also specify the value as '$it Kibps'",
+ )
+ ofKibps(it.toDouble())
+ },
+ serializerFun = { this.encodeString(it.toString()) },
+ ifMatches("$NUM_GROUP$BITS$PER$SEC") { ofbps(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$BYTES$PER$SEC") { ofBps(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$KIBI$BITS$PER$SEC") { ofKibps(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$KILO$BITS$PER$SEC") { ofKbps(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$KIBI$BYTES$PER$SEC") { ofKiBps(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$KILO$BYTES$PER$SEC") { ofKBps(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$MEBI$BITS$PER$SEC") { ofMibps(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$MEGA$BITS$PER$SEC") { ofMbps(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$MEBI$BYTES$PER$SEC") { ofMiBps(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$MEGA$BYTES$PER$SEC") { ofMBps(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$GIBI$BITS$PER$SEC") { ofGibps(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$GIGA$BITS$PER$SEC") { ofGbps(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$GIBI$BYTES$PER$SEC") { ofGiBps(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$GIGA$BYTES$PER$SEC") { ofGBps(json.decNumFromStr(groupValues[1])) },
+ )
+ }
+}
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
new file mode 100644
index 00000000..e32d9e88
--- /dev/null
+++ b/opendc-common/src/main/kotlin/org/opendc/common/units/DataSize.kt
@@ -0,0 +1,220 @@
+/*
+ * 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.units.Time.Companion.toTime
+import org.opendc.common.utils.fmt
+import java.time.Duration
+
+/**
+ * Represents data size value.
+ * @see[Unit]
+ */
+@JvmInline
+@Serializable(with = DataSize.Companion.DataSerializer::class)
+public value class DataSize private constructor(
+ // In MiB.
+ override val value: Double,
+) : Unit<DataSize> {
+ @InternalUse
+ override fun new(value: Double): DataSize = DataSize(value)
+
+ public fun toBits(): Double = toKib() * 1024
+
+ public fun toBytes(): Double = toKiB() * 1024
+
+ // Metric prefixes.
+
+ public fun toKb(): Double = toBits() / 1e3
+
+ public fun toKB(): Double = toBytes() / 1e3
+
+ public fun toMb(): Double = toKb() / 1e3
+
+ public fun toMB(): Double = toKB() / 1e3
+
+ public fun toGb(): Double = toMb() / 1e3
+
+ public fun toGB(): Double = toMB() / 1e3
+
+ public fun toTb(): Double = toGb() / 1e3
+
+ public fun toTB(): Double = toGB() / 1e3
+
+ // Binary prefixes.
+
+ public fun toKib(): Double = toMib() * 1024
+
+ public fun toKiB(): Double = toMiB() * 1024
+
+ public fun toMib(): Double = toMiB() * 8
+
+ public fun toMiB(): Double = value
+
+ public fun toGib(): Double = toMib() / 1024
+
+ public fun toGiB(): Double = toMiB() / 1024
+
+ public fun toTib(): Double = toGib() / 1024
+
+ public fun toTiB(): Double = toGiB() / 1024
+
+ 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"
+ }
+
+ public operator fun div(time: Time): DataRate = DataRate.ofKBps(this.toKiB() / time.toSec())
+
+ public operator fun div(duration: Duration): DataRate = this / duration.toTime()
+
+ public companion object {
+ @JvmStatic public val ZERO: DataSize = DataSize(.0)
+
+ @JvmStatic
+ @JvmName("ofBits")
+ public fun ofBits(bits: Number): DataSize = ofKib(bits.toDouble() / 1024)
+
+ @JvmStatic
+ @JvmName("ofBytes")
+ public fun ofBytes(bytes: Number): DataSize = ofKiB(bytes.toDouble() / 1024)
+
+ // Metric prefixes.
+
+ @JvmStatic
+ @JvmName("ofKb")
+ public fun ofKb(kb: Number): DataSize = ofBits(kb.toDouble() * 1e3)
+
+ @JvmStatic
+ @JvmName("ofKB")
+ public fun ofKB(kB: Number): DataSize = ofBytes(kB.toDouble() * 1e3)
+
+ @JvmStatic
+ @JvmName("ofMb")
+ public fun ofMb(mb: Number): DataSize = ofKb(mb.toDouble() * 1e3)
+
+ @JvmStatic
+ @JvmName("ofMB")
+ public fun ofMB(mB: Number): DataSize = ofKB(mB.toDouble() * 1e3)
+
+ @JvmStatic
+ @JvmName("ofGb")
+ public fun ofGb(gb: Number): DataSize = ofMb(gb.toDouble() * 1e3)
+
+ @JvmStatic
+ @JvmName("ofGB")
+ public fun ofGB(gB: Number): DataSize = ofMB(gB.toDouble() * 1e3)
+
+ @JvmStatic
+ @JvmName("ofTb")
+ public fun ofTb(tb: Number): DataSize = ofGb(tb.toDouble() * 1e3)
+
+ @JvmStatic
+ @JvmName("ofTB")
+ public fun ofTB(tB: Number): DataSize = ofGB(tB.toDouble() * 1e3)
+
+ // Binary prefixes.
+
+ @JvmStatic
+ @JvmName("ofKib")
+ public fun ofKib(kib: Number): DataSize = ofMib(kib.toDouble() / 1024)
+
+ @JvmStatic
+ @JvmName("ofKiB")
+ public fun ofKiB(kiB: Number): DataSize = ofMiB(kiB.toDouble() / 1024)
+
+ @JvmStatic
+ @JvmName("ofMib")
+ public fun ofMib(mib: Number): DataSize = ofMiB(mib.toDouble() / 8)
+
+ @JvmStatic
+ @JvmName("ofMiB")
+ public fun ofMiB(miB: Number): DataSize = DataSize(miB.toDouble())
+
+ @JvmStatic
+ @JvmName("ofGib")
+ public fun ofGib(gib: Number): DataSize = ofMib(gib.toDouble() * 1024)
+
+ @JvmStatic
+ @JvmName("ofGiB")
+ public fun ofGiB(giB: Number): DataSize = ofMiB(giB.toDouble() * 1024)
+
+ @JvmStatic
+ @JvmName("ofTib")
+ public fun ofTib(tib: Number): DataSize = ofGib(tib.toDouble() * 1024)
+
+ @JvmStatic
+ @JvmName("ofTiB")
+ public fun ofTiB(tiB: Number): DataSize = ofGiB(tiB.toDouble() * 1024)
+
+ /**
+ * Serializer for [DataSize] 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.
+ * "data": "100GB"
+ * "data": " 1 MB "
+ * // etc.
+ * ```
+ */
+ internal object DataSerializer : UnitSerializer<DataSize>(
+ ifNumber = {
+ LOG.warn(
+ "deserialization of number with no unit of measure for unit 'DataSize', " +
+ "assuming it is in MiB. Keep in mind that you can also specify the value as '$it MiB'",
+ )
+ ofMiB(it.toDouble())
+ },
+ serializerFun = { this.encodeString(it.toString()) },
+ ifMatches("$NUM_GROUP$BITS") { ofBits(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$BYTES") { ofBytes(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$KIBI$BITS") { ofKib(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$KILO$BITS") { ofKb(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$KIBI$BYTES") { ofKiB(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$KILO$BYTES") { ofKB(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$MEBI$BITS") { ofMib(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$MEGA$BITS") { ofMb(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$MEBI$BYTES") { ofMiB(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$MEGA$BYTES") { ofMB(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$GIBI$BITS") { ofGib(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$GIGA$BITS") { ofGb(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$GIBI$BYTES") { ofGiB(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$GIGA$BYTES") { ofGB(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$TEBI$BITS") { ofTib(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$TERA$BITS") { ofTb(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$TEBI$BYTES") { ofTiB(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$TERA$BYTES") { ofTB(json.decNumFromStr(groupValues[1])) },
+ )
+ }
+}
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
new file mode 100644
index 00000000..467192a0
--- /dev/null
+++ b/opendc-common/src/main/kotlin/org/opendc/common/units/Energy.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.units.Time.Companion.toTime
+import org.opendc.common.utils.fmt
+import org.opendc.common.utils.ifNeg0thenPos0
+import java.time.Duration
+import kotlin.text.RegexOption.IGNORE_CASE
+
+/**
+ * Represents energy values.
+ * @see[Unit]
+ */
+@JvmInline
+@Serializable(with = Energy.Companion.EnergySerializer::class)
+public value class Energy private constructor(
+ // In Joule
+ override val value: Double,
+) : Unit<Energy> {
+ override fun new(value: Double): Energy = Energy(value.ifNeg0thenPos0())
+
+ public fun toJoule(): Double = value
+
+ public fun toKJoule(): Double = value / 1000
+
+ public fun toWh(): Double = value / 3600
+
+ public fun toKWh(): Double = toWh() / 1000
+
+ override fun toString(): String = fmtValue()
+
+ override fun fmtValue(fmt: String): String =
+ if (value >= 1000.0) {
+ "${toJoule().fmt(fmt)} Joule"
+ } else {
+ "${toKJoule().fmt(fmt)} KJoule"
+ }
+
+ public operator fun div(time: Time): Power = Power.ofWatts(toWh() / time.toHours())
+
+ public operator fun div(duration: Duration): Power = this / duration.toTime()
+
+ public companion object {
+ @JvmStatic
+ public val ZERO: Energy = Energy(.0)
+
+ @JvmStatic
+ @JvmName("ofJoule")
+ public fun ofJoule(joule: Number): Energy = Energy(joule.toDouble())
+
+ @JvmStatic
+ @JvmName("ofKJoule")
+ public fun ofKJoule(kJoule: Number): Energy = ofJoule(kJoule.toDouble() * 1000)
+
+ @JvmStatic
+ @JvmName("ofWh")
+ public fun ofWh(wh: Number): Energy = ofJoule(wh.toDouble() * 3600)
+
+ @JvmStatic
+ @JvmName("ofKWh")
+ public fun ofKWh(kWh: Number): Energy = ofWh(kWh.toDouble() * 1000.0)
+
+ private val JOULES = Regex("\\s*(?:j|(?:joule|Joule)(?:|s))")
+
+ /**
+ * Serializer for [Energy] 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.
+ * "energy": "1 KWh"
+ * "energy": " 3 watts-hour "
+ * "energy": "10.5 Joules"
+ * // etc.
+ * ```
+ */
+ internal object EnergySerializer : UnitSerializer<Energy>(
+ ifNumber = {
+ LOG.warn(
+ "deserialization of number with no unit of measure, assuming it is in Joule" +
+ "Keep in mind that you can also specify the value as '$it Joule'",
+ )
+ ofJoule(it.toDouble())
+ },
+ serializerFun = { this.encodeString(it.toString()) },
+ ifMatches("$NUM_GROUP$WATTS$PER$HOUR", IGNORE_CASE) { ofWh(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$KILO$WATTS$PER$HOUR", IGNORE_CASE) { ofKWh(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$JOULES", IGNORE_CASE) { ofJoule(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$KILO$JOULES", IGNORE_CASE) { ofKJoule(json.decNumFromStr(groupValues[1])) },
+ )
+ }
+}
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
new file mode 100644
index 00000000..df1b49f6
--- /dev/null
+++ b/opendc-common/src/main/kotlin/org/opendc/common/units/Frequency.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.units.Time.Companion.toTime
+import org.opendc.common.utils.fmt
+import org.opendc.common.utils.ifNeg0thenPos0
+import java.time.Duration
+import kotlin.text.RegexOption.IGNORE_CASE
+
+/**
+ * Represents frequency values.
+ * @see[Unit]
+ */
+@JvmInline
+@Serializable(with = Frequency.Companion.FrequencySerializer::class)
+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) })
+
+ public fun toHz(): Double = value * 1e6
+
+ public fun toKHz(): Double = value * 1e3
+
+ public fun toMHz(): Double = value
+
+ public fun toGHz(): Double = value / 1e3
+
+ 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"
+ }
+
+ public operator fun times(time: Time): Double = toHz() * time.toSec()
+
+ public operator fun times(duration: Duration): Double = toHz() * duration.toTime().toSec()
+
+ public companion object {
+ @JvmStatic public val ZERO: Frequency = Frequency(.0)
+
+ @JvmStatic
+ @JvmName("ofHz")
+ public fun ofHz(hz: Number): Frequency = ofMHz(hz.toDouble() / 1e6)
+
+ @JvmStatic
+ @JvmName("ofKHz")
+ public fun ofKHz(kHz: Number): Frequency = ofMHz(kHz.toDouble() / 1e3)
+
+ @JvmStatic
+ @JvmName("ofMHz")
+ public fun ofMHz(mHz: Number): Frequency = Frequency(mHz.toDouble())
+
+ @JvmStatic
+ @JvmName("ofGHz")
+ public fun ofGHz(gHz: Number): Frequency = ofMHz(gHz.toDouble() * 1e3)
+
+ private val HERTZ = Regex("\\s*(?:Hz|Hertz|hz|hertz)\\s*?")
+
+ /**
+ * Serializer for [Frequency] 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.
+ * "frequency": "1000 Hz"
+ * "frequency": " 10 GHz "
+ * "frequency": "2megahertz"
+ * // etc.
+ * ```
+ */
+ internal object FrequencySerializer : UnitSerializer<Frequency>(
+ ifNumber = {
+ LOG.warn("deserialization of number with no unit of measure, assuming it is in MHz...")
+ ofMHz(it.toDouble())
+ },
+ serializerFun = { this.encodeString(it.toString()) },
+ ifMatches("$NUM_GROUP$HERTZ", IGNORE_CASE) { ofHz(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$KILO$HERTZ", IGNORE_CASE) { ofKHz(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$MEGA$HERTZ", IGNORE_CASE) { ofMHz(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$GIGA$HERTZ", IGNORE_CASE) { ofGHz(json.decNumFromStr(groupValues[1])) },
+ )
+ }
+}
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
new file mode 100644
index 00000000..377fdecc
--- /dev/null
+++ b/opendc-common/src/main/kotlin/org/opendc/common/units/Percentage.kt
@@ -0,0 +1,260 @@
+/*
+ * 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 mu.KotlinLogging
+import org.opendc.common.annotations.InternalUse
+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.
+ *
+ * As all [Unit]s, offers the vast majority
+ * of mathematical operations that one would perform on a simple [Double].
+ */
+@Serializable(with = Percentage.Companion.PercentageSerializer::class)
+public sealed interface Percentage : Unit<Percentage> {
+ override val value: Double
+
+ /**
+ * @return the value as a ratio (e.g. 50% -> 0.5)
+ */
+ public fun toRatio(): Double = value
+
+ /**
+ * @return the value as percentage (50.6% -> 50.6)
+ */
+ public fun toPercentageValue(): Double = value * 1e2
+
+ /**
+ * @return *this* percentage converted to [BoundedPercentage].
+ */
+ public fun toBoundedPercentage(): BoundedPercentage
+
+ /**
+ * @return *this* percentage converted to [UnboundedPercentage].
+ */
+ public fun toUnboundedPercentage(): UnboundedPercentage
+
+ /**
+ * ```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 companion object {
+ @JvmStatic public val ZERO: Percentage = UnboundedPercentage(.0)
+
+ @JvmStatic
+ @JvmName("ofRatio")
+ public fun ofRatio(ratio: Double): UnboundedPercentage = UnboundedPercentage(ratio)
+
+ @JvmStatic
+ @JvmName("ofRatioBounded")
+ public fun ofRatioBounded(ratio: Double): BoundedPercentage = BoundedPercentage(ratio)
+
+ @JvmStatic
+ @JvmName("ofPercentage")
+ public fun ofPercentage(percentage: Number): UnboundedPercentage = UnboundedPercentage(percentage.toDouble() / 100)
+
+ @JvmStatic
+ @JvmName("ofPercentageBounded")
+ public fun ofPercentageBounded(percentage: Double): BoundedPercentage = BoundedPercentage(percentage / 100)
+
+ /**
+ * @return the percentage resulting from [this] / [other].
+ */
+ public infix fun Number.percentageOf(other: Number): UnboundedPercentage = UnboundedPercentage(this.toDouble() / other.toDouble())
+
+ /**
+ * @return the *bounded* percentage resulting from [this] / [other].
+ */
+ public infix fun Number.boundedPercentageOf(other: Number): BoundedPercentage =
+ BoundedPercentage(this.toDouble() / other.toDouble())
+
+ /**
+ * @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)
+
+ 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,
+ * hence `object :` instead of class instantiation.
+ *
+ * For implementation purposes it always deserialize an [UnboundedPercentage] as [Percentage].
+ *
+ * ```json
+ * // e.g.
+ * "percentage": 0.5 // 50% with warning
+ * "percentage": " 30% "
+ * "percentage": "120%" // 120% (unbounded)
+ * // etc.
+ * ```
+ */
+ internal object PercentageSerializer : UnitSerializer<Percentage>(
+ ifNumber = {
+ LOG.warn(
+ "deserialization of number with no unit of measure, assuming it is a ratio." +
+ "Keep in mind that you can also specify the value as '${it.toDouble() * 100}%'",
+ )
+ ofRatio(it.toDouble())
+ },
+ serializerFun = { this.encodeString(it.toString()) },
+ ifMatches("$NUM_GROUP$PERCENTAGE", IGNORE_CASE) { ofPercentage(json.decNumFromStr(groupValues[1])) },
+ )
+ }
+}
+
+/**
+ * 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
new file mode 100644
index 00000000..fc9f6bf4
--- /dev/null
+++ b/opendc-common/src/main/kotlin/org/opendc/common/units/Power.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.units.Time.Companion.toTime
+import org.opendc.common.utils.fmt
+import org.opendc.common.utils.ifNeg0thenPos0
+import java.time.Duration
+import kotlin.text.RegexOption.IGNORE_CASE
+
+/**
+ * Represents power values.
+ * @see[Unit]
+ */
+@JvmInline
+@Serializable(with = Power.Companion.PowerSerializer::class)
+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 =
+ if (value >= 1000.0) {
+ "${toKWatts().fmt(fmt)} KWatts"
+ } else {
+ "${toWatts().fmt(fmt)} Watts"
+ }
+
+ public operator fun times(time: Time): Energy = Energy.ofWh(toWatts() * time.toHours())
+
+ public operator fun times(duration: Duration): Energy = this * duration.toTime()
+
+ public companion object {
+ @JvmStatic
+ public val ZERO: Power = Power(.0)
+
+ @JvmStatic
+ @JvmName("ofWatts")
+ public fun ofWatts(watts: Number): Power = Power(watts.toDouble())
+
+ @JvmStatic
+ @JvmName("ofKWatts")
+ public fun ofKWatts(kWatts: Number): Power = Power(kWatts.toDouble() * 1000.0)
+
+ /**
+ * Serializer for [Power] 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.
+ * "power-draw": "4 watts"
+ * "power-draw": " 1 KWatt "
+ * // etc.
+ * ```
+ */
+ internal object PowerSerializer : UnitSerializer<Power>(
+ ifNumber = {
+ LOG.warn(
+ "deserialization of number with no unit of measure, assuming it is in Watts." +
+ "Keep in mind that you can also specify the value as '$it W'",
+ )
+ ofWatts(it.toDouble())
+ },
+ serializerFun = { this.encodeString(it.toString()) },
+ ifMatches("$NUM_GROUP$WATTS", IGNORE_CASE) { ofWatts(json.decNumFromStr(groupValues[1])) },
+ ifMatches("$NUM_GROUP$KILO$WATTS", IGNORE_CASE) { ofKWatts(json.decNumFromStr(groupValues[1])) },
+ )
+ }
+}
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
new file mode 100644
index 00000000..9d72ddfc
--- /dev/null
+++ b/opendc-common/src/main/kotlin/org/opendc/common/units/Time.kt
@@ -0,0 +1,166 @@
+/*
+ * 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/Unit.kt b/opendc-common/src/main/kotlin/org/opendc/common/units/Unit.kt
new file mode 100644
index 00000000..8bcbb148
--- /dev/null
+++ b/opendc-common/src/main/kotlin/org/opendc/common/units/Unit.kt
@@ -0,0 +1,386 @@
+/*
+ * Copyright (c) 2024 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+@file:OptIn(InternalUse::class)
+
+package org.opendc.common.units
+
+import org.opendc.common.annotations.InternalUse
+import org.opendc.common.units.Time.Companion.toTime
+import org.opendc.common.utils.DFLT_MIN_EPS
+import org.opendc.common.utils.adaptiveEps
+import org.opendc.common.utils.approx
+import org.opendc.common.utils.approxLarger
+import org.opendc.common.utils.approxLargerOrEq
+import org.opendc.common.utils.approxSmaller
+import org.opendc.common.utils.approxSmallerOrEq
+import java.time.Duration
+import kotlin.experimental.ExperimentalTypeInference
+
+/**
+ * Value classes can extend this interface to represent
+ * unit of measure with much higher type safety than [Double] (*If used from kotlin*)
+ * and approximately same performances.
+ * ```kotlin
+ * // e.g.
+ * @JvmInline value class DataRate(override val value) : Unit<DataRate> { }
+ * ```
+ * This interface provides most of the utility functions and
+ * mathematical operations that are available to [Double] (including threshold comparison methods),
+ * but applicable to [T] (also with scalar multiplication and division),
+ * and operations between different unit of measures.
+ *
+ * ```
+ * // e.g. sum of data-rates
+ * val a: DataRate = DataRate.ofMibps(100)
+ * val b: DataRate = DataRate.ofGibps(1)
+ * val c: DataRate = a + b
+ * c.fmt("%.3f") // "1.097 Gibps"
+ *
+ * // e.g. data-rate times scalar
+ * val e: DataRate = a * 2
+ * e.fmt() // "200 Mibps"
+ *
+ * // e.g. threshold comparison
+ * if (e approx a) { ... }
+ *
+ * // e.g. operations between different unit of measures
+ * val a: DataRate = DataRate.ofMBps(1)
+ * val b: Time = Time.ofSec(3)
+ * val c: DataSize = a * b
+ * c.fmt() // "3MB"
+ * ```
+ * &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
+ * to interact with kotlin code concerning inline classes should be made accessible to java.**
+ * Java will never be able to invoke instance methods, only static ones.
+ *
+ * Java sees value classes as the standard data type they represent (in this case double).
+ * Meaning there is no type safety from java, nevertheless functions can be invoked
+ * to provide methods the correct unit value (and for improved understandability).
+ *
+ * ```kotlin
+ * // kotlin
+ * @JvmStatic @JvmName("function")
+ * fun function(time: Time) { }
+ * ```
+ * ```java
+ * // java
+ * double time = Time.ofHours(2);
+ * function(time)
+ * // or
+ * function(Time.ofHours(2))
+ * ```
+ *
+ * @param[T] the unit of measure that is represented (e.g. [DataRate])
+ */
+public sealed interface Unit<T : Unit<T>> : Comparable<T> {
+ /**
+ * The actual value of this unit of measure used for computation and comparisons.
+ *
+ * What magnitude this value represents (e.g. Kbps, Mbps etc.) is up to the interface implementation,
+ * and it does not interfere with the operations, hence this property should be reserved for internal use.
+ */
+ @InternalUse
+ public val value: Double
+
+ /**
+ * @return the sum with [other] as [T].
+ */
+ public operator fun plus(other: T): T = new(value + other.value)
+
+ /**
+ * @return the subtraction of [other] from *this* as [T].
+ */
+ public operator fun minus(other: T): T = new(value - other.value)
+
+ /**
+ * @return *this* divided by scalar [scalar] as [T].
+ */
+ public operator fun div(scalar: Number): T = new(value / scalar.toDouble())
+
+ /**
+ * @return *this* divided by [other] as [Double].
+ */
+ public operator fun div(other: T): Double = value / other.value
+
+ /**
+ * @return *this* multiplied by scalar [scalar] as [T].
+ */
+ public operator fun times(scalar: Number): T = new(value * scalar.toDouble())
+
+ /**
+ * @return *this* negated.
+ */
+ public operator fun unaryMinus(): T = new(-value)
+
+ public override operator fun compareTo(other: T): Int = this.value.compareTo(other.value)
+
+ /**
+ * @return `true` if *this* is equal to 0 (using `==` operator).
+ */
+ public fun isZero(): Boolean = value == .0 || value == -.0
+
+ /**
+ * @return `true` if *this* is approximately equal to 0.
+ * @see[Double.approx]
+ */
+ public fun approxZero(epsilon: Double = DFLT_MIN_EPS): Boolean = value.approx(.0, epsilon = epsilon)
+
+ /**
+ * @see[Double.approx]
+ */
+ public fun approx(
+ other: T,
+ minEpsilon: Double = DFLT_MIN_EPS,
+ epsilon: Double = adaptiveEps(this.value, other.value, minEpsilon),
+ ): Boolean = this == other || this.value.approx(other.value, minEpsilon, epsilon)
+
+ /**
+ * @see[Double.approx]
+ */
+ public infix fun approx(other: T): Boolean = approx(other, minEpsilon = DFLT_MIN_EPS)
+
+ /**
+ * @see[Double.approxLarger]
+ */
+ public fun approxLarger(
+ other: T,
+ minEpsilon: Double = DFLT_MIN_EPS,
+ epsilon: Double = adaptiveEps(this.value, other.value, minEpsilon),
+ ): Boolean = this.value.approxLarger(other.value, minEpsilon, epsilon)
+
+ /**
+ * @see[Double.approxLarger]
+ */
+ public infix fun approxLarger(other: T): Boolean = approxLarger(other, minEpsilon = DFLT_MIN_EPS)
+
+ /**
+ * @see[Double.approxLargerOrEq]
+ */
+ public fun approxLargerOrEq(
+ other: T,
+ minEpsilon: Double = DFLT_MIN_EPS,
+ epsilon: Double = adaptiveEps(this.value, other.value, minEpsilon),
+ ): Boolean = this.value.approxLargerOrEq(other.value, minEpsilon, epsilon)
+
+ /**
+ * @see[Double.approxLargerOrEq]
+ */
+ public infix fun approxLargerOrEq(other: T): Boolean = approxLargerOrEq(other, minEpsilon = DFLT_MIN_EPS)
+
+ /**
+ * @see[Double.approxSmaller]
+ */
+ public fun approxSmaller(
+ other: T,
+ minEpsilon: Double = DFLT_MIN_EPS,
+ epsilon: Double = adaptiveEps(this.value, other.value, minEpsilon),
+ ): Boolean = this.value.approxSmaller(other.value, minEpsilon, epsilon)
+
+ /**
+ * @see[Double.approxSmaller]
+ */
+ public infix fun approxSmaller(other: T): Boolean = approxSmaller(other, minEpsilon = DFLT_MIN_EPS)
+
+ /**
+ * @see[Double.approxSmallerOrEq]
+ */
+ public fun approxSmallerOrEq(
+ other: T,
+ minEpsilon: Double = DFLT_MIN_EPS,
+ epsilon: Double = adaptiveEps(this.value, other.value, minEpsilon),
+ ): Boolean = this.value.approxSmallerOrEq(other.value, minEpsilon, epsilon)
+
+ /**
+ * @see[Double.approxSmallerOrEq]
+ */
+ public infix fun approxSmallerOrEq(other: T): Boolean = approxSmallerOrEq(other, minEpsilon = DFLT_MIN_EPS)
+
+ /**
+ * @return the max value between *this* and [other].
+ */
+ @Suppress("UNCHECKED_CAST")
+ public infix fun max(other: T): T = if (this.value > other.value) this as T else other
+
+ /**
+ * @return the minimum value between *this* and [other].
+ */
+ @Suppress("UNCHECKED_CAST")
+ public infix fun min(other: T): T = if (this.value < other.value) this as T else other
+
+ /**
+ * @return the absolute value of *this*.
+ */
+ public fun abs(): T = new(kotlin.math.abs(value))
+
+ /**
+ * @return *this* approximated to [to] if within `0 - epsilon` and `0 + epsilon`.
+ */
+ @Suppress("UNCHECKED_CAST")
+ public fun roundToIfWithinEpsilon(
+ to: T,
+ epsilon: Double = DFLT_MIN_EPS,
+ ): T =
+ if (this.value in (to.value - epsilon)..(to.value + epsilon)) {
+ to
+ } else {
+ this as T
+ }
+
+ /**
+ * The "constructor" of [T] that this interface uses to
+ * instantiate new [T] when performing operations.
+ */
+ @InternalUse
+ public fun new(value: Double): T
+
+ /**
+ * Returns the formatted string representation of the unit of measure (e.g. "1.2 Gbps")
+ * with the formatter [fmt] applied to the value part of the resulting string.
+ *
+ * ```kotlin
+ * val dr = DataRate.ofGbps(1.234567)
+ * dr.fmtValue() // "1.234567 Gbps"
+ * dr.fmtValue("%.2f") // "1.23 Gbps"
+ * ```
+ */
+ public fun fmtValue(fmt: String = "%f"): String
+
+ public companion object {
+ /**
+ * @return [unit] multiplied by scalar [this].
+ */
+ public operator fun <T : Unit<T>> Number.times(unit: T): T = unit * this
+
+ /**
+ * @return minimum value between [a] and [b].
+ */
+ public fun <T : Unit<T>> min(
+ a: T,
+ b: T,
+ ): T = if (a.value < b.value) a else b
+
+ /**
+ * @return minimum value between [units].
+ */
+ public fun <T : Unit<T>> minOf(vararg units: T): T = units.minBy { it.value }
+
+ /**
+ * @return maximum value between [a] and [b].
+ */
+ public fun <T : Unit<T>> max(
+ a: T,
+ b: T,
+ ): T = if (a.value > b.value) a else b
+
+ /**
+ * @return maximum value between [units].
+ */
+ public fun <T : Unit<T>> maxOf(vararg units: T): T = units.maxBy { it.value }
+
+ // maxBy and minBy need to be defined in implementations.
+
+ // Operations whose 'this' is a `Unit` are defined here.
+ // Operations whose 'this' is not a `Unit` are defined in their classes
+ // and not as extension function so that they do not need to be imported
+
+ public operator fun Duration.times(dataRate: DataRate): DataSize = toTime() * dataRate
+
+ public operator fun Duration.times(power: Power): Energy = toTime() * power
+
+ public operator fun Number.div(time: Time): Frequency = Frequency.ofHz(this.toDouble() / time.toSec())
+
+ public operator fun Number.div(duration: Duration): Frequency = this / duration.toTime()
+
+ // Defined here so that they can overload the same method name, instead of having a different name forEach unit.
+ // You can not overload `sumOf` and using that name results in not being able to use the overloads for unit and for number in the same file.
+
+ // A reified version that does not need overloads can be also be defined, with a switch statement on the reified unit type for the base value.
+ // Then, if a unit is not included in the switch, a runtime error occurs, not compile time.
+
+ @OptIn(ExperimentalTypeInference::class)
+ @OverloadResolutionByLambdaReturnType
+ @JvmName("sumOfDataRate")
+ public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> DataRate): DataRate {
+ var sum: DataRate = DataRate.ZERO
+ forEach { sum += selector(it) }
+ return sum
+ }
+
+ @OptIn(ExperimentalTypeInference::class)
+ @OverloadResolutionByLambdaReturnType
+ @JvmName("sumOfDataSize")
+ public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> DataSize): DataSize {
+ var sum: DataSize = DataSize.ZERO
+ forEach { sum += selector(it) }
+ return sum
+ }
+
+ @OptIn(ExperimentalTypeInference::class)
+ @OverloadResolutionByLambdaReturnType
+ @JvmName("sumOfEnergy")
+ public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> Energy): Energy {
+ var sum: Energy = Energy.ZERO
+ forEach { sum += selector(it) }
+ return sum
+ }
+
+ @OptIn(ExperimentalTypeInference::class)
+ @OverloadResolutionByLambdaReturnType
+ @JvmName("sumOfPower")
+ public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> Power): Power {
+ var sum: Power = Power.ZERO
+ forEach { sum += selector(it) }
+ return sum
+ }
+
+ @OptIn(ExperimentalTypeInference::class)
+ @OverloadResolutionByLambdaReturnType
+ @JvmName("sumOfTime")
+ public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> Time): Time {
+ var sum: Time = Time.ZERO
+ forEach { sum += selector(it) }
+ return sum
+ }
+
+ @OptIn(ExperimentalTypeInference::class)
+ @OverloadResolutionByLambdaReturnType
+ @JvmName("sumOfFrequency")
+ public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> Frequency): Frequency {
+ var sum: Frequency = Frequency.ZERO
+ forEach { sum += selector(it) }
+ return sum
+ }
+
+ @OptIn(ExperimentalTypeInference::class)
+ @OverloadResolutionByLambdaReturnType
+ @JvmName("sumOfPercentage")
+ public inline fun <T> Iterable<T>.sumOfUnit(selector: (T) -> Percentage): Percentage {
+ var sum: Percentage = Percentage.ZERO
+ forEach { sum += selector(it) }
+ return sum
+ }
+ }
+}
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
new file mode 100644
index 00000000..aaf18498
--- /dev/null
+++ b/opendc-common/src/main/kotlin/org/opendc/common/units/UnitSerializer.kt
@@ -0,0 +1,215 @@
+/*
+ * 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.
+ */
+
+package org.opendc.common.units
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.JsonTransformingSerializer
+import mu.KotlinLogging
+
+/**
+ * Serializer for [T].
+ * @param[ifNumber] function invoked if the value to parse is a number without unit of measure.
+ * ```json
+ * // json e.g.
+ * "value": 3
+ * // or
+ * "value": "3"
+ * ```
+ * @param[serializerFun] function invoked when [T] needs to be serialized.
+ *
+ * @param[conditions] conditions used during the deserialization process.
+ * If the condition returns [T] then it is considered as the result of the deserialization.
+ * If the condition returns `null` the next condition is tested, until one
+ * satisfied condition is found, throws exception otherwise.
+ */
+internal open class UnitSerializer<T : Unit<T>>(
+ ifNumber: (Number) -> T,
+ serializerFun: Encoder.(T) -> kotlin.Unit,
+ vararg conditions: String.() -> T?,
+) : OnlyString<T>(
+ object : KSerializer<T> {
+ override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("unit-serializer", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): T {
+ val strField = decoder.decodeString()
+ try {
+ // If the field is a number.
+ return ifNumber(json.decodeFromString<Double>(strField))
+ } catch (e: Exception) {
+ // No ops.
+ }
+
+ conditions.forEach { condition ->
+ // If condition satisfied return result.
+ strField.condition()?.let { return it }
+ }
+
+ throw RuntimeException("unable to parse unit of measure $strField")
+ }
+
+ override fun serialize(
+ encoder: Encoder,
+ value: T,
+ ) {
+ serializerFun(encoder, value)
+ }
+ },
+ ) {
+ companion object {
+ // TODO: replace with `by logger()` if pr #241 is approved
+ val LOG = KotlinLogging.logger(name = this::class.java.enclosingClass.simpleName)
+
+ val json = Json
+
+ /**
+ * @return a lambda that can be passed as condition to [UnitSerializer] constructor.
+ */
+ fun <T> ifMatches(
+ regex: Regex,
+ block: MatchResult.() -> T,
+ ): String.() -> T? =
+ {
+ regex.matchEntire(this)?.block()
+ }
+
+ /**
+ * @return a lambda that can be passed as condition to [UnitSerializer] constructor.
+ */
+ fun <T> ifMatches(
+ regexStr: String,
+ vararg options: RegexOption = emptyArray(),
+ block: MatchResult.() -> T,
+ ): String.() -> T? =
+ {
+ Regex(regexStr, options.toSet()).matchEntire(this)?.block()
+ }
+
+ /**
+ * @return a lambda that can be passed as condition to [UnitSerializer] constructor.
+ */
+ fun <T> ifNoExc(block: String.() -> T): String.() -> T? =
+ {
+ try {
+ block()
+ } catch (_: Exception) {
+ null
+ }
+ }
+
+ // Constants that are used by multiple serializers to build consistent
+ // (and easy to change) regexes for deserialization.
+ // There is no guarantee that they are used with `IGNORE_CASE` option.
+
+ @JvmStatic
+ protected val NUM_GROUP = Regex("\\s*([\\de.-]+)\\s*?")
+
+ @JvmStatic
+ protected val BITS = Regex("\\s*(?:b|(?:bit|Bit)(?:|s))\\s?")
+
+ @JvmStatic
+ protected val BYTES = Regex("\\s*(?:B|(?:byte|Byte)(?:|s))\\s?")
+
+ @JvmStatic
+ protected val NANO = Regex("\\s*(?:n|nano|Nano)\\s*?")
+
+ @JvmStatic
+ protected val MICRO = Regex("\\s*(?:micro|Micro)\\s*?")
+
+ @JvmStatic
+ protected val MILLI = Regex("\\s*(?:m|milli|Milli)\\s*?")
+
+ @JvmStatic
+ protected val KILO = Regex("\\s*(?:K|Kilo|k|kilo)\\s*?")
+
+ @JvmStatic
+ protected val KIBI = Regex("\\s*(?:Ki|Kibi|ki|kibi)\\s?")
+
+ @JvmStatic
+ protected val MEGA = Regex("\\s*(?:M|Mega|m|mega)\\s*?")
+
+ @JvmStatic
+ protected val MEBI = Regex("\\s*(?:Mi|Mebi|mi|mebi)\\s*?")
+
+ @JvmStatic
+ protected val GIGA = Regex("\\s*(?:G|Giga|g|giga)\\s*?")
+
+ @JvmStatic
+ protected val GIBI = Regex("\\s*(?:Gi|Gibi|gi|gibi)\\s*?")
+
+ @JvmStatic
+ protected val TERA = Regex("\\s*(?:T|Tera|t|tera)\\s*?")
+
+ @JvmStatic
+ protected val TEBI = Regex("\\s*(?:Ti|Tebi|ti|tebi)\\s*?")
+
+ @JvmStatic
+ protected val WATTS = Regex("\\s*(?:w|watts|W|Watts)\\s*?")
+
+ @JvmStatic
+ protected val PER = Regex("\\s*(?:p|per|/)\\s*?")
+
+ @JvmStatic
+ protected val SEC = Regex("\\s*(?:s|sec|Sec|second|Second)\\s*?")
+
+ @JvmStatic
+ protected val MIN = Regex("\\s*(?:m|min|Min|minute|Minute)\\s*?")
+
+ @JvmStatic
+ protected val HOUR = Regex("\\s*(?:h|hour|Hour)\\s*?")
+ }
+}
+
+/**
+ * Allows manipulating an abstract JSON representation of the class before serialization or deserialization.
+ * Maps a [JsonPrimitive] to its [String] representation.
+ *
+ * ```json
+ * // e.g.
+ * "value": 3
+ * // for deserialization becomes
+ * "value": "3"
+ */
+internal open class OnlyString<T : Any>(tSerial: KSerializer<T>) : JsonTransformingSerializer<T>(tSerial) {
+ override fun transformDeserialize(element: JsonElement): JsonElement = JsonPrimitive(element.toString().trim('"'))
+}
+
+/**
+ * Kotlin's serialization plugin does not have a serializer for [Number].
+ * ```kotlin
+ * // This function allows, when the type inferred without
+ * // type parameter is Number, to replace
+ * Json.decodeFromString<Double>(str)
+ * // with
+ * Json.decNumFromStr(str)
+ *
+ * ```
+ */
+internal fun Json.decNumFromStr(str: String): Number = decodeFromString<Double>(str)
diff --git a/opendc-common/src/main/kotlin/org/opendc/common/utils/DoubleUtils.kt b/opendc-common/src/main/kotlin/org/opendc/common/utils/DoubleUtils.kt
new file mode 100644
index 00000000..ebf6ad21
--- /dev/null
+++ b/opendc-common/src/main/kotlin/org/opendc/common/utils/DoubleUtils.kt
@@ -0,0 +1,225 @@
+/*
+ * 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.
+ */
+
+package org.opendc.common.utils
+
+import org.slf4j.Logger
+import kotlin.math.abs
+
+/**
+ * When comparing 2 doubles, `==` can produce wrong results. The threshold comparison method check that
+ * the difference between both numbers is within a specified tolerance, commonly called epsilon.
+ * In this case we use adaptive epsilons, meaning the epsilon is adjusted proportionally to
+ * the values that are being compared.
+ *
+ * This value represents the default epsilon multiplier used if an epsilon is not provided.
+ */
+internal const val DFLT_EPS_MULTIPLIER: Double = 1e-05
+internal const val DFLT_MIN_EPS: Double = 1.0e-06
+
+/**
+ * Compares [this] with [other] using threshold comparison method with epsilon = [epsilon].
+ *
+ * @param[minEpsilon] the minimum epsilon that can be computed when [epsilon] is not provided.
+ * If [epsilon] is provided, this param has no effect.
+ * @param[epsilon] represent the tolerance of the comparison.
+ * If not provided an adaptive epsilon is computed (based on the largest value in the comparison).
+ * @return `true` if [this] is considered equal to [other], `false` otherwise.
+ */
+@JvmOverloads
+public fun Double.approx(
+ other: Double,
+ minEpsilon: Double = DFLT_MIN_EPS,
+ epsilon: Double = adaptiveEps(this, other, minEpsilon),
+): Boolean = this == other || abs(this - other) <= epsilon
+
+/**
+ * Infix version of [approx].
+ * @see[approx]
+ */
+@JvmSynthetic
+@JvmName("approx, jvm name to avoid same jvm signature (not invokable from java)")
+public infix fun Double.approx(other: Double): Boolean = approx(other, epsilon = DFLT_EPS_MULTIPLIER)
+
+/**
+ * @return [this] approximated to [to] if within `[to] - epsilon` and `[to] + epsilon`.
+ */
+@JvmOverloads
+public fun Double.roundToIfWithinEpsilon(
+ to: Double,
+ epsilon: Double = DFLT_MIN_EPS,
+): Double =
+ if (this in (to - epsilon)..(to + epsilon)) {
+ to
+ } else {
+ this
+ }
+
+/**
+ * Compares [this] with [other] using threshold comparison method with epsilon = [epsilon].
+ *
+ * @param[minEpsilon] the minimum epsilon that can be computed when [epsilon] is not provided.
+ * If [epsilon] is provided, this param has no effect.
+ * @param[epsilon] represent the tolerance of the comparison.
+ * If not provided an adaptive epsilon is computed (based on the largest value in the comparison).
+ * @return `true` if [this] is considered larger than [other], `false` otherwise.
+ */
+@JvmOverloads
+public fun Double.approxLarger(
+ other: Double,
+ minEpsilon: Double = DFLT_MIN_EPS,
+ epsilon: Double = adaptiveEps(this, other, minEpsilon),
+): Boolean = (this - other) > epsilon
+
+/**
+ * Infix version of [approxLarger].
+ * @see[approxLarger]
+ */
+@JvmSynthetic
+@JvmName("approxLarger, jvm name to avoid same jvm signature (not invokable from java)")
+public infix fun Double.approxLarger(other: Double): Boolean = this.approxLarger(other, epsilon = DFLT_EPS_MULTIPLIER)
+
+/**
+ * Compares [this] with [other] using threshold comparison method with epsilon = [epsilon].
+ *
+ * @param[minEpsilon] the minimum epsilon that can be computed when [epsilon] is not provided.
+ * If [epsilon] is provided, this param has no effect.
+ * @param[epsilon] represent the tolerance of the comparison.
+ * If not provided an adaptive epsilon is computed (based on the largest value in the comparison).
+ * @return `true` if [this] is considered larger or equal than [other], `false` otherwise.
+ */
+@JvmOverloads
+public fun Double.approxLargerOrEq(
+ other: Double,
+ minEpsilon: Double = DFLT_MIN_EPS,
+ epsilon: Double = adaptiveEps(this, other, minEpsilon),
+): Boolean = (this - other) > -epsilon
+
+/**
+ * Infix version of [approxLargerOrEq].
+ * @see[approxLargerOrEq]
+ */
+@JvmSynthetic
+@JvmName("approxLargerOrEq, jvm name to avoid same jvm signature (not invokable from java)")
+public infix fun Double.approxLargerOrEq(other: Double): Boolean = this.approxLargerOrEq(other, epsilon = DFLT_EPS_MULTIPLIER)
+
+/**
+ * Compares [this] with [other] using threshold comparison method with epsilon = [epsilon].
+ *
+ * @param[minEpsilon] the minimum epsilon that can be computed when [epsilon] is not provided.
+ * If [epsilon] is provided, this param has no effect.
+ * @param[epsilon] represent the tolerance of the comparison.
+ * If not provided an adaptive epsilon is computed (based on the largest value in the comparison).
+ * @return `true` if [this] is considered smaller than [other], `false` otherwise.
+ */
+@JvmOverloads
+public fun Double.approxSmaller(
+ other: Double,
+ minEpsilon: Double = DFLT_MIN_EPS,
+ epsilon: Double = adaptiveEps(this, other, minEpsilon),
+): Boolean = (this - other) < -epsilon
+
+/**
+ * Infix version of [approxLarger].
+ * @see[approxLarger]
+ */
+@JvmSynthetic
+@JvmName("approxSmaller, jvm name to avoid same jvm signature (not invokable from java)")
+public infix fun Double.approxSmaller(other: Double): Boolean = this.approxLarger(other, epsilon = DFLT_EPS_MULTIPLIER)
+
+/**
+ * Compares [this] with [other] using threshold comparison method with epsilon = [epsilon].
+ *
+ * @param[minEpsilon] the minimum epsilon that can be computed when [epsilon] is not provided.
+ * If [epsilon] is provided, this param has no effect.
+ * @param[epsilon] represent the tolerance of the comparison.
+ * If not provided an adaptive epsilon is computed (based on the largest value in the comparison).
+ * @return `true` if [this] is considered smaller or equal than [other], `false` otherwise.
+ */
+@JvmOverloads
+public fun Double.approxSmallerOrEq(
+ other: Double,
+ minEpsilon: Double = DFLT_MIN_EPS,
+ epsilon: Double = adaptiveEps(this, other, minEpsilon),
+): Boolean = this - other < epsilon
+
+/**
+ * Infix version of [approxSmallerOrEq].
+ * @see[approxSmallerOrEq]
+ */
+@JvmSynthetic
+@JvmName("approxSmallerOrEq, jvm name to avoid same jvm signature (not invokable from java)")
+public infix fun Double.approxSmallerOrEq(other: Double): Boolean = approxSmallerOrEq(other, DFLT_EPS_MULTIPLIER)
+
+/**
+ * @return the result of [block] if [this] is NaN, [this] otherwise.
+ */
+public inline infix fun Double.ifNaN(block: () -> Double): Double =
+ if (this.isNaN()) {
+ block()
+ } else {
+ this
+ }
+
+/**
+ * @return [replacement] if [this] is NaN, [this] otherwise.
+ */
+public infix fun Double.ifNaN(replacement: Double): Double =
+ if (this.isNaN()) {
+ replacement
+ } else {
+ this
+ }
+
+/**
+ * @return adaptive epsilon computed proportionally to the max absolute value of [a] and [b]
+ */
+internal fun adaptiveEps(
+ a: Double,
+ b: Double,
+ minEpsilon: Double = DFLT_MIN_EPS,
+): Double = DFLT_EPS_MULTIPLIER * maxOf(minEpsilon, abs(a), abs(b))
+
+/**
+ * ```kotlin
+ * // replace
+ * String.format("%.3f", doubleValue)
+ * // with
+ * doubleValue.fmt("%.3f")
+ * ```
+ *
+ * @return [this] formatted by [fmt].
+ */
+public fun Double.fmt(fmt: String): String = String.format(fmt, this)
+
+/**
+ * If [this] is a `-.0` [Double], it converts it to a `+.0` one.
+ * Useful for comparisons, since `-.0 >= +.0` is `false`.
+ * @param[warnLogger] the [Logger] to use to log the warning msg if any.
+ */
+public fun Double.ifNeg0thenPos0(warnLogger: Logger? = null): Double =
+ if (this == -.0) {
+ warnLogger?.warn("negative 0 floating point converted to positive 0")
+ .0
+ } else {
+ this
+ }