< prev index next >

src/java.base/share/classes/java/text/CompactNumberFormat.java

Print this page
rev 60594 : [mq]: 8251499


  25 package java.text;
  26 
  27 import java.io.IOException;
  28 import java.io.InvalidObjectException;
  29 import java.io.ObjectInputStream;
  30 import java.math.BigDecimal;
  31 import java.math.BigInteger;
  32 import java.math.RoundingMode;
  33 import java.util.ArrayList;
  34 import java.util.Arrays;
  35 import java.util.HashMap;
  36 import java.util.List;
  37 import java.util.Locale;
  38 import java.util.Map;
  39 import java.util.Objects;
  40 import java.util.concurrent.atomic.AtomicInteger;
  41 import java.util.concurrent.atomic.AtomicLong;
  42 import java.util.regex.Matcher;
  43 import java.util.regex.Pattern;
  44 import java.util.stream.Collectors;
  45 import java.util.stream.Stream;
  46 
  47 
  48 /**
  49  * <p>
  50  * {@code CompactNumberFormat} is a concrete subclass of {@code NumberFormat}
  51  * that formats a decimal number in its compact form.
  52  *
  53  * The compact number formatting is designed for the environment where the space
  54  * is limited, and the formatted string can be displayed in that limited space.
  55  * It is defined by LDML's specification for
  56  * <a href = "http://unicode.org/reports/tr35/tr35-numbers.html#Compact_Number_Formats">
  57  * Compact Number Formats</a>. A compact number formatting refers
  58  * to the representation of a number in a shorter form, based on the patterns
  59  * provided for a given locale.
  60  *
  61  * <p>
  62  * For example:
  63  * <br>In the {@link java.util.Locale#US US locale}, {@code 1000} can be formatted
  64  * as {@code "1K"}, and {@code 1000000} as {@code "1M"}, depending upon the
  65  * <a href = "#compact_number_style" >style</a> used.


 247     /**
 248      * List of positive suffix patterns of this formatter's
 249      * compact number patterns.
 250      */
 251     private transient List<Patterns> positiveSuffixPatterns;
 252 
 253     /**
 254      * List of negative suffix patterns of this formatter's
 255      * compact number patterns.
 256      */
 257     private transient List<Patterns> negativeSuffixPatterns;
 258 
 259     /**
 260      * List of divisors of this formatter's compact number patterns.
 261      * Divisor can be either Long or BigInteger (if the divisor value goes
 262      * beyond long boundary)
 263      */
 264     private transient List<Number> divisors;
 265 
 266     /**






 267      * The {@code DecimalFormatSymbols} object used by this format.
 268      * It contains the symbols used to format numbers. For example,
 269      * the grouping separator, decimal separator, and so on.
 270      * This field must not be {@code null}.
 271      *
 272      * @serial
 273      * @see DecimalFormatSymbols
 274      */
 275     private DecimalFormatSymbols symbols;
 276 
 277     /**
 278      * The decimal pattern which is used for formatting the numbers
 279      * matching special pattern "0". This field must not be {@code null}.
 280      *
 281      * @serial
 282      * @see DecimalFormat
 283      */
 284     private final String decimalPattern;
 285 
 286     /**


 442         setMaximumIntegerDigits(decimalFormat.getMaximumIntegerDigits());
 443         setMinimumIntegerDigits(decimalFormat.getMinimumIntegerDigits());
 444         setMaximumFractionDigits(decimalFormat.getMaximumFractionDigits());
 445         setMinimumFractionDigits(decimalFormat.getMinimumFractionDigits());
 446 
 447         super.setGroupingUsed(decimalFormat.isGroupingUsed());
 448         super.setParseIntegerOnly(decimalFormat.isParseIntegerOnly());
 449 
 450         this.compactPatterns = compactPatterns;
 451 
 452         // DecimalFormat used for formatting numbers with special pattern "0".
 453         // Formatting is delegated to the DecimalFormat's number formatting
 454         // with no fraction digits
 455         this.decimalPattern = decimalPattern;
 456         defaultDecimalFormat = new DecimalFormat(this.decimalPattern,
 457                 this.symbols);
 458         defaultDecimalFormat.setMaximumFractionDigits(0);
 459 
 460         this.pluralRules = pluralRules;
 461 
 462         // Process compact patterns to extract the prefixes, suffixes and
 463         // divisors
 464         processCompactPatterns();
 465     }
 466 
 467     /**
 468      * Formats a number to produce a string representing its compact form.
 469      * The number can be of any subclass of {@link java.lang.Number}.
 470      * @param number     the number to format
 471      * @param toAppendTo the {@code StringBuffer} to which the formatted
 472      *                   text is to be appended
 473      * @param fieldPosition    keeps track on the position of the field within
 474      *                         the returned string. For example, for formatting
 475      *                         a number {@code 123456789} in the
 476      *                         {@link java.util.Locale#US US locale},
 477      *                         if the given {@code fieldPosition} is
 478      *                         {@link NumberFormat#INTEGER_FIELD}, the begin
 479      *                         index and end index of {@code fieldPosition}
 480      *                         will be set to 0 and 3, respectively for the
 481      *                         output string {@code 123M}. Similarly, positions
 482      *                         of the prefix and the suffix fields can be


 572         // part of the rounded value is used for matching the compact
 573         // number pattern
 574         // For example, if roundingMode is HALF_UP with min fraction
 575         // digits = 0, the number 999.6 should round up
 576         // to 1000 and outputs 1K/thousand in "en_US" locale
 577         DigitList dList = new DigitList();
 578         dList.setRoundingMode(getRoundingMode());
 579         number = isNegative ? -number : number;
 580         dList.set(isNegative, number, getMinimumFractionDigits());
 581 
 582         double roundedNumber = dList.getDouble();
 583         int compactDataIndex = selectCompactPattern((long) roundedNumber);
 584         if (compactDataIndex != -1) {
 585             long divisor = (Long) divisors.get(compactDataIndex);
 586             int iPart = getIntegerPart(number, divisor);
 587             String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
 588             String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
 589 
 590             if (!prefix.isEmpty() || !suffix.isEmpty()) {
 591                 appendPrefix(result, prefix, delegate);

 592                 roundedNumber = roundedNumber / divisor;
 593                 decimalFormat.setDigitList(roundedNumber, isNegative, getMaximumFractionDigits());
 594                 decimalFormat.subformatNumber(result, delegate, isNegative,
 595                         false, getMaximumIntegerDigits(), getMinimumIntegerDigits(),
 596                         getMaximumFractionDigits(), getMinimumFractionDigits());
 597                 appendSuffix(result, suffix, delegate);

 598             } else {
 599                 defaultDecimalFormat.doubleSubformat(number, result, delegate, isNegative);
 600             }
 601         } else {
 602             defaultDecimalFormat.doubleSubformat(number, result, delegate, isNegative);
 603         }
 604         return result;
 605     }
 606 
 607     /**
 608      * Formats a long to produce a string representing its compact form.
 609      * @param number    the long number to format
 610      * @param result    where the text is to be appended
 611      * @param fieldPosition    keeps track on the position of the field within
 612      *                         the returned string. For example, to format
 613      *                         a number {@code 123456789} in the
 614      *                         {@link java.util.Locale#US US locale},
 615      *                         if the given {@code fieldPosition} is
 616      *                         {@link NumberFormat#INTEGER_FIELD}, the begin
 617      *                         index and end index of {@code fieldPosition}


 638 
 639     private StringBuffer format(long number, StringBuffer result, FieldDelegate delegate) {
 640         boolean isNegative = (number < 0);
 641         if (isNegative) {
 642             number = -number;
 643         }
 644 
 645         if (number < 0) { // LONG_MIN
 646             BigInteger bigIntegerValue = BigInteger.valueOf(number);
 647             return format(bigIntegerValue, result, delegate, true);
 648         }
 649 
 650         int compactDataIndex = selectCompactPattern(number);
 651         if (compactDataIndex != -1) {
 652             long divisor = (Long) divisors.get(compactDataIndex);
 653             int iPart = getIntegerPart(number, divisor);
 654             String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
 655             String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
 656             if (!prefix.isEmpty() || !suffix.isEmpty()) {
 657                 appendPrefix(result, prefix, delegate);

 658                 if ((number % divisor == 0)) {
 659                     number = number / divisor;
 660                     decimalFormat.setDigitList(number, isNegative, 0);
 661                     decimalFormat.subformatNumber(result, delegate,
 662                             isNegative, true, getMaximumIntegerDigits(),
 663                             getMinimumIntegerDigits(), getMaximumFractionDigits(),
 664                             getMinimumFractionDigits());
 665                 } else {
 666                     // To avoid truncation of fractional part store
 667                     // the value in double and follow double path instead of
 668                     // long path
 669                     double dNumber = (double) number / divisor;
 670                     decimalFormat.setDigitList(dNumber, isNegative, getMaximumFractionDigits());
 671                     decimalFormat.subformatNumber(result, delegate,
 672                             isNegative, false, getMaximumIntegerDigits(),
 673                             getMinimumIntegerDigits(), getMaximumFractionDigits(),
 674                             getMinimumFractionDigits());
 675                 }
 676                 appendSuffix(result, suffix, delegate);

 677             } else {
 678                 number = isNegative ? -number : number;
 679                 defaultDecimalFormat.format(number, result, delegate);
 680             }
 681         } else {
 682             number = isNegative ? -number : number;
 683             defaultDecimalFormat.format(number, result, delegate);
 684         }
 685         return result;
 686     }
 687 
 688     /**
 689      * Formats a BigDecimal to produce a string representing its compact form.
 690      * @param number    the BigDecimal number to format
 691      * @param result    where the text is to be appended
 692      * @param fieldPosition    keeps track on the position of the field within
 693      *                         the returned string. For example, to format
 694      *                         a number {@code 1234567.89} in the
 695      *                         {@link java.util.Locale#US US locale},
 696      *                         if the given {@code fieldPosition} is


 731         // For example, If roundingMode is HALF_UP with min fraction digits = 0,
 732         // the number 999.6 should round up
 733         // to 1000 and outputs 1K/thousand in "en_US" locale
 734         number = number.setScale(getMinimumFractionDigits(), getRoundingMode());
 735 
 736         int compactDataIndex;
 737         if (number.toBigInteger().bitLength() < 64) {
 738             long longNumber = number.toBigInteger().longValue();
 739             compactDataIndex = selectCompactPattern(longNumber);
 740         } else {
 741             compactDataIndex = selectCompactPattern(number.toBigInteger());
 742         }
 743 
 744         if (compactDataIndex != -1) {
 745             Number divisor = divisors.get(compactDataIndex);
 746             int iPart = getIntegerPart(number.doubleValue(), divisor.doubleValue());
 747             String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
 748             String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
 749             if (!prefix.isEmpty() || !suffix.isEmpty()) {
 750                 appendPrefix(result, prefix, delegate);

 751                 number = number.divide(new BigDecimal(divisor.toString()), getRoundingMode());
 752                 decimalFormat.setDigitList(number, isNegative, getMaximumFractionDigits());
 753                 decimalFormat.subformatNumber(result, delegate, isNegative,
 754                         false, getMaximumIntegerDigits(), getMinimumIntegerDigits(),
 755                         getMaximumFractionDigits(), getMinimumFractionDigits());
 756                 appendSuffix(result, suffix, delegate);

 757             } else {
 758                 number = isNegative ? number.negate() : number;
 759                 defaultDecimalFormat.format(number, result, delegate);
 760             }
 761         } else {
 762             number = isNegative ? number.negate() : number;
 763             defaultDecimalFormat.format(number, result, delegate);
 764         }
 765         return result;
 766     }
 767 
 768     /**
 769      * Formats a BigInteger to produce a string representing its compact form.
 770      * @param number    the BigInteger number to format
 771      * @param result    where the text is to be appended
 772      * @param fieldPosition    keeps track on the position of the field within
 773      *                         the returned string. For example, to format
 774      *                         a number {@code 123456789} in the
 775      *                         {@link java.util.Locale#US US locale},
 776      *                         if the given {@code fieldPosition} is


 796         fieldPosition.setEndIndex(0);
 797         return format(number, result, fieldPosition.getFieldDelegate(), false);
 798     }
 799 
 800     private StringBuffer format(BigInteger number, StringBuffer result,
 801             FieldDelegate delegate, boolean formatLong) {
 802 
 803         boolean isNegative = number.signum() == -1;
 804         if (isNegative) {
 805             number = number.negate();
 806         }
 807 
 808         int compactDataIndex = selectCompactPattern(number);
 809         if (compactDataIndex != -1) {
 810             Number divisor = divisors.get(compactDataIndex);
 811             int iPart = getIntegerPart(number.doubleValue(), divisor.doubleValue());
 812             String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
 813             String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
 814             if (!prefix.isEmpty() || !suffix.isEmpty()) {
 815                 appendPrefix(result, prefix, delegate);

 816                 if (number.mod(new BigInteger(divisor.toString()))
 817                         .compareTo(BigInteger.ZERO) == 0) {
 818                     number = number.divide(new BigInteger(divisor.toString()));
 819 
 820                     decimalFormat.setDigitList(number, isNegative, 0);
 821                     decimalFormat.subformatNumber(result, delegate,
 822                             isNegative, true, getMaximumIntegerDigits(),
 823                             getMinimumIntegerDigits(), getMaximumFractionDigits(),
 824                             getMinimumFractionDigits());
 825                 } else {
 826                     // To avoid truncation of fractional part store the value in
 827                     // BigDecimal and follow BigDecimal path instead of
 828                     // BigInteger path
 829                     BigDecimal nDecimal = new BigDecimal(number)
 830                             .divide(new BigDecimal(divisor.toString()), getRoundingMode());
 831                     decimalFormat.setDigitList(nDecimal, isNegative, getMaximumFractionDigits());
 832                     decimalFormat.subformatNumber(result, delegate,
 833                             isNegative, false, getMaximumIntegerDigits(),
 834                             getMinimumIntegerDigits(), getMaximumFractionDigits(),
 835                             getMinimumFractionDigits());
 836                 }
 837                 appendSuffix(result, suffix, delegate);

 838             } else {
 839                 number = isNegative ? number.negate() : number;
 840                 defaultDecimalFormat.format(number, result, delegate, formatLong);
 841             }
 842         } else {
 843             number = isNegative ? number.negate() : number;
 844             defaultDecimalFormat.format(number, result, delegate, formatLong);
 845         }
 846         return result;
 847     }
 848 
 849     /**
 850      * Obtain the designated affix from the appropriate list of affixes,
 851      * based on the given arguments.
 852      */
 853     private String getAffix(boolean isExpanded, boolean isPrefix, boolean isNegative, int compactDataIndex, int iPart) {
 854         return (isExpanded ? (isPrefix ? (isNegative ? negativePrefixes : positivePrefixes) :
 855                                          (isNegative ? negativeSuffixes : positiveSuffixes)) :
 856                              (isPrefix ? (isNegative ? negativePrefixPatterns : positivePrefixPatterns) :
 857                                          (isNegative ? negativeSuffixPatterns : positiveSuffixPatterns)))


 888             FieldDelegate delegate) {
 889         append(result, expandAffix(suffix), delegate,
 890                 getFieldPositions(suffix, NumberFormat.Field.SUFFIX));
 891     }
 892 
 893     /**
 894      * Appends the {@code string} to the {@code result}.
 895      * {@code delegate} is notified of SIGN, PREFIX and/or SUFFIX
 896      * field positions.
 897      * @param result the resulting string, where the text is to be appended
 898      * @param string the text to append
 899      * @param delegate notified of the locations of sub fields
 900      * @param positions a list of {@code FieldPostion} in the given
 901      *                  string
 902      */
 903     private void append(StringBuffer result, String string,
 904             FieldDelegate delegate, List<FieldPosition> positions) {
 905         if (!string.isEmpty()) {
 906             int start = result.length();
 907             result.append(string);
 908             for (int counter = 0; counter < positions.size(); counter++) {
 909                 FieldPosition fp = positions.get(counter);
 910                 Format.Field attribute = fp.getFieldAttribute();
 911                 delegate.formatted(attribute, attribute,
 912                         start + fp.getBeginIndex(),
 913                         start + fp.getEndIndex(), result);
 914             }
 915         }
 916     }
 917 
 918     /**
 919      * Expands an affix {@code pattern} into a string of literals.
 920      * All characters in the pattern are literals unless prefixed by QUOTE.
 921      * The character prefixed by QUOTE is replaced with its respective
 922      * localized literal.
 923      * @param pattern a compact number pattern affix
 924      * @return an expanded affix
 925      */
 926     private String expandAffix(String pattern) {
 927         // Return if no quoted character exists
 928         if (pattern.indexOf(QUOTE) < 0) {
 929             return pattern;


1081             format((BigInteger) obj, sb, delegate, false);
1082         } else if (obj == null) {
1083             throw new NullPointerException(
1084                     "formatToCharacterIterator must be passed non-null object");
1085         } else {
1086             throw new IllegalArgumentException(
1087                     "Cannot format given Object as a Number");
1088         }
1089         return delegate.getIterator(sb.toString());
1090     }
1091 
1092     /**
1093      * Computes the divisor using minimum integer digits and
1094      * matched pattern index.
1095      * @param minIntDigits string of 0s in compact pattern
1096      * @param patternIndex index of matched compact pattern
1097      * @return divisor value for the number matching the compact
1098      *         pattern at given {@code patternIndex}
1099      */
1100     private Number computeDivisor(String minIntDigits, int patternIndex) {
1101         int count = minIntDigits.length() - 1;
1102         Number matchedValue;
1103         // The divisor value can go above long range, if the compact patterns
1104         // goes above index 18, divisor may need to be stored as BigInteger,
1105         // since long can't store numbers >= 10^19,
1106         if (patternIndex < 19) {
1107             matchedValue = (long) Math.pow(RANGE_MULTIPLIER, patternIndex);
1108         } else {
1109             matchedValue = BigInteger.valueOf(RANGE_MULTIPLIER).pow(patternIndex);
1110         }
1111         Number divisor = matchedValue;
1112         if (count != 0) {
1113             if (matchedValue instanceof BigInteger) {
1114                 BigInteger bigValue = (BigInteger) matchedValue;
1115                 if (bigValue.compareTo(BigInteger.valueOf((long) Math.pow(RANGE_MULTIPLIER, count))) < 0) {
1116                     throw new IllegalArgumentException("Invalid Pattern"
1117                             + " [" + compactPatterns[patternIndex]
1118                             + "]: min integer digits specified exceeds the limit"
1119                             + " for the index " + patternIndex);
1120                 }
1121                 divisor = bigValue.divide(BigInteger.valueOf((long) Math.pow(RANGE_MULTIPLIER, count)));
1122             } else {
1123                 long longValue = (long) matchedValue;
1124                 if (longValue < (long) Math.pow(RANGE_MULTIPLIER, count)) {
1125                     throw new IllegalArgumentException("Invalid Pattern"
1126                             + " [" + compactPatterns[patternIndex]
1127                             + "]: min integer digits specified exceeds the limit"
1128                             + " for the index " + patternIndex);
1129                 }
1130                 divisor = longValue / (long) Math.pow(RANGE_MULTIPLIER, count);
1131             }
1132         }
1133         return divisor;
1134     }
1135 
1136     /**
1137      * Process the series of compact patterns to compute the
1138      * series of prefixes, suffixes and their respective divisor
1139      * value.
1140      *
1141      */
1142     private static final Pattern PLURALS =
1143             Pattern.compile("^\\{(?<plurals>.*)\\}$");
1144     private static final Pattern COUNT_PATTERN =
1145             Pattern.compile("(zero|one|two|few|many|other):((' '|[^ ])+)[ ]*");
1146     private void processCompactPatterns() {
1147         int size = compactPatterns.length;
1148         positivePrefixPatterns = new ArrayList<>(size);
1149         negativePrefixPatterns = new ArrayList<>(size);
1150         positiveSuffixPatterns = new ArrayList<>(size);
1151         negativeSuffixPatterns = new ArrayList<>(size);
1152         divisors = new ArrayList<>(size);

1153 
1154         for (int index = 0; index < size; index++) {
1155             String text = compactPatterns[index];
1156             positivePrefixPatterns.add(new Patterns());
1157             negativePrefixPatterns.add(new Patterns());
1158             positiveSuffixPatterns.add(new Patterns());
1159             negativeSuffixPatterns.add(new Patterns());

1160 
1161             // check if it is the old style
1162             Matcher m = text != null ? PLURALS.matcher(text) : null;
1163             if (m != null && m.matches()) {
1164                 final int idx = index;
1165                 String plurals = m.group("plurals");
1166                 COUNT_PATTERN.matcher(plurals).results()
1167                         .forEach(mr -> applyPattern(mr.group(1), mr.group(2), idx));
1168             } else {
1169                 applyPattern("other", text, index);
1170             }
1171         }
1172 
1173         rulesMap = buildPluralRulesMap();
1174     }
1175 
1176     /**
1177      * Build the plural rules map.
1178      *
1179      * @throws IllegalArgumentException if the {@code pluralRules} has invalid syntax,


1181      */
1182     private Map<String, String> buildPluralRulesMap() {
1183         // length limitation check. 2K for now.
1184         if (pluralRules.length() > 2_048) {
1185             throw new IllegalArgumentException("plural rules is too long (> 2,048)");
1186         }
1187 
1188         try {
1189             return Arrays.stream(pluralRules.split(";"))
1190                 .map(this::validateRule)
1191                 .collect(Collectors.toMap(
1192                         r -> r.replaceFirst(":.*", ""),
1193                         r -> r.replaceFirst("[^:]+:", "")
1194                 ));
1195         } catch (IllegalStateException ise) {
1196             throw new IllegalArgumentException(ise);
1197         }
1198     }
1199 
1200     // Patterns for plurals syntax validation
1201     private final static String EXPR = "([niftvw]{1})\\s*(([/\\%])\\s*(\\d+))*";
1202     private final static String RELATION = "(!{0,1}=)";
1203     private final static String VALUE_RANGE = "((\\d+)\\.\\.(\\d+)|\\d+)";
1204     private final static String CONDITION = EXPR + "\\s*" +
1205                                              RELATION + "\\s*" +
1206                                              VALUE_RANGE + "\\s*" +
1207                                              "(\\,\\s*" + VALUE_RANGE + ")*";
1208     private final static Pattern PLURALRULES_PATTERN =
1209             Pattern.compile("(zero|one|two|few|many):\\s*" +
1210                             CONDITION +
1211                             "(\\s*(and|or)\\s*" + CONDITION + ")*");
1212 
1213     /**
1214      * Validates a plural rule.
1215      * @param rule rule to validate
1216      * @throws IllegalArgumentException if the {@code rule} has invalid syntax
1217      * @return the input rule (trimmed)
1218      */
1219     private String validateRule(String rule) {
1220         rule = rule.trim();
1221         if (!rule.isEmpty() && !rule.equals("other:")) {
1222             Matcher validator = PLURALRULES_PATTERN.matcher(rule);
1223             if (!validator.matches()) {
1224                 throw new IllegalArgumentException("Invalid plural rules syntax: " + rule);
1225             }
1226         }
1227 


1383                 positiveSuffix = suffix.toString();
1384                 negativePrefix = positivePrefix;
1385                 negativeSuffix = positiveSuffix;
1386             } else {
1387                 negativePrefix = prefix.toString();
1388                 negativeSuffix = suffix.toString();
1389                 gotNegative = true;
1390             }
1391 
1392             // If there is no negative pattern, or if the negative pattern is
1393             // identical to the positive pattern, then prepend the minus sign to
1394             // the positive pattern to form the negative pattern.
1395             if (!gotNegative
1396                     || (negativePrefix.equals(positivePrefix)
1397                     && negativeSuffix.equals(positiveSuffix))) {
1398                 negativeSuffix = positiveSuffix;
1399                 negativePrefix = "'-" + positivePrefix;
1400             }
1401         }
1402 
1403         // If no 0s are specified in a non empty pattern, it is invalid
1404         if (!pattern.isEmpty() && zeros.isEmpty()) {
1405             throw new IllegalArgumentException("Invalid pattern"
1406                     + " [" + pattern + "]: all patterns must include digit"
1407                     + " placement 0s");
1408         }
1409 
1410         // Only if positive affix exists; else put empty strings
1411         if (!positivePrefix.isEmpty() || !positiveSuffix.isEmpty()) {
1412             positivePrefixPatterns.get(index).put(count, positivePrefix);
1413             negativePrefixPatterns.get(index).put(count, negativePrefix);
1414             positiveSuffixPatterns.get(index).put(count, positiveSuffix);
1415             negativeSuffixPatterns.get(index).put(count, negativeSuffix);

1416             if (divisors.size() <= index) {
1417                 divisors.add(computeDivisor(zeros, index));
1418             }
1419         } else {
1420             positivePrefixPatterns.get(index).put(count, "");
1421             negativePrefixPatterns.get(index).put(count, "");
1422             positiveSuffixPatterns.get(index).put(count, "");
1423             negativeSuffixPatterns.get(index).put(count, "");

1424             if (divisors.size() <= index) {
1425                 divisors.add(1L);
1426             }
1427         }
1428     }
1429 
1430     private final transient DigitList digitList = new DigitList();
1431     private static final int STATUS_INFINITE = 0;
1432     private static final int STATUS_POSITIVE = 1;
1433     private static final int STATUS_LENGTH   = 2;
1434 
1435     private static final char ZERO_DIGIT = '0';
1436     private static final char DIGIT = '#';
1437     private static final char DECIMAL_SEPARATOR = '.';
1438     private static final char GROUPING_SEPARATOR = ',';
1439     private static final char MINUS_SIGN = '-';
1440     private static final char PERCENT = '%';
1441     private static final char PER_MILLE = '\u2030';
1442     private static final char SEPARATOR = ';';
1443     private static final char CURRENCY_SIGN = '\u00A4';


