Path: blob/master/src/java.base/share/classes/java/text/CompactNumberFormat.java
41152 views
/*1* Copyright (c) 2018, 2021, 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*/24package java.text;2526import java.io.IOException;27import java.io.InvalidObjectException;28import java.io.ObjectInputStream;29import java.math.BigDecimal;30import java.math.BigInteger;31import java.math.RoundingMode;32import java.util.ArrayList;33import java.util.Arrays;34import java.util.HashMap;35import java.util.List;36import java.util.Locale;37import java.util.Map;38import java.util.Objects;39import java.util.concurrent.atomic.AtomicInteger;40import java.util.concurrent.atomic.AtomicLong;41import java.util.regex.Matcher;42import java.util.regex.Pattern;43import java.util.stream.Collectors;444546/**47* <p>48* {@code CompactNumberFormat} is a concrete subclass of {@code NumberFormat}49* that formats a decimal number in its compact form.50*51* The compact number formatting is designed for the environment where the space52* is limited, and the formatted string can be displayed in that limited space.53* It is defined by LDML's specification for54* <a href = "http://unicode.org/reports/tr35/tr35-numbers.html#Compact_Number_Formats">55* Compact Number Formats</a>. A compact number formatting refers56* to the representation of a number in a shorter form, based on the patterns57* provided for a given locale.58*59* <p>60* For example:61* <br>In the {@link java.util.Locale#US US locale}, {@code 1000} can be formatted62* as {@code "1K"}, and {@code 1000000} as {@code "1M"}, depending upon the63* <a href = "#compact_number_style" >style</a> used.64* <br>In the {@code "hi_IN"} locale, {@code 1000} can be formatted as65* "1 \u0939\u091C\u093C\u093E\u0930", and {@code 50000000} as "5 \u0915.",66* depending upon the <a href = "#compact_number_style" >style</a> used.67*68* <p>69* To obtain a {@code CompactNumberFormat} for a locale, use one70* of the factory methods given by {@code NumberFormat} for compact number71* formatting. For example,72* {@link NumberFormat#getCompactNumberInstance(Locale, Style)}.73*74* <blockquote><pre>75* NumberFormat fmt = NumberFormat.getCompactNumberInstance(76* new Locale("hi", "IN"), NumberFormat.Style.SHORT);77* String result = fmt.format(1000);78* </pre></blockquote>79*80* <h2><a id="compact_number_style">Style</a></h2>81* <p>82* A number can be formatted in the compact forms with two different83* styles, {@link NumberFormat.Style#SHORT SHORT}84* and {@link NumberFormat.Style#LONG LONG}. Use85* {@link NumberFormat#getCompactNumberInstance(Locale, Style)} for formatting and86* parsing a number in {@link NumberFormat.Style#SHORT SHORT} or87* {@link NumberFormat.Style#LONG LONG} compact form,88* where the given {@code Style} parameter requests the desired89* format. A {@link NumberFormat.Style#SHORT SHORT} style90* compact number instance in the {@link java.util.Locale#US US locale} formats91* {@code 10000} as {@code "10K"}. However, a92* {@link NumberFormat.Style#LONG LONG} style instance in same locale93* formats {@code 10000} as {@code "10 thousand"}.94*95* <h2><a id="compact_number_patterns">Compact Number Patterns</a></h2>96* <p>97* The compact number patterns are represented in a series of patterns where each98* pattern is used to format a range of numbers. An example of99* {@link NumberFormat.Style#SHORT SHORT} styled compact number patterns100* for the {@link java.util.Locale#US US locale} is {@code {"", "", "", "0K",101* "00K", "000K", "0M", "00M", "000M", "0B", "00B", "000B", "0T", "00T", "000T"}},102* ranging from {@code 10}<sup>{@code 0}</sup> to {@code 10}<sup>{@code 14}</sup>.103* There can be any number of patterns and they are104* strictly index based starting from the range {@code 10}<sup>{@code 0}</sup>.105* For example, in the above patterns, pattern at index 3106* ({@code "0K"}) is used for formatting {@code number >= 1000 and number < 10000},107* pattern at index 4 ({@code "00K"}) is used for formatting108* {@code number >= 10000 and number < 100000} and so on. In most of the locales,109* patterns with the range110* {@code 10}<sup>{@code 0}</sup>-{@code 10}<sup>{@code 2}</sup> are empty111* strings, which implicitly means a special pattern {@code "0"}.112* A special pattern {@code "0"} is used for any range which does not contain113* a compact pattern. This special pattern can appear explicitly for any specific114* range, or considered as a default pattern for an empty string.115*116* <p>117* A compact pattern contains a positive and negative subpattern118* separated by a subpattern boundary character {@code ';' (U+003B)},119* for example, {@code "0K;-0K"}. Each subpattern has a prefix,120* minimum integer digits, and suffix. The negative subpattern121* is optional, if absent, then the positive subpattern prefixed with the122* minus sign ({@code '-' U+002D HYPHEN-MINUS}) is used as the negative123* subpattern. That is, {@code "0K"} alone is equivalent to {@code "0K;-0K"}.124* If there is an explicit negative subpattern, it serves only to specify125* the negative prefix and suffix. The number of minimum integer digits,126* and other characteristics are all the same as the positive pattern.127* That means that {@code "0K;-00K"} produces precisely the same behavior128* as {@code "0K;-0K"}.129*130* <p>131* Many characters in a compact pattern are taken literally, they are matched132* during parsing and output unchanged during formatting.133* <a href = "DecimalFormat.html#special_pattern_character">Special characters</a>,134* on the other hand, stand for other characters, strings, or classes of135* characters. They must be quoted, using single quote {@code ' (U+0027)}136* unless noted otherwise, if they are to appear in the prefix or suffix137* as literals. For example, 0\u0915'.'.138*139* <h3>Plurals</h3>140* <p>141* In case some localization requires compact number patterns to be different for142* plurals, each singular and plural pattern can be enumerated within a pair of143* curly brackets <code>'{' (U+007B)</code> and <code>'}' (U+007D)</code>, separated144* by a space {@code ' ' (U+0020)}. If this format is used, each pattern needs to be145* prepended by its {@code count}, followed by a single colon {@code ':' (U+003A)}.146* If the pattern includes spaces literally, they must be quoted.147* <p>148* For example, the compact number pattern representing millions in German locale can be149* specified as {@code "{one:0' 'Million other:0' 'Millionen}"}. The {@code count}150* follows LDML's151* <a href="https://unicode.org/reports/tr35/tr35-numbers.html#Language_Plural_Rules">152* Language Plural Rules</a>.153* <p>154* A compact pattern has the following syntax:155* <blockquote><pre>156* <i>Pattern:</i>157* <i>SimplePattern</i>158* '{' <i>PluralPattern</i> <i>[' ' PluralPattern]<sub>optional</sub></i> '}'159* <i>SimplePattern:</i>160* <i>PositivePattern</i>161* <i>PositivePattern</i> <i>[; NegativePattern]<sub>optional</sub></i>162* <i>PluralPattern:</i>163* <i>Count</i>:<i>SimplePattern</i>164* <i>Count:</i>165* "zero" / "one" / "two" / "few" / "many" / "other"166* <i>PositivePattern:</i>167* <i>Prefix<sub>optional</sub></i> <i>MinimumInteger</i> <i>Suffix<sub>optional</sub></i>168* <i>NegativePattern:</i>169* <i>Prefix<sub>optional</sub></i> <i>MinimumInteger</i> <i>Suffix<sub>optional</sub></i>170* <i>Prefix:</i>171* Any Unicode characters except \uFFFE, \uFFFF, and172* <a href = "DecimalFormat.html#special_pattern_character">special characters</a>.173* <i>Suffix:</i>174* Any Unicode characters except \uFFFE, \uFFFF, and175* <a href = "DecimalFormat.html#special_pattern_character">special characters</a>.176* <i>MinimumInteger:</i>177* 0178* 0 <i>MinimumInteger</i>179* </pre></blockquote>180*181* <h2>Formatting</h2>182* The default formatting behavior returns a formatted string with no fractional183* digits, however users can use the {@link #setMinimumFractionDigits(int)}184* method to include the fractional part.185* The number {@code 1000.0} or {@code 1000} is formatted as {@code "1K"}186* not {@code "1.00K"} (in the {@link java.util.Locale#US US locale}). For this187* reason, the patterns provided for formatting contain only the minimum188* integer digits, prefix and/or suffix, but no fractional part.189* For example, patterns used are {@code {"", "", "", 0K, 00K, ...}}. If the pattern190* selected for formatting a number is {@code "0"} (special pattern),191* either explicit or defaulted, then the general number formatting provided by192* {@link java.text.DecimalFormat DecimalFormat}193* for the specified locale is used.194*195* <h2>Parsing</h2>196* The default parsing behavior does not allow a grouping separator until197* grouping used is set to {@code true} by using198* {@link #setGroupingUsed(boolean)}. The parsing of the fractional part199* depends on the {@link #isParseIntegerOnly()}. For example, if the200* parse integer only is set to true, then the fractional part is skipped.201*202* <h2>Rounding</h2>203* {@code CompactNumberFormat} provides rounding modes defined in204* {@link java.math.RoundingMode} for formatting. By default, it uses205* {@link java.math.RoundingMode#HALF_EVEN RoundingMode.HALF_EVEN}.206*207* @see NumberFormat.Style208* @see NumberFormat209* @see DecimalFormat210* @since 12211*/212public final class CompactNumberFormat extends NumberFormat {213214@java.io.Serial215private static final long serialVersionUID = 7128367218649234678L;216217/**218* The patterns for compact form of numbers for this219* {@code CompactNumberFormat}. A possible example is220* {@code {"", "", "", "0K", "00K", "000K", "0M", "00M", "000M", "0B",221* "00B", "000B", "0T", "00T", "000T"}} ranging from222* {@code 10}<sup>{@code 0}</sup>-{@code 10}<sup>{@code 14}</sup>,223* where each pattern is used to format a range of numbers.224* For example, {@code "0K"} is used for formatting225* {@code number >= 1000 and number < 10000}, {@code "00K"} is used for226* formatting {@code number >= 10000 and number < 100000} and so on.227* This field must not be {@code null}.228*229* @serial230*/231private String[] compactPatterns;232233/**234* List of positive prefix patterns of this formatter's235* compact number patterns.236*/237private transient List<Patterns> positivePrefixPatterns;238239/**240* List of negative prefix patterns of this formatter's241* compact number patterns.242*/243private transient List<Patterns> negativePrefixPatterns;244245/**246* List of positive suffix patterns of this formatter's247* compact number patterns.248*/249private transient List<Patterns> positiveSuffixPatterns;250251/**252* List of negative suffix patterns of this formatter's253* compact number patterns.254*/255private transient List<Patterns> negativeSuffixPatterns;256257/**258* List of divisors of this formatter's compact number patterns.259* Divisor can be either Long or BigInteger (if the divisor value goes260* beyond long boundary)261*/262private transient List<Number> divisors;263264/**265* List of place holders that represent minimum integer digits at each index266* for each count.267*/268private transient List<Patterns> placeHolderPatterns;269270/**271* The {@code DecimalFormatSymbols} object used by this format.272* It contains the symbols used to format numbers. For example,273* the grouping separator, decimal separator, and so on.274* This field must not be {@code null}.275*276* @serial277* @see DecimalFormatSymbols278*/279private DecimalFormatSymbols symbols;280281/**282* The decimal pattern which is used for formatting the numbers283* matching special pattern "0". This field must not be {@code null}.284*285* @serial286* @see DecimalFormat287*/288private final String decimalPattern;289290/**291* A {@code DecimalFormat} used by this format for getting corresponding292* general number formatting behavior for compact numbers.293*294*/295private transient DecimalFormat decimalFormat;296297/**298* A {@code DecimalFormat} used by this format for getting general number299* formatting behavior for the numbers which can't be represented as compact300* numbers. For example, number matching the special pattern "0" are301* formatted through general number format pattern provided by302* {@link java.text.DecimalFormat DecimalFormat}303* for the specified locale.304*305*/306private transient DecimalFormat defaultDecimalFormat;307308/**309* The number of digits between grouping separators in the integer portion310* of a compact number. For the grouping to work while formatting, this311* field needs to be greater than 0 with grouping used set as true.312* This field must not be negative.313*314* @serial315*/316private byte groupingSize = 0;317318/**319* Returns whether the {@link #parse(String, ParsePosition)}320* method returns {@code BigDecimal}.321*322* @serial323*/324private boolean parseBigDecimal = false;325326/**327* The {@code RoundingMode} used in this compact number format.328* This field must not be {@code null}.329*330* @serial331*/332private RoundingMode roundingMode = RoundingMode.HALF_EVEN;333334/**335* The {@code pluralRules} used in this compact number format.336* {@code pluralRules} is a String designating plural rules which associate337* the {@code Count} keyword, such as "{@code one}", and the338* actual integer number. Its syntax is defined in Unicode Consortium's339* <a href = "http://unicode.org/reports/tr35/tr35-numbers.html#Plural_rules_syntax">340* Plural rules syntax</a>.341* The default value is an empty string, meaning there is no plural rules.342*343* @serial344* @since 14345*/346private String pluralRules = "";347348/**349* The map for plural rules that maps LDML defined tags (e.g. "one") to350* its rule.351*/352private transient Map<String, String> rulesMap;353354/**355* Special pattern used for compact numbers356*/357private static final String SPECIAL_PATTERN = "0";358359/**360* Multiplier for compact pattern range. In361* the list compact patterns each compact pattern362* specify the range with the multiplication factor of 10363* of its previous compact pattern range.364* For example, 10^0, 10^1, 10^2, 10^3, 10^4...365*366*/367private static final int RANGE_MULTIPLIER = 10;368369/**370* Creates a {@code CompactNumberFormat} using the given decimal pattern,371* decimal format symbols and compact patterns.372* To obtain the instance of {@code CompactNumberFormat} with the standard373* compact patterns for a {@code Locale} and {@code Style},374* it is recommended to use the factory methods given by375* {@code NumberFormat} for compact number formatting. For example,376* {@link NumberFormat#getCompactNumberInstance(Locale, Style)}.377*378* @param decimalPattern a decimal pattern for general number formatting379* @param symbols the set of symbols to be used380* @param compactPatterns an array of381* <a href = "CompactNumberFormat.html#compact_number_patterns">382* compact number patterns</a>383* @throws NullPointerException if any of the given arguments is384* {@code null}385* @throws IllegalArgumentException if the given {@code decimalPattern} or the386* {@code compactPatterns} array contains an invalid pattern387* or if a {@code null} appears in the array of compact388* patterns389* @see DecimalFormat#DecimalFormat(java.lang.String, DecimalFormatSymbols)390* @see DecimalFormatSymbols391*/392public CompactNumberFormat(String decimalPattern,393DecimalFormatSymbols symbols, String[] compactPatterns) {394this(decimalPattern, symbols, compactPatterns, "");395}396397/**398* Creates a {@code CompactNumberFormat} using the given decimal pattern,399* decimal format symbols, compact patterns, and plural rules.400* To obtain the instance of {@code CompactNumberFormat} with the standard401* compact patterns for a {@code Locale}, {@code Style}, and {@code pluralRules},402* it is recommended to use the factory methods given by403* {@code NumberFormat} for compact number formatting. For example,404* {@link NumberFormat#getCompactNumberInstance(Locale, Style)}.405*406* @param decimalPattern a decimal pattern for general number formatting407* @param symbols the set of symbols to be used408* @param compactPatterns an array of409* <a href = "CompactNumberFormat.html#compact_number_patterns">410* compact number patterns</a>411* @param pluralRules a String designating plural rules which associate412* the {@code Count} keyword, such as "{@code one}", and the413* actual integer number. Its syntax is defined in Unicode Consortium's414* <a href = "http://unicode.org/reports/tr35/tr35-numbers.html#Plural_rules_syntax">415* Plural rules syntax</a>416* @throws NullPointerException if any of the given arguments is417* {@code null}418* @throws IllegalArgumentException if the given {@code decimalPattern},419* the {@code compactPatterns} array contains an invalid pattern,420* a {@code null} appears in the array of compact patterns,421* or if the given {@code pluralRules} contains an invalid syntax422* @see DecimalFormat#DecimalFormat(java.lang.String, DecimalFormatSymbols)423* @see DecimalFormatSymbols424* @since 14425*/426public CompactNumberFormat(String decimalPattern,427DecimalFormatSymbols symbols, String[] compactPatterns,428String pluralRules) {429430Objects.requireNonNull(decimalPattern, "decimalPattern");431Objects.requireNonNull(symbols, "symbols");432Objects.requireNonNull(compactPatterns, "compactPatterns");433Objects.requireNonNull(pluralRules, "pluralRules");434435this.symbols = symbols;436// Instantiating the DecimalFormat with "0" pattern; this acts just as a437// basic pattern; the properties (For example, prefix/suffix)438// are later computed based on the compact number formatting process.439decimalFormat = new DecimalFormat(SPECIAL_PATTERN, this.symbols);440441// Initializing the super class state with the decimalFormat values442// to represent this CompactNumberFormat.443// For setting the digits counts, use overridden setXXX methods of this444// CompactNumberFormat, as it performs check with the max range allowed445// for compact number formatting446setMaximumIntegerDigits(decimalFormat.getMaximumIntegerDigits());447setMinimumIntegerDigits(decimalFormat.getMinimumIntegerDigits());448setMaximumFractionDigits(decimalFormat.getMaximumFractionDigits());449setMinimumFractionDigits(decimalFormat.getMinimumFractionDigits());450451super.setGroupingUsed(decimalFormat.isGroupingUsed());452super.setParseIntegerOnly(decimalFormat.isParseIntegerOnly());453454this.compactPatterns = compactPatterns;455456// DecimalFormat used for formatting numbers with special pattern "0".457// Formatting is delegated to the DecimalFormat's number formatting458// with no fraction digits459this.decimalPattern = decimalPattern;460defaultDecimalFormat = new DecimalFormat(this.decimalPattern,461this.symbols);462defaultDecimalFormat.setMaximumFractionDigits(0);463464this.pluralRules = pluralRules;465466// Process compact patterns to extract the prefixes, suffixes, place holders, and467// divisors468processCompactPatterns();469}470471/**472* Formats a number to produce a string representing its compact form.473* The number can be of any subclass of {@link java.lang.Number}.474* @param number the number to format475* @param toAppendTo the {@code StringBuffer} to which the formatted476* text is to be appended477* @param fieldPosition keeps track on the position of the field within478* the returned string. For example, for formatting479* a number {@code 123456789} in the480* {@link java.util.Locale#US US locale},481* if the given {@code fieldPosition} is482* {@link NumberFormat#INTEGER_FIELD}, the begin483* index and end index of {@code fieldPosition}484* will be set to 0 and 3, respectively for the485* output string {@code 123M}. Similarly, positions486* of the prefix and the suffix fields can be487* obtained using {@link NumberFormat.Field#PREFIX}488* and {@link NumberFormat.Field#SUFFIX} respectively.489* @return the {@code StringBuffer} passed in as {@code toAppendTo}490* @throws IllegalArgumentException if {@code number} is491* {@code null} or not an instance of {@code Number}492* @throws NullPointerException if {@code toAppendTo} or493* {@code fieldPosition} is {@code null}494* @throws ArithmeticException if rounding is needed with rounding495* mode being set to {@code RoundingMode.UNNECESSARY}496* @see FieldPosition497*/498@Override499public final StringBuffer format(Object number,500StringBuffer toAppendTo,501FieldPosition fieldPosition) {502503if (number == null) {504throw new IllegalArgumentException("Cannot format null as a number");505}506507if (number instanceof Long || number instanceof Integer508|| number instanceof Short || number instanceof Byte509|| number instanceof AtomicInteger510|| number instanceof AtomicLong511|| (number instanceof BigInteger512&& ((BigInteger) number).bitLength() < 64)) {513return format(((Number) number).longValue(), toAppendTo,514fieldPosition);515} else if (number instanceof BigDecimal) {516return format((BigDecimal) number, toAppendTo, fieldPosition);517} else if (number instanceof BigInteger) {518return format((BigInteger) number, toAppendTo, fieldPosition);519} else if (number instanceof Number) {520return format(((Number) number).doubleValue(), toAppendTo, fieldPosition);521} else {522throw new IllegalArgumentException("Cannot format "523+ number.getClass().getName() + " as a number");524}525}526527/**528* Formats a double to produce a string representing its compact form.529* @param number the double number to format530* @param result where the text is to be appended531* @param fieldPosition keeps track on the position of the field within532* the returned string. For example, to format533* a number {@code 1234567.89} in the534* {@link java.util.Locale#US US locale}535* if the given {@code fieldPosition} is536* {@link NumberFormat#INTEGER_FIELD}, the begin537* index and end index of {@code fieldPosition}538* will be set to 0 and 1, respectively for the539* output string {@code 1M}. Similarly, positions540* of the prefix and the suffix fields can be541* obtained using {@link NumberFormat.Field#PREFIX}542* and {@link NumberFormat.Field#SUFFIX} respectively.543* @return the {@code StringBuffer} passed in as {@code result}544* @throws NullPointerException if {@code result} or545* {@code fieldPosition} is {@code null}546* @throws ArithmeticException if rounding is needed with rounding547* mode being set to {@code RoundingMode.UNNECESSARY}548* @see FieldPosition549*/550@Override551public StringBuffer format(double number, StringBuffer result,552FieldPosition fieldPosition) {553554fieldPosition.setBeginIndex(0);555fieldPosition.setEndIndex(0);556return format(number, result, fieldPosition.getFieldDelegate());557}558559private StringBuffer format(double number, StringBuffer result,560FieldDelegate delegate) {561562boolean nanOrInfinity = decimalFormat.handleNaN(number, result, delegate);563if (nanOrInfinity) {564return result;565}566567boolean isNegative = ((number < 0.0)568|| (number == 0.0 && 1 / number < 0.0));569570nanOrInfinity = decimalFormat.handleInfinity(number, result, delegate, isNegative);571if (nanOrInfinity) {572return result;573}574575// Round the double value with min fraction digits, the integer576// part of the rounded value is used for matching the compact577// number pattern578// For example, if roundingMode is HALF_UP with min fraction579// digits = 0, the number 999.6 should round up580// to 1000 and outputs 1K/thousand in "en_US" locale581DigitList dList = new DigitList();582dList.setRoundingMode(getRoundingMode());583number = isNegative ? -number : number;584dList.set(isNegative, number, getMinimumFractionDigits());585586double roundedNumber = dList.getDouble();587int compactDataIndex = selectCompactPattern((long) roundedNumber);588if (compactDataIndex != -1) {589long divisor = (Long) divisors.get(compactDataIndex);590int iPart = getIntegerPart(number, divisor);591String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);592String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);593594if (!prefix.isEmpty() || !suffix.isEmpty()) {595appendPrefix(result, prefix, delegate);596if (!placeHolderPatterns.get(compactDataIndex).get(iPart).isEmpty()) {597roundedNumber = roundedNumber / divisor;598decimalFormat.setDigitList(roundedNumber, isNegative, getMaximumFractionDigits());599decimalFormat.subformatNumber(result, delegate, isNegative,600false, getMaximumIntegerDigits(), getMinimumIntegerDigits(),601getMaximumFractionDigits(), getMinimumFractionDigits());602appendSuffix(result, suffix, delegate);603}604} else {605defaultDecimalFormat.doubleSubformat(number, result, delegate, isNegative);606}607} else {608defaultDecimalFormat.doubleSubformat(number, result, delegate, isNegative);609}610return result;611}612613/**614* Formats a long to produce a string representing its compact form.615* @param number the long number to format616* @param result where the text is to be appended617* @param fieldPosition keeps track on the position of the field within618* the returned string. For example, to format619* a number {@code 123456789} in the620* {@link java.util.Locale#US US locale},621* if the given {@code fieldPosition} is622* {@link NumberFormat#INTEGER_FIELD}, the begin623* index and end index of {@code fieldPosition}624* will be set to 0 and 3, respectively for the625* output string {@code 123M}. Similarly, positions626* of the prefix and the suffix fields can be627* obtained using {@link NumberFormat.Field#PREFIX}628* and {@link NumberFormat.Field#SUFFIX} respectively.629* @return the {@code StringBuffer} passed in as {@code result}630* @throws NullPointerException if {@code result} or631* {@code fieldPosition} is {@code null}632* @throws ArithmeticException if rounding is needed with rounding633* mode being set to {@code RoundingMode.UNNECESSARY}634* @see FieldPosition635*/636@Override637public StringBuffer format(long number, StringBuffer result,638FieldPosition fieldPosition) {639640fieldPosition.setBeginIndex(0);641fieldPosition.setEndIndex(0);642return format(number, result, fieldPosition.getFieldDelegate());643}644645private StringBuffer format(long number, StringBuffer result, FieldDelegate delegate) {646boolean isNegative = (number < 0);647if (isNegative) {648number = -number;649}650651if (number < 0) { // LONG_MIN652BigInteger bigIntegerValue = BigInteger.valueOf(number);653return format(bigIntegerValue, result, delegate, true);654}655656int compactDataIndex = selectCompactPattern(number);657if (compactDataIndex != -1) {658long divisor = (Long) divisors.get(compactDataIndex);659int iPart = getIntegerPart(number, divisor);660String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);661String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);662if (!prefix.isEmpty() || !suffix.isEmpty()) {663appendPrefix(result, prefix, delegate);664if (!placeHolderPatterns.get(compactDataIndex).get(iPart).isEmpty()) {665if ((number % divisor == 0)) {666number = number / divisor;667decimalFormat.setDigitList(number, isNegative, 0);668decimalFormat.subformatNumber(result, delegate,669isNegative, true, getMaximumIntegerDigits(),670getMinimumIntegerDigits(), getMaximumFractionDigits(),671getMinimumFractionDigits());672} else {673// To avoid truncation of fractional part store674// the value in double and follow double path instead of675// long path676double dNumber = (double) number / divisor;677decimalFormat.setDigitList(dNumber, isNegative, getMaximumFractionDigits());678decimalFormat.subformatNumber(result, delegate,679isNegative, false, getMaximumIntegerDigits(),680getMinimumIntegerDigits(), getMaximumFractionDigits(),681getMinimumFractionDigits());682}683appendSuffix(result, suffix, delegate);684}685} else {686number = isNegative ? -number : number;687defaultDecimalFormat.format(number, result, delegate);688}689} else {690number = isNegative ? -number : number;691defaultDecimalFormat.format(number, result, delegate);692}693return result;694}695696/**697* Formats a BigDecimal to produce a string representing its compact form.698* @param number the BigDecimal number to format699* @param result where the text is to be appended700* @param fieldPosition keeps track on the position of the field within701* the returned string. For example, to format702* a number {@code 1234567.89} in the703* {@link java.util.Locale#US US locale},704* if the given {@code fieldPosition} is705* {@link NumberFormat#INTEGER_FIELD}, the begin706* index and end index of {@code fieldPosition}707* will be set to 0 and 1, respectively for the708* output string {@code 1M}. Similarly, positions709* of the prefix and the suffix fields can be710* obtained using {@link NumberFormat.Field#PREFIX}711* and {@link NumberFormat.Field#SUFFIX} respectively.712* @return the {@code StringBuffer} passed in as {@code result}713* @throws ArithmeticException if rounding is needed with rounding714* mode being set to {@code RoundingMode.UNNECESSARY}715* @throws NullPointerException if any of the given parameter716* is {@code null}717* @see FieldPosition718*/719private StringBuffer format(BigDecimal number, StringBuffer result,720FieldPosition fieldPosition) {721722Objects.requireNonNull(number);723fieldPosition.setBeginIndex(0);724fieldPosition.setEndIndex(0);725return format(number, result, fieldPosition.getFieldDelegate());726}727728private StringBuffer format(BigDecimal number, StringBuffer result,729FieldDelegate delegate) {730731boolean isNegative = number.signum() == -1;732if (isNegative) {733number = number.negate();734}735736// Round the value with min fraction digits, the integer737// part of the rounded value is used for matching the compact738// number pattern739// For example, If roundingMode is HALF_UP with min fraction digits = 0,740// the number 999.6 should round up741// to 1000 and outputs 1K/thousand in "en_US" locale742number = number.setScale(getMinimumFractionDigits(), getRoundingMode());743744int compactDataIndex;745if (number.toBigInteger().bitLength() < 64) {746long longNumber = number.toBigInteger().longValue();747compactDataIndex = selectCompactPattern(longNumber);748} else {749compactDataIndex = selectCompactPattern(number.toBigInteger());750}751752if (compactDataIndex != -1) {753Number divisor = divisors.get(compactDataIndex);754int iPart = getIntegerPart(number.doubleValue(), divisor.doubleValue());755String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);756String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);757if (!prefix.isEmpty() || !suffix.isEmpty()) {758appendPrefix(result, prefix, delegate);759if (!placeHolderPatterns.get(compactDataIndex).get(iPart).isEmpty()) {760number = number.divide(new BigDecimal(divisor.toString()), getRoundingMode());761decimalFormat.setDigitList(number, isNegative, getMaximumFractionDigits());762decimalFormat.subformatNumber(result, delegate, isNegative,763false, getMaximumIntegerDigits(), getMinimumIntegerDigits(),764getMaximumFractionDigits(), getMinimumFractionDigits());765appendSuffix(result, suffix, delegate);766}767} else {768number = isNegative ? number.negate() : number;769defaultDecimalFormat.format(number, result, delegate);770}771} else {772number = isNegative ? number.negate() : number;773defaultDecimalFormat.format(number, result, delegate);774}775return result;776}777778/**779* Formats a BigInteger to produce a string representing its compact form.780* @param number the BigInteger number to format781* @param result where the text is to be appended782* @param fieldPosition keeps track on the position of the field within783* the returned string. For example, to format784* a number {@code 123456789} in the785* {@link java.util.Locale#US US locale},786* if the given {@code fieldPosition} is787* {@link NumberFormat#INTEGER_FIELD}, the begin index788* and end index of {@code fieldPosition} will be set789* to 0 and 3, respectively for the output string790* {@code 123M}. Similarly, positions of the791* prefix and the suffix fields can be obtained792* using {@link NumberFormat.Field#PREFIX} and793* {@link NumberFormat.Field#SUFFIX} respectively.794* @return the {@code StringBuffer} passed in as {@code result}795* @throws ArithmeticException if rounding is needed with rounding796* mode being set to {@code RoundingMode.UNNECESSARY}797* @throws NullPointerException if any of the given parameter798* is {@code null}799* @see FieldPosition800*/801private StringBuffer format(BigInteger number, StringBuffer result,802FieldPosition fieldPosition) {803804Objects.requireNonNull(number);805fieldPosition.setBeginIndex(0);806fieldPosition.setEndIndex(0);807return format(number, result, fieldPosition.getFieldDelegate(), false);808}809810private StringBuffer format(BigInteger number, StringBuffer result,811FieldDelegate delegate, boolean formatLong) {812813boolean isNegative = number.signum() == -1;814if (isNegative) {815number = number.negate();816}817818int compactDataIndex = selectCompactPattern(number);819if (compactDataIndex != -1) {820Number divisor = divisors.get(compactDataIndex);821int iPart = getIntegerPart(number.doubleValue(), divisor.doubleValue());822String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);823String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);824if (!prefix.isEmpty() || !suffix.isEmpty()) {825appendPrefix(result, prefix, delegate);826if (!placeHolderPatterns.get(compactDataIndex).get(iPart).isEmpty()) {827if (number.mod(new BigInteger(divisor.toString()))828.compareTo(BigInteger.ZERO) == 0) {829number = number.divide(new BigInteger(divisor.toString()));830831decimalFormat.setDigitList(number, isNegative, 0);832decimalFormat.subformatNumber(result, delegate,833isNegative, true, getMaximumIntegerDigits(),834getMinimumIntegerDigits(), getMaximumFractionDigits(),835getMinimumFractionDigits());836} else {837// To avoid truncation of fractional part store the value in838// BigDecimal and follow BigDecimal path instead of839// BigInteger path840BigDecimal nDecimal = new BigDecimal(number)841.divide(new BigDecimal(divisor.toString()), getRoundingMode());842decimalFormat.setDigitList(nDecimal, isNegative, getMaximumFractionDigits());843decimalFormat.subformatNumber(result, delegate,844isNegative, false, getMaximumIntegerDigits(),845getMinimumIntegerDigits(), getMaximumFractionDigits(),846getMinimumFractionDigits());847}848appendSuffix(result, suffix, delegate);849}850} else {851number = isNegative ? number.negate() : number;852defaultDecimalFormat.format(number, result, delegate, formatLong);853}854} else {855number = isNegative ? number.negate() : number;856defaultDecimalFormat.format(number, result, delegate, formatLong);857}858return result;859}860861/**862* Obtain the designated affix from the appropriate list of affixes,863* based on the given arguments.864*/865private String getAffix(boolean isExpanded, boolean isPrefix, boolean isNegative, int compactDataIndex, int iPart) {866return (isExpanded ? (isPrefix ? (isNegative ? negativePrefixes : positivePrefixes) :867(isNegative ? negativeSuffixes : positiveSuffixes)) :868(isPrefix ? (isNegative ? negativePrefixPatterns : positivePrefixPatterns) :869(isNegative ? negativeSuffixPatterns : positiveSuffixPatterns)))870.get(compactDataIndex).get(iPart);871}872873/**874* Appends the {@code prefix} to the {@code result} and also set the875* {@code NumberFormat.Field.SIGN} and {@code NumberFormat.Field.PREFIX}876* field positions.877* @param result the resulting string, where the pefix is to be appended878* @param prefix prefix to append879* @param delegate notified of the locations of880* {@code NumberFormat.Field.SIGN} and881* {@code NumberFormat.Field.PREFIX} fields882*/883private void appendPrefix(StringBuffer result, String prefix,884FieldDelegate delegate) {885append(result, expandAffix(prefix), delegate,886getFieldPositions(prefix, NumberFormat.Field.PREFIX));887}888889/**890* Appends {@code suffix} to the {@code result} and also set the891* {@code NumberFormat.Field.SIGN} and {@code NumberFormat.Field.SUFFIX}892* field positions.893* @param result the resulting string, where the suffix is to be appended894* @param suffix suffix to append895* @param delegate notified of the locations of896* {@code NumberFormat.Field.SIGN} and897* {@code NumberFormat.Field.SUFFIX} fields898*/899private void appendSuffix(StringBuffer result, String suffix,900FieldDelegate delegate) {901append(result, expandAffix(suffix), delegate,902getFieldPositions(suffix, NumberFormat.Field.SUFFIX));903}904905/**906* Appends the {@code string} to the {@code result}.907* {@code delegate} is notified of SIGN, PREFIX and/or SUFFIX908* field positions.909* @param result the resulting string, where the text is to be appended910* @param string the text to append911* @param delegate notified of the locations of sub fields912* @param positions a list of {@code FieldPostion} in the given913* string914*/915private void append(StringBuffer result, String string,916FieldDelegate delegate, List<FieldPosition> positions) {917if (!string.isEmpty()) {918int start = result.length();919result.append(string);920for (FieldPosition fp : positions) {921Format.Field attribute = fp.getFieldAttribute();922delegate.formatted(attribute, attribute,923start + fp.getBeginIndex(),924start + fp.getEndIndex(), result);925}926}927}928929/**930* Expands an affix {@code pattern} into a string of literals.931* All characters in the pattern are literals unless prefixed by QUOTE.932* The character prefixed by QUOTE is replaced with its respective933* localized literal.934* @param pattern a compact number pattern affix935* @return an expanded affix936*/937private String expandAffix(String pattern) {938// Return if no quoted character exists939if (pattern.indexOf(QUOTE) < 0) {940return pattern;941}942StringBuilder sb = new StringBuilder();943for (int index = 0; index < pattern.length();) {944char ch = pattern.charAt(index++);945if (ch == QUOTE) {946ch = pattern.charAt(index++);947if (ch == MINUS_SIGN) {948sb.append(symbols.getMinusSignText());949continue;950}951}952sb.append(ch);953}954return sb.toString();955}956957/**958* Returns a list of {@code FieldPostion} in the given {@code pattern}.959* @param pattern the pattern to be parsed for {@code FieldPosition}960* @param field whether a PREFIX or SUFFIX field961* @return a list of {@code FieldPostion}962*/963private List<FieldPosition> getFieldPositions(String pattern, Field field) {964List<FieldPosition> positions = new ArrayList<>();965StringBuilder affix = new StringBuilder();966int stringIndex = 0;967for (int index = 0; index < pattern.length();) {968char ch = pattern.charAt(index++);969if (ch == QUOTE) {970ch = pattern.charAt(index++);971if (ch == MINUS_SIGN) {972String minusText = symbols.getMinusSignText();973FieldPosition fp = new FieldPosition(NumberFormat.Field.SIGN);974fp.setBeginIndex(stringIndex);975fp.setEndIndex(stringIndex + minusText.length());976positions.add(fp);977stringIndex += minusText.length();978affix.append(minusText);979continue;980}981}982stringIndex++;983affix.append(ch);984}985if (affix.length() != 0) {986FieldPosition fp = new FieldPosition(field);987fp.setBeginIndex(0);988fp.setEndIndex(affix.length());989positions.add(fp);990}991return positions;992}993994/**995* Select the index of the matched compact number pattern for996* the given {@code long} {@code number}.997*998* @param number number to be formatted999* @return index of matched compact pattern;1000* -1 if no compact patterns specified1001*/1002private int selectCompactPattern(long number) {10031004if (compactPatterns.length == 0) {1005return -1;1006}10071008// Minimum index can be "0", max index can be "size - 1"1009int dataIndex = number <= 1 ? 0 : (int) Math.log10(number);1010dataIndex = Math.min(dataIndex, compactPatterns.length - 1);1011return dataIndex;1012}10131014/**1015* Select the index of the matched compact number1016* pattern for the given {@code BigInteger} {@code number}.1017*1018* @param number number to be formatted1019* @return index of matched compact pattern;1020* -1 if no compact patterns specified1021*/1022private int selectCompactPattern(BigInteger number) {10231024int matchedIndex = -1;1025if (compactPatterns.length == 0) {1026return matchedIndex;1027}10281029BigInteger currentValue = BigInteger.ONE;10301031// For formatting a number, the greatest type less than1032// or equal to number is used1033for (int index = 0; index < compactPatterns.length; index++) {1034if (number.compareTo(currentValue) > 0) {1035// Input number is greater than current type; try matching with1036// the next1037matchedIndex = index;1038currentValue = currentValue.multiply(BigInteger.valueOf(RANGE_MULTIPLIER));1039continue;1040}1041if (number.compareTo(currentValue) < 0) {1042// Current type is greater than the input number;1043// take the previous pattern1044break;1045} else {1046// Equal1047matchedIndex = index;1048break;1049}1050}1051return matchedIndex;1052}10531054/**1055* Formats an Object producing an {@code AttributedCharacterIterator}.1056* The returned {@code AttributedCharacterIterator} can be used1057* to build the resulting string, as well as to determine information1058* about the resulting string.1059* <p>1060* Each attribute key of the {@code AttributedCharacterIterator} will1061* be of type {@code NumberFormat.Field}, with the attribute value1062* being the same as the attribute key. The prefix and the suffix1063* parts of the returned iterator (if present) are represented by1064* the attributes {@link NumberFormat.Field#PREFIX} and1065* {@link NumberFormat.Field#SUFFIX} respectively.1066*1067*1068* @throws NullPointerException if obj is null1069* @throws IllegalArgumentException when the Format cannot format the1070* given object1071* @throws ArithmeticException if rounding is needed with rounding1072* mode being set to {@code RoundingMode.UNNECESSARY}1073* @param obj The object to format1074* @return an {@code AttributedCharacterIterator} describing the1075* formatted value1076*/1077@Override1078public AttributedCharacterIterator formatToCharacterIterator(Object obj) {1079CharacterIteratorFieldDelegate delegate1080= new CharacterIteratorFieldDelegate();1081StringBuffer sb = new StringBuffer();10821083if (obj instanceof Double || obj instanceof Float) {1084format(((Number) obj).doubleValue(), sb, delegate);1085} else if (obj instanceof Long || obj instanceof Integer1086|| obj instanceof Short || obj instanceof Byte1087|| obj instanceof AtomicInteger || obj instanceof AtomicLong) {1088format(((Number) obj).longValue(), sb, delegate);1089} else if (obj instanceof BigDecimal) {1090format((BigDecimal) obj, sb, delegate);1091} else if (obj instanceof BigInteger) {1092format((BigInteger) obj, sb, delegate, false);1093} else if (obj == null) {1094throw new NullPointerException(1095"formatToCharacterIterator must be passed non-null object");1096} else {1097throw new IllegalArgumentException(1098"Cannot format given Object as a Number");1099}1100return delegate.getIterator(sb.toString());1101}11021103/**1104* Computes the divisor using minimum integer digits and1105* matched pattern index.1106* @param minIntDigits string of 0s in compact pattern1107* @param patternIndex index of matched compact pattern1108* @return divisor value for the number matching the compact1109* pattern at given {@code patternIndex}1110*/1111private Number computeDivisor(String minIntDigits, int patternIndex) {1112int count = minIntDigits.length();1113Number matchedValue;1114// The divisor value can go above long range, if the compact patterns1115// goes above index 18, divisor may need to be stored as BigInteger,1116// since long can't store numbers >= 10^19,1117if (patternIndex < 19) {1118matchedValue = (long) Math.pow(RANGE_MULTIPLIER, patternIndex);1119} else {1120matchedValue = BigInteger.valueOf(RANGE_MULTIPLIER).pow(patternIndex);1121}1122Number divisor = matchedValue;1123if (count > 0) {1124if (matchedValue instanceof BigInteger bigValue) {1125if (bigValue.compareTo(BigInteger.valueOf((long) Math.pow(RANGE_MULTIPLIER, count - 1))) < 0) {1126throw new IllegalArgumentException("Invalid Pattern"1127+ " [" + compactPatterns[patternIndex]1128+ "]: min integer digits specified exceeds the limit"1129+ " for the index " + patternIndex);1130}1131divisor = bigValue.divide(BigInteger.valueOf((long) Math.pow(RANGE_MULTIPLIER, count - 1)));1132} else {1133long longValue = (long) matchedValue;1134if (longValue < (long) Math.pow(RANGE_MULTIPLIER, count - 1)) {1135throw new IllegalArgumentException("Invalid Pattern"1136+ " [" + compactPatterns[patternIndex]1137+ "]: min integer digits specified exceeds the limit"1138+ " for the index " + patternIndex);1139}1140divisor = longValue / (long) Math.pow(RANGE_MULTIPLIER, count - 1);1141}1142}1143return divisor;1144}11451146/**1147* Process the series of compact patterns to compute the1148* series of prefixes, suffixes and their respective divisor1149* value.1150*1151*/1152private static final Pattern PLURALS =1153Pattern.compile("^\\{(?<plurals>.*)}$");1154private static final Pattern COUNT_PATTERN =1155Pattern.compile("(zero|one|two|few|many|other):((' '|[^ ])+)[ ]*");1156private void processCompactPatterns() {1157int size = compactPatterns.length;1158positivePrefixPatterns = new ArrayList<>(size);1159negativePrefixPatterns = new ArrayList<>(size);1160positiveSuffixPatterns = new ArrayList<>(size);1161negativeSuffixPatterns = new ArrayList<>(size);1162divisors = new ArrayList<>(size);1163placeHolderPatterns = new ArrayList<>(size);11641165for (int index = 0; index < size; index++) {1166String text = compactPatterns[index];1167positivePrefixPatterns.add(new Patterns());1168negativePrefixPatterns.add(new Patterns());1169positiveSuffixPatterns.add(new Patterns());1170negativeSuffixPatterns.add(new Patterns());1171placeHolderPatterns.add(new Patterns());11721173// check if it is the old style1174Matcher m = text != null ? PLURALS.matcher(text) : null;1175if (m != null && m.matches()) {1176final int idx = index;1177String plurals = m.group("plurals");1178COUNT_PATTERN.matcher(plurals).results()1179.forEach(mr -> applyPattern(mr.group(1), mr.group(2), idx));1180} else {1181applyPattern("other", text, index);1182}1183}11841185rulesMap = buildPluralRulesMap();1186}11871188/**1189* Build the plural rules map.1190*1191* @throws IllegalArgumentException if the {@code pluralRules} has invalid syntax,1192* or its length exceeds 2,048 chars1193*/1194private Map<String, String> buildPluralRulesMap() {1195// length limitation check. 2K for now.1196if (pluralRules.length() > 2_048) {1197throw new IllegalArgumentException("plural rules is too long (> 2,048)");1198}11991200try {1201return Arrays.stream(pluralRules.split(";"))1202.map(this::validateRule)1203.collect(Collectors.toMap(1204r -> r.replaceFirst(":.*", ""),1205r -> r.replaceFirst("[^:]+:", "")1206));1207} catch (IllegalStateException ise) {1208throw new IllegalArgumentException(ise);1209}1210}12111212// Patterns for plurals syntax validation1213private static final String EXPR = "([niftvwe])\\s*(([/%])\\s*(\\d+))*";1214private static final String RELATION = "(!?=)";1215private static final String VALUE_RANGE = "((\\d+)\\.\\.(\\d+)|\\d+)";1216private static final String CONDITION = EXPR + "\\s*" +1217RELATION + "\\s*" +1218VALUE_RANGE + "\\s*" +1219"(,\\s*" + VALUE_RANGE + ")*";1220private static final Pattern PLURALRULES_PATTERN =1221Pattern.compile("(zero|one|two|few|many):\\s*" +1222CONDITION +1223"(\\s*(and|or)\\s*" + CONDITION + ")*");12241225/**1226* Validates a plural rule.1227* @param rule rule to validate1228* @throws IllegalArgumentException if the {@code rule} has invalid syntax1229* @return the input rule (trimmed)1230*/1231private String validateRule(String rule) {1232rule = rule.trim();1233if (!rule.isEmpty() && !rule.equals("other:")) {1234Matcher validator = PLURALRULES_PATTERN.matcher(rule);1235if (!validator.matches()) {1236throw new IllegalArgumentException("Invalid plural rules syntax: " + rule);1237}1238}12391240return rule;1241}12421243/**1244* Process a compact pattern at a specific {@code index}1245* @param pattern the compact pattern to be processed1246* @param index index in the array of compact patterns1247*1248*/1249private void applyPattern(String count, String pattern, int index) {12501251if (pattern == null) {1252throw new IllegalArgumentException("A null compact pattern" +1253" encountered at index: " + index);1254}12551256int start = 0;1257boolean gotNegative = false;12581259String positivePrefix = "";1260String positiveSuffix = "";1261String negativePrefix = "";1262String negativeSuffix = "";1263String zeros = "";1264for (int j = 1; j >= 0 && start < pattern.length(); --j) {12651266StringBuffer prefix = new StringBuffer();1267StringBuffer suffix = new StringBuffer();1268boolean inQuote = false;1269// The phase ranges from 0 to 2. Phase 0 is the prefix. Phase 1 is1270// the section of the pattern with digits. Phase 2 is the suffix.1271// The separation of the characters into phases is1272// strictly enforced; if phase 1 characters are to appear in the1273// suffix, for example, they must be quoted.1274int phase = 0;12751276// The affix is either the prefix or the suffix.1277StringBuffer affix = prefix;12781279for (int pos = start; pos < pattern.length(); ++pos) {1280char ch = pattern.charAt(pos);1281switch (phase) {1282case 0:1283case 2:1284// Process the prefix / suffix characters1285if (inQuote) {1286// A quote within quotes indicates either the closing1287// quote or two quotes, which is a quote literal. That1288// is, we have the second quote in 'do' or 'don''t'.1289if (ch == QUOTE) {1290if ((pos + 1) < pattern.length()1291&& pattern.charAt(pos + 1) == QUOTE) {1292++pos;1293affix.append("''"); // 'don''t'1294} else {1295inQuote = false; // 'do'1296}1297continue;1298}1299} else {1300// Process unquoted characters seen in prefix or suffix1301// phase.1302switch (ch) {1303case ZERO_DIGIT:1304phase = 1;1305--pos; // Reprocess this character1306continue;1307case QUOTE:1308// A quote outside quotes indicates either the1309// opening quote or two quotes, which is a quote1310// literal. That is, we have the first quote in 'do'1311// or o''clock.1312if ((pos + 1) < pattern.length()1313&& pattern.charAt(pos + 1) == QUOTE) {1314++pos;1315affix.append("''"); // o''clock1316} else {1317inQuote = true; // 'do'1318}1319continue;1320case SEPARATOR:1321// Don't allow separators before we see digit1322// characters of phase 1, and don't allow separators1323// in the second pattern (j == 0).1324if (phase == 0 || j == 0) {1325throw new IllegalArgumentException(1326"Unquoted special character '"1327+ ch + "' in pattern \"" + pattern + "\"");1328}1329start = pos + 1;1330pos = pattern.length();1331continue;1332case MINUS_SIGN:1333affix.append("'-");1334continue;1335case DECIMAL_SEPARATOR:1336case GROUPING_SEPARATOR:1337case DIGIT:1338case PERCENT:1339case PER_MILLE:1340case CURRENCY_SIGN:1341throw new IllegalArgumentException(1342"Unquoted special character '" + ch1343+ "' in pattern \"" + pattern + "\"");1344default:1345break;1346}1347}1348// Note that if we are within quotes, or if this is an1349// unquoted, non-special character, then we usually fall1350// through to here.1351affix.append(ch);1352break;13531354case 1:1355// The negative subpattern (j = 0) serves only to specify the1356// negative prefix and suffix, so all the phase 1 characters,1357// for example, digits, zeroDigit, groupingSeparator,1358// decimalSeparator, exponent are ignored1359if (j == 0) {1360while (pos < pattern.length()) {1361char negPatternChar = pattern.charAt(pos);1362if (negPatternChar == ZERO_DIGIT) {1363++pos;1364} else {1365// Not a phase 1 character, consider it as1366// suffix and parse it in phase 21367--pos; //process it again in outer loop1368phase = 2;1369affix = suffix;1370break;1371}1372}1373continue;1374}1375// Consider only '0' as valid pattern char which can appear1376// in number part, rest can be either suffix or prefix1377if (ch == ZERO_DIGIT) {1378zeros = zeros + "0";1379} else {1380phase = 2;1381affix = suffix;1382--pos;1383}1384break;1385}1386}13871388if (inQuote) {1389throw new IllegalArgumentException("Invalid single quote"1390+ " in pattern \"" + pattern + "\"");1391}13921393if (j == 1) {1394positivePrefix = prefix.toString();1395positiveSuffix = suffix.toString();1396negativePrefix = positivePrefix;1397negativeSuffix = positiveSuffix;1398} else {1399negativePrefix = prefix.toString();1400negativeSuffix = suffix.toString();1401gotNegative = true;1402}14031404// If there is no negative pattern, or if the negative pattern is1405// identical to the positive pattern, then prepend the minus sign to1406// the positive pattern to form the negative pattern.1407if (!gotNegative1408|| (negativePrefix.equals(positivePrefix)1409&& negativeSuffix.equals(positiveSuffix))) {1410negativeSuffix = positiveSuffix;1411negativePrefix = "'-" + positivePrefix;1412}1413}14141415// Only if positive affix exists; else put empty strings1416if (!positivePrefix.isEmpty() || !positiveSuffix.isEmpty()) {1417positivePrefixPatterns.get(index).put(count, positivePrefix);1418negativePrefixPatterns.get(index).put(count, negativePrefix);1419positiveSuffixPatterns.get(index).put(count, positiveSuffix);1420negativeSuffixPatterns.get(index).put(count, negativeSuffix);1421placeHolderPatterns.get(index).put(count, zeros);1422if (divisors.size() <= index) {1423divisors.add(computeDivisor(zeros, index));1424}1425} else {1426positivePrefixPatterns.get(index).put(count, "");1427negativePrefixPatterns.get(index).put(count, "");1428positiveSuffixPatterns.get(index).put(count, "");1429negativeSuffixPatterns.get(index).put(count, "");1430placeHolderPatterns.get(index).put(count, "");1431if (divisors.size() <= index) {1432divisors.add(1L);1433}1434}1435}14361437private final transient DigitList digitList = new DigitList();1438private static final int STATUS_INFINITE = 0;1439private static final int STATUS_POSITIVE = 1;1440private static final int STATUS_LENGTH = 2;14411442private static final char ZERO_DIGIT = '0';1443private static final char DIGIT = '#';1444private static final char DECIMAL_SEPARATOR = '.';1445private static final char GROUPING_SEPARATOR = ',';1446private static final char MINUS_SIGN = '-';1447private static final char PERCENT = '%';1448private static final char PER_MILLE = '\u2030';1449private static final char SEPARATOR = ';';1450private static final char CURRENCY_SIGN = '\u00A4';1451private static final char QUOTE = '\'';14521453// Expanded form of positive/negative prefix/suffix,1454// the expanded form contains special characters in1455// its localized form, which are used for matching1456// while parsing a string to number1457private transient List<Patterns> positivePrefixes;1458private transient List<Patterns> negativePrefixes;1459private transient List<Patterns> positiveSuffixes;1460private transient List<Patterns> negativeSuffixes;14611462private void expandAffixPatterns() {1463positivePrefixes = new ArrayList<>(compactPatterns.length);1464negativePrefixes = new ArrayList<>(compactPatterns.length);1465positiveSuffixes = new ArrayList<>(compactPatterns.length);1466negativeSuffixes = new ArrayList<>(compactPatterns.length);1467for (int index = 0; index < compactPatterns.length; index++) {1468positivePrefixes.add(positivePrefixPatterns.get(index).expandAffix());1469negativePrefixes.add(negativePrefixPatterns.get(index).expandAffix());1470positiveSuffixes.add(positiveSuffixPatterns.get(index).expandAffix());1471negativeSuffixes.add(negativeSuffixPatterns.get(index).expandAffix());1472}1473}14741475/**1476* Parses a compact number from a string to produce a {@code Number}.1477* <p>1478* The method attempts to parse text starting at the index given by1479* {@code pos}.1480* If parsing succeeds, then the index of {@code pos} is updated1481* to the index after the last character used (parsing does not necessarily1482* use all characters up to the end of the string), and the parsed1483* number is returned. The updated {@code pos} can be used to1484* indicate the starting point for the next call to this method.1485* If an error occurs, then the index of {@code pos} is not1486* changed, the error index of {@code pos} is set to the index of1487* the character where the error occurred, and {@code null} is returned.1488* <p>1489* The value is the numeric part in the given text multiplied1490* by the numeric equivalent of the affix attached1491* (For example, "K" = 1000 in {@link java.util.Locale#US US locale}).1492* The subclass returned depends on the value of1493* {@link #isParseBigDecimal}.1494* <ul>1495* <li>If {@link #isParseBigDecimal()} is false (the default),1496* most integer values are returned as {@code Long}1497* objects, no matter how they are written: {@code "17K"} and1498* {@code "17.000K"} both parse to {@code Long.valueOf(17000)}.1499* If the value cannot fit into {@code Long}, then the result is1500* returned as {@code Double}. This includes values with a1501* fractional part, infinite values, {@code NaN},1502* and the value -0.0.1503* <p>1504* Callers may use the {@code Number} methods {@code doubleValue},1505* {@code longValue}, etc., to obtain the type they want.1506*1507* <li>If {@link #isParseBigDecimal()} is true, values are returned1508* as {@code BigDecimal} objects. The special cases negative1509* and positive infinity and NaN are returned as {@code Double}1510* instances holding the values of the corresponding1511* {@code Double} constants.1512* </ul>1513* <p>1514* {@code CompactNumberFormat} parses all Unicode characters that represent1515* decimal digits, as defined by {@code Character.digit()}. In1516* addition, {@code CompactNumberFormat} also recognizes as digits the ten1517* consecutive characters starting with the localized zero digit defined in1518* the {@code DecimalFormatSymbols} object.1519* <p>1520* {@code CompactNumberFormat} parse does not allow parsing scientific1521* notations. For example, parsing a string {@code "1.05E4K"} in1522* {@link java.util.Locale#US US locale} breaks at character 'E'1523* and returns 1.05.1524*1525* @param text the string to be parsed1526* @param pos a {@code ParsePosition} object with index and error1527* index information as described above1528* @return the parsed value, or {@code null} if the parse fails1529* @throws NullPointerException if {@code text} or1530* {@code pos} is null1531*1532*/1533@Override1534public Number parse(String text, ParsePosition pos) {15351536Objects.requireNonNull(text);1537Objects.requireNonNull(pos);15381539// Lazily expanding the affix patterns, on the first parse1540// call on this instance1541// If not initialized, expand and load all affixes1542if (positivePrefixes == null) {1543expandAffixPatterns();1544}15451546// The compact number multiplier for parsed string.1547// Its value is set on parsing prefix and suffix. For example,1548// in the {@link java.util.Locale#US US locale} parsing {@code "1K"}1549// sets its value to 1000, as K (thousand) is abbreviated form of 1000.1550Number cnfMultiplier = 1L;15511552// Special case NaN1553if (text.regionMatches(pos.index, symbols.getNaN(),15540, symbols.getNaN().length())) {1555pos.index = pos.index + symbols.getNaN().length();1556return Double.NaN;1557}15581559int position = pos.index;1560int oldStart = pos.index;1561boolean gotPositive = false;1562boolean gotNegative = false;1563int matchedPosIndex = -1;1564int matchedNegIndex = -1;1565String matchedPosPrefix = "";1566String matchedNegPrefix = "";1567String defaultPosPrefix = defaultDecimalFormat.getPositivePrefix();1568String defaultNegPrefix = defaultDecimalFormat.getNegativePrefix();1569double num = parseNumberPart(text, position);15701571// Prefix matching1572for (int compactIndex = 0; compactIndex < compactPatterns.length; compactIndex++) {1573String positivePrefix = getAffix(true, true, false, compactIndex, (int)num);1574String negativePrefix = getAffix(true, true, true, compactIndex, (int)num);15751576// Do not break if a match occur; there is a possibility that the1577// subsequent affixes may match the longer subsequence in the given1578// string.1579// For example, matching "Mdx 3" with "M", "Md" as prefix should1580// match with "Md"1581boolean match = matchAffix(text, position, positivePrefix,1582defaultPosPrefix, matchedPosPrefix);1583if (match) {1584matchedPosIndex = compactIndex;1585matchedPosPrefix = positivePrefix;1586gotPositive = true;1587}15881589match = matchAffix(text, position, negativePrefix,1590defaultNegPrefix, matchedNegPrefix);1591if (match) {1592matchedNegIndex = compactIndex;1593matchedNegPrefix = negativePrefix;1594gotNegative = true;1595}1596}15971598// Given text does not match the non empty valid compact prefixes1599// check with the default prefixes1600if (!gotPositive && !gotNegative) {1601if (text.regionMatches(pos.index, defaultPosPrefix, 0,1602defaultPosPrefix.length())) {1603// Matches the default positive prefix1604matchedPosPrefix = defaultPosPrefix;1605gotPositive = true;1606}1607if (text.regionMatches(pos.index, defaultNegPrefix, 0,1608defaultNegPrefix.length())) {1609// Matches the default negative prefix1610matchedNegPrefix = defaultNegPrefix;1611gotNegative = true;1612}1613}16141615// If both match, take the longest one1616if (gotPositive && gotNegative) {1617if (matchedPosPrefix.length() > matchedNegPrefix.length()) {1618gotNegative = false;1619} else if (matchedPosPrefix.length() < matchedNegPrefix.length()) {1620gotPositive = false;1621}1622}16231624// Update the position and take compact multiplier1625// only if it matches the compact prefix, not the default1626// prefix; else multiplier should be 11627// If there's no number part, no need to go further, just1628// return the multiplier.1629if (gotPositive || gotNegative) {1630position += gotPositive ? matchedPosPrefix.length() : matchedNegPrefix.length();1631int matchedIndex = gotPositive ? matchedPosIndex : matchedNegIndex;1632if (matchedIndex != -1) {1633cnfMultiplier = divisors.get(matchedIndex);1634if (placeHolderPatterns.get(matchedIndex).get(num).isEmpty()) {1635pos.index = position;1636return cnfMultiplier;1637}1638}1639}16401641digitList.setRoundingMode(getRoundingMode());1642boolean[] status = new boolean[STATUS_LENGTH];16431644// Call DecimalFormat.subparseNumber() method to parse the1645// number part of the input text1646position = decimalFormat.subparseNumber(text, position,1647digitList, false, false, status);16481649if (position == -1) {1650// Unable to parse the number successfully1651pos.index = oldStart;1652pos.errorIndex = oldStart;1653return null;1654}16551656// If parse integer only is true and the parsing is broken at1657// decimal point, then pass/ignore all digits and move pointer1658// at the start of suffix, to process the suffix part1659if (isParseIntegerOnly()1660&& text.charAt(position) == symbols.getDecimalSeparator()) {1661position++; // Pass decimal character1662for (; position < text.length(); ++position) {1663char ch = text.charAt(position);1664int digit = ch - symbols.getZeroDigit();1665if (digit < 0 || digit > 9) {1666digit = Character.digit(ch, 10);1667// Parse all digit characters1668if (!(digit >= 0 && digit <= 9)) {1669break;1670}1671}1672}1673}16741675// Number parsed successfully; match prefix and1676// suffix to obtain multiplier1677pos.index = position;1678Number multiplier = computeParseMultiplier(text, pos,1679gotPositive ? matchedPosPrefix : matchedNegPrefix,1680status, gotPositive, gotNegative, num);16811682if (multiplier.longValue() == -1L) {1683return null;1684} else if (multiplier.longValue() != 1L) {1685cnfMultiplier = multiplier;1686}16871688// Special case INFINITY1689if (status[STATUS_INFINITE]) {1690if (status[STATUS_POSITIVE]) {1691return Double.POSITIVE_INFINITY;1692} else {1693return Double.NEGATIVE_INFINITY;1694}1695}16961697if (isParseBigDecimal()) {1698BigDecimal bigDecimalResult = digitList.getBigDecimal();16991700if (cnfMultiplier.longValue() != 1) {1701bigDecimalResult = bigDecimalResult1702.multiply(new BigDecimal(cnfMultiplier.toString()));1703}1704if (!status[STATUS_POSITIVE]) {1705bigDecimalResult = bigDecimalResult.negate();1706}1707return bigDecimalResult;1708} else {1709Number cnfResult;1710if (digitList.fitsIntoLong(status[STATUS_POSITIVE], isParseIntegerOnly())) {1711long longResult = digitList.getLong();1712cnfResult = generateParseResult(longResult, false,1713longResult < 0, status, cnfMultiplier);1714} else {1715cnfResult = generateParseResult(digitList.getDouble(),1716true, false, status, cnfMultiplier);1717}1718return cnfResult;1719}1720}17211722private static final Pattern DIGITS = Pattern.compile("\\p{Nd}+");1723/**1724* Parse the number part in the input text into a number1725*1726* @param text input text to be parsed1727* @param position starting position1728* @return the number1729*/1730private double parseNumberPart(String text, int position) {1731if (text.startsWith(symbols.getInfinity(), position)) {1732return Double.POSITIVE_INFINITY;1733} else if (!text.startsWith(symbols.getNaN(), position)) {1734Matcher m = DIGITS.matcher(text);1735if (m.find(position)) {1736String digits = m.group();1737int cp = digits.codePointAt(0);1738if (Character.isDigit(cp)) {1739return Double.parseDouble(digits.codePoints()1740.map(Character::getNumericValue)1741.mapToObj(Integer::toString)1742.collect(Collectors.joining()));1743}1744} else {1745// no numbers. return 1.0 for possible no-placeholder pattern1746return 1.0;1747}1748}1749return Double.NaN;1750}17511752/**1753* Returns the parsed result by multiplying the parsed number1754* with the multiplier representing the prefix and suffix.1755*1756* @param number parsed number component1757* @param gotDouble whether the parsed number contains decimal1758* @param gotLongMin whether the parsed number is Long.MIN1759* @param status boolean status flags indicating whether the1760* value is infinite and whether it is positive1761* @param cnfMultiplier compact number multiplier1762* @return parsed result1763*/1764private Number generateParseResult(Number number, boolean gotDouble,1765boolean gotLongMin, boolean[] status, Number cnfMultiplier) {17661767if (gotDouble) {1768if (cnfMultiplier.longValue() != 1L) {1769double doubleResult = number.doubleValue() * cnfMultiplier.doubleValue();1770doubleResult = (double) convertIfNegative(doubleResult, status, gotLongMin);1771// Check if a double can be represeneted as a long1772long longResult = (long) doubleResult;1773gotDouble = ((doubleResult != (double) longResult)1774|| (doubleResult == 0.0 && 1 / doubleResult < 0.0));1775return gotDouble ? (Number) doubleResult : (Number) longResult;1776}1777} else {1778if (cnfMultiplier.longValue() != 1L) {1779Number result;1780if ((cnfMultiplier instanceof Long) && !gotLongMin) {1781long longMultiplier = (long) cnfMultiplier;1782try {1783result = Math.multiplyExact(number.longValue(),1784longMultiplier);1785} catch (ArithmeticException ex) {1786// If number * longMultiplier can not be represented1787// as long return as double1788result = number.doubleValue() * cnfMultiplier.doubleValue();1789}1790} else {1791// cnfMultiplier can not be stored into long or the number1792// part is Long.MIN, return as double1793result = number.doubleValue() * cnfMultiplier.doubleValue();1794}1795return convertIfNegative(result, status, gotLongMin);1796}1797}17981799// Default number1800return convertIfNegative(number, status, gotLongMin);1801}18021803/**1804* Negate the parsed value if the positive status flag is false1805* and the value is not a Long.MIN1806* @param number parsed value1807* @param status boolean status flags indicating whether the1808* value is infinite and whether it is positive1809* @param gotLongMin whether the parsed number is Long.MIN1810* @return the resulting value1811*/1812private Number convertIfNegative(Number number, boolean[] status,1813boolean gotLongMin) {18141815if (!status[STATUS_POSITIVE] && !gotLongMin) {1816if (number instanceof Long) {1817return -(long) number;1818} else {1819return -(double) number;1820}1821} else {1822return number;1823}1824}18251826/**1827* Attempts to match the given {@code affix} in the1828* specified {@code text}.1829*/1830private boolean matchAffix(String text, int position, String affix,1831String defaultAffix, String matchedAffix) {18321833// Check with the compact affixes which are non empty and1834// do not match with default affix1835if (!affix.isEmpty() && !affix.equals(defaultAffix)) {1836// Look ahead only for the longer match than the previous match1837if (matchedAffix.length() < affix.length()) {1838return text.regionMatches(position, affix, 0, affix.length());1839}1840}1841return false;1842}18431844/**1845* Attempts to match given {@code prefix} and {@code suffix} in1846* the specified {@code text}.1847*/1848private boolean matchPrefixAndSuffix(String text, int position, String prefix,1849String matchedPrefix, String defaultPrefix, String suffix,1850String matchedSuffix, String defaultSuffix) {18511852// Check the compact pattern suffix only if there is a1853// compact prefix match or a default prefix match1854// because the compact prefix and suffix should match at the same1855// index to obtain the multiplier.1856// The prefix match is required because of the possibility of1857// same prefix at multiple index, in which case matching the suffix1858// is used to obtain the single match18591860if (prefix.equals(matchedPrefix)1861|| matchedPrefix.equals(defaultPrefix)) {1862return matchAffix(text, position, suffix, defaultSuffix, matchedSuffix);1863}1864return false;1865}18661867/**1868* Computes multiplier by matching the given {@code matchedPrefix}1869* and suffix in the specified {@code text} from the lists of1870* prefixes and suffixes extracted from compact patterns.1871*1872* @param text the string to parse1873* @param parsePosition the {@code ParsePosition} object representing the1874* index and error index of the parse string1875* @param matchedPrefix prefix extracted which needs to be matched to1876* obtain the multiplier1877* @param status upon return contains boolean status flags indicating1878* whether the value is positive1879* @param gotPositive based on the prefix parsed; whether the number is positive1880* @param gotNegative based on the prefix parsed; whether the number is negative1881* @return the multiplier matching the prefix and suffix; -1 otherwise1882*/1883private Number computeParseMultiplier(String text, ParsePosition parsePosition,1884String matchedPrefix, boolean[] status, boolean gotPositive,1885boolean gotNegative, double num) {18861887int position = parsePosition.index;1888boolean gotPos = false;1889boolean gotNeg = false;1890int matchedPosIndex = -1;1891int matchedNegIndex = -1;1892String matchedPosSuffix = "";1893String matchedNegSuffix = "";1894for (int compactIndex = 0; compactIndex < compactPatterns.length; compactIndex++) {1895String positivePrefix = getAffix(true, true, false, compactIndex, (int)num);1896String negativePrefix = getAffix(true, true, true, compactIndex, (int)num);1897String positiveSuffix = getAffix(true, false, false, compactIndex, (int)num);1898String negativeSuffix = getAffix(true, false, true, compactIndex, (int)num);18991900// Do not break if a match occur; there is a possibility that the1901// subsequent affixes may match the longer subsequence in the given1902// string.1903// For example, matching "3Mdx" with "M", "Md" should match with "Md"1904boolean match = matchPrefixAndSuffix(text, position, positivePrefix, matchedPrefix,1905defaultDecimalFormat.getPositivePrefix(), positiveSuffix,1906matchedPosSuffix, defaultDecimalFormat.getPositiveSuffix());1907if (match) {1908matchedPosIndex = compactIndex;1909matchedPosSuffix = positiveSuffix;1910gotPos = true;1911}19121913match = matchPrefixAndSuffix(text, position, negativePrefix, matchedPrefix,1914defaultDecimalFormat.getNegativePrefix(), negativeSuffix,1915matchedNegSuffix, defaultDecimalFormat.getNegativeSuffix());1916if (match) {1917matchedNegIndex = compactIndex;1918matchedNegSuffix = negativeSuffix;1919gotNeg = true;1920}1921}19221923// Suffix in the given text does not match with the compact1924// patterns suffixes; match with the default suffix1925if (!gotPos && !gotNeg) {1926String positiveSuffix = defaultDecimalFormat.getPositiveSuffix();1927String negativeSuffix = defaultDecimalFormat.getNegativeSuffix();1928if (text.regionMatches(position, positiveSuffix, 0,1929positiveSuffix.length())) {1930// Matches the default positive prefix1931matchedPosSuffix = positiveSuffix;1932gotPos = true;1933}1934if (text.regionMatches(position, negativeSuffix, 0,1935negativeSuffix.length())) {1936// Matches the default negative suffix1937matchedNegSuffix = negativeSuffix;1938gotNeg = true;1939}1940}19411942// If both matches, take the longest one1943if (gotPos && gotNeg) {1944if (matchedPosSuffix.length() > matchedNegSuffix.length()) {1945gotNeg = false;1946} else if (matchedPosSuffix.length() < matchedNegSuffix.length()) {1947gotPos = false;1948} else {1949// If longest comparison fails; take the positive and negative1950// sign of matching prefix1951gotPos = gotPositive;1952gotNeg = gotNegative;1953}1954}19551956// Fail if neither or both1957if (gotPos == gotNeg) {1958parsePosition.errorIndex = position;1959return -1L;1960}19611962Number cnfMultiplier;1963// Update the parse position index and take compact multiplier1964// only if it matches the compact suffix, not the default1965// suffix; else multiplier should be 11966if (gotPos) {1967parsePosition.index = position + matchedPosSuffix.length();1968cnfMultiplier = matchedPosIndex != -11969? divisors.get(matchedPosIndex) : 1L;1970} else {1971parsePosition.index = position + matchedNegSuffix.length();1972cnfMultiplier = matchedNegIndex != -11973? divisors.get(matchedNegIndex) : 1L;1974}1975status[STATUS_POSITIVE] = gotPos;1976return cnfMultiplier;1977}19781979/**1980* Reconstitutes this {@code CompactNumberFormat} from a stream1981* (that is, deserializes it) after performing some validations.1982* This method throws InvalidObjectException, if the stream data is invalid1983* because of the following reasons,1984* <ul>1985* <li> If any of the {@code decimalPattern}, {@code compactPatterns},1986* {@code symbols} or {@code roundingMode} is {@code null}.1987* <li> If the {@code decimalPattern} or the {@code compactPatterns} array1988* contains an invalid pattern or if a {@code null} appears in the array of1989* compact patterns.1990* <li> If the {@code minimumIntegerDigits} is greater than the1991* {@code maximumIntegerDigits} or the {@code minimumFractionDigits} is1992* greater than the {@code maximumFractionDigits}. This check is performed1993* by superclass's Object.1994* <li> If any of the minimum/maximum integer/fraction digit count is1995* negative. This check is performed by superclass's readObject.1996* <li> If the minimum or maximum integer digit count is larger than 309 or1997* if the minimum or maximum fraction digit count is larger than 340.1998* <li> If the grouping size is negative or larger than 127.1999* </ul>2000* If the {@code pluralRules} field is not deserialized from the stream, it2001* will be set to an empty string.2002*2003* @param inStream the stream2004* @throws IOException if an I/O error occurs2005* @throws ClassNotFoundException if the class of a serialized object2006* could not be found2007*/2008@java.io.Serial2009private void readObject(ObjectInputStream inStream) throws IOException,2010ClassNotFoundException {20112012inStream.defaultReadObject();2013if (decimalPattern == null || compactPatterns == null2014|| symbols == null || roundingMode == null) {2015throw new InvalidObjectException("One of the 'decimalPattern',"2016+ " 'compactPatterns', 'symbols' or 'roundingMode'"2017+ " is null");2018}20192020// Check only the maximum counts because NumberFormat.readObject has2021// already ensured that the maximum is greater than the minimum count.2022if (getMaximumIntegerDigits() > DecimalFormat.DOUBLE_INTEGER_DIGITS2023|| getMaximumFractionDigits() > DecimalFormat.DOUBLE_FRACTION_DIGITS) {2024throw new InvalidObjectException("Digit count out of range");2025}20262027// Check if the grouping size is negative, on an attempt to2028// put value > 127, it wraps around, so check just negative value2029if (groupingSize < 0) {2030throw new InvalidObjectException("Grouping size is negative");2031}20322033// pluralRules is since 14. Fill in empty string if it is null2034if (pluralRules == null) {2035pluralRules = "";2036}20372038try {2039processCompactPatterns();2040} catch (IllegalArgumentException ex) {2041throw new InvalidObjectException(ex.getMessage());2042}20432044decimalFormat = new DecimalFormat(SPECIAL_PATTERN, symbols);2045decimalFormat.setMaximumFractionDigits(getMaximumFractionDigits());2046decimalFormat.setMinimumFractionDigits(getMinimumFractionDigits());2047decimalFormat.setMaximumIntegerDigits(getMaximumIntegerDigits());2048decimalFormat.setMinimumIntegerDigits(getMinimumIntegerDigits());2049decimalFormat.setRoundingMode(getRoundingMode());2050decimalFormat.setGroupingSize(getGroupingSize());2051decimalFormat.setGroupingUsed(isGroupingUsed());2052decimalFormat.setParseIntegerOnly(isParseIntegerOnly());20532054try {2055defaultDecimalFormat = new DecimalFormat(decimalPattern, symbols);2056defaultDecimalFormat.setMaximumFractionDigits(0);2057} catch (IllegalArgumentException ex) {2058throw new InvalidObjectException(ex.getMessage());2059}20602061}20622063/**2064* Sets the maximum number of digits allowed in the integer portion of a2065* number.2066* The maximum allowed integer range is 309, if the {@code newValue} > 309,2067* then the maximum integer digits count is set to 309. Negative input2068* values are replaced with 0.2069*2070* @param newValue the maximum number of integer digits to be shown2071* @see #getMaximumIntegerDigits()2072*/2073@Override2074public void setMaximumIntegerDigits(int newValue) {2075// The maximum integer digits is checked with the allowed range before calling2076// the DecimalFormat.setMaximumIntegerDigits, which performs the negative check2077// on the given newValue while setting it as max integer digits.2078// For example, if a negative value is specified, it is replaced with 02079decimalFormat.setMaximumIntegerDigits(Math.min(newValue,2080DecimalFormat.DOUBLE_INTEGER_DIGITS));2081super.setMaximumIntegerDigits(decimalFormat.getMaximumIntegerDigits());2082if (decimalFormat.getMinimumIntegerDigits() > decimalFormat.getMaximumIntegerDigits()) {2083decimalFormat.setMinimumIntegerDigits(decimalFormat.getMaximumIntegerDigits());2084super.setMinimumIntegerDigits(decimalFormat.getMinimumIntegerDigits());2085}2086}20872088/**2089* Sets the minimum number of digits allowed in the integer portion of a2090* number.2091* The maximum allowed integer range is 309, if the {@code newValue} > 309,2092* then the minimum integer digits count is set to 309. Negative input2093* values are replaced with 0.2094*2095* @param newValue the minimum number of integer digits to be shown2096* @see #getMinimumIntegerDigits()2097*/2098@Override2099public void setMinimumIntegerDigits(int newValue) {2100// The minimum integer digits is checked with the allowed range before calling2101// the DecimalFormat.setMinimumIntegerDigits, which performs check on the given2102// newValue while setting it as min integer digits. For example, if a negative2103// value is specified, it is replaced with 02104decimalFormat.setMinimumIntegerDigits(Math.min(newValue,2105DecimalFormat.DOUBLE_INTEGER_DIGITS));2106super.setMinimumIntegerDigits(decimalFormat.getMinimumIntegerDigits());2107if (decimalFormat.getMinimumIntegerDigits() > decimalFormat.getMaximumIntegerDigits()) {2108decimalFormat.setMaximumIntegerDigits(decimalFormat.getMinimumIntegerDigits());2109super.setMaximumIntegerDigits(decimalFormat.getMaximumIntegerDigits());2110}2111}21122113/**2114* Sets the minimum number of digits allowed in the fraction portion of a2115* number.2116* The maximum allowed fraction range is 340, if the {@code newValue} > 340,2117* then the minimum fraction digits count is set to 340. Negative input2118* values are replaced with 0.2119*2120* @param newValue the minimum number of fraction digits to be shown2121* @see #getMinimumFractionDigits()2122*/2123@Override2124public void setMinimumFractionDigits(int newValue) {2125// The minimum fraction digits is checked with the allowed range before2126// calling the DecimalFormat.setMinimumFractionDigits, which performs2127// check on the given newValue while setting it as min fraction2128// digits. For example, if a negative value is specified, it is2129// replaced with 02130decimalFormat.setMinimumFractionDigits(Math.min(newValue,2131DecimalFormat.DOUBLE_FRACTION_DIGITS));2132super.setMinimumFractionDigits(decimalFormat.getMinimumFractionDigits());2133if (decimalFormat.getMinimumFractionDigits() > decimalFormat.getMaximumFractionDigits()) {2134decimalFormat.setMaximumFractionDigits(decimalFormat.getMinimumFractionDigits());2135super.setMaximumFractionDigits(decimalFormat.getMaximumFractionDigits());2136}2137}21382139/**2140* Sets the maximum number of digits allowed in the fraction portion of a2141* number.2142* The maximum allowed fraction range is 340, if the {@code newValue} > 340,2143* then the maximum fraction digits count is set to 340. Negative input2144* values are replaced with 0.2145*2146* @param newValue the maximum number of fraction digits to be shown2147* @see #getMaximumFractionDigits()2148*/2149@Override2150public void setMaximumFractionDigits(int newValue) {2151// The maximum fraction digits is checked with the allowed range before2152// calling the DecimalFormat.setMaximumFractionDigits, which performs2153// check on the given newValue while setting it as max fraction digits.2154// For example, if a negative value is specified, it is replaced with 02155decimalFormat.setMaximumFractionDigits(Math.min(newValue,2156DecimalFormat.DOUBLE_FRACTION_DIGITS));2157super.setMaximumFractionDigits(decimalFormat.getMaximumFractionDigits());2158if (decimalFormat.getMinimumFractionDigits() > decimalFormat.getMaximumFractionDigits()) {2159decimalFormat.setMinimumFractionDigits(decimalFormat.getMaximumFractionDigits());2160super.setMinimumFractionDigits(decimalFormat.getMinimumFractionDigits());2161}2162}21632164/**2165* Gets the {@link java.math.RoundingMode} used in this2166* {@code CompactNumberFormat}.2167*2168* @return the {@code RoundingMode} used for this2169* {@code CompactNumberFormat}2170* @see #setRoundingMode(RoundingMode)2171*/2172@Override2173public RoundingMode getRoundingMode() {2174return roundingMode;2175}21762177/**2178* Sets the {@link java.math.RoundingMode} used in this2179* {@code CompactNumberFormat}.2180*2181* @param roundingMode the {@code RoundingMode} to be used2182* @see #getRoundingMode()2183* @throws NullPointerException if {@code roundingMode} is {@code null}2184*/2185@Override2186public void setRoundingMode(RoundingMode roundingMode) {2187decimalFormat.setRoundingMode(roundingMode);2188this.roundingMode = roundingMode;2189}21902191/**2192* Returns the grouping size. Grouping size is the number of digits between2193* grouping separators in the integer portion of a number. For example,2194* in the compact number {@code "12,347 trillion"} for the2195* {@link java.util.Locale#US US locale}, the grouping size is 3.2196*2197* @return the grouping size2198* @see #setGroupingSize2199* @see java.text.NumberFormat#isGroupingUsed2200* @see java.text.DecimalFormatSymbols#getGroupingSeparator2201*/2202public int getGroupingSize() {2203return groupingSize;2204}22052206/**2207* Sets the grouping size. Grouping size is the number of digits between2208* grouping separators in the integer portion of a number. For example,2209* in the compact number {@code "12,347 trillion"} for the2210* {@link java.util.Locale#US US locale}, the grouping size is 3. The grouping2211* size must be greater than or equal to zero and less than or equal to 127.2212*2213* @param newValue the new grouping size2214* @see #getGroupingSize2215* @see java.text.NumberFormat#setGroupingUsed2216* @see java.text.DecimalFormatSymbols#setGroupingSeparator2217* @throws IllegalArgumentException if {@code newValue} is negative or2218* larger than 1272219*/2220public void setGroupingSize(int newValue) {2221if (newValue < 0 || newValue > 127) {2222throw new IllegalArgumentException(2223"The value passed is negative or larger than 127");2224}2225groupingSize = (byte) newValue;2226decimalFormat.setGroupingSize(groupingSize);2227}22282229/**2230* Returns true if grouping is used in this format. For example, with2231* grouping on and grouping size set to 3, the number {@code 12346567890987654}2232* can be formatted as {@code "12,347 trillion"} in the2233* {@link java.util.Locale#US US locale}.2234* The grouping separator is locale dependent.2235*2236* @return {@code true} if grouping is used;2237* {@code false} otherwise2238* @see #setGroupingUsed2239*/2240@Override2241public boolean isGroupingUsed() {2242return super.isGroupingUsed();2243}22442245/**2246* Sets whether or not grouping will be used in this format.2247*2248* @param newValue {@code true} if grouping is used;2249* {@code false} otherwise2250* @see #isGroupingUsed2251*/2252@Override2253public void setGroupingUsed(boolean newValue) {2254decimalFormat.setGroupingUsed(newValue);2255super.setGroupingUsed(newValue);2256}22572258/**2259* Returns true if this format parses only an integer from the number2260* component of a compact number.2261* Parsing an integer means that only an integer is considered from the2262* number component, prefix/suffix is still considered to compute the2263* resulting output.2264* For example, in the {@link java.util.Locale#US US locale}, if this method2265* returns {@code true}, the string {@code "1234.78 thousand"} would be2266* parsed as the value {@code 1234000} (1234 (integer part) * 10002267* (thousand)) and the fractional part would be skipped.2268* The exact format accepted by the parse operation is locale dependent.2269*2270* @return {@code true} if compact numbers should be parsed as integers2271* only; {@code false} otherwise2272*/2273@Override2274public boolean isParseIntegerOnly() {2275return super.isParseIntegerOnly();2276}22772278/**2279* Sets whether or not this format parses only an integer from the number2280* component of a compact number.2281*2282* @param value {@code true} if compact numbers should be parsed as2283* integers only; {@code false} otherwise2284* @see #isParseIntegerOnly2285*/2286@Override2287public void setParseIntegerOnly(boolean value) {2288decimalFormat.setParseIntegerOnly(value);2289super.setParseIntegerOnly(value);2290}22912292/**2293* Returns whether the {@link #parse(String, ParsePosition)}2294* method returns {@code BigDecimal}. The default value is false.2295*2296* @return {@code true} if the parse method returns BigDecimal;2297* {@code false} otherwise2298* @see #setParseBigDecimal2299*2300*/2301public boolean isParseBigDecimal() {2302return parseBigDecimal;2303}23042305/**2306* Sets whether the {@link #parse(String, ParsePosition)}2307* method returns {@code BigDecimal}.2308*2309* @param newValue {@code true} if the parse method returns BigDecimal;2310* {@code false} otherwise2311* @see #isParseBigDecimal2312*2313*/2314public void setParseBigDecimal(boolean newValue) {2315parseBigDecimal = newValue;2316}23172318/**2319* Checks if this {@code CompactNumberFormat} is equal to the2320* specified {@code obj}. The objects of type {@code CompactNumberFormat}2321* are compared, other types return false; obeys the general contract of2322* {@link java.lang.Object#equals(java.lang.Object) Object.equals}.2323*2324* @param obj the object to compare with2325* @return true if this is equal to the other {@code CompactNumberFormat}2326*/2327@Override2328public boolean equals(Object obj) {23292330if (!super.equals(obj)) {2331return false;2332}23332334CompactNumberFormat other = (CompactNumberFormat) obj;2335return decimalPattern.equals(other.decimalPattern)2336&& symbols.equals(other.symbols)2337&& Arrays.equals(compactPatterns, other.compactPatterns)2338&& roundingMode.equals(other.roundingMode)2339&& pluralRules.equals(other.pluralRules)2340&& groupingSize == other.groupingSize2341&& parseBigDecimal == other.parseBigDecimal;2342}23432344/**2345* Returns the hash code for this {@code CompactNumberFormat} instance.2346*2347* @return hash code for this {@code CompactNumberFormat}2348*/2349@Override2350public int hashCode() {2351return 31 * super.hashCode() +2352Objects.hash(decimalPattern, symbols, roundingMode, pluralRules)2353+ Arrays.hashCode(compactPatterns) + groupingSize2354+ Boolean.hashCode(parseBigDecimal);2355}23562357/**2358* Creates and returns a copy of this {@code CompactNumberFormat}2359* instance.2360*2361* @return a clone of this instance2362*/2363@Override2364public CompactNumberFormat clone() {2365CompactNumberFormat other = (CompactNumberFormat) super.clone();2366other.compactPatterns = compactPatterns.clone();2367other.symbols = (DecimalFormatSymbols) symbols.clone();2368return other;2369}23702371/**2372* Abstraction of affix or number (represented by zeros) patterns for each "count" tag.2373*/2374private final class Patterns {2375private final Map<String, String> patternsMap = new HashMap<>();23762377void put(String count, String pattern) {2378patternsMap.put(count, pattern);2379}23802381String get(double num) {2382return patternsMap.getOrDefault(getPluralCategory(num),2383patternsMap.getOrDefault("other", ""));2384}23852386Patterns expandAffix() {2387Patterns ret = new Patterns();2388patternsMap.forEach((key, value) -> ret.put(key, CompactNumberFormat.this.expandAffix(value)));2389return ret;2390}2391}23922393private int getIntegerPart(double number, double divisor) {2394return BigDecimal.valueOf(number)2395.divide(BigDecimal.valueOf(divisor), roundingMode).intValue();2396}23972398/**2399* Returns LDML's tag from the plurals rules2400*2401* @param input input number in double type2402* @return LDML "count" tag2403*/2404private String getPluralCategory(double input) {2405if (rulesMap != null) {2406return rulesMap.entrySet().stream()2407.filter(e -> matchPluralRule(e.getValue(), input))2408.map(Map.Entry::getKey)2409.findFirst()2410.orElse("other");2411}24122413// defaults to "other"2414return "other";2415}24162417private static boolean matchPluralRule(String condition, double input) {2418return Arrays.stream(condition.split("or"))2419.anyMatch(and_condition -> Arrays.stream(and_condition.split("and"))2420.allMatch(r -> relationCheck(r, input)));2421}24222423private static final String NAMED_EXPR = "(?<op>[niftvwe])\\s*((?<div>[/%])\\s*(?<val>\\d+))*";2424private static final String NAMED_RELATION = "(?<rel>!?=)";2425private static final String NAMED_VALUE_RANGE = "(?<start>\\d+)\\.\\.(?<end>\\d+)|(?<value>\\d+)";2426private static final Pattern EXPR_PATTERN = Pattern.compile(NAMED_EXPR);2427private static final Pattern RELATION_PATTERN = Pattern.compile(NAMED_RELATION);2428private static final Pattern VALUE_RANGE_PATTERN = Pattern.compile(NAMED_VALUE_RANGE);24292430/**2431* Checks if the 'input' equals the value, or within the range.2432*2433* @param valueOrRange A string representing either a single value or a range2434* @param input to examine in double2435* @return match indicator2436*/2437private static boolean valOrRangeMatches(String valueOrRange, double input) {2438Matcher m = VALUE_RANGE_PATTERN.matcher(valueOrRange);24392440if (m.find()) {2441String value = m.group("value");2442if (value != null) {2443return input == Double.parseDouble(value);2444} else {2445return input >= Double.parseDouble(m.group("start")) &&2446input <= Double.parseDouble(m.group("end"));2447}2448}24492450return false;2451}24522453/**2454* Checks if the input value satisfies the relation. Each possible value or range is2455* separated by a comma ','2456*2457* @param relation relation string, e.g, "n = 1, 3..5", or "n != 1, 3..5"2458* @param input value to examine in double2459* @return boolean to indicate whether the relation satisfies or not. If the relation2460* is '=', true if any of the possible value/range satisfies. If the relation is '!=',2461* none of the possible value/range should satisfy to return true.2462*/2463private static boolean relationCheck(String relation, double input) {2464Matcher expr = EXPR_PATTERN.matcher(relation);24652466if (expr.find()) {2467double lop = evalLOperand(expr, input);2468Matcher rel = RELATION_PATTERN.matcher(relation);24692470if (rel.find(expr.end())) {2471var conditions =2472Arrays.stream(relation.substring(rel.end()).split(","));24732474if (Objects.equals(rel.group("rel"), "!=")) {2475return conditions.noneMatch(c -> valOrRangeMatches(c, lop));2476} else {2477return conditions.anyMatch(c -> valOrRangeMatches(c, lop));2478}2479}2480}24812482return false;2483}24842485/**2486* Evaluates the left operand value.2487*2488* @param expr Match result2489* @param input value to examine in double2490* @return resulting double value2491*/2492private static double evalLOperand(Matcher expr, double input) {2493double ret = 0;24942495if (input == Double.POSITIVE_INFINITY) {2496ret = input;2497} else {2498String op = expr.group("op");2499if (Objects.equals(op, "n") || Objects.equals(op, "i")) {2500ret = input;2501}25022503String divop = expr.group("div");2504if (divop != null) {2505String divisor = expr.group("val");2506switch (divop) {2507case "%" -> ret %= Double.parseDouble(divisor);2508case "/" -> ret /= Double.parseDouble(divisor);2509}2510}2511}25122513return ret;2514}2515}251625172518