Skip to content

Commit

Permalink
Fix double rounding issues for multipleOf assertion (#71)
Browse files Browse the repository at this point in the history
Resolves #70
  • Loading branch information
OptimumCode authored Feb 28, 2024
1 parent f1ff524 commit b1bd666
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import io.github.optimumcode.json.schema.internal.factories.number.util.NumberCo
import io.github.optimumcode.json.schema.internal.factories.number.util.number
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlin.math.abs
import kotlin.math.floor
import kotlin.math.log10
import kotlin.math.max
import kotlin.math.pow
import kotlin.math.round

@Suppress("unused")
internal object MultipleOfAssertionFactory : AbstractAssertionFactory("multipleOf") {
Expand Down Expand Up @@ -66,6 +68,8 @@ private fun isZero(first: Double): Boolean {
return first == -0.0 || first == 0.0
}

private const val ROUND_THRESHOLD = 0.000000001

private tailrec fun rem(
first: Double,
second: Double,
Expand All @@ -75,16 +79,38 @@ private tailrec fun rem(
if (first < 1 && first > -1) {
val newDegree = max(floor(log10(second)), degree)
val newPow = 10.0.pow(-newDegree)
rem((first * newPow), (second * newPow))
rem(safeRound(first * newPow), safeRound(second * newPow))
} else {
val pow = 10.0.pow(-degree)
(first * pow) % (second * pow)
val newFirst = safeRound(first * pow)
val newSecond = safeRound(second * pow)

newFirst % newSecond
}
} else {
first % second
}
}

/**
* Rounds the [value] if an abs delta between original value and result of rounding
* is less than [ROUND_THRESHOLD].
* Otherwise, the original value is return.
*
* This method tries to solve the issue with double operation when not a precise result is returned.
* E.g. `19.99 * 100 = 1998.9999999999998` instead of `1999.0`
*/
private fun safeRound(value: Double): Double {
val rounded = round(value)
return if (abs(rounded - value) < ROUND_THRESHOLD) {
rounded
} else {
// we return the original value because the result was precise,
// and we don't need rounding to fix issue with double operations
value
}
}

private fun rem(
first: Long,
second: Double,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.github.optimumcode.json.pointer.JsonPointer
import io.github.optimumcode.json.schema.JsonSchema
import io.github.optimumcode.json.schema.ValidationError
import io.github.optimumcode.json.schema.base.KEY
import io.kotest.assertions.assertSoftly
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContainExactly
Expand Down Expand Up @@ -174,5 +175,22 @@ class JsonSchemaMultipleOfValidationTest : FunSpec() {
}
}
}

test("BUG_70 rounding problem with some numbers because double does not behave as you expect") {
val schema =
JsonSchema.fromDefinition(
"""
{
"multipleOf": 0.01
}
""".trimIndent(),
)
val errors = mutableListOf<ValidationError>()
val valid = schema.validate(JsonPrimitive(19.99), errors::add)
assertSoftly {
valid shouldBe true
errors shouldHaveSize 0
}
}
}
}

0 comments on commit b1bd666

Please sign in to comment.