1600             if (text.regionMatches(pos.index, defaultNegPrefix, 0,
1601                     defaultNegPrefix.length())) {
1602                 // Matches the default negative prefix
1603                 matchedNegPrefix = defaultNegPrefix;
1604                 gotNegative = true;
1605             }
1606         }
1607 
1608         // If both match, take the longest one
1609         if (gotPositive && gotNegative) {
1610             if (matchedPosPrefix.length() > matchedNegPrefix.length()) {
1611                 gotNegative = false;
1612             } else if (matchedPosPrefix.length() < matchedNegPrefix.length()) {
1613                 gotPositive = false;
1614             }
1615         }
1616 
1617         // Update the position and take compact multiplier
1618         // only if it matches the compact prefix, not the default
1619         // prefix; else multiplier should be 1


1620         if (gotPositive) {
1621             position += matchedPosPrefix.length();
1622             cnfMultiplier = matchedPosIndex != -1
1623                     ? divisors.get(matchedPosIndex) : 1L;





1624         } else if (gotNegative) {
1625             position += matchedNegPrefix.length();
1626             cnfMultiplier = matchedNegIndex != -1
1627                     ? divisors.get(matchedNegIndex) : 1L;





1628         }
1629 
1630         digitList.setRoundingMode(getRoundingMode());
1631         boolean[] status = new boolean[STATUS_LENGTH];
1632 
1633         // Call DecimalFormat.subparseNumber() method to parse the
1634         // number part of the input text
1635         position = decimalFormat.subparseNumber(text, position,
1636                 digitList, false, false, status);
1637 
1638         if (position == -1) {
1639             // Unable to parse the number successfully
1640             pos.index = oldStart;
1641             pos.errorIndex = oldStart;
1642             return null;
1643         }
1644 
1645         // If parse integer only is true and the parsing is broken at
1646         // decimal point, then pass/ignore all digits and move pointer
1647         // at the start of suffix, to process the suffix part


