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<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