summaryrefslogtreecommitdiff
path: root/opendc-common/src/main/kotlin/org/opendc/common/utils/DoubleUtils.kt
diff options
context:
space:
mode:
Diffstat (limited to 'opendc-common/src/main/kotlin/org/opendc/common/utils/DoubleUtils.kt')
-rw-r--r--opendc-common/src/main/kotlin/org/opendc/common/utils/DoubleUtils.kt225
1 files changed, 225 insertions, 0 deletions
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
+ }