You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2015/09/14 02:27:48 UTC
[4/4] incubator-freemarker git commit: Initial implementation of
extended DecimalFormat format string parsing. It's not used by FTL yet. Also
currency-related options are missing.
Initial implementation of extended DecimalFormat format string parsing. It's not used by FTL yet. Also currency-related options are missing.
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/1d5198f6
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/1d5198f6
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/1d5198f6
Branch: refs/heads/2.3-gae
Commit: 1d5198f67c2e90206742ace0900ea989088a2de0
Parents: aab0286
Author: ddekany <dd...@apache.org>
Authored: Mon Sep 14 02:27:27 2015 +0200
Committer: ddekany <dd...@apache.org>
Committed: Mon Sep 14 02:27:27 2015 +0200
----------------------------------------------------------------------
.../core/ExtendedDecimalFormatParser.java | 460 +++++++++++++++++++
.../core/ExtendedDecimalFormatTest.java | 246 ++++++++++
2 files changed, 706 insertions(+)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1d5198f6/src/main/java/freemarker/core/ExtendedDecimalFormatParser.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/ExtendedDecimalFormatParser.java b/src/main/java/freemarker/core/ExtendedDecimalFormatParser.java
new file mode 100644
index 0000000..95acbba
--- /dev/null
+++ b/src/main/java/freemarker/core/ExtendedDecimalFormatParser.java
@@ -0,0 +1,460 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package freemarker.core;
+
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Set;
+
+import freemarker.template.utility.StringUtil;
+
+class ExtendedDecimalFormatParser {
+
+ private static final HashMap<String, ? extends ParameterHandler> PARAM_HANDLERS;
+
+ static {
+ HashMap<String, ParameterHandler> m = new HashMap<String, ParameterHandler>();
+ m.put("ro", new ParameterHandler() {
+
+ public void handle(ExtendedDecimalFormatParser parser, String value)
+ throws InvalidParameterValueException {
+ RoundingMode parsedValue;
+ if (value.equals("u")) {
+ parsedValue = RoundingMode.UP;
+ } else if (value.equals("d")) {
+ parsedValue = RoundingMode.DOWN;
+ } else if (value.equals("c")) {
+ parsedValue = RoundingMode.CEILING;
+ } else if (value.equals("f")) {
+ parsedValue = RoundingMode.FLOOR;
+ } else if (value.equals("hd")) {
+ parsedValue = RoundingMode.HALF_DOWN;
+ } else if (value.equals("he")) {
+ parsedValue = RoundingMode.HALF_EVEN;
+ } else if (value.equals("hu")) {
+ parsedValue = RoundingMode.HALF_UP;
+ } else if (value.equals("un")) {
+ parsedValue = RoundingMode.UNNECESSARY;
+ } else {
+ throw new InvalidParameterValueException("Should be one of: u, d, c, f, hd, he, hu, un");
+ }
+
+ if (_JavaVersions.JAVA_6 == null) {
+ throw new InvalidParameterValueException("For setting the rounding mode you need Java 6 or later.");
+ }
+
+ parser.roundingMode = parsedValue;
+ }
+ });
+ m.put("mul", new ParameterHandler() {
+
+ public void handle(ExtendedDecimalFormatParser parser, String value)
+ throws InvalidParameterValueException {
+ try {
+ parser.multipier = Integer.valueOf(value);
+ } catch (NumberFormatException e) {
+ throw new InvalidParameterValueException("Malformed integer.");
+ }
+ }
+ });
+ m.put("dec", new ParameterHandler() {
+
+ public void handle(ExtendedDecimalFormatParser parser, String value)
+ throws InvalidParameterValueException {
+ if (value.length() != 1) {
+ throw new InvalidParameterValueException("Must contain exactly 1 character.");
+ }
+ parser.symbols.setDecimalSeparator(value.charAt(0));
+ }
+ });
+ m.put("grp", new ParameterHandler() {
+
+ public void handle(ExtendedDecimalFormatParser parser, String value)
+ throws InvalidParameterValueException {
+ if (value.length() != 1) {
+ throw new InvalidParameterValueException("Must contain exactly 1 character.");
+ }
+ parser.symbols.setGroupingSeparator(value.charAt(0));
+ }
+ });
+ m.put("exp", new ParameterHandler() {
+
+ public void handle(ExtendedDecimalFormatParser parser, String value)
+ throws InvalidParameterValueException {
+ if (_JavaVersions.JAVA_6 == null) {
+ throw new InvalidParameterValueException(
+ "For setting the exponent separator you need Java 6 or later.");
+ }
+ _JavaVersions.JAVA_6.setExponentSeparator(parser.symbols, value);
+ }
+ });
+ m.put("min", new ParameterHandler() {
+
+ public void handle(ExtendedDecimalFormatParser parser, String value)
+ throws InvalidParameterValueException {
+ if (value.length() != 1) {
+ throw new InvalidParameterValueException("Must contain exactly 1 character.");
+ }
+ parser.symbols.setMinusSign(value.charAt(0));
+ }
+ });
+ m.put("inf", new ParameterHandler() {
+
+ public void handle(ExtendedDecimalFormatParser parser, String value)
+ throws InvalidParameterValueException {
+ parser.symbols.setInfinity(value);
+ }
+ });
+ m.put("nan", new ParameterHandler() {
+
+ public void handle(ExtendedDecimalFormatParser parser, String value)
+ throws InvalidParameterValueException {
+ parser.symbols.setNaN(value);
+ }
+ });
+ m.put("prc", new ParameterHandler() {
+
+ public void handle(ExtendedDecimalFormatParser parser, String value)
+ throws InvalidParameterValueException {
+ if (value.length() != 1) {
+ throw new InvalidParameterValueException("Must contain exactly 1 character.");
+ }
+ parser.symbols.setPercent(value.charAt(0));
+ }
+ });
+ m.put("prm", new ParameterHandler() {
+
+ public void handle(ExtendedDecimalFormatParser parser, String value)
+ throws InvalidParameterValueException {
+ if (value.length() != 1) {
+ throw new InvalidParameterValueException("Must contain exactly 1 character.");
+ }
+ parser.symbols.setPerMill(value.charAt(0));
+ }
+ });
+ m.put("zero", new ParameterHandler() {
+
+ public void handle(ExtendedDecimalFormatParser parser, String value)
+ throws InvalidParameterValueException {
+ if (value.length() != 1) {
+ throw new InvalidParameterValueException("Must contain exactly 1 character.");
+ }
+ parser.symbols.setZeroDigit(value.charAt(0));
+ }
+ });
+ PARAM_HANDLERS = m;
+ }
+
+ private static final String SNIP_MARK = "[...]";
+ private static final int MAX_QUOTATION_LENGTH = 10; // Must be more than SNIP_MARK.length!
+
+ private final String src;
+ private int pos = 0;
+
+ private final DecimalFormatSymbols symbols;
+ private RoundingMode roundingMode;
+ private Integer multipier;
+
+ static DecimalFormat parse(String formatString, Locale locale) throws ParseException {
+ return new ExtendedDecimalFormatParser(formatString, locale).parse();
+ }
+
+ private DecimalFormat parse() throws ParseException {
+ String stdPattern = fetchStandardPattern();
+ skipWS();
+ parseFormatStringExtension();
+
+ DecimalFormat decimalFormat = new DecimalFormat(stdPattern, symbols);
+
+ if (roundingMode != null) {
+ if (_JavaVersions.JAVA_6 == null) {
+ throw new ParseException("Setting rounding mode needs Java 6 or later", 0);
+ }
+ _JavaVersions.JAVA_6.setRoundingMode(decimalFormat, roundingMode);
+ }
+
+ if (multipier != null) {
+ decimalFormat.setMultiplier(multipier.intValue());
+ }
+
+ return decimalFormat;
+ }
+
+ private void parseFormatStringExtension() throws ParseException {
+ int ln = src.length();
+
+ if (pos == ln) {
+ return;
+ }
+
+ do {
+ int namePos = pos;
+ String name = fetchName();
+ if (name == null) {
+ throw newExpectedSgParseException("name");
+ }
+
+ skipWS();
+
+ if (!fetchChar('=')) {
+ throw newExpectedSgParseException("\"=\"");
+ }
+
+ skipWS();
+
+ int valuePos = pos;
+ String value = fetchValue();
+ if (value == null) {
+ throw newExpectedSgParseException("value");
+ }
+ int paramEndPos = pos;
+
+ applyFormatStringExtensionParameter(name, namePos, value, valuePos);
+
+ skipWS();
+
+ // Optional comma
+ if (fetchChar(',')) {
+ skipWS();
+ } else {
+ if (pos == ln) {
+ return;
+ }
+ if (pos == paramEndPos) {
+ throw newExpectedSgParseException("parameter separator whitespace or comma");
+ }
+ }
+ } while (true);
+ }
+
+ private void applyFormatStringExtensionParameter(
+ String name, int namePos, String value, int valuePos) throws ParseException {
+ ParameterHandler handler = PARAM_HANDLERS.get(name);
+ if (handler == null) {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("Unsupported parameter name, ").append(StringUtil.jQuote(name));
+ sb.append(". The supported names are: ");
+ Set<String> legalNames = PARAM_HANDLERS.keySet();
+ String[] legalNameArr = legalNames.toArray(new String[legalNames.size()]);
+ Arrays.sort(legalNameArr);
+ for (int i = 0; i < legalNameArr.length; i++) {
+ if (i != 0) {
+ sb.append(", ");
+ }
+ sb.append(legalNameArr[i]);
+ }
+ throw new java.text.ParseException(sb.toString(), namePos);
+ }
+
+ try {
+ handler.handle(this, value);
+ } catch (InvalidParameterValueException e) {
+ throw new java.text.ParseException(
+ StringUtil.jQuote(value) + " is an invalid value for the \"" + name + "\" parameter: " + e.message,
+ valuePos);
+ }
+ }
+
+ private void skipWS() {
+ int ln = src.length();
+ while (pos < ln && isWS(src.charAt(pos))) {
+ pos++;
+ }
+ }
+
+ private boolean fetchChar(char fetchedChar) {
+ if (pos < src.length() && src.charAt(pos) == fetchedChar) {
+ pos++;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private boolean isWS(char c) {
+ return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\u00A0';
+ }
+
+ private String fetchName() throws ParseException {
+ int ln = src.length();
+ int startPos = pos;
+ boolean firstChar = true;
+ scanUntilEnd: while (pos < ln) {
+ char c = src.charAt(pos);
+ if (firstChar) {
+ if (!Character.isJavaIdentifierStart(c)) {
+ break scanUntilEnd;
+ }
+ firstChar = false;
+ } else if (!Character.isJavaIdentifierPart(c)) {
+ break scanUntilEnd;
+ }
+ pos++;
+ }
+ return !firstChar ? src.substring(startPos, pos) : null;
+ }
+
+ private String fetchValue() throws ParseException {
+ int ln = src.length();
+ int startPos = pos;
+ boolean quotedMode = false;
+ boolean needsUnescaping = false;
+ scanUntilEnd: while (pos < ln) {
+ char c = src.charAt(pos);
+ if (c == '\'') {
+ if (!quotedMode) {
+ if (startPos != pos) {
+ throw new java.text.ParseException(
+ "The \"'\" character can only be used for quoting values, "
+ + "but it was in the middle of an non-quoted value.",
+ pos);
+ }
+ quotedMode = true;
+ } else {
+ if (pos + 1 < ln && src.charAt(pos + 1) == '\'') {
+ pos++; // skip "''" (escaped "'")
+ needsUnescaping = true;
+ } else {
+ String str = src.substring(startPos + 1, pos);
+ pos++;
+ return needsUnescaping ? unescape(str) : str;
+ }
+ }
+ } else {
+ if (!quotedMode && !Character.isJavaIdentifierPart(c)) {
+ break scanUntilEnd;
+ }
+ }
+ pos++;
+ } // while
+ if (quotedMode) {
+ throw new java.text.ParseException(
+ "The \"'\" quotation wasn't closed when the end of the source was reached.",
+ pos);
+ }
+ return startPos == pos ? null : src.substring(startPos, pos);
+ }
+
+ private String unescape(String s) {
+ return StringUtil.replace(s, "\'\'", "\'");
+ }
+
+ private String fetchStandardPattern() {
+ int pos = this.pos;
+ int ln = src.length();
+ int semicolonCnt = 0;
+ boolean quotedMode = false;
+ findStdPartEnd: while (pos < ln) {
+ char c = src.charAt(pos);
+ if (c == ';' && !quotedMode) {
+ semicolonCnt++;
+ if (semicolonCnt == 2) {
+ break findStdPartEnd;
+ }
+ } else if (c == '\'') {
+ if (quotedMode) {
+ if (pos + 1 < ln && src.charAt(pos + 1) == '\'') {
+ // Skips "''" used for escaping "'"
+ pos++;
+ } else {
+ quotedMode = false;
+ }
+ } else {
+ quotedMode = true;
+ }
+ }
+ pos++;
+ }
+
+ String stdFormatStr;
+ if (semicolonCnt < 2) { // We have a standard DecimalFormat string
+ // Note that "0.0;" and "0.0" gives the same result with DecimalFormat, so we leave a ';' there
+ stdFormatStr = src;
+ } else { // `pos` points to the 2nd ';'
+ int stdEndPos = pos;
+ if (src.charAt(pos - 1) == ';') { // we have a ";;"
+ // Note that ";;" is illegal in DecimalFormat, so this is backward compatible.
+ stdEndPos--;
+ }
+ stdFormatStr = src.substring(0, stdEndPos);
+ }
+
+ if (pos < ln) {
+ pos++; // Skips closing ';'
+ }
+ this.pos = pos;
+
+ return stdFormatStr;
+ }
+
+ private ExtendedDecimalFormatParser(String formatString, Locale locale) {
+ src = formatString;
+ this.symbols = new DecimalFormatSymbols(locale);
+ }
+
+ private ParseException newExpectedSgParseException(String expectedThing) {
+ String quotation;
+
+ // Ignore trailing WS when calculating the length:
+ int i = src.length() - 1;
+ while (i >= 0 && Character.isWhitespace(src.charAt(i))) {
+ i--;
+ }
+ int ln = i + 1;
+
+ if (pos < ln) {
+ int qEndPos = pos + MAX_QUOTATION_LENGTH;
+ if (qEndPos >= ln) {
+ quotation = src.substring(pos, ln);
+ } else {
+ quotation = src.substring(pos, qEndPos - SNIP_MARK.length()) + SNIP_MARK;
+ }
+ } else {
+ quotation = null;
+ }
+
+ return new ParseException(
+ "Expected a(n) " + expectedThing + " at position " + pos + " (0-based), but "
+ + (quotation == null ? "reached the end of the input." : "found: " + quotation),
+ pos);
+ }
+
+ private interface ParameterHandler {
+
+ void handle(ExtendedDecimalFormatParser parser, String value)
+ throws InvalidParameterValueException;
+
+ }
+
+ private static class InvalidParameterValueException extends Exception {
+
+ private final String message;
+
+ public InvalidParameterValueException(String message) {
+ this.message = message;
+ }
+
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1d5198f6/src/test/java/freemarker/core/ExtendedDecimalFormatTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/ExtendedDecimalFormatTest.java b/src/test/java/freemarker/core/ExtendedDecimalFormatTest.java
new file mode 100644
index 0000000..d460fd4
--- /dev/null
+++ b/src/test/java/freemarker/core/ExtendedDecimalFormatTest.java
@@ -0,0 +1,246 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package freemarker.core;
+
+import static freemarker.test.hamcerst.Matchers.*;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.text.DecimalFormat;
+import java.text.ParseException;
+import java.util.Locale;
+
+import org.junit.Test;
+
+public class ExtendedDecimalFormatTest {
+
+ private static final Locale LOC = Locale.US;
+
+ @Test
+ public void testNonExtended() throws ParseException {
+ for (String fStr : new String[] { "0.00", "0.###", "#,#0.###", "#0.####", "0.0;m", "0.0;",
+ "0'x'", "0'x';'m'", "0';'", "0';';m", "0';';'#'m';'", "0';;'", "" }) {
+ assertFormatsEquivalent(new DecimalFormat(fStr), ExtendedDecimalFormatParser.parse(fStr, LOC));
+ }
+
+ try {
+ new DecimalFormat(";");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+ try {
+ ExtendedDecimalFormatParser.parse(";", LOC);
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void testNonExtended2() throws ParseException {
+ assertFormatsEquivalent(new DecimalFormat("0.0"), ExtendedDecimalFormatParser.parse("0.0;", LOC));
+ assertFormatsEquivalent(new DecimalFormat("0.0"), ExtendedDecimalFormatParser.parse("0.0;;", LOC));
+ assertFormatsEquivalent(new DecimalFormat("0.0;m"), ExtendedDecimalFormatParser.parse("0.0;m;", LOC));
+ assertFormatsEquivalent(new DecimalFormat(""), ExtendedDecimalFormatParser.parse(";;", LOC));
+ assertFormatsEquivalent(new DecimalFormat("0'x'"), ExtendedDecimalFormatParser.parse("0'x';;", LOC));
+ assertFormatsEquivalent(new DecimalFormat("0'x';'m'"), ExtendedDecimalFormatParser.parse("0'x';'m';", LOC));
+ assertFormatsEquivalent(new DecimalFormat("0';'"), ExtendedDecimalFormatParser.parse("0';';;", LOC));
+ assertFormatsEquivalent(new DecimalFormat("0';';m"), ExtendedDecimalFormatParser.parse("0';';m;", LOC));
+ assertFormatsEquivalent(new DecimalFormat("0';';'#'m';'"), ExtendedDecimalFormatParser.parse("0';';'#'m';';",
+ LOC));
+ assertFormatsEquivalent(new DecimalFormat("0';;'"), ExtendedDecimalFormatParser.parse("0';;';;", LOC));
+
+ try {
+ new DecimalFormat(";m");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+ try {
+ ExtendedDecimalFormatParser.parse(";m", LOC);
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+ try {
+ ExtendedDecimalFormatParser.parse(";m;", LOC);
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+ }
+
+ @SuppressWarnings("boxing")
+ @Test
+ public void testExtendedParamsParsing() throws ParseException {
+ for (String fs : new String[] {
+ "00.##;; dec='D'", "00.##;;dec=D", "00.##;; dec = D ", "00.##;; dec = 'D' " }) {
+ assertFormatted(fs, 1.125, "01D12");
+ }
+ for (String fs : new String[] {
+ ",#0.0;; dec=D, grp=_", ",#0.0;;dec=D,grp=_", ",#0.0;; dec = D , grp = _ ", ",#0.0;; dec='D', grp='_'"
+ }) {
+ assertFormatted(fs, 12345, "1_23_45D0");
+ }
+
+ assertFormatted("0.0;;inf=infinity", Double.POSITIVE_INFINITY, "infinity");
+ assertFormatted("0.0;;inf='infinity'", Double.POSITIVE_INFINITY, "infinity");
+ assertFormatted("0.0;;inf=''", Double.POSITIVE_INFINITY, "");
+ assertFormatted("0.0;;inf='x''y'", Double.POSITIVE_INFINITY, "x'y");
+ assertFormatted("0.0;;dec=''''", 1, "1'0");
+
+ try {
+ ExtendedDecimalFormatParser.parse(";;dec=D,", LOC);
+ fail();
+ } catch (java.text.ParseException e) {
+ assertThat(e.getMessage(),
+ allOf(containsStringIgnoringCase("expected a(n) name"), containsString(" end of ")));
+ }
+ try {
+ ExtendedDecimalFormatParser.parse(";;xdec=D,", LOC);
+ fail();
+ } catch (java.text.ParseException e) {
+ assertThat(e.getMessage(),
+ allOf(containsString("\"xdec\""), containsString("name")));
+ }
+ try {
+ ExtendedDecimalFormatParser.parse(";;dec='D", LOC);
+ fail();
+ } catch (java.text.ParseException e) {
+ assertThat(e.getMessage(),
+ allOf(containsString("quotation"), containsString("closed")));
+ }
+ try {
+ ExtendedDecimalFormatParser.parse(";;dec='D'grp=G", LOC);
+ fail();
+ } catch (java.text.ParseException e) {
+ assertThat(e.getMessage(), allOf(
+ containsString("separator"), containsString("whitespace"), containsString("comma")));
+ }
+ try {
+ ExtendedDecimalFormatParser.parse(";;dec=., grp=G", LOC);
+ fail();
+ } catch (java.text.ParseException e) {
+ assertThat(e.getMessage(), allOf(
+ containsStringIgnoringCase("expected a(n) value"), containsString("., grp")));
+ }
+ try {
+ ExtendedDecimalFormatParser.parse("0.0;;dec=''", LOC);
+ fail();
+ } catch (java.text.ParseException e) {
+ assertThat(e.getMessage(), allOf(
+ containsStringIgnoringCase("\"dec\""), containsString("exactly 1 char")));
+ }
+ try {
+ ExtendedDecimalFormatParser.parse("0.0;;mul=ten", LOC);
+ fail();
+ } catch (java.text.ParseException e) {
+ assertThat(e.getMessage(), allOf(
+ containsString("\"mul\""), containsString("\"ten\""), containsString("integer")));
+ }
+ }
+
+ @SuppressWarnings("boxing")
+ @Test
+ public void testExtendedParamsEffect() throws ParseException {
+ assertFormatted("0",
+ 1.5, "2", 2.5, "2", 3.5, "4", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-2", -2.5, "-2", -1.6, "-2");
+ assertFormatted("0;; ro=he",
+ 1.5, "2", 2.5, "2", 3.5, "4", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-2", -2.5, "-2", -1.6, "-2");
+ assertFormatted("0;; ro=hu",
+ 1.5, "2", 2.5, "3", 3.5, "4", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-2", -2.5, "-3", -1.6, "-2");
+ assertFormatted("0;; ro=hd",
+ 1.5, "1", 2.5, "2", 3.5, "3", 1.4, "1", 1.6, "2", -1.4, "-1", -1.5, "-1", -2.5, "-2", -1.6, "-2");
+ assertFormatted("0;; ro=f",
+ 1.5, "1", 2.5, "2", 3.5, "3", 1.4, "1", 1.6, "1", -1.4, "-2", -1.5, "-2", -2.5, "-3", -1.6, "-2");
+ assertFormatted("0;; ro=c",
+ 1.5, "2", 2.5, "3", 3.5, "4", 1.4, "2", 1.6, "2", -1.4, "-1", -1.5, "-1", -2.5, "-2", -1.6, "-1");
+ assertFormatted("0;; ro=un", 2, "2");
+ try {
+ assertFormatted("0;; ro=un", 2.5, "2");
+ fail();
+ } catch (ArithmeticException e) {
+ // Expected
+ }
+
+ assertFormatted("0.##;; mul=100", 12.345, "1234.5");
+ assertFormatted("0.##;; mul=1000", 12.345, "12345");
+
+ assertFormatted(",##0.##;; grp=_ dec=D", 12345.1, "12_345D1", 1, "1");
+
+ assertFormatted("0.##E0;; exp='*10^'", 12345.1, "1.23*10^4");
+
+ assertFormatted("0.##;; min=m", -1, "m1", 1, "1");
+
+ assertFormatted("0.##;; inf=foo", Double.POSITIVE_INFINITY, "foo", Double.NEGATIVE_INFINITY, "-foo");
+
+ assertFormatted("0.##;; nan=foo", Double.NaN, "foo");
+
+ assertFormatted("0%;; prc='c'", 0.75, "75c");
+
+ assertFormatted("0\u2030;; prm='m'", 0.75, "750m");
+
+ assertFormatted("0.00;; zero='@'", 10.5, "A@.E@");
+ }
+
+ @Test
+ public void testLocale() throws ParseException {
+ assertEquals("1000.0", ExtendedDecimalFormatParser.parse("0.0", Locale.US).format(1000));
+ assertEquals("1000,0", ExtendedDecimalFormatParser.parse("0.0", Locale.FRANCE).format(1000));
+ assertEquals("1_000.0", ExtendedDecimalFormatParser.parse(",000.0;;grp=_", Locale.US).format(1000));
+ assertEquals("1_000,0", ExtendedDecimalFormatParser.parse(",000.0;;grp=_", Locale.FRANCE).format(1000));
+ }
+
+
+ private void assertFormatted(String formatString, Object... numberAndExpectedOutput) throws ParseException {
+ if (numberAndExpectedOutput.length % 2 != 0) {
+ throw new IllegalArgumentException();
+ }
+
+ DecimalFormat df = ExtendedDecimalFormatParser.parse(formatString, LOC);
+ Number num = null;
+ for (int i = 0; i < numberAndExpectedOutput.length; i++) {
+ if (i % 2 == 0) {
+ num = (Number) numberAndExpectedOutput[i];
+ } else {
+ assertEquals(numberAndExpectedOutput[i], df.format(num));
+ }
+ }
+ }
+
+ private void assertFormatsEquivalent(DecimalFormat dfExpected, DecimalFormat dfActual) {
+ for (int signum : new int[] { 1, -1 }) {
+ assertFormatsEquivalent(dfExpected, dfActual, signum * 0);
+ assertFormatsEquivalent(dfExpected, dfActual, signum * 0.5);
+ assertFormatsEquivalent(dfExpected, dfActual, signum * 0.25);
+ assertFormatsEquivalent(dfExpected, dfActual, signum * 0.125);
+ assertFormatsEquivalent(dfExpected, dfActual, signum * 1);
+ assertFormatsEquivalent(dfExpected, dfActual, signum * 10);
+ assertFormatsEquivalent(dfExpected, dfActual, signum * 100);
+ assertFormatsEquivalent(dfExpected, dfActual, signum * 1000);
+ assertFormatsEquivalent(dfExpected, dfActual, signum * 10000);
+ assertFormatsEquivalent(dfExpected, dfActual, signum * 100000);
+ }
+ }
+
+ private void assertFormatsEquivalent(DecimalFormat dfExpected, DecimalFormat dfActual, double n) {
+ assertEquals(dfExpected.format(n), dfActual.format(n));
+ }
+
+}