Skip to content

Commit c6b2ce3

Browse files
committed
Correct multipleOf assertion to work with small numbers
1 parent 6a9107a commit c6b2ce3

File tree

2 files changed

+101
-3
lines changed

2 files changed

+101
-3
lines changed

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

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import kotlinx.serialization.json.JsonElement
88
import kotlinx.serialization.json.JsonPrimitive
99
import kotlinx.serialization.json.doubleOrNull
1010
import kotlinx.serialization.json.longOrNull
11+
import kotlin.math.absoluteValue
12+
import kotlin.math.floor
13+
import kotlin.math.log10
14+
import kotlin.math.max
15+
import kotlin.math.pow
1116

1217
@Suppress("unused")
1318
internal object MultipleOfAssertionFactory : AbstractAssertionFactory("multipleOf") {
@@ -39,13 +44,42 @@ private fun isMultipleOf(a: Number, b: Number): Boolean = when (a) {
3944
}
4045

4146
private infix fun Double.isMultipleOf(number: Number): Boolean = when (number) {
42-
is Double -> (this % number).let { it == 0.0 && it == -0.0 }
43-
is Long -> (this % number).let { it == 0.0 && it == -0.0 }
47+
is Double -> isZero(rem(this, number))
48+
is Long -> isZero((this % number))
4449
else -> false
4550
}
4651

4752
private infix fun Long.isMultipleOf(number: Number): Boolean = when (number) {
4853
is Long -> this % number == 0L
49-
is Double -> (this % number).let { it == 0.0 && it == -0.0 }
54+
is Double -> isZero(rem(this, number))
5055
else -> false
56+
}
57+
58+
private fun isZero(first: Double): Boolean {
59+
return first == -0.0 || first == 0.0
60+
}
61+
62+
private tailrec fun rem(first: Double, second: Double): Double {
63+
return if (second < 1 && second > -1) {
64+
val degree = floor(log10(second))
65+
if (first < 1 && first > -1) {
66+
val newDegree = max(floor(log10(second)), degree)
67+
val newPow = 10.0.pow(-newDegree)
68+
rem((first * newPow), (second * newPow))
69+
} else {
70+
val pow = 10.0.pow(-degree)
71+
(first * pow) % (second * pow)
72+
}
73+
} else {
74+
first % second
75+
}
76+
}
77+
78+
private fun rem(first: Long, second: Double): Double {
79+
return if (second < 1 && second > -1) {
80+
val degree = floor(log10(second))
81+
first % (second * 10.0.pow(-degree))
82+
} else {
83+
first % second
84+
}
5185
}

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package io.github.optimumcode.json.schema.assertions.number
22

3+
import io.github.optimumcode.json.pointer.JsonPointer
4+
import io.github.optimumcode.json.schema.ErrorCollector
5+
import io.github.optimumcode.json.schema.ErrorCollector.Companion
36
import io.github.optimumcode.json.schema.JsonSchema
47
import io.github.optimumcode.json.schema.ValidationError
58
import io.github.optimumcode.json.schema.base.KEY
69
import io.kotest.assertions.throwables.shouldThrow
710
import io.kotest.core.spec.style.FunSpec
11+
import io.kotest.core.test.TestScope
12+
import io.kotest.matchers.collections.shouldContainExactly
813
import io.kotest.matchers.collections.shouldHaveSize
914
import io.kotest.matchers.shouldBe
1015
import kotlinx.serialization.ExperimentalSerializationApi
@@ -110,5 +115,64 @@ class JsonSchemaMultipleOfValidationTest : FunSpec() {
110115
errors shouldHaveSize 0
111116
}
112117
}
118+
119+
JsonSchema.fromDefinition(
120+
"""
121+
{
122+
"${KEY}schema": "http://json-schema.org/draft-07/schema#",
123+
"multipleOf": 0.0001
124+
}
125+
""".trimIndent(),
126+
).apply {
127+
listOf(
128+
JsonUnquotedLiteral("0.0075"),
129+
JsonUnquotedLiteral("0.075"),
130+
JsonUnquotedLiteral("0.75"),
131+
JsonUnquotedLiteral("7.5"),
132+
JsonUnquotedLiteral("75"),
133+
JsonUnquotedLiteral("750"),
134+
JsonUnquotedLiteral("12391239123"),
135+
JsonUnquotedLiteral("-0.0075"),
136+
JsonUnquotedLiteral("-0.075"),
137+
JsonUnquotedLiteral("-0.75"),
138+
JsonUnquotedLiteral("-7.5"),
139+
JsonUnquotedLiteral("-75"),
140+
JsonUnquotedLiteral("-750"),
141+
JsonUnquotedLiteral("-12391239123"),
142+
).forEach {
143+
test("small number $it is multiple of 0.0001") {
144+
val errors = mutableListOf<ValidationError>()
145+
validate(it, errors::add) shouldBe true
146+
errors shouldHaveSize 0
147+
}
148+
}
149+
150+
listOf(
151+
JsonUnquotedLiteral("0.00001"),
152+
JsonUnquotedLiteral("0.00011"),
153+
JsonUnquotedLiteral("0.00751"),
154+
JsonUnquotedLiteral("0.01751"),
155+
JsonUnquotedLiteral("0.71751"),
156+
JsonUnquotedLiteral("1.71751"),
157+
JsonUnquotedLiteral("-0.00001"),
158+
JsonUnquotedLiteral("-0.00011"),
159+
JsonUnquotedLiteral("-0.00751"),
160+
JsonUnquotedLiteral("-0.01751"),
161+
JsonUnquotedLiteral("-0.71751"),
162+
JsonUnquotedLiteral("-1.71751"),
163+
).forEach {
164+
test("small number $it is not a multiple of 0.0001") {
165+
val errors = mutableListOf<ValidationError>()
166+
validate(it, errors::add) shouldBe false
167+
errors.shouldContainExactly(
168+
ValidationError(
169+
schemaPath = JsonPointer("/multipleOf"),
170+
objectPath = JsonPointer.ROOT,
171+
message = "$it is not a multiple of 0.0001",
172+
)
173+
)
174+
}
175+
}
176+
}
113177
}
114178
}

0 commit comments

Comments
 (0)