Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(858)

Unified Diff: pkg/intl/lib/number_format.dart

Issue 13814018: Make use of the patterns in Intl number formatting (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Addressing review comments Created 7 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « pkg/intl/lib/intl.dart ('k') | pkg/intl/lib/number_symbols.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: pkg/intl/lib/number_format.dart
diff --git a/pkg/intl/lib/number_format.dart b/pkg/intl/lib/number_format.dart
index ed5cf2be23653d36cd506c4fbb3adf35f60a4f4d..c700e6217f0325f13b761aaad7e5e7e470efe279 100644
--- a/pkg/intl/lib/number_format.dart
+++ b/pkg/intl/lib/number_format.dart
@@ -2,14 +2,41 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-library number_format;
-
-import 'dart:math';
-
-import "intl.dart";
-import "number_symbols.dart";
-import "number_symbols_data.dart";
-
+part of intl;
+/**
+ * Provides the ability to format a number in a locale-specific way. The
+ * format is specified as a pattern using a subset of the ICU formatting
+ * patterns.
+ *
+ * 0 - A single digit
+ * # - A single digit, omitted if the value is zero
+ * . - Decimal separator
+ * - - Minus sign
+ * , - Grouping separator
+ * E - Separates mantissa and expontent
+ * + - Before an exponent, indicates it should be prefixed with a plus sign.
+ * % - In prefix or suffix, multiply by 100 and show as percentage
+ * \u2030 - In prefix or suffix, multiply by 1000 and show as per mille
+ * \u00A4 - Currency sign, replaced by currency name
+ * ' - Used to quote special characters
+ * ; - Used to separate the positive and negative patterns if both are present
+ *
+ * For example,
+ * var f = new NumberFormat("###.0#", "en_US");
+ * print(f.format(12.345));
+ * ==> 12.34
+ * If the locale is not specified, it will default to the current locale. If
+ * the format is not specified it will print in a basic format with at least
+ * one integer digit and three fraction digits.
+ *
+ * There are also standard patterns available via the special constructors. e.g.
+ * var symbols = new NumberFormat.percentFormat("ar");
+ * There are four such constructors: decimalFormat, percentFormat,
+ * scientificFormat and currencyForamt. However, at the moment,
+ * scientificFormat prints only as equivalent to "#E0" and does not take
+ * into account significant digits. currencyFormat will always use the name
+ * of the currency rather than the symbol.
+ */
class NumberFormat {
/** Variables to determine how number printing behaves. */
// TODO(alanknight): If these remain as variables and are set based on the
@@ -18,19 +45,64 @@ class NumberFormat {
String _positivePrefix = '';
String _negativeSuffix = '';
String _positiveSuffix = '';
- /** How many numbers in a group when using punctuation to group digits in
+ /**
+ * 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;
bool _decimalSeparatorAlwaysShown = false;
+ bool _useSignForPositiveExponent = false;
bool _useExponentialNotation = false;
+
int _maximumIntegerDigits = 40;
int _minimumIntegerDigits = 1;
- int _maximumFractionDigits = 3; // invariant, >= minFractionDigits
+ int _maximumFractionDigits = 3;
int _minimumFractionDigits = 0;
int _minimumExponentDigits = 0;
- bool _useSignForPositiveExponent = false;
+
+ int _multiplier = 1;
+
+ /**
+ * Stores the pattern used to create this format. This isn't used, but
+ * is helpful in debugging.
+ */
+ String _pattern;
+ /**
+ * Set the maximum digits printed to the left of the decimal point.
+ * Normally this is computed from the pattern, but it's exposed here for
+ * testing purposes and for rare cases where you want to force it explicitly.
+ */
+ void setMaximumIntegerDigits(int max) {
+ _maximumIntegerDigits = max;
+ }
+
+ /**
+ * Set the minimum number of digits printed to the left of the decimal point.
+ * Normally this is computed from the pattern, but it's exposed here for
+ * testing purposes and for rare cases where you want to force it explicitly.
+ */
+ void setMinimumIntegerDigits(int min) {
+ _minimumIntegerDigits = min;
+ }
+
+ /**
+ * Set the maximum number of digits printed to the right of the decimal point.
+ * Normally this is computed from the pattern, but it's exposed here for
+ * testing purposes and for rare cases where you want to force it explicitly.
+ */
+ void setMaximumFractionDigits(int max) {
+ _maximumFractionDigits = max;
+ }
+
+ /**
+ * Set the minimum digits printed to the left of the decimal point.
+ * Normally this is computed from the pattern, but it's exposed here for
+ * testing purposes and for rare cases where you want to force it explicitly.
+ */
+ void setMinimumFractionDigits(int max) {
+ _minimumFractionDigits = max;
+ }
/** The locale in which we print numbers. */
final String _locale;
@@ -48,15 +120,37 @@ class NumberFormat {
StringBuffer _buffer;
/**
- * Create a number format that prints in [newPattern] as it applies in
+ * Create a number format that prints using [newPattern] as it applies in
* [locale].
*/
- NumberFormat([String newPattern, String locale]):
- _locale = Intl.verifiedLocale(locale, localeExists) {
- // TODO(alanknight): There will need to be some kind of async setup
- // operations so as not to bring along every locale in every program.
+ factory NumberFormat([String newPattern, String locale]) {
+ return new NumberFormat._forPattern(locale, (x) => newPattern);
+ }
+
+ /** Create a number format that prints as DECIMAL_PATTERN. */
+ NumberFormat.decimalPattern([String locale]) :
+ this._forPattern(locale, (x) => x.DECIMAL_PATTERN);
+
+ /** Create a number format that prints as PERCENT_PATTERN. */
+ NumberFormat.percentPattern([String locale]) :
+ this._forPattern(locale, (x) => x.PERCENT_PATTERN);
+
+ /** Create a number format that prints as SCIENTIFIC_PATTERN. */
+ NumberFormat.scientificPattern([String locale]) :
+ this._forPattern(locale, (x) => x.SCIENTIFIC_PATTERN);
+
+ /** Create a number format that prints as CURRENCY_PATTERN. */
+ NumberFormat.currencyPattern([String locale]) :
+ this._forPattern(locale, (x) => x.CURRENCY_PATTERN);
+
+ /**
+ * 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) :
+ _locale = Intl.verifiedLocale(locale, localeExists) {
_symbols = numberFormatSymbols[_locale];
- _setPattern(newPattern);
+ _setPattern(getPattern(_symbols));
}
/**
@@ -81,9 +175,6 @@ class NumberFormat {
return _symbols;
}
- // TODO(alanknight): Actually use the pattern and locale.
- _setPattern(String x) {}
-
/**
* Format [number] according to our pattern and return the formatted string.
*/
@@ -95,7 +186,7 @@ class NumberFormat {
_newBuffer();
_add(_signPrefix(number));
- _formatNumber(number.abs());
+ _formatNumber(number.abs() * _multiplier);
_add(_signSuffix(number));
var result = _buffer.toString();
@@ -115,7 +206,7 @@ class NumberFormat {
}
/** Format the number in exponential notation. */
- _formatExponential(num number) {
+ void _formatExponential(num number) {
if (number == 0.0) {
_formatFixed(number);
_formatExponent(0);
@@ -123,16 +214,32 @@ class NumberFormat {
}
var exponent = (log(number) / log(10)).floor();
- var mantissa = number / pow(10, exponent);
-
- if (_minimumIntegerDigits < 1) {
- exponent++;
- mantissa /= 10;
+ var mantissa = number / pow(10.0, exponent);
+
+ var minIntDigits = _minimumIntegerDigits;
+ if (_maximumIntegerDigits > 1 &&
+ _maximumIntegerDigits > _minimumIntegerDigits) {
+ // A repeating range is defined; adjust to it as follows.
+ // If repeat == 3, we have 6,5,4=>3; 3,2,1=>0; 0,-1,-2=>-3;
+ // -3,-4,-5=>-6, etc. This takes into account that the
+ // exponent we have here is off by one from what we expect;
+ // it is for the format 0.MMMMMx10^n.
+ while ((exponent % _maximumIntegerDigits) != 0) {
+ mantissa *= 10;
+ exponent--;
+ }
+ minIntDigits = 1;
} else {
- exponent -= _minimumIntegerDigits - 1;
- mantissa *= pow(10, _minimumIntegerDigits - 1);
+ // No repeating range is defined, use minimum integer digits.
+ if (_minimumIntegerDigits < 1) {
+ exponent++;
+ mantissa /= 10;
+ } else {
+ exponent -= _minimumIntegerDigits - 1;
+ mantissa *= pow(10, _minimumIntegerDigits - 1);
+ }
}
- _formatFixed(number);
+ _formatFixed(mantissa);
_formatExponent(exponent);
}
@@ -150,24 +257,45 @@ class NumberFormat {
_pad(_minimumExponentDigits, exponent.toString());
}
+ /** Used to test if we have exceeded Javascript integer limits. */
+ final _maxInt = pow(2, 52);
+
/**
* Format the basic number portion, inluding the fractional digits.
*/
void _formatFixed(num number) {
- // Round the number.
+ // Very fussy math to get integer and fractional parts.
var power = pow(10, _maximumFractionDigits);
- var intValue = number.truncate();
- var multiplied = (number * power).round();
- var fracValue = (multiplied - intValue * power).floor();
+ var shiftedNumber = (number * power);
+ // We must not roundToDouble() an int or it will lose precision. We must not
+ // round() a large double or it will take its loss of precision and
+ // preserve it in an int, which we will then print to the right
+ // of the decimal place. Therefore, only roundToDouble if we are already
+ // a double.
+ if (shiftedNumber is double) {
+ shiftedNumber = shiftedNumber.roundToDouble();
+ }
+ var intValue, fracValue;
+ if (shiftedNumber.isInfinite) {
+ intValue = number.toInt();
+ fracValue = 0;
+ } else {
+ intValue = shiftedNumber.round() ~/ power;
+ fracValue = (shiftedNumber - intValue * power).floor();
+ }
var fractionPresent = _minimumFractionDigits > 0 || fracValue > 0;
- // On dartj2s the integer part may be large enough to be a floating
- // point value, in which case we reduce it until it is small enough
- // to be printed as an integer and pad the remainder with zeros.
+ // If the int part is larger than 2^52 and we're on Javascript (so it's
+ // really a float) it will lose precision, so pad out the rest of it
+ // with zeros. Check for Javascript by seeing if an integer is double.
var paddingDigits = new StringBuffer();
- while ((intValue & 0x7fffffff) != intValue) {
- paddingDigits.write(symbols.ZERO_DIGIT);
- intValue = intValue ~/ 10;
+ if (1 is double && intValue > _maxInt) {
+ var howManyDigitsTooBig = (log(intValue) / LN10).ceil() - 16;
+ var divisor = pow(10, howManyDigitsTooBig).round();
+ for (var each in new List(howManyDigitsTooBig.toInt())) {
+ paddingDigits.write(symbols.ZERO_DIGIT);
+ }
+ intValue = (intValue / divisor).truncate();
}
var integerDigits = "${intValue}${paddingDigits}".codeUnits;
var digitLength = integerDigits.length;
@@ -193,7 +321,7 @@ class NumberFormat {
void _formatFractionPart(String fractionPart) {
var fractionCodes = fractionPart.codeUnits;
var fractionLength = fractionPart.length;
- while (fractionPart[fractionLength - 1] == '0' &&
+ while(fractionCodes[fractionLength - 1] == _zero &&
fractionLength > _minimumFractionDigits + 1) {
fractionLength--;
}
@@ -258,14 +386,14 @@ class NumberFormat {
}
/** Returns the code point for the character '0'. */
- int get _zero => '0'.codeUnits.first;
+ final _zero = '0'.codeUnits.first;
/** Returns the code point for the locale's zero digit. */
// Note that there is a slight risk of a locale's zero digit not fitting
// into a single code unit, but it seems very unlikely, and if it did,
// there's a pretty good chance that our assumptions about being able to do
// arithmetic on it would also be invalid.
- int get _localeZero => symbols.ZERO_DIGIT.codeUnits.first;
+ get _localeZero => symbols.ZERO_DIGIT.codeUnits.first;
/**
* Returns the prefix for [x] based on whether it's positive or negative.
@@ -282,4 +410,332 @@ class NumberFormat {
String _signSuffix(num x) {
return x.isNegative ? _negativeSuffix : _positiveSuffix;
}
+
+ void _setPattern(String newPattern) {
+ if (newPattern == null) return;
+ // Make spaces non-breaking
+ _pattern = newPattern.replaceAll(' ', '\u00a0');
+ var parser = new _NumberFormatParser(this, newPattern);
+ parser.parse();
+ }
+
+ String toString() => "NumberFormat($_locale, $_pattern)";
}
+
+/**
+ * Private class that parses the numeric formatting pattern and sets the
+ * variables in [format] to appropriate values. Instances of this are
+ * transient and store parsing state in instance variables, so can only be used
+ * to parse a single pattern.
+ */
+class _NumberFormatParser {
+
+ /**
+ * The special characters in the pattern language. All others are treated
+ * as literals.
+ */
+ static const _PATTERN_SEPARATOR = ';';
+ static const _QUOTE = "'";
+ static const _PATTERN_DIGIT = '#';
+ static const _PATTERN_ZERO_DIGIT = '0';
+ static const _PATTERN_GROUPING_SEPARATOR = ',';
+ static const _PATTERN_DECIMAL_SEPARATOR = '.';
+ static const _PATTERN_CURRENCY_SIGN = '\u00A4';
+ static const _PATTERN_PER_MILLE = '\u2030';
+ static const _PATTERN_PERCENT = '%';
+ static const _PATTERN_EXPONENT = 'E';
+ static const _PATTERN_PLUS = '+';
+
+ /** The format whose state we are setting. */
+ final NumberFormat format;
+
+ /** The pattern we are parsing. */
+ final _StringIterator pattern;
+
+ /**
+ * Create a new [_NumberFormatParser] for a particular [NumberFormat] and
+ * [input] pattern.
+ */
+ _NumberFormatParser(this.format, input) : pattern = _iterator(input) {
+ pattern.moveNext();
+ }
+
+ /** The [NumberSymbols] for the locale in which our [format] prints. */
+ NumberSymbols get symbols => format.symbols;
+
+ /** Parse the input pattern and set the values. */
+ void parse() {
+ format._positivePrefix = _parseAffix();
+ var trunk = _parseTrunk();
+ format._positiveSuffix = _parseAffix();
+ // If we have separate positive and negative patterns, now parse the
+ // the negative version.
+ if (pattern.current == _NumberFormatParser._PATTERN_SEPARATOR) {
+ pattern.moveNext();
+ format._negativePrefix = _parseAffix();
+ // Skip over the negative trunk, verifying that it's identical to the
+ // positive trunk.
+ for (var each in _iterator(trunk)) {
+ if (pattern.current != each && pattern.current != null) {
+ throw new FormatException(
+ "Positive and negative trunks must be the same");
+ }
+ pattern.moveNext();
+ }
+ format._negativeSuffix = _parseAffix();
+ } else {
+ // If no negative affix is specified, they share the same positive affix.
+ format._negativePrefix = format._positivePrefix + format._negativePrefix;
+ format._negativeSuffix = format._negativeSuffix + format._positiveSuffix;
+ }
+ }
+
+ /** Variable used in parsing prefixes and suffixes to keep track of
+ * whether or not we are in a quoted region. */
+ bool inQuote = false;
+
+ /**
+ * Parse a prefix or suffix and return the prefix/suffix string. Note that
+ * this also may modify the state of [format].
+ */
+ String _parseAffix() {
+ var affix = new StringBuffer();
+ inQuote = false;
+ var loop = true;
+ while (loop) {
+ loop = parseCharacterAffix(affix) && pattern.moveNext();
+ }
+ return affix.toString();
+ }
+
+ /**
+ * Parse an individual character as part of a prefix or suffix. Return true
+ * if we should continue to look for more affix characters, and false if
+ * we have reached the end.
+ */
+ bool parseCharacterAffix(StringBuffer affix) {
+ var ch = pattern.current;
+ if (ch == null) return false;
+ if (ch == _QUOTE) {
+ var nextChar = pattern.peek;
+ if (nextChar == _QUOTE) {
+ pattern.moveNext();
+ affix.write(_QUOTE); // 'don''t'
+ } else {
+ inQuote = !inQuote;
+ }
+ return true;
+ }
+
+ if (inQuote) {
+ affix.write(ch);
+ } else {
+ switch (ch) {
+ case _PATTERN_DIGIT:
+ case _PATTERN_ZERO_DIGIT:
+ case _PATTERN_GROUPING_SEPARATOR:
+ case _PATTERN_DECIMAL_SEPARATOR:
+ case _PATTERN_SEPARATOR:
+ return false;
+ case _PATTERN_CURRENCY_SIGN:
+ // TODO(alanknight): Handle the local/global/portable currency signs
+ affix.write(symbols.DEF_CURRENCY_CODE);
+ break;
+ case _PATTERN_PERCENT:
+ if (format._multiplier != 1) {
+ throw new FormatException('Too many percent/permill');
+ }
+ format._multiplier = 100;
+ affix.write(symbols.PERCENT);
+ break;
+ case _PATTERN_PER_MILLE:
+ if (format._multiplier != 1) {
+ throw new FormatException('Too many percent/permill');
+ }
+ format._multiplier = 1000;
+ affix.write(symbols.PERMILL);
+ break;
+ default:
+ affix.write(ch);
+ }
+ }
+ return true;
+ }
+
+ /** Variables used in [parseTrunk] and [parseTrunkCharacter]. */
+ var decimalPos;
+ var digitLeftCount;
+ var zeroDigitCount;
+ var digitRightCount;
+ var groupingCount;
+ var trunk;
+
+ /**
+ * Parse the "trunk" portion of the pattern, the piece that doesn't include
+ * positive or negative prefixes or suffixes.
+ */
+ String _parseTrunk() {
+ decimalPos = -1;
+ digitLeftCount = 0;
+ zeroDigitCount = 0;
+ digitRightCount = 0;
+ groupingCount = -1;
+
+ var loop = true;
+ trunk = new StringBuffer();
+ while (pattern.current != null && loop) {
+ loop = parseTrunkCharacter();
+ }
+
+ if (zeroDigitCount == 0 && digitLeftCount > 0 && decimalPos >= 0) {
+ // Handle '###.###' and '###.' and '.###'
+ var n = decimalPos;
+ if (n == 0) { // Handle '.###'
+ n++;
+ }
+ digitRightCount = digitLeftCount - n;
+ digitLeftCount = n - 1;
+ zeroDigitCount = 1;
+ }
+
+ // Do syntax checking on the digits.
+ if (decimalPos < 0 && digitRightCount > 0 ||
+ decimalPos >= 0 && (decimalPos < digitLeftCount ||
+ decimalPos > digitLeftCount + zeroDigitCount) ||
+ groupingCount == 0) {
+ throw new FormatException('Malformed pattern "${pattern.input}"');
+ }
+ var totalDigits = digitLeftCount + zeroDigitCount + digitRightCount;
+
+ format._maximumFractionDigits =
+ decimalPos >= 0 ? totalDigits - decimalPos : 0;
+ if (decimalPos >= 0) {
+ format._minimumFractionDigits =
+ digitLeftCount + zeroDigitCount - decimalPos;
+ if (format._minimumFractionDigits < 0) {
+ format._minimumFractionDigits = 0;
+ }
+ }
+
+ // The effectiveDecimalPos is the position the decimal is at or would be at
+ // if there is no decimal. Note that if decimalPos<0, then digitTotalCount
+ // == digitLeftCount + zeroDigitCount.
+ var effectiveDecimalPos = decimalPos >= 0 ? decimalPos : totalDigits;
+ format._minimumIntegerDigits = effectiveDecimalPos - digitLeftCount;
+ if (format._useExponentialNotation) {
+ format._maximumIntegerDigits =
+ digitLeftCount + format._minimumIntegerDigits;
+
+ // In exponential display, we need to at least show something.
+ if (format._maximumFractionDigits == 0 &&
+ format._minimumIntegerDigits == 0) {
+ format._minimumIntegerDigits = 1;
+ }
+ }
+
+ format._groupingSize = max(0, groupingCount);
+ format._decimalSeparatorAlwaysShown = decimalPos == 0 ||
+ decimalPos == totalDigits;
+
+ return trunk.toString();
+ }
+
+ /**
+ * Parse an individual character of the trunk. Return true if we should
+ * continue to look for additional trunk characters or false if we have
+ * reached the end.
+ */
+ bool parseTrunkCharacter() {
+ var ch = pattern.current;
+ switch (ch) {
+ case _PATTERN_DIGIT:
+ if (zeroDigitCount > 0) {
+ digitRightCount++;
+ } else {
+ digitLeftCount++;
+ }
+ if (groupingCount >= 0 && decimalPos < 0) {
+ groupingCount++;
+ }
+ break;
+ case _PATTERN_ZERO_DIGIT:
+ if (digitRightCount > 0) {
+ throw new FormatException('Unexpected "0" in pattern "'
+ + pattern.input + '"');
+ }
+ zeroDigitCount++;
+ if (groupingCount >= 0 && decimalPos < 0) {
+ groupingCount++;
+ }
+ break;
+ case _PATTERN_GROUPING_SEPARATOR:
+ groupingCount = 0;
+ break;
+ case _PATTERN_DECIMAL_SEPARATOR:
+ if (decimalPos >= 0) {
+ throw new FormatException(
+ 'Multiple decimal separators in pattern "$pattern"');
+ }
+ decimalPos = digitLeftCount + zeroDigitCount + digitRightCount;
+ break;
+ case _PATTERN_EXPONENT:
+ trunk.write(ch);
+ if (format._useExponentialNotation) {
+ throw new FormatException(
+ 'Multiple exponential symbols in pattern "$pattern"');
+ }
+ format._useExponentialNotation = true;
+ format._minimumExponentDigits = 0;
+
+ // exponent pattern can have a optional '+'.
+ pattern.moveNext();
+ var nextChar = pattern.current;
+ if (nextChar == _PATTERN_PLUS) {
+ trunk.write(pattern.current);
+ pattern.moveNext();
+ format._useSignForPositiveExponent = true;
+ }
+
+ // Use lookahead to parse out the exponential part
+ // of the pattern, then jump into phase 2.
+ while (pattern.current == _PATTERN_ZERO_DIGIT) {
+ trunk.write(pattern.current);
+ pattern.moveNext();
+ format._minimumExponentDigits++;
+ }
+
+ if ((digitLeftCount + zeroDigitCount) < 1 ||
+ format._minimumExponentDigits < 1) {
+ throw new FormatException(
+ 'Malformed exponential pattern "$pattern"');
+ }
+ return false;
+ default:
+ return false;
+ }
+ trunk.write(ch);
+ pattern.moveNext();
+ return true;
+ }
+}
+
+/**
+ * Return an iterator on the string as a list of substrings.
+ */
+Iterator _iterator(String s) => new _StringIterator(s);
+
+/**
+ * Provides an iterator over a string as a list of substrings, and also
+ * gives us a lookahead of one via the [peek] method.
+ */
+class _StringIterator implements Iterator<String> {
+ String input;
+ var index = -1;
+ inBounds(i) => i >= 0 && i < input.length;
+ _StringIterator(this.input);
+ String get current => inBounds(index) ? input[index] : null;
+
+ bool moveNext() => inBounds(++index);
+ String get peek => inBounds(index + 1) ? input[index + 1] : null;
+ Iterator<String> get iterator => this;
+}
« no previous file with comments | « pkg/intl/lib/intl.dart ('k') | pkg/intl/lib/number_symbols.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698