1691                         .multiply(new BigDecimal(cnfMultiplier.toString()));
1692             }
1693             if (!status[STATUS_POSITIVE]) {
1694                 bigDecimalResult = bigDecimalResult.negate();
1695             }
1696             return bigDecimalResult;
1697         } else {
1698             Number cnfResult;
1699             if (digitList.fitsIntoLong(status[STATUS_POSITIVE], isParseIntegerOnly())) {
1700                 long longResult = digitList.getLong();
1701                 cnfResult = generateParseResult(longResult, false,
1702                         longResult < 0, status, cnfMultiplier);
1703             } else {
1704                 cnfResult = generateParseResult(digitList.getDouble(),
1705                         true, false, status, cnfMultiplier);
1706             }
1707             return cnfResult;
1708         }
1709     }
1710 

1711     /**
1712      * Parse the number part in the input text into a number
1713      *
1714      * @param text input text to be parsed
1715      * @param position starting position
1716      * @return the number
1717      */
1718     private static Pattern DIGITS = Pattern.compile("\\p{Nd}+");
1719     private double parseNumberPart(String text, int position) {
1720         if (text.startsWith(symbols.getInfinity(), position)) {
1721             return Double.POSITIVE_INFINITY;
1722         } else if (!text.startsWith(symbols.getNaN(), position)) {
1723             Matcher m = DIGITS.matcher(text);
1724             if (m.find(position)) {
1725                 String digits = m.group();
1726                 int cp = digits.codePointAt(0);
1727                 if (Character.isDigit(cp)) {
1728                     return Double.parseDouble(digits.codePoints()
1729                         .map(Character::getNumericValue)
1730                         .mapToObj(Integer::toString)
1731                         .collect(Collectors.joining()));
1732                 }



1733             }
1734         }
1735         return Double.NaN;
1736     }
1737 
1738     /**
1739      * Returns the parsed result by multiplying the parsed number
1740      * with the multiplier representing the prefix and suffix.
1741      *
1742      * @param number parsed number component
1743      * @param gotDouble whether the parsed number contains decimal
1744      * @param gotLongMin whether the parsed number is Long.MIN
1745      * @param status boolean status flags indicating whether the
1746      *               value is infinite and whether it is positive
1747      * @param cnfMultiplier compact number multiplier
1748      * @return parsed result
1749      */
1750     private Number generateParseResult(Number number, boolean gotDouble,
1751             boolean gotLongMin, boolean[] status, Number cnfMultiplier) {
1752 


1804             } else {
1805                 return -(double) number;
1806             }
1807         } else {
1808             return number;
1809         }
1810     }
1811 
1812     /**
1813      * Attempts to match the given {@code affix} in the
1814      * specified {@code text}.
1815      */
1816     private boolean matchAffix(String text, int position, String affix,
1817             String defaultAffix, String matchedAffix) {
1818 
1819         // Check with the compact affixes which are non empty and
1820         // do not match with default affix
1821         if (!affix.isEmpty() && !affix.equals(defaultAffix)) {
1822             // Look ahead only for the longer match than the previous match
1823             if (matchedAffix.length() < affix.length()) {
1824                 if (text.regionMatches(position, affix, 0, affix.length())) {
1825                     return true;
1826                 }
1827             }
1828         }
1829         return false;
1830     }
1831 
1832     /**
1833      * Attempts to match given {@code prefix} and {@code suffix} in
1834      * the specified {@code text}.
1835      */
1836     private boolean matchPrefixAndSuffix(String text, int position, String prefix,
1837             String matchedPrefix, String defaultPrefix, String suffix,
1838             String matchedSuffix, String defaultSuffix) {
1839 
1840         // Check the compact pattern suffix only if there is a
1841         // compact prefix match or a default prefix match
1842         // because the compact prefix and suffix should match at the same
1843         // index to obtain the multiplier.
1844         // The prefix match is required because of the possibility of
1845         // same prefix at multiple index, in which case matching the suffix
1846         // is used to obtain the single match


2340                 Objects.hash(decimalPattern, symbols, roundingMode, pluralRules)
2341                 + Arrays.hashCode(compactPatterns) + groupingSize
2342                 + Boolean.hashCode(parseBigDecimal);
2343     }
2344 
2345     /**
2346      * Creates and returns a copy of this {@code CompactNumberFormat}
2347      * instance.
2348      *
2349      * @return a clone of this instance
2350      */
2351     @Override
2352     public CompactNumberFormat clone() {
2353         CompactNumberFormat other = (CompactNumberFormat) super.clone();
2354         other.compactPatterns = compactPatterns.clone();
2355         other.symbols = (DecimalFormatSymbols) symbols.clone();
2356         return other;
2357     }
2358 
2359     /**
2360      * Abstraction of affix patterns for each "count" tag.
2361      */
2362     private final class Patterns {
2363         private Map<String, String> patternsMap = new HashMap<>();
2364 
2365         void put(String count, String pattern) {
2366             patternsMap.put(count, pattern);
2367         }
2368 
2369         String get(double num) {
2370             return patternsMap.getOrDefault(getPluralCategory(num),
2371                     patternsMap.getOrDefault("other", ""));
2372         }
2373 
2374         Patterns expandAffix() {
2375             Patterns ret = new Patterns();
2376             patternsMap.entrySet().stream()
2377                     .forEach(e -> ret.put(e.getKey(), CompactNumberFormat.this.expandAffix(e.getValue())));
2378             return ret;
2379         }
2380     }
2381 
2382     private final int getIntegerPart(double number, double divisor) {
2383         return BigDecimal.valueOf(number)
2384                 .divide(BigDecimal.valueOf(divisor), roundingMode).intValue();
2385     }
2386 
2387     /**
2388      * Returns LDML's tag from the plurals rules
2389      *
2390      * @param input input number in double type
2391      * @return LDML "count" tag
2392      */
2393     private String getPluralCategory(double input) {
2394         if (rulesMap != null) {
2395             return rulesMap.entrySet().stream()
2396                     .filter(e -> matchPluralRule(e.getValue(), input))
2397                     .map(e -> e.getKey())
2398                     .findFirst()
2399                     .orElse("other");
2400         }
2401 
2402         // defaults to "other"
2403         return "other";
2404     }
2405 
2406     private static boolean matchPluralRule(String condition, double input) {
2407         return Arrays.stream(condition.split("or"))
2408             .anyMatch(and_condition -> {
2409                 return Arrays.stream(and_condition.split("and"))
2410                     .allMatch(r -> relationCheck(r, input));
2411             });
2412     }
2413 
2414     private final static String NAMED_EXPR = "(?<op>[niftvw]{1})\\s*((?<div>[/\\%])\\s*(?<val>\\d+))*";
2415     private final static String NAMED_RELATION = "(?<rel>!{0,1}=)";
2416     private final static String NAMED_VALUE_RANGE = "(?<start>\\d+)\\.\\.(?<end>\\d+)|(?<value>\\d+)";
2417     private final static Pattern EXPR_PATTERN = Pattern.compile(NAMED_EXPR);
2418     private final static Pattern RELATION_PATTERN = Pattern.compile(NAMED_RELATION);
2419     private final static Pattern VALUE_RANGE_PATTERN = Pattern.compile(NAMED_VALUE_RANGE);
2420 
2421     /**
2422      * Checks if the 'input' equals the value, or within the range.
2423      *
2424      * @param valueOrRange A string representing either a single value or a range
2425      * @param input to examine in double
2426      * @return match indicator
2427      */
2428     private static boolean valOrRangeMatches(String valueOrRange, double input) {
2429         Matcher m = VALUE_RANGE_PATTERN.matcher(valueOrRange);
2430 
2431         if (m.find()) {
2432             String value = m.group("value");
2433             if (value != null) {
2434                 return input == Double.parseDouble(value);
2435             } else {


2445      * Checks if the input value satisfies the relation. Each possible value or range is
2446      * separated by a comma ','
2447      *
2448      * @param relation relation string, e.g, "n = 1, 3..5", or "n != 1, 3..5"
2449      * @param input value to examine in double
2450      * @return boolean to indicate whether the relation satisfies or not. If the relation
2451      *  is '=', true if any of the possible value/range satisfies. If the relation is '!=',
2452      *  none of the possible value/range should satisfy to return true.
2453      */
2454     private static boolean relationCheck(String relation, double input) {
2455         Matcher expr = EXPR_PATTERN.matcher(relation);
2456 
2457         if (expr.find()) {
2458             double lop = evalLOperand(expr, input);
2459             Matcher rel = RELATION_PATTERN.matcher(relation);
2460 
2461             if (rel.find(expr.end())) {
2462                 var conditions =
2463                     Arrays.stream(relation.substring(rel.end()).split(","));
2464 
2465                 if (rel.group("rel").equals("!=")) {
2466                     return conditions.noneMatch(c -> valOrRangeMatches(c, lop));
2467                 } else {
2468                     return conditions.anyMatch(c -> valOrRangeMatches(c, lop));
2469                 }
2470             }
2471         }
2472 
2473         return false;
2474     }
2475 
2476     /**
2477      * Evaluates the left operand value.
2478      *
2479      * @param expr Match result
2480      * @param input value to examine in double
2481      * @return resulting double value
2482      */
2483     private static double evalLOperand(Matcher expr, double input) {
2484         double ret = 0;
2485 
2486         if (input == Double.POSITIVE_INFINITY) {
2487             ret =input;
2488         } else {
2489             String op = expr.group("op");
2490             if (op.equals("n") || op.equals("i")) {
2491                 ret = input;
2492             }
2493 
2494             String divop = expr.group("div");
2495             if (divop != null) {
2496                 String divisor = expr.group("val");
2497                 switch (divop) {
2498                     case "%":
2499                         ret %= Double.parseDouble(divisor);
2500                         break;
2501                     case "/":
2502                         ret /= Double.parseDouble(divisor);
2503                         break;
2504                 }
2505             }
2506         }
2507 
2508         return ret;
2509     }
2510 }


  25 package java.text;
  26 
  27 import java.io.IOException;
  28 import java.io.InvalidObjectException;
  29 import java.io.ObjectInputStream;
  30 import java.math.BigDecimal;
  31 import java.math.BigInteger;
  32 import java.math.RoundingMode;
  33 import java.util.ArrayList;
  34 import java.util.Arrays;
  35 import java.util.HashMap;
  36 import java.util.List;
  37 import java.util.Locale;
  38 import java.util.Map;
  39 import java.util.Objects;
  40 import java.util.concurrent.atomic.AtomicInteger;
  41 import java.util.concurrent.atomic.AtomicLong;
  42 import java.util.regex.Matcher;
  43 import java.util.regex.Pattern;
  44 import java.util.stream.Collectors;

  45 
  46 
  47 /**
  48  * <p>
  49  * {@code CompactNumberFormat} is a concrete subclass of {@code NumberFormat}
  50  * that formats a decimal number in its compact form.
  51  *
  52  * The compact number formatting is designed for the environment where the space
  53  * is limited, and the formatted string can be displayed in that limited space.
  54  * It is defined by LDML's specification for
  55  * <a href = "http://unicode.org/reports/tr35/tr35-numbers.html#Compact_Number_Formats">
  56  * Compact Number Formats</a>. A compact number formatting refers
  57  * to the representation of a number in a shorter form, based on the patterns
  58  * provided for a given locale.
  59  *
  60  * <p>
  61  * For example:
  62  * <br>In the {@link java.util.Locale#US US locale}, {@code 1000} can be formatted
  63  * as {@code "1K"}, and {@code 1000000} as {@code "1M"}, depending upon the
  64  * <a href = "#compact_number_style" >style</a> used.


 246     /**
 247      * List of positive suffix patterns of this formatter's
 248      * compact number patterns.
 249      */
 250     private transient List<Patterns> positiveSuffixPatterns;
 251 
 252     /**
 253      * List of negative suffix patterns of this formatter's
 254      * compact number patterns.
 255      */
 256     private transient List<Patterns> negativeSuffixPatterns;
 257 
 258     /**
 259      * List of divisors of this formatter's compact number patterns.
 260      * Divisor can be either Long or BigInteger (if the divisor value goes
 261      * beyond long boundary)
 262      */
 263     private transient List<Number> divisors;
 264 
 265     /**
 266      * List of place holders that represent minimum integer digits at each index
 267      * for each count.
 268      */
 269     private transient List<Patterns> placeHolders;
 270 
 271     /**
 272      * The {@code DecimalFormatSymbols} object used by this format.
 273      * It contains the symbols used to format numbers. For example,
 274      * the grouping separator, decimal separator, and so on.
 275      * This field must not be {@code null}.
 276      *
 277      * @serial
 278      * @see DecimalFormatSymbols
 279      */
 280     private DecimalFormatSymbols symbols;
 281 
 282     /**
 283      * The decimal pattern which is used for formatting the numbers
 284      * matching special pattern "0". This field must not be {@code null}.
 285      *
 286      * @serial
 287      * @see DecimalFormat
 288      */
 289     private final String decimalPattern;
 290 
 291     /**


 447         setMaximumIntegerDigits(decimalFormat.getMaximumIntegerDigits());
 448         setMinimumIntegerDigits(decimalFormat.getMinimumIntegerDigits());
 449         setMaximumFractionDigits(decimalFormat.getMaximumFractionDigits());
 450         setMinimumFractionDigits(decimalFormat.getMinimumFractionDigits());
 451 
 452         super.setGroupingUsed(decimalFormat.isGroupingUsed());
 453         super.setParseIntegerOnly(decimalFormat.isParseIntegerOnly());
 454 
 455         this.compactPatterns = compactPatterns;
 456 
 457         // DecimalFormat used for formatting numbers with special pattern "0".
 458         // Formatting is delegated to the DecimalFormat's number formatting
 459         // with no fraction digits
 460         this.decimalPattern = decimalPattern;
 461         defaultDecimalFormat = new DecimalFormat(this.decimalPattern,
 462                 this.symbols);
 463         defaultDecimalFormat.setMaximumFractionDigits(0);
 464 
 465         this.pluralRules = pluralRules;
 466 
 467         // Process compact patterns to extract the prefixes, suffixes, place holders, and
 468         // divisors
 469         processCompactPatterns();
 470     }
 471 
 472     /**
 473      * Formats a number to produce a string representing its compact form.
 474      * The number can be of any subclass of {@link java.lang.Number}.
 475      * @param number     the number to format
 476      * @param toAppendTo the {@code StringBuffer} to which the formatted
 477      *                   text is to be appended
 478      * @param fieldPosition    keeps track on the position of the field within
 479      *                         the returned string. For example, for formatting
 480      *                         a number {@code 123456789} in the
 481      *                         {@link java.util.Locale#US US locale},
 482      *                         if the given {@code fieldPosition} is
 483      *                         {@link NumberFormat#INTEGER_FIELD}, the begin
 484      *                         index and end index of {@code fieldPosition}
 485      *                         will be set to 0 and 3, respectively for the
 486      *                         output string {@code 123M}. Similarly, positions
 487      *                         of the prefix and the suffix fields can be


 577         // part of the rounded value is used for matching the compact
 578         // number pattern
 579         // For example, if roundingMode is HALF_UP with min fraction
 580         // digits = 0, the number 999.6 should round up
 581         // to 1000 and outputs 1K/thousand in "en_US" locale
 582         DigitList dList = new DigitList();
 583         dList.setRoundingMode(getRoundingMode());
 584         number = isNegative ? -number : number;
 585         dList.set(isNegative, number, getMinimumFractionDigits());
 586 
 587         double roundedNumber = dList.getDouble();
 588         int compactDataIndex = selectCompactPattern((long) roundedNumber);
 589         if (compactDataIndex != -1) {
 590             long divisor = (Long) divisors.get(compactDataIndex);
 591             int iPart = getIntegerPart(number, divisor);
 592             String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
 593             String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
 594 
 595             if (!prefix.isEmpty() || !suffix.isEmpty()) {
 596                 appendPrefix(result, prefix, delegate);
 597                 if (!placeHolders.get(compactDataIndex).get(iPart).isEmpty()) {
 598                     roundedNumber = roundedNumber / divisor;
 599                     decimalFormat.setDigitList(roundedNumber, isNegative, getMaximumFractionDigits());
 600                     decimalFormat.subformatNumber(result, delegate, isNegative,
 601                             false, getMaximumIntegerDigits(), getMinimumIntegerDigits(),
 602                             getMaximumFractionDigits(), getMinimumFractionDigits());
 603                     appendSuffix(result, suffix, delegate);
 604                 }
 605             } else {
 606                 defaultDecimalFormat.doubleSubformat(number, result, delegate, isNegative);
 607             }
 608         } else {
 609             defaultDecimalFormat.doubleSubformat(number, result, delegate, isNegative);
 610         }
 611         return result;
 612     }
 613 
 614     /**
 615      * Formats a long to produce a string representing its compact form.
 616      * @param number    the long number to format
 617      * @param result    where the text is to be appended
 618      * @param fieldPosition    keeps track on the position of the field within
 619      *                         the returned string. For example, to format
 620      *                         a number {@code 123456789} in the
 621      *                         {@link java.util.Locale#US US locale},
 622      *                         if the given {@code fieldPosition} is
 623      *                         {@link NumberFormat#INTEGER_FIELD}, the begin
 624      *                         index and end index of {@code fieldPosition}


 645 
 646     private StringBuffer format(long number, StringBuffer result, FieldDelegate delegate) {
 647         boolean isNegative = (number < 0);
 648         if (isNegative) {
 649             number = -number;
 650         }
 651 
 652         if (number < 0) { // LONG_MIN
 653             BigInteger bigIntegerValue = BigInteger.valueOf(number);
 654             return format(bigIntegerValue, result, delegate, true);
 655         }
 656 
 657         int compactDataIndex = selectCompactPattern(number);
 658         if (compactDataIndex != -1) {
 659             long divisor = (Long) divisors.get(compactDataIndex);
 660             int iPart = getIntegerPart(number, divisor);
 661             String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
 662             String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
 663             if (!prefix.isEmpty() || !suffix.isEmpty()) {
 664                 appendPrefix(result, prefix, delegate);
 665                 if (!placeHolders.get(compactDataIndex).get(iPart).isEmpty()) {
 666                     if ((number % divisor == 0)) {
 667                         number = number / divisor;
 668                         decimalFormat.setDigitList(number, isNegative, 0);
 669                         decimalFormat.subformatNumber(result, delegate,
 670                                 isNegative, true, getMaximumIntegerDigits(),
 671                                 getMinimumIntegerDigits(), getMaximumFractionDigits(),
 672                                 getMinimumFractionDigits());
 673                     } else {
 674                         // To avoid truncation of fractional part store
 675                         // the value in double and follow double path instead of
 676                         // long path
 677                         double dNumber = (double) number / divisor;
 678                         decimalFormat.setDigitList(dNumber, isNegative, getMaximumFractionDigits());
 679                         decimalFormat.subformatNumber(result, delegate,
 680                                 isNegative, false, getMaximumIntegerDigits(),
 681                                 getMinimumIntegerDigits(), getMaximumFractionDigits(),
 682                                 getMinimumFractionDigits());
 683                     }
 684                     appendSuffix(result, suffix, delegate);
 685                 }
 686             } else {
 687                 number = isNegative ? -number : number;
 688                 defaultDecimalFormat.format(number, result, delegate);
 689             }
 690         } else {
 691             number = isNegative ? -number : number;
 692             defaultDecimalFormat.format(number, result, delegate);
 693         }
 694         return result;
 695     }
 696 
 697     /**
 698      * Formats a BigDecimal to produce a string representing its compact form.
 699      * @param number    the BigDecimal number to format
 700      * @param result    where the text is to be appended
 701      * @param fieldPosition    keeps track on the position of the field within
 702      *                         the returned string. For example, to format
 703      *                         a number {@code 1234567.89} in the
 704      *                         {@link java.util.Locale#US US locale},
 705      *                         if the given {@code fieldPosition} is


 740         // For example, If roundingMode is HALF_UP with min fraction digits = 0,
 741         // the number 999.6 should round up
 742         // to 1000 and outputs 1K/thousand in "en_US" locale
 743         number = number.setScale(getMinimumFractionDigits(), getRoundingMode());
 744 
 745         int compactDataIndex;
 746         if (number.toBigInteger().bitLength() < 64) {
 747             long longNumber = number.toBigInteger().longValue();
 748             compactDataIndex = selectCompactPattern(longNumber);
 749         } else {
 750             compactDataIndex = selectCompactPattern(number.toBigInteger());
 751         }
 752 
 753         if (compactDataIndex != -1) {
 754             Number divisor = divisors.get(compactDataIndex);
 755             int iPart = getIntegerPart(number.doubleValue(), divisor.doubleValue());
 756             String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
 757             String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
 758             if (!prefix.isEmpty() || !suffix.isEmpty()) {
 759                 appendPrefix(result, prefix, delegate);
 760                 if (!placeHolders.get(compactDataIndex).get(iPart).isEmpty()) {
 761                     number = number.divide(new BigDecimal(divisor.toString()), getRoundingMode());
 762                     decimalFormat.setDigitList(number, isNegative, getMaximumFractionDigits());
 763                     decimalFormat.subformatNumber(result, delegate, isNegative,
 764                             false, getMaximumIntegerDigits(), getMinimumIntegerDigits(),
 765                             getMaximumFractionDigits(), getMinimumFractionDigits());
 766                     appendSuffix(result, suffix, delegate);
 767                 }
 768             } else {
 769                 number = isNegative ? number.negate() : number;
 770                 defaultDecimalFormat.format(number, result, delegate);
 771             }
 772         } else {
 773             number = isNegative ? number.negate() : number;
 774             defaultDecimalFormat.format(number, result, delegate);
 775         }
 776         return result;
 777     }
 778 
 779     /**
 780      * Formats a BigInteger to produce a string representing its compact form.
 781      * @param number    the BigInteger number to format
 782      * @param result    where the text is to be appended
 783      * @param fieldPosition    keeps track on the position of the field within
 784      *                         the returned string. For example, to format
 785      *                         a number {@code 123456789} in the
 786      *                         {@link java.util.Locale#US US locale},
 787      *                         if the given {@code fieldPosition} is


 807         fieldPosition.setEndIndex(0);
 808         return format(number, result, fieldPosition.getFieldDelegate(), false);
 809     }
 810 
 811     private StringBuffer format(BigInteger number, StringBuffer result,
 812             FieldDelegate delegate, boolean formatLong) {
 813 
 814         boolean isNegative = number.signum() == -1;
 815         if (isNegative) {
 816             number = number.negate();
 817         }
 818 
 819         int compactDataIndex = selectCompactPattern(number);
 820         if (compactDataIndex != -1) {
 821             Number divisor = divisors.get(compactDataIndex);
 822             int iPart = getIntegerPart(number.doubleValue(), divisor.doubleValue());
 823             String prefix = getAffix(false, true, isNegative, compactDataIndex, iPart);
 824             String suffix = getAffix(false, false, isNegative, compactDataIndex, iPart);
 825             if (!prefix.isEmpty() || !suffix.isEmpty()) {
 826                 appendPrefix(result, prefix, delegate);
 827                 if (!placeHolders.get(compactDataIndex).get(iPart).isEmpty()) {
 828                     if (number.mod(new BigInteger(divisor.toString()))
 829                             .compareTo(BigInteger.ZERO) == 0) {
 830                         number = number.divide(new BigInteger(divisor.toString()));
 831 
 832                         decimalFormat.setDigitList(number, isNegative, 0);
 833                         decimalFormat.subformatNumber(result, delegate,
 834                                 isNegative, true, getMaximumIntegerDigits(),
 835                                 getMinimumIntegerDigits(), getMaximumFractionDigits(),
 836                                 getMinimumFractionDigits());
 837                     } else {
 838                         // To avoid truncation of fractional part store the value in
 839                         // BigDecimal and follow BigDecimal path instead of
 840                         // BigInteger path
 841                         BigDecimal nDecimal = new BigDecimal(number)
 842                                 .divide(new BigDecimal(divisor.toString()), getRoundingMode());
 843                         decimalFormat.setDigitList(nDecimal, isNegative, getMaximumFractionDigits());
 844                         decimalFormat.subformatNumber(result, delegate,
 845                                 isNegative, false, getMaximumIntegerDigits(),
 846                                 getMinimumIntegerDigits(), getMaximumFractionDigits(),
 847                                 getMinimumFractionDigits());
 848                     }
 849                     appendSuffix(result, suffix, delegate);
 850                 }
 851             } else {
 852                 number = isNegative ? number.negate() : number;
 853                 defaultDecimalFormat.format(number, result, delegate, formatLong);
 854             }
 855         } else {
 856             number = isNegative ? number.negate() : number;
 857             defaultDecimalFormat.format(number, result, delegate, formatLong);
 858         }
 859         return result;
 860     }
 861 
 862     /**
 863      * Obtain the designated affix from the appropriate list of affixes,
 864      * based on the given arguments.
 865      */
 866     private String getAffix(boolean isExpanded, boolean isPrefix, boolean isNegative, int compactDataIndex, int iPart) {
 867         return (isExpanded ? (isPrefix ? (isNegative ? negativePrefixes : positivePrefixes) :
 868                                          (isNegative ? negativeSuffixes : positiveSuffixes)) :
 869                              (isPrefix ? (isNegative ? negativePrefixPatterns : positivePrefixPatterns) :
 870                                          (isNegative ? negativeSuffixPatterns : positiveSuffixPatterns)))


 901             FieldDelegate delegate) {
 902         append(result, expandAffix(suffix), delegate,
 903                 getFieldPositions(suffix, NumberFormat.Field.SUFFIX));
 904     }
 905 
 906     /**
 907      * Appends the {@code string} to the {@code result}.
 908      * {@code delegate} is notified of SIGN, PREFIX and/or SUFFIX
 909      * field positions.
 910      * @param result the resulting string, where the text is to be appended
 911      * @param string the text to append
 912      * @param delegate notified of the locations of sub fields
 913      * @param positions a list of {@code FieldPostion} in the given
 914      *                  string
 915      */
 916     private void append(StringBuffer result, String string,
 917             FieldDelegate delegate, List<FieldPosition> positions) {
 918         if (!string.isEmpty()) {
 919             int start = result.length();
 920             result.append(string);
 921             for (FieldPosition fp : positions) {

 922                 Format.Field attribute = fp.getFieldAttribute();
 923                 delegate.formatted(attribute, attribute,
 924                         start + fp.getBeginIndex(),
 925                         start + fp.getEndIndex(), result);
 926             }
 927         }
 928     }
 929 
 930     /**
 931      * Expands an affix {@code pattern} into a string of literals.
 932      * All characters in the pattern are literals unless prefixed by QUOTE.
 933      * The character prefixed by QUOTE is replaced with its respective
 934      * localized literal.
 935      * @param pattern a compact number pattern affix
 936      * @return an expanded affix
 937      */
 938     private String expandAffix(String pattern) {
 939         // Return if no quoted character exists
 940         if (pattern.indexOf(QUOTE) < 0) {
 941             return pattern;


1093             format((BigInteger) obj, sb, delegate, false);
1094         } else if (obj == null) {
1095             throw new NullPointerException(
1096                     "formatToCharacterIterator must be passed non-null object");
1097         } else {
1098             throw new IllegalArgumentException(
1099                     "Cannot format given Object as a Number");
1100         }
1101         return delegate.getIterator(sb.toString());
1102     }
1103 
1104     /**
1105      * Computes the divisor using minimum integer digits and
1106      * matched pattern index.
1107      * @param minIntDigits string of 0s in compact pattern
1108      * @param patternIndex index of matched compact pattern
1109      * @return divisor value for the number matching the compact
1110      *         pattern at given {@code patternIndex}
1111      */
1112     private Number computeDivisor(String minIntDigits, int patternIndex) {
1113         int count = minIntDigits.length();
1114         Number matchedValue;
1115         // The divisor value can go above long range, if the compact patterns
1116         // goes above index 18, divisor may need to be stored as BigInteger,
1117         // since long can't store numbers >= 10^19,
1118         if (patternIndex < 19) {
1119             matchedValue = (long) Math.pow(RANGE_MULTIPLIER, patternIndex);
1120         } else {
1121             matchedValue = BigInteger.valueOf(RANGE_MULTIPLIER).pow(patternIndex);
1122         }
1123         Number divisor = matchedValue;
1124         if (count > 0) {
1125             if (matchedValue instanceof BigInteger) {
1126                 BigInteger bigValue = (BigInteger) matchedValue;
1127                 if (bigValue.compareTo(BigInteger.valueOf((long) Math.pow(RANGE_MULTIPLIER, count - 1))) < 0) {
1128                     throw new IllegalArgumentException("Invalid Pattern"
1129                             + " [" + compactPatterns[patternIndex]
1130                             + "]: min integer digits specified exceeds the limit"
1131                             + " for the index " + patternIndex);
1132                 }
1133                 divisor = bigValue.divide(BigInteger.valueOf((long) Math.pow(RANGE_MULTIPLIER, count - 1)));
1134             } else {
1135                 long longValue = (long) matchedValue;
1136                 if (longValue < (long) Math.pow(RANGE_MULTIPLIER, count - 1)) {
1137                     throw new IllegalArgumentException("Invalid Pattern"
1138                             + " [" + compactPatterns[patternIndex]
1139                             + "]: min integer digits specified exceeds the limit"
1140                             + " for the index " + patternIndex);
1141                 }
1142                 divisor = longValue / (long) Math.pow(RANGE_MULTIPLIER, count - 1);
1143             }
1144         }
1145         return divisor;
1146     }
1147 
1148     /**
1149      * Process the series of compact patterns to compute the
1150      * series of prefixes, suffixes and their respective divisor
1151      * value.
1152      *
1153      */
1154     private static final Pattern PLURALS =
1155             Pattern.compile("^\\{(?<plurals>.*)}$");
1156     private static final Pattern COUNT_PATTERN =
1157             Pattern.compile("(zero|one|two|few|many|other):((' '|[^ ])+)[ ]*");
1158     private void processCompactPatterns() {
1159         int size = compactPatterns.length;
1160         positivePrefixPatterns = new ArrayList<>(size);
1161         negativePrefixPatterns = new ArrayList<>(size);
1162         positiveSuffixPatterns = new ArrayList<>(size);
1163         negativeSuffixPatterns = new ArrayList<>(size);
1164         divisors = new ArrayList<>(size);
1165         placeHolders = new ArrayList<>(size);
1166 
1167         for (int index = 0; index < size; index++) {
1168             String text = compactPatterns[index];
1169             positivePrefixPatterns.add(new Patterns());
1170             negativePrefixPatterns.add(new Patterns());
1171             positiveSuffixPatterns.add(new Patterns());
1172             negativeSuffixPatterns.add(new Patterns());
1173             placeHolders.add(new Patterns());
1174 
1175             // check if it is the old style
1176             Matcher m = text != null ? PLURALS.matcher(text) : null;
1177             if (m != null && m.matches()) {
1178                 final int idx = index;
1179                 String plurals = m.group("plurals");
1180                 COUNT_PATTERN.matcher(plurals).results()
1181                         .forEach(mr -> applyPattern(mr.group(1), mr.group(2), idx));
1182             } else {
1183                 applyPattern("other", text, index);
1184             }
1185         }
1186 
1187         rulesMap = buildPluralRulesMap();
1188     }
1189 
1190     /**
1191      * Build the plural rules map.
1192      *
1193      * @throws IllegalArgumentException if the {@code pluralRules} has invalid syntax,


1195      */
1196     private Map<String, String> buildPluralRulesMap() {
1197         // length limitation check. 2K for now.
1198         if (pluralRules.length() > 2_048) {
1199             throw new IllegalArgumentException("plural rules is too long (> 2,048)");
1200         }
1201 
1202         try {
1203             return Arrays.stream(pluralRules.split(";"))
1204                 .map(this::validateRule)
1205                 .collect(Collectors.toMap(
1206                         r -> r.replaceFirst(":.*", ""),
1207                         r -> r.replaceFirst("[^:]+:", "")
1208                 ));
1209         } catch (IllegalStateException ise) {
1210             throw new IllegalArgumentException(ise);
1211         }
1212     }
1213 
1214     // Patterns for plurals syntax validation
1215     private final static String EXPR = "([niftvw])\\s*(([/%])\\s*(\\d+))*";
1216     private final static String RELATION = "(!?=)";
1217     private final static String VALUE_RANGE = "((\\d+)\\.\\.(\\d+)|\\d+)";
1218     private final static String CONDITION = EXPR + "\\s*" +
1219                                              RELATION + "\\s*" +
1220                                              VALUE_RANGE + "\\s*" +
1221                                              "(,\\s*" + VALUE_RANGE + ")*";
1222     private final static Pattern PLURALRULES_PATTERN =
1223             Pattern.compile("(zero|one|two|few|many):\\s*" +
1224                             CONDITION +
1225                             "(\\s*(and|or)\\s*" + CONDITION + ")*");
1226 
1227     /**
1228      * Validates a plural rule.
1229      * @param rule rule to validate
1230      * @throws IllegalArgumentException if the {@code rule} has invalid syntax
1231      * @return the input rule (trimmed)
1232      */
1233     private String validateRule(String rule) {
1234         rule = rule.trim();
1235         if (!rule.isEmpty() && !rule.equals("other:")) {
1236             Matcher validator = PLURALRULES_PATTERN.matcher(rule);
1237             if (!validator.matches()) {
1238                 throw new IllegalArgumentException("Invalid plural rules syntax: " + rule);
1239             }
1240         }
1241 


1397                 positiveSuffix = suffix.toString();
1398                 negativePrefix = positivePrefix;
1399                 negativeSuffix = positiveSuffix;
1400             } else {
1401                 negativePrefix = prefix.toString();
1402                 negativeSuffix = suffix.toString();
1403                 gotNegative = true;
1404             }
1405 
1406             // If there is no negative pattern, or if the negative pattern is
1407             // identical to the positive pattern, then prepend the minus sign to
1408             // the positive pattern to form the negative pattern.
1409             if (!gotNegative
1410                     || (negativePrefix.equals(positivePrefix)
1411                     && negativeSuffix.equals(positiveSuffix))) {
1412                 negativeSuffix = positiveSuffix;
1413                 negativePrefix = "'-" + positivePrefix;
1414             }
1415         }
1416 







1417         // Only if positive affix exists; else put empty strings
1418         if (!positivePrefix.isEmpty() || !positiveSuffix.isEmpty()) {
1419             positivePrefixPatterns.get(index).put(count, positivePrefix);
1420             negativePrefixPatterns.get(index).put(count, negativePrefix);
1421             positiveSuffixPatterns.get(index).put(count, positiveSuffix);
1422             negativeSuffixPatterns.get(index).put(count, negativeSuffix);
1423             placeHolders.get(index).put(count, zeros);
1424             if (divisors.size() <= index) {
1425                 divisors.add(computeDivisor(zeros, index));
1426             }
1427         } else {
1428             positivePrefixPatterns.get(index).put(count, "");
1429             negativePrefixPatterns.get(index).put(count, "");
1430             positiveSuffixPatterns.get(index).put(count, "");
1431             negativeSuffixPatterns.get(index).put(count, "");
1432             placeHolders.get(index).put(count, "");
1433             if (divisors.size() <= index) {
1434                 divisors.add(1L);
1435             }
1436         }
1437     }
1438 
1439     private final transient DigitList digitList = new DigitList();
1440     private static final int STATUS_INFINITE = 0;
1441     private static final int STATUS_POSITIVE = 1;
1442     private static final int STATUS_LENGTH   = 2;
1443 
1444     private static final char ZERO_DIGIT = '0';
1445     private static final char DIGIT = '#';
1446     private static final char DECIMAL_SEPARATOR = '.';
1447     private static final char GROUPING_SEPARATOR = ',';
1448     private static final char MINUS_SIGN = '-';
1449     private static final char PERCENT = '%';
1450     private static final char PER_MILLE = '\u2030';
1451     private static final char SEPARATOR = ';';
1452     private static final char CURRENCY_SIGN = '\u00A4';


