Intl currency formatting can specify decimalDigits and has table of defaults
(rolling forward after Automated g4 rollback of changelist 111723849).
*** Reason for rollback ***
Rolling forward
*** Original change description ***
Automated g4 rollback of changelist 111713489.
*** Reason for rollback ***
Breaks currency tests in adsense due to upper/lower case discrepancy
*** Original change description ***
Intl currency formatting can specify decimalDigits and has table of defaults
***
***
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=111970339
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf59256..318f5a9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,9 @@
* Switch all the source to use line comments.
* Slight improvement to the error message when parsing dates has an invalid
value.
+ * Introduce new NumberFormat.currency constructor which can explicitly take a
+ separate currency name and symbol, as well as the number of decimal digits.
+ * Provide a default number of decimal digits per-currency.
## 0.12.5
* Parse Eras in DateFormat.
diff --git a/lib/number_symbols_data.dart b/lib/number_symbols_data.dart
index 596acfb..9b32cbe 100644
--- a/lib/number_symbols_data.dart
+++ b/lib/number_symbols_data.dart
@@ -1949,3 +1949,72 @@
CURRENCY_PATTERN: '\u00A4#,##0.00',
DEF_CURRENCY_CODE: 'ZAR')
};
+
+final currencyFractionDigits = {
+ "ADP": 0,
+ "AFN": 0,
+ "ALL": 0,
+ "AMD": 0,
+ "BHD": 3,
+ "BIF": 0,
+ "BYR": 0,
+ "CAD": 2,
+ "CHF": 2,
+ "CLF": 4,
+ "CLP": 0,
+ "COP": 0,
+ "CRC": 0,
+ "CZK": 2,
+ "DEFAULT": 2,
+ "DJF": 0,
+ "ESP": 0,
+ "GNF": 0,
+ "GYD": 0,
+ "HUF": 2,
+ "IDR": 0,
+ "IQD": 0,
+ "IRR": 0,
+ "ISK": 0,
+ "ITL": 0,
+ "JOD": 3,
+ "JPY": 0,
+ "KMF": 0,
+ "KPW": 0,
+ "KRW": 0,
+ "KWD": 3,
+ "LAK": 0,
+ "LBP": 0,
+ "LUF": 0,
+ "LYD": 3,
+ "MGA": 0,
+ "MGF": 0,
+ "MMK": 0,
+ "MNT": 0,
+ "MRO": 0,
+ "MUR": 0,
+ "OMR": 3,
+ "PKR": 0,
+ "PYG": 0,
+ "RSD": 0,
+ "RWF": 0,
+ "SLL": 0,
+ "SOS": 0,
+ "STD": 0,
+ "SYP": 0,
+ "TMM": 0,
+ "TND": 3,
+ "TRL": 0,
+ "TWD": 2,
+ "TZS": 0,
+ "UGX": 0,
+ "UYI": 0,
+ "UZS": 0,
+ "VND": 0,
+ "VUV": 0,
+ "XAF": 0,
+ "XOF": 0,
+ "XPF": 0,
+ "YER": 0,
+ "ZMK": 0,
+ "ZWD": 0,
+};
diff --git a/lib/src/intl/number_format.dart b/lib/src/intl/number_format.dart
index 5af03d4..2fcff9b 100644
--- a/lib/src/intl/number_format.dart
+++ b/lib/src/intl/number_format.dart
@@ -32,7 +32,8 @@
/// There are also standard patterns available via the special constructors.
/// e.g.
/// var percent = new NumberFormat.percentFormat("ar");
-/// var eurosInUSFormat = new NumberFormat.currencyPattern("en_US", "€");
+/// var eurosInUSFormat = new NumberFormat.currency(locale: "en_US",
+/// symbol: "€");
/// There are four such constructors: decimalFormat, percentFormat,
/// scientificFormat and currencyFormat. However, at the moment,
/// scientificFormat prints only as equivalent to "#E0" and does not take
@@ -46,13 +47,16 @@
String _positivePrefix = '';
String _negativeSuffix = '';
String _positiveSuffix = '';
+
/// How many numbers in a group when using punctuation to group digits in
/// large numbers. e.g. in en_US: "1,000,000" has a grouping size of 3 digits
/// between commas.
int _groupingSize = 3;
+
/// In some formats the last grouping size may be different than previous
/// ones, e.g. Hindi.
int _finalGroupingSize = 3;
+
/// Set to true if the format has explicitly set the grouping size.
bool _groupingSizeSetExplicitly = false;
bool _decimalSeparatorAlwaysShown = false;
@@ -88,9 +92,50 @@
/// Caches the symbols used for our locale.
NumberSymbols _symbols;
- /// The name (or symbol) of the currency to print.
+ /// The name of the currency to print, in ISO 4217 form.
String currencyName;
+ /// The symbol to be used when formatting this as currency.
+ ///
+ /// For example, "$", "US$", or "€".
+ String _currencySymbol;
+
+ /// The symbol to be used when formatting this as currency.
+ ///
+ /// For example, "$", "US$", or "€".
+ String get currencySymbol => _currencySymbol ?? currencyName;
+
+ /// The number of decimal places to use when formatting.
+ ///
+ /// If this is not explicitly specified in the constructor, then for
+ /// currencies we use the default value for the currency if the name is given,
+ /// otherwise we use the value from the pattern for the locale.
+ ///
+ /// So, for example,
+ /// new NumberFormat.currency(name: 'USD', decimalDigits: 7)
+ /// will format with 7 decimal digits, because that's what we asked for. But
+ /// new NumberFormat.currency(locale: 'en_US', name: 'JPY')
+ /// will format with zero, because that's the default for JPY, and the
+ /// currency's default takes priority over the locale's default.
+ /// new NumberFormat.currency(locale: 'en_US')
+ /// will format with two, which is the default for that locale.
+ ///
+ int get decimalDigits => _decimalDigits;
+
+ int _decimalDigits;
+
+ /// For currencies, the default number of decimal places to use in
+ /// formatting. Defaults to two for non-currencies or currencies where it's
+ /// not specified.
+ int get _defaultDecimalDigits =>
+ currencyFractionDigits[currencyName.toUpperCase()] ??
+ currencyFractionDigits['DEFAULT'];
+
+ /// If we have a currencyName, use that currencies decimal digits, unless
+ /// we've explicitly specified some other number.
+ bool get _overridesDecimalDigits =>
+ decimalDigits != null || currencyName != symbols.DEF_CURRENCY_CODE;
+
/// Transient internal state in which to build up the result of the format
/// operation. We can have this be just an instance variable because Dart is
/// single-threaded and unless we do an asynchronous operation in the process
@@ -115,22 +160,77 @@
NumberFormat.scientificPattern([String locale])
: this._forPattern(locale, (x) => x.SCIENTIFIC_PATTERN);
- /// Create a number format that prints as CURRENCY_PATTERN. If provided,
+ /// A regular expression to validate currency names are exactly three
+ /// alphabetic characters.
+ static final _checkCurrencyName = new RegExp(r'^[a-zA-Z]{3}$');
+
+ /// Create a number format that prints as CURRENCY_PATTERN. (Deprecated:
+ /// prefer NumberFormat.currency)
+ ///
+ /// If provided,
/// use [nameOrSymbol] in place of the default currency name. e.g.
/// var eurosInCurrentLocale = new NumberFormat
/// .currencyPattern(Intl.defaultLocale, "€");
- NumberFormat.currencyPattern([String locale, String nameOrSymbol])
- : this._forPattern(locale, (x) => x.CURRENCY_PATTERN, nameOrSymbol);
+ factory NumberFormat.currencyPattern(
+ [String locale, String currencyNameOrSymbol]) {
+ // If it looks like an iso4217 name, pass as name, otherwise as symbol.
+ if (currencyNameOrSymbol != null &&
+ _checkCurrencyName.hasMatch(currencyNameOrSymbol)) {
+ return new NumberFormat.currency(
+ locale: locale, name: currencyNameOrSymbol);
+ } else {
+ return new NumberFormat.currency(
+ locale: locale, symbol: currencyNameOrSymbol);
+ }
+ }
+
+ /// Create a [NumberFormat] that formats using the locale's CURRENCY_PATTERN.
+ ///
+ /// If [locale] is not specified, it will use the current default locale.
+ ///
+ /// If [name] is specified, the currency with that ISO 4217 name will be used.
+ /// Otherwise we will use the default currency name for the current locale. If
+ /// no [symbol] is specified, we will use the currency name in the formatted
+ /// result. e.g.
+ /// var f = new NumberFormat.currency(locale: 'en_US', name: 'EUR')
+ /// will format currency like "EUR1.23". If we did not specify the name, it
+ /// would format like "USD1.23".
+ ///
+ /// If [symbol] is used, then that symbol will be used in formatting instead
+ /// of the name. e.g.
+ /// var eurosInCurrentLocale = new NumberFormat.currency(symbol: "€");
+ /// will format like "€1.23". Otherwise it will use the currency name.
+ /// If this is not explicitly specified in the constructor, then for
+ /// currencies we use the default value for the currency if the name is given,
+ /// otherwise we use the value from the pattern for the locale.
+ ///
+ /// If [decimalDigits] is specified, numbers will format with that many digits
+ /// after the decimal place. If it's not, they will use the default for the
+ /// currency in [name], and the default currency for [locale] if the currency
+ /// name is not specified. e.g.
+ /// new NumberFormat.currency(name: 'USD', decimalDigits: 7)
+ /// will format with 7 decimal digits, because that's what we asked for. But
+ /// new NumberFormat.currency(locale: 'en_US', name: 'JPY')
+ /// will format with zero, because that's the default for JPY, and the
+ /// currency's default takes priority over the locale's default.
+ /// new NumberFormat.currency(locale: 'en_US')
+ /// will format with two, which is the default for that locale.
+ // TODO(alanknight): Should we allow decimalDigits on other numbers.
+ NumberFormat.currency(
+ {String locale, String name, String symbol, int decimalDigits})
+ : this._forPattern(locale, (x) => x.CURRENCY_PATTERN,
+ name: name, currencySymbol: symbol, decimalDigits: decimalDigits);
/// Create a number format that prints in a pattern we get from
/// the [getPattern] function using the locale [locale].
NumberFormat._forPattern(String locale, Function getPattern,
- [this.currencyName])
+ {name, currencySymbol, decimalDigits})
: _locale = Intl.verifiedLocale(locale, localeExists) {
+ this._currencySymbol = currencySymbol;
+ this._decimalDigits = decimalDigits;
_symbols = numberFormatSymbols[_locale];
- if (currencyName == null) {
- currencyName = _symbols.DEF_CURRENCY_CODE;
- }
+ currencyName = name ?? _symbols.DEF_CURRENCY_CODE;
+
_setPattern(getPattern(_symbols));
}
@@ -437,8 +537,14 @@
if (newPattern == null) return;
// Make spaces non-breaking
_pattern = newPattern.replaceAll(' ', '\u00a0');
- var parser = new _NumberFormatParser(this, newPattern, currencyName);
+ var parser = new _NumberFormatParser(
+ this, newPattern, currencySymbol, decimalDigits);
parser.parse();
+ if (_overridesDecimalDigits) {
+ var digits = decimalDigits ?? _defaultDecimalDigits;
+ minimumFractionDigits = digits;
+ maximumFractionDigits = digits;
+ }
}
String toString() => "NumberFormat($_locale, $_pattern)";
@@ -475,9 +581,11 @@
/// Did we see something that indicates this is, or at least might be,
/// a negative number.
bool gotNegative = false;
+
/// Did we see the required positive suffix at the end. Should
/// match [gotPositive].
bool gotPositiveSuffix = false;
+
/// Did we see the required negative suffix at the end. Should
/// match [gotNegative].
bool gotNegativeSuffix = false;
@@ -695,11 +803,16 @@
final _StringIterator pattern;
/// We can be passed a specific currency symbol, regardless of the locale.
- String currencyName;
+ String currencySymbol;
+
+ /// We can be given a specific number of decimal places, overriding the
+ /// default.
+ final int decimalDigits;
/// Create a new [_NumberFormatParser] for a particular [NumberFormat] and
/// [input] pattern.
- _NumberFormatParser(this.format, input, this.currencyName)
+ _NumberFormatParser(
+ this.format, input, this.currencySymbol, this.decimalDigits)
: pattern = _iterator(input) {
pattern.moveNext();
}
@@ -775,7 +888,7 @@
return false;
case _PATTERN_CURRENCY_SIGN:
// TODO(alanknight): Handle the local/global/portable currency signs
- affix.write(currencyName);
+ affix.write(currencySymbol);
break;
case _PATTERN_PERCENT:
if (format._multiplier != 1 && format._multiplier != _PERCENT_SCALE) {
@@ -1034,16 +1147,16 @@
_MicroMoney operator ~/(divisor) {
if (divisor is! int) {
- throw new ArgumentError.value(divisor, 'divisor',
- '_MicroMoney ~/ only supports int arguments.');
+ throw new ArgumentError.value(
+ divisor, 'divisor', '_MicroMoney ~/ only supports int arguments.');
}
return new _MicroMoney((_integerPart ~/ divisor) * _multiplier);
}
_MicroMoney operator *(other) {
if (other is! int) {
- throw new ArgumentError.value(other, 'other',
- '_MicroMoney * only supports int arguments.');
+ throw new ArgumentError.value(
+ other, 'other', '_MicroMoney * only supports int arguments.');
}
return new _MicroMoney(
(_integerPart * other) * _multiplier + (_fractionPart * other));
@@ -1053,8 +1166,8 @@
/// not division by another MicroMoney
_MicroMoney remainder(other) {
if (other is! int) {
- throw new ArgumentError.value(other, 'other',
- '_MicroMoney.remainder only supports int arguments.');
+ throw new ArgumentError.value(
+ other, 'other', '_MicroMoney.remainder only supports int arguments.');
}
return new _MicroMoney(_micros.remainder(other * _multiplier));
}
diff --git a/test/fixnum_test.dart b/test/fixnum_test.dart
index ea634c8..9a8c346 100644
--- a/test/fixnum_test.dart
+++ b/test/fixnum_test.dart
@@ -62,7 +62,7 @@
main() {
test('int64', () {
int64Values.forEach((number, expected) {
- var currency = new NumberFormat.currencyPattern().format(number);
+ var currency = new NumberFormat.currency().format(number);
expect(currency, expected.first);
var percent = new NumberFormat.percentPattern().format(number);
expect(percent, expected[1]);
@@ -71,7 +71,7 @@
test('int32', () {
int32Values.forEach((number, expected) {
- var currency = new NumberFormat.currencyPattern().format(number);
+ var currency = new NumberFormat.currency().format(number);
expect(currency, expected.first);
var percent = new NumberFormat.percentPattern().format(number);
expect(percent, expected[1]);
diff --git a/test/number_format_test.dart b/test/number_format_test.dart
index df41c41..652cc14 100644
--- a/test/number_format_test.dart
+++ b/test/number_format_test.dart
@@ -81,35 +81,11 @@
var testFormats = standardFormats(locale);
var testLength = (testFormats.length * 3) + 1;
var list = mainList.take(testLength).iterator;
+ list.moveNext();
mainList = mainList.skip(testLength);
- var nextLocaleFromList = (list..moveNext()).current;
- test("Test against ICU data for $locale", () {
- expect(locale, nextLocaleFromList);
- for (var format in testFormats) {
- var formatted = format.format(123);
- var negative = format.format(-12.3);
- var large = format.format(1234567890);
- var expected = (list..moveNext()).current;
- expect(formatted, expected);
- var expectedNegative = (list..moveNext()).current;
- // Some of these results from CLDR have a leading LTR/RTL indicator,
- // which we don't want. We also treat the difference between Unicode
- // minus sign (2212) and hyphen-minus (45) as not significant.
- expectedNegative = expectedNegative
- .replaceAll("\u200e", "")
- .replaceAll("\u200f", "")
- .replaceAll("\u2212", "-");
- expect(negative, expectedNegative);
- var expectedLarge = (list..moveNext()).current;
- expect(large, expectedLarge);
- var readBack = format.parse(formatted);
- expect(readBack, 123);
- var readBackNegative = format.parse(negative);
- expect(readBackNegative, -12.3);
- var readBackLarge = format.parse(large);
- expect(readBackLarge, 1234567890);
- }
- });
+ if (locale == list.current) {
+ testAgainstIcu(locale, testFormats, list);
+ }
}
test('Simple set of numbers', () {
@@ -162,7 +138,7 @@
test('Explicit currency name', () {
var amount = 1000000.32;
- var usConvention = new NumberFormat.currencyPattern('en_US', '€');
+ var usConvention = new NumberFormat.currency(locale: 'en_US', symbol: '€');
var formatted = usConvention.format(amount);
expect(formatted, '€1,000,000.32');
var readBack = usConvention.parse(formatted);
@@ -175,7 +151,7 @@
expect(readBack, amount);
/// Verify we can leave off the currency and it gets filled in.
- var plainSwiss = new NumberFormat.currencyPattern('de_CH');
+ var plainSwiss = new NumberFormat.currency(locale: 'de_CH');
formatted = plainSwiss.format(amount);
expect(formatted, r"CHF" + nbsp + "1'000'000.32");
readBack = plainSwiss.parse(formatted);
@@ -198,10 +174,74 @@
});
test('Unparseable', () {
- var format = new NumberFormat.currencyPattern();
+ var format = new NumberFormat.currency();
expect(() => format.parse("abcdefg"), throwsFormatException);
expect(() => format.parse(""), throwsFormatException);
expect(() => format.parse("1.0zzz"), throwsFormatException);
expect(() => format.parse("-∞+1"), throwsFormatException);
});
+
+ var digitsCheck = {
+ 0: "@4",
+ 1: "@4.3",
+ 2: "@4.32",
+ 3: "@4.322",
+ 4: "@4.3220",
+ };
+
+ test('Decimal digits', () {
+ var amount = 4.3219876;
+ for (var digits in digitsCheck.keys) {
+ var f = new NumberFormat.currency(
+ locale: 'en_US', symbol: '@', decimalDigits: digits);
+ var formatted = f.format(amount);
+ expect(formatted, digitsCheck[digits]);
+ }
+ var defaultFormat = new NumberFormat.currency(locale: 'en_US', symbol: '@');
+ var formatted = defaultFormat.format(amount);
+ expect(formatted, digitsCheck[2]);
+
+ var jpy =
+ new NumberFormat.currency(locale: 'en_US', name: 'JPY', symbol: '@');
+ formatted = jpy.format(amount);
+ expect(formatted, digitsCheck[0]);
+
+ var jpyLower =
+ new NumberFormat.currency(locale: 'en_US', name: 'jpy', symbol: '@');
+ formatted = jpyLower.format(amount);
+ expect(formatted, digitsCheck[0]);
+
+ var tnd = new NumberFormat.currency(name: 'TND', symbol: '@');
+ formatted = tnd.format(amount);
+ expect(formatted, digitsCheck[3]);
+ });
+}
+
+void testAgainstIcu(locale, List<NumberFormat> testFormats, list) {
+ test("Test against ICU data for $locale", () {
+ for (var format in testFormats) {
+ var formatted = format.format(123);
+ var negative = format.format(-12.3);
+ var large = format.format(1234567890);
+ var expected = (list..moveNext()).current;
+ expect(formatted, expected);
+ var expectedNegative = (list..moveNext()).current;
+ // Some of these results from CLDR have a leading LTR/RTL indicator,
+ // which we don't want. We also treat the difference between Unicode
+ // minus sign (2212) and hyphen-minus (45) as not significant.
+ expectedNegative = expectedNegative
+ .replaceAll("\u200e", "")
+ .replaceAll("\u200f", "")
+ .replaceAll("\u2212", "-");
+ expect(negative, expectedNegative);
+ var expectedLarge = (list..moveNext()).current;
+ expect(large, expectedLarge);
+ var readBack = format.parse(formatted);
+ expect(readBack, 123);
+ var readBackNegative = format.parse(negative);
+ expect(readBackNegative, -12.3);
+ var readBackLarge = format.parse(large);
+ expect(readBackLarge, 1234567890);
+ }
+ });
}