From 4f98fb2bf8204f6af52cd6eeb3313d21c6ca95bc Mon Sep 17 00:00:00 2001 From: Alessio Leonardo Tomei <122273875+T0mexX@users.noreply.github.com> Date: Thu, 22 Aug 2024 14:40:57 +0200 Subject: [PATCH] Added Unit of measurament system with new deserialization (#242) --- opendc-common/build.gradle.kts | 5 + .../opendc/common/annotations/InternalUse.kt | 28 ++ .../org/opendc/common/units/DataRate.kt | 183 +++++++++ .../org/opendc/common/units/DataSize.kt | 220 ++++++++++ .../kotlin/org/opendc/common/units/Energy.kt | 118 ++++++ .../org/opendc/common/units/Frequency.kt | 115 ++++++ .../org/opendc/common/units/Percentage.kt | 260 ++++++++++++ .../kotlin/org/opendc/common/units/Power.kt | 102 +++++ .../kotlin/org/opendc/common/units/Time.kt | 166 ++++++++ .../kotlin/org/opendc/common/units/Unit.kt | 386 ++++++++++++++++++ .../org/opendc/common/units/UnitSerializer.kt | 215 ++++++++++ .../org/opendc/common/utils/DoubleUtils.kt | 225 ++++++++++ .../compute/topology/TopologyFactories.kt | 7 +- .../compute/topology/specs/TopologySpecs.kt | 33 +- .../simulator/compute/model/MemoryUnit.java | 2 +- site/docs/documentation/Input/Topology.md | 35 ++ 16 files changed, 2086 insertions(+), 14 deletions(-) create mode 100644 opendc-common/src/main/kotlin/org/opendc/common/annotations/InternalUse.kt create mode 100644 opendc-common/src/main/kotlin/org/opendc/common/units/DataRate.kt create mode 100644 opendc-common/src/main/kotlin/org/opendc/common/units/DataSize.kt create mode 100644 opendc-common/src/main/kotlin/org/opendc/common/units/Energy.kt create mode 100644 opendc-common/src/main/kotlin/org/opendc/common/units/Frequency.kt create mode 100644 opendc-common/src/main/kotlin/org/opendc/common/units/Percentage.kt create mode 100644 opendc-common/src/main/kotlin/org/opendc/common/units/Power.kt create mode 100644 opendc-common/src/main/kotlin/org/opendc/common/units/Time.kt create mode 100644 opendc-common/src/main/kotlin/org/opendc/common/units/Unit.kt create mode 100644 opendc-common/src/main/kotlin/org/opendc/common/units/UnitSerializer.kt create mode 100644 opendc-common/src/main/kotlin/org/opendc/common/utils/DoubleUtils.kt diff --git a/opendc-common/build.gradle.kts b/opendc-common/build.gradle.kts index e0524f3c1..2dd35d83b 100644 --- a/opendc-common/build.gradle.kts +++ b/opendc-common/build.gradle.kts @@ -26,10 +26,15 @@ description = "Common functionality used across OpenDC modules" // Build configuration plugins { `kotlin-library-conventions` + kotlin("plugin.serialization") version "1.9.22" } +val serializationVersion = "1.6.0" + dependencies { api(libs.kotlinx.coroutines) + implementation(libs.kotlin.logging) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion") testImplementation(projects.opendcSimulator.opendcSimulatorCore) } 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 000000000..e32aa811d --- /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 000000000..2af45b7b5 --- /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 { + @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( + 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 000000000..e32d9e88d --- /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 { + @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( + 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 000000000..467192a01 --- /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 { + 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( + 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 000000000..df1b49f65 --- /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 { + 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( + 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 000000000..377fdecc4 --- /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 { + 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.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.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( + 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 000000000..fc9f6bf43 --- /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 { + @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( + 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 000000000..9d72ddfc0 --- /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