/* * 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 java.text; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import libcore.util.EmptyArray; /** * Returns a fixed string based on a numeric value. The class can be used in * conjunction with the {@link MessageFormat} class to handle plurals in * messages. {@code ChoiceFormat} enables users to attach a format to a range of * numbers. The choice is specified with an ascending list of doubles, where * each item specifies a half-open interval up to the next item as in the * following: X matches j if and only if {@code limit[j] <= X < limit[j+1]}. * <p> * If there is no match, then either the first or last index is used. The first * or last index is used depending on whether the number is too low or too high. * The length of the format array must be the same as the length of the limits * array. * <h5>Examples:</h5> * <blockquote> * * <pre> * double[] limits = {1, 2, 3, 4, 5, 6, 7}; * String[] fmts = {"Sun", "Mon", "Tue", "Wed", "Thur", "Fri", "Sat"}; * * double[] limits2 = {0, 1, ChoiceFormat.nextDouble(1)}; * String[] fmts2 = {"no files", "one file", "many files"}; * </pre> * </blockquote> * <p> * ChoiceFormat.nextDouble(double) allows to get the double following the one * passed to the method. This is used to create half open intervals. * <p> * {@code ChoiceFormat} objects also may be converted to and from patterns. * The conversion can be done programmatically, as in the example above, or * by using a pattern like the following: * <blockquote> * * <pre> * "1#Sun|2#Mon|3#Tue|4#Wed|5#Thur|6#Fri|7#Sat" * "0#are no files|1#is one file|1<are many files" * </pre> * * </blockquote> * <p> * where: * <ul> * <li><number>"#"</number> specifies an inclusive limit value;</li> * <li><number>"<"</number> specifies an exclusive limit value.</li> * </ul> */ public class ChoiceFormat extends NumberFormat { private static final long serialVersionUID = 1795184449645032964L; private double[] choiceLimits; private String[] choiceFormats; /** * Constructs a new {@code ChoiceFormat} with the specified double values * and associated strings. When calling * {@link #format(double, StringBuffer, FieldPosition) format} with a double * value {@code d}, then the element {@code i} in {@code formats} is * selected where {@code i} fulfills {@code limits[i] <= d < limits[i+1]}. * <p> * The length of the {@code limits} and {@code formats} arrays must be the * same. * * @param limits * an array of doubles in ascending order. The lowest and highest * possible values are negative and positive infinity. * @param formats * the strings associated with the ranges defined through {@code * limits}. The lower bound of the associated range is at the * same index as the string. */ public ChoiceFormat(double[] limits, String[] formats) { setChoices(limits, formats); } /** * Constructs a new {@code ChoiceFormat} with the strings and limits parsed * from the specified pattern. * * @param template * the pattern of strings and ranges. * @throws IllegalArgumentException * if an error occurs while parsing the pattern. */ public ChoiceFormat(String template) { applyPattern(template); } /** * Parses the pattern to determine new strings and ranges for this * {@code ChoiceFormat}. * * @param template * the pattern of strings and ranges. * @throws IllegalArgumentException * if an error occurs while parsing the pattern. */ public void applyPattern(String template) { double[] limits = new double[5]; List<String> formats = new ArrayList<String>(); int length = template.length(), limitCount = 0, index = 0; StringBuffer buffer = new StringBuffer(); NumberFormat format = NumberFormat.getInstance(Locale.US); ParsePosition position = new ParsePosition(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 = EmptyArray.DOUBLE; choiceFormats = EmptyArray.STRING; 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()); } } /** * Returns a new instance of {@code ChoiceFormat} with the same ranges and * strings as this {@code ChoiceFormat}. * * @return a shallow copy of this {@code ChoiceFormat}. * * @see java.lang.Cloneable */ @Override public Object clone() { ChoiceFormat clone = (ChoiceFormat) super.clone(); clone.choiceLimits = choiceLimits.clone(); clone.choiceFormats = choiceFormats.clone(); return clone; } /** * Compares the specified object with this {@code ChoiceFormat}. The object * must be an instance of {@code ChoiceFormat} and have the same limits and * formats to be equal to this instance. * * @param object * the object to compare with this instance. * @return {@code true} if the specified object is equal to this instance; * {@code false} otherwise. * @see #hashCode */ @Override public boolean equals(Object object) { if (this == object) { return true; } if (!(object instanceof ChoiceFormat)) { return false; } ChoiceFormat choice = (ChoiceFormat) object; return Arrays.equals(choiceLimits, choice.choiceLimits) && Arrays.equals(choiceFormats, choice.choiceFormats); } /** * Appends the string associated with the range in which the specified * double value fits to the specified string buffer. * * @param value * the double to format. * @param buffer * the target string buffer to append the formatted value to. * @param field * a {@code FieldPosition} which is ignored. * @return the string buffer. */ @Override public StringBuffer format(double value, StringBuffer buffer, FieldPosition 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]); } /** * Appends the string associated with the range in which the specified long * value fits to the specified string buffer. * * @param value * the long to format. * @param buffer * the target string buffer to append the formatted value to. * @param field * a {@code FieldPosition} which is ignored. * @return the string buffer. */ @Override public StringBuffer format(long value, StringBuffer buffer, FieldPosition field) { return format((double) value, buffer, field); } /** * Returns the strings associated with the ranges of this {@code * ChoiceFormat}. * * @return an array of format strings. */ public Object[] getFormats() { return choiceFormats; } /** * Returns the limits of this {@code ChoiceFormat}. * * @return the array of doubles which make up the limits of this {@code * ChoiceFormat}. */ public double[] getLimits() { return choiceLimits; } /** * Returns an integer hash code for the receiver. Objects which are equal * return the same value for this method. * * @return the receiver's hash. * * @see #equals */ @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; } /** * Returns the double value which is closest to the specified double but * larger. * * @param value * a double value. * @return the next larger double value. */ public static final 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); } /** * Returns the double value which is closest to the specified double but * either larger or smaller as specified. * * @param value * a double value. * @param increment * {@code true} to get the next larger value, {@code false} to * get the previous smaller value. * @return the next larger or smaller double value. */ public static double nextDouble(double value, boolean increment) { return increment ? nextDouble(value) : previousDouble(value); } /** * Parses a double from the specified string starting at the index specified * by {@code position}. The string is compared to the strings of this * {@code ChoiceFormat} and if a match occurs then the lower bound of the * corresponding range in the limits array is returned. If the string is * successfully parsed then the index of the {@code ParsePosition} passed to * this method is updated to the index following the parsed text. * <p> * If one of the format strings of this {@code ChoiceFormat} instance is * found in {@code string} starting at {@code position.getIndex()} then * <ul> * <li>the index in {@code position} is set to the index following the * parsed text; * <li>the {@link java.lang.Double Double} corresponding to the format * string is returned.</li> * </ul> * <p> * If none of the format strings is found in {@code string} then * <ul> * <li>the error index in {@code position} is set to the current index in * {@code position};</li> * <li> {@link java.lang.Double#NaN Double.NaN} is returned. * </ul> * @param string * the source string to parse. * @param position * input/output parameter, specifies the start index in {@code * string} from where to start parsing. See the <em>Returns</em> * section for a description of the output values. * @return a Double resulting from the parse, or Double.NaN if there is an * error */ @Override public Number parse(String string, ParsePosition 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 new Double(choiceLimits[i]); } } position.setErrorIndex(offset); return new Double(Double.NaN); } /** * Returns the double value which is closest to the specified double but * smaller. * * @param value * a double value. * @return the next smaller double value. */ public static final 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); } /** * Sets the double values and associated strings of this ChoiceFormat. When * calling {@link #format(double, StringBuffer, FieldPosition) format} with * a double value {@code d}, then the element {@code i} in {@code formats} * is selected where {@code i} fulfills * {@code limits[i] <= d < limits[i+1]}. * <p> * The length of the {@code limits} and {@code formats} arrays must be the * same. * * @param limits * an array of doubles in ascending order. The lowest and highest * possible values are negative and positive infinity. * @param formats * the strings associated with the ranges defined through {@code * limits}. The lower bound of the associated range is at the * same index as the string. */ 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; } /** * Returns the pattern of this {@code ChoiceFormat} which specifies the * ranges and their associated strings. * * @return the pattern. */ 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(); } }