Skip to content

Commit 390a45a

Browse files
authored
Merge pull request #401 from zgyorffi/master
Fix #400, keep precision in case of big numbers
2 parents e918632 + 0779550 commit 390a45a

File tree

10 files changed

+191
-38
lines changed

10 files changed

+191
-38
lines changed

core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@
219219
<dependency>
220220
<groupId>org.json</groupId>
221221
<artifactId>json</artifactId>
222-
<version>20190722</version>
222+
<version>20201115</version>
223223
</dependency>
224224
<dependency>
225225
<groupId>com.google.guava</groupId>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.everit.json.schema;
2+
3+
import java.math.BigDecimal;
4+
import java.math.BigInteger;
5+
import java.util.Objects;
6+
7+
8+
public class NumberComparator {
9+
10+
static BigDecimal getAsBigDecimal(Object number) {
11+
if (number instanceof BigDecimal) {
12+
return (BigDecimal) number;
13+
} else if (number instanceof BigInteger) {
14+
return new BigDecimal((BigInteger) number);
15+
} else if (number instanceof Integer || number instanceof Long) {
16+
return new BigDecimal(((Number) number).longValue());
17+
} else {
18+
double d = ((Number) number).doubleValue();
19+
return BigDecimal.valueOf(d);
20+
}
21+
}
22+
23+
static boolean deepEquals(Number num1, Number num2) {
24+
if (num1.getClass() != num2.getClass()) {
25+
return compare(num1, num2) == 0;
26+
}
27+
return Objects.equals(num1, num2);
28+
}
29+
30+
static int compare(Number num1, Number num2) {
31+
return getAsBigDecimal(num1).compareTo(getAsBigDecimal(num2));
32+
}
33+
34+
}

core/src/main/java/org/everit/json/schema/NumberSchemaValidatingVisitor.java

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package org.everit.json.schema;
22

3-
import static java.lang.String.format;
4-
53
import java.math.BigDecimal;
64

5+
import static org.everit.json.schema.NumberComparator.compare;
6+
import static org.everit.json.schema.NumberComparator.getAsBigDecimal;
7+
78

