package client.net.sf.saxon.ce.functions; import client.net.sf.saxon.ce.expr.*; import client.net.sf.saxon.ce.tree.linked.CharSlice; import client.net.sf.saxon.ce.tree.util.FastStringBuffer; import client.net.sf.saxon.ce.om.Item; import client.net.sf.saxon.ce.om.NamespaceResolver; import client.net.sf.saxon.ce.om.StructuredQName; import client.net.sf.saxon.ce.trans.DecimalFormatManager; import client.net.sf.saxon.ce.trans.DecimalSymbols; import client.net.sf.saxon.ce.trans.XPathException; import client.net.sf.saxon.ce.value.*; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; /** * XSLT 2.0 implementation of format-number() function - removes the dependence on the JDK. */ public class FormatNumber extends SystemFunction { public FormatNumber newInstance() { return new FormatNumber(); } private NamespaceResolver nsContext = null; // held only if the third argument is present, and its value is not known statically private DecimalFormatManager decimalFormatManager = null; // held only if the decimalFormatSymbols cannot be determined statically private DecimalSymbols decimalFormatSymbols = null; // held only if the decimal format to use can be determined statically private transient String picture = null; // held transiently at compile time if the picture is known statically private SubPicture[] subPictures = null; // held if the picture is known statically private transient boolean checked = false; // the second time checkArguments is called, it's a global check so the static context is inaccurate public void checkArguments(ExpressionVisitor visitor) throws XPathException { StaticContext env = visitor.getStaticContext(); if (checked) { return; } checked = true; super.checkArguments(visitor); decimalFormatManager = env.getDecimalFormatManager(); if (decimalFormatManager == null) { // create a decimal format manager which will allow a "default default" format only decimalFormatManager = new DecimalFormatManager(); } if (argument[1] instanceof StringLiteral) { // picture is known statically - optimize for this common case picture = ((StringLiteral)argument[1]).getStringValue(); } if (argument.length==3) { if (argument[2] instanceof StringLiteral) { // common case, decimal format name is supplied as a string literal String lexicalName = ((StringLiteral)argument[2]).getStringValue(); StructuredQName qName; try { qName = StructuredQName.fromLexicalQName(lexicalName, false, env.getNamespaceResolver()); } catch (XPathException e) { XPathException se = new XPathException("Invalid decimal format name. " + e.getMessage()); se.setErrorCode("XTDE1280"); throw se; } decimalFormatSymbols = decimalFormatManager.getNamedDecimalFormat(qName); if (decimalFormatSymbols == null) { XPathException se = new XPathException("Unknown decimal format name " + lexicalName); se.setErrorCode("XTDE1280"); throw se; } } else { // we need to save the namespace context nsContext = env.getNamespaceResolver(); } } else { // two arguments only: it uses the default decimal format decimalFormatSymbols = decimalFormatManager.getDefaultDecimalFormat(); } } /** * Analyze a picture string into two sub-pictures. * @param picture the picture as written (possibly two subpictures separated by a semicolon) * @param dfs the decimal format symbols * @return an array of two sub-pictures, the positive and the negative sub-pictures respectively. * If there is only one sub-picture, the second one is null. */ private static SubPicture[] getSubPictures(String picture, DecimalSymbols dfs) throws XPathException { int[] picture4 = StringValue.expand(picture); SubPicture[] pics = new SubPicture[2]; if (picture4.length==0) { XPathException err = new XPathException("format-number() picture is zero-length"); err.setErrorCode("XTDE1310"); throw err; } int sep = -1; for (int c=0; c<picture4.length; c++) { if (picture4[c] == dfs.patternSeparator) { if (c==0) { grumble("first subpicture is zero-length"); } else if (sep >= 0) { grumble("more than one pattern separator"); } else if (sep == picture4.length-1) { grumble("second subpicture is zero-length"); } sep = c; } } if (sep<0) { pics[0] = new SubPicture(picture4, dfs); pics[1] = null; } else { int[] pic0 = new int[sep]; System.arraycopy(picture4, 0, pic0, 0, sep); int[] pic1 = new int[picture4.length - sep - 1]; System.arraycopy(picture4, sep+1, pic1, 0, picture4.length - sep - 1); pics[0] = new SubPicture(pic0, dfs); pics[1] = new SubPicture(pic1, dfs); } return pics; } /** * preEvaluate: this method suppresses compile-time evaluation by doing nothing. * We can't evaluate early because we don't have access to the DecimalFormatManager. * @param visitor the expression visitor */ public Expression preEvaluate(ExpressionVisitor visitor) throws XPathException { return this; } /** * Evaluate in a context where a string is wanted */ public CharSequence evaluateAsString(XPathContext context) throws XPathException { int numArgs = argument.length; DecimalSymbols dfs = decimalFormatSymbols; AtomicValue av0 = (AtomicValue)argument[0].evaluateItem(context); if (av0 == null) { av0 = DoubleValue.NaN; } NumericValue number = (NumericValue)av0; if (dfs == null) { DecimalFormatManager dfm = decimalFormatManager; if (numArgs==2) { dfs = dfm.getDefaultDecimalFormat(); } else { // the decimal-format name was given as a run-time expression String lexicalName = argument[2].evaluateItem(context).getStringValue(); StructuredQName qName = null; try { qName = StructuredQName.fromLexicalQName(lexicalName, false, nsContext); } catch (XPathException e) { dynamicError("Invalid decimal format name. " + e.getMessage(), "XTDE1280", context); } dfs = dfm.getNamedDecimalFormat(qName); if (dfs==null) { dynamicError("format-number function: decimal-format '" + lexicalName + "' is not defined", "XTDE1280", context); } } } SubPicture[] pics = subPictures; if (pics == null) { String format = argument[1].evaluateItem(context).getStringValue(); pics = getSubPictures(format, dfs); } return formatNumber(number, pics, dfs); } /** * Evaluate in a general context */ public Item evaluateItem(XPathContext c) throws XPathException { return new StringValue(evaluateAsString(c)); } /** * Format a number according to a supplied picture string * @param number the number * @param picture the picture string * @param symbols the decimal formatting options * @return the formatted number * @throws XPathException */ public static String formatNumber(NumericValue number, String picture, DecimalSymbols symbols) throws XPathException { SubPicture[] subPictures = getSubPictures(picture, symbols); return formatNumber(number, subPictures, symbols).toString(); } /** * Format a number, given the two subpictures and the decimal format symbols * @param number the number to be formatted * @param subPictures the negative and positive subPictures * @param dfs the decimal format symbols to be used * @return the formatted number */ private static CharSequence formatNumber(NumericValue number, SubPicture[] subPictures, DecimalSymbols dfs) { NumericValue absN = number; SubPicture pic; String minusSign = ""; if (number.signum() < 0) { absN = number.negate(); if (subPictures[1]==null) { pic = subPictures[0]; minusSign = "" + unicodeChar(dfs.minusSign); } else { pic = subPictures[1]; } } else { pic = subPictures[0]; } return pic.format(absN, dfs, minusSign); } private static void grumble(String s) throws XPathException { throw new XPathException("format-number picture: " + s, "XTDE1310"); } /** * Convert a double to a BigDecimal. In general there will be several BigDecimal values that * are equal to the supplied value, and the one we want to choose is the one with fewest non-zero * digits. The algorithm used is rather pragmatic: look for a string of zeroes or nines, try rounding * the number down or up as approriate, then convert the adjusted value to a double to see if it's * equal to the original: if not, use the original value unchanged. * @param value the double to be converted * @param precision 2 for a double, 1 for a float * @return the result of conversion to a double */ public static BigDecimal adjustToDecimal(double value, int precision) { final String zeros = (precision == 1 ? "00000" : "000000000"); final String nines = (precision == 1 ? "99999" : "999999999"); BigDecimal initial = new BigDecimal(value); BigDecimal trial = null; FastStringBuffer fsb = new FastStringBuffer(FastStringBuffer.TINY); DecimalValue.decimalToString(initial, fsb); String s = fsb.toString(); int start = (s.charAt(0) == '-' ? 1 : 0); int p = s.indexOf("."); int i = s.lastIndexOf(zeros); if (i > 0) { if (p < 0 || i < p) { // we're in the integer part // try replacing all following digits with zeros and seeing if we get the same double back FastStringBuffer sb = new FastStringBuffer(s.length()); sb.append(s.substring(0, i)); for (int n=i; n<s.length(); n++) { sb.append(s.charAt(n)=='.' ? '.' : '0'); } trial = new BigDecimal(sb.toString()); } else { // we're in the fractional part // try truncating the number before the zeros and seeing if we get the same double back trial = new BigDecimal(s.substring(0, i)); } } else { i = s.indexOf(nines); if (i >= 0) { if (i == start) { // number starts with 99999... or -99999. Try rounding up to 100000.. or -100000... FastStringBuffer sb = new FastStringBuffer(s.length() + 1); if (start == 1) { sb.append('-'); } sb.append('1'); for (int n=start; n<s.length(); n++) { sb.append(s.charAt(n)=='.' ? '.' : '0'); } trial = new BigDecimal(sb.toString()); } else { // try rounding up while (i >= 0 && (s.charAt(i) == '9' || s.charAt(i) == '.')) { i--; } if (i < 0 || s.charAt(i) == '-') { return initial; // can't happen: we've already handled numbers starting 99999.. } else if (p < 0 || i < p) { // we're in the integer part FastStringBuffer sb = new FastStringBuffer(s.length()); sb.append(s.substring(0, i)); sb.append((char)((int)s.charAt(i) + 1)); for (int n=i; n<s.length(); n++) { sb.append(s.charAt(n)=='.' ? '.' : '0'); } trial = new BigDecimal(sb.toString()); } else { // we're in the fractional part - can ignore following digits String s2 = s.substring(0, i) + (char)((int)s.charAt(i) + 1); trial = new BigDecimal(s2); } } } } if (trial != null && (precision==1 ? trial.floatValue() == value : trial.doubleValue() == value)) { return trial; } else { return initial; } } /** * Inner class to represent one sub-picture (the negative or positive subpicture) */ private static class SubPicture { int minWholePartSize = 0; int maxWholePartSize = 0; int minFractionPartSize = 0; int maxFractionPartSize = 0; boolean isPercent = false; boolean isPerMille = false; String prefix = ""; String suffix = ""; int[] wholePartGroupingPositions = null; int[] fractionalPartGroupingPositions = null; public SubPicture(int[] pic, DecimalSymbols dfs) throws XPathException { final int percentSign = dfs.percent; final int perMilleSign = dfs.permill; final int decimalSeparator = dfs.decimalSeparator; final int groupingSeparator = dfs.groupingSeparator; final int digitSign = dfs.digit; final int zeroDigit = dfs.zeroDigit; List wholePartPositions = null; List fractionalPartPositions = null; boolean foundDigit = false; boolean foundDecimalSeparator = false; for (int i=0; i<pic.length; i++) { if (pic[i] == digitSign || pic[i] == zeroDigit) { foundDigit = true; break; } } if (!foundDigit) { grumble("subpicture contains no digit or zero-digit sign"); } int phase = 0; // phase = 0: passive characters at start // phase = 1: digit signs in whole part // phase = 2: zero-digit signs in whole part // phase = 3: zero-digit signs in fractional part // phase = 4: digit signs in fractional part // phase = 5: passive characters at end for (int i=0; i<pic.length; i++) { int c = pic[i]; if (c == percentSign || c == perMilleSign) { if (isPercent || isPerMille) { grumble("Cannot have more than one percent or per-mille character in a sub-picture"); } isPercent = (c==percentSign); isPerMille = (c==perMilleSign); switch (phase) { case 0: prefix += unicodeChar(c); break; case 1: case 2: case 3: case 4: case 5: phase = 5; suffix += unicodeChar(c); break; } } else if (c == digitSign) { switch (phase) { case 0: case 1: phase = 1; maxWholePartSize++; break; case 2: grumble("Digit sign must not appear after a zero-digit sign in the integer part of a sub-picture"); break; case 3: case 4: phase = 4; maxFractionPartSize++; break; case 5: grumble("Passive character must not appear between active characters in a sub-picture"); break; } } else if (c == zeroDigit) { switch (phase) { case 0: case 1: case 2: phase = 2; minWholePartSize++; maxWholePartSize++; break; case 3: minFractionPartSize++; maxFractionPartSize++; break; case 4: grumble("Zero digit sign must not appear after a digit sign in the fractional part of a sub-picture"); break; case 5: grumble("Passive character must not appear between active characters in a sub-picture"); break; } } else if (c == decimalSeparator) { switch (phase) { case 0: case 1: case 2: phase = 3; foundDecimalSeparator = true; break; case 3: case 4: case 5: if (foundDecimalSeparator) { grumble("There must only be one decimal separator in a sub-picture"); } else { grumble("Decimal separator cannot come after a character in the suffix"); } break; } } else if (c == groupingSeparator) { switch (phase) { case 0: case 1: case 2: if (wholePartPositions == null) { wholePartPositions = new ArrayList(3); } wholePartPositions.add(Integer.valueOf(maxWholePartSize)); // note these are positions from a false offset, they will be corrected later break; case 3: case 4: if (maxFractionPartSize == 0) { grumble("Grouping separator cannot be adjacent to decimal separator"); } if (fractionalPartPositions == null) { fractionalPartPositions = new ArrayList(3); } fractionalPartPositions.add(Integer.valueOf(maxFractionPartSize)); break; case 5: grumble("Grouping separator found in suffix of sub-picture"); break; } } else { // passive character found switch (phase) { case 0: prefix += unicodeChar(c); break; case 1: case 2: case 3: case 4: case 5: phase = 5; suffix += unicodeChar(c); break; } } } if (minWholePartSize == 0 && !foundDecimalSeparator) { minWholePartSize = 1; } // System.err.println("minWholePartSize = " + minWholePartSize); // System.err.println("maxWholePartSize = " + maxWholePartSize); // System.err.println("minFractionPartSize = " + minFractionPartSize); // System.err.println("maxFractionPartSize = " + maxFractionPartSize); // Sort out the grouping positions if (wholePartPositions != null) { // convert to positions relative to the decimal separator int n = wholePartPositions.size(); wholePartGroupingPositions = new int[n]; for (int i=0; i<n; i++) { wholePartGroupingPositions[i] = maxWholePartSize - ((Integer)wholePartPositions.get(n - i - 1)).intValue(); } if (n > 1) { boolean regular = true; int first = wholePartGroupingPositions[0]; for (int i=1; i<n; i++) { if (wholePartGroupingPositions[i] != i * first) { regular = false; break; } } if (regular) { wholePartGroupingPositions = new int[1]; wholePartGroupingPositions[0] = first; } } if (wholePartGroupingPositions[0] == 0) { grumble("Cannot have a grouping separator adjacent to the decimal separator"); } } if (fractionalPartPositions != null) { int n = fractionalPartPositions.size(); fractionalPartGroupingPositions = new int[n]; for (int i=0; i<n; i++) { fractionalPartGroupingPositions[i] = ((Integer)fractionalPartPositions.get(i)).intValue(); } } } /** * Format a number using this sub-picture * @param value the absolute value of the number to be formatted * @param dfs the decimal format symbols to be used * @param minusSign the representation of a minus sign to be used * @return the formatted number */ public CharSequence format(NumericValue value, DecimalSymbols dfs, String minusSign) { // System.err.println("Formatting " + value); if (value.isNaN()) { return dfs.NaN; // changed by W3C Bugzilla 2712 } if ((value instanceof DoubleValue || value instanceof FloatValue) && Double.isInfinite(value.getDoubleValue())) { return minusSign + prefix + dfs.infinity + suffix; } int multiplier = 1; if (isPercent) { multiplier = 100; } else if (isPerMille) { multiplier = 1000; } if (multiplier != 1) { try { value = (NumericValue)ArithmeticExpression.compute( value, Calculator.TIMES, IntegerValue.makeIntegerValue(multiplier), null); } catch (XPathException e) { value = new DoubleValue(value.getDoubleValue() * multiplier); } } FastStringBuffer sb = new FastStringBuffer(FastStringBuffer.TINY); if (value instanceof DoubleValue || value instanceof FloatValue) { BigDecimal dec = adjustToDecimal(value.getDoubleValue(), 2); formatDecimal(dec, sb); //formatDouble(value.getDoubleValue(), sb); } else if (value instanceof IntegerValue) { formatInteger(value, sb); } else if (value instanceof DecimalValue) { //noinspection RedundantCast formatDecimal(((DecimalValue)value).getDecimalValue(), sb); } // System.err.println("Justified number: " + sb.toString()); // Map the digits and decimal point to use the selected characters int[] ib = StringValue.expand(sb); int ibused = ib.length; int point = sb.indexOf('.'); if (point == -1) { point = sb.length(); } else { ib[point] = dfs.decimalSeparator; // If there is no fractional part, delete the decimal point if (maxFractionPartSize == 0) { ibused--; } } // Map the digits if (dfs.zeroDigit != '0') { int newZero = dfs.zeroDigit; for (int i=0; i<ibused; i++) { int c = ib[i]; if (c>='0' && c<='9') { ib[i] = (c-'0'+newZero); } } } // Add the whole-part grouping separators if (wholePartGroupingPositions != null) { if (wholePartGroupingPositions.length == 1) { // grouping separators are at regular positions int g = wholePartGroupingPositions[0]; int p = point - g; while (p > 0) { ib = insert(ib, ibused++, dfs.groupingSeparator, p); //sb.insert(p, unicodeChar(dfs.groupingSeparator)); p -= g; } } else { // grouping separators are at irregular positions for (int i=0; i<wholePartGroupingPositions.length; i++) { int p = point - wholePartGroupingPositions[i]; if (p > 0) { ib = insert(ib, ibused++, dfs.groupingSeparator, p); //sb.insert(p, unicodeChar(dfs.groupingSeparator)); } } } } // Add the fractional-part grouping separators if (fractionalPartGroupingPositions != null) { // grouping separators are at irregular positions. for (int i=0; i<fractionalPartGroupingPositions.length; i++) { int p = point + 1 + fractionalPartGroupingPositions[i] + i; if (p < ibused-1) { ib = insert(ib, ibused++, dfs.groupingSeparator, p); //sb.insert(p, dfs.groupingSeparator); } else { break; } } } // System.err.println("Grouped number: " + sb.toString()); //sb.insert(0, prefix); //sb.insert(0, minusSign); //sb.append(suffix); FastStringBuffer res = new FastStringBuffer(prefix.length() + minusSign.length() + suffix.length() + ibused); res.append(minusSign); res.append(prefix); res.append(StringValue.contract(ib, ibused)); res.append(suffix); return res; } /** * Format a number supplied as a decimal * @param dval the decimal value * @param fsb the FastStringBuffer to contain the result */ private void formatDecimal(BigDecimal dval, FastStringBuffer fsb) { dval = dval.setScale(maxFractionPartSize, BigDecimal.ROUND_HALF_EVEN); DecimalValue.decimalToString(dval, fsb); int point = fsb.indexOf('.'); int intDigits; if (point >= 0) { int zz = maxFractionPartSize - minFractionPartSize; while (zz>0) { if (fsb.charAt(fsb.length()-1) == '0') { fsb.setLength(fsb.length()-1); zz--; } else { break; } } intDigits = point; if (fsb.charAt(fsb.length()-1) == '.') { fsb.setLength(fsb.length()-1); } } else { intDigits = fsb.length(); if (minFractionPartSize > 0) { fsb.append('.'); for (int i=0; i<minFractionPartSize; i++) { fsb.append('0'); } } } if (minWholePartSize == 0 && intDigits == 1 && fsb.charAt(0) == '0') { fsb.removeCharAt(0); } else { fsb.prependRepeated('0', minWholePartSize - intDigits); } } /** * Format a number supplied as a integer * @param value the integer value * @param fsb the FastStringBuffer to contain the result */ private void formatInteger(NumericValue value, FastStringBuffer fsb) { fsb.append(value.getStringValueCS()); int leadingZeroes = minWholePartSize - fsb.length(); fsb.prependRepeated('0', leadingZeroes); if (minFractionPartSize != 0) { fsb.append('.'); for (int i=0; i < minFractionPartSize; i++) { fsb.append('0'); } } } } /** * Convert a Unicode character (possibly >65536) to a String, using a surrogate pair if necessary * @param ch the Unicode codepoint value * @return a string representing the Unicode codepoint, either a string of one character or a surrogate pair */ private static CharSequence unicodeChar(int ch) { if (ch<65536) { return "" + (char)ch; } else { // output a surrogate pair //To compute the numeric value of the character corresponding to a surrogate //pair, use this formula (all numbers are hex): //(FirstChar - D800) * 400 + (SecondChar - DC00) + 10000 ch -= 65536; char[] sb = new char[2]; sb[0] = ((char)((ch / 1024) + 55296)); sb[1] = ((char)((ch % 1024) + 56320)); return new CharSlice(sb, 0, 2); } } /** * Insert an integer into an array of integers. This may or may not modify the supplied array. * @param array the initial array * @param used the number of items in the initial array that are used * @param value the integer to be inserted * @param position the position of the new integer in the final array * @return the new array, with the new integer inserted */ private static int[] insert(int[] array, int used, int value, int position) { if (used+1 > array.length) { int[] a2 = new int[used+10]; System.arraycopy(array, 0, a2, 0, used); array = a2; } for (int i=used-1; i>=position; i--) { array[i+1] = array[i]; } array[position] = value; return array; } } // 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.