]> git.saurik.com Git - apple/icu.git/blobdiff - icuSources/i18n/number_patternstring.cpp
ICU-62107.0.1.tar.gz
[apple/icu.git] / icuSources / i18n / number_patternstring.cpp
diff --git a/icuSources/i18n/number_patternstring.cpp b/icuSources/i18n/number_patternstring.cpp
new file mode 100644 (file)
index 0000000..63195ee
--- /dev/null
@@ -0,0 +1,1070 @@
+// © 2017 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+#include "unicode/utypes.h"
+
+#if !UCONFIG_NO_FORMATTING
+
+// Allow implicit conversion from char16_t* to UnicodeString for this file:
+// Helpful in toString methods and elsewhere.
+#define UNISTR_FROM_STRING_EXPLICIT
+#define UNISTR_FROM_CHAR_EXPLICIT
+
+#include "uassert.h"
+#include "number_patternstring.h"
+#include "unicode/utf16.h"
+#include "number_utils.h"
+#include "number_roundingutils.h"
+
+using namespace icu;
+using namespace icu::number;
+using namespace icu::number::impl;
+
+
+void PatternParser::parseToPatternInfo(const UnicodeString& patternString, ParsedPatternInfo& patternInfo,
+                                       UErrorCode& status) {
+    patternInfo.consumePattern(patternString, status);
+}
+
+DecimalFormatProperties
+PatternParser::parseToProperties(const UnicodeString& pattern, IgnoreRounding ignoreRounding,
+                                 UErrorCode& status) {
+    DecimalFormatProperties properties;
+    parseToExistingPropertiesImpl(pattern, properties, ignoreRounding, status);
+    return properties;
+}
+
+DecimalFormatProperties PatternParser::parseToProperties(const UnicodeString& pattern,
+                                                         UErrorCode& status) {
+    return parseToProperties(pattern, IGNORE_ROUNDING_NEVER, status);
+}
+
+void
+PatternParser::parseToExistingProperties(const UnicodeString& pattern, DecimalFormatProperties& properties,
+                                         IgnoreRounding ignoreRounding, UErrorCode& status) {
+    parseToExistingPropertiesImpl(pattern, properties, ignoreRounding, status);
+}
+
+
+char16_t ParsedPatternInfo::charAt(int32_t flags, int32_t index) const {
+    const Endpoints& endpoints = getEndpoints(flags);
+    if (index < 0 || index >= endpoints.end - endpoints.start) {
+        U_ASSERT(false);
+    }
+    return pattern.charAt(endpoints.start + index);
+}
+
+int32_t ParsedPatternInfo::length(int32_t flags) const {
+    return getLengthFromEndpoints(getEndpoints(flags));
+}
+
+int32_t ParsedPatternInfo::getLengthFromEndpoints(const Endpoints& endpoints) {
+    return endpoints.end - endpoints.start;
+}
+
+UnicodeString ParsedPatternInfo::getString(int32_t flags) const {
+    const Endpoints& endpoints = getEndpoints(flags);
+    if (endpoints.start == endpoints.end) {
+        return UnicodeString();
+    }
+    // Create a new UnicodeString
+    return UnicodeString(pattern, endpoints.start, endpoints.end - endpoints.start);
+}
+
+const Endpoints& ParsedPatternInfo::getEndpoints(int32_t flags) const {
+    bool prefix = (flags & AFFIX_PREFIX) != 0;
+    bool isNegative = (flags & AFFIX_NEGATIVE_SUBPATTERN) != 0;
+    bool padding = (flags & AFFIX_PADDING) != 0;
+    if (isNegative && padding) {
+        return negative.paddingEndpoints;
+    } else if (padding) {
+        return positive.paddingEndpoints;
+    } else if (prefix && isNegative) {
+        return negative.prefixEndpoints;
+    } else if (prefix) {
+        return positive.prefixEndpoints;
+    } else if (isNegative) {
+        return negative.suffixEndpoints;
+    } else {
+        return positive.suffixEndpoints;
+    }
+}
+
+bool ParsedPatternInfo::positiveHasPlusSign() const {
+    return positive.hasPlusSign;
+}
+
+bool ParsedPatternInfo::hasNegativeSubpattern() const {
+    return fHasNegativeSubpattern;
+}
+
+bool ParsedPatternInfo::negativeHasMinusSign() const {
+    return negative.hasMinusSign;
+}
+
+bool ParsedPatternInfo::hasCurrencySign() const {
+    return positive.hasCurrencySign || (fHasNegativeSubpattern && negative.hasCurrencySign);
+}
+
+bool ParsedPatternInfo::containsSymbolType(AffixPatternType type, UErrorCode& status) const {
+    return AffixUtils::containsType(pattern, type, status);
+}
+
+bool ParsedPatternInfo::hasBody() const {
+    return positive.integerTotal > 0;
+}
+
+/////////////////////////////////////////////////////
+/// BEGIN RECURSIVE DESCENT PARSER IMPLEMENTATION ///
+/////////////////////////////////////////////////////
+
+UChar32 ParsedPatternInfo::ParserState::peek() {
+    if (offset == pattern.length()) {
+        return -1;
+    } else {
+        return pattern.char32At(offset);
+    }
+}
+
+UChar32 ParsedPatternInfo::ParserState::next() {
+    int codePoint = peek();
+    offset += U16_LENGTH(codePoint);
+    return codePoint;
+}
+
+void ParsedPatternInfo::consumePattern(const UnicodeString& patternString, UErrorCode& status) {
+    if (U_FAILURE(status)) { return; }
+    this->pattern = patternString;
+
+    // This class is not intended for writing twice!
+    // Use move assignment to overwrite instead.
+    U_ASSERT(state.offset == 0);
+
+    // pattern := subpattern (';' subpattern)?
+    currentSubpattern = &positive;
+    consumeSubpattern(status);
+    if (U_FAILURE(status)) { return; }
+    if (state.peek() == u';') {
+        state.next(); // consume the ';'
+        // Don't consume the negative subpattern if it is empty (trailing ';')
+        if (state.peek() != -1) {
+            fHasNegativeSubpattern = true;
+            currentSubpattern = &negative;
+            consumeSubpattern(status);
+            if (U_FAILURE(status)) { return; }
+        }
+    }
+    if (state.peek() != -1) {
+        state.toParseException(u"Found unquoted special character");
+        status = U_UNQUOTED_SPECIAL;
+    }
+}
+
+void ParsedPatternInfo::consumeSubpattern(UErrorCode& status) {
+    // subpattern := literals? number exponent? literals?
+    consumePadding(PadPosition::UNUM_PAD_BEFORE_PREFIX, status);
+    if (U_FAILURE(status)) { return; }
+    consumeAffix(currentSubpattern->prefixEndpoints, status);
+    if (U_FAILURE(status)) { return; }
+    consumePadding(PadPosition::UNUM_PAD_AFTER_PREFIX, status);
+    if (U_FAILURE(status)) { return; }
+    consumeFormat(status);
+    if (U_FAILURE(status)) { return; }
+    consumeExponent(status);
+    if (U_FAILURE(status)) { return; }
+    consumePadding(PadPosition::UNUM_PAD_BEFORE_SUFFIX, status);
+    if (U_FAILURE(status)) { return; }
+    consumeAffix(currentSubpattern->suffixEndpoints, status);
+    if (U_FAILURE(status)) { return; }
+    consumePadding(PadPosition::UNUM_PAD_AFTER_SUFFIX, status);
+    if (U_FAILURE(status)) { return; }
+}
+
+void ParsedPatternInfo::consumePadding(PadPosition paddingLocation, UErrorCode& status) {
+    if (state.peek() != u'*') {
+        return;
+    }
+    if (currentSubpattern->hasPadding) {
+        state.toParseException(u"Cannot have multiple pad specifiers");
+        status = U_MULTIPLE_PAD_SPECIFIERS;
+        return;
+    }
+    currentSubpattern->paddingLocation = paddingLocation;
+    currentSubpattern->hasPadding = true;
+    state.next(); // consume the '*'
+    currentSubpattern->paddingEndpoints.start = state.offset;
+    consumeLiteral(status);
+    currentSubpattern->paddingEndpoints.end = state.offset;
+}
+
+void ParsedPatternInfo::consumeAffix(Endpoints& endpoints, UErrorCode& status) {
+    // literals := { literal }
+    endpoints.start = state.offset;
+    while (true) {
+        switch (state.peek()) {
+            case u'#':
+            case u'@':
+            case u';':
+            case u'*':
+            case u'.':
+            case u',':
+            case u'0':
+            case u'1':
+            case u'2':
+            case u'3':
+            case u'4':
+            case u'5':
+            case u'6':
+            case u'7':
+            case u'8':
+            case u'9':
+            case -1:
+                // Characters that cannot appear unquoted in a literal
+                // break outer;
+                goto after_outer;
+
+            case u'%':
+                currentSubpattern->hasPercentSign = true;
+                break;
+
+            case u'‰':
+                currentSubpattern->hasPerMilleSign = true;
+                break;
+
+            case u'¤':
+                currentSubpattern->hasCurrencySign = true;
+                break;
+
+            case u'-':
+                currentSubpattern->hasMinusSign = true;
+                break;
+
+            case u'+':
+                currentSubpattern->hasPlusSign = true;
+                break;
+
+            default:
+                break;
+        }
+        consumeLiteral(status);
+        if (U_FAILURE(status)) { return; }
+    }
+    after_outer:
+    endpoints.end = state.offset;
+}
+
+void ParsedPatternInfo::consumeLiteral(UErrorCode& status) {
+    if (state.peek() == -1) {
+        state.toParseException(u"Expected unquoted literal but found EOL");
+        status = U_PATTERN_SYNTAX_ERROR;
+        return;
+    } else if (state.peek() == u'\'') {
+        state.next(); // consume the starting quote
+        while (state.peek() != u'\'') {
+            if (state.peek() == -1) {
+                state.toParseException(u"Expected quoted literal but found EOL");
+                status = U_PATTERN_SYNTAX_ERROR;
+                return;
+            } else {
+                state.next(); // consume a quoted character
+            }
+        }
+        state.next(); // consume the ending quote
+    } else {
+        // consume a non-quoted literal character
+        state.next();
+    }
+}
+
+void ParsedPatternInfo::consumeFormat(UErrorCode& status) {
+    consumeIntegerFormat(status);
+    if (U_FAILURE(status)) { return; }
+    if (state.peek() == u'.') {
+        state.next(); // consume the decimal point
+        currentSubpattern->hasDecimal = true;
+        currentSubpattern->widthExceptAffixes += 1;
+        consumeFractionFormat(status);
+        if (U_FAILURE(status)) { return; }
+    }
+}
+
+void ParsedPatternInfo::consumeIntegerFormat(UErrorCode& status) {
+    // Convenience reference:
+    ParsedSubpatternInfo& result = *currentSubpattern;
+
+    while (true) {
+        switch (state.peek()) {
+            case u',':
+                result.widthExceptAffixes += 1;
+                result.groupingSizes <<= 16;
+                break;
+
+            case u'#':
+                if (result.integerNumerals > 0) {
+                    state.toParseException(u"# cannot follow 0 before decimal point");
+                    status = U_UNEXPECTED_TOKEN;
+                    return;
+                }
+                result.widthExceptAffixes += 1;
+                result.groupingSizes += 1;
+                if (result.integerAtSigns > 0) {
+                    result.integerTrailingHashSigns += 1;
+                } else {
+                    result.integerLeadingHashSigns += 1;
+                }
+                result.integerTotal += 1;
+                break;
+
+            case u'@':
+                if (result.integerNumerals > 0) {
+                    state.toParseException(u"Cannot mix 0 and @");
+                    status = U_UNEXPECTED_TOKEN;
+                    return;
+                }
+                if (result.integerTrailingHashSigns > 0) {
+                    state.toParseException(u"Cannot nest # inside of a run of @");
+                    status = U_UNEXPECTED_TOKEN;
+                    return;
+                }
+                result.widthExceptAffixes += 1;
+                result.groupingSizes += 1;
+                result.integerAtSigns += 1;
+                result.integerTotal += 1;
+                break;
+
+            case u'0':
+            case u'1':
+            case u'2':
+            case u'3':
+            case u'4':
+            case u'5':
+            case u'6':
+            case u'7':
+            case u'8':
+            case u'9':
+                if (result.integerAtSigns > 0) {
+                    state.toParseException(u"Cannot mix @ and 0");
+                    status = U_UNEXPECTED_TOKEN;
+                    return;
+                }
+                result.widthExceptAffixes += 1;
+                result.groupingSizes += 1;
+                result.integerNumerals += 1;
+                result.integerTotal += 1;
+                if (!result.rounding.isZero() || state.peek() != u'0') {
+                    result.rounding.appendDigit(static_cast<int8_t>(state.peek() - u'0'), 0, true);
+                }
+                break;
+
+            default:
+                goto after_outer;
+        }
+        state.next(); // consume the symbol
+    }
+
+    after_outer:
+    // Disallow patterns with a trailing ',' or with two ',' next to each other
+    auto grouping1 = static_cast<int16_t> (result.groupingSizes & 0xffff);
+    auto grouping2 = static_cast<int16_t> ((result.groupingSizes >> 16) & 0xffff);
+    auto grouping3 = static_cast<int16_t> ((result.groupingSizes >> 32) & 0xffff);
+    if (grouping1 == 0 && grouping2 != -1) {
+        state.toParseException(u"Trailing grouping separator is invalid");
+        status = U_UNEXPECTED_TOKEN;
+        return;
+    }
+    if (grouping2 == 0 && grouping3 != -1) {
+        state.toParseException(u"Grouping width of zero is invalid");
+        status = U_PATTERN_SYNTAX_ERROR;
+        return;
+    }
+}
+
+void ParsedPatternInfo::consumeFractionFormat(UErrorCode& status) {
+    // Convenience reference:
+    ParsedSubpatternInfo& result = *currentSubpattern;
+
+    int32_t zeroCounter = 0;
+    while (true) {
+        switch (state.peek()) {
+            case u'#':
+                result.widthExceptAffixes += 1;
+                result.fractionHashSigns += 1;
+                result.fractionTotal += 1;
+                zeroCounter++;
+                break;
+
+            case u'0':
+            case u'1':
+            case u'2':
+            case u'3':
+            case u'4':
+            case u'5':
+            case u'6':
+            case u'7':
+            case u'8':
+            case u'9':
+                if (result.fractionHashSigns > 0) {
+                    state.toParseException(u"0 cannot follow # after decimal point");
+                    status = U_UNEXPECTED_TOKEN;
+                    return;
+                }
+                result.widthExceptAffixes += 1;
+                result.fractionNumerals += 1;
+                result.fractionTotal += 1;
+                if (state.peek() == u'0') {
+                    zeroCounter++;
+                } else {
+                    result.rounding
+                            .appendDigit(static_cast<int8_t>(state.peek() - u'0'), zeroCounter, false);
+                    zeroCounter = 0;
+                }
+                break;
+
+            default:
+                return;
+        }
+        state.next(); // consume the symbol
+    }
+}
+
+void ParsedPatternInfo::consumeExponent(UErrorCode& status) {
+    // Convenience reference:
+    ParsedSubpatternInfo& result = *currentSubpattern;
+
+    if (state.peek() != u'E') {
+        return;
+    }
+    if ((result.groupingSizes & 0xffff0000L) != 0xffff0000L) {
+        state.toParseException(u"Cannot have grouping separator in scientific notation");
+        status = U_MALFORMED_EXPONENTIAL_PATTERN;
+        return;
+    }
+    state.next(); // consume the E
+    result.widthExceptAffixes++;
+    if (state.peek() == u'+') {
+        state.next(); // consume the +
+        result.exponentHasPlusSign = true;
+        result.widthExceptAffixes++;
+    }
+    while (state.peek() == u'0') {
+        state.next(); // consume the 0
+        result.exponentZeros += 1;
+        result.widthExceptAffixes++;
+    }
+}
+
+///////////////////////////////////////////////////
+/// END RECURSIVE DESCENT PARSER IMPLEMENTATION ///
+///////////////////////////////////////////////////
+
+void PatternParser::parseToExistingPropertiesImpl(const UnicodeString& pattern,
+                                                  DecimalFormatProperties& properties,
+                                                  IgnoreRounding ignoreRounding, UErrorCode& status) {
+    if (pattern.length() == 0) {
+        // Backwards compatibility requires that we reset to the default values.
+        // TODO: Only overwrite the properties that "saveToProperties" normally touches?
+        properties.clear();
+        return;
+    }
+
+    ParsedPatternInfo patternInfo;
+    parseToPatternInfo(pattern, patternInfo, status);
+    if (U_FAILURE(status)) { return; }
+    patternInfoToProperties(properties, patternInfo, ignoreRounding, status);
+}
+
+void
+PatternParser::patternInfoToProperties(DecimalFormatProperties& properties, ParsedPatternInfo& patternInfo,
+                                       IgnoreRounding _ignoreRounding, UErrorCode& status) {
+    // Translate from PatternParseResult to Properties.
+    // Note that most data from "negative" is ignored per the specification of DecimalFormat.
+
+    const ParsedSubpatternInfo& positive = patternInfo.positive;
+
+    bool ignoreRounding;
+    if (_ignoreRounding == IGNORE_ROUNDING_NEVER) {
+        ignoreRounding = false;
+    } else if (_ignoreRounding == IGNORE_ROUNDING_IF_CURRENCY) {
+        ignoreRounding = positive.hasCurrencySign;
+    } else {
+        U_ASSERT(_ignoreRounding == IGNORE_ROUNDING_ALWAYS);
+        ignoreRounding = true;
+    }
+
+    // Grouping settings
+    auto grouping1 = static_cast<int16_t> (positive.groupingSizes & 0xffff);
+    auto grouping2 = static_cast<int16_t> ((positive.groupingSizes >> 16) & 0xffff);
+    auto grouping3 = static_cast<int16_t> ((positive.groupingSizes >> 32) & 0xffff);
+    if (grouping2 != -1) {
+        properties.groupingSize = grouping1;
+        properties.groupingUsed = true;
+    } else {
+        properties.groupingSize = -1;
+        properties.groupingUsed = false;
+    }
+    if (grouping3 != -1) {
+        properties.secondaryGroupingSize = grouping2;
+    } else {
+        properties.secondaryGroupingSize = -1;
+    }
+
+    // For backwards compatibility, require that the pattern emit at least one min digit.
+    int minInt, minFrac;
+    if (positive.integerTotal == 0 && positive.fractionTotal > 0) {
+        // patterns like ".##"
+        minInt = 0;
+        minFrac = uprv_max(1, positive.fractionNumerals);
+    } else if (positive.integerNumerals == 0 && positive.fractionNumerals == 0) {
+        // patterns like "#.##"
+        minInt = 1;
+        minFrac = 0;
+    } else {
+        minInt = positive.integerNumerals;
+        minFrac = positive.fractionNumerals;
+    }
+
+    // Rounding settings
+    // Don't set basic rounding when there is a currency sign; defer to CurrencyUsage
+    if (positive.integerAtSigns > 0) {
+        properties.minimumFractionDigits = -1;
+        properties.maximumFractionDigits = -1;
+        properties.roundingIncrement = 0.0;
+        properties.minimumSignificantDigits = positive.integerAtSigns;
+        properties.maximumSignificantDigits = positive.integerAtSigns + positive.integerTrailingHashSigns;
+    } else if (!positive.rounding.isZero()) {
+        if (!ignoreRounding) {
+            properties.minimumFractionDigits = minFrac;
+            properties.maximumFractionDigits = positive.fractionTotal;
+            properties.roundingIncrement = positive.rounding.toDouble();
+        } else {
+            properties.minimumFractionDigits = -1;
+            properties.maximumFractionDigits = -1;
+            properties.roundingIncrement = 0.0;
+        }
+        properties.minimumSignificantDigits = -1;
+        properties.maximumSignificantDigits = -1;
+    } else {
+        if (!ignoreRounding) {
+            properties.minimumFractionDigits = minFrac;
+            properties.maximumFractionDigits = positive.fractionTotal;
+            properties.roundingIncrement = 0.0;
+        } else {
+            properties.minimumFractionDigits = -1;
+            properties.maximumFractionDigits = -1;
+            properties.roundingIncrement = 0.0;
+        }
+        properties.minimumSignificantDigits = -1;
+        properties.maximumSignificantDigits = -1;
+    }
+
+    // If the pattern ends with a '.' then force the decimal point.
+    if (positive.hasDecimal && positive.fractionTotal == 0) {
+        properties.decimalSeparatorAlwaysShown = true;
+    } else {
+        properties.decimalSeparatorAlwaysShown = false;
+    }
+
+    // Scientific notation settings
+    if (positive.exponentZeros > 0) {
+        properties.exponentSignAlwaysShown = positive.exponentHasPlusSign;
+        properties.minimumExponentDigits = positive.exponentZeros;
+        if (positive.integerAtSigns == 0) {
+            // patterns without '@' can define max integer digits, used for engineering notation
+            properties.minimumIntegerDigits = positive.integerNumerals;
+            properties.maximumIntegerDigits = positive.integerTotal;
+        } else {
+            // patterns with '@' cannot define max integer digits
+            properties.minimumIntegerDigits = 1;
+            properties.maximumIntegerDigits = -1;
+        }
+    } else {
+        properties.exponentSignAlwaysShown = false;
+        properties.minimumExponentDigits = -1;
+        properties.minimumIntegerDigits = minInt;
+        properties.maximumIntegerDigits = -1;
+    }
+
+    // Compute the affix patterns (required for both padding and affixes)
+    UnicodeString posPrefix = patternInfo.getString(AffixPatternProvider::AFFIX_PREFIX);
+    UnicodeString posSuffix = patternInfo.getString(0);
+
+    // Padding settings
+    if (positive.hasPadding) {
+        // The width of the positive prefix and suffix templates are included in the padding
+        int paddingWidth = positive.widthExceptAffixes +
+                           AffixUtils::estimateLength(posPrefix, status) +
+                           AffixUtils::estimateLength(posSuffix, status);
+        properties.formatWidth = paddingWidth;
+        UnicodeString rawPaddingString = patternInfo.getString(AffixPatternProvider::AFFIX_PADDING);
+        if (rawPaddingString.length() == 1) {
+            properties.padString = rawPaddingString;
+        } else if (rawPaddingString.length() == 2) {
+            if (rawPaddingString.charAt(0) == u'\'') {
+                properties.padString.setTo(u"'", -1);
+            } else {
+                properties.padString = rawPaddingString;
+            }
+        } else {
+            properties.padString = UnicodeString(rawPaddingString, 1, rawPaddingString.length() - 2);
+        }
+        properties.padPosition = positive.paddingLocation;
+    } else {
+        properties.formatWidth = -1;
+        properties.padString.setToBogus();
+        properties.padPosition.nullify();
+    }
+
+    // Set the affixes
+    // Always call the setter, even if the prefixes are empty, especially in the case of the
+    // negative prefix pattern, to prevent default values from overriding the pattern.
+    properties.positivePrefixPattern = posPrefix;
+    properties.positiveSuffixPattern = posSuffix;
+    if (patternInfo.fHasNegativeSubpattern) {
+        properties.negativePrefixPattern = patternInfo.getString(
+                AffixPatternProvider::AFFIX_NEGATIVE_SUBPATTERN | AffixPatternProvider::AFFIX_PREFIX);
+        properties.negativeSuffixPattern = patternInfo.getString(
+                AffixPatternProvider::AFFIX_NEGATIVE_SUBPATTERN);
+    } else {
+        properties.negativePrefixPattern.setToBogus();
+        properties.negativeSuffixPattern.setToBogus();
+    }
+
+    // Set the magnitude multiplier
+    if (positive.hasPercentSign) {
+        properties.magnitudeMultiplier = 2;
+    } else if (positive.hasPerMilleSign) {
+        properties.magnitudeMultiplier = 3;
+    } else {
+        properties.magnitudeMultiplier = 0;
+    }
+}
+
+///////////////////////////////////////////////////////////////////
+/// End PatternStringParser.java; begin PatternStringUtils.java ///
+///////////////////////////////////////////////////////////////////
+
+UnicodeString PatternStringUtils::propertiesToPatternString(const DecimalFormatProperties& properties,
+                                                            UErrorCode& status) {
+    UnicodeString sb;
+
+    // Convenience references
+    // The uprv_min() calls prevent DoS
+    int dosMax = 100;
+    int groupingSize = uprv_min(properties.secondaryGroupingSize, dosMax);
+    int firstGroupingSize = uprv_min(properties.groupingSize, dosMax);
+    int paddingWidth = uprv_min(properties.formatWidth, dosMax);
+    NullableValue<PadPosition> paddingLocation = properties.padPosition;
+    UnicodeString paddingString = properties.padString;
+    int minInt = uprv_max(uprv_min(properties.minimumIntegerDigits, dosMax), 0);
+    int maxInt = uprv_min(properties.maximumIntegerDigits, dosMax);
+    int minFrac = uprv_max(uprv_min(properties.minimumFractionDigits, dosMax), 0);
+    int maxFrac = uprv_min(properties.maximumFractionDigits, dosMax);
+    int minSig = uprv_min(properties.minimumSignificantDigits, dosMax);
+    int maxSig = uprv_min(properties.maximumSignificantDigits, dosMax);
+    bool alwaysShowDecimal = properties.decimalSeparatorAlwaysShown;
+    int exponentDigits = uprv_min(properties.minimumExponentDigits, dosMax);
+    bool exponentShowPlusSign = properties.exponentSignAlwaysShown;
+    UnicodeString pp = properties.positivePrefix;
+    UnicodeString ppp = properties.positivePrefixPattern;
+    UnicodeString ps = properties.positiveSuffix;
+    UnicodeString psp = properties.positiveSuffixPattern;
+    UnicodeString np = properties.negativePrefix;
+    UnicodeString npp = properties.negativePrefixPattern;
+    UnicodeString ns = properties.negativeSuffix;
+    UnicodeString nsp = properties.negativeSuffixPattern;
+
+    // Prefixes
+    if (!ppp.isBogus()) {
+        sb.append(ppp);
+    }
+    sb.append(AffixUtils::escape(pp));
+    int afterPrefixPos = sb.length();
+
+    // Figure out the grouping sizes.
+    int grouping1, grouping2, grouping;
+    if (groupingSize != uprv_min(dosMax, -1) && firstGroupingSize != uprv_min(dosMax, -1) &&
+        groupingSize != firstGroupingSize) {
+        grouping = groupingSize;
+        grouping1 = groupingSize;
+        grouping2 = firstGroupingSize;
+    } else if (groupingSize != uprv_min(dosMax, -1)) {
+        grouping = groupingSize;
+        grouping1 = 0;
+        grouping2 = groupingSize;
+    } else if (firstGroupingSize != uprv_min(dosMax, -1)) {
+        grouping = groupingSize;
+        grouping1 = 0;
+        grouping2 = firstGroupingSize;
+    } else {
+        grouping = 0;
+        grouping1 = 0;
+        grouping2 = 0;
+    }
+    int groupingLength = grouping1 + grouping2 + 1;
+
+    // Figure out the digits we need to put in the pattern.
+    double roundingInterval = properties.roundingIncrement;
+    UnicodeString digitsString;
+    int digitsStringScale = 0;
+    if (maxSig != uprv_min(dosMax, -1)) {
+        // Significant Digits.
+        while (digitsString.length() < minSig) {
+            digitsString.append(u'@');
+        }
+        while (digitsString.length() < maxSig) {
+            digitsString.append(u'#');
+        }
+    } else if (roundingInterval != 0.0) {
+        // Rounding Interval.
+        digitsStringScale = -roundingutils::doubleFractionLength(roundingInterval);
+        // TODO: Check for DoS here?
+        DecimalQuantity incrementQuantity;
+        incrementQuantity.setToDouble(roundingInterval);
+        incrementQuantity.adjustMagnitude(-digitsStringScale);
+        incrementQuantity.roundToMagnitude(0, kDefaultMode, status);
+        UnicodeString str = incrementQuantity.toPlainString();
+        if (str.charAt(0) == u'-') {
+            // TODO: Unsupported operation exception or fail silently?
+            digitsString.append(str, 1, str.length() - 1);
+        } else {
+            digitsString.append(str);
+        }
+    }
+    while (digitsString.length() + digitsStringScale < minInt) {
+        digitsString.insert(0, u'0');
+    }
+    while (-digitsStringScale < minFrac) {
+        digitsString.append(u'0');
+        digitsStringScale--;
+    }
+
+    // Write the digits to the string builder
+    int m0 = uprv_max(groupingLength, digitsString.length() + digitsStringScale);
+    m0 = (maxInt != dosMax) ? uprv_max(maxInt, m0) - 1 : m0 - 1;
+    int mN = (maxFrac != dosMax) ? uprv_min(-maxFrac, digitsStringScale) : digitsStringScale;
+    for (int magnitude = m0; magnitude >= mN; magnitude--) {
+        int di = digitsString.length() + digitsStringScale - magnitude - 1;
+        if (di < 0 || di >= digitsString.length()) {
+            sb.append(u'#');
+        } else {
+            sb.append(digitsString.charAt(di));
+        }
+        if (magnitude > grouping2 && grouping > 0 && (magnitude - grouping2) % grouping == 0) {
+            sb.append(u',');
+        } else if (magnitude > 0 && magnitude == grouping2) {
+            sb.append(u',');
+        } else if (magnitude == 0 && (alwaysShowDecimal || mN < 0)) {
+            sb.append(u'.');
+        }
+    }
+
+    // Exponential notation
+    if (exponentDigits != uprv_min(dosMax, -1)) {
+        sb.append(u'E');
+        if (exponentShowPlusSign) {
+            sb.append(u'+');
+        }
+        for (int i = 0; i < exponentDigits; i++) {
+            sb.append(u'0');
+        }
+    }
+
+    // Suffixes
+    int beforeSuffixPos = sb.length();
+    if (!psp.isBogus()) {
+        sb.append(psp);
+    }
+    sb.append(AffixUtils::escape(ps));
+
+    // Resolve Padding
+    if (paddingWidth != -1 && !paddingLocation.isNull()) {
+        while (paddingWidth - sb.length() > 0) {
+            sb.insert(afterPrefixPos, u'#');
+            beforeSuffixPos++;
+        }
+        int addedLength;
+        switch (paddingLocation.get(status)) {
+            case PadPosition::UNUM_PAD_BEFORE_PREFIX:
+                addedLength = escapePaddingString(paddingString, sb, 0, status);
+                sb.insert(0, u'*');
+                afterPrefixPos += addedLength + 1;
+                beforeSuffixPos += addedLength + 1;
+                break;
+            case PadPosition::UNUM_PAD_AFTER_PREFIX:
+                addedLength = escapePaddingString(paddingString, sb, afterPrefixPos, status);
+                sb.insert(afterPrefixPos, u'*');
+                afterPrefixPos += addedLength + 1;
+                beforeSuffixPos += addedLength + 1;
+                break;
+            case PadPosition::UNUM_PAD_BEFORE_SUFFIX:
+                escapePaddingString(paddingString, sb, beforeSuffixPos, status);
+                sb.insert(beforeSuffixPos, u'*');
+                break;
+            case PadPosition::UNUM_PAD_AFTER_SUFFIX:
+                sb.append(u'*');
+                escapePaddingString(paddingString, sb, sb.length(), status);
+                break;
+        }
+        if (U_FAILURE(status)) { return sb; }
+    }
+
+    // Negative affixes
+    // Ignore if the negative prefix pattern is "-" and the negative suffix is empty
+    if (!np.isBogus() || !ns.isBogus() || (npp.isBogus() && !nsp.isBogus()) ||
+        (!npp.isBogus() && (npp.length() != 1 || npp.charAt(0) != u'-' || nsp.length() != 0))) {
+        sb.append(u';');
+        if (!npp.isBogus()) {
+            sb.append(npp);
+        }
+        sb.append(AffixUtils::escape(np));
+        // Copy the positive digit format into the negative.
+        // This is optional; the pattern is the same as if '#' were appended here instead.
+        // NOTE: It is not safe to append the UnicodeString to itself, so we need to copy.
+        // See http://bugs.icu-project.org/trac/ticket/13707
+        UnicodeString copy(sb);
+        sb.append(copy, afterPrefixPos, beforeSuffixPos - afterPrefixPos);
+        if (!nsp.isBogus()) {
+            sb.append(nsp);
+        }
+        sb.append(AffixUtils::escape(ns));
+    }
+
+    return sb;
+}
+
+int PatternStringUtils::escapePaddingString(UnicodeString input, UnicodeString& output, int startIndex,
+                                            UErrorCode& status) {
+    (void) status;
+    if (input.length() == 0) {
+        input.setTo(kFallbackPaddingString, -1);
+    }
+    int startLength = output.length();
+    if (input.length() == 1) {
+        if (input.compare(u"'", -1) == 0) {
+            output.insert(startIndex, u"''", -1);
+        } else {
+            output.insert(startIndex, input);
+        }
+    } else {
+        output.insert(startIndex, u'\'');
+        int offset = 1;
+        for (int i = 0; i < input.length(); i++) {
+            // it's okay to deal in chars here because the quote mark is the only interesting thing.
+            char16_t ch = input.charAt(i);
+            if (ch == u'\'') {
+                output.insert(startIndex + offset, u"''", -1);
+                offset += 2;
+            } else {
+                output.insert(startIndex + offset, ch);
+                offset += 1;
+            }
+        }
+        output.insert(startIndex + offset, u'\'');
+    }
+    return output.length() - startLength;
+}
+
+UnicodeString
+PatternStringUtils::convertLocalized(const UnicodeString& input, const DecimalFormatSymbols& symbols,
+                                     bool toLocalized, UErrorCode& status) {
+    // Construct a table of strings to be converted between localized and standard.
+    static constexpr int32_t LEN = 21;
+    UnicodeString table[LEN][2];
+    int standIdx = toLocalized ? 0 : 1;
+    int localIdx = toLocalized ? 1 : 0;
+    table[0][standIdx] = u"%";
+    table[0][localIdx] = symbols.getConstSymbol(DecimalFormatSymbols::kPercentSymbol);
+    table[1][standIdx] = u"‰";
+    table[1][localIdx] = symbols.getConstSymbol(DecimalFormatSymbols::kPerMillSymbol);
+    table[2][standIdx] = u".";
+    table[2][localIdx] = symbols.getConstSymbol(DecimalFormatSymbols::kDecimalSeparatorSymbol);
+    table[3][standIdx] = u",";
+    table[3][localIdx] = symbols.getConstSymbol(DecimalFormatSymbols::kGroupingSeparatorSymbol);
+    table[4][standIdx] = u"-";
+    table[4][localIdx] = symbols.getConstSymbol(DecimalFormatSymbols::kMinusSignSymbol);
+    table[5][standIdx] = u"+";
+    table[5][localIdx] = symbols.getConstSymbol(DecimalFormatSymbols::kPlusSignSymbol);
+    table[6][standIdx] = u";";
+    table[6][localIdx] = symbols.getConstSymbol(DecimalFormatSymbols::kPatternSeparatorSymbol);
+    table[7][standIdx] = u"@";
+    table[7][localIdx] = symbols.getConstSymbol(DecimalFormatSymbols::kSignificantDigitSymbol);
+    table[8][standIdx] = u"E";
+    table[8][localIdx] = symbols.getConstSymbol(DecimalFormatSymbols::kExponentialSymbol);
+    table[9][standIdx] = u"*";
+    table[9][localIdx] = symbols.getConstSymbol(DecimalFormatSymbols::kPadEscapeSymbol);
+    table[10][standIdx] = u"#";
+    table[10][localIdx] = symbols.getConstSymbol(DecimalFormatSymbols::kDigitSymbol);
+    for (int i = 0; i < 10; i++) {
+        table[11 + i][standIdx] = u'0' + i;
+        table[11 + i][localIdx] = symbols.getConstDigitSymbol(i);
+    }
+
+    // Special case: quotes are NOT allowed to be in any localIdx strings.
+    // Substitute them with '’' instead.
+    for (int32_t i = 0; i < LEN; i++) {
+        table[i][localIdx].findAndReplace(u'\'', u'’');
+    }
+
+    // Iterate through the string and convert.
+    // State table:
+    // 0 => base state
+    // 1 => first char inside a quoted sequence in input and output string
+    // 2 => inside a quoted sequence in input and output string
+    // 3 => first char after a close quote in input string;
+    // close quote still needs to be written to output string
+    // 4 => base state in input string; inside quoted sequence in output string
+    // 5 => first char inside a quoted sequence in input string;
+    // inside quoted sequence in output string
+    UnicodeString result;
+    int state = 0;
+    for (int offset = 0; offset < input.length(); offset++) {
+        UChar ch = input.charAt(offset);
+
+        // Handle a quote character (state shift)
+        if (ch == u'\'') {
+            if (state == 0) {
+                result.append(u'\'');
+                state = 1;
+                continue;
+            } else if (state == 1) {
+                result.append(u'\'');
+                state = 0;
+                continue;
+            } else if (state == 2) {
+                state = 3;
+                continue;
+            } else if (state == 3) {
+                result.append(u'\'');
+                result.append(u'\'');
+                state = 1;
+                continue;
+            } else if (state == 4) {
+                state = 5;
+                continue;
+            } else {
+                U_ASSERT(state == 5);
+                result.append(u'\'');
+                result.append(u'\'');
+                state = 4;
+                continue;
+            }
+        }
+
+        if (state == 0 || state == 3 || state == 4) {
+            for (auto& pair : table) {
+                // Perform a greedy match on this symbol string
+                UnicodeString temp = input.tempSubString(offset, pair[0].length());
+                if (temp == pair[0]) {
+                    // Skip ahead past this region for the next iteration
+                    offset += pair[0].length() - 1;
+                    if (state == 3 || state == 4) {
+                        result.append(u'\'');
+                        state = 0;
+                    }
+                    result.append(pair[1]);
+                    goto continue_outer;
+                }
+            }
+            // No replacement found. Check if a special quote is necessary
+            for (auto& pair : table) {
+                UnicodeString temp = input.tempSubString(offset, pair[1].length());
+                if (temp == pair[1]) {
+                    if (state == 0) {
+                        result.append(u'\'');
+                        state = 4;
+                    }
+                    result.append(ch);
+                    goto continue_outer;
+                }
+            }
+            // Still nothing. Copy the char verbatim. (Add a close quote if necessary)
+            if (state == 3 || state == 4) {
+                result.append(u'\'');
+                state = 0;
+            }
+            result.append(ch);
+        } else {
+            U_ASSERT(state == 1 || state == 2 || state == 5);
+            result.append(ch);
+            state = 2;
+        }
+        continue_outer:;
+    }
+    // Resolve final quotes
+    if (state == 3 || state == 4) {
+        result.append(u'\'');
+        state = 0;
+    }
+    if (state != 0) {
+        // Malformed localized pattern: unterminated quote
+        status = U_PATTERN_SYNTAX_ERROR;
+    }
+    return result;
+}
+
+void PatternStringUtils::patternInfoToStringBuilder(const AffixPatternProvider& patternInfo, bool isPrefix,
+                                                    int8_t signum, UNumberSignDisplay signDisplay,
+                                                    StandardPlural::Form plural,
+                                                    bool perMilleReplacesPercent, UnicodeString& output) {
+
+    // Should the output render '+' where '-' would normally appear in the pattern?
+    bool plusReplacesMinusSign = signum != -1 && (
+            signDisplay == UNUM_SIGN_ALWAYS || signDisplay == UNUM_SIGN_ACCOUNTING_ALWAYS || (
+                    signum == 1 && (
+                            signDisplay == UNUM_SIGN_EXCEPT_ZERO ||
+                            signDisplay == UNUM_SIGN_ACCOUNTING_EXCEPT_ZERO))) &&
+                                 patternInfo.positiveHasPlusSign() == false;
+
+    // Should we use the affix from the negative subpattern? (If not, we will use the positive
+    // subpattern.)
+    bool useNegativeAffixPattern = patternInfo.hasNegativeSubpattern() && (
+            signum == -1 || (patternInfo.negativeHasMinusSign() && plusReplacesMinusSign));
+
+    // Resolve the flags for the affix pattern.
+    int flags = 0;
+    if (useNegativeAffixPattern) {
+        flags |= AffixPatternProvider::AFFIX_NEGATIVE_SUBPATTERN;
+    }
+    if (isPrefix) {
+        flags |= AffixPatternProvider::AFFIX_PREFIX;
+    }
+    if (plural != StandardPlural::Form::COUNT) {
+        U_ASSERT(plural == (AffixPatternProvider::AFFIX_PLURAL_MASK & plural));
+        flags |= plural;
+    }
+
+    // Should we prepend a sign to the pattern?
+    bool prependSign;
+    if (!isPrefix || useNegativeAffixPattern) {
+        prependSign = false;
+    } else if (signum == -1) {
+        prependSign = signDisplay != UNUM_SIGN_NEVER;
+    } else {
+        prependSign = plusReplacesMinusSign;
+    }
+
+    // Compute the length of the affix pattern.
+    int length = patternInfo.length(flags) + (prependSign ? 1 : 0);
+
+    // Finally, set the result into the StringBuilder.
+    output.remove();
+    for (int index = 0; index < length; index++) {
+        char16_t candidate;
+        if (prependSign && index == 0) {
+            candidate = u'-';
+        } else if (prependSign) {
+            candidate = patternInfo.charAt(flags, index - 1);
+        } else {
+            candidate = patternInfo.charAt(flags, index);
+        }
+        if (plusReplacesMinusSign && candidate == u'-') {
+            candidate = u'+';
+        }
+        if (perMilleReplacesPercent && candidate == u'%') {
+            candidate = u'‰';
+        }
+        output.append(candidate);
+    }
+}
+
+#endif /* #if !UCONFIG_NO_FORMATTING */