1/*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements.  See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package java.text;
19
20import java.util.ArrayList;
21import java.util.Arrays;
22import java.util.List;
23import java.util.Locale;
24import libcore.util.EmptyArray;
25
26/**
27 * Returns a fixed string based on a numeric value. The class can be used in
28 * conjunction with the {@link MessageFormat} class to handle plurals in
29 * messages. {@code ChoiceFormat} enables users to attach a format to a range of
30 * numbers. The choice is specified with an ascending list of doubles, where
31 * each item specifies a half-open interval up to the next item as in the
32 * following: X matches j if and only if {@code limit[j] <= X < limit[j+1]}.
33 * <p>
34 * If there is no match, then either the first or last index is used. The first
35 * or last index is used depending on whether the number is too low or too high.
36 * The length of the format array must be the same as the length of the limits
37 * array.
38 * <h5>Examples:</h5>
39 * <blockquote>
40 *
41 * <pre>
42 * double[] limits = {1, 2, 3, 4, 5, 6, 7};
43 * String[] fmts = {"Sun", "Mon", "Tue", "Wed", "Thur", "Fri", "Sat"};
44 *
45 * double[] limits2 = {0, 1, ChoiceFormat.nextDouble(1)};
46 * String[] fmts2 = {"no files", "one file", "many files"};
47 * </pre>
48 * </blockquote>
49 * <p>
50 * ChoiceFormat.nextDouble(double) allows to get the double following the one
51 * passed to the method. This is used to create half open intervals.
52 * <p>
53 * {@code ChoiceFormat} objects also may be converted to and from patterns.
54 * The conversion can be done programmatically, as in the example above, or
55 * by using a pattern like the following:
56 * <blockquote>
57 *
58 * <pre>
59 * "1#Sun|2#Mon|3#Tue|4#Wed|5#Thur|6#Fri|7#Sat"
60 * "0#are no files|1#is one file|1&lt;are many files"
61 * </pre>
62 *
63 * </blockquote>
64 * <p>
65 * where:
66 * <ul>
67 * <li><number>"#"</number> specifies an inclusive limit value;</li>
68 * <li><number>"<"</number> specifies an exclusive limit value.</li>
69 * </ul>
70 */
71public class ChoiceFormat extends NumberFormat {
72
73    private static final long serialVersionUID = 1795184449645032964L;
74
75    private double[] choiceLimits;
76
77    private String[] choiceFormats;
78
79    /**
80     * Constructs a new {@code ChoiceFormat} with the specified double values
81     * and associated strings. When calling
82     * {@link #format(double, StringBuffer, FieldPosition) format} with a double
83     * value {@code d}, then the element {@code i} in {@code formats} is
84     * selected where {@code i} fulfills {@code limits[i] <= d < limits[i+1]}.
85     * <p>
86     * The length of the {@code limits} and {@code formats} arrays must be the
87     * same.
88     *
89     * @param limits
90     *            an array of doubles in ascending order. The lowest and highest
91     *            possible values are negative and positive infinity.
92     * @param formats
93     *            the strings associated with the ranges defined through {@code
94     *            limits}. The lower bound of the associated range is at the
95     *            same index as the string.
96     */
97    public ChoiceFormat(double[] limits, String[] formats) {
98        setChoices(limits, formats);
99    }
100
101    /**
102     * Constructs a new {@code ChoiceFormat} with the strings and limits parsed
103     * from the specified pattern.
104     *
105     * @param template
106     *            the pattern of strings and ranges.
107     * @throws IllegalArgumentException
108     *            if an error occurs while parsing the pattern.
109     */
110    public ChoiceFormat(String template) {
111        applyPattern(template);
112    }
113
114    /**
115     * Parses the pattern to determine new strings and ranges for this
116     * {@code ChoiceFormat}.
117     *
118     * @param template
119     *            the pattern of strings and ranges.
120     * @throws IllegalArgumentException
121     *            if an error occurs while parsing the pattern.
122     */
123    public void applyPattern(String template) {
124        double[] limits = new double[5];
125        List<String> formats = new ArrayList<String>();
126        int length = template.length(), limitCount = 0, index = 0;
127        StringBuffer buffer = new StringBuffer();
128        NumberFormat format = NumberFormat.getInstance(Locale.US);
129        ParsePosition position = new ParsePosition(0);
130        while (true) {
131            index = skipWhitespace(template, index);
132            if (index >= length) {
133                if (limitCount == limits.length) {
134                    choiceLimits = limits;
135                } else {
136                    choiceLimits = new double[limitCount];
137                    System.arraycopy(limits, 0, choiceLimits, 0, limitCount);
138                }
139                choiceFormats = new String[formats.size()];
140                for (int i = 0; i < formats.size(); i++) {
141                    choiceFormats[i] = formats.get(i);
142                }
143                return;
144            }
145
146            position.setIndex(index);
147            Number value = format.parse(template, position);
148            index = skipWhitespace(template, position.getIndex());
149            if (position.getErrorIndex() != -1 || index >= length) {
150                // Fix Harmony 540
151                choiceLimits = EmptyArray.DOUBLE;
152                choiceFormats = EmptyArray.STRING;
153                return;
154            }
155            char ch = template.charAt(index++);
156            if (limitCount == limits.length) {
157                double[] newLimits = new double[limitCount * 2];
158                System.arraycopy(limits, 0, newLimits, 0, limitCount);
159                limits = newLimits;
160            }
161            double next;
162            switch (ch) {
163                case '#':
164                case '\u2264':
165                    next = value.doubleValue();
166                    break;
167                case '<':
168                    next = nextDouble(value.doubleValue());
169                    break;
170                default:
171                    throw new IllegalArgumentException("Bad character '" + ch + "' in template: " + template);
172            }
173            if (limitCount > 0 && next <= limits[limitCount - 1]) {
174                throw new IllegalArgumentException("Bad template: " + template);
175            }
176            buffer.setLength(0);
177            position.setIndex(index);
178            upTo(template, position, buffer, '|');
179            index = position.getIndex();
180            limits[limitCount++] = next;
181            formats.add(buffer.toString());
182        }
183    }
184
185    /**
186     * Returns a new instance of {@code ChoiceFormat} with the same ranges and
187     * strings as this {@code ChoiceFormat}.
188     *
189     * @return a shallow copy of this {@code ChoiceFormat}.
190     *
191     * @see java.lang.Cloneable
192     */
193    @Override
194    public Object clone() {
195        ChoiceFormat clone = (ChoiceFormat) super.clone();
196        clone.choiceLimits = choiceLimits.clone();
197        clone.choiceFormats = choiceFormats.clone();
198        return clone;
199    }
200
201    /**
202     * Compares the specified object with this {@code ChoiceFormat}. The object
203     * must be an instance of {@code ChoiceFormat} and have the same limits and
204     * formats to be equal to this instance.
205     *
206     * @param object
207     *            the object to compare with this instance.
208     * @return {@code true} if the specified object is equal to this instance;
209     *         {@code false} otherwise.
210     * @see #hashCode
211     */
212    @Override
213    public boolean equals(Object object) {
214        if (this == object) {
215            return true;
216        }
217        if (!(object instanceof ChoiceFormat)) {
218            return false;
219        }
220        ChoiceFormat choice = (ChoiceFormat) object;
221        return Arrays.equals(choiceLimits, choice.choiceLimits)
222                && Arrays.equals(choiceFormats, choice.choiceFormats);
223    }
224
225    /**
226     * Appends the string associated with the range in which the specified
227     * double value fits to the specified string buffer.
228     *
229     * @param value
230     *            the double to format.
231     * @param buffer
232     *            the target string buffer to append the formatted value to.
233     * @param field
234     *            a {@code FieldPosition} which is ignored.
235     * @return the string buffer.
236     */
237    @Override
238    public StringBuffer format(double value, StringBuffer buffer,
239            FieldPosition field) {
240        for (int i = choiceLimits.length - 1; i >= 0; i--) {
241            if (choiceLimits[i] <= value) {
242                return buffer.append(choiceFormats[i]);
243            }
244        }
245        return choiceFormats.length == 0 ? buffer : buffer
246                .append(choiceFormats[0]);
247    }
248
249    /**
250     * Appends the string associated with the range in which the specified long
251     * value fits to the specified string buffer.
252     *
253     * @param value
254     *            the long to format.
255     * @param buffer
256     *            the target string buffer to append the formatted value to.
257     * @param field
258     *            a {@code FieldPosition} which is ignored.
259     * @return the string buffer.
260     */
261    @Override
262    public StringBuffer format(long value, StringBuffer buffer,
263            FieldPosition field) {
264        return format((double) value, buffer, field);
265    }
266
267    /**
268     * Returns the strings associated with the ranges of this {@code
269     * ChoiceFormat}.
270     *
271     * @return an array of format strings.
272     */
273    public Object[] getFormats() {
274        return choiceFormats;
275    }
276
277    /**
278     * Returns the limits of this {@code ChoiceFormat}.
279     *
280     * @return the array of doubles which make up the limits of this {@code
281     *         ChoiceFormat}.
282     */
283    public double[] getLimits() {
284        return choiceLimits;
285    }
286
287    /**
288     * Returns an integer hash code for the receiver. Objects which are equal
289     * return the same value for this method.
290     *
291     * @return the receiver's hash.
292     *
293     * @see #equals
294     */
295    @Override
296    public int hashCode() {
297        int hashCode = 0;
298        for (int i = 0; i < choiceLimits.length; i++) {
299            long v = Double.doubleToLongBits(choiceLimits[i]);
300            hashCode += (int) (v ^ (v >>> 32)) + choiceFormats[i].hashCode();
301        }
302        return hashCode;
303    }
304
305    /**
306     * Equivalent to {@link Math#nextUp(double)}.
307     */
308    public static final double nextDouble(double value) {
309        return Math.nextUp(value);
310    }
311
312    /**
313     * Equivalent to {@link Math#nextUp(double)} if {@code increment == true}, and
314     * {@link Math#nextAfter(double, double)} with {@code direction == Double.NEGATIVE_INFINITY}
315     * otherwise.
316     */
317    public static double nextDouble(double value, boolean increment) {
318        return increment ? nextDouble(value) : previousDouble(value);
319    }
320
321    /**
322     * Parses a double from the specified string starting at the index specified
323     * by {@code position}. The string is compared to the strings of this
324     * {@code ChoiceFormat} and if a match occurs then the lower bound of the
325     * corresponding range in the limits array is returned. If the string is
326     * successfully parsed then the index of the {@code ParsePosition} passed to
327     * this method is updated to the index following the parsed text.
328     * <p>
329     * If one of the format strings of this {@code ChoiceFormat} instance is
330     * found in {@code string} starting at {@code position.getIndex()} then
331     * <ul>
332     * <li>the index in {@code position} is set to the index following the
333     * parsed text;
334     * <li>the {@link java.lang.Double Double} corresponding to the format
335     * string is returned.</li>
336     * </ul>
337     * <p>
338     * If none of the format strings is found in {@code string} then
339     * <ul>
340     * <li>the error index in {@code position} is set to the current index in
341     * {@code position};</li>
342     * <li> {@link java.lang.Double#NaN Double.NaN} is returned.
343     * </ul>
344     * @param string
345     *            the source string to parse.
346     * @param position
347     *            input/output parameter, specifies the start index in {@code
348     *            string} from where to start parsing. See the <em>Returns</em>
349     *            section for a description of the output values.
350     * @return a Double resulting from the parse, or Double.NaN if there is an
351     *         error
352     */
353    @Override
354    public Number parse(String string, ParsePosition position) {
355        int offset = position.getIndex();
356        for (int i = 0; i < choiceFormats.length; i++) {
357            if (string.startsWith(choiceFormats[i], offset)) {
358                position.setIndex(offset + choiceFormats[i].length());
359                return new Double(choiceLimits[i]);
360            }
361        }
362        position.setErrorIndex(offset);
363        return new Double(Double.NaN);
364    }
365
366    /**
367     * Equivalent to {@link Math#nextAfter(double, double)} with
368     * {@code direction == Double.NEGATIVE_INFINITY}.
369     */
370    public static final double previousDouble(double value) {
371        return Math.nextAfter(value, Double.NEGATIVE_INFINITY);
372    }
373
374    /**
375     * Sets the double values and associated strings of this ChoiceFormat. When
376     * calling {@link #format(double, StringBuffer, FieldPosition) format} with
377     * a double value {@code d}, then the element {@code i} in {@code formats}
378     * is selected where {@code i} fulfills
379     * {@code limits[i] <= d < limits[i+1]}.
380     * <p>
381     * The length of the {@code limits} and {@code formats} arrays must be the
382     * same.
383     *
384     * @param limits
385     *            an array of doubles in ascending order. The lowest and highest
386     *            possible values are negative and positive infinity.
387     * @param formats
388     *            the strings associated with the ranges defined through {@code
389     *            limits}. The lower bound of the associated range is at the
390     *            same index as the string.
391     */
392    public void setChoices(double[] limits, String[] formats) {
393        if (limits.length != formats.length) {
394            throw new IllegalArgumentException("limits.length != formats.length: " +
395                                               limits.length + " != " + formats.length);
396        }
397        choiceLimits = limits;
398        choiceFormats = formats;
399    }
400
401    private int skipWhitespace(String string, int index) {
402        int length = string.length();
403        while (index < length && Character.isWhitespace(string.charAt(index))) {
404            index++;
405        }
406        return index;
407    }
408
409    /**
410     * Returns the pattern of this {@code ChoiceFormat} which specifies the
411     * ranges and their associated strings.
412     *
413     * @return the pattern.
414     */
415    public String toPattern() {
416        StringBuilder buffer = new StringBuilder();
417        for (int i = 0; i < choiceLimits.length; i++) {
418            if (i != 0) {
419                buffer.append('|');
420            }
421
422            final String previous = String.valueOf(previousDouble(choiceLimits[i]));
423            final String limit = String.valueOf(choiceLimits[i]);
424
425            // Hack to make the output of toPattern parseable by another ChoiceFormat.
426            // String.valueOf() will emit "Infinity", which isn't parseable by our NumberFormat
427            // instances.
428            //
429            // Ideally, we'd just use NumberFormat.format() to emit output (to be symmetric with
430            // our usage of NumberFormat.parse()) but it's hard set the right number of significant
431            // digits in order to output a format string that's equivalent to the original input.
432            if (Double.isInfinite(choiceLimits[i]) ||
433                    Double.isInfinite(previousDouble(choiceLimits[i]))) {
434                if (choiceLimits[i] < 0) {
435                    buffer.append("-\u221E");
436                    buffer.append('<');
437                } else {
438                    buffer.append('\u221E');
439                    buffer.append('<');
440                }
441            } else if (previous.length() < limit.length()) {
442                // What the... i don't even.... sigh. This is trying to figure out whether the
443                // element was a "<" or a "#". The idea being that users will specify "reasonable"
444                // quantities and calling nextDouble will result in a "longer" number in most cases.
445                buffer.append(previous);
446                buffer.append('<');
447            } else {
448                buffer.append(limit);
449                buffer.append('#');
450            }
451            boolean quote = (choiceFormats[i].indexOf('|') != -1);
452            if (quote) {
453                buffer.append('\'');
454            }
455            buffer.append(choiceFormats[i]);
456            if (quote) {
457                buffer.append('\'');
458            }
459        }
460        return buffer.toString();
461    }
462}
463