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