Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix double rounding issues for multipleOf assertion #71

Merged
merged 1 commit into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}
}
}
Loading