1609             if (text.regionMatches(pos.index, defaultNegPrefix, 0,
1610                     defaultNegPrefix.length())) {
1611                 // Matches the default negative prefix
1612                 matchedNegPrefix = defaultNegPrefix;
1613                 gotNegative = true;
1614             }
1615         }
1616 
1617         // If both match, take the longest one
1618         if (gotPositive && gotNegative) {
1619             if (matchedPosPrefix.length() > matchedNegPrefix.length()) {
1620                 gotNegative = false;
1621             } else if (matchedPosPrefix.length() < matchedNegPrefix.length()) {
1622                 gotPositive = false;
1623             }
1624         }
1625 
1626         // Update the position and take compact multiplier
1627         // only if it matches the compact prefix, not the default
1628         // prefix; else multiplier should be 1
1629         // If there's no number part, no need to go further, just
1630         // return the multiplier.
1631         if (gotPositive) {
1632             position += matchedPosPrefix.length();
1633             if (matchedPosIndex != -1) {
1634                 cnfMultiplier = divisors.get(matchedPosIndex);
1635                 if (placeHolders.get(matchedPosIndex).get(num).isEmpty()) {
1636                     pos.index = position;
1637                     return cnfMultiplier;
1638                 }
1639             }
1640         } else if (gotNegative) {
1641             position += matchedNegPrefix.length();
1642             if (matchedNegIndex != -1) {
1643                 cnfMultiplier = divisors.get(matchedNegIndex);
1644                 if (placeHolders.get(matchedNegIndex).get(num).isEmpty()) {
1645                     pos.index = position;
1646                     return cnfMultiplier;
1647                 }
1648             }
1649         }
1650 
1651         digitList.setRoundingMode(getRoundingMode());
1652         boolean[] status = new boolean[STATUS_LENGTH];
1653 
1654         // Call DecimalFormat.subparseNumber() method to parse the
1655         // number part of the input text
1656         position = decimalFormat.subparseNumber(text, position,
1657                 digitList, false, false, status);
1658 
1659         if (position == -1) {
1660             // Unable to parse the number successfully
1661             pos.index = oldStart;
1662             pos.errorIndex = oldStart;
1663             return null;
1664         }
1665 
1666         // If parse integer only is true and the parsing is broken at
1667         // decimal point, then pass/ignore all digits and move pointer
1668         // at the start of suffix, to process the suffix part


