001: package net.sf.saxon.value;
002:
003: import net.sf.saxon.Err;
004: import net.sf.saxon.expr.Token;
005: import net.sf.saxon.expr.XPathContext;
006: import net.sf.saxon.om.FastStringBuffer;
007: import net.sf.saxon.trans.DynamicError;
008: import net.sf.saxon.trans.XPathException;
009: import net.sf.saxon.type.*;
010:
011: import java.math.BigDecimal;
012: import java.math.BigInteger;
013: import java.util.regex.Pattern;
014:
015: /**
016: * A decimal value
017: */
018:
019: public final class DecimalValue extends NumericValue {
020:
021: private static final int DIVIDE_PRECISION = 18;
022:
023: private BigDecimal value;
024:
025: public static final BigDecimal ONE = BigDecimal.valueOf(1);
026: public static final BigInteger TEN = BigInteger.valueOf(10);
027: public static final BigDecimal ONE_MILLION = BigDecimal
028: .valueOf(1000000);
029:
030: /**
031: * Constructor supplying a BigDecimal
032: * @param value the value of the DecimalValue
033: */
034:
035: public DecimalValue(BigDecimal value) {
036: this .value = value;
037: loseTrailingZeros();
038: }
039:
040: private static final Pattern decimalPattern = Pattern
041: .compile("(\\-|\\+)?((\\.[0-9]+)|([0-9]+(\\.[0-9]*)?))");
042:
043: /**
044: * Factory method to construct a DecimalValue from a string
045: * @param in the value of the DecimalValue
046: * @param validate true if validation is required; false if the caller knows that the value is valid
047: * @return the required DecimalValue if the input is valid, or an ErrorValue encapsulating the error
048: * message if not.
049: */
050:
051: public static AtomicValue makeDecimalValue(CharSequence in,
052: boolean validate) {
053: String trimmed = trimWhitespace(in).toString();
054: try {
055: if (validate) {
056: if (!decimalPattern.matcher(trimmed).matches()) {
057: ValidationException err = new ValidationException(
058: "Cannot convert string "
059: + Err.wrap(trimmed, Err.VALUE)
060: + " to xs:decimal");
061: err.setErrorCode("FORG0001");
062: return new ValidationErrorValue(err);
063: }
064: }
065: BigDecimal val = new BigDecimal(trimmed);
066: DecimalValue value = new DecimalValue(val);
067: if (trimmed.charAt(trimmed.length() - 1) == '0') {
068: value.loseTrailingZeros();
069: }
070: return value;
071: } catch (NumberFormatException err) {
072: ValidationException e = new ValidationException(
073: "Cannot convert string "
074: + Err.wrap(trimmed, Err.VALUE)
075: + " to xs:decimal");
076: e.setErrorCode("FORG0001");
077: return new ValidationErrorValue(e);
078: }
079: }
080:
081: /**
082: * Constructor supplying a double
083: * @param in the value of the DecimalValue
084: */
085:
086: public DecimalValue(double in) throws ValidationException {
087: try {
088: this .value = new BigDecimal(in);
089: loseTrailingZeros();
090: } catch (NumberFormatException err) {
091: // Must be a special value such as NaN or infinity
092: ValidationException e = new ValidationException(
093: "Cannot convert double "
094: + Err.wrap(in + "", Err.VALUE)
095: + " to decimal");
096: e.setErrorCode("FORG0001");
097: throw e;
098: }
099: }
100:
101: /**
102: * Constructor supplying a long integer
103: * @param in the value of the DecimalValue
104: */
105:
106: public DecimalValue(long in) {
107: this .value = BigDecimal.valueOf(in);
108: }
109:
110: /**
111: * Remove insignificant trailing zeros (the Java BigDecimal class retains trailing zeros,
112: * but the XPath 2.0 xs:decimal type does not)
113: */
114:
115: private void loseTrailingZeros() {
116: int scale = value.scale();
117: if (scale > 0) {
118: BigInteger i = value.unscaledValue();
119: while (true) {
120: BigInteger[] dr = i.divideAndRemainder(TEN);
121: if (dr[1].equals(BigInteger.ZERO)) {
122: i = dr[0];
123: scale--;
124: if (scale == 0) {
125: break;
126: }
127: } else {
128: break;
129: }
130: }
131: if (scale != value.scale()) {
132: value = new BigDecimal(i, scale);
133: }
134: }
135: }
136:
137: /**
138: * Get the value
139: */
140:
141: public BigDecimal getValue() {
142: return value;
143: }
144:
145: /**
146: * Get the hashCode. This must conform to the rules for other NumericValue hashcodes
147: * @see NumericValue#hashCode
148: */
149:
150: public int hashCode() {
151: BigDecimal round = value.setScale(0, BigDecimal.ROUND_DOWN);
152: long value = round.longValue();
153: if (value > Integer.MIN_VALUE && value < Integer.MAX_VALUE) {
154: return (int) value;
155: } else {
156: return new Double(this .getDoubleValue()).hashCode();
157: }
158: }
159:
160: public boolean effectiveBooleanValue(XPathContext context) {
161: return value.signum() != 0;
162: }
163:
164: /**
165: * Convert to target data type
166: */
167:
168: public AtomicValue convertPrimitive(BuiltInAtomicType requiredType,
169: boolean validate, XPathContext context) {
170: switch (requiredType.getPrimitiveType()) {
171: case Type.BOOLEAN:
172: // 0.0 => false, anything else => true
173: return BooleanValue.get(value.signum() != 0);
174: case Type.NUMBER:
175: case Type.DECIMAL:
176: case Type.ANY_ATOMIC:
177: case Type.ITEM:
178: return this ;
179: case Type.INTEGER:
180: return BigIntegerValue.makeValue(value.toBigInteger());
181: case Type.DOUBLE:
182: return new DoubleValue(value.doubleValue());
183: case Type.FLOAT:
184: return new FloatValue(value.floatValue());
185: case Type.STRING:
186: return new StringValue(getStringValueCS());
187: case Type.UNTYPED_ATOMIC:
188: return new UntypedAtomicValue(getStringValueCS());
189: default:
190: ValidationException err = new ValidationException(
191: "Cannot convert decimal to "
192: + requiredType.getDisplayName());
193: err.setErrorCode("XPTY0004");
194: err.setIsTypeError(true);
195: return new ValidationErrorValue(err);
196: }
197: }
198:
199: /**
200: * Get the value as a String
201: * @return a String representation of the value
202: */
203:
204: public String getStringValue() {
205: return decimalToString(value);
206: }
207:
208: public static String decimalToString(BigDecimal value) {
209: // Can't use the plain BigDecimal#toString() under JDK 1.5 because this produces values like "1E-5".
210: // JDK 1.5 offers BigDecimal#toPlainString() which might do the job directly
211: if (value.scale() <= 0) {
212: return value.toString();
213: } else {
214: boolean negative = value.signum() < 0;
215: String s = value.abs().unscaledValue().toString();
216: int len = s.length();
217: int scale = value.scale();
218: FastStringBuffer sb = new FastStringBuffer(len + 1);
219: if (negative) {
220: sb.append('-');
221: }
222: if (scale >= len) {
223: sb.append("0.");
224: for (int i = len; i < scale; i++) {
225: sb.append('0');
226: }
227: sb.append(s);
228: } else {
229: sb.append(s.substring(0, len - scale));
230: sb.append('.');
231: sb.append(s.substring(len - scale));
232: }
233: return sb.toString();
234: }
235: }
236:
237: /**
238: * Determine the data type of the expression
239: * @return Type.DECIMAL
240: * @param th
241: */
242:
243: public ItemType getItemType(TypeHierarchy th) {
244: return Type.DECIMAL_TYPE;
245: }
246:
247: /**
248: * Negate the value
249: */
250:
251: public NumericValue negate() {
252: return new DecimalValue(value.negate());
253: }
254:
255: /**
256: * Implement the XPath floor() function
257: */
258:
259: public NumericValue floor() {
260: return new DecimalValue(value.setScale(0,
261: BigDecimal.ROUND_FLOOR));
262: }
263:
264: /**
265: * Implement the XPath ceiling() function
266: */
267:
268: public NumericValue ceiling() {
269: return new DecimalValue(value.setScale(0,
270: BigDecimal.ROUND_CEILING));
271: }
272:
273: /**
274: * Implement the XPath round() function
275: */
276:
277: public NumericValue round() {
278: // The XPath rules say that we should round to the nearest integer, with .5 rounding towards
279: // positive infinity. Unfortunately this is not one of the rounding modes that the Java BigDecimal
280: // class supports.
281:
282: // If the value is positive, we use ROUND_HALF_UP; if it is negative, we use ROUND_HALF_DOWN (here "UP"
283: // means "away from zero")
284:
285: switch (value.signum()) {
286: case -1:
287: return new DecimalValue(value.setScale(0,
288: BigDecimal.ROUND_HALF_DOWN));
289: case 0:
290: return this ;
291: case +1:
292: return new DecimalValue(value.setScale(0,
293: BigDecimal.ROUND_HALF_UP));
294: default:
295: // can't happen
296: return this ;
297: }
298:
299: }
300:
301: /**
302: * Implement the XPath round-to-half-even() function
303: */
304:
305: public NumericValue roundToHalfEven(int scale) {
306: if (scale < 0) {
307: try {
308: AtomicValue val = convert(Type.INTEGER, null);
309: if (val instanceof IntegerValue) {
310: return ((IntegerValue) val).roundToHalfEven(scale);
311: } else {
312: return ((BigIntegerValue) val)
313: .roundToHalfEven(scale);
314: }
315: } catch (XPathException err) {
316: throw new IllegalArgumentException(
317: "internal error in integer-decimal conversion");
318: }
319: } else {
320: return new DecimalValue(value.setScale(scale,
321: BigDecimal.ROUND_HALF_EVEN));
322: }
323: }
324:
325: /**
326: * Determine whether the value is negative, zero, or positive
327: * @return -1 if negative, 0 if zero, +1 if positive, NaN if NaN
328: */
329:
330: public double signum() {
331: return value.signum();
332: }
333:
334: /**
335: * Determine whether the value is a whole number, that is, whether it compares
336: * equal to some integer
337: */
338:
339: public boolean isWholeNumber() {
340: return value.scale() == 0
341: || value.equals(value
342: .setScale(0, BigDecimal.ROUND_DOWN));
343: }
344:
345: /**
346: * Evaluate a binary arithmetic operator.
347: */
348:
349: public NumericValue arithmetic(int operator, NumericValue other,
350: XPathContext context) throws XPathException {
351: if (other instanceof DecimalValue) {
352: try {
353: switch (operator) {
354: case Token.PLUS:
355: return new DecimalValue(value
356: .add(((DecimalValue) other).value));
357: case Token.MINUS:
358: return new DecimalValue(value
359: .subtract(((DecimalValue) other).value));
360: case Token.MULT:
361: return new DecimalValue(value
362: .multiply(((DecimalValue) other).value));
363: case Token.DIV:
364: int scale = Math.max(DIVIDE_PRECISION, Math.max(
365: value.scale(), ((DecimalValue) other).value
366: .scale()));
367: //int scale = value.scale() + ((DecimalValue)other).value.scale() + DIVIDE_PRECISION;
368: BigDecimal result = value.divide(
369: ((DecimalValue) other).value, scale,
370: BigDecimal.ROUND_HALF_DOWN);
371: return new DecimalValue(result);
372: case Token.IDIV:
373: if (((DecimalValue) other).value.signum() == 0) {
374: DynamicError e = new DynamicError(
375: "Integer division by zero");
376: e.setErrorCode("FOAR0001");
377: e.setXPathContext(context);
378: throw e;
379: }
380: BigInteger quot = value.divide(
381: ((DecimalValue) other).value, 0,
382: BigDecimal.ROUND_DOWN).toBigInteger();
383: return BigIntegerValue.makeValue(quot);
384: case Token.MOD:
385: //BigDecimal quotient = value.divide(((DecimalValue)other).value, ((DecimalValue)other).value.scale(), BigDecimal.ROUND_DOWN);
386: BigDecimal quotient = value.divide(
387: ((DecimalValue) other).value, 0,
388: BigDecimal.ROUND_DOWN);
389: BigDecimal remainder = value.subtract(quotient
390: .multiply(((DecimalValue) other).value));
391: return new DecimalValue(remainder);
392: default:
393: throw new AssertionError("Unknown operator");
394: }
395: } catch (ArithmeticException err) {
396: throw new DynamicError(err);
397: }
398: } else if (NumericValue.isInteger(other)) {
399: return arithmetic(operator, (DecimalValue) other.convert(
400: Type.DECIMAL, context), context);
401: } else {
402: final TypeHierarchy th = context.getNamePool()
403: .getTypeHierarchy();
404: final NumericValue n = (NumericValue) convert(other
405: .getItemType(th).getPrimitiveType(), context);
406: return n.arithmetic(operator, other, context);
407: }
408: }
409:
410: /**
411: * Compare the value to another numeric value
412: */
413:
414: public int compareTo(Object other) {
415: if ((NumericValue.isInteger((NumericValue) other))) {
416: // deliberately triggers a ClassCastException if other value is the wrong type
417: try {
418: return compareTo(((NumericValue) other).convert(
419: Type.DECIMAL, null));
420: } catch (XPathException err) {
421: throw new AssertionError(
422: "Conversion of integer to decimal should never fail");
423: }
424: } else if (other instanceof BigIntegerValue) {
425: return value.compareTo(((BigIntegerValue) other)
426: .asDecimal());
427: } else if (other instanceof DecimalValue) {
428: return value.compareTo(((DecimalValue) other).value);
429: } else if (other instanceof FloatValue) {
430: try {
431: return ((FloatValue) convert(Type.FLOAT, null))
432: .compareTo(other);
433: } catch (XPathException err) {
434: throw new AssertionError(
435: "Conversion of decimal to float should never fail");
436: }
437: } else {
438: return super .compareTo(other);
439: }
440: }
441:
442: /**
443: * Compare two values for equality. This supports identity constraints in XML Schema,
444: * which allow list-valued elements and attributes to participate in key and uniqueness constraints.
445: * This method returns false if any error occurs during the comparison, or if any of the items
446: * in either sequence is a node rather than an atomic value. The default implementation of
447: * schemaEquals() is the same as equals(), but subclasses can override this.
448: */
449:
450: public boolean schemaEquals(Value obj) {
451: if (obj instanceof AtomicValue) {
452: obj = ((AtomicValue) obj).getPrimitiveValue();
453: }
454: try {
455: if (obj instanceof DecimalValue) {
456: return value.equals(((DecimalValue) obj).value);
457: } else if (obj instanceof IntegerValue) {
458: return schemaEquals(((IntegerValue) obj).convert(
459: Type.DECIMAL, null));
460: } else if (obj instanceof BigIntegerValue) {
461: return schemaEquals(((BigIntegerValue) obj).convert(
462: Type.DECIMAL, null));
463: } else {
464: return false;
465: }
466: } catch (XPathException e) {
467: return false;
468: }
469: }
470:
471: /**
472: * Convert to Java object (for passing to external functions)
473: */
474:
475: public Object convertToJava(Class target, XPathContext context)
476: throws XPathException {
477: if (target == Object.class
478: || target.isAssignableFrom(BigDecimal.class)) {
479: return value;
480: } else if (target.isAssignableFrom(DecimalValue.class)) {
481: return this ;
482: } else if (target == boolean.class) {
483: BooleanValue bval = (BooleanValue) convert(Type.BOOLEAN,
484: context);
485: return Boolean.valueOf(bval.getBooleanValue());
486: } else if (target == Boolean.class) {
487: BooleanValue bval = (BooleanValue) convert(Type.BOOLEAN,
488: context);
489: return Boolean.valueOf(bval.getBooleanValue());
490: } else if (target == String.class
491: || target == CharSequence.class) {
492: return getStringValue();
493: } else if (target == double.class || target == Double.class) {
494: return new Double(value.doubleValue());
495: } else if (target == float.class || target == Float.class) {
496: return new Float(value.floatValue());
497: } else if (target == long.class || target == Long.class) {
498: return new Long(value.longValue());
499: } else if (target == int.class || target == Integer.class) {
500: return new Integer(value.intValue());
501: } else if (target == short.class || target == Short.class) {
502: return new Short(value.shortValue());
503: } else if (target == byte.class || target == Byte.class) {
504: return new Byte(value.byteValue());
505: } else if (target == char.class || target == Character.class) {
506: return new Character((char) value.intValue());
507: } else {
508: Object o = super .convertToJava(target, context);
509: if (o == null) {
510: throw new DynamicError("Conversion of decimal to "
511: + target.getName() + " is not supported");
512: }
513: return o;
514: }
515: }
516:
517: }
518:
519: //
520: // The contents of this file are subject to the Mozilla Public License Version 1.0 (the "License");
521: // you may not use this file except in compliance with the License. You may obtain a copy of the
522: // License at http://www.mozilla.org/MPL/
523: //
524: // Software distributed under the License is distributed on an "AS IS" basis,
525: // WITHOUT WARRANTY OF ANY KIND, either express or implied.
526: // See the License for the specific language governing rights and limitations under the License.
527: //
528: // The Original Code is: all this file except the asStringXT() and zeros() methods (not currently used).
529: //
530: // The Initial Developer of the Original Code is Michael H. Kay.
531: //
532: // Portions created by (xt) are Copyright (C) (James Clark). All Rights Reserved.
533: //
534: // Contributor(s): none.
535: //
|