/* * eXist Open Source Native XML Database * Copyright (C) 2012 The eXist Project * http://exist-db.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * $Id$ */ package org.exist.xquery.functions.fn; import org.exist.dom.QName; import org.exist.xquery.*; import org.exist.xquery.value.*; import java.lang.String; import java.math.BigDecimal; /** * fn:format-number($value as numeric?, $picture as xs:string) as xs:string * fn:format-number($value as numeric?, $picture as xs:string, $decimal-format-name as xs:string) as xs:string * * @author <a href="mailto:shabanovd@gmail.com">Dmitriy Shabanov</a> * */ public class FnFormatNumbers extends BasicFunction { private static final SequenceType NUMBER_PARAMETER = new FunctionParameterSequenceType("value", Type.NUMBER, Cardinality.ZERO_OR_ONE, "The number to format"); private static final SequenceType PICTURE = new FunctionParameterSequenceType("picture", Type.STRING, Cardinality.EXACTLY_ONE, "The format pattern string. Please see the JavaDoc for java.text.DecimalFormat to get the specifics of this format string."); private static final String PICTURE_DESCRIPTION = "The formatting of a number is controlled by a picture string. The picture string is a sequence of ·characters·, in which the characters assigned to the variables decimal-separator-sign, grouping-sign, decimal-digit-family, optional-digit-sign and pattern-separator-sign are classified as active characters, and all other characters (including the percent-sign and per-mille-sign) are classified as passive characters."; private static final SequenceType DECIMAL_FORMAT = new FunctionParameterSequenceType("decimal-format-name", Type.STRING, Cardinality.EXACTLY_ONE, "The decimal-format name must be a QName, which is expanded as described in [2.4 Qualified Names]. It is an error if the stylesheet does not contain a declaration of the decimal-format with the specified expanded-name."); private static final String DECIMAL_FORMAT_DESCRIPTION = ""; private static final FunctionReturnSequenceType FUNCTION_RETURN_TYPE = new FunctionReturnSequenceType(Type.STRING, Cardinality.ONE, "the formatted string"); public final static FunctionSignature signatures[] = { new FunctionSignature( new QName("format-number", Function.BUILTIN_FUNCTION_NS, FnModule.PREFIX), PICTURE_DESCRIPTION, new SequenceType[] {NUMBER_PARAMETER, PICTURE}, FUNCTION_RETURN_TYPE ), new FunctionSignature( new QName("format-number", Function.BUILTIN_FUNCTION_NS, FnModule.PREFIX), DECIMAL_FORMAT_DESCRIPTION, new SequenceType[] {NUMBER_PARAMETER, PICTURE, DECIMAL_FORMAT}, FUNCTION_RETURN_TYPE ) }; /** * @param context */ public FnFormatNumbers(XQueryContext context, FunctionSignature signature) { super(context, signature); } @Override public Sequence eval(Sequence[] args, Sequence contextSequence) throws XPathException { if (args[0].isEmpty()) { return Sequence.EMPTY_SEQUENCE; } final NumericValue numericValue = (NumericValue)args[0].itemAt(0); try { final Formatter[] formatters = prepare(args[1].getStringValue()); final String value = format(formatters[0], numericValue); return new StringValue(value); } catch (final java.lang.IllegalArgumentException e) { e.printStackTrace(); throw new XPathException(e.getMessage()); } } private String format(Formatter f, NumericValue numericValue) throws XPathException { if (numericValue.isNaN()) { return NaN; } final String minuSign = numericValue.isNegative()? String.valueOf(MINUS_SIGN) : ""; if (numericValue.isInfinite()) { return minuSign + f.prefix + INFINITY + f.suffix; } NumericValue factor = null; if (f.isPercent) { factor = new IntegerValue(100); } else if (f.isPerMille) { factor = new IntegerValue(1000); } if (factor != null) { try { numericValue = (NumericValue) numericValue.mult(factor); } catch (final XPathException e) { e.printStackTrace(); throw e; } } int pl = 0; final StringBuilder sb = new StringBuilder(); if (numericValue.hasFractionalPart()) { BigDecimal val = ((DecimalValue)numericValue.convertTo(Type.DECIMAL)).getValue(); val = val.setScale(f.flMAX, BigDecimal.ROUND_HALF_EVEN); final String number = val.toPlainString(); sb.append(number); pl = number.indexOf('.'); if (pl < 0) { sb.append('.'); for (int i = 0; i < f.flMIN; i++) { sb.append('0'); } } else { } } else { final String str = numericValue.getStringValue(); pl = str.length(); formatInt(str, sb, f); } if (f.mg != 0) { int pos = pl - f.mg; while (pos > 0) { sb.insert(pos, ','); pos -= f.mg; } } return sb.toString(); } private void formatInt(String number, StringBuilder sb, Formatter f) { final int leadingZ = f.mlMIN - number.length(); for (int i = 0; i < leadingZ; i++) { sb.append('0'); } sb.append(number); if (f.flMIN > 0) { sb.append("."); for (int i = 0; i < f.mlMIN; i++) { sb.append('0'); } } } private final char DECIMAL_SEPARATOR_SIGN = '.'; private final char GROUPING_SEPARATOR_SIGN = ','; private final String INFINITY = "Infinity"; private final char MINUS_SIGN = '-'; private final String NaN = "NaN"; private final char PERCENT_SIGN = '%'; private final char PER_MILLE_SIGN = '\u2030'; private final char MANDATORY_DIGIT_SIGN = '0'; private final char OPTIONAL_DIGIT_SIGN = '#'; private final char PATTERN_SEPARATOR_SIGN = ';'; private Formatter[] prepare(String picture) throws XPathException { if (picture.length() == 0) {throw new XPathException(this, ErrorCodes.XTDE1310, "format-number() picture is zero-length");} final String[] pics = picture.split(String.valueOf(PATTERN_SEPARATOR_SIGN)); final Formatter[] formatters = new Formatter[2]; for (int i = 0; i < pics.length; i++) { formatters[i] = new Formatter(pics[i]); } return formatters; } class Formatter { String prefix = "", suffix = ""; boolean ds = false, isPercent = false, isPerMille = false; int mlMAX = 0, flMAX = 0; int mlMIN = 0, flMIN = 0; int mg = 0, fg = 0; public Formatter(String picture) throws XPathException { if ( ! ( picture.contains(String.valueOf(OPTIONAL_DIGIT_SIGN)) || picture.contains(String.valueOf(MANDATORY_DIGIT_SIGN)) ) ) { throw new XPathException(FnFormatNumbers.this, ErrorCodes.XTDE1310, "A sub-picture must contain at least one character that is an optional-digit-sign or a member of the decimal-digit-family."); } int bmg = -1, bfg = -1; // 0 - beginning passive-chars // 1 - digit signs // 2 - zero signs // 3 - fractional zero signs // 4 - fractional digit signs // 5 - ending passive-chars short phase = 0; for (int i = 0; i < picture.length(); i++) { char ch = picture.charAt(i); switch (ch) { case OPTIONAL_DIGIT_SIGN: switch (phase) { case 0: case 1: mlMAX++; phase = 1; break; case 2: throw new XPathException(FnFormatNumbers.this, ErrorCodes.XTDE1310, ""); case 3: case 4: flMAX++; phase = 4; break; case 5: throw new XPathException(FnFormatNumbers.this, ErrorCodes.XTDE1310, "A sub-picture must not contain a passive character that is preceded by an active character and that is followed by another active character. " + "Found at optional-digit-sign."); } break; case MANDATORY_DIGIT_SIGN: switch (phase) { case 0: case 1: case 2: mlMIN++; mlMAX++; phase = 2; break; case 3: flMIN++; flMAX++; break; case 4: throw new XPathException(FnFormatNumbers.this, ErrorCodes.XTDE1310, ""); case 5: throw new XPathException(FnFormatNumbers.this, ErrorCodes.XTDE1310, "A sub-picture must not contain a passive character that is preceded by an active character and that is followed by another active character. " + "Found at mandatory-digit-sign."); } break; case GROUPING_SEPARATOR_SIGN: switch (phase) { case 0: case 1: case 2: if (bmg == -1) { bmg = i; } else { mg = i - bmg; bmg = -1; } break; case 3: case 4: if (bfg == -1) { bfg = i; } else { fg = i - bfg; bfg = -1; } break; case 5: throw new XPathException(FnFormatNumbers.this, ErrorCodes.XTDE1310, "A sub-picture must not contain a passive character that is preceded by an active character and that is followed by another active character. " + "Found at grouping-separator-sign."); } break; case DECIMAL_SEPARATOR_SIGN: switch (phase) { case 0: case 1: case 2: if (bmg != -1) { mg = i - bmg - 1; bmg = -1; } ds = true; phase = 3; break; case 3: case 4: case 5: if (ds) {throw new XPathException(FnFormatNumbers.this, ErrorCodes.XTDE1310, "A sub-picture must not contain more than one decimal-separator-sign.");} throw new XPathException(FnFormatNumbers.this, ErrorCodes.XTDE1310, "A sub-picture must not contain a passive character that is preceded by an active character and that is followed by another active character. " + "Found at decimal-separator-sign."); } break; case PERCENT_SIGN: case PER_MILLE_SIGN: if (isPercent || isPerMille) {throw new XPathException(FnFormatNumbers.this, ErrorCodes.XTDE1310, "A sub-picture must not contain more than one percent-sign or per-mille-sign, and it must not contain one of each.");} isPercent = ch == PERCENT_SIGN; isPerMille = ch == PER_MILLE_SIGN; switch (phase) { case 0: prefix += ch; break; case 1: case 2: case 3: case 4: case 5: phase = 5; suffix += ch; break; } break; default: //passive chars switch (phase) { case 0: prefix += ch; break; case 1: case 2: case 3: case 4: case 5: if (bmg != -1) { mg = i - bmg - 1; bmg = -1; } suffix += ch; phase = 5; break; } break; } } if (mlMIN == 0 && !ds) {mlMIN = 1;} // System.out.println("prefix = "+prefix); // System.out.println("suffix = "+suffix); // System.out.println("ds = "+ds); // System.out.println("isPercent = "+isPercent); // System.out.println("isPerMille = "+isPerMille); // System.out.println("ml = "+mlMAX); // System.out.println("fl = "+flMAX); // System.out.println("mg = "+mg); // System.out.println("fg = "+fg); } } }