diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/node/NumberNode.java b/smithy-model/src/main/java/software/amazon/smithy/model/node/NumberNode.java index a52349cb58c..dd457251080 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/node/NumberNode.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/node/NumberNode.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. @@ -31,17 +31,45 @@ */ public final class NumberNode extends Node { - private final Number value; + private final BigDecimal value; + private final Number originalValue; private final String stringCache; - private volatile BigDecimal equality; + private boolean isNaN; + private boolean isPositiveInfinity; + private boolean isNegativeInfinity; public NumberNode(Number value, SourceLocation sourceLocation) { super(sourceLocation); - this.value = Objects.requireNonNull(value); + originalValue = value; stringCache = value.toString(); + this.value = toBigDecimal(originalValue); + } + private BigDecimal toBigDecimal(Number value) { if (value instanceof BigDecimal) { - equality = (BigDecimal) value; + return (BigDecimal) value; + } else if (value instanceof Integer || value instanceof Long || value instanceof Short + || value instanceof Byte) { + return BigDecimal.valueOf(value.longValue()); + } else if (value instanceof Float || value instanceof Double) { + double d = value.doubleValue(); + if (Double.isNaN(d)) { + isNaN = true; + return null; + } else if (Double.isInfinite(d)) { + if (stringCache.startsWith("-")) { + isNegativeInfinity = true; + } else { + isPositiveInfinity = true; + } + return null; + } else { + return BigDecimal.valueOf(d); + } + } else if (value instanceof BigInteger) { + return new BigDecimal((BigInteger) value); + } else { + return new BigDecimal(stringCache); } } @@ -51,28 +79,61 @@ public NumberNode(Number value, SourceLocation sourceLocation) { * @return Returns a number. */ public Number getValue() { - return value; + return originalValue; } /** - * Returns true if the node contains a natural number. + * Gets the number value as a BigDecimal if possible. * - * @return Returns true if the node contains a natural number. + *

NaN and infinite numbers will return an empty Optional. + * + * @return Returns the BigDecimal value of the wrapped number. */ + public Optional asBigDecimal() { + return Optional.ofNullable(value); + } + + @Deprecated public boolean isNaturalNumber() { return !isFloatingPointNumber(); } + /** + * Check the value is negative, including negative infinity. + * + *

Any number >= 0, +Infinity, and NaN return false. + * + * @return Return true if negative. + */ + public boolean isNegative() { + return isNegativeInfinity || (value != null && value.compareTo(BigDecimal.ZERO) < 0); + } + /** * Returns true if the node contains a floating point number. * * @return Returns true if the node contains a floating point number. */ public boolean isFloatingPointNumber() { - return value instanceof Float - || value instanceof Double - || value instanceof BigDecimal - || stringCache.contains("."); + return value == null || value.scale() > 0 || toString().contains("."); + } + + /** + * Returns true if the number is a floating point NaN. + * + * @return Return true if NaN. + */ + public boolean isNaN() { + return isNaN; + } + + /** + * Returns true if the number is infinite. + * + * @return Return true if infinite. + */ + public boolean isInfinite() { + return isPositiveInfinity || isNegativeInfinity; } @Override @@ -117,19 +178,7 @@ public Optional asNumberNode() { * @return Returns true if set to zero. */ public boolean isZero() { - // Do a cheap test based on the serialized value of the number first. - // This test covers byte, short, integer, and long. - if (toString().equals("0") || toString().equals("0.0")) { - return true; - } else if (value instanceof BigDecimal) { - return value.equals(BigDecimal.ZERO); - } else if (value instanceof BigInteger) { - return value.equals(BigInteger.ZERO); - } else if (value instanceof Float) { - return value.floatValue() == 0f; - } else { - return value.doubleValue() == 0d; - } + return value != null && value.compareTo(BigDecimal.ZERO) == 0; } @Override @@ -139,42 +188,12 @@ public boolean equals(Object other) { } else if (other == this) { return true; } else { - NumberNode otherNode = (NumberNode) other; - - // This only works if both values are the same type. - if (value.equals(otherNode.value)) { - return true; - } - - // Attempt a cheap check based on the string cache. - if (stringCache.equals(otherNode.stringCache)) { - return true; - } - - // Convert both to BigDecimal and compare equality. - return getEquality().equals(otherNode.getEquality()); - } - } - - private BigDecimal getEquality() { - BigDecimal e = equality; - - if (e == null) { - if (value instanceof Integer) { - e = BigDecimal.valueOf(value.intValue()); - } else if (value instanceof Short) { - e = BigDecimal.valueOf(value.shortValue()); - } else if (value instanceof Byte) { - e = BigDecimal.valueOf(value.byteValue()); - } else if (value instanceof Long) { - e = BigDecimal.valueOf(value.longValue()); - } else { - e = new BigDecimal(stringCache); - } - equality = e; + NumberNode o = (NumberNode) other; + return isNaN == o.isNaN + && isPositiveInfinity == o.isPositiveInfinity + && isNegativeInfinity == o.isNegativeInfinity + && Objects.equals(value, o.value); } - - return e; } @Override diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/NodeValidationVisitor.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/NodeValidationVisitor.java index 330b377f7c2..a3e74ebd245 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/NodeValidationVisitor.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/NodeValidationVisitor.java @@ -218,7 +218,7 @@ public List bigIntegerShape(BigIntegerShape shape) { private List validateNaturalNumber(Shape shape, Long min, Long max) { return value.asNumberNode() .map(number -> { - if (!number.isNaturalNumber()) { + if (number.isFloatingPointNumber()) { return ListUtils.of(event(String.format( "%s shapes must not have floating point values, but found `%s` provided for `%s`", shape.getType(), number.getValue(), shape.getId()))); diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/node/NumberNodeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/node/NumberNodeTest.java index 910860574d3..b146cc1a9d3 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/node/NumberNodeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/node/NumberNodeTest.java @@ -17,6 +17,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -199,6 +200,11 @@ public float floatValue() { public double doubleValue() { return 0; } + + @Override + public String toString() { + return "0.000"; + } }, true); cases.put(new Number() { @Override @@ -283,4 +289,14 @@ public void compareDoubleAndFloat() { assertThat(left, equalTo(right)); } + + @Test + public void detectsNegativeValues() { + assertThat(NumberNode.from(Double.NEGATIVE_INFINITY).isNegative(), is(true)); + assertThat(NumberNode.from(Double.POSITIVE_INFINITY).isNegative(), is(false)); + assertThat(NumberNode.from(Double.NaN).isNegative(), is(false)); + assertThat(NumberNode.from(0).isNegative(), is(false)); + assertThat(NumberNode.from(1).isNegative(), is(false)); + assertThat(NumberNode.from(-1).isNegative(), is(true)); + } }