Skip to content

Commit cb231f5

Browse files
committed
Some tweaking of #2855, change name to canConvertToExactIntegral(), add more testing
1 parent 891b186 commit cb231f5

15 files changed

+189
-44
lines changed

release-notes/CREDITS-2.x

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,6 +1226,16 @@ Swayam Raina (swayamraina@github)
12261226
* Contributed #2761: Support multiple names in `JsonSubType.Type`
12271227
(2.12.0)
12281228
1229+
Oguzhan Unlu (oguzhanunlu@github)
1230+
* Requested #2855: Add `JsonNode.canConvertToExactIntegral()` to indicate whether
1231+
floating-point/BigDecimal values could be converted to integers losslessly
1232+
(2.12.0)
1233+
1234+
Siavash Soleymani (siavashsoleymani@github)
1235+
* Contributed implementation for #2855:Add `JsonNode.canConvertToExactIntegral()` to
1236+
indicate whether floating-point/BigDecimal values could be converted to integers losslessly
1237+
(2.12.0)
1238+
12291239
Ilya Golovin (ilgo0413@github)
12301240
* Contributed #2873: `MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS` should work for enum as keys
12311241
(2.12.0)

release-notes/VERSION-2.x

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ Project: jackson-databind
1818
#2804: Throw `InvalidFormatException` instead of `MismatchedInputException`
1919
for ACCEPT_FLOAT_AS_INT coercion failures
2020
(requested by mjustin@github)
21+
#2855: Add `JsonNode.canConvertToExactIntegral()` to indicate whether floating-point/BigDecimal
22+
values could be converted to integers losslessly
23+
(requested by Oguzhan U; implementation contributed by Siavash S)
2124

2225
2.12.0-rc1 (12-Oct-2020)
2326

