diff --git a/build.gradle.kts b/build.gradle.kts index 44e87fd..3f7d095 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { testImplementation("org.assertj", "assertj-core", "3.22.0") testImplementation("org.junit.jupiter", "junit-jupiter-api", "5.8.2") testRuntimeOnly("org.junit.jupiter", "junit-jupiter-engine", "5.8.2") + implementation(kotlin("reflect")) } tasks { @@ -94,13 +95,20 @@ tasks { // By default, build everything, put it somewhere convenient, and run the tests. defaultTasks = mutableListOf("build", "test") +val compileArgs = listOf( + "-Xopt-in=kotlin.RequiresOptIn" +) + val compileKotlin: KotlinCompile by tasks compileKotlin.kotlinOptions { jvmTarget = "8" + freeCompilerArgs += compileArgs } + val compileTestKotlin: KotlinCompile by tasks compileTestKotlin.kotlinOptions { jvmTarget = "8" + freeCompilerArgs += compileArgs } java { diff --git a/src/main/kotlin/org/ageseries/libage/data/BiMap.kt b/src/main/kotlin/org/ageseries/libage/data/BiMap.kt index 848be88..0272c79 100644 --- a/src/main/kotlin/org/ageseries/libage/data/BiMap.kt +++ b/src/main/kotlin/org/ageseries/libage/data/BiMap.kt @@ -57,7 +57,7 @@ interface MutableBiMap: BiMap { * the guarantees above. */ /** - * Add a bijection from [f] to [b], returning true iff some bijection had to be replaced. + * Add a bijection from [f] to [b], returning true if some bijection had to be replaced. */ fun addOrReplace(f: F, b: B): Boolean @@ -65,8 +65,14 @@ interface MutableBiMap: BiMap { * Add a bijection from [f] to [b]. If this replaces some other bijection, raise an error. */ fun add(f: F, b: B) { - if(forward[f] != null || backward[b] != null) error("would replace a bijection") - addOrReplace(f, b) // assert not this + val existingF = forward[f] + val existingB = backward[b] + + if(existingF != null || existingB != null) { + error("Associating $f and $b would replace $existingF $existingB") + } + + addOrReplace(f, b) } /** @@ -94,6 +100,20 @@ interface MutableBiMap: BiMap { } } +/** + * Bijectively associates the keys with values selected using [valueSelector]. + * Similar to Kotlin's [associateWith]. + * */ +inline fun Iterable.associateWithBi(valueSelector: (K) -> V): BiMap { + val result = mutableBiMapOf() + + this.forEach { k -> + result.add(k, valueSelector(k)) + } + + return result +} + /** * An implementation of [MutableBiMap] using a pair of Kotlin's standard maps. */ @@ -139,17 +159,24 @@ class MutableMapPairBiMap(pairs: Iterator>): MutableBiMap forward.clear() backward.clear() } + + override fun toString() = buildString { + forward.forEach { (f, b) -> + this.appendLine("$f <-> $b") + } + } } /** * An immutable view of a realized [MutableBiMap]. */ @JvmInline -value class ImmutableBiMapView(val inner: MutableBiMap): BiMap { +value class ImmutableBiMapView(private val inner: MutableBiMap): BiMap { override val forward: Map - inline get() = inner.forward + get() = inner.forward + override val backward: Map - inline get() = inner.backward + get() = inner.backward } /** diff --git a/src/main/kotlin/org/ageseries/libage/data/DisjointSet.kt b/src/main/kotlin/org/ageseries/libage/data/DisjointSet.kt index f22f785..9673030 100644 --- a/src/main/kotlin/org/ageseries/libage/data/DisjointSet.kt +++ b/src/main/kotlin/org/ageseries/libage/data/DisjointSet.kt @@ -1,25 +1,19 @@ package org.ageseries.libage.data /** - * A class for using the Disjoint Sets "Union-Find" algorithm. + * Implementation of the Disjoint-Set data structure designed for use in conjunction with the Union-Find algorithm. + * It is intended to be extended by inheritors, where the term "Super" (akin to a **superclass**) clarifies its nature. + * To enhance usability, a *self-type parameter* ([Self]) has been integrated. * - * There are two ways to use this: + * The [representative] can be retrieved as [Self], and the [union] operation readily accepts [Self]. + * Essentially, inheritors can work with instances of their class without necessitating casting (unlike the old implementation). * - * - Inherit from it, using the methods on the object itself; - * - compose it into your class; or - * - do both. - * - * Typical use case is as follows: - * - * 1. Choose your method above; for the former, the DisjointSet object is `this`; for the latter, substitute your field. - * 2. Unify DisjointSets by calling [unite]. - * 3. Determine if two DisjointSets are in the same component by testing their [representative] for equality. - * 4. Optionally, mutate the data on a DisjointSet subclass' representative, knowing that the mutations are visible from all DisjointSets with the same representative. - * - * Note that it is difficult to control which instance will ultimately *be* the representative; in the cases where it can't be avoided, [priority] can be used, but this is advisable only as a last resort (since it reduces the optimality of this algorithm). + * It's important to note that controlling which instance ultimately becomes the representative can be challenging. + * In cases where this is unavoidable, the [priority] parameter can be employed. + * However, it's advisable to use this option sparingly, as it compromises the optimality of the algorithm and should only be considered as a last resort. */ -open class DisjointSet { - +@Suppress("UNCHECKED_CAST") // Side effect of self parameter. Fine in our case. +abstract class SuperDisjointSet> { /** * The size of this tree; loosely, how many Sets have been merged, including transitively, with this one. * @@ -32,7 +26,8 @@ open class DisjointSet { * * Following this recursively will lead to the [representative]. All representatives refer to themselves as their parent. */ - var parent: DisjointSet = this + @Suppress("LeakingThis") // Fine in this case. + var parent: Self = this as Self /** * The priority of the merge. If set, this overrides the "merge by size" algorithm in [unite]. @@ -48,34 +43,65 @@ open class DisjointSet { * * This is implemented using the "Path splitting" algorithm. */ - val representative: DisjointSet + val representative: Self get() { - var cur = this - while (cur.parent != cur) { - val next = cur.parent - cur.parent = next.parent - cur = next + var current = this + + while (current.parent != current) { + val next = current.parent + current.parent = next.parent + current = next } - return cur + + return current as Self } /** - * Unite this instance with another instance of DisjointSet. + * *Union operation*. * * After this is done, both this and [other] will have the same [representative] as each other (and as all other Sets with which they were previously united). * * This is implemented using the "by size" merge algorithm, adjusted for [priority]. */ - open fun unite(other: DisjointSet) { - val trep = representative - val orep = other.representative - if (trep == orep) return - val (bigger, smaller) = when { - trep.priority > orep.priority -> Pair(trep, orep) - orep.priority > trep.priority -> Pair(orep, trep) - else -> if (trep.size < orep.size) Pair(orep, trep) else Pair(trep, orep) + open fun unite(other: Self) { + val thisRep = representative + val otherRep = other.representative + + if (thisRep == otherRep){ + return + } + + val bigger: Self + val smaller: Self + + // Override with priority: + if(thisRep.priority > otherRep.priority) { + bigger = thisRep + smaller = otherRep + } + else if(otherRep.priority > thisRep.priority) { + bigger = otherRep + smaller = thisRep + } + else { + // By size: + if (thisRep.size < otherRep.size) { + bigger = otherRep + smaller = thisRep + } else { + bigger = thisRep + smaller = otherRep + } } + smaller.parent = bigger.parent bigger.size += smaller.size } } + +/** + * Composable disjoint set, implemented as a [SuperDisjointSet]. + * This implementation proves beneficial in specific algorithms where certain elements are partitioned using union-find. + * These elements might represent pure data or simply may not want or need to implement [SuperDisjointSet]. + * */ +class DisjointSet(override var priority: Int = 0) : SuperDisjointSet() // *It also proves useful in unit tests. \ No newline at end of file diff --git a/src/main/kotlin/org/ageseries/libage/data/Events.kt b/src/main/kotlin/org/ageseries/libage/data/Events.kt new file mode 100644 index 0000000..e2365e1 --- /dev/null +++ b/src/main/kotlin/org/ageseries/libage/data/Events.kt @@ -0,0 +1,118 @@ +package org.ageseries.libage.data + +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.reflect.KClass + +/** + * Producer of events which are distributed to [EventHandler]s. + * */ +interface EventSource { + /** + * Adds a [handler] for the event [eventClass]. + * */ + fun registerHandler(eventClass: KClass<*>, handler: EventHandler) + + /** + * Removes [handler] for the event [eventClass]. + * */ + fun unregisterHandler(eventClass: KClass<*>, handler: EventHandler) +} + +/** + * Dispatcher for events of different types. + * */ +interface EventDispatcher { + /** + * Sends the [event] to all handlers capable of handling it. + * @return The number of handlers which received the event. + * */ + fun send(event: Event): Int +} + +/** + * The Event Bus oversees a collection of event handlers. + * When an event is dispatched, it is relayed to all handlers capable of managing that specific event type. + * Currently, event polymorphism is not implemented so a handler must be registered with the exact class of the event. + * The system is designed with thread safety in mind. + * @param allowList An optional whitelist of event classes. If a handler for an event which is not allowed is registered, an error is raised. Same goes for events that are sent. + * */ +class EventBus(private val allowList: Set>? = null) : EventSource, EventDispatcher { + private val handlers = ConcurrentHashMap, CopyOnWriteArrayList>>() + + private fun validateEvent(eventClass: KClass<*>) { + if (allowList != null) { + check(allowList.contains(eventClass)) { + "The event manager prohibits $eventClass" + } + } + + } + + override fun registerHandler(eventClass: KClass<*>, handler: EventHandler) { + validateEvent(eventClass) + + val handlers = handlers.computeIfAbsent(eventClass) { + CopyOnWriteArrayList() + } + + handlers.add(handler) + } + + override fun unregisterHandler(eventClass: KClass<*>, handler: EventHandler) { + validateEvent(eventClass) + + val handlers = handlers[eventClass] + ?: error("Could not find handlers for $eventClass") + + if (!handlers.remove(handler)) { + error("Could not remove handler $handler") + } + } + + /** + * Sends an event to all subscribed listeners. + * */ + override fun send(event: Event): Int { + validateEvent(event.javaClass.kotlin) + + val listeners = this.handlers[event::class] + ?: return 0 + + var visited = 0 + listeners.forEach { + it.handle(event) + ++visited + } + + return visited + } +} + +/** + * Registers an event handler for events of type [TEvent]. + * */ +@Suppress("UNCHECKED_CAST") +inline fun EventSource.registerHandler(handler: EventHandler) { + registerHandler(TEvent::class, handler as EventHandler) +} + +/** + * Removes an event handler for events of type [TEvent]. + * */ +@Suppress("UNCHECKED_CAST") +inline fun EventSource.unregisterHandler(handler: EventHandler) { + unregisterHandler(TEvent::class, handler as EventHandler) +} + +/** + * Marker interface implemented by all events. + * */ +interface Event + +/** + * A handler for events of the specified type. + * */ +fun interface EventHandler { + fun handle(event: T) +} \ No newline at end of file diff --git a/src/main/kotlin/org/ageseries/libage/data/MultiMap.kt b/src/main/kotlin/org/ageseries/libage/data/MultiMap.kt index db27643..ce2fe85 100644 --- a/src/main/kotlin/org/ageseries/libage/data/MultiMap.kt +++ b/src/main/kotlin/org/ageseries/libage/data/MultiMap.kt @@ -243,3 +243,23 @@ fun Map.toMultiMap(): MultiMap = entries.map { it.toPair() }. * Creates a MutableMultiMap from a Map, with each key associated to a set of size at most one. */ fun Map.toMutableMultiMap(): MutableMultiMap = entries.map { it.toPair() }.toMutableMultiMap() + +/** + * Constructs a [MultiMap] keyed by [K] with [T] values. + * Similar to [associateBy] + * */ +inline fun Iterable.associateByMulti(keySelector: (T) -> K): MultiMap { + val result = MutableSetMapMultiMap() + + this.forEach { value -> + result[keySelector(value)].add(value) + } + + return result +} + +inline fun Array.associateByMulti(keySelector: (T) -> K): MultiMap { + val result = MutableSetMapMultiMap() + this.forEach { value -> result[keySelector(value)].add(value) } + return result +} \ No newline at end of file diff --git a/src/main/kotlin/org/ageseries/libage/data/Quantity.kt b/src/main/kotlin/org/ageseries/libage/data/Quantity.kt index c2212e1..ecf14fe 100644 --- a/src/main/kotlin/org/ageseries/libage/data/Quantity.kt +++ b/src/main/kotlin/org/ageseries/libage/data/Quantity.kt @@ -1,9 +1,26 @@ +@file:Suppress("unused") +@file:JvmName(JVM_NAME) + package org.ageseries.libage.data import org.ageseries.libage.mathematics.rounded +import org.ageseries.libage.utils.sourceName import kotlin.math.abs import kotlin.math.log import kotlin.math.pow +import kotlin.reflect.KProperty +import kotlin.reflect.jvm.kotlinProperty + +typealias ScaleRef = KProperty> + +private const val JVM_NAME = "QuantityKt" +private const val CLASS_NAME = "org.ageseries.libage.data.$JVM_NAME" + +private val SELF by lazy { + checkNotNull(Class.forName(CLASS_NAME)) { + "Failed to resolve Quantity class $CLASS_NAME" + } +} /** * A linear scale. @@ -27,56 +44,157 @@ data class Scale(val factor: Double, val base: Double) { } /** - * [Scale] parameterized by [Unit], which corresponds to the physical property that the scale measures (e.g. distance, time, ...) + * [Scale], parameterized by [Unit], aligns with the physical attribute measured by the scale (such as distance, time, and so forth). + * Notably, [Unit] remains distinct from the [scale]; for instance, [Distance] can be measured in [METER]s, feet, dragon tails, american football fields, and other units. + * It is important to recognize that [Unit] functions merely as a compiler mechanism and holds no inherent functional significance. + * @param dimensionType The class of the *symbolic interface* [Unit]. This is the same for all auxiliary units. * */ -data class QuantityScale(val scale: Scale) { - val base get() = scale.base - +open class QuantityScale(val dimensionType: Class<*>, val scale: Scale) { val factor get() = scale.factor /** * Amplifies this scale [amplify] times. + * Example: *GRAMS * 1000* will result in *KILOGRAMS*. * */ - operator fun times(amplify: Double) = QuantityScale(Scale(scale.factor / amplify, scale.base)) + operator fun times(amplify: Double) = QuantityScale(dimensionType, Scale(scale.factor / amplify, scale.base)) /** * Reduces this scale [reduce] times. + * Example: *KILOGRAMS / 1000* will result in *GRAMS*. * */ - operator fun div(reduce: Double) = QuantityScale(Scale(scale.factor * reduce, scale.base)) + operator fun div(reduce: Double) = QuantityScale(dimensionType, Scale(scale.factor * reduce, scale.base)) /** * Amplifies this scale 1000 times. + * Example: *+GRAMS* will result in *KILOGRAMS*. * */ operator fun unaryPlus() = this * 1000.0 /** * Reduces this scale 1000 times. + * Example: *-KILOGRAMS* will result in *GRAMS*. * */ operator fun unaryMinus() = this / 1000.0 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as QuantityScale<*> + + return scale == other.scale + } + + override fun hashCode(): Int { + return scale.hashCode() + } +} + +/** + * Signifies a multiplication factor employed to adjust a [QuantityScale]. + * Presently, its extension is not supported; to minimize runtime cost, a lookup table is precomputed. + * This design ensures the avoidance of additional allocations for new instances of [QuantityScale] (the composition cost boils down to accessing the precomputed instance). + * */ +class ScaleMultiplier internal constructor(val factor: Double, val index: Int) { + /** + * Composes the base [scale] with this multiplier, returning a scale which is equivalent to the result of *[scale] * [factor]*. + * */ + operator fun times(scale: SourceQuantityScale) = scale.multiples[index] } /** - * Represents a physical quantity, characterised by a [Unit] and a real number [value]. + * Composes the base scale with this multiplier, returning another source scale. + * */ +internal infix fun ScaleMultiplier.sourceCompose(scale: SourceQuantityScale) = SourceQuantityScale(scale.dimensionType, scale.multiples[index].scale) + +/** Transforms the units into **pico- (×10⁻¹²)** */ +val PICO = ScaleMultiplier(1e-12, 0) + +/** Transforms the units into **nano- (×10⁻⁹)** */ +val NANO = ScaleMultiplier(1e-9, 1) + +/** Transforms the units into **micro- (×10⁻⁶)** */ +val MICRO = ScaleMultiplier(1e-6, 2) + +/** Transforms the units into **milli- (×10⁻³)** */ +val MILLI = ScaleMultiplier(1e-3, 3) + +/** Transforms the units into **kilo- (×10³)** */ +val KILO = ScaleMultiplier(1e3, 4) + +/** Transforms the units into **nano- (×10⁶)** */ +val MEGA = ScaleMultiplier(1e6, 5) + +/** Transforms the units into **nano- (×10⁹)** */ +val GIGA = ScaleMultiplier(1e9, 6) + +/** Transforms the units into **nano- (×10¹²)** */ +val TERA = ScaleMultiplier(1e12, 7) + +private val MULTIPLIERS = listOf(PICO, NANO, MICRO, MILLI, KILO, MEGA, GIGA, TERA) + +/** + * [QuantityScale] that is composable with [ScaleMultiplier]. + * "Source" means it is a fundamental multiple of the standard (base) scale; normal multiplication/division yields non-source [QuantityScale] instances. + * Yielding [SourceQuantityScale] would cause infinite recursive creation of child scales. + * */ +class SourceQuantityScale(dimensionType: Class<*>, scale: Scale) : QuantityScale(dimensionType, scale) { + /** + * Pre-computed lookup table for multiples of this scale. + * When we apply a [ScaleMultiplier], we just fetch the pre-computed scale. + * */ + internal val multiples = Array(MULTIPLIERS.size) { index -> + val multiplier = MULTIPLIERS.first { it.index == index } + // This operation creates the base [QuantityScale] which doesn't just recursively keep creating other lookup tables. + this * multiplier.factor + } +} + +/** + * Amplifies this scale and results in a new [SourceQuantityScale] (as opposed to [times] which results in [QuantityScale]). + * Separating from [this.times] is necessary to prevent the children scales creating children scales infinitely. + * */ +internal infix fun SourceQuantityScale.sourceAmplify(amplify: Double) = SourceQuantityScale(dimensionType, Scale(scale.factor / amplify, scale.base)) + +/** + * Reduces this scale and results in a new [SourceQuantityScale] (as opposed to [div] which results in [QuantityScale]). + * Separating from [this.div] is necessary to prevent the children scales creating children scales infinitely. + * */ +internal infix fun SourceQuantityScale.sourceReduce(reduce: Double) = SourceQuantityScale(dimensionType, Scale(scale.factor * reduce, scale.base)) + +internal fun SourceQuantityScale.sourceAmp() = this sourceAmplify 1000.0 + +internal fun SourceQuantityScale.sourceSub() = this sourceReduce 1000.0 + +/** + * Denotes a tangible quantity distinguished by a designated [Unit] and a numeric [value]. + * The [value] is consistently expressed in fundamental units, such as Kelvin, Meter, Kilogram, and the like. + * This design is sufficiently straightforward to be instantiated as an inline value-class, thereby rendering the runtime overhead of carrying around [Quantity] virtually negligible. + * The [not] operator was chosen because it is the least "ugly" way to quickly fetch the [value] for subsequent operations with [Double]. + * If you don't like it, Grissess, just access [value]. * */ @JvmInline value class Quantity(val value: Double) : Comparable> { - constructor(quantity: Double, s: QuantityScale) : this(s.scale.unmap(quantity)) + /** + * Creates a [Quantity] where its [value] is established as the equivalent of [sourceValue] in the [standardScale]. + * */ + constructor(sourceValue: Double, scale: QuantityScale) : this(scale.scale.unmap(sourceValue)) val isZero get() = value == 0.0 /** - * Gets the numerical value of this quantity. + * Gets the [value] quickly and nicely. * */ operator fun not() = value - operator fun unaryMinus() = Quantity(-value) operator fun unaryPlus() = Quantity(+value) operator fun plus(b: Quantity) = Quantity(this.value + b.value) operator fun minus(b: Quantity) = Quantity(this.value - b.value) operator fun times(scalar: Double) = Quantity(this.value * scalar) operator fun div(scalar: Double) = Quantity(this.value / scalar) + /** - * Divides the quantity by another quantity of the same unit. This, in turn, cancels out the quantity, returning the resulting number. + * Divides the quantity by another quantity of the same unit. This, in turn, cancels out the [Unit], returning the resulting number. * */ operator fun div(b: Quantity) = this.value / b.value @@ -90,15 +208,26 @@ value class Quantity(val value: Double) : Comparable> { operator fun rangeTo(s: QuantityScale) = s.scale.map(value) override fun toString() = value.toString() - - fun reparam(factor: Double = 1.0) = Quantity(value * factor) } -/** An iterator over a sequence of values of type `Quantity`. */ +@JvmName("quantityPlusDouble") operator fun Quantity.plus(b: Double) = Quantity(this.value + b) +@JvmName("quantityMinusDouble") operator fun Quantity.minus(b: Double) = Quantity(this.value - b) +@JvmName("doublePlusQuantity") operator fun Double.plus(b: Quantity) = Quantity(this + b.value) +@JvmName("doubleMinusQuantity") operator fun Double.minus(b: Quantity) = Quantity(this - b.value) + +fun min(a: Quantity, b: Quantity) = Quantity(kotlin.math.min(!a, !b)) +fun max(a: Quantity, b: Quantity) = Quantity(kotlin.math.max(!a, !b)) +fun abs(q: Quantity) = Quantity(abs(!q)) + +/** + * An iterator over a sequence of values of type [Quantity]. + * */ abstract class QuantityIterator : Iterator> { final override fun next() = nextQuantity() - /** Returns the next value in the sequence without boxing. */ + /** + * Returns the next value in the sequence without boxing. + * */ abstract fun nextQuantity() : Quantity } @@ -138,290 +267,533 @@ class QuantityArray(val backing: DoubleArray) { } } -fun min(a: Quantity, b: Quantity) = Quantity(kotlin.math.min(!a, !b)) -fun max(a: Quantity, b: Quantity) = Quantity(kotlin.math.max(!a, !b)) -fun abs(q: Quantity) = Quantity(kotlin.math.abs(!q)) +/** + * Categorizes the given numerical value into a scale and represents it using a corresponding scale factor. + * A scale factor (such as "kilo," "mega," etc.) is selected from a predefined list based on the magnitude of the given value in the specified base. + * The chosen scale factor is inserted into the designated position marked by '#' within the unit string. + * If '#' does not exist, it will be implied that the multiple shall be inserted at the start of the string. + * [symbol] can also be left empty (you'll still get the number in formatted form and suffixed with the multiple). + * Examples: + * ``` + * classifyIntoMultiple( + * value = 1500.0, + * base = 1000.0, + * map = listOf(1.0 to "kilo"), + * symbol = "meters" + * ) // 1.5 kilometers + * + * classifyIntoMultiple( + * value = 10.0, + * base = 1000.0, + * map = listOf(1.0 to "kilo"), + * symbol = "meters" + * ) // 10.0 meters (because inferFist is true by default) + * + * classifyIntoMultiple( + * value = 10.0, + * base = 1000.0, + * map = listOf(1.0 to "kilo"), + * symbol = "meters", + * inferFist = false + * ) // 0.01 kilometers (because inferFist is false) + * + * classifyIntoMultiple( + * value = 10.0, + * base = 1000.0, + * map = listOf(1.0 to "kilo", 0.0 to "*"), + * symbol = "meters", + * inferFist = false + * ) // 10.0 *meters + * + * // Trying out non-integer magnitudes is left as an exercise to the reader. + * ``` + * @param value The value to classify. + * @param base The base to use. Usually, this is 10 or 1000. + * @param map A map of magnitudes *to* multiplier. Not actually a [Map] because the algorithm scans the entire map to find the best multiplier (justified because we don't care) + * @param symbol The unit to place after the multiplier. + * @param decimals The number of decimals to round the final value to. + * @param inferFirst If true, no multiple will be prepended to the [symbol], if the value is around magnitude 0. + * */ +fun classifyIntoMultiple(value: Double, base: Double, map: List>, symbol: String, decimals: Int = 2, inferFirst: Boolean = true) : String { + if(value < 0.0) { + return "-${classifyIntoMultiple(-value, base, map, symbol)}" + } + + val magnitude = log(value, base) + + var (targetMagnitude, prefix) = map + .filter { it.first <= magnitude } + .maxByOrNull { it.first } ?: + map.minByOrNull { abs(it.first - magnitude) }!! + + if(inferFirst) { + if(0.0 <= magnitude && abs(magnitude - targetMagnitude) > abs(magnitude)) { + prefix = "" + targetMagnitude = 0.0 + } + } + + val multiple = if(symbol.isNotEmpty()) { + if(symbol.contains("#")) { + symbol.replace("#", prefix) + } + else { + prefix + symbol + } + } + else { + prefix + } + + return "${(value / base.pow(targetMagnitude)).rounded(decimals)} $multiple" +} + +/** + * Describes the base of classification base and multiples. + * @param value The classification base. + * @param multiples Impromptu map of magnitudes to the desired multiple. + * */ +enum class ScaleMultiples(val value: Double, val multiples: List>) { + Base1000Standard( + 1000.0, + listOf( + -1.0 to "m", -2.0 to "µ", -3.0 to "n", -4.0 to "p", -5.0 to "f", -6.0 to "a", -7.0 to "z", -8.0 to "y", + 1.0 to "k", 2.0 to "M", 3.0 to "G", 4.0 to "T", 5.0 to "P", 6.0 to "E", 7.0 to "Z", 8.0 to "Y" + ) + ), + Base1000Long( + 1000.0, + listOf( + -1.0 to "milli", + -2.0 to "micro", + -3.0 to "nano", + -4.0 to "pico", + -5.0 to "femto", + -6.0 to "atto", + -7.0 to "zepto", + -8.0 to "yocto", + 1.0 to "kilo", + 2.0 to "mega", + 3.0 to "giga", + 4.0 to "tera", + 5.0 to "peta", + 6.0 to "exa", + 7.0 to "zetta", + 8.0 to "yotta" + ) + ), + Base10Standard( + 10.0, + listOf( + -1.0 to "d", + -2.0 to "c", + -3.0 to "m", + -6.0 to "µ", + -9.0 to "n", + -12.0 to "p", + -15.0 to "f", + -18.0 to "a", + -21.0 to "z", + -24.0 to "y", + 1.0 to "da", + 2.0 to "h", + 3.0 to "k", + 6.0 to "M", + 9.0 to "G", + 12.0 to "T", + 15.0 to "P", + 18.0 to "E", + 21.0 to "Z", + 24.0 to "Y" + ) + ), + Base10Long( + 10.0, + listOf( + -1.0 to "deci", + -2.0 to "centi", + -3.0 to "milli", + -6.0 to "micro", + -9.0 to "nano", + -12.0 to "pico", + -15.0 to "femto", + -18.0 to "atto", + -21.0 to "zepto", + -24.0 to "yocto", + 1.0 to "deka", + 2.0 to "hecto", + 3.0 to "kilo", + 6.0 to "mega", + 9.0 to "giga", + 12.0 to "tera", + 15.0 to "peta", + 18.0 to "exa", + 21.0 to "zetta", + 24.0 to "yotta" + ) + ) +} + +/** + * Classifies this physical dimension with the [symbol] (e.g. *"#m"* for meters). The symbol *#* is where the multiple will be placed. + * @param factor An adjustment factor for special cases. E.G. for [KILOGRAM], this factor is set to *1000* and the symbol is "g". + * @param base The base to use. This specifies the prefixes (e.g. "kilo", "mega", ...) to place in front of the [symbol] when [classifyIntoMultiple]ing. + * */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class DimensionClassifier( + val symbol: String, + val factor: Double = 1.0, + val base: ScaleMultiples = ScaleMultiples.Base1000Standard +) + +/** + * Auxiliary classifier for scales. It is applied to [QuantityScale] fields. + * This is used to override the default classification (e.g. using a config option downstream). + * @param identifiers Override names for this auxiliary scale. These will be used as identifiers instead of [symbol]. Use this when the symbol cannot be typed (e.g. Å) + * */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class ScaleClassifier( + val symbol: String, + vararg val identifiers: String, + val factor: Double = 1.0, + val base: ScaleMultiples = ScaleMultiples.Base1000Standard +) /** * Defines the standard scale of the [Unit] (a scale with factor 1). * */ -fun standardScale(factor: Double = 1.0) = QuantityScale(Scale(factor, 0.0)) +inline fun standardScale(factor: Double = 1.0) = SourceQuantityScale( + Unit::class.java, + Scale(factor, 0.0) +) + +@DimensionClassifier("g", factor = 1000.0) interface Mass +val KILOGRAM = standardScale() +@ScaleClassifier("g") val GRAM = KILOGRAM.sourceSub() +@ScaleClassifier("gr") val GRAIN = KILOGRAM sourceAmplify 6.479891e-5 +@ScaleClassifier("oz") val OUNCE = KILOGRAM sourceAmplify 0.0283495231 +@ScaleClassifier("st") val STONE = KILOGRAM sourceAmplify 6.35029318 +@ScaleClassifier("lb") val POUND = KILOGRAM sourceAmplify 0.45359237 +@ScaleClassifier("t") val TON = KILOGRAM sourceAmplify 907.18474 + +@ScaleClassifier("firkin") val FIRKIN = KILOGRAM sourceAmplify 40.8233 +@DimensionClassifier("Da") interface AtomicMass +val DALTON = standardScale() -interface Mass -val KILOGRAMS = standardScale() -val GRAMS = -KILOGRAMS -val MILLIGRAMS = -GRAMS -val kg by ::KILOGRAMS -val g by ::GRAMS -val mg by ::MILLIGRAMS +/** + * Gets the atomic mass expressed in [KILOGRAM]. + * The result may not be useful due to the limited precision we're working with. + * */ +fun Quantity.asStandardMass() = Quantity(!this * 1.66053906660e-27, KILOGRAM) -interface Time +@DimensionClassifier("s") interface Time val SECOND = standardScale