89
class NumberSchemaValidatingVisitor extends Visitor {
9-
10+
1011
private final Object subject;
1112

1213
private final ValidatingVisitor owner;
@@ -15,7 +16,7 @@ class NumberSchemaValidatingVisitor extends Visitor {
1516

1617
private boolean exclusiveMaximum;
1718

18-
private double numberSubject;
19+
private Number numberSubject;
1920

2021
NumberSchemaValidatingVisitor(Object subject, ValidatingVisitor owner) {
2122
this.subject = subject;
@@ -26,7 +27,7 @@ class NumberSchemaValidatingVisitor extends Visitor {
2627
void visitNumberSchema(NumberSchema numberSchema) {
2728
Class expectedType = numberSchema.requiresInteger() ? Integer.class : Number.class;
2829
if (owner.passesTypeCheck(expectedType, numberSchema.requiresInteger() || numberSchema.isRequiresNumber(), numberSchema.isNullable())) {
29-
this.numberSubject = ((Number) subject).doubleValue();
30+
this.numberSubject = ((Number) subject);
3031
super.visitNumberSchema(numberSchema);
3132
}
3233
}
@@ -41,17 +42,18 @@ void visitMinimum(Number minimum) {
4142
if (minimum == null) {
4243
return;
4344
}
44-
if (exclusiveMinimum && numberSubject <= minimum.doubleValue()) {
45+
int comparison = compare(numberSubject, minimum);
46+
if (exclusiveMinimum && comparison <= 0) {
4547
owner.failure(subject + " is not greater than " + minimum, "exclusiveMinimum");
46-
} else if (numberSubject < minimum.doubleValue()) {
48+
} else if (comparison < 0) {
4749
owner.failure(subject + " is not greater or equal to " + minimum, "minimum");
4850
}
4951
}
5052

5153
@Override
5254
void visitExclusiveMinimumLimit(Number exclusiveMinimumLimit) {
5355
if (exclusiveMinimumLimit != null) {
54-
if (numberSubject <= exclusiveMinimumLimit.doubleValue()) {
56+
if (compare(numberSubject, exclusiveMinimumLimit) <= 0) {
5557
owner.failure(subject + " is not greater than " + exclusiveMinimumLimit, "exclusiveMinimum");
5658
}
5759
}
@@ -62,9 +64,10 @@ void visitMaximum(Number maximum) {
6264
if (maximum == null) {
6365
return;
6466
}
65-
if (exclusiveMaximum && maximum.doubleValue() <= numberSubject) {
67+
int comparison = compare(maximum, numberSubject);
68+
if (exclusiveMaximum && comparison <= 0) {
6669
owner.failure(subject + " is not less than " + maximum, "exclusiveMaximum");
67-
} else if (maximum.doubleValue() < numberSubject) {
70+
} else if (comparison < 0) {
6871
owner.failure(subject + " is not less or equal to " + maximum, "maximum");
6972
}
7073
}
@@ -77,7 +80,7 @@ void visitExclusiveMaximum(boolean exclusiveMaximum) {
7780
@Override
7881
void visitExclusiveMaximumLimit(Number exclusiveMaximumLimit) {
7982
if (exclusiveMaximumLimit != null) {
80-
if (numberSubject >= exclusiveMaximumLimit.doubleValue()) {
83+
if (compare(numberSubject, exclusiveMaximumLimit) >= 0) {
8184
owner.failure(subject + " is not less than " + exclusiveMaximumLimit, "exclusiveMaximum");
8285
}
8386
}
@@ -86,8 +89,8 @@ void visitExclusiveMaximumLimit(Number exclusiveMaximumLimit) {
8689
@Override
8790
void visitMultipleOf(Number multipleOf) {
8891
if (multipleOf != null) {
89-
BigDecimal remainder = BigDecimal.valueOf(numberSubject).remainder(
90-
BigDecimal.valueOf(multipleOf.doubleValue()));
92+
BigDecimal remainder = getAsBigDecimal(numberSubject).remainder(
93+
getAsBigDecimal(multipleOf));
9194
if (remainder.compareTo(BigDecimal.ZERO) != 0) {
9295
owner.failure(subject + " is not a multiple of " + multipleOf, "multipleOf");
9396
}

core/src/main/java/org/everit/json/schema/ObjectComparator.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package org.everit.json.schema;
22

3-
import static org.everit.json.schema.loader.OrgJsonUtil.getNames;
3+
import org.json.JSONArray;
4+
import org.json.JSONObject;
45

56
import java.util.Arrays;
67
import java.util.Objects;
78

8-
import org.json.JSONArray;
9-
import org.json.JSONObject;
9+
import static org.everit.json.schema.loader.OrgJsonUtil.getNames;
10+
1011

1112
/**
1213
* Deep-equals implementation on primitive wrappers, {@link JSONObject} and {@link JSONArray}.
@@ -33,6 +34,12 @@ public static boolean deepEquals(Object obj1, Object obj2) {
3334
return false;
3435
}
3536
return deepEqualObjects((JSONObject) obj1, (JSONObject) obj2);
37+
} else if (obj1 instanceof Number) {
38+
if (!(obj2 instanceof Number)) {
39+
return false;
40+
} else {
41+
return NumberComparator.deepEquals((Number) obj1, (Number) obj2);
42+
}
3643
}
3744
return Objects.equals(obj1, obj2);
3845
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.everit.json.schema;
2+
3+
import org.junit.jupiter.params.ParameterizedTest;
4+
import org.junit.jupiter.params.provider.MethodSource;
5+
6+
import java.math.BigDecimal;
7+
import java.math.BigInteger;
8+
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
import static org.junit.jupiter.api.Assertions.assertFalse;
11+
import static org.junit.jupiter.api.Assertions.assertTrue;
12+
13+
14+
public class NumberComparatorTest {
15+
private static Object[][] equalCases() {
16+
return new Object[][] {
17+
{ "int 1, double 1.0", 1, 1.0d },
18+
{ "int 1, long 1", 1, 1L },
19+
{ "double 1.0, long 1", 1.0d, 1L },
20+
{ "int 2, double 2.0", 2, 2.0d },
21+
{ "int 3, double 3.00000", 3, 3.00000d },
22+
{ "int -1, double -1.0", -1, -1.0d },
23+
{ "double 1.0, double 1.000", 1.0d, 1.000d },
24+
{ "big decimal, double 0.0001", new BigDecimal("0.000100000000000"), 0.0001d },
25+
{ "big decimal, double 1.1", new BigDecimal("1.100000000000000"), 1.1d },
26+
{ "big integer, big decimal", new BigInteger("18446744073709551616"), new BigDecimal("18446744073709551616.0") },
27+
{ "big integers", new BigInteger("18446744073709551616"), new BigInteger("18446744073709551616") },
28+
{ "big decimals", new BigDecimal("18446744073709551616.0"), new BigDecimal("18446744073709551616.0") },
29+
{ "big decimal, double large value", new BigDecimal("1844674407370.000000"), 1844674407370.0d },
30+
{ "long, big decimal", 184467440737L, new BigDecimal("184467440737.000000") },
31+
};
32+
}
33+
34+
private static Object[][] notEqualCases() {
35+
return new Object[][] {
36+
{ "int 1, double 1.01", 1, 1.01d },
37+
{ "long 1, double 1.1", 1L, 1.1d },
38+
{ "int 2, double 2.1", 2, 2.1d },
39+
{ "int 3, double 3.00001", 3, 3.00001d },
40+
{ "double -1.1, int -1", -1.1d, -1 },
41+
{ "double 1.0, double 1.001", 1.0d, 1.001d },
42+
{ "big integer, big decimal", new BigInteger("18446744073709551616"), new BigDecimal("18446744073709551616.1") },
43+
{ "big integers", new BigInteger("18446744073709551616"), new BigInteger("18446744073709551617") },
44+
{ "double, big integer", 1844674407370.0d, new BigInteger("18446744073709551616") },
45+
{ "big decimals", new BigDecimal("18446744073709551616.0"), new BigDecimal("18446744073709551616.1") },
46+
{ "double, big decimal,", 1844674407370.0d, new BigDecimal("1844674407370.000001") },
47+
{ "long, big decimal", 184467440737L, new BigDecimal("184467440737.000001") },
48+
};
49+
}
50+
51+
@ParameterizedTest(name = "{0} (numbers are equal)")
52+
@MethodSource("equalCases")
53+
public void numberComparationSuccess(String testcaseName, Number arg1, Number arg2) {
54+
assertTrue(NumberComparator.deepEquals(arg1, arg2));
55+
assertEquals(0, NumberComparator.compare(arg2, arg1));
56+
assertEquals(0, NumberComparator.compare(arg1, arg2));
57+
}
58+
59+
@ParameterizedTest(name = "{0} (numbers are not equal)")
60+
@MethodSource("notEqualCases")
61+
public void numberComparationFailure(String testcaseName, Number arg1, Number arg2) {
62+
assertFalse(ObjectComparator.deepEquals(arg1, arg2));
63+
assertFalse(ObjectComparator.deepEquals(arg2, arg1));
64+
assertEquals(-1, NumberComparator.compare(arg1, arg2));
65+
assertEquals(1, NumberComparator.compare(arg2, arg1));
66+
}
67+
}

core/src/test/java/org/everit/json/schema/NumberSchemaTest.java

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,23 @@
1515
*/
1616
package org.everit.json.schema;
1717

18-
import static org.everit.json.schema.JSONMatcher.sameJsonAs;
19-
import static org.everit.json.schema.TestSupport.buildWithLocation;
20-
import static org.everit.json.schema.TestSupport.loadAsV6;
21-
import static org.junit.jupiter.api.Assertions.assertEquals;
22-
import static org.junit.jupiter.api.Assertions.fail;
23-
24-
import java.math.BigInteger;
25-
import java.util.concurrent.atomic.AtomicInteger;
26-
import java.util.concurrent.atomic.AtomicLong;
27-
18+
import nl.jqno.equalsverifier.EqualsVerifier;
19+
import nl.jqno.equalsverifier.Warning;
2820
import org.everit.json.schema.loader.SchemaLoader;
2921
import org.hamcrest.MatcherAssert;
3022
import org.json.JSONObject;
3123
import org.junit.jupiter.api.Test;
3224

33-
import nl.jqno.equalsverifier.EqualsVerifier;
34-
import nl.jqno.equalsverifier.Warning;
25+
import java.math.BigDecimal;
26+
import java.math.BigInteger;
27+
import java.util.concurrent.atomic.AtomicInteger;
28+
import java.util.concurrent.atomic.AtomicLong;
29+
30+
import static org.everit.json.schema.JSONMatcher.sameJsonAs;
31+
import static org.everit.json.schema.TestSupport.buildWithLocation;
32+
import static org.everit.json.schema.TestSupport.loadAsV6;
33+
import static org.junit.jupiter.api.Assertions.assertEquals;
34+
import static org.junit.jupiter.api.Assertions.fail;
3535

3636
public class NumberSchemaTest {
3737

@@ -47,7 +47,7 @@ public void exclusiveMinimum() {
4747
}
4848

4949
@Test
50-
public void maximum() {
50+
public void maximumFailure() {
5151
NumberSchema subject = buildWithLocation(NumberSchema.builder().maximum(20.0));
5252
TestSupport.failureOf(subject)
5353
.expectedKeyword("maximum")
@@ -73,6 +73,33 @@ public void minimumFailure() {
7373
.expect();
7474
}
7575

76+
@Test
77+
public void minimumFailureBigDecimal() {
78+
NumberSchema subject = buildWithLocation(NumberSchema.builder().minimum(new BigDecimal("10000000000000000.0")));
79+
TestSupport.failureOf(subject)
80+
.expectedKeyword("minimum")
81+
.input(new BigDecimal("9999999999999999.0"))
82+
.expect();
83+
}
84+
85+
@Test
86+
public void minimumFailureLong() {
87+
NumberSchema subject = buildWithLocation(NumberSchema.builder().minimum(2^62L+1));
88+
TestSupport.failureOf(subject)
89+
.expectedKeyword("minimum")
90+
.input(2^62L)
91+
.expect();
92+
}
93+
94+
@Test
95+
public void minimumFailureBigInt() {
96+
NumberSchema subject = buildWithLocation(NumberSchema.builder().minimum(new BigInteger("2").pow(65).add(BigInteger.ONE)));
97+
TestSupport.failureOf(subject)
98+
.expectedKeyword("minimum")
99+
.input(new BigInteger("2").pow(65))
100+
.expect();
101+
}
102+
76103
@Test
77104
public void exclusiveMinimumLimit() {
78105
TestSupport.failureOf(NumberSchema.builder().exclusiveMinimum(10))

core/src/test/java/org/everit/json/schema/ObjectComparatorTest.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ public class ObjectComparatorTest {
2020

2121
private static List<Arguments> failingCases() {
2222
return Stream.of(
23-
Arguments.of("array, null", EMPTY_ARRAY, null),
24-
Arguments.of("array, object", EMPTY_ARRAY, EMPTY_OBJECT),
25-
Arguments.of("object, null", EMPTY_OBJECT, null),
26-
Arguments.of("arrays with different length", EMPTY_ARRAY, new JSONArray("[null]")),
27-
Arguments.of("arrays with different elems", new JSONArray("[true, false]"), new JSONArray("[false, true]")),
28-
Arguments.of("objects with different length", EMPTY_OBJECT, new JSONObject("{\"a\":true}"))
23+
Arguments.of("array, null", EMPTY_ARRAY, null),
24+
Arguments.of("array, object", EMPTY_ARRAY, EMPTY_OBJECT),
25+
Arguments.of("object, null", EMPTY_OBJECT, null),
26+
Arguments.of("arrays with different length", EMPTY_ARRAY, new JSONArray("[null]")),
27+
Arguments.of("arrays with different elems", new JSONArray("[true, false]"), new JSONArray("[false, true]")),
28+
Arguments.of("objects with different length", EMPTY_OBJECT, new JSONObject("{\"a\":true}")),
29+
Arguments.of("number and not number", EMPTY_OBJECT, 1)
2930
).collect(Collectors.toList());
3031
}
3132

core/src/test/java/org/everit/json/schema/loader/NumberSchemaLoadingTest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import org.json.JSONObject;
77
import org.junit.jupiter.api.Test;
88

9+
import java.math.BigDecimal;
10+
911
import static org.everit.json.schema.TestSupport.loadAsV6;
1012
import static org.junit.jupiter.api.Assertions.assertEquals;
1113

@@ -50,8 +52,8 @@ public void v6Attributes() {
5052
public void v6DoubleLimits() {
5153
NumberSchema expected = NumberSchema.builder()
5254
.requiresNumber(true)
53-
.exclusiveMinimum(5.5)
54-
.exclusiveMaximum(10.1)
55+
.exclusiveMinimum(new BigDecimal("5.5"))
56+
.exclusiveMaximum(new BigDecimal("10.1"))
5557
.build();
5658

5759
NumberSchema actual = (NumberSchema) loadAsV6(get("v6DoubleLimits"));
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"number": {
5+
"type": "number",
6+
"maximum": 9999999999999999
7+
}
8+
}
9+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"number": 10000000000000001
3+
}

0 commit comments

Comments
 (0)