src/main/java/com/fasterxml/jackson/databind/JsonNode.java

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -316,14 +316,6 @@ public final boolean isNumber() {
316316
*/
317317
public boolean isInt() { return false; }
318318

319-
/**
320-
* Method that can be used to check whether contained value
321-
* has a fractional part or not.
322-
*
323-
* @return True if the value has fractional part
324-
*/
325-
public boolean hasFractionalPart() { return false; }
326-
327319
/**
328320
* Method that can be used to check whether contained value
329321
* is a number represented as Java <code>long</code>.
@@ -413,6 +405,29 @@ public final boolean isBinary() {
413405
*/
414406
public boolean canConvertToLong() { return false; }
415407

408+
/**
409+
* Method that can be used to check whether contained value
410+
* is numeric (returns true for {@link #isNumber()}) and
411+
* can be losslessly converted to integral number (specifically,
412+
* {@link BigInteger} but potentially others, see
413+
* {@link #canConvertToInt} and {@link #canConvertToInt}).
414+
* Latter part allows floating-point numbers
415+
* (for which {@link #isFloatingPointNumber()} returns {@code true})
416+
* that do not have fractional part.
417+
* Note that "not-a-number" values of {@code double} and {@code float}
418+
* will return {@code false} as they can not be converted to matching
419+
* integral representations.
420+
*
421+
* @return True if the value is an actual number with no fractional
422+
* part; false for non-numeric types, NaN representations of floating-point
423+
* numbers, and floating-point numbers with fractional part.
424+
*
425+
* @since 2.12
426+
*/
427+
public boolean canConvertToExactIntegral() {
428+
return isIntegralNumber();
429+
}
430+
416431
/*
417432
/**********************************************************
418433
/* Public API, straight value access

src/main/java/com/fasterxml/jackson/databind/node/DecimalNode.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,20 @@ public class DecimalNode
5757
@Override
5858
public boolean isBigDecimal() { return true; }
5959

60-
@Override
61-
public boolean hasFractionalPart() {
62-
return !(_value.signum() == 0 || _value.scale() <= 0 || _value.stripTrailingZeros().scale() <= 0);
63-
}
64-
6560
@Override public boolean canConvertToInt() {
6661
return (_value.compareTo(MIN_INTEGER) >= 0) && (_value.compareTo(MAX_INTEGER) <= 0);
6762
}
6863
@Override public boolean canConvertToLong() {
6964
return (_value.compareTo(MIN_LONG) >= 0) && (_value.compareTo(MAX_LONG) <= 0);
7065
}
7166

67+
@Override // since 2.12
68+
public boolean canConvertToExactIntegral() {
69+
return (_value.signum() == 0)
70+
|| (_value.scale() <= 0)
71+
|| (_value.stripTrailingZeros().scale() <= 0);
72+
}
73+
7274
@Override
7375
public Number numberValue() { return _value; }
7476

src/main/java/com/fasterxml/jackson/databind/node/DoubleNode.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,19 @@ public class DoubleNode
5252
@Override
5353
public boolean isDouble() { return true; }
5454

55-
@Override
56-
public boolean hasFractionalPart() {
57-
return !Double.isNaN(_value) && !Double.isInfinite(_value) && _value % 1 != 0.0;
58-
}
59-
6055
@Override public boolean canConvertToInt() {
6156
return (_value >= Integer.MIN_VALUE && _value <= Integer.MAX_VALUE);
6257
}
6358
@Override public boolean canConvertToLong() {
6459
return (_value >= Long.MIN_VALUE && _value <= Long.MAX_VALUE);
6560
}
66-
61+
62+
@Override // since 2.12
63+
public boolean canConvertToExactIntegral() {
64+
return !Double.isNaN(_value) && !Double.isInfinite(_value)
65+
&& (_value == Math.rint(_value));
66+
}
67+
6768
@Override
6869
public Number numberValue() {
6970
return Double.valueOf(_value);

src/main/java/com/fasterxml/jackson/databind/node/FloatNode.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,20 @@ public class FloatNode extends NumericNode
5353
@Override
5454
public boolean isFloat() { return true; }
5555

56-
@Override
57-
public boolean hasFractionalPart() {
58-
return !Float.isNaN(_value) && !Float.isInfinite(_value) && _value % 1 != 0.0;
59-
}
60-
6156
@Override public boolean canConvertToInt() {
6257
return (_value >= Integer.MIN_VALUE && _value <= Integer.MAX_VALUE);
6358
}
6459

6560
@Override public boolean canConvertToLong() {
6661
return (_value >= Long.MIN_VALUE && _value <= Long.MAX_VALUE);
6762
}
68-
63+
64+
@Override // since 2.12
65+
public boolean canConvertToExactIntegral() {
66+
return !Float.isNaN(_value) && !Float.isInfinite(_value)
67+
&& (_value == Math.round(_value));
68+
}
69+
6970
@Override
7071
public Number numberValue() {
7172
return Float.valueOf(_value);
@@ -102,6 +103,7 @@ public String asText() {
102103
// @since 2.9
103104
@Override
104105
public boolean isNaN() {
106+
// Java 8 will have `Float.isFinite()` to combine both checks
105107
return Float.isNaN(_value) || Float.isInfinite(_value);
106108
}
107109

src/test/java/com/fasterxml/jackson/databind/node/ArrayNodeTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ public class ArrayNodeTest
2020
public void testDirectCreation() throws IOException
2121
{
2222
ArrayNode n = new ArrayNode(JsonNodeFactory.instance);
23+
24+
assertFalse(n.isBoolean());
25+
assertFalse(n.isTextual());
26+
assertFalse(n.isNumber());
27+
assertFalse(n.canConvertToInt());
28+
assertFalse(n.canConvertToLong());
29+
assertFalse(n.canConvertToExactIntegral());
30+
2331
assertStandardEquals(n);
2432
assertFalse(n.elements().hasNext());
2533
assertFalse(n.fieldNames().hasNext());

src/test/java/com/fasterxml/jackson/databind/node/NodeTestBase.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,21 @@
77
abstract class NodeTestBase extends BaseMapTest
88
{
99
protected void assertNodeNumbersForNonNumeric(JsonNode n)
10-
{
10+
{
1111
assertFalse(n.isNumber());
12+
assertFalse(n.canConvertToInt());
13+
assertFalse(n.canConvertToLong());
14+
assertFalse(n.canConvertToExactIntegral());
15+
1216
assertEquals(0, n.asInt());
1317
assertEquals(-42, n.asInt(-42));
1418
assertEquals(0, n.asLong());
1519
assertEquals(12345678901L, n.asLong(12345678901L));
1620
assertEquals(0.0, n.asDouble());
1721
assertEquals(-19.25, n.asDouble(-19.25));
1822
}
19-
23+
24+
// Test to check conversions, coercions
2025
protected void assertNodeNumbers(JsonNode n, int expInt, double expDouble)
2126
{
2227
assertEquals(expInt, n.asInt());

src/test/java/com/fasterxml/jackson/databind/node/NotANumberConversionTest.java

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,33 @@
66

77
public class NotANumberConversionTest extends BaseMapTest
88
{
9-
private final ObjectMapper m = new ObjectMapper();
10-
{
11-
m.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
12-
}
9+
private final ObjectMapper MAPPER = jsonMapperBuilder()
10+
.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
11+
.build();
1312

1413
public void testBigDecimalWithNaN() throws Exception
1514
{
16-
JsonNode tree = m.valueToTree(new DoubleWrapper(Double.NaN));
15+
JsonNode tree = MAPPER.valueToTree(new DoubleWrapper(Double.NaN));
1716
assertNotNull(tree);
18-
String json = m.writeValueAsString(tree);
17+
String json = MAPPER.writeValueAsString(tree);
1918
assertNotNull(json);
2019

21-
tree = m.valueToTree(new DoubleWrapper(Double.NEGATIVE_INFINITY));
20+
tree = MAPPER.valueToTree(new DoubleWrapper(Double.NEGATIVE_INFINITY));
2221
assertNotNull(tree);
23-
json = m.writeValueAsString(tree);
22+
json = MAPPER.writeValueAsString(tree);
2423
assertNotNull(json);
2524

26-
tree = m.valueToTree(new DoubleWrapper(Double.POSITIVE_INFINITY));
25+
tree = MAPPER.valueToTree(new DoubleWrapper(Double.POSITIVE_INFINITY));
2726
assertNotNull(tree);
28-
json = m.writeValueAsString(tree);
27+
json = MAPPER.writeValueAsString(tree);
2928
assertNotNull(json);
3029
}
3130

3231
// for [databind#1315]: no accidental coercion to DoubleNode
3332
public void testBigDecimalWithoutNaN() throws Exception
3433
{
3534
BigDecimal input = new BigDecimal(Double.MIN_VALUE).divide(new BigDecimal(10L));
36-
JsonNode tree = m.readTree(input.toString());
35+
JsonNode tree = MAPPER.readTree(input.toString());
3736
assertTrue(tree.isBigDecimal());
3837
BigDecimal output = tree.decimalValue();
3938
assertEquals(input, output);

src/test/java/com/fasterxml/jackson/databind/node/NumberNodesTest.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,15 @@ public void testIntViaMapper() throws Exception
5050
assertType(result, IntNode.class);
5151
assertFalse(result.isLong());
5252
assertFalse(result.isFloatingPointNumber());
53-
assertFalse(result.hasFractionalPart());
5453
assertFalse(result.isDouble());
5554
assertFalse(result.isNull());
5655
assertFalse(result.isTextual());
5756
assertFalse(result.isMissingNode());
5857

58+
assertTrue(result.canConvertToInt());
59+
assertTrue(result.canConvertToLong());
60+
assertTrue(result.canConvertToExactIntegral());
61+
5962
assertEquals(value, result.numberValue().intValue());
6063
assertEquals(value, result.intValue());
6164
assertEquals(String.valueOf(value), result.asText());
@@ -123,7 +126,7 @@ public void testLong()
123126

124127
public void testLongViaMapper() throws Exception
125128
{
126-
// need to use something being 32-bit value space
129+
// need to use something beyond 32-bit value space
127130
long value = 12345678L << 32;
128131
JsonNode result = MAPPER.readTree(String.valueOf(value));
129132
assertTrue(result.isNumber());
@@ -132,7 +135,6 @@ public void testLongViaMapper() throws Exception
132135
assertType(result, LongNode.class);
133136
assertFalse(result.isInt());
134137
assertFalse(result.isFloatingPointNumber());
135-
assertFalse(result.hasFractionalPart());
136138
assertFalse(result.isDouble());
137139
assertFalse(result.isNull());
138140
assertFalse(result.isTextual());
@@ -143,6 +145,10 @@ public void testLongViaMapper() throws Exception
143145
assertEquals(String.valueOf(value), result.asText());
144146
assertEquals((double) value, result.doubleValue());
145147

148+
assertFalse(result.canConvertToInt());
149+
assertTrue(result.canConvertToLong());
150+
assertTrue(result.canConvertToExactIntegral());
151+
146152
// also, equality should work ok
147153
assertEquals(result, LongNode.valueOf(value));
148154
}
@@ -187,7 +193,8 @@ public void testDoubleViaMapper() throws Exception
187193
assertFalse(result.isNull());
188194
assertType(result, DoubleNode.class);
189195
assertTrue(result.isFloatingPointNumber());
190-
assertTrue(result.hasFractionalPart());
196+
assertFalse(result.isIntegralNumber());
197+
assertFalse(result.canConvertToExactIntegral());
191198
assertTrue(result.isDouble());
192199
assertFalse(result.isInt());
193200
assertFalse(result.isLong());
@@ -215,7 +222,8 @@ public void testFloat()
215222
assertEquals(JsonParser.NumberType.FLOAT, n.numberType());
216223
assertEquals(0, n.intValue());
217224
assertTrue(n.isFloatingPointNumber());
218-
assertTrue(n.hasFractionalPart());
225+
assertFalse(n.isIntegralNumber());
226+
assertFalse(n.canConvertToExactIntegral());
219227

220228
// NOTE: conversion to double NOT as simple as with exact numbers like 0.25:
221229
assertEquals(0.45f, n.floatValue());
@@ -286,13 +294,16 @@ public void testDecimalNode() throws Exception
286294
assertType(result, DecimalNode.class);
287295
assertFalse(result.isInt());
288296
assertTrue(result.isFloatingPointNumber());
289-
assertTrue(result.hasFractionalPart());
290297
assertTrue(result.isBigDecimal());
291298
assertFalse(result.isDouble());
292299
assertFalse(result.isNull());
293300
assertFalse(result.isTextual());
294301
assertFalse(result.isMissingNode());
295302

303+
assertFalse(result.canConvertToExactIntegral());
304+
assertTrue(result.canConvertToInt());
305+
assertTrue(result.canConvertToLong());
306+
296307
assertEquals(value, result.numberValue());
297308
assertEquals(value.toString(), result.asText());
298309

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.fasterxml.jackson.databind.node;
2+
3+
import java.math.BigDecimal;
4+
5+
import com.fasterxml.jackson.databind.*;
6+
7+
// for [databind#2885]
8+
public class NumberToIntegralConversionTest extends BaseMapTest
9+
{
10+
private final ObjectMapper MAPPER = sharedMapper();
11+
private final JsonNodeFactory NODES = MAPPER.getNodeFactory();
12+
13+
public void testFloatToIntegrals() throws Exception
14+
{
15+
assertTrue(NODES.numberNode(0f).canConvertToExactIntegral());
16+
assertTrue(NODES.numberNode((float) 0).canConvertToExactIntegral());
17+
assertTrue(NODES.numberNode(0.000f).canConvertToExactIntegral());
18+
19+
assertFalse(NODES.numberNode(0.001f).canConvertToExactIntegral());
20+
assertFalse(NODES.numberNode(0.25f).canConvertToExactIntegral());
21+
}
22+
23+
public void testDoubleToIntegrals() throws Exception
24+
{
25+
assertTrue(NODES.numberNode(0d).canConvertToExactIntegral());
26+
assertTrue(NODES.numberNode((double) 0).canConvertToExactIntegral());
27+
assertTrue(NODES.numberNode(0.000d).canConvertToExactIntegral());
28+
assertTrue(NODES.numberNode((double) Integer.MAX_VALUE).canConvertToExactIntegral());
29+
assertTrue(NODES.numberNode((double) Integer.MIN_VALUE).canConvertToExactIntegral());
30+
31+
assertFalse(NODES.numberNode(0.001d).canConvertToExactIntegral());
32+
assertFalse(NODES.numberNode(0.25d).canConvertToExactIntegral());
33+
34+
assertFalse(NODES.numberNode(12000000.5d).canConvertToExactIntegral());
35+
}
36+
37+
public void testNaNsToIntegrals() throws Exception
38+
{
39+
assertFalse(NODES.numberNode(Float.NaN).canConvertToExactIntegral());
40+
assertFalse(NODES.numberNode(Float.NEGATIVE_INFINITY).canConvertToExactIntegral());
41+
assertFalse(NODES.numberNode(Float.POSITIVE_INFINITY).canConvertToExactIntegral());
42+
43+
assertFalse(NODES.numberNode(Double.NaN).canConvertToExactIntegral());
44+
assertFalse(NODES.numberNode(Double.NEGATIVE_INFINITY).canConvertToExactIntegral());
45+
assertFalse(NODES.numberNode(Double.POSITIVE_INFINITY).canConvertToExactIntegral());
46+
}
47+
48+
public void testBigDecimalToIntegrals() throws Exception
49+
{
50+
assertTrue(NODES.numberNode(BigDecimal.ZERO).canConvertToExactIntegral());
51+
assertTrue(NODES.numberNode(BigDecimal.TEN).canConvertToExactIntegral());
52+
assertTrue(NODES.numberNode(BigDecimal.valueOf(Integer.MAX_VALUE)).canConvertToExactIntegral());
53+
assertTrue(NODES.numberNode(BigDecimal.valueOf(Integer.MIN_VALUE)).canConvertToExactIntegral());
54+
assertTrue(NODES.numberNode(BigDecimal.valueOf(Long.MAX_VALUE)).canConvertToExactIntegral());
55+
assertTrue(NODES.numberNode(BigDecimal.valueOf(Long.MIN_VALUE)).canConvertToExactIntegral());
56+
57+
assertFalse(NODES.numberNode(BigDecimal.valueOf(0.001)).canConvertToExactIntegral());
58+
assertFalse(NODES.numberNode(BigDecimal.valueOf(0.25)).canConvertToExactIntegral());
59+
60+
assertFalse(NODES.numberNode(BigDecimal.valueOf(12000000.5)).canConvertToExactIntegral());
61+
}
62+
}

0 commit comments

Comments
 (0)