net.sf.saxon.value.BigDecimalValue Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of Saxon-HE Show documentation
Show all versions of Saxon-HE Show documentation
The XSLT and XQuery Processor
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2018-2023 Saxonica Limited
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package net.sf.saxon.value;
import net.sf.saxon.expr.sort.XPathComparable;
import net.sf.saxon.lib.StringCollator;
import net.sf.saxon.str.BMPString;
import net.sf.saxon.str.StringConstants;
import net.sf.saxon.str.UnicodeString;
import net.sf.saxon.trans.Err;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.transpile.CSharpReplaceBody;
import net.sf.saxon.type.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.regex.Pattern;
/**
* An implementation class for decimal values other than integers
* @since 9.8. This class was previously named "DecimalValue". In 9.8 a new DecimalValue
* class is introduced, to more faithfully reflect the XDM type hierarchy, so that every
* instance of xs:decimal is now implemented as an instance of DecimalValue.
*/
public final class BigDecimalValue extends DecimalValue {
public static final int DIVIDE_PRECISION = 18;
private final BigDecimal value;
// cached property holding the equivalent double. (Note: breaks immutability...)
private double doubleValue = Double.NaN; // meaning unknown
public static final BigDecimal BIG_DECIMAL_ONE_MILLION = BigDecimal.valueOf(1_000_000);
public static final BigDecimal BIG_DECIMAL_ONE_BILLION = BigDecimal.valueOf(1_000_000_000);
public static final BigDecimalValue ZERO = new BigDecimalValue(BigDecimal.valueOf(0));
public static final BigDecimalValue ONE = new BigDecimalValue(BigDecimal.valueOf(1));
public static final BigDecimalValue TWO = new BigDecimalValue(BigDecimal.valueOf(2));
public static final BigDecimalValue THREE = new BigDecimalValue(BigDecimal.valueOf(3));
public static final BigDecimal MAX_INT = BigDecimal.valueOf(Integer.MAX_VALUE);
/**
* Constructor supplying a BigDecimal
*
* @param value the value of the DecimalValue
*/
public BigDecimalValue(BigDecimal value) {
super(BuiltInAtomicType.DECIMAL);
this.value = value.stripTrailingZeros();
}
/**
* Constructor supplying a BigDecimal and a type label
*
* @param value the value of the DecimalValue
* @param typeLabel the type label, which must be a subtype of DECIMAL
*/
public BigDecimalValue(BigDecimal value, AtomicType typeLabel) {
super(typeLabel);
this.value = value.stripTrailingZeros();
}
private static final Pattern decimalPattern = Pattern.compile("(\\-|\\+)?((\\.[0-9]+)|([0-9]+(\\.[0-9]*)?))");
/**
* Factory method to construct a DecimalValue from a string
*
* @param in the value of the DecimalValue
* @param validate true if validation is required; false if the caller knows that the value is valid
* @return the required DecimalValue if the input is valid, or a ValidationFailure encapsulating the error
* message if not.
*/
public static ConversionResult makeDecimalValue(String in, boolean validate) {
try {
return parse(in);
} catch (NumberFormatException err) {
ValidationFailure e = new ValidationFailure(
"Cannot convert string " + Err.wrap(in, Err.VALUE) +
" to xs:decimal: " + err.getMessage());
e.setErrorCode("FORG0001");
return e;
}
}
/**
* Factory method to construct a DecimalValue from a string, throwing an unchecked exception
* if the value is invalid
*
* @param in the lexical representation of the DecimalValue
* @return the required DecimalValue
* @throws NumberFormatException if the value is invalid
*/
public static BigDecimalValue parse(CharSequence in) throws NumberFormatException {
StringBuilder digits = new StringBuilder(in.length());
int scale = 0;
int state = 0;
// 0 - in initial whitespace; 1 - after sign
// 3 - after decimal point; 5 - in final whitespace
boolean foundDigit = false;
int len = in.length();
for (int i = 0; i < len; i++) {
char c = in.charAt(i);
switch (c) {
case ' ':
case '\t':
case '\r':
case '\n':
if (state != 0) {
state = 5;
}
break;
case '+':
if (state != 0) {
throw new NumberFormatException("unexpected sign");
}
state = 1;
break;
case '-':
if (state != 0) {
throw new NumberFormatException("unexpected sign");
}
state = 1;
digits.append(c);
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
if (state == 0) {
state = 1;
} else if (state >= 3) {
scale++;
}
if (state == 5) {
throw new NumberFormatException("contains embedded whitespace");
}
digits.append(c);
foundDigit = true;
break;
case '.':
if (state == 5) {
throw new NumberFormatException("contains embedded whitespace");
}
if (state >= 3) {
throw new NumberFormatException("more than one decimal point");
}
state = 3;
break;
default:
throw new NumberFormatException("invalid character '" + c + "'");
}
}
if (!foundDigit) {
throw new NumberFormatException("no digits in value");
}
// remove insignificant trailing zeroes
while (scale > 0) {
if (digits.charAt(digits.length() - 1) == '0') {
digits.setLength(digits.length() - 1);
scale--;
} else {
break;
}
}
if (digits.length() == 0 || (digits.length() == 1 && digits.charAt(0) == '-')) {
return BigDecimalValue.ZERO;
}
BigInteger bigInt = new BigInteger(digits.toString());
BigDecimal bigDec = new BigDecimal(bigInt, scale);
return new BigDecimalValue(bigDec);
}
/**
* Test whether a string is castable to a decimal value
*
* @param in the string to be tested
* @return true if the string has the correct format for a decimal
*/
public static boolean castableAsDecimal(String in) {
String trimmed = Whitespace.trim(in).toString();
return decimalPattern.matcher(trimmed).matches();
}
/**
* Constructor supplying a double
*
* @param in the value of the DecimalValue
* @throws ValidationException if the supplied value cannot be converted, typically because it is INF or NaN.
*/
public BigDecimalValue(double in) throws ValidationException {
super(BuiltInAtomicType.DECIMAL);
try {
BigDecimal d = new BigDecimal(in);
// Note, this gives a different result from BigDecimal.valueOf(in) - it retains more precision.
value = d.stripTrailingZeros();
} catch (Exception err) {
// Must be a special value such as NaN or infinity
ValidationFailure e = new ValidationFailure(
"Cannot convert double " + Err.wrap(in + "", Err.VALUE) + " to decimal");
e.setErrorCode("FOCA0002");
throw e.makeException();
}
}
/**
* Constructor supplying a long integer
*
* @param in the value of the DecimalValue
*/
public BigDecimalValue(long in) {
super(BuiltInAtomicType.DECIMAL);
value = BigDecimal.valueOf(in);
}
/**
* Create a copy of this atomic value, with a different type label
*
* @param typeLabel the type label of the new copy. The caller is responsible for checking that
* the value actually conforms to this type.
*/
@Override
public AtomicValue copyAsSubType(AtomicType typeLabel) {
if (typeLabel.getPrimitiveItemType() == BuiltInAtomicType.INTEGER) {
return IntegerValue.makeIntegerValue(value.toBigInteger()).copyAsSubType(typeLabel);
} else {
return new BigDecimalValue(value, typeLabel);
}
}
/**
* Determine the primitive type of the value. This delivers the same answer as
* getItemType().getPrimitiveItemType(). The primitive types are
* the 19 primitive types of XML Schema, plus xs:integer, xs:dayTimeDuration and xs:yearMonthDuration,
* and xs:untypedAtomic. For external objects, the result is AnyAtomicType.
*/
@Override
public BuiltInAtomicType getPrimitiveType() {
return BuiltInAtomicType.DECIMAL;
}
/**
* Get the numeric value as a double
*
* @return A double representing this numeric value; NaN if it cannot be
* converted
*/
@Override
public double getDoubleValue() {
if (Double.isNaN(doubleValue)) {
doubleValue = value.doubleValue();
}
return doubleValue;
}
/**
* Get the numeric value converted to a float
*
* @return a float representing this numeric value; NaN if it cannot be converted
*/
@Override
public float getFloatValue() {
return (float) value.doubleValue();
}
/**
* Return the numeric value as a Java long.
*
* @return the numeric value as a Java long. This performs truncation
* towards zero.
* @throws net.sf.saxon.trans.XPathException
* if the value cannot be converted
*/
@Override
public long longValue() throws XPathException {
return (long) value.doubleValue();
}
/**
* Get the value
*/
@Override
public BigDecimal getDecimalValue() {
return value;
}
/**
* Get the hashCode. This must conform to the rules for other NumericValue hashcodes
*
* @see NumericValue#hashCode
*/
public int hashCode() {
BigDecimal round = value.setScale(0, RoundingMode.DOWN);
long longVal;
try {
longVal = round.longValue();
} catch (Exception e) {
// This path is for C#, where converting BigDecimal to long gives an OverflowException if out of range
longVal = Long.MAX_VALUE;
}
if (longVal > Integer.MIN_VALUE && longVal < Integer.MAX_VALUE) {
return (int) longVal;
} else {
return Double.valueOf(getDoubleValue()).hashCode();
}
}
@Override
public boolean effectiveBooleanValue() {
return value.signum() != 0;
}
/**
* Get the value of the item as a CharSequence. This is in some cases more efficient than
* the version of the method that returns a String.
*/
// public CharSequence getStringValueCS() {
// return decimalToString(value, new StringBuilder(20));
// }
/**
* Get the canonical lexical representation as defined in XML Schema. This is not always the same
* as the result of casting to a string according to the XPath rules. For xs:decimal, the canonical
* representation always contains a decimal point.
* @return the canonical lexical representation
*/
@Override
public UnicodeString getCanonicalLexicalRepresentation() {
UnicodeString s = this.getUnicodeStringValue().tidy();
if (s.indexOf('.') < 0) {
s = s.concat(StringConstants.POINT_ZERO);
}
return s;
}
/**
* Get the value as a String
*
* @return a String representation of the value
*/
/*@NotNull*/
@Override
public UnicodeString getPrimitiveStringValue() {
return BMPString.of(decimalToString(value, new StringBuilder(16)).toString());
}
/**
* Convert a decimal value to a string, using the XPath rules for formatting
*
* @param value the decimal value to be converted
* @param fsb the StringBuilder to which the value is to be appended
* @return the supplied StringBuilder, suitably populated
*/
@CSharpReplaceBody(code="return fsb.append(value.ToString(\"G\", System.Globalization.CultureInfo.InvariantCulture));")
public static StringBuilder decimalToString(BigDecimal value, StringBuilder fsb) {
// Can't use BigDecimal#toString() under JDK 1.5 because this produces values like "1E-5".
// Can't use BigDecimal#toPlainString() because it retains trailing zeroes to represent the scale
int scale = value.scale();
if (scale == 0) {
fsb.append(value.toString());
return fsb;
} else if (scale < 0) {
String s = value.abs().unscaledValue().toString();
if (s.equals("0")) {
fsb.append('0');
return fsb;
}
//StringBuilder sb = new StringBuilder(s.length() + (-scale) + 2);
if (value.signum() < 0) {
fsb.append('-');
}
fsb.append(s);
for (int i = 0; i < -scale; i++) {
fsb.append('0');
}
return fsb;
} else {
String s = value.abs().unscaledValue().toString();
if (s.equals("0")) {
fsb.append('0');
return fsb;
}
int len = s.length();
//StringBuilder sb = new StringBuilder(len+1);
if (value.signum() < 0) {
fsb.append('-');
}
if (scale >= len) {
fsb.append("0.");
for (int i = len; i < scale; i++) {
fsb.append('0');
}
fsb.append(s);
} else {
fsb.append(s.substring(0, len - scale));
fsb.append('.');
fsb.append(s.substring(len - scale));
}
return fsb;
}
}
/**
* Negate the value
*/
@Override
public NumericValue negate() {
return new BigDecimalValue(value.negate());
}
/**
* Implement the XPath floor() function
*/
@Override
@CSharpReplaceBody(code="return new BigDecimalValue(Singulink.Numerics.BigDecimal.Floor(value));")
public NumericValue floor() {
return new BigDecimalValue(value.setScale(0, RoundingMode.FLOOR));
}
/**
* Implement the XPath ceiling() function
*/
@Override
@CSharpReplaceBody(code = "return new BigDecimalValue(Singulink.Numerics.BigDecimal.Ceiling(value));")
public NumericValue ceiling() {
return new BigDecimalValue(value.setScale(0, RoundingMode.CEILING));
}
/**
* Implement the XPath round() function
*/
@Override
@CSharpReplaceBody(code = "return new BigDecimalValue(Saxon.Impl.Helpers.BigDecimalUtils.Round(value, scale));")
public NumericValue round(int scale) {
// The XPath rules say that we should round to the nearest integer, with .5 rounding towards
// positive infinity. Unfortunately this is not one of the rounding modes that the Java BigDecimal
// class supports, so we need different rules depending on the value.
// If the value is positive, we use ROUND_HALF_UP; if it is negative, we use ROUND_HALF_DOWN (here "UP"
// means "away from zero")
if (scale >= value.scale()) {
// no-op - see bug #4027
return this;
}
switch (value.signum()) {
case -1:
return new BigDecimalValue(value.setScale(scale, RoundingMode.HALF_DOWN));
case 0:
return this;
case +1:
return new BigDecimalValue(value.setScale(scale, RoundingMode.HALF_UP));
default:
// can't happen
return this;
}
}
/**
* Implement the XPath round-half-to-even() function
*/
@Override
@CSharpReplaceBody(code = "return new BigDecimalValue(Singulink.Numerics.BigDecimal.Round(value, scale, Singulink.Numerics.RoundingMode.MidpointToEven));")
public NumericValue roundHalfToEven(int scale) {
if (scale >= value.scale()) {
return this;
}
BigDecimal scaledValue = value.setScale(scale, RoundingMode.HALF_EVEN);
return new BigDecimalValue(scaledValue.stripTrailingZeros());
}
/**
* Determine whether the value is negative, zero, or positive
*
* @return -1 if negative, 0 if zero, +1 if positive, NaN if NaN
*/
@Override
public int signum() {
return value.signum();
}
/**
* Determine whether the value is a whole number, that is, whether it compares
* equal to some integer
*/
@Override
@CSharpReplaceBody(code = "return value.DecimalPlaces == 0;")
public boolean isWholeNumber() {
return value.scale() == 0 ||
value.compareTo(value.setScale(0, RoundingMode.DOWN)) == 0;
}
/**
* Test whether a number is a possible subscript into a sequence, that is,
* a whole number greater than zero and less than 2^31
*
* @return the number as an int if it is a possible subscript, or -1 otherwise
*/
@Override
public int asSubscript() {
if (isWholeNumber() && value.compareTo(BigDecimal.ZERO) > 0 && value.compareTo(MAX_INT) <= 0) {
try {
return (int)longValue();
} catch (XPathException e) {
return -1;
}
} else {
return -1;
}
}
/**
* Get the absolute value as defined by the XPath abs() function
*
* @return the absolute value
* @since 9.2
*/
@Override
public NumericValue abs() {
if (value.signum() > 0) {
return this;
} else {
return new BigDecimalValue(value.negate());
}
}
@Override
public XPathComparable getXPathComparable(StringCollator collator, int implicitTimezone) {
return this;
}
/**
* Compare the value to another numeric value
*/
@Override
public int compareTo(XPathComparable other) {
if (other instanceof NumericValue) {
if (NumericValue.isInteger(((NumericValue)other))) {
// deliberately triggers a ClassCastException if other value is the wrong type
try {
return value.compareTo(((NumericValue)other).getDecimalValue());
} catch (XPathException err) {
throw new AssertionError("Conversion of integer to decimal should never fail");
}
} else if (other instanceof BigDecimalValue) {
return value.compareTo(((BigDecimalValue) other).value);
} else if (other instanceof FloatValue) {
return -other.compareTo(this);
} else {
return super.compareTo(other);
}
} else {
throw new ClassCastException("Cannot compare xs:decimal to " + other.toString());
}
}
/**
* Compare the value to a long
*
* @param other the value to be compared with
* @return -1 if this is less, 0 if this is equal, +1 if this is greater or if this is NaN
*/
@Override
public int compareTo(long other) {
if (other == 0) {
return value.signum();
}
return value.compareTo(BigDecimal.valueOf(other));
}
/**
* Determine whether two atomic values are identical, as determined by XML Schema rules. This is a stronger
* test than equality (even schema-equality); for example two dateTime values are not identical unless
* they are in the same timezone.
* Note that even this check ignores the type annotation of the value. The integer 3 and the short 3
* are considered identical, even though they are not fully interchangeable. "Identical" means the
* same point in the value space, regardless of type annotation.
* NaN is identical to itself.
*
* @param v the other value to be compared with this one
* @return true if the two values are identical, false otherwise.
*/
@Override
public boolean isIdentical(/*@NotNull*/ AtomicValue v) {
return (v instanceof DecimalValue) && equals(v);
}
}