1712                         .multiply(new BigDecimal(cnfMultiplier.toString()));
1713             }
1714             if (!status[STATUS_POSITIVE]) {
1715                 bigDecimalResult = bigDecimalResult.negate();
1716             }
1717             return bigDecimalResult;
1718         } else {
1719             Number cnfResult;
1720             if (digitList.fitsIntoLong(status[STATUS_POSITIVE], isParseIntegerOnly())) {
1721                 long longResult = digitList.getLong();
1722                 cnfResult = generateParseResult(longResult, false,
1723                         longResult < 0, status, cnfMultiplier);
1724             } else {
1725                 cnfResult = generateParseResult(digitList.getDouble(),
1726                         true, false, status, cnfMultiplier);
1727             }
1728             return cnfResult;
1729         }
1730     }
1731 
1732     private static final Pattern DIGITS = Pattern.compile("\\p{Nd}+");
1733     /**
1734      * Parse the number part in the input text into a number
1735      *
1736      * @param text input text to be parsed
1737      * @param position starting position
1738      * @return the number
1739      */

1740     private double parseNumberPart(String text, int position) {
1741         if (text.startsWith(symbols.getInfinity(), position)) {
1742             return Double.POSITIVE_INFINITY;
1743         } else if (!text.startsWith(symbols.getNaN(), position)) {
1744             Matcher m = DIGITS.matcher(text);
1745             if (m.find(position)) {
1746                 String digits = m.group();
1747                 int cp = digits.codePointAt(0);
1748                 if (Character.isDigit(cp)) {
1749                     return Double.parseDouble(digits.codePoints()
1750                         .map(Character::getNumericValue)
1751                         .mapToObj(Integer::toString)
1752                         .collect(Collectors.joining()));
1753                 }
1754             } else {
1755                 // no numbers. return 1.0 for possible no-placeholder pattern
1756                 return 1.0;
1757             }
1758         }
1759         return Double.NaN;
1760     }
1761 
1762     /**
1763      * Returns the parsed result by multiplying the parsed number
1764      * with the multiplier representing the prefix and suffix.
1765      *
1766      * @param number parsed number component
1767      * @param gotDouble whether the parsed number contains decimal
1768      * @param gotLongMin whether the parsed number is Long.MIN
1769      * @param status boolean status flags indicating whether the
1770      *               value is infinite and whether it is positive
1771      * @param cnfMultiplier compact number multiplier
1772      * @return parsed result
1773      */
1774     private Number generateParseResult(Number number, boolean gotDouble,
1775             boolean gotLongMin, boolean[] status, Number cnfMultiplier) {
1776 


1828             } else {
1829                 return -(double) number;
1830             }
1831         } else {
1832             return number;
1833         }
1834     }
1835 
1836     /**
1837      * Attempts to match the given {@code affix} in the
1838      * specified {@code text}.
1839      */
1840     private boolean matchAffix(String text, int position, String affix,
1841             String defaultAffix, String matchedAffix) {
1842 
1843         // Check with the compact affixes which are non empty and
1844         // do not match with default affix
1845         if (!affix.isEmpty() && !affix.equals(defaultAffix)) {
1846             // Look ahead only for the longer match than the previous match
1847             if (matchedAffix.length() < affix.length()) {
1848                 return text.regionMatches(position, affix, 0, affix.length());


1849             }
1850         }
1851         return false;
1852     }
1853 
1854     /**
1855      * Attempts to match given {@code prefix} and {@code suffix} in
1856      * the specified {@code text}.
1857      */
1858     private boolean matchPrefixAndSuffix(String text, int position, String prefix,
1859             String matchedPrefix, String defaultPrefix, String suffix,
1860             String matchedSuffix, String defaultSuffix) {
1861 
1862         // Check the compact pattern suffix only if there is a
1863         // compact prefix match or a default prefix match
1864         // because the compact prefix and suffix should match at the same
1865         // index to obtain the multiplier.
1866         // The prefix match is required because of the possibility of
1867         // same prefix at multiple index, in which case matching the suffix
1868         // is used to obtain the single match


2362                 Objects.hash(decimalPattern, symbols, roundingMode, pluralRules)
2363                 + Arrays.hashCode(compactPatterns) + groupingSize
2364                 + Boolean.hashCode(parseBigDecimal);
2365     }
2366 
2367     /**
2368      * Creates and returns a copy of this {@code CompactNumberFormat}
2369      * instance.
2370      *
2371      * @return a clone of this instance
2372      */
2373     @Override
2374     public CompactNumberFormat clone() {
2375         CompactNumberFormat other = (CompactNumberFormat) super.clone();
2376         other.compactPatterns = compactPatterns.clone();
2377         other.symbols = (DecimalFormatSymbols) symbols.clone();
2378         return other;
2379     }
2380 
2381     /**
2382      * Abstraction of affix or number (represented by zeros) patterns for each "count" tag.
2383      */
2384     private final class Patterns {
2385         private final Map<String, String> patternsMap = new HashMap<>();
2386 
2387         void put(String count, String pattern) {
2388             patternsMap.put(count, pattern);
2389         }
2390 
2391         String get(double num) {
2392             return patternsMap.getOrDefault(getPluralCategory(num),
2393                     patternsMap.getOrDefault("other", ""));
2394         }
2395 
2396         Patterns expandAffix() {
2397             Patterns ret = new Patterns();
2398             patternsMap.forEach((key, value) -> ret.put(key, CompactNumberFormat.this.expandAffix(value)));

2399             return ret;
2400         }
2401     }
2402 
2403     private int getIntegerPart(double number, double divisor) {
2404         return BigDecimal.valueOf(number)
2405                 .divide(BigDecimal.valueOf(divisor), roundingMode).intValue();
2406     }
2407 
2408     /**
2409      * Returns LDML's tag from the plurals rules
2410      *
2411      * @param input input number in double type
2412      * @return LDML "count" tag
2413      */
2414     private String getPluralCategory(double input) {
2415         if (rulesMap != null) {
2416             return rulesMap.entrySet().stream()
2417                     .filter(e -> matchPluralRule(e.getValue(), input))
2418                     .map(Map.Entry::getKey)
2419                     .findFirst()
2420                     .orElse("other");
2421         }
2422 
2423         // defaults to "other"
2424         return "other";
2425     }
2426 
2427     private static boolean matchPluralRule(String condition, double input) {
2428         return Arrays.stream(condition.split("or"))
2429             .anyMatch(and_condition -> Arrays.stream(and_condition.split("and"))
2430                 .allMatch(r -> relationCheck(r, input)));


2431     }
2432 
2433     private final static String NAMED_EXPR = "(?<op>[niftvw])\\s*((?<div>[/%])\\s*(?<val>\\d+))*";
2434     private final static String NAMED_RELATION = "(?<rel>!?=)";
2435     private final static String NAMED_VALUE_RANGE = "(?<start>\\d+)\\.\\.(?<end>\\d+)|(?<value>\\d+)";
2436     private final static Pattern EXPR_PATTERN = Pattern.compile(NAMED_EXPR);
2437     private final static Pattern RELATION_PATTERN = Pattern.compile(NAMED_RELATION);
2438     private final static Pattern VALUE_RANGE_PATTERN = Pattern.compile(NAMED_VALUE_RANGE);
2439 
2440     /**
2441      * Checks if the 'input' equals the value, or within the range.
2442      *
2443      * @param valueOrRange A string representing either a single value or a range
2444      * @param input to examine in double
2445      * @return match indicator
2446      */
2447     private static boolean valOrRangeMatches(String valueOrRange, double input) {
2448         Matcher m = VALUE_RANGE_PATTERN.matcher(valueOrRange);
2449 
2450         if (m.find()) {
2451             String value = m.group("value");
2452             if (value != null) {
2453                 return input == Double.parseDouble(value);
2454             } else {


2464      * Checks if the input value satisfies the relation. Each possible value or range is
2465      * separated by a comma ','
2466      *
2467      * @param relation relation string, e.g, "n = 1, 3..5", or "n != 1, 3..5"
2468      * @param input value to examine in double
2469      * @return boolean to indicate whether the relation satisfies or not. If the relation
2470      *  is '=', true if any of the possible value/range satisfies. If the relation is '!=',
2471      *  none of the possible value/range should satisfy to return true.
2472      */
2473     private static boolean relationCheck(String relation, double input) {
2474         Matcher expr = EXPR_PATTERN.matcher(relation);
2475 
2476         if (expr.find()) {
2477             double lop = evalLOperand(expr, input);
2478             Matcher rel = RELATION_PATTERN.matcher(relation);
2479 
2480             if (rel.find(expr.end())) {
2481                 var conditions =
2482                     Arrays.stream(relation.substring(rel.end()).split(","));
2483 
2484                 if (Objects.equals(rel.group("rel"), "!=")) {
2485                     return conditions.noneMatch(c -> valOrRangeMatches(c, lop));
2486                 } else {
2487                     return conditions.anyMatch(c -> valOrRangeMatches(c, lop));
2488                 }
2489             }
2490         }
2491 
2492         return false;
2493     }
2494 
2495     /**
2496      * Evaluates the left operand value.
2497      *
2498      * @param expr Match result
2499      * @param input value to examine in double
2500      * @return resulting double value
2501      */
2502     private static double evalLOperand(Matcher expr, double input) {
2503         double ret = 0;
2504 
2505         if (input == Double.POSITIVE_INFINITY) {
2506             ret =input;
2507         } else {
2508             String op = expr.group("op");
2509             if (Objects.equals(op, "n") || Objects.equals(op, "i")) {
2510                 ret = input;
2511             }
2512 
2513             String divop = expr.group("div");
2514             if (divop != null) {
2515                 String divisor = expr.group("val");
2516                 switch (divop) {
2517                     case "%" -> ret %= Double.parseDouble(divisor);
2518                     case "/" -> ret /= Double.parseDouble(divisor);




2519                 }
2520             }
2521         }
2522 
2523         return ret;
2524     }
2525 }
< prev index next >