Skip to content

Commit b1bd666

Browse files
authored
Fix double rounding issues for multipleOf assertion (#71)
Resolves #70
1 parent f1ff524 commit b1bd666

File tree

2 files changed

+46
-2
lines changed

2 files changed

+46
-2
lines changed

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/factories/number/MultipleOfAssertionFactory.kt

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import io.github.optimumcode.json.schema.internal.factories.number.util.NumberCo
77
import io.github.optimumcode.json.schema.internal.factories.number.util.number
88
import kotlinx.serialization.json.JsonElement
99
import kotlinx.serialization.json.JsonPrimitive
10+
import kotlin.math.abs
1011
import kotlin.math.floor
1112
import kotlin.math.log10
1213
import kotlin.math.max
1314
import kotlin.math.pow
15+
import kotlin.math.round
1416

1517
@Suppress("unused")
1618
internal object MultipleOfAssertionFactory : AbstractAssertionFactory("multipleOf") {
@@ -66,6 +68,8 @@ private fun isZero(first: Double): Boolean {
6668
return first == -0.0 || first == 0.0
6769
}
6870

71+
private const val ROUND_THRESHOLD = 0.000000001
72+
6973
private tailrec fun rem(
7074
first: Double,
7175
second: Double,
@@ -75,16 +79,38 @@ private tailrec fun rem(
7579
if (first < 1 && first > -1) {
7680
val newDegree = max(floor(log10(second)), degree)
7781
val newPow = 10.0.pow(-newDegree)
78-
rem((first * newPow), (second * newPow))
82+
rem(safeRound(first * newPow), safeRound(second * newPow))
7983
} else {
8084
val pow = 10.0.pow(-degree)
81-
(first * pow) % (second * pow)
85+
val newFirst = safeRound(first * pow)
86+
val newSecond = safeRound(second * pow)
87+
88+
newFirst % newSecond
8289
}
8390
} else {
8491
first % second
8592
}
8693
}
8794

95+
/**
96+
* Rounds the [value] if an abs delta between original value and result of rounding
97+
* is less than [ROUND_THRESHOLD].
98+
* Otherwise, the original value is return.
99+
*
100+
* This method tries to solve the issue with double operation when not a precise result is returned.
101+
* E.g. `19.99 * 100 = 1998.9999999999998` instead of `1999.0`
102+
*/
103+
private fun safeRound(value: Double): Double {
104+
val rounded = round(value)
105+
return if (abs(rounded - value) < ROUND_THRESHOLD) {
106+
rounded
107+
} else {
108+
// we return the original value because the result was precise,
109+
// and we don't need rounding to fix issue with double operations
110+
value
111+
}
112+
}
113+
88114
private fun rem(
89115
first: Long,
90116
second: Double,

src/commonTest/kotlin/io/github/optimumcode/json/schema/assertions/number/JsonSchemaMultipleOfValidationTest.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import io.github.optimumcode.json.pointer.JsonPointer
44
import io.github.optimumcode.json.schema.JsonSchema
55
import io.github.optimumcode.json.schema.ValidationError
66
import io.github.optimumcode.json.schema.base.KEY
7+
import io.kotest.assertions.assertSoftly
78
import io.kotest.assertions.throwables.shouldThrow
89
import io.kotest.core.spec.style.FunSpec
910
import io.kotest.matchers.collections.shouldContainExactly
@@ -174,5 +175,22 @@ class JsonSchemaMultipleOfValidationTest : FunSpec() {
174175
}
175176
}
176177
}
178+
179+
test("BUG_70 rounding problem with some numbers because double does not behave as you expect") {
180+
val schema =
181+
JsonSchema.fromDefinition(
182+
"""
183+
{
184+
"multipleOf": 0.01
185+
}
186+
""".trimIndent(),
187+
)
188+
val errors = mutableListOf<ValidationError>()
189+
val valid = schema.validate(JsonPrimitive(19.99), errors::add)
190+
assertSoftly {
191+
valid shouldBe true
192+
errors shouldHaveSize 0
193+
}
194+
}
177195
}
178196
}

0 commit comments

Comments
 (0)