From 90cc2c46778e8b17eead64b661fb10f31a2a813e Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Fri, 3 Nov 2017 00:32:16 +0300 Subject: [PATCH] Add implementation of java.text.MessageFormat --- .../classlib/java/text/TChoiceFormat.java | 241 +++++++ .../classlib/java/text/TDecimalFormat.java | 6 + .../classlib/java/text/TMessageFormat.java | 651 ++++++++++++++++++ .../classlib/java/text/MessageFormatTest.java | 612 ++++++++++++++++ 4 files changed, 1510 insertions(+) create mode 100644 classlib/src/main/java/org/teavm/classlib/java/text/TChoiceFormat.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/text/TMessageFormat.java create mode 100644 tests/src/test/java/org/teavm/classlib/java/text/MessageFormatTest.java diff --git a/classlib/src/main/java/org/teavm/classlib/java/text/TChoiceFormat.java b/classlib/src/main/java/org/teavm/classlib/java/text/TChoiceFormat.java new file mode 100644 index 000000000..c0b13fd63 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/text/TChoiceFormat.java @@ -0,0 +1,241 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * Licensed 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 org.teavm.classlib.java.text; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.teavm.classlib.java.util.TLocale; + +public class TChoiceFormat extends TNumberFormat { + private double[] choiceLimits; + private String[] choiceFormats; + + public TChoiceFormat(double[] limits, String[] formats) { + setChoices(limits, formats); + } + + public TChoiceFormat(String template) { + applyPattern(template); + } + + public void applyPattern(String template) { + double[] limits = new double[5]; + List formats = new ArrayList<>(); + int length = template.length(); + int limitCount = 0; + int index = 0; + StringBuffer buffer = new StringBuffer(); + TNumberFormat format = TNumberFormat.getInstance(TLocale.US); + TParsePosition position = new TParsePosition(0); + while (true) { + index = skipWhitespace(template, index); + if (index >= length) { + if (limitCount == limits.length) { + choiceLimits = limits; + } else { + choiceLimits = new double[limitCount]; + System.arraycopy(limits, 0, choiceLimits, 0, limitCount); + } + choiceFormats = new String[formats.size()]; + for (int i = 0; i < formats.size(); i++) { + choiceFormats[i] = formats.get(i); + } + return; + } + + position.setIndex(index); + Number value = format.parse(template, position); + index = skipWhitespace(template, position.getIndex()); + if (position.getErrorIndex() != -1 || index >= length) { + // Fix Harmony 540 + choiceLimits = new double[0]; + choiceFormats = new String[0]; + return; + } + char ch = template.charAt(index++); + if (limitCount == limits.length) { + double[] newLimits = new double[limitCount * 2]; + System.arraycopy(limits, 0, newLimits, 0, limitCount); + limits = newLimits; + } + double next; + switch (ch) { + case '#': + case '\u2264': + next = value.doubleValue(); + break; + case '<': + next = nextDouble(value.doubleValue()); + break; + default: + throw new IllegalArgumentException(); + } + if (limitCount > 0 && next <= limits[limitCount - 1]) { + throw new IllegalArgumentException(); + } + buffer.setLength(0); + position.setIndex(index); + upTo(template, position, buffer, '|'); + index = position.getIndex(); + limits[limitCount++] = next; + formats.add(buffer.toString()); + } + } + + @Override + public Object clone() { + TChoiceFormat clone = (TChoiceFormat) super.clone(); + clone.choiceLimits = choiceLimits.clone(); + clone.choiceFormats = choiceFormats.clone(); + return clone; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof TChoiceFormat)) { + return false; + } + TChoiceFormat choice = (TChoiceFormat) object; + return Arrays.equals(choiceLimits, choice.choiceLimits) + && Arrays.equals(choiceFormats, choice.choiceFormats); + } + + @Override + public StringBuffer format(double value, StringBuffer buffer, TFieldPosition field) { + for (int i = choiceLimits.length - 1; i >= 0; i--) { + if (choiceLimits[i] <= value) { + return buffer.append(choiceFormats[i]); + } + } + return choiceFormats.length == 0 ? buffer : buffer.append(choiceFormats[0]); + } + + @Override + public StringBuffer format(long value, StringBuffer buffer, TFieldPosition field) { + return format((double) value, buffer, field); + } + + public Object[] getFormats() { + return choiceFormats; + } + + public double[] getLimits() { + return choiceLimits; + } + + @Override + public int hashCode() { + int hashCode = 0; + for (int i = 0; i < choiceLimits.length; i++) { + long v = Double.doubleToLongBits(choiceLimits[i]); + hashCode += (int) (v ^ (v >>> 32)) + choiceFormats[i].hashCode(); + } + return hashCode; + } + + public static double nextDouble(double value) { + if (value == Double.POSITIVE_INFINITY) { + return value; + } + long bits; + // Handle -0.0 + if (value == 0) { + bits = 0; + } else { + bits = Double.doubleToLongBits(value); + } + return Double.longBitsToDouble(value < 0 ? bits - 1 : bits + 1); + } + + public static double nextDouble(double value, boolean increment) { + return increment ? nextDouble(value) : previousDouble(value); + } + + @Override + public Number parse(String string, TParsePosition position) { + int offset = position.getIndex(); + for (int i = 0; i < choiceFormats.length; i++) { + if (string.startsWith(choiceFormats[i], offset)) { + position.setIndex(offset + choiceFormats[i].length()); + return choiceLimits[i]; + } + } + position.setErrorIndex(offset); + return Double.NaN; + } + + public static double previousDouble(double value) { + if (value == Double.NEGATIVE_INFINITY) { + return value; + } + long bits; + // Handle 0.0 + if (value == 0) { + bits = 0x8000000000000000L; + } else { + bits = Double.doubleToLongBits(value); + } + return Double.longBitsToDouble(value <= 0 ? bits + 1 : bits - 1); + } + + public void setChoices(double[] limits, String[] formats) { + if (limits.length != formats.length) { + throw new IllegalArgumentException(); + } + choiceLimits = limits; + choiceFormats = formats; + } + + private int skipWhitespace(String string, int index) { + int length = string.length(); + while (index < length && Character.isWhitespace(string.charAt(index))) { + index++; + } + return index; + } + + public String toPattern() { + StringBuilder buffer = new StringBuilder(); + for (int i = 0; i < choiceLimits.length; i++) { + if (i != 0) { + buffer.append('|'); + } + String previous = String.valueOf(previousDouble(choiceLimits[i])); + String limit = String.valueOf(choiceLimits[i]); + if (previous.length() < limit.length()) { + buffer.append(previous); + buffer.append('<'); + } else { + buffer.append(limit); + buffer.append('#'); + } + boolean quote = choiceFormats[i].indexOf('|') != -1; + if (quote) { + buffer.append('\''); + } + buffer.append(choiceFormats[i]); + if (quote) { + buffer.append('\''); + } + } + return buffer.toString(); + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/text/TDecimalFormat.java b/classlib/src/main/java/org/teavm/classlib/java/text/TDecimalFormat.java index 901117bcf..185b30082 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/text/TDecimalFormat.java +++ b/classlib/src/main/java/org/teavm/classlib/java/text/TDecimalFormat.java @@ -47,6 +47,7 @@ public class TDecimalFormat extends TNumberFormat { private boolean decimalSeparatorAlwaysShown; private boolean parseBigDecimal; int exponentDigits; + String pattern; public TDecimalFormat() { this(CLDRHelper.resolveNumberFormat(TLocale.getDefault().getLanguage(), TLocale.getDefault().getCountry())); @@ -65,6 +66,11 @@ public class TDecimalFormat extends TNumberFormat { TDecimalFormatParser parser = new TDecimalFormatParser(); parser.parse(pattern); parser.apply(this); + this.pattern = pattern; + } + + String toPattern() { + return pattern; } public DecimalFormatSymbols getDecimalFormatSymbols() { diff --git a/classlib/src/main/java/org/teavm/classlib/java/text/TMessageFormat.java b/classlib/src/main/java/org/teavm/classlib/java/text/TMessageFormat.java new file mode 100644 index 000000000..357cbfbef --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/text/TMessageFormat.java @@ -0,0 +1,651 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * Licensed 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 org.teavm.classlib.java.text; + +import java.text.DateFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import org.teavm.classlib.java.util.TIterator; +import org.teavm.classlib.java.util.TLocale; + +public class TMessageFormat extends TFormat { + private TLocale locale = TLocale.getDefault(); + transient private String[] strings; + private int[] argumentNumbers; + private TFormat[] formats; + private int maxOffset; + transient private int maxArgumentIndex; + + public TMessageFormat(String template, TLocale locale) { + this.locale = locale; + applyPattern(template); + } + + public TMessageFormat(String template) { + applyPattern(template); + } + + public void applyPattern(String template) { + int length = template.length(); + StringBuffer buffer = new StringBuffer(); + TParsePosition position = new TParsePosition(0); + List localStrings = new ArrayList<>(); + int argCount = 0; + int[] args = new int[10]; + int maxArg = -1; + List localFormats = new ArrayList<>(); + while (position.getIndex() < length) { + if (TFormat.upTo(template, position, buffer, '{')) { + int arg = 0; + int offset = position.getIndex(); + if (offset >= length) { + throw new IllegalArgumentException("Invalid argument number"); + } + // Get argument number + while (true) { + char ch = template.charAt(offset++); + if (ch == '}' || ch == ',') { + break; + } + if (ch < '0' && ch > '9') { + throw new IllegalArgumentException("Invalid argument number"); + } + arg = arg * 10 + (ch - '0'); + if (arg < 0 || offset >= length) { + throw new IllegalArgumentException("Invalid argument number"); + } + } + offset--; + position.setIndex(offset); + localFormats.add(parseVariable(template, position)); + if (argCount >= args.length) { + int[] newArgs = new int[args.length * 2]; + System.arraycopy(args, 0, newArgs, 0, args.length); + args = newArgs; + } + args[argCount++] = arg; + if (arg > maxArg) { + maxArg = arg; + } + } + localStrings.add(buffer.toString()); + buffer.setLength(0); + } + this.strings = new String[localStrings.size()]; + for (int i = 0; i < localStrings.size(); i++) { + this.strings[i] = localStrings.get(i); + } + argumentNumbers = args; + this.formats = new TFormat[argCount]; + for (int i = 0; i < argCount; i++) { + this.formats[i] = localFormats.get(i); + } + maxOffset = argCount - 1; + maxArgumentIndex = maxArg; + } + + @Override + public Object clone() { + TMessageFormat clone = (TMessageFormat) super.clone(); + TFormat[] array = new TFormat[formats.length]; + for (int i = formats.length; --i >= 0;) { + if (formats[i] != null) { + array[i] = (TFormat) formats[i].clone(); + } + } + clone.formats = array; + return clone; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof TMessageFormat)) { + return false; + } + TMessageFormat format = (TMessageFormat) object; + if (maxOffset != format.maxOffset) { + return false; + } + // Must use a loop since the lengths may be different due + // to serialization cross-loading + for (int i = 0; i <= maxOffset; i++) { + if (argumentNumbers[i] != format.argumentNumbers[i]) { + return false; + } + } + return locale.equals(format.locale) + && Arrays.equals(strings, format.strings) + && Arrays.equals(formats, format.formats); + } + + @Override + public TAttributedCharacterIterator formatToCharacterIterator(Object object) { + if (object == null) { + throw new NullPointerException(); + } + + StringBuffer buffer = new StringBuffer(); + List fields = new ArrayList<>(); + + // format the message, and find fields + formatImpl((Object[]) object, buffer, new TFieldPosition(0), fields); + + // create an AttributedString with the formatted buffer + TAttributedString as = new TAttributedString(buffer.toString()); + + // add TMessageFormat field attributes and values to the AttributedString + for (FieldContainer fc : fields) { + as.addAttribute(fc.attribute, fc.value, fc.start, fc.end); + } + + // return the CharacterIterator from AttributedString + return as.getIterator(); + } + + public final StringBuffer format(Object[] objects, StringBuffer buffer, TFieldPosition field) { + return formatImpl(objects, buffer, field, null); + } + + private StringBuffer formatImpl(Object[] objects, StringBuffer buffer, TFieldPosition position, + List fields) { + TFieldPosition passedField = new TFieldPosition(0); + for (int i = 0; i <= maxOffset; i++) { + buffer.append(strings[i]); + int begin = buffer.length(); + Object arg; + if (objects != null && argumentNumbers[i] < objects.length) { + arg = objects[argumentNumbers[i]]; + } else { + buffer.append('{'); + buffer.append(argumentNumbers[i]); + buffer.append('}'); + handleArgumentField(begin, buffer.length(), argumentNumbers[i], position, fields); + continue; + } + TFormat format = formats[i]; + if (format == null || arg == null) { + if (arg instanceof Number) { + format = TNumberFormat.getInstance(); + } else if (arg instanceof Date) { + format = TDateFormat.getInstance(); + } else { + buffer.append(arg); + handleArgumentField(begin, buffer.length(), argumentNumbers[i], position, fields); + continue; + } + } + if (format instanceof TChoiceFormat) { + String result = format.format(arg); + TMessageFormat mf = new TMessageFormat(result); + mf.setLocale(locale); + mf.format(objects, buffer, passedField); + handleArgumentField(begin, buffer.length(), argumentNumbers[i], position, fields); + handleformat(format, arg, begin, fields); + } else { + format.format(arg, buffer, passedField); + handleArgumentField(begin, buffer.length(), argumentNumbers[i], position, fields); + handleformat(format, arg, begin, fields); + } + } + if (maxOffset + 1 < strings.length) { + buffer.append(strings[maxOffset + 1]); + } + return buffer; + } + + private void handleArgumentField(int begin, int end, int argnumber, TFieldPosition position, + List fields) { + if (fields != null) { + fields.add(new FieldContainer(begin, end, Field.ARGUMENT, argnumber)); + } else { + if (position != null + && position.getFieldAttribute() == Field.ARGUMENT + && position.getEndIndex() == 0) { + position.setBeginIndex(begin); + position.setEndIndex(end); + } + } + } + + /** + * An inner class to store attributes, values, start and end indices. + * Instances of this inner class are used as elements for the fields vector + */ + static class FieldContainer { + int start; + int end; + TAttributedCharacterIterator.Attribute attribute; + Object value; + + FieldContainer(int start, int end, TAttributedCharacterIterator.Attribute attribute, Object value) { + this.start = start; + this.end = end; + this.attribute = attribute; + this.value = value; + } + } + + private void handleformat(TFormat format, Object arg, int begin, List fields) { + if (fields != null) { + TAttributedCharacterIterator iterator = format.formatToCharacterIterator(arg); + while (iterator.getIndex() != iterator.getEndIndex()) { + int start = iterator.getRunStart(); + int end = iterator.getRunLimit(); + + TIterator iter = iterator.getAttributes().keySet().iterator(); + while (iter.hasNext()) { + TAttributedCharacterIterator.Attribute attribute = iter.next(); + Object value = iterator.getAttribute(attribute); + fields.add(new FieldContainer(begin + start, begin + end, attribute, value)); + } + iterator.setIndex(end); + } + } + } + + @Override + public final StringBuffer format(Object object, StringBuffer buffer, TFieldPosition field) { + return format((Object[]) object, buffer, field); + } + + public static String format(String template, Object... objects) { + if (objects != null) { + for (int i = 0; i < objects.length; i++) { + if (objects[i] == null) { + objects[i] = "null"; + } + } + } + return new TMessageFormat(template).format(objects, new StringBuffer(), new TFieldPosition(0)).toString(); + } + + public TFormat[] getFormats() { + return formats.clone(); + } + + public TFormat[] getFormatsByArgumentIndex() { + TFormat[] answer = new TFormat[maxArgumentIndex + 1]; + for (int i = 0; i < maxOffset + 1; i++) { + answer[argumentNumbers[i]] = formats[i]; + } + return answer; + } + + public void setFormatByArgumentIndex(int argIndex, TFormat format) { + for (int i = 0; i < maxOffset + 1; i++) { + if (argumentNumbers[i] == argIndex) { + formats[i] = format; + } + } + } + + public void setFormatsByArgumentIndex(TFormat[] formats) { + for (int j = 0; j < formats.length; j++) { + for (int i = 0; i < maxOffset + 1; i++) { + if (argumentNumbers[i] == j) { + this.formats[i] = formats[j]; + } + } + } + } + + public TLocale getLocale() { + return locale; + } + + @Override + public int hashCode() { + int hashCode = 0; + for (int i = 0; i <= maxOffset; i++) { + hashCode += argumentNumbers[i] + strings[i].hashCode(); + if (formats[i] != null) { + hashCode += formats[i].hashCode(); + } + } + if (maxOffset + 1 < strings.length) { + hashCode += strings[maxOffset + 1].hashCode(); + } + if (locale != null) { + return hashCode + locale.hashCode(); + } + return hashCode; + } + + public Object[] parse(String string) throws ParseException { + TParsePosition position = new TParsePosition(0); + Object[] result = parse(string, position); + if (position.getIndex() == 0) { + throw new ParseException("MessageFormat.parseObject(String) parse failure", position.getErrorIndex()); + } + return result; + } + + public Object[] parse(String string, TParsePosition position) { + if (string == null) { + return new Object[0]; + } + TParsePosition internalPos = new TParsePosition(0); + int offset = position.getIndex(); + Object[] result = new Object[maxArgumentIndex + 1]; + for (int i = 0; i <= maxOffset; i++) { + String sub = strings[i]; + if (!string.startsWith(sub, offset)) { + position.setErrorIndex(offset); + return null; + } + offset += sub.length(); + Object parse; + TFormat format = formats[i]; + if (format == null) { + if (i + 1 < strings.length) { + int next = string.indexOf(strings[i + 1], offset); + if (next == -1) { + position.setErrorIndex(offset); + return null; + } + parse = string.substring(offset, next); + offset = next; + } else { + parse = string.substring(offset); + offset = string.length(); + } + } else { + internalPos.setIndex(offset); + parse = format.parseObject(string, internalPos); + if (internalPos.getErrorIndex() != -1) { + position.setErrorIndex(offset); + return null; + } + offset = internalPos.getIndex(); + } + result[argumentNumbers[i]] = parse; + } + if (maxOffset + 1 < strings.length) { + String sub = strings[maxOffset + 1]; + if (!string.startsWith(sub, offset)) { + position.setErrorIndex(offset); + return null; + } + offset += sub.length(); + } + position.setIndex(offset); + return result; + } + + @Override + public Object parseObject(String string, TParsePosition position) { + return parse(string, position); + } + + private int match(String string, TParsePosition position, boolean last, String[] tokens) { + int length = string.length(); + int offset = position.getIndex(); + int token = -1; + while (offset < length && Character.isWhitespace(string.charAt(offset))) { + offset++; + } + for (int i = tokens.length; --i >= 0;) { + if (string.regionMatches(true, offset, tokens[i], 0, tokens[i].length())) { + token = i; + break; + } + } + if (token == -1) { + return -1; + } + offset += tokens[token].length(); + while (offset < length && Character.isWhitespace(string.charAt(offset))) { + offset++; + } + char ch; + if (offset < length) { + ch = string.charAt(offset); + if (ch == '}' || (!last && ch == ',')) { + position.setIndex(offset + 1); + return token; + } + } + return -1; + } + + private TFormat parseVariable(String string, TParsePosition position) { + int length = string.length(); + int offset = position.getIndex(); + char ch; + if (offset >= length) { + throw new IllegalArgumentException("Missing element format"); + } + + ch = string.charAt(offset++); + if (ch != '}' && ch != ',') { + throw new IllegalArgumentException("Missing element format"); + } + + position.setIndex(offset); + if (ch == '}') { + return null; + } + int type = match(string, position, false, new String[] { "time", "date", "number", "choice" }); + if (type == -1) { + throw new IllegalArgumentException("Unknown element format"); + } + StringBuffer buffer = new StringBuffer(); + ch = string.charAt(position.getIndex() - 1); + switch (type) { + case 0: // time + case 1: // date + if (ch == '}') { + return type == 1 + ? TDateFormat.getDateInstance(DateFormat.DEFAULT, locale) + : TDateFormat.getTimeInstance(DateFormat.DEFAULT, locale); + } + int dateStyle = match(string, position, true, new String[] { "full", "long", "medium", "short" }); + if (dateStyle == -1) { + TFormat.upToWithQuotes(string, position, buffer, '}', '{'); + return new TSimpleDateFormat(buffer.toString(), locale); + } + switch (dateStyle) { + case 0: + dateStyle = DateFormat.FULL; + break; + case 1: + dateStyle = DateFormat.LONG; + break; + case 2: + dateStyle = DateFormat.MEDIUM; + break; + case 3: + dateStyle = DateFormat.SHORT; + break; + } + return type == 1 + ? TDateFormat.getDateInstance(dateStyle, locale) + : TDateFormat.getTimeInstance(dateStyle, locale); + case 2: // number + if (ch == '}') { + return TNumberFormat.getInstance(); + } + int numberStyle = match(string, position, true, new String[] { "currency", "percent", "integer" }); + if (numberStyle == -1) { + upToWithQuotes(string, position, buffer, '}', '{'); + return new TDecimalFormat(buffer.toString(), new TDecimalFormatSymbols(locale)); + } + switch (numberStyle) { + case 0: // currency + return TNumberFormat.getCurrencyInstance(locale); + case 1: // percent + return TNumberFormat.getPercentInstance(locale); + } + return TNumberFormat.getIntegerInstance(locale); + } + // choice + try { + upToWithQuotes(string, position, buffer, '}', '{'); + } catch (IllegalArgumentException e) { + // ignored + } + return new TChoiceFormat(buffer.toString()); + } + + public void setFormat(int offset, TFormat format) { + formats[offset] = format; + } + + public void setFormats(TFormat[] formats) { + int min = this.formats.length; + if (formats.length < min) { + min = formats.length; + } + for (int i = 0; i < min; i++) { + this.formats[i] = formats[i]; + } + } + + public void setLocale(TLocale locale) { + this.locale = locale; + for (int i = 0; i <= maxOffset; i++) { + TFormat format = formats[i]; + if (format instanceof TDecimalFormat) { + formats[i] = new TDecimalFormat(((TDecimalFormat) format).toPattern(), + new TDecimalFormatSymbols(locale)); + } else if (format instanceof TSimpleDateFormat) { + formats[i] = new TSimpleDateFormat(((TSimpleDateFormat) format).toPattern(), locale); + } + + } + } + + private String decodeDecimalFormat(StringBuffer buffer, TFormat format) { + buffer.append(",number"); + if (format.equals(TNumberFormat.getNumberInstance(locale))) { + // Empty block + } else if (format.equals(TNumberFormat.getIntegerInstance(locale))) { + buffer.append(",integer"); + } else if (format.equals(TNumberFormat.getCurrencyInstance(locale))) { + buffer.append(",currency"); + } else if (format.equals(TNumberFormat.getPercentInstance(locale))) { + buffer.append(",percent"); + } else { + buffer.append(','); + return ((TDecimalFormat) format).toPattern(); + } + return null; + } + + private String decodeSimpleDateFormat(StringBuffer buffer, TFormat format) { + if (format.equals(TDateFormat.getTimeInstance(DateFormat.DEFAULT, locale))) { + buffer.append(",time"); + } else if (format.equals(TDateFormat.getDateInstance(DateFormat.DEFAULT, locale))) { + buffer.append(",date"); + } else if (format.equals(TDateFormat.getTimeInstance(DateFormat.SHORT, locale))) { + buffer.append(",time,short"); + } else if (format.equals(TDateFormat.getDateInstance(DateFormat.SHORT, locale))) { + buffer.append(",date,short"); + } else if (format.equals(TDateFormat.getTimeInstance(DateFormat.LONG, locale))) { + buffer.append(",time,long"); + } else if (format.equals(TDateFormat.getDateInstance(DateFormat.LONG, locale))) { + buffer.append(",date,long"); + } else if (format.equals(TDateFormat.getTimeInstance(DateFormat.FULL, locale))) { + buffer.append(",time,full"); + } else if (format.equals(TDateFormat.getDateInstance(DateFormat.FULL, locale))) { + buffer.append(",date,full"); + } else { + buffer.append(",date,"); + return ((TSimpleDateFormat) format).toPattern(); + } + return null; + } + + public String toPattern() { + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i <= maxOffset; i++) { + appendQuoted(buffer, strings[i]); + buffer.append('{'); + buffer.append(argumentNumbers[i]); + TFormat format = formats[i]; + String pattern = null; + if (format instanceof TChoiceFormat) { + buffer.append(",choice,"); + pattern = ((TChoiceFormat) format).toPattern(); + } else if (format instanceof TDecimalFormat) { + pattern = decodeDecimalFormat(buffer, format); + } else if (format instanceof TSimpleDateFormat) { + pattern = decodeSimpleDateFormat(buffer, format); + } else if (format != null) { + throw new IllegalArgumentException("Unknown format"); + } + if (pattern != null) { + boolean quote = false; + int index = 0; + int length = pattern.length(); + int count = 0; + while (index < length) { + char ch = pattern.charAt(index++); + if (ch == '\'') { + quote = !quote; + } + if (!quote) { + if (ch == '{') { + count++; + } + if (ch == '}') { + if (count > 0) { + count--; + } else { + buffer.append("'}"); + ch = '\''; + } + } + } + buffer.append(ch); + } + } + buffer.append('}'); + } + if (maxOffset + 1 < strings.length) { + appendQuoted(buffer, strings[maxOffset + 1]); + } + return buffer.toString(); + } + + private void appendQuoted(StringBuffer buffer, String string) { + int length = string.length(); + for (int i = 0; i < length; i++) { + char ch = string.charAt(i); + if (ch == '{' || ch == '}') { + buffer.append('\''); + buffer.append(ch); + buffer.append('\''); + } else { + buffer.append(ch); + } + } + } + + public static class Field extends TFormat.Field { + public static final Field ARGUMENT = new Field("message argument field"); + + protected Field(String fieldName) { + super(fieldName); + } + } +} diff --git a/tests/src/test/java/org/teavm/classlib/java/text/MessageFormatTest.java b/tests/src/test/java/org/teavm/classlib/java/text/MessageFormatTest.java new file mode 100644 index 000000000..a0301f809 --- /dev/null +++ b/tests/src/test/java/org/teavm/classlib/java/text/MessageFormatTest.java @@ -0,0 +1,612 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * Licensed 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 org.teavm.classlib.java.text; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import java.text.ChoiceFormat; +import java.text.DateFormat; +import java.text.FieldPosition; +import java.text.Format; +import java.text.MessageFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.junit.TeaVMProperties; +import org.teavm.junit.TeaVMProperty; +import org.teavm.junit.TeaVMTestRunner; + +@RunWith(TeaVMTestRunner.class) +@TeaVMProperties(value = { @TeaVMProperty(key = "java.util.Locale.available", value = "en, en_US, fr") }) +public class MessageFormatTest { + private MessageFormat format1; + private MessageFormat format2; + private MessageFormat format3; + private Locale defaultLocale; + + public MessageFormatTest() { + defaultLocale = Locale.getDefault(); + Locale.setDefault(Locale.US); + + // test with repeating formats and max argument index < max offset + String pattern = "A {3, number, currency} B {2, time} C {0, number, percent} D {4} " + + "E {1,choice,0#off|1#on} F {0, date}"; + format1 = new MessageFormat(pattern); + + // test with max argument index > max offset + pattern = "A {3, number, currency} B {8, time} C {0, number, percent} D {6} " + + "E {1,choice,0#off|1#on} F {0, date}"; + format2 = new MessageFormat(pattern); + + // test with argument number being zero + pattern = "A B C D E F"; + format3 = new MessageFormat(pattern); + } + + @Test + public void constructorLjava_lang_StringLjava_util_Locale() { + // Test for method java.text.MessageFormat(java.lang.String, + // java.util.Locale) + Locale mk = new Locale("mk", "MK"); + MessageFormat format = new MessageFormat( + "Date: {0,date} Currency: {1, number, currency} Integer: {2, number, integer}", + mk); + + assertTrue("Wrong locale1", format.getLocale().equals(mk)); + assertTrue("Wrong locale2", format.getFormats()[0].equals(DateFormat.getDateInstance(DateFormat.DEFAULT, mk))); + assertTrue("Wrong locale3", format.getFormats()[1].equals(NumberFormat.getCurrencyInstance(mk))); + assertTrue("Wrong locale4", format.getFormats()[2].equals(NumberFormat.getIntegerInstance(mk))); + } + + @Test + public void constructorLjava_lang_String() { + // Test for method java.text.MessageFormat(java.lang.String) + MessageFormat format = new MessageFormat( + "abc {4,time} def {3,date} ghi {2,number} jkl {1,choice,0#low|1#high} mnop {0}"); + assertTrue("Not a MessageFormat", format.getClass() == MessageFormat.class); + Format[] formats = format.getFormats(); + assertNotNull("null formats", formats); + assertTrue("Wrong format count: " + formats.length, formats.length >= 5); + assertTrue("Wrong time format", formats[0].equals(DateFormat.getTimeInstance())); + assertTrue("Wrong date format", formats[1].equals(DateFormat.getDateInstance())); + assertTrue("Wrong number format", formats[2].equals(NumberFormat.getInstance())); + assertTrue("Wrong choice format", formats[3].equals(new ChoiceFormat("0.0#low|1.0#high"))); + assertNull("Wrong string format", formats[4]); + + Date date = new Date(); + FieldPosition pos = new FieldPosition(-1); + StringBuffer buffer = new StringBuffer(); + format.format(new Object[] { "123", 1.6, 7.2, date, date }, buffer, pos); + String result = buffer.toString(); + buffer.setLength(0); + buffer.append("abc "); + buffer.append(DateFormat.getTimeInstance().format(date)); + buffer.append(" def "); + buffer.append(DateFormat.getDateInstance().format(date)); + buffer.append(" ghi "); + buffer.append(NumberFormat.getInstance().format(new Double(7.2))); + buffer.append(" jkl high mnop 123"); + assertTrue("Wrong answer:\n" + result + "\n" + buffer, result.equals(buffer.toString())); + + assertEquals("Simple string", "Test message", new MessageFormat("Test message").format(new Object[0])); + + result = new MessageFormat("Don't").format(new Object[0]); + assertTrue("Should not throw IllegalArgumentException: " + result, "Dont".equals(result)); + + try { + new MessageFormat("Invalid {1,foobar} format descriptor!"); + fail("Expected test_ConstructorLjava_lang_String to throw IAE."); + } catch (IllegalArgumentException ex) { + // expected + } + + try { + new MessageFormat("Invalid {1,date,invalid-spec} format descriptor!"); + } catch (IllegalArgumentException ex) { + // expected + } + + // Regression for HARMONY-65 + try { + new MessageFormat("{0,number,integer"); + fail("Assert 0: Failed to detect unmatched brackets."); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void applyPatternLjava_lang_String() { + MessageFormat format = new MessageFormat("test"); + format.applyPattern("xx {0}"); + assertEquals("Invalid number", "xx 46", format.format(new Object[] { 46 })); + Date date = new Date(); + String result = format.format(new Object[] { date }); + String expected = "xx " + DateFormat.getInstance().format(date); + assertTrue("Invalid date:\n" + result + "\n" + expected, result.equals(expected)); + format = new MessageFormat("{0,date}{1,time}{2,number,integer}"); + format.applyPattern("nothing"); + assertEquals("Found formats", "nothing", format.toPattern()); + + format.applyPattern("{0}"); + assertNull("Wrong format", format.getFormats()[0]); + assertEquals("Wrong pattern", "{0}", format.toPattern()); + + format.applyPattern("{0, \t\u001ftime }"); + assertTrue("Wrong time format", format.getFormats()[0].equals(DateFormat.getTimeInstance())); + assertEquals("Wrong time pattern", "{0,time}", format.toPattern()); + format.applyPattern("{0,Time, Short\n}"); + assertTrue("Wrong short time format", format.getFormats()[0].equals( + DateFormat.getTimeInstance(DateFormat.SHORT))); + assertEquals("Wrong short time pattern", "{0,time,short}", format.toPattern()); + format.applyPattern("{0,TIME,\nmedium }"); + assertTrue("Wrong medium time format", format.getFormats()[0].equals( + DateFormat.getTimeInstance(DateFormat.MEDIUM))); + assertEquals("Wrong medium time pattern", "{0,time}", format.toPattern()); + format.applyPattern("{0,time,LONG}"); + assertTrue("Wrong long time format", format.getFormats()[0].equals( + DateFormat.getTimeInstance(DateFormat.LONG))); + assertEquals("Wrong long time pattern", "{0,time,long}", format.toPattern()); + format.setLocale(Locale.FRENCH); // use French since English has the + // same LONG and FULL time patterns + format.applyPattern("{0,time, Full}"); + assertTrue("Wrong full time format", format.getFormats()[0] + .equals(DateFormat.getTimeInstance(DateFormat.FULL, Locale.FRENCH))); + assertEquals("Wrong full time pattern", "{0,time,full}", format.toPattern()); + format.setLocale(Locale.getDefault()); + + format.applyPattern("{0, date}"); + assertTrue("Wrong date format", format.getFormats()[0].equals(DateFormat.getDateInstance())); + assertEquals("Wrong date pattern", "{0,date}", format.toPattern()); + format.applyPattern("{0, date, short}"); + assertTrue("Wrong short date format", format.getFormats()[0] + .equals(DateFormat.getDateInstance(DateFormat.SHORT))); + assertEquals("Wrong short date pattern", "{0,date,short}", format.toPattern()); + format.applyPattern("{0, date, medium}"); + assertTrue("Wrong medium date format", format.getFormats()[0] + .equals(DateFormat.getDateInstance(DateFormat.MEDIUM))); + assertEquals("Wrong medium date pattern", + "{0,date}", format.toPattern()); + format.applyPattern("{0, date, long}"); + assertTrue("Wrong long date format", format.getFormats()[0] + .equals(DateFormat.getDateInstance(DateFormat.LONG))); + assertEquals("Wrong long date pattern", "{0,date,long}", format.toPattern()); + format.applyPattern("{0, date, full}"); + assertTrue("Wrong full date format", format.getFormats()[0] + .equals(DateFormat.getDateInstance(DateFormat.FULL))); + assertEquals("Wrong full date pattern", "{0,date,full}", format.toPattern()); + + format.applyPattern("{0, date, MMM d {hh:mm:ss}}"); + assertEquals("Wrong time/date format", " MMM d {hh:mm:ss}", ((SimpleDateFormat) (format + .getFormats()[0])).toPattern()); + assertEquals("Wrong time/date pattern", + "{0,date, MMM d {hh:mm:ss}}", format.toPattern()); + + format.applyPattern("{0, number}"); + assertTrue("Wrong number format", format.getFormats()[0].equals(NumberFormat.getNumberInstance())); + assertEquals("Wrong number pattern", "{0,number}", format.toPattern()); + format.applyPattern("{0, number, currency}"); + assertTrue("Wrong currency number format", format.getFormats()[0].equals(NumberFormat.getCurrencyInstance())); + assertEquals("Wrong currency number pattern", "{0,number,currency}", format.toPattern()); + format.applyPattern("{0, number, percent}"); + assertTrue("Wrong percent number format", format.getFormats()[0].equals(NumberFormat.getPercentInstance())); + assertEquals("Wrong percent number pattern", "{0,number,percent}", format.toPattern()); + format.applyPattern("{0, number, integer}"); + NumberFormat nf = NumberFormat.getInstance(); + nf.setMaximumFractionDigits(0); + nf.setParseIntegerOnly(true); + assertTrue("Wrong integer number format", format.getFormats()[0].equals(nf)); + assertEquals("Wrong integer number pattern", "{0,number,integer}", format.toPattern()); + + format.applyPattern("{0, number, {'#'}##0.0E0}"); + + /* + * TODO validate these assertions + * String actual = ((DecimalFormat)(format.getFormats()[0])).toPattern(); + * assertEquals("Wrong pattern number format", "' {#}'##0.0E0", actual); + * assertEquals("Wrong pattern number pattern", "{0,number,' {#}'##0.0E0}", format.toPattern()); + * + */ + + format.applyPattern("{0, choice,0#no|1#one|2#{1,number}}"); + assertEquals("Wrong choice format", "0.0#no|1.0#one|2.0#{1,number}", + ((ChoiceFormat) format.getFormats()[0]).toPattern()); + assertEquals("Wrong choice pattern", "{0,choice,0.0#no|1.0#one|2.0#{1,number}}", format.toPattern()); + assertEquals("Wrong formatted choice", "3.6", format.format(new Object[] { 2, 3.6f })); + + try { + format.applyPattern("WRONG MESSAGE FORMAT {0,number,{}"); + fail("Expected IllegalArgumentException for invalid pattern"); + } catch (IllegalArgumentException e) { + // expected + } + + // Regression for HARMONY-65 + MessageFormat mf = new MessageFormat("{0,number,integer}"); + String badpattern = "{0,number,#"; + try { + mf.applyPattern(badpattern); + fail("Assert 0: Failed to detect unmatched brackets."); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void test_clone() { + MessageFormat format = new MessageFormat("'{'choice'}'{0}"); + MessageFormat clone = (MessageFormat) format.clone(); + assertTrue("Clone not equal", format.equals(clone)); + assertEquals("Wrong answer", "{choice}{0}", format.format(new Object[] {})); + clone.setFormat(0, DateFormat.getInstance()); + assertTrue("Clone shares format data", !format.equals(clone)); + format = (MessageFormat) clone.clone(); + Format[] formats = clone.getFormats(); + ((SimpleDateFormat) formats[0]).applyPattern("adk123"); + assertTrue("Clone shares format data", !format.equals(clone)); + } + + @Test + public void test_equalsLjava_lang_Object() { + MessageFormat format1 = new MessageFormat("{0}"); + MessageFormat format2 = new MessageFormat("{1}"); + assertTrue("Should not be equal", !format1.equals(format2)); + format2.applyPattern("{0}"); + assertTrue("Should be equal", format1.equals(format2)); + SimpleDateFormat date = (SimpleDateFormat) DateFormat.getTimeInstance(); + format1.setFormat(0, DateFormat.getTimeInstance()); + format2.setFormat(0, new SimpleDateFormat(date.toPattern())); + assertTrue("Should be equal2", format1.equals(format2)); + } + + @Test + public void test_hashCode() { + assertEquals("Should be equal", 3648, new MessageFormat("rr", null).hashCode()); + } + + @Test + public void format$Ljava_lang_ObjectLjava_lang_StringBufferLjava_text_FieldPosition() { + MessageFormat format = new MessageFormat("{1,number,integer}"); + StringBuffer buffer = new StringBuffer(); + format.format(new Object[] { "0", 53.863 }, buffer, new FieldPosition(0)); + assertEquals("Wrong result", "54", buffer.toString()); + format.applyPattern("{0,choice,0#zero|1#one '{1,choice,2#two {2,time}}'}"); + Date date = new Date(); + String expected = "one two " + DateFormat.getTimeInstance().format(date); + String result = format.format(new Object[] { 1.6, 3, date }); + assertTrue("Choice not recursive:\n" + expected + "\n" + result, expected.equals(result)); + } + + @Test + public void getFormats() { + // test with repeating formats and max argument index < max offset + Format[] formats = format1.getFormats(); + Format[] correctFormats = new Format[] { + NumberFormat.getCurrencyInstance(), + DateFormat.getTimeInstance(), + NumberFormat.getPercentInstance(), null, + new ChoiceFormat("0#off|1#on"), DateFormat.getDateInstance() + }; + + assertEquals("Test1:Returned wrong number of formats:", correctFormats.length, formats.length); + for (int i = 0; i < correctFormats.length; i++) { + assertEquals("Test1:wrong format for pattern index " + i + ":", correctFormats[i], formats[i]); + } + + // test with max argument index > max offset + formats = format2.getFormats(); + correctFormats = new Format[] { NumberFormat.getCurrencyInstance(), + DateFormat.getTimeInstance(), + NumberFormat.getPercentInstance(), null, + new ChoiceFormat("0#off|1#on"), DateFormat.getDateInstance() + }; + + assertEquals("Test2:Returned wrong number of formats:", correctFormats.length, formats.length); + for (int i = 0; i < correctFormats.length; i++) { + assertEquals("Test2:wrong format for pattern index " + i + ":", correctFormats[i], formats[i]); + } + + // test with argument number being zero + formats = format3.getFormats(); + assertEquals("Test3: Returned wrong number of formats:", 0, formats.length); + } + + @Test + public void getFormatsByArgumentIndex() { + // test with repeating formats and max argument index < max offset + Format[] formats = format1.getFormatsByArgumentIndex(); + Format[] correctFormats = new Format[] { DateFormat.getDateInstance(), + new ChoiceFormat("0#off|1#on"), DateFormat.getTimeInstance(), + NumberFormat.getCurrencyInstance(), null }; + + assertEquals("Test1:Returned wrong number of formats:", + correctFormats.length, formats.length); + for (int i = 0; i < correctFormats.length; i++) { + assertEquals("Test1:wrong format for argument index " + i + ":", correctFormats[i], formats[i]); + } + + // test with max argument index > max offset + formats = format2.getFormatsByArgumentIndex(); + correctFormats = new Format[] { DateFormat.getDateInstance(), + new ChoiceFormat("0#off|1#on"), null, + NumberFormat.getCurrencyInstance(), null, null, null, null, + DateFormat.getTimeInstance() + }; + + assertEquals("Test2:Returned wrong number of formats:", correctFormats.length, formats.length); + for (int i = 0; i < correctFormats.length; i++) { + assertEquals("Test2:wrong format for argument index " + i + ":", correctFormats[i], formats[i]); + } + + // test with argument number being zero + formats = format3.getFormatsByArgumentIndex(); + assertEquals("Test3: Returned wrong number of formats:", 0, formats.length); + } + + @Test + public void setFormatByArgumentIndexILjava_text_Format() { + MessageFormat f1 = (MessageFormat) format1.clone(); + f1.setFormatByArgumentIndex(0, DateFormat.getTimeInstance()); + f1.setFormatByArgumentIndex(4, new ChoiceFormat("1#few|2#ok|3#a lot")); + + // test with repeating formats and max argument index < max offset + // compare getFormatsByArgumentIndex() results after calls to + // setFormatByArgumentIndex() + Format[] formats = f1.getFormatsByArgumentIndex(); + + Format[] correctFormats = new Format[] { DateFormat.getTimeInstance(), + new ChoiceFormat("0#off|1#on"), DateFormat.getTimeInstance(), + NumberFormat.getCurrencyInstance(), + new ChoiceFormat("1#few|2#ok|3#a lot") + }; + + assertEquals("Test1A:Returned wrong number of formats:", correctFormats.length, formats.length); + for (int i = 0; i < correctFormats.length; i++) { + assertEquals("Test1B:wrong format for argument index " + i + ":", correctFormats[i], formats[i]); + } + + // compare getFormats() results after calls to + // setFormatByArgumentIndex() + formats = f1.getFormats(); + + correctFormats = new Format[] { NumberFormat.getCurrencyInstance(), + DateFormat.getTimeInstance(), DateFormat.getTimeInstance(), + new ChoiceFormat("1#few|2#ok|3#a lot"), + new ChoiceFormat("0#off|1#on"), DateFormat.getTimeInstance() + }; + + assertEquals("Test1C:Returned wrong number of formats:", correctFormats.length, formats.length); + for (int i = 0; i < correctFormats.length; i++) { + assertEquals("Test1D:wrong format for pattern index " + i + ":", correctFormats[i], formats[i]); + } + + // test setting argumentIndexes that are not used + MessageFormat f2 = (MessageFormat) format2.clone(); + f2.setFormatByArgumentIndex(2, NumberFormat.getPercentInstance()); + f2.setFormatByArgumentIndex(4, DateFormat.getTimeInstance()); + + formats = f2.getFormatsByArgumentIndex(); + correctFormats = format2.getFormatsByArgumentIndex(); + + assertEquals("Test2A:Returned wrong number of formats:", correctFormats.length, formats.length); + for (int i = 0; i < correctFormats.length; i++) { + assertEquals("Test2B:wrong format for argument index " + i + ":", correctFormats[i], formats[i]); + } + + formats = f2.getFormats(); + correctFormats = format2.getFormats(); + + assertEquals("Test2C:Returned wrong number of formats:", correctFormats.length, formats.length); + for (int i = 0; i < correctFormats.length; i++) { + assertEquals("Test2D:wrong format for pattern index " + i + ":", correctFormats[i], formats[i]); + } + + // test exceeding the argumentIndex number + MessageFormat f3 = (MessageFormat) format3.clone(); + f3.setFormatByArgumentIndex(1, NumberFormat.getCurrencyInstance()); + + formats = f3.getFormatsByArgumentIndex(); + assertEquals("Test3A:Returned wrong number of formats:", 0, formats.length); + + formats = f3.getFormats(); + assertEquals("Test3B:Returned wrong number of formats:", 0, formats.length); + } + + @Test + public void setFormatsByArgumentIndex$Ljava_text_Format() { + MessageFormat f1 = (MessageFormat) format1.clone(); + + // test with repeating formats and max argument index < max offset + // compare getFormatsByArgumentIndex() results after calls to + // setFormatsByArgumentIndex(Format[]) + Format[] correctFormats = new Format[] { DateFormat.getTimeInstance(), + new ChoiceFormat("0#off|1#on"), DateFormat.getTimeInstance(), + NumberFormat.getCurrencyInstance(), + new ChoiceFormat("1#few|2#ok|3#a lot") + }; + + f1.setFormatsByArgumentIndex(correctFormats); + Format[] formats = f1.getFormatsByArgumentIndex(); + + assertEquals("Test1A:Returned wrong number of formats:", correctFormats.length, formats.length); + for (int i = 0; i < correctFormats.length; i++) { + assertEquals("Test1B:wrong format for argument index " + i + ":", correctFormats[i], formats[i]); + } + + // compare getFormats() results after calls to + // setFormatByArgumentIndex() + formats = f1.getFormats(); + correctFormats = new Format[] { NumberFormat.getCurrencyInstance(), + DateFormat.getTimeInstance(), DateFormat.getTimeInstance(), + new ChoiceFormat("1#few|2#ok|3#a lot"), + new ChoiceFormat("0#off|1#on"), DateFormat.getTimeInstance() + }; + + assertEquals("Test1C:Returned wrong number of formats:", correctFormats.length, formats.length); + for (int i = 0; i < correctFormats.length; i++) { + assertEquals("Test1D:wrong format for pattern index " + i + ":", correctFormats[i], formats[i]); + } + + // test setting argumentIndexes that are not used + MessageFormat f2 = (MessageFormat) format2.clone(); + Format[] inputFormats = new Format[] { DateFormat.getDateInstance(), + new ChoiceFormat("0#off|1#on"), + NumberFormat.getPercentInstance(), + NumberFormat.getCurrencyInstance(), + DateFormat.getTimeInstance(), null, null, null, + DateFormat.getTimeInstance() + }; + f2.setFormatsByArgumentIndex(inputFormats); + + formats = f2.getFormatsByArgumentIndex(); + correctFormats = format2.getFormatsByArgumentIndex(); + + assertEquals("Test2A:Returned wrong number of formats:", correctFormats.length, formats.length); + for (int i = 0; i < correctFormats.length; i++) { + assertEquals("Test2B:wrong format for argument index " + i + ":", correctFormats[i], formats[i]); + } + + formats = f2.getFormats(); + correctFormats = new Format[] { NumberFormat.getCurrencyInstance(), + DateFormat.getTimeInstance(), DateFormat.getDateInstance(), + null, new ChoiceFormat("0#off|1#on"), + DateFormat.getDateInstance() + }; + + assertEquals("Test2C:Returned wrong number of formats:", correctFormats.length, formats.length); + for (int i = 0; i < correctFormats.length; i++) { + assertEquals("Test2D:wrong format for pattern index " + i + ":", correctFormats[i], formats[i]); + } + + // test exceeding the argumentIndex number + MessageFormat f3 = (MessageFormat) format3.clone(); + f3.setFormatsByArgumentIndex(inputFormats); + + formats = f3.getFormatsByArgumentIndex(); + assertEquals("Test3A:Returned wrong number of formats:", 0, formats.length); + + formats = f3.getFormats(); + assertEquals("Test3B:Returned wrong number of formats:", 0, formats.length); + } + + @Test + public void parseLjava_lang_StringLjava_text_ParsePosition() { + MessageFormat format = new MessageFormat("date is {0,date,MMM d, yyyy}"); + ParsePosition pos = new ParsePosition(2); + Object[] result = format.parse("xxdate is Feb 28, 1999", pos); + assertTrue("No result: " + result.length, result.length >= 1); + assertTrue("Wrong answer", result[0].equals(new GregorianCalendar(1999, Calendar.FEBRUARY, 28).getTime())); + + MessageFormat mf = new MessageFormat("vm={0},{1},{2}"); + result = mf.parse("vm=win,foo,bar", new ParsePosition(0)); + assertTrue("Invalid parse", result[0].equals("win") && result[1].equals("foo") && result[2].equals("bar")); + + mf = new MessageFormat("{0}; {0}; {0}"); + String parse = "a; b; c"; + result = mf.parse(parse, new ParsePosition(0)); + assertEquals("Wrong variable result", "c", result[0]); + + mf = new MessageFormat("before {0}, after {1,number}"); + parse = "before you, after 42"; + pos.setIndex(0); + pos.setErrorIndex(8); + result = mf.parse(parse, pos); + assertEquals(2, result.length); + } + + @Test + public void setLocaleLjava_util_Locale() { + MessageFormat format = new MessageFormat("date {0,date}"); + format.setLocale(Locale.CHINA); + assertEquals("Wrong locale1", Locale.CHINA, format.getLocale()); + format.applyPattern("{1,date}"); + assertEquals("Wrong locale3", DateFormat.getDateInstance(DateFormat.DEFAULT, + Locale.CHINA), format.getFormats()[0]); + } + + @Test + public void toPattern() { + String pattern = "[{0}]"; + MessageFormat mf = new MessageFormat(pattern); + assertTrue("Wrong pattern", mf.toPattern().equals(pattern)); + + // Regression for HARMONY-59 + new MessageFormat("CHOICE {1,choice}").toPattern(); + } + + @After + public void tearDown() { + Locale.setDefault(defaultLocale); + } + + @Test + public void constructorLjava_util_Locale() { + // Regression for HARMONY-65 + try { + new MessageFormat("{0,number,integer", Locale.US); + fail("Assert 0: Failed to detect unmatched brackets."); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void parse() throws ParseException { + // Regression for HARMONY-63 + MessageFormat mf = new MessageFormat("{0,number,#,####}", Locale.US); + Object[] res = mf.parse("1,00,00"); + assertEquals("Assert 0: incorrect size of parsed data ", 1, res.length); + assertEquals("Assert 1: parsed value incorrectly", 10000L, res[0]); + } + + @Test + public void format_Object() { + // Regression for HARMONY-1875 + Locale.setDefault(Locale.CANADA); + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + String pat = "text here {0, date, yyyyyyyyy } and here"; + String etalon = "text here 000002007 and here"; + MessageFormat obj = new MessageFormat(pat); + assertEquals(etalon, obj.format(new Object[] { new Date(1198141737640L) })); + + assertEquals("{0}", MessageFormat.format("{0}", (Object[]) null)); + assertEquals("nullABC", MessageFormat.format("{0}{1}", new String[]{null, "ABC"})); + } + + @Test + public void testHARMONY5323() { + Object[] messageArgs = new Object[11]; + for (int i = 0; i < messageArgs.length; i++) { + messageArgs[i] = "dumb" + i; + } + + String res = MessageFormat.format("bgcolor=\"{10}\"", messageArgs); + assertEquals(res, "bgcolor=\"dumb10\""); + } +}