Path: blob/master/src/java.base/share/classes/java/text/ChoiceFormat.java
41152 views
/*1* Copyright (c) 1996, 2019, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425/*26* (C) Copyright Taligent, Inc. 1996, 1997 - All Rights Reserved27* (C) Copyright IBM Corp. 1996 - 1998 - All Rights Reserved28*29* The original version of this source code and documentation is copyrighted30* and owned by Taligent, Inc., a wholly-owned subsidiary of IBM. These31* materials are provided under terms of a License Agreement between Taligent32* and Sun. This technology is protected by multiple US and International33* patents. This notice and attribution to Taligent may not be removed.34* Taligent is a registered trademark of Taligent, Inc.35*36*/3738package java.text;3940import java.io.InvalidObjectException;41import java.io.IOException;42import java.io.ObjectInputStream;43import java.util.Arrays;4445/**46* A {@code ChoiceFormat} allows you to attach a format to a range of numbers.47* It is generally used in a {@code MessageFormat} for handling plurals.48* The choice is specified with an ascending list of doubles, where each item49* specifies a half-open interval up to the next item:50* <blockquote>51* <pre>52* X matches j if and only if limit[j] ≤ X < limit[j+1]53* </pre>54* </blockquote>55* If there is no match, then either the first or last index is used, depending56* on whether the number (X) is too low or too high. If the limit array is not57* in ascending order, the results of formatting will be incorrect. ChoiceFormat58* also accepts <code>\u221E</code> as equivalent to infinity(INF).59*60* <p>61* <strong>Note:</strong>62* {@code ChoiceFormat} differs from the other {@code Format}63* classes in that you create a {@code ChoiceFormat} object with a64* constructor (not with a {@code getInstance} style factory65* method). The factory methods aren't necessary because {@code ChoiceFormat}66* doesn't require any complex setup for a given locale. In fact,67* {@code ChoiceFormat} doesn't implement any locale specific behavior.68*69* <p>70* When creating a {@code ChoiceFormat}, you must specify an array of formats71* and an array of limits. The length of these arrays must be the same.72* For example,73* <ul>74* <li>75* <em>limits</em> = {1,2,3,4,5,6,7}<br>76* <em>formats</em> = {"Sun","Mon","Tue","Wed","Thur","Fri","Sat"}77* <li>78* <em>limits</em> = {0, 1, ChoiceFormat.nextDouble(1)}<br>79* <em>formats</em> = {"no files", "one file", "many files"}<br>80* ({@code nextDouble} can be used to get the next higher double, to81* make the half-open interval.)82* </ul>83*84* <p>85* Here is a simple example that shows formatting and parsing:86* <blockquote>87* <pre>{@code88* double[] limits = {1,2,3,4,5,6,7};89* String[] dayOfWeekNames = {"Sun","Mon","Tue","Wed","Thur","Fri","Sat"};90* ChoiceFormat form = new ChoiceFormat(limits, dayOfWeekNames);91* ParsePosition status = new ParsePosition(0);92* for (double i = 0.0; i <= 8.0; ++i) {93* status.setIndex(0);94* System.out.println(i + " -> " + form.format(i) + " -> "95* + form.parse(form.format(i),status));96* }97* }</pre>98* </blockquote>99* Here is a more complex example, with a pattern format:100* <blockquote>101* <pre>{@code102* double[] filelimits = {0,1,2};103* String[] filepart = {"are no files","is one file","are {2} files"};104* ChoiceFormat fileform = new ChoiceFormat(filelimits, filepart);105* Format[] testFormats = {fileform, null, NumberFormat.getInstance()};106* MessageFormat pattform = new MessageFormat("There {0} on {1}");107* pattform.setFormats(testFormats);108* Object[] testArgs = {null, "ADisk", null};109* for (int i = 0; i < 4; ++i) {110* testArgs[0] = new Integer(i);111* testArgs[2] = testArgs[0];112* System.out.println(pattform.format(testArgs));113* }114* }</pre>115* </blockquote>116* <p>117* Specifying a pattern for ChoiceFormat objects is fairly straightforward.118* For example:119* <blockquote>120* <pre>{@code121* ChoiceFormat fmt = new ChoiceFormat(122* "-1#is negative| 0#is zero or fraction | 1#is one |1.0<is 1+ |2#is two |2<is more than 2.");123* System.out.println("Formatter Pattern : " + fmt.toPattern());124*125* System.out.println("Format with -INF : " + fmt.format(Double.NEGATIVE_INFINITY));126* System.out.println("Format with -1.0 : " + fmt.format(-1.0));127* System.out.println("Format with 0 : " + fmt.format(0));128* System.out.println("Format with 0.9 : " + fmt.format(0.9));129* System.out.println("Format with 1.0 : " + fmt.format(1));130* System.out.println("Format with 1.5 : " + fmt.format(1.5));131* System.out.println("Format with 2 : " + fmt.format(2));132* System.out.println("Format with 2.1 : " + fmt.format(2.1));133* System.out.println("Format with NaN : " + fmt.format(Double.NaN));134* System.out.println("Format with +INF : " + fmt.format(Double.POSITIVE_INFINITY));135* }</pre>136* </blockquote>137* And the output result would be like the following:138* <blockquote>139* <pre>{@code140* Format with -INF : is negative141* Format with -1.0 : is negative142* Format with 0 : is zero or fraction143* Format with 0.9 : is zero or fraction144* Format with 1.0 : is one145* Format with 1.5 : is 1+146* Format with 2 : is two147* Format with 2.1 : is more than 2.148* Format with NaN : is negative149* Format with +INF : is more than 2.150* }</pre>151* </blockquote>152*153* <h2><a id="synchronization">Synchronization</a></h2>154*155* <p>156* Choice formats are not synchronized.157* It is recommended to create separate format instances for each thread.158* If multiple threads access a format concurrently, it must be synchronized159* externally.160*161*162* @see DecimalFormat163* @see MessageFormat164* @author Mark Davis165* @since 1.1166*/167public class ChoiceFormat extends NumberFormat {168169// Proclaim serial compatibility with 1.1 FCS170@java.io.Serial171private static final long serialVersionUID = 1795184449645032964L;172173/**174* Sets the pattern.175* @param newPattern See the class description.176* @throws NullPointerException if {@code newPattern}177* is {@code null}178*/179public void applyPattern(String newPattern) {180StringBuffer[] segments = new StringBuffer[2];181for (int i = 0; i < segments.length; ++i) {182segments[i] = new StringBuffer();183}184double[] newChoiceLimits = new double[30];185String[] newChoiceFormats = new String[30];186int count = 0;187int part = 0;188double startValue = 0;189double oldStartValue = Double.NaN;190boolean inQuote = false;191for (int i = 0; i < newPattern.length(); ++i) {192char ch = newPattern.charAt(i);193if (ch=='\'') {194// Check for "''" indicating a literal quote195if ((i+1)<newPattern.length() && newPattern.charAt(i+1)==ch) {196segments[part].append(ch);197++i;198} else {199inQuote = !inQuote;200}201} else if (inQuote) {202segments[part].append(ch);203} else if (ch == '<' || ch == '#' || ch == '\u2264') {204if (segments[0].length() == 0) {205throw new IllegalArgumentException("Each interval must"206+ " contain a number before a format");207}208209String tempBuffer = segments[0].toString();210if (tempBuffer.equals("\u221E")) {211startValue = Double.POSITIVE_INFINITY;212} else if (tempBuffer.equals("-\u221E")) {213startValue = Double.NEGATIVE_INFINITY;214} else {215startValue = Double.parseDouble(tempBuffer);216}217218if (ch == '<' && startValue != Double.POSITIVE_INFINITY &&219startValue != Double.NEGATIVE_INFINITY) {220startValue = nextDouble(startValue);221}222if (startValue <= oldStartValue) {223throw new IllegalArgumentException("Incorrect order of"224+ " intervals, must be in ascending order");225}226segments[0].setLength(0);227part = 1;228} else if (ch == '|') {229if (count == newChoiceLimits.length) {230newChoiceLimits = doubleArraySize(newChoiceLimits);231newChoiceFormats = doubleArraySize(newChoiceFormats);232}233newChoiceLimits[count] = startValue;234newChoiceFormats[count] = segments[1].toString();235++count;236oldStartValue = startValue;237segments[1].setLength(0);238part = 0;239} else {240segments[part].append(ch);241}242}243// clean up last one244if (part == 1) {245if (count == newChoiceLimits.length) {246newChoiceLimits = doubleArraySize(newChoiceLimits);247newChoiceFormats = doubleArraySize(newChoiceFormats);248}249newChoiceLimits[count] = startValue;250newChoiceFormats[count] = segments[1].toString();251++count;252}253choiceLimits = new double[count];254System.arraycopy(newChoiceLimits, 0, choiceLimits, 0, count);255choiceFormats = new String[count];256System.arraycopy(newChoiceFormats, 0, choiceFormats, 0, count);257}258259/**260* Gets the pattern.261*262* @return the pattern string263*/264public String toPattern() {265StringBuilder result = new StringBuilder();266for (int i = 0; i < choiceLimits.length; ++i) {267if (i != 0) {268result.append('|');269}270// choose based upon which has less precision271// approximate that by choosing the closest one to an integer.272// could do better, but it's not worth it.273double less = previousDouble(choiceLimits[i]);274double tryLessOrEqual = Math.abs(Math.IEEEremainder(choiceLimits[i], 1.0d));275double tryLess = Math.abs(Math.IEEEremainder(less, 1.0d));276277if (tryLessOrEqual < tryLess) {278result.append(choiceLimits[i]);279result.append('#');280} else {281if (choiceLimits[i] == Double.POSITIVE_INFINITY) {282result.append("\u221E");283} else if (choiceLimits[i] == Double.NEGATIVE_INFINITY) {284result.append("-\u221E");285} else {286result.append(less);287}288result.append('<');289}290// Append choiceFormats[i], using quotes if there are special characters.291// Single quotes themselves must be escaped in either case.292String text = choiceFormats[i];293boolean needQuote = text.indexOf('<') >= 0294|| text.indexOf('#') >= 0295|| text.indexOf('\u2264') >= 0296|| text.indexOf('|') >= 0;297if (needQuote) result.append('\'');298if (text.indexOf('\'') < 0) result.append(text);299else {300for (int j=0; j<text.length(); ++j) {301char c = text.charAt(j);302result.append(c);303if (c == '\'') result.append(c);304}305}306if (needQuote) result.append('\'');307}308return result.toString();309}310311/**312* Constructs with limits and corresponding formats based on the pattern.313*314* @param newPattern the new pattern string315* @throws NullPointerException if {@code newPattern} is316* {@code null}317* @see #applyPattern318*/319public ChoiceFormat(String newPattern) {320applyPattern(newPattern);321}322323/**324* Constructs with the limits and the corresponding formats.325*326* @param limits limits in ascending order327* @param formats corresponding format strings328* @throws NullPointerException if {@code limits} or {@code formats}329* is {@code null}330* @see #setChoices331*/332public ChoiceFormat(double[] limits, String[] formats) {333setChoices(limits, formats);334}335336/**337* Set the choices to be used in formatting.338* @param limits contains the top value that you want339* parsed with that format, and should be in ascending sorted order. When340* formatting X, the choice will be the i, where341* limit[i] ≤ X {@literal <} limit[i+1].342* If the limit array is not in ascending order, the results of formatting343* will be incorrect.344* @param formats are the formats you want to use for each limit.345* They can be either Format objects or Strings.346* When formatting with object Y,347* if the object is a NumberFormat, then ((NumberFormat) Y).format(X)348* is called. Otherwise Y.toString() is called.349* @throws NullPointerException if {@code limits} or350* {@code formats} is {@code null}351*/352public void setChoices(double[] limits, String formats[]) {353if (limits.length != formats.length) {354throw new IllegalArgumentException(355"Array and limit arrays must be of the same length.");356}357choiceLimits = Arrays.copyOf(limits, limits.length);358choiceFormats = Arrays.copyOf(formats, formats.length);359}360361/**362* Get the limits passed in the constructor.363* @return the limits.364*/365public double[] getLimits() {366double[] newLimits = Arrays.copyOf(choiceLimits, choiceLimits.length);367return newLimits;368}369370/**371* Get the formats passed in the constructor.372* @return the formats.373*/374public Object[] getFormats() {375Object[] newFormats = Arrays.copyOf(choiceFormats, choiceFormats.length);376return newFormats;377}378379// Overrides380381/**382* Specialization of format. This method really calls383* {@code format(double, StringBuffer, FieldPosition)}384* thus the range of longs that are supported is only equal to385* the range that can be stored by double. This will never be386* a practical limitation.387*/388public StringBuffer format(long number, StringBuffer toAppendTo,389FieldPosition status) {390return format((double)number, toAppendTo, status);391}392393/**394* Returns pattern with formatted double.395* @param number number to be formatted and substituted.396* @param toAppendTo where text is appended.397* @param status ignore no useful status is returned.398* @throws NullPointerException if {@code toAppendTo}399* is {@code null}400*/401public StringBuffer format(double number, StringBuffer toAppendTo,402FieldPosition status) {403// find the number404int i;405for (i = 0; i < choiceLimits.length; ++i) {406if (!(number >= choiceLimits[i])) {407// same as number < choiceLimits, except catchs NaN408break;409}410}411--i;412if (i < 0) i = 0;413// return either a formatted number, or a string414return toAppendTo.append(choiceFormats[i]);415}416417/**418* Parses a Number from the input text.419* @param text the source text.420* @param status an input-output parameter. On input, the421* status.index field indicates the first character of the422* source text that should be parsed. On exit, if no error423* occurred, status.index is set to the first unparsed character424* in the source text. On exit, if an error did occur,425* status.index is unchanged and status.errorIndex is set to the426* first index of the character that caused the parse to fail.427* @return A Number representing the value of the number parsed.428* @throws NullPointerException if {@code status} is {@code null}429* or if {@code text} is {@code null} and the list of430* choice strings is not empty.431*/432public Number parse(String text, ParsePosition status) {433// find the best number (defined as the one with the longest parse)434int start = status.index;435int furthest = start;436double bestNumber = Double.NaN;437double tempNumber = 0.0;438for (int i = 0; i < choiceFormats.length; ++i) {439String tempString = choiceFormats[i];440if (text.regionMatches(start, tempString, 0, tempString.length())) {441status.index = start + tempString.length();442tempNumber = choiceLimits[i];443if (status.index > furthest) {444furthest = status.index;445bestNumber = tempNumber;446if (furthest == text.length()) break;447}448}449}450status.index = furthest;451if (status.index == start) {452status.errorIndex = furthest;453}454return Double.valueOf(bestNumber);455}456457/**458* Finds the least double greater than {@code d}.459* If {@code NaN}, returns same value.460* <p>Used to make half-open intervals.461*462* @implNote This is equivalent to calling463* {@link Math#nextUp(double) Math.nextUp(d)}464*465* @param d the reference value466* @return the least double value greather than {@code d}467* @see #previousDouble468*/469public static final double nextDouble (double d) {470return Math.nextUp(d);471}472473/**474* Finds the greatest double less than {@code d}.475* If {@code NaN}, returns same value.476*477* @implNote This is equivalent to calling478* {@link Math#nextDown(double) Math.nextDown(d)}479*480* @param d the reference value481* @return the greatest double value less than {@code d}482* @see #nextDouble483*/484public static final double previousDouble (double d) {485return Math.nextDown(d);486}487488/**489* Overrides Cloneable490*/491public Object clone()492{493ChoiceFormat other = (ChoiceFormat) super.clone();494// for primitives or immutables, shallow clone is enough495other.choiceLimits = choiceLimits.clone();496other.choiceFormats = choiceFormats.clone();497return other;498}499500/**501* Generates a hash code for the message format object.502*/503public int hashCode() {504int result = choiceLimits.length;505if (choiceFormats.length > 0) {506// enough for reasonable distribution507result ^= choiceFormats[choiceFormats.length-1].hashCode();508}509return result;510}511512/**513* Equality comparison between two514*/515public boolean equals(Object obj) {516if (obj == null) return false;517if (this == obj) // quick check518return true;519if (getClass() != obj.getClass())520return false;521ChoiceFormat other = (ChoiceFormat) obj;522return (Arrays.equals(choiceLimits, other.choiceLimits)523&& Arrays.equals(choiceFormats, other.choiceFormats));524}525526/**527* After reading an object from the input stream, do a simple verification528* to maintain class invariants.529* @throws InvalidObjectException if the objects read from the stream is invalid.530*/531@java.io.Serial532private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {533in.defaultReadObject();534if (choiceLimits.length != choiceFormats.length) {535throw new InvalidObjectException(536"limits and format arrays of different length.");537}538}539540// ===============privates===========================541542/**543* A list of lower bounds for the choices. The formatter will return544* {@code choiceFormats[i]} if the number being formatted is greater than or equal to545* {@code choiceLimits[i]} and less than {@code choiceLimits[i+1]}.546* @serial547*/548private double[] choiceLimits;549550/**551* A list of choice strings. The formatter will return552* {@code choiceFormats[i]} if the number being formatted is greater than or equal to553* {@code choiceLimits[i]} and less than {@code choiceLimits[i+1]}.554* @serial555*/556private String[] choiceFormats;557558/**559* Finds the least double greater than {@code d} (if {@code positive} is560* {@code true}), or the greatest double less than {@code d} (if561* {@code positive} is {@code false}).562* If {@code NaN}, returns same value.563*564* @implNote This is equivalent to calling565* {@code positive ? Math.nextUp(d) : Math.nextDown(d)}566*567* @param d the reference value568* @param positive {@code true} if the least double is desired;569* {@code false} otherwise570* @return the least or greater double value571*/572public static double nextDouble (double d, boolean positive) {573return positive ? Math.nextUp(d) : Math.nextDown(d);574}575576private static double[] doubleArraySize(double[] array) {577int oldSize = array.length;578double[] newArray = new double[oldSize * 2];579System.arraycopy(array, 0, newArray, 0, oldSize);580return newArray;581}582583private String[] doubleArraySize(String[] array) {584int oldSize = array.length;585String[] newArray = new String[oldSize * 2];586System.arraycopy(array, 0, newArray, 0, oldSize);587return newArray;588}589590}591592593