java-json-tools / jackson-coreutils

JSON Pointer (RFC 6901) and numeric JSON equivalence for Jackson (2.2.x)
Other
5 stars 6 forks source link

JsonNumEquals fails to compare IntNode and LongNode with same value #45

Open grimsa opened 4 years ago

grimsa commented 4 years ago

Problem

JsonNode intNode = IntNode.valueOf(1);
JsonNode longNode = LongNode.valueOf(1);
boolean same = JsonNumEquals.getInstance().equivalent(intNode, longNode);
// at this point same == false

Cause In com.github.fge.jackson.JsonNumEquals#numEquals:

private static boolean numEquals(final JsonNode a, final JsonNode b)
{
    /*
     * If both numbers are integers, delegate to JsonNode.
     */
    if (a.isIntegralNumber() && b.isIntegralNumber())     // this is true
        return a.equals(b);           // yet this is false

    /*
     * Otherwise, compare decimal values.
     */
    return a.decimalValue().compareTo(b.decimalValue()) == 0;
}
grimsa commented 3 years ago

Workaround

Since Jackson 2.6.0 there is JsonNode#equals(Comparator<JsonNode>, JsonNode) method [1].

The numeric equality comparator can be implemented like this:

class NumericEqualityComparator implements Comparator<JsonNode> {
    NumericEqualityComparator() {
    }

    @Override
    public int compare(JsonNode a, JsonNode b) {
        return areNodesNumericallyEqual(a, b)
                ? 0
                : 1;
    }

    private boolean areNodesNumericallyEqual(JsonNode a, JsonNode b) {
        return a.isNumber() && b.isNumber()
                ? areNumberNodesNumericallyEqual(a, b)
                : a.equals(b);
    }

    private boolean areNumberNodesNumericallyEqual(JsonNode a, JsonNode b) {
        if (a.isIntegralNumber() && b.isIntegralNumber()) {
            return a.canConvertToLong() && b.canConvertToLong()
                    ? a.longValue() == b.longValue()                        // Comparing as long to avoid BigInteger allocations
                    : a.bigIntegerValue().equals(b.bigIntegerValue());
        }
        return a.isBigDecimal() || b.isBigDecimal()
                ? a.decimalValue().compareTo(b.decimalValue()) == 0
                : Double.compare(a.doubleValue(), b.doubleValue()) == 0;    // Comparing as double to avoid BigDecimal allocations
    }
}

And then JsonNode comparison is performed like this node.equals(new NumericEqualityComparator(), otherNode)

[1] https://github.com/FasterXML/jackson-databind/issues/790