From 47c57c83835ca475d7dc7429526bd7f20a0ff920 Mon Sep 17 00:00:00 2001 From: Tim Chevalier Date: Thu, 25 Jun 2026 10:03:47 -0700 Subject: [PATCH] ICU-23429 MessageFormat: Implement currency formatting Implement currency formatting according to the spec for Unicode MessageFormat. --- icu4c/source/i18n/messageformat2_evaluation.h | 1 + .../source/i18n/messageformat2_formatter.cpp | 2 + .../i18n/messageformat2_function_registry.cpp | 552 +++++++++++++++--- ...essageformat2_function_registry_internal.h | 80 ++- .../intltest/messageformat2test_read_json.cpp | 2 + testdata/message2/currency-options.json | 314 ++++++++++ .../message2/spec/functions/currency.json | 56 ++ 7 files changed, 901 insertions(+), 106 deletions(-) create mode 100644 testdata/message2/currency-options.json create mode 100644 testdata/message2/spec/functions/currency.json diff --git a/icu4c/source/i18n/messageformat2_evaluation.h b/icu4c/source/i18n/messageformat2_evaluation.h index 6f10a234a139..1d037a02ca51 100644 --- a/icu4c/source/i18n/messageformat2_evaluation.h +++ b/icu4c/source/i18n/messageformat2_evaluation.h @@ -32,6 +32,7 @@ U_NAMESPACE_BEGIN namespace message2 { namespace functions { + static constexpr std::u16string_view CURRENCY = u"currency"; static constexpr std::u16string_view DATETIME = u"datetime"; static constexpr std::u16string_view DATE = u"date"; static constexpr std::u16string_view TIME = u"time"; diff --git a/icu4c/source/i18n/messageformat2_formatter.cpp b/icu4c/source/i18n/messageformat2_formatter.cpp index 0cce2b267a02..fa8375d02da9 100644 --- a/icu4c/source/i18n/messageformat2_formatter.cpp +++ b/icu4c/source/i18n/messageformat2_formatter.cpp @@ -161,6 +161,7 @@ namespace message2 { LocalPointer dateTime(StandardFunctions::DateTime::dateTime(success)); LocalPointer date(StandardFunctions::DateTime::date(success)); LocalPointer time(StandardFunctions::DateTime::time(success)); + LocalPointer currency(StandardFunctions::Number::currency(success)); LocalPointer number(StandardFunctions::Number::number(success)); LocalPointer integer(StandardFunctions::Number::integer(success)); LocalPointer string(StandardFunctions::String::string(success)); @@ -172,6 +173,7 @@ namespace message2 { dateTime.orphan(), success) .adoptFunction(FunctionName(functions::DATE), date.orphan(), success) .adoptFunction(FunctionName(functions::TIME), time.orphan(), success) + .adoptFunction(FunctionName(functions::CURRENCY), currency.orphan(), success) .adoptFunction(FunctionName(functions::NUMBER), number.orphan(), success) .adoptFunction(FunctionName(functions::INTEGER), diff --git a/icu4c/source/i18n/messageformat2_function_registry.cpp b/icu4c/source/i18n/messageformat2_function_registry.cpp index 3dcec364515e..b8eca2c99336 100644 --- a/icu4c/source/i18n/messageformat2_function_registry.cpp +++ b/icu4c/source/i18n/messageformat2_function_registry.cpp @@ -140,6 +140,7 @@ void MFFunctionRegistry::checkStandard() const { checkFunction(functions::DATETIME); checkFunction(functions::DATE); checkFunction(functions::TIME); + checkFunction(functions::CURRENCY); checkFunction(functions::NUMBER); checkFunction(functions::INTEGER); checkFunction(functions::STRING); @@ -380,12 +381,13 @@ bool isInteger(const Formattable& s) { } } -bool isDigitSizeOption(const UnicodeString& s) { - return s == UnicodeString("minimumIntegerDigits") - || s == UnicodeString("minimumFractionDigits") - || s == UnicodeString("maximumFractionDigits") - || s == UnicodeString("minimumSignificantDigits") - || s == UnicodeString("maximumSignificantDigits"); +bool isDigitSizeOption(std::u16string_view s) { + return s == options::FRACTION_DIGITS + || s == options::MINIMUM_INTEGER_DIGITS + || s == options::MINIMUM_FRACTION_DIGITS + || s == options::MAXIMUM_FRACTION_DIGITS + || s == options::MINIMUM_SIGNIFICANT_DIGITS + || s == options::MAXIMUM_SIGNIFICANT_DIGITS; } /* static */ void StandardFunctions::validateDigitSizeOptions(const FunctionOptions& opts, @@ -393,6 +395,13 @@ bool isDigitSizeOption(const UnicodeString& s) { CHECK_ERROR(status); for (int32_t i = 0; i < opts.optionsCount(); i++) { const ResolvedFunctionOption& opt = opts.options[i]; + if (opt.getName() == options::FRACTION_DIGITS) { + UErrorCode localStatus = U_ZERO_ERROR; + auto s = opt.getValue().unwrap().getString(localStatus); + if (U_SUCCESS(localStatus) && s == options::AUTO) { + continue; + } + } if (isDigitSizeOption(opt.getName()) && !isInteger(opt.getValue().unwrap())) { status = U_MF_BAD_OPTION; return; @@ -400,21 +409,137 @@ bool isDigitSizeOption(const UnicodeString& s) { } } +static double parseNumberLiteral(const UnicodeString& inputStr, UErrorCode& errorCode) { + if (U_FAILURE(errorCode)) { + return {}; + } + + // Validate string according to `number-literal` production + // in the spec for `:number`. This is because some cases are + // forbidden by this grammar, but allowed by StringToDouble. + if (!validateNumberLiteral(inputStr)) { + errorCode = U_MF_OPERAND_MISMATCH_ERROR; + return 0; + } + + // Convert to double using double_conversion::StringToDoubleConverter + using namespace double_conversion; + int processedCharactersCount = 0; + StringToDoubleConverter converter(0, 0, 0, "", ""); + int32_t len = inputStr.length(); + double result = + converter.StringToDouble(reinterpret_cast(inputStr.getBuffer()), + len, + &processedCharactersCount); + if (processedCharactersCount != len) { + errorCode = U_MF_OPERAND_MISMATCH_ERROR; + } + return result; +} + +static UNumberFormatRoundingMode validateRoundingMode(const UnicodeString& opt, UErrorCode& status) { + if (U_FAILURE(status)) { + return UNumberFormatRoundingMode::UNUM_ROUND_HALFUP; + } + if (opt.isEmpty()) { + return UNumberFormatRoundingMode::UNUM_ROUND_HALFUP; + } + if (opt == options::CEIL) { + return UNumberFormatRoundingMode::UNUM_ROUND_CEILING; + } + if (opt == options::FLOOR) { + return UNumberFormatRoundingMode::UNUM_ROUND_FLOOR; + } + if (opt == options::EXPAND) { + return UNumberFormatRoundingMode::UNUM_ROUND_UP; + } + if (opt == options::TRUNC) { + return UNumberFormatRoundingMode::UNUM_ROUND_DOWN; + } + if (opt == options::HALF_CEIL) { + return UNumberFormatRoundingMode::UNUM_ROUND_HALF_CEILING; + } + if (opt == options::HALF_FLOOR) { + return UNumberFormatRoundingMode::UNUM_ROUND_HALF_FLOOR; + } + if (opt == options::HALF_EXPAND) { + return UNumberFormatRoundingMode::UNUM_ROUND_HALFUP; + } + if (opt == options::HALF_TRUNC) { + return UNumberFormatRoundingMode::UNUM_ROUND_HALFDOWN; + } + if (opt == options::HALF_EVEN) { + return UNumberFormatRoundingMode::UNUM_ROUND_HALFEVEN; + } + status = U_MF_BAD_OPTION; + return UNumberFormatRoundingMode::UNUM_ROUND_HALFUP; +} + +static int validateRoundingIncrement(const UnicodeString& opt, UErrorCode& status) { + if (U_FAILURE(status)) { + return -1; + } + + if (opt.isEmpty()) { + return 1; + } + + double num = parseNumberLiteral(opt, status); + if (U_FAILURE(status)) { + return -1; + } + + int result = std::trunc(num); + + if (num != result) { + status = U_MF_BAD_OPTION; + return -1; + } + + switch (result) { + case 1: + case 2: + case 5: + case 10: + case 20: + case 25: + case 50: + case 100: + case 200: + case 250: + case 500: + case 1000: + case 2000: + case 2500: + case 5000: + return result; + default: { + status = U_MF_BAD_OPTION; + return -1; + } + } +} + +/* static */ StandardFunctions::Number* +StandardFunctions::Number::currency(UErrorCode& success) { + return create(NumberType::kCurrency, success); +} + /* static */ StandardFunctions::Number* StandardFunctions::Number::integer(UErrorCode& success) { - return create(true, success); + return create(NumberType::kInteger, success); } /* static */ StandardFunctions::Number* StandardFunctions::Number::number(UErrorCode& success) { - return create(false, success); + return create(NumberType::kNumber, success); } /* static */ StandardFunctions::Number* -StandardFunctions::Number::create(bool isInteger, UErrorCode& success) { +StandardFunctions::Number::create(StandardFunctions::NumberType numberType, UErrorCode& success) { NULL_ON_ERROR(success); - LocalPointer result(new Number(isInteger)); + LocalPointer result(new Number(numberType)); if (!result.isValid()) { success = U_MEMORY_ALLOCATION_ERROR; return nullptr; @@ -437,6 +562,82 @@ LocalPointer StandardFunctions::Number::call(const FunctionContex return val; } +void StandardFunctions::requireNoRoundingIncrement(const FunctionOptions& opts, UErrorCode& status) { + if (U_FAILURE(status)) { + return; + } + UnicodeString opt = opts.getStringFunctionOption(options::ROUNDING_INCREMENT); + if (!opt.isEmpty()) { + status = U_MF_BAD_OPTION; + } +} + +static number::Precision significantDigits(int32_t min, int32_t max) { + using namespace number; + + Precision p = Precision::unlimited(); + U_ASSERT(min != -1 || max != -1); + if (min != -1 && max != -1) { + p = Precision::minMaxSignificantDigits(min, max); + } else if (min != -1) { + p = Precision::minSignificantDigits(min); + } else { + p = Precision::maxSignificantDigits(max); + } + return p; +} + +static number::Precision significantDigitsWithRoundingPriority(int32_t min, int32_t max, const number::FractionPrecision& p, UNumberRoundingPriority priority) { + using namespace number; + + U_ASSERT(priority == UNumberRoundingPriority::UNUM_ROUNDING_PRIORITY_RELAXED || priority == UNumberRoundingPriority::UNUM_ROUNDING_PRIORITY_STRICT); + Precision result = Precision::unlimited(); + U_ASSERT(min != -1 || max != -1); + if (min != -1 && max != -1) { + result = p.withSignificantDigits(min, max, priority); + } else if (min != -1) { + // Only the maximum precision value is used for the calculation + } else { + result = p.withSignificantDigits(1, max, priority); + } + return result; +} + +number::Precision StandardFunctions::withRoundingIncrement(const FunctionOptions& opts, bool& hasIncrement, const DigitSizeOption& fractionDigits, const UChar* currency, UErrorCode& status) { + using namespace number; + + UErrorCode localStatus = U_ZERO_ERROR; + UnicodeString roundingIncrement = opts.getStringFunctionOption(options::ROUNDING_INCREMENT, localStatus); + int increment = 1; + if (U_SUCCESS(localStatus)) { + increment = validateRoundingIncrement(roundingIncrement, status); + if (U_FAILURE(status)) { + status = U_MF_BAD_OPTION; + return CurrencyPrecision::currency(UCURR_USAGE_STANDARD); + } + } + if (increment != 1) { + hasIncrement = true; + } + // Divide by 100 to get cents-based rounding + // The default for currency formatting is the number of minor + // unit digits provided by the ISO 4217 currency code list + // (2 if the list doesn't provide that information) + int32_t minFractionToUse = 2; + localStatus = U_ZERO_ERROR; + if (fractionDigits.isAuto()) { + minFractionToUse = ucurr_getDefaultFractionDigits(currency, &localStatus); + if (U_FAILURE(localStatus)) { + localStatus = U_ZERO_ERROR; + minFractionToUse = 2; + } + } else { + minFractionToUse = fractionDigits.value(); + } + double incrementToUse = U_SUCCESS(localStatus) ? increment / 100.0 : 0.01; + return Precision::increment(incrementToUse).withMinFraction(minFractionToUse); +} + /* static */ number::LocalizedNumberFormatter StandardFunctions::formatterForOptions(const Number& number, const Locale& locale, const FunctionOptions& opts, @@ -445,6 +646,25 @@ LocalPointer StandardFunctions::Number::call(const FunctionContex using namespace number; + UnicodeString currency; + CurrencyUnit currencyUnit; + if (number.numberType == NumberType::kCurrency) { + currency = opts.getStringFunctionOption(options::CURRENCY); + if (currency.isEmpty()) { + status = U_MF_OPERAND_MISMATCH_ERROR; + return {}; + } + // Validate currency code + currencyUnit = CurrencyUnit(currency.getBuffer(), status); + if (U_FAILURE(status)) { + // Either the operand is numeric (in which case the spec says + // "A numeric operand without a currency option results in a Bad Operand error") + // or it's non-numeric, in which case the error will be U_MF_OPERAND_MISMATCH_ERROR anyway. + status = U_MF_OPERAND_MISMATCH_ERROR; + return {}; + } + } + validateDigitSizeOptions(opts, status); if (U_FAILURE(status)) { return {}; @@ -453,14 +673,13 @@ LocalPointer StandardFunctions::Number::call(const FunctionContex if (U_SUCCESS(status)) { Formattable opt; nf = NumberFormatter::with(); - bool isInteger = number.isInteger; - if (isInteger) { + if (number.numberType == NumberType::kInteger) { nf = nf.precision(Precision::integer()); } // Notation options - if (!isInteger) { + if (number.numberType == NumberType::kNumber) { // These options only apply to `:number` // Default notation is simple @@ -482,43 +701,119 @@ LocalPointer StandardFunctions::Number::call(const FunctionContex // Already set to default } nf = nf.notation(notation); - } - // Style options -- specific to `:number` - if (!isInteger) { if (number.usePercent(opts)) { nf = nf.unit(NoUnit::percent()).scale(Scale::powerOfTen(2)); } } int32_t maxSignificantDigits = number.maximumSignificantDigits(opts); - if (!isInteger) { + if (number.numberType != NumberType::kInteger) { int32_t minFractionDigits = number.minimumFractionDigits(opts); int32_t maxFractionDigits = number.maximumFractionDigits(opts); int32_t minSignificantDigits = number.minimumSignificantDigits(opts); + FractionPrecision fractionPrecision; Precision p = Precision::unlimited(); bool precisionOptions = false; + bool noSignificantDigits = minSignificantDigits == -1 && maxSignificantDigits == -1; // Returning -1 means the option wasn't provided - if (maxFractionDigits != -1 && minFractionDigits != -1) { - precisionOptions = true; - p = Precision::minMaxFraction(minFractionDigits, maxFractionDigits); - } else if (minFractionDigits != -1) { - precisionOptions = true; - p = Precision::minFraction(minFractionDigits); - } else if (maxFractionDigits != -1) { - precisionOptions = true; - p = Precision::maxFraction(maxFractionDigits); + if (number.numberType == NumberType::kNumber) { + if (maxFractionDigits != -1 && minFractionDigits != -1) { + precisionOptions = true; + fractionPrecision = Precision::minMaxFraction(minFractionDigits, maxFractionDigits); + } else if (minFractionDigits != -1) { + precisionOptions = true; + fractionPrecision = Precision::minFraction(minFractionDigits); + } else if (maxFractionDigits != -1) { + precisionOptions = true; + fractionPrecision = Precision::maxFraction(maxFractionDigits); + } } - if (minSignificantDigits != -1) { - precisionOptions = true; - p = p.minSignificantDigits(minSignificantDigits); + if (number.numberType == NumberType::kCurrency) { + DigitSizeOption fractionDigits = number.fractionDigits(opts); + bool noFractionDigits = fractionDigits.isInvalid(); + + if (!noSignificantDigits) { + // Can't specify rounding increment if significant digits are specified + requireNoRoundingIncrement(opts, status); + if (U_FAILURE(status)) { + status = U_MF_BAD_OPTION; + return {}; + } + } + + // No fraction digits and no significant digits + if (noFractionDigits && noSignificantDigits) { + p = withRoundingIncrement(opts, precisionOptions, DigitSizeOption::autoVal(), currency.getBuffer(), status); + if (U_FAILURE(status)) { + status = U_MF_BAD_OPTION; + return {}; + } + } + // No fraction digits and significant digits + else if (noFractionDigits && !noSignificantDigits) { + precisionOptions = true; + p = significantDigits(minSignificantDigits, maxSignificantDigits); + // Can't specify rounding increment if significant digits is specified + requireNoRoundingIncrement(opts, status); + // Fraction digits and significant digits + } else if (!noFractionDigits && !noSignificantDigits) { + // Both fractionDigits and significant digits are specified -- + // use roundingPriority to resolve conflict + + precisionOptions = true; + if (fractionDigits.isAuto()) { + p = significantDigits(minSignificantDigits, maxSignificantDigits); + } else { + fractionPrecision = FractionPrecision::fixedFraction(fractionDigits.value()); + + UnicodeString roundingPriority = opts.getStringFunctionOption(options::ROUNDING_PRIORITY); + if (roundingPriority == options::MORE_PRECISION) { + precisionOptions = true; + p = significantDigitsWithRoundingPriority(minSignificantDigits, maxSignificantDigits, fractionPrecision, UNumberRoundingPriority::UNUM_ROUNDING_PRIORITY_RELAXED); + } else if (roundingPriority == options::LESS_PRECISION) { + precisionOptions = true; + p = significantDigitsWithRoundingPriority(minSignificantDigits, maxSignificantDigits, fractionPrecision, UNumberRoundingPriority::UNUM_ROUNDING_PRIORITY_STRICT); + } else { + p = significantDigits(minSignificantDigits, maxSignificantDigits); + } + } + } else { + precisionOptions = true; + + // Fraction digits and no significant digits + p = withRoundingIncrement(opts, precisionOptions, fractionDigits, currency.getBuffer(), status); + if (U_FAILURE(status)) { + status = U_MF_BAD_OPTION; + return {}; } + + } + } else { + p = fractionPrecision; } - if (maxSignificantDigits != -1) { - precisionOptions = true; - p = p.maxSignificantDigits(maxSignificantDigits); + + if (number.numberType == NumberType::kCurrency) { + UnicodeString trailingZeroDisplay = opts.getStringFunctionOption(options::TRAILING_ZERO_DISPLAY); + if (trailingZeroDisplay == options::STRIP_IF_INTEGER) { + precisionOptions = true; + p = p.trailingZeroDisplay(UNumberTrailingZeroDisplay::UNUM_TRAILING_ZERO_HIDE_IF_WHOLE); + } else { + p = p.trailingZeroDisplay(UNumberTrailingZeroDisplay::UNUM_TRAILING_ZERO_AUTO); + } + + } else { + if (minSignificantDigits != -1) { + precisionOptions = true; + p = p.minSignificantDigits(minSignificantDigits); + } + if (maxSignificantDigits != -1) { + precisionOptions = true; + p = p.maxSignificantDigits(maxSignificantDigits); + } } + if (precisionOptions) { nf = nf.precision(p); } @@ -586,35 +881,39 @@ LocalPointer StandardFunctions::Number::call(const FunctionContex } } } - return nf.locale(locale); -} -static double parseNumberLiteral(const UnicodeString& inputStr, UErrorCode& errorCode) { - if (U_FAILURE(errorCode)) { - return {}; - } + // Currency options + if (number.numberType == NumberType::kCurrency) { + nf = nf.unit(currencyUnit); - // Validate string according to `number-literal` production - // in the spec for `:number`. This is because some cases are - // forbidden by this grammar, but allowed by StringToDouble. - if (!validateNumberLiteral(inputStr)) { - errorCode = U_MF_OPERAND_MISMATCH_ERROR; - return 0; - } + UnicodeString currencySign = opts.getStringFunctionOption(options::CURRENCY_SIGN); + if (currencySign == options::ACCOUNTING) { + nf = nf.sign(UNumberSignDisplay::UNUM_SIGN_ACCOUNTING); + } - // Convert to double using double_conversion::StringToDoubleConverter - using namespace double_conversion; - int processedCharactersCount = 0; - StringToDoubleConverter converter(0, 0, 0, "", ""); - int32_t len = inputStr.length(); - double result = - converter.StringToDouble(reinterpret_cast(inputStr.getBuffer()), - len, - &processedCharactersCount); - if (processedCharactersCount != len) { - errorCode = U_MF_OPERAND_MISMATCH_ERROR; + UnicodeString currencyDisplay = opts.getStringFunctionOption(options::CURRENCY_DISPLAY); + if (currencyDisplay == options::NARROW_SYMBOL) { + nf = nf.unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_NARROW); + } else if (currencyDisplay == options::NAME) { + nf = nf.unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FULL_NAME); + } else if (currencyDisplay == options::CODE) { + nf = nf.unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_ISO_CODE); + } else if (currencyDisplay == options::NEVER) { + nf = nf.unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_HIDDEN); + } else { + nf = nf.unitWidth(UNumberUnitWidth::UNUM_UNIT_WIDTH_FORMAL); + } + + UnicodeString roundingMode = opts.getStringFunctionOption(options::ROUNDING_MODE); + UNumberFormatRoundingMode mode = validateRoundingMode(roundingMode, status); + if (U_FAILURE(status)) { + status = U_MF_BAD_OPTION; + return {}; + } + nf = nf.roundingMode(mode); } - return result; + + return nf.locale(locale); } static UChar32 digitToChar(int32_t val, UErrorCode errorCode) { @@ -652,68 +951,107 @@ static UChar32 digitToChar(int32_t val, UErrorCode errorCode) { return '0'; } -int32_t StandardFunctions::Number::digitSizeOption(const FunctionOptions& opts, - const UnicodeString& k) const { +StandardFunctions::DigitSizeOption StandardFunctions::Number::digitSizeOptionWithAuto(const FunctionOptions& opts, + std::u16string_view k) const { + return digitSizeOption(opts, k, true); +} + +int32_t StandardFunctions::Number::digitSizeOptionNoAuto(const FunctionOptions& opts, + std::u16string_view k) const { + StandardFunctions::DigitSizeOption result = digitSizeOption(opts, k, false); + U_ASSERT(!result.isAuto()); + if (result.isInvalid()) { + return -1; + } + return result.value(); +} + +StandardFunctions::DigitSizeOption StandardFunctions::Number::digitSizeOption(const FunctionOptions& opts, + const std::u16string_view k, + bool allowAuto) const { UErrorCode localStatus = U_ZERO_ERROR; - const FunctionValue* opt = opts.getFunctionOption(k, - localStatus); + const FunctionValue* opt = opts.getFunctionOption(k, localStatus); if (U_SUCCESS(localStatus)) { // First try the formatted value UnicodeString formatted = opt->formatToString(localStatus); int64_t val = 0; if (U_SUCCESS(localStatus)) { + if (formatted == options::AUTO && allowAuto) { + return DigitSizeOption::autoVal(); + } val = getInt64Value(Locale("en-US"), Formattable(formatted), localStatus); + if (U_SUCCESS(localStatus)) { + return DigitSizeOption::intVal(static_cast(val)); + } } if (U_FAILURE(localStatus)) { localStatus = U_ZERO_ERROR; - } - // Next try the operand - val = getInt64Value(Locale("en-US"), opt->unwrap(), localStatus); - if (U_SUCCESS(localStatus)) { - return static_cast(val); + + // Next try the operand + auto unwrapped = opt->unwrap(); + UnicodeString s = unwrapped.getString(localStatus); + if (U_SUCCESS(localStatus)) { + if (s == options::AUTO && allowAuto) { + return DigitSizeOption::autoVal(); + } + } else { + localStatus = U_ZERO_ERROR; + } + val = getInt64Value(Locale("en-US"), opt->unwrap(), localStatus); + if (U_SUCCESS(localStatus)) { + return DigitSizeOption::intVal(static_cast(val)); + } } } // Returning -1 indicates that the option wasn't provided or was a non-integer. // The caller needs to check for that case, since passing -1 to Precision::maxFraction() // is an error. - return -1; + return DigitSizeOption::invalid(); +} + +StandardFunctions::DigitSizeOption StandardFunctions::Number::fractionDigits(const FunctionOptions& opts) const { + if (numberType != NumberType::kCurrency) { + return DigitSizeOption::invalid(); + } + + return digitSizeOptionWithAuto(opts, options::FRACTION_DIGITS); } int32_t StandardFunctions::Number::maximumFractionDigits(const FunctionOptions& opts) const { - if (isInteger) { + if (numberType == NumberType::kInteger) { return 0; } - return digitSizeOption(opts, UnicodeString("maximumFractionDigits")); + return digitSizeOptionNoAuto(opts, options::MAXIMUM_FRACTION_DIGITS); } int32_t StandardFunctions::Number::minimumFractionDigits(const FunctionOptions& opts) const { Formattable opt; - if (isInteger) { + if (numberType == NumberType::kInteger) { return -1; } - return digitSizeOption(opts, UnicodeString("minimumFractionDigits")); + return digitSizeOptionNoAuto(opts, options::MINIMUM_FRACTION_DIGITS); } int32_t StandardFunctions::Number::minimumIntegerDigits(const FunctionOptions& opts) const { - return digitSizeOption(opts, UnicodeString("minimumIntegerDigits")); + return digitSizeOptionNoAuto(opts, options::MINIMUM_INTEGER_DIGITS); } int32_t StandardFunctions::Number::minimumSignificantDigits(const FunctionOptions& opts) const { - if (isInteger) { + if (numberType == NumberType::kInteger) { return -1; } - return digitSizeOption(opts, UnicodeString("minimumSignificantDigits")); + return digitSizeOptionNoAuto(opts, options::MINIMUM_SIGNIFICANT_DIGITS); } int32_t StandardFunctions::Number::maximumSignificantDigits(const FunctionOptions& opts) const { - return digitSizeOption(opts, UnicodeString("maximumSignificantDigits")); + return digitSizeOptionNoAuto(opts, options::MAXIMUM_SIGNIFICANT_DIGITS); } bool StandardFunctions::Number::usePercent(const FunctionOptions& opts) const { const UnicodeString& style = opts.getStringFunctionOption(UnicodeString("style")); - if (isInteger || style.length() == 0) { + if ((numberType == NumberType::kInteger) || style.length() == 0) { return false; } return (style == UnicodeString("percent")); @@ -734,36 +1072,56 @@ StandardFunctions::NumberValue::NumberValue(const Number& parent, locale = context.getLocale(); opts = options.mergeOptions(arg.getResolvedOptions(), errorCode); innerValue = arg.unwrap(); - functionName = UnicodeString(parent.isInteger ? "integer" : "number"); + functionName = functions::NUMBER; + numberType = parent.numberType; + + switch (numberType) { + case NumberType::kInteger: + functionName = functions::INTEGER; + break; + case NumberType::kCurrency: + functionName = functions::CURRENCY; + break; + default: + break; + } inputDir = context.getDirection(); dir = outputDirectionalityFromUDir(inputDir, locale); number::LocalizedNumberFormatter realFormatter; - realFormatter = formatterForOptions(parent, locale, opts, errorCode); + UErrorCode localStatus = U_ZERO_ERROR; + realFormatter = formatterForOptions(parent, locale, opts, localStatus); int64_t integerValue = 0; + // Need to validate operand before validating options if (U_SUCCESS(errorCode)) { switch (innerValue.getType()) { case UFMT_DOUBLE: { double d = innerValue.getDouble(errorCode); U_ASSERT(U_SUCCESS(errorCode)); - formattedNumber = realFormatter.formatDouble(d, errorCode); - integerValue = static_cast(std::round(d)); + if (U_SUCCESS(localStatus)) { + formattedNumber = realFormatter.formatDouble(d, errorCode); + integerValue = static_cast(std::round(d)); + } break; } case UFMT_LONG: { int32_t l = innerValue.getLong(errorCode); U_ASSERT(U_SUCCESS(errorCode)); - formattedNumber = realFormatter.formatInt(l, errorCode); - integerValue = l; + if (U_SUCCESS(localStatus)) { + formattedNumber = realFormatter.formatInt(l, errorCode); + integerValue = l; + } break; } case UFMT_INT64: { int64_t i = innerValue.getInt64(errorCode); U_ASSERT(U_SUCCESS(errorCode)); - formattedNumber = realFormatter.formatInt(i, errorCode); - integerValue = i; + if (U_SUCCESS(localStatus)) { + formattedNumber = realFormatter.formatInt(i, errorCode); + integerValue = i; + } break; } case UFMT_STRING: { @@ -773,8 +1131,10 @@ StandardFunctions::NumberValue::NumberValue(const Number& parent, double d = parseNumberLiteral(s, errorCode); if (U_FAILURE(errorCode)) return; - formattedNumber = realFormatter.formatDouble(d, errorCode); - integerValue = static_cast(std::round(d)); + if (U_SUCCESS(localStatus)) { + formattedNumber = realFormatter.formatDouble(d, errorCode); + integerValue = static_cast(std::round(d)); + } break; } default: { @@ -785,13 +1145,17 @@ StandardFunctions::NumberValue::NumberValue(const Number& parent, } } + if (U_SUCCESS(errorCode) && U_FAILURE(localStatus)) { + errorCode = localStatus; + } + // Ignore U_USING_DEFAULT_WARNING if (errorCode == U_USING_DEFAULT_WARNING) { errorCode = U_ZERO_ERROR; } // Need to set the integer value if invoked as :integer - if (parent.isInteger) { + if (numberType == NumberType::kInteger) { innerValue = Formattable(integerValue); } } @@ -1585,9 +1949,9 @@ double formattableToNumber(const Formattable& arg, UErrorCode& status) { } static bool isTestFunction(const UnicodeString& s) { - return (s == u"test:format" - || s == u"test:select" - || s == u"test:function"); + return (s == functions::TEST_FORMAT + || s == functions::TEST_SELECT + || s == functions::TEST_FUNCTION); } static void setFailsFromFunctionValue(const FunctionValue& optionValue, @@ -1597,24 +1961,24 @@ static void setFailsFromFunctionValue(const FunctionValue& optionValue, UnicodeString failsString = optionValue.unwrap().getString(status); if (U_SUCCESS(status)) { // 9i. If its value resolves to the string 'always', then - if (failsString == u"always") { + if (failsString == options::ALWAYS) { // 9ia. Set FailsFormat to be true failsFormat = true; // 9ib. Set FailsSelect to be true. failsSelect = true; } // 9ii. Else if its value resolves to the string "format", then - else if (failsString == u"format") { + else if (failsString == options::FORMAT) { // 9ia. Set FailsFormat to be true failsFormat = true; } // 9iii. Else if its value resolves to the string "select", then - else if (failsString == u"select") { + else if (failsString == options::SELECT) { // 9iiia. Set FailsSelect to be true. failsSelect = true; } // 9iv. Else if its value does not resolve to the string "never", then - else if (failsString != u"never") { + else if (failsString != options::NEVER) { // 9iv(a). Emit "bad-option" Resolution Error. status = U_MF_BAD_OPTION; } diff --git a/icu4c/source/i18n/messageformat2_function_registry_internal.h b/icu4c/source/i18n/messageformat2_function_registry_internal.h index a9f7b3a2c39f..79821e6d3b0f 100644 --- a/icu4c/source/i18n/messageformat2_function_registry_internal.h +++ b/icu4c/source/i18n/messageformat2_function_registry_internal.h @@ -2,6 +2,7 @@ // License & terms of use: http://www.unicode.org/copyright.html #include "unicode/utypes.h" +#include "uvector.h" // U_ASSERT #ifndef U_HIDE_DEPRECATED_API @@ -25,10 +26,16 @@ namespace message2 { // Constants for option names namespace options { +static constexpr std::u16string_view ACCOUNTING = u"accounting"; static constexpr std::u16string_view ALWAYS = u"always"; static constexpr std::u16string_view AUTO = u"auto"; +static constexpr std::u16string_view CEIL = u"ceil"; +static constexpr std::u16string_view CODE = u"code"; static constexpr std::u16string_view COMPACT = u"compact"; static constexpr std::u16string_view COMPACT_DISPLAY = u"compactDisplay"; +static constexpr std::u16string_view CURRENCY = u"currency"; +static constexpr std::u16string_view CURRENCY_DISPLAY = u"currencyDisplay"; +static constexpr std::u16string_view CURRENCY_SIGN = u"currencySign"; static constexpr std::u16string_view DATE_STYLE = u"dateStyle"; static constexpr std::u16string_view DAY = u"day"; static constexpr std::u16string_view DECIMAL_PLACES = u"decimalPlaces"; @@ -36,10 +43,20 @@ static constexpr std::u16string_view DEFAULT_UPPER = u"DEFAULT"; static constexpr std::u16string_view ENGINEERING = u"engineering"; static constexpr std::u16string_view EXACT = u"exact"; static constexpr std::u16string_view EXCEPT_ZERO = u"exceptZero"; +static constexpr std::u16string_view EXPAND = u"expand"; static constexpr std::u16string_view FAILS = u"fails"; +static constexpr std::u16string_view FLOOR = u"floor"; +static constexpr std::u16string_view FORMAT = u"format"; +static constexpr std::u16string_view FRACTION_DIGITS = u"fractionDigits"; static constexpr std::u16string_view FULL_UPPER = u"FULL"; +static constexpr std::u16string_view HALF_CEIL = u"halfCeil"; +static constexpr std::u16string_view HALF_EVEN = u"halfEven"; +static constexpr std::u16string_view HALF_EXPAND = u"halfExpand"; +static constexpr std::u16string_view HALF_FLOOR = u"halfFloor"; +static constexpr std::u16string_view HALF_TRUNC = u"halfTrunc"; static constexpr std::u16string_view HOUR = u"hour"; static constexpr std::u16string_view INHERIT = u"inherit"; +static constexpr std::u16string_view LESS_PRECISION = u"lessPrecision"; static constexpr std::u16string_view LONG = u"long"; static constexpr std::u16string_view LONG_UPPER = u"LONG"; static constexpr std::u16string_view LTR = u"ltr"; @@ -52,7 +69,10 @@ static constexpr std::u16string_view MINIMUM_INTEGER_DIGITS = u"minimumIntegerDi static constexpr std::u16string_view MINIMUM_SIGNIFICANT_DIGITS = u"minimumSignificantDigits"; static constexpr std::u16string_view MINUTE = u"minute"; static constexpr std::u16string_view MONTH = u"month"; +static constexpr std::u16string_view MORE_PRECISION = u"morePrecision"; +static constexpr std::u16string_view NAME = u"name"; static constexpr std::u16string_view NARROW = u"narrow"; +static constexpr std::u16string_view NARROW_SYMBOL = u"narrowSymbol"; static constexpr std::u16string_view NEGATIVE = u"negative"; static constexpr std::u16string_view NEVER = u"never"; static constexpr std::u16string_view NOTATION = u"notation"; @@ -60,6 +80,9 @@ static constexpr std::u16string_view NUMBERING_SYSTEM = u"numberingSystem"; static constexpr std::u16string_view NUMERIC = u"numeric"; static constexpr std::u16string_view ORDINAL = u"ordinal"; static constexpr std::u16string_view PERCENT_STRING = u"percent"; +static constexpr std::u16string_view ROUNDING_INCREMENT = u"roundingIncrement"; +static constexpr std::u16string_view ROUNDING_MODE = u"roundingMode"; +static constexpr std::u16string_view ROUNDING_PRIORITY = u"roundingPriority"; static constexpr std::u16string_view RTL = u"rtl"; static constexpr std::u16string_view SCIENTIFIC = u"scientific"; static constexpr std::u16string_view SECOND = u"second"; @@ -67,8 +90,11 @@ static constexpr std::u16string_view SELECT = u"select"; static constexpr std::u16string_view SHORT = u"short"; static constexpr std::u16string_view SHORT_UPPER = u"SHORT"; static constexpr std::u16string_view SIGN_DISPLAY = u"signDisplay"; +static constexpr std::u16string_view STRIP_IF_INTEGER = u"stripIfInteger"; static constexpr std::u16string_view STYLE = u"style"; static constexpr std::u16string_view TIME_STYLE = u"timeStyle"; +static constexpr std::u16string_view TRAILING_ZERO_DISPLAY = u"trailingZeroDisplay"; +static constexpr std::u16string_view TRUNC = u"trunc"; static constexpr std::u16string_view TWO_DIGIT = u"2-digit"; static constexpr std::u16string_view U_DIR = u"u:dir"; static constexpr std::u16string_view U_ID = u"u:id"; @@ -78,20 +104,43 @@ static constexpr std::u16string_view YEAR = u"year"; } // namespace options // Built-in functions - /* - The standard functions are :datetime, :date, :time, - :number, :integer, and :string, - per https://github.com/unicode-org/message-format-wg/blob/main/spec/registry.md - as of https://github.com/unicode-org/message-format-wg/releases/tag/LDML45-alpha - */ + // See https://github.com/unicode-org/message-format-wg/blob/main/spec/functions/README.md class StandardFunctions { friend class MessageFormatter; public: + + typedef enum NumberType { + kCurrency, + kInteger, + kNumber + } NumberType; + + class DigitSizeOption { + public: + bool isAuto() const { return isAutoVal && !isInvalidVal; } + bool isInvalid() const { return isInvalidVal; } + int32_t value() const { + U_ASSERT(!isAutoVal && !isInvalidVal); + return val; + } + static DigitSizeOption autoVal() { return DigitSizeOption(false); } + static DigitSizeOption intVal(int32_t i) { return DigitSizeOption(i); } + static DigitSizeOption invalid() { return DigitSizeOption(true); } + private: + explicit DigitSizeOption(bool invalid) : isAutoVal(true), isInvalidVal(invalid) {} + explicit DigitSizeOption(int32_t i) : isAutoVal(false), isInvalidVal(false), val(i) {} + const bool isAutoVal = false; + const bool isInvalidVal = false; + const int32_t val = 0; + }; + // Used for normalizing variable names and keys for comparison static UnicodeString normalizeNFC(const UnicodeString&); private: + static void requireNoRoundingIncrement(const FunctionOptions&, UErrorCode&); + static number::Precision withRoundingIncrement(const FunctionOptions&, bool&, const DigitSizeOption&, const UChar*, UErrorCode&); static void validateDigitSizeOptions(const FunctionOptions&, UErrorCode&); static void checkSelectOption(const FunctionOptions&, UErrorCode&); static UnicodeString getStringOption(const FunctionOptions& opts, @@ -138,8 +187,9 @@ static constexpr std::u16string_view YEAR = u"year"; class Number : public Function { public: + static Number* currency(UErrorCode& success); static Number* integer(UErrorCode& success); - static Number* number( UErrorCode& success); + static Number* number(UErrorCode& success); LocalPointer call(const FunctionContext& context, const FunctionValue& operand, @@ -157,11 +207,16 @@ static constexpr std::u16string_view YEAR = u"year"; PLURAL_EXACT } PluralType; - static Number* create(bool, UErrorCode&); - Number(bool isInt) : isInteger(isInt) /*, icuFormatter(number::NumberFormatter::withLocale(loc))*/ {} + static Number* create(NumberType, UErrorCode&); + Number(NumberType t) : numberType(t) {} // These options have their own accessor methods, since they have different default values. - int32_t digitSizeOption(const FunctionOptions&, const UnicodeString&) const; + DigitSizeOption digitSizeOption(const FunctionOptions&, std::u16string_view, bool) const; + DigitSizeOption digitSizeOptionWithAuto(const FunctionOptions&, + std::u16string_view) const; + int32_t digitSizeOptionNoAuto(const FunctionOptions&, + std::u16string_view) const; + DigitSizeOption fractionDigits(const FunctionOptions& options) const; int32_t maximumFractionDigits(const FunctionOptions& options) const; int32_t minimumFractionDigits(const FunctionOptions& options) const; int32_t minimumSignificantDigits(const FunctionOptions& options) const; @@ -169,7 +224,7 @@ static constexpr std::u16string_view YEAR = u"year"; int32_t minimumIntegerDigits(const FunctionOptions& options) const; bool usePercent(const FunctionOptions& options) const; - const bool isInteger = false; + const NumberType numberType = NumberType::kNumber; const number::LocalizedNumberFormatter icuFormatter; static PluralType pluralType(const FunctionOptions& opts); @@ -189,13 +244,14 @@ static constexpr std::u16string_view YEAR = u"year"; int32_t* prefs, int32_t& prefsLen, UErrorCode& status) const override; - UBool isSelectable() const override { return true; } + UBool isSelectable() const override { return numberType != NumberType::kCurrency; } NumberValue(); const UnicodeString& getFunctionName() const override { return functionName; } virtual ~NumberValue(); private: friend class Number; + NumberType numberType = NumberType::kNumber; number::FormattedNumber formattedNumber; NumberValue(const Number&, const FunctionContext&, diff --git a/icu4c/source/test/intltest/messageformat2test_read_json.cpp b/icu4c/source/test/intltest/messageformat2test_read_json.cpp index 4c3b1a24a01f..149d05c0406c 100644 --- a/icu4c/source/test/intltest/messageformat2test_read_json.cpp +++ b/icu4c/source/test/intltest/messageformat2test_read_json.cpp @@ -329,6 +329,7 @@ void TestMessageFormat2::jsonTestsFromFiles(IcuTestErrorCode& errorCode) { runTestsFromJsonFile(*this, "spec/pattern-selection.json", errorCode); // Do valid function tests + runTestsFromJsonFile(*this, "spec/functions/currency.json", errorCode); runTestsFromJsonFile(*this, "spec/functions/date.json", errorCode); runTestsFromJsonFile(*this, "spec/functions/datetime.json", errorCode); runTestsFromJsonFile(*this, "spec/functions/integer.json", errorCode); @@ -337,6 +338,7 @@ void TestMessageFormat2::jsonTestsFromFiles(IcuTestErrorCode& errorCode) { runTestsFromJsonFile(*this, "spec/functions/time.json", errorCode); // Other tests (non-spec) + runTestsFromJsonFile(*this, "currency-options.json", errorCode); runTestsFromJsonFile(*this, "more-functions.json", errorCode); runTestsFromJsonFile(*this, "valid-tests.json", errorCode); runTestsFromJsonFile(*this, "resolution-errors.json", errorCode); diff --git a/testdata/message2/currency-options.json b/testdata/message2/currency-options.json new file mode 100644 index 000000000000..ef972d413bc8 --- /dev/null +++ b/testdata/message2/currency-options.json @@ -0,0 +1,314 @@ +{ + "$schema": "../../schemas/v0/tests.schema.json", + "scenario": "Currency function", + "description": "The built-in formatter for currencies.", + "defaultTestProperties": { + "tags": [":currency"], + "bidiIsolation": "none", + "locale": "en-US", + "expErrors": [] + }, + "tests": [ + { + "src": "{42 :currency currency=EUR}", + "exp": "€42.00" + + }, + { + "src": ".local $n = {42 :number} {{{$n :currency currency=EUR}}}", + "exp": "€42.00" + }, + { + "src": ".local $n = {42 :integer} {{{$n :currency currency=EUR}}}", + "exp": "€42.00" + }, + { + "src": ".local $n = {42 :currency currency=EUR} {{{$n :currency}}}", + "exp": "€42.00" + }, + { + "src": "{42 :currency currency=EUR fractionDigits=0}", + "exp": "€42" + }, + { + "src": "{42 :currency currency=EUR fractionDigits=auto}", + "exp": "€42.00" + }, + { + "src": "{42 :currency currency=EUR fractionDigits=2}", + "exp": "€42.00" + }, + { + "src": "{42.17 :currency currency=EUR fractionDigits=2 roundingIncrement=5}", + "exp": "€42.15" + }, + { + "src": "{42 :currency currency=EUR currencyDisplay=narrowSymbol}", + "exp": "€42.00" + }, + { + "src": "{42 :currency currency=EUR currencyDisplay=symbol}", + "exp": "€42.00" + }, + { + "src": "{42 :currency currency=EUR currencyDisplay=name}", + "exp": "42.00 euros" + }, + { + "src": "{42 :currency currency=EUR currencyDisplay=code}", + "exp": "EUR 42.00" + }, + { + "src": "{42 :currency currency=EUR currencyDisplay=never}", + "exp": "42.00" + }, + + { + "src": "{$x :currency currency=EUR}", + "params": [{ "name": "x", "value": 41 }], + "exp": "€41.00" + }, + { + "src": ".local $n = {-42 :currency currency=USD trailingZeroDisplay=stripIfInteger} {{{$n :currency currencySign=accounting}}}", + "exp": "($42)" + }, + { + "src": "{420000 :currency currency=EUR useGrouping=auto}", + "exp": "€420,000.00" + }, + { + "locale": "es", + "src": "{4201 :currency currency=EUR useGrouping=always}", + "exp": "4.201,00 €" + }, + { + "src": "{4201 :currency currency=EUR useGrouping=never}", + "exp": "€4201.00" + }, + { + "locale": "es", + "src": "{4201 :currency currency=EUR useGrouping=min2}", + "exp": "4201,00 €" + }, + { + "locale": "es", + "src": "{420100 :currency currency=EUR useGrouping=min2}", + "exp": "420.100,00 €" + }, + { + "src": "{42 :currency currency=EUR minimumIntegerDigits=3}", + "exp": "€042.00" + }, + { + "src": "{42.75 :currency currency=EUR minimumSignificantDigits=5}", + "exp": "€42.750" + }, + { + "src": "{42.75 :currency currency=EUR maximumSignificantDigits=3}", + "exp": "€42.8" + }, + { + "src": "{42.75 :currency currency=EUR fractionDigits=5 minimumSignificantDigits=2 roundingPriority=auto}", + "exp": "€42.75" + }, + { + "src": "{42.75 :currency currency=EUR fractionDigits=5 maximumSignificantDigits=2 roundingPriority=auto}", + "exp": "€43" + }, + { + "src": "{42.75 :currency currency=EUR fractionDigits=5 minimumSignificantDigits=2 roundingPriority=morePrecision}", + "exp": "€42.75" + }, + { + "src": "{42.75 :currency currency=EUR fractionDigits=5 maximumSignificantDigits=2 roundingPriority=morePrecision}", + "exp": "€42.75000" + }, + { + "src": "{42.75 :currency currency=EUR fractionDigits=5 minimumSignificantDigits=2 roundingPriority=lessPrecision}", + "exp": "€42.75" + }, + { + "src": "{42.75 :currency currency=EUR fractionDigits=5 maximumSignificantDigits=2 roundingPriority=lessPrecision}", + "exp": "€43" + }, + { + "src": "{42.75 :currency currency=EUR roundingPriority=morePrecision}", + "exp": "€42.75" + }, + { + "src": "{42.75 :currency currency=EUR roundingPriority=lessPrecision}", + "exp": "€42.75" + }, + { + "src": "{42.75 :currency currency=EUR roundingIncrement=2}", + "exp": "€42.76" + }, + { + "src": "{42.75 :currency currency=EUR roundingIncrement=5}", + "exp": "€42.75" + }, + { + "src": "{42.75 :currency currency=EUR roundingIncrement=250}", + "exp": "€42.50" + }, + { + "src": "{42.75 :currency currency=EUR roundingIncrement=1000}", + "exp": "€40.00" + }, + { + "src": "{42.75 :currency currency=EUR roundingIncrement=10 roundingMode=ceil}", + "exp": "€42.80" + }, + { + "src": "{42.75 :currency currency=EUR roundingIncrement=10 roundingMode=floor}", + "exp": "€42.70" + }, + { + "src": "{42.75 :currency currency=EUR roundingIncrement=10 roundingMode=expand}", + "exp": "€42.80" + }, + { + "src": "{-42.75 :currency currency=EUR roundingIncrement=10 roundingMode=expand}", + "exp": "-€42.80" + }, + { + "src": "{-42.75 :currency currency=EUR roundingIncrement=10 roundingMode=ceil}", + "exp": "-€42.70" + }, + { + "src": "{-42.75 :currency currency=EUR roundingIncrement=10 roundingMode=floor}", + "exp": "-€42.80" + }, + { + "src": "{42.75 :currency currency=EUR roundingIncrement=10 roundingMode=halfCeil}", + "exp": "€42.80" + }, + { + "src": "{42.74 :currency currency=EUR roundingIncrement=10 roundingMode=halfCeil}", + "exp": "€42.70" + }, + { + "src": "{42.74 :currency currency=EUR roundingIncrement=10 roundingMode=ceil}", + "exp": "€42.80" + }, + { + "src": "{42.75 :currency currency=EUR roundingIncrement=10 roundingMode=trunc}", + "exp": "€42.70" + }, + { + "src": "{-42.75 :currency currency=EUR roundingIncrement=10 roundingMode=trunc}", + "exp": "-€42.70" + }, + { + "src": "{42.75 :currency currency=EUR roundingIncrement=10 roundingMode=halfFloor}", + "exp": "€42.70" + }, + { + "src": "{42.74 :currency currency=EUR roundingIncrement=10 roundingMode=halfFloor}", + "exp": "€42.70" + }, + { + "src": "{42.77 :currency currency=EUR roundingIncrement=10 roundingMode=halfFloor}", + "exp": "€42.80" + }, + { + "src": "{42.75 :currency currency=EUR roundingIncrement=10 roundingMode=halfExpand}", + "exp": "€42.80" + }, + { + "src": "{42.77 :currency currency=EUR roundingIncrement=10 roundingMode=halfExpand}", + "exp": "€42.80" + }, + { + "src": "{-42.74 :currency currency=EUR roundingIncrement=10 roundingMode=halfExpand}", + "exp": "-€42.70" + }, + { + "src": "{-42.75 :currency currency=EUR roundingIncrement=10 roundingMode=halfExpand}", + "exp": "-€42.80" + }, + { + "src": "{-42.77 :currency currency=EUR roundingIncrement=10 roundingMode=halfExpand}", + "exp": "-€42.80" + }, + { + "src": "{42.74 :currency currency=EUR roundingIncrement=10 roundingMode=halfTrunc}", + "exp": "€42.70" + }, + { + "src": "{42.75 :currency currency=EUR roundingIncrement=10 roundingMode=halfTrunc}", + "exp": "€42.70" + }, + { + "src": "{42.77 :currency currency=EUR roundingIncrement=10 roundingMode=halfTrunc}", + "exp": "€42.80" + }, + { + "src": "{-42.74 :currency currency=EUR roundingIncrement=10 roundingMode=halfTrunc}", + "exp": "-€42.70" + }, + { + "src": "{-42.75 :currency currency=EUR roundingIncrement=10 roundingMode=halfTrunc}", + "exp": "-€42.70" + }, + { + "src": "{-42.77 :currency currency=EUR roundingIncrement=10 roundingMode=halfTrunc}", + "exp": "-€42.80" + }, + { + "src": "{42.74 :currency currency=EUR roundingIncrement=10 roundingMode=halfTrunc}", + "exp": "€42.70" + }, + { + "src": "{42.74 :currency currency=EUR roundingIncrement=5 roundingMode=halfEven}", + "exp": "€42.75" + }, + { + "src": "{42.725 :currency currency=EUR roundingIncrement=5 roundingMode=halfEven}", + "exp": "€42.70" + }, + { + "src": "{42.77 :currency currency=EUR roundingIncrement=5 roundingMode=halfEven}", + "exp": "€42.75" + }, + { + "src": "{-42.74 :currency currency=EUR roundingIncrement=5 roundingMode=halfEven}", + "exp": "-€42.75" + }, + { + "src": "{-42.725 :currency currency=EUR roundingIncrement=5 roundingMode=halfEven}", + "exp": "-€42.70" + }, + { + "src": "{-42.77 :currency currency=EUR roundingIncrement=5 roundingMode=halfEven}", + "exp": "-€42.75" + }, + { + "src": "{42.74 :currency currency=EUR roundingIncrement=5 roundingMode=halfEven}", + "exp": "€42.75" + }, + { + "src": "{42.75 :currency currency=EUR roundingIncrement=2 minSignificantDigits=3 maxSignificantDigits=7}", + "exp": "€42.76" + }, + { + "src": "{42.75 :currency currency=EUR roundingIncrement=2 fractionDigits=6}", + "exp": "€42.760000" + }, + { + "src": "{42.75 :currency currency=XOF roundingIncrement=2 fractionDigits=auto}", + "exp": "F CFA 42.76", + "comment": "Result should be F CFA 42 -- see ICU-23433" + }, + { + "src": "{42.75 :currency currency=EUR roundingIncrement=2 fractionDigits=1}", + "exp": "€42.76", + "comment": "Result should be €42.8 -- see ICU-23433" + }, + { + "src": "{42.75 :currency currency=XOF roundingIncrement=2 fractionDigits=1}", + "exp": "F CFA 42.76", + "comment": "Result should be F CFA 42.8 -- see ICU-23433" + } + ] +} diff --git a/testdata/message2/spec/functions/currency.json b/testdata/message2/spec/functions/currency.json new file mode 100644 index 000000000000..9a440d3e24b1 --- /dev/null +++ b/testdata/message2/spec/functions/currency.json @@ -0,0 +1,56 @@ +{ + "$schema": "../../schemas/v0/tests.schema.json", + "scenario": "Currency function", + "description": "The built-in formatter for currencies.", + "defaultTestProperties": { + "tags": [":currency"], + "bidiIsolation": "none", + "locale": "en-US", + "expErrors": [] + }, + "tests": [ + { + "src": "{:currency}", + "expErrors": [{ "type": "bad-operand" }] + }, + { + "src": "{foo :currency}", + "expErrors": [{ "type": "bad-operand" }] + }, + { + "src": "{42 :currency}", + "expErrors": [{ "type": "bad-operand" }] + }, + { + "src": ".local $n = {42 :number} {{{$n :currency}}}", + "expErrors": [{ "type": "bad-operand" }] + }, + { + "src": "{42 :currency currency=EUR}" + }, + { + "src": ".local $n = {42 :number} {{{$n :currency currency=EUR}}}" + }, + { + "src": ".local $n = {42 :integer} {{{$n :currency currency=EUR}}}" + }, + { + "src": ".local $n = {42 :currency currency=EUR} {{{$n :currency}}}" + }, + { + "src": "{42 :currency currency=EUR fractionDigits=auto}" + }, + { + "src": "{42 :currency currency=EUR fractionDigits=2}" + }, + { + "src": "{$x :currency currency=EUR}", + "params": [{ "name": "x", "value": 41 }] + }, + { + "src": ".local $n = {42 :currency currency=EUR} .match $n * {{other}}", + "exp": "other", + "expErrors": [{ "type": "bad-selector" }] + } + ] +}