diff options
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.kt | 225 |
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 + } |
