blob: 5d9cf08a5a491600dd22d4144d4f4601fc60da15 [file] [log] [blame]
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// 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.
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 percent = new NumberFormat.percentFormat("ar");
* var eurosInUSFormat = new NumberFormat.currencyPattern("en_US", "€");
* 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
* into account significant digits. The currencyFormat will default to the
* three-letter name of the currency if no explicit name/symbol is provided.
*/
class NumberFormat {
/** Variables to determine how number printing behaves. */
// TODO(alanknight): If these remain as variables and are set based on the
// pattern, can we make them final?
String _negativePrefix = '-';
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;
bool _useSignForPositiveExponent = false;
bool _useExponentialNotation = false;
int maximumIntegerDigits = 40;
int minimumIntegerDigits = 1;
int maximumFractionDigits = 3;
int minimumFractionDigits = 0;
int minimumExponentDigits = 0;
/**
* For percent and permille, what are we multiplying by in order to
* get the printed value, e.g. 100 for percent.
*/
int get _multiplier => _internalMultiplier;
set _multiplier(int x) {
_internalMultiplier = x;
_multiplierDigits = (log(_multiplier) / LN10).round();
}
int _internalMultiplier = 1;
/** How many digits are there in the [_multiplier]. */
int _multiplierDigits = 0;
/**
* Stores the pattern used to create this format. This isn't used, but
* is helpful in debugging.
*/
String _pattern;
/** The locale in which we print numbers. */
final String _locale;
/** Caches the symbols used for our locale. */
NumberSymbols _symbols;
/** The name (or symbol) of the currency to print. */
String currencyName;
/**
* 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
* of formatting then there will only ever be one number being formatted
* at a time. In languages with threads we'd need to pass this on the stack.
*/
final StringBuffer _buffer = new StringBuffer();
/**
* Create a number format that prints using [newPattern] as it applies in
* [locale].
*/
factory NumberFormat([String newPattern, String locale]) =>
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. 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);
/**
* 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]) :
_locale = Intl.verifiedLocale(locale, localeExists) {
_symbols = numberFormatSymbols[_locale];
if (currencyName == null) {
currencyName = _symbols.DEF_CURRENCY_CODE;
}
_setPattern(getPattern(_symbols));
}
/**
* Return the locale code in which we operate, e.g. 'en_US' or 'pt'.
*/
String get locale => _locale;
/**
* Return true if the locale exists, or if it is null. The null case
* is interpreted to mean that we use the default locale.
*/
static bool localeExists(localeName) {
if (localeName == null) return false;
return numberFormatSymbols.containsKey(localeName);
}
/**
* Return the symbols which are used in our locale. Cache them to avoid
* repeated lookup.
*/
NumberSymbols get symbols => _symbols;
/**
* Format [number] according to our pattern and return the formatted string.
*/
String format(number) {
if (_isNaN(number)) return symbols.NAN;
if (_isInfinite(number)) return "${_signPrefix(number)}${symbols.INFINITY}";
_add(_signPrefix(number));
_formatNumber(number.abs());
_add(_signSuffix(number));
var result = _buffer.toString();
_buffer.clear();
return result;
}
/**
* Parse the number represented by the string. If it's not
* parseable, throws a [FormatException].
*/
num parse(String text) => new _NumberParser(this, text).value;
/**
* Format the main part of the number in the form dictated by the pattern.
*/
void _formatNumber(number) {
if (_useExponentialNotation) {
_formatExponential(number);
} else {
_formatFixed(number);
}
}
/** Format the number in exponential notation. */
void _formatExponential(num number) {
if (number == 0.0) {
_formatFixed(number);
_formatExponent(0);
return;
}
var exponent = (log(number) / log(10)).floor();
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 {
// 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(mantissa);
_formatExponent(exponent);
}
/**
* Format the exponent portion, e.g. in "1.3e-5" the "e-5".
*/
void _formatExponent(num exponent) {
_add(symbols.EXP_SYMBOL);
if (exponent < 0) {
exponent = -exponent;
_add(symbols.MINUS_SIGN);
} else if (_useSignForPositiveExponent) {
_add(symbols.PLUS_SIGN);
}
_pad(minimumExponentDigits, exponent.toString());
}
/** Used to test if we have exceeded Javascript integer limits. */
final _maxInt = pow(2, 52);
/**
* Helpers to check numbers that don't conform to the [num] interface,
* e.g. Int64
*/
_isInfinite(number) => number is num ? number.isInfinite : false;
_isNaN(number) => number is num ? number.isNaN : false;
_round(number) => number is num ? number.round() : number;
_floor(number) => number is num ? number.floor() : number;
/**
* Format the basic number portion, inluding the fractional digits.
*/
void _formatFixed(number) {
var integerPart;
int fractionPart;
int extraIntegerDigits;
final power = pow(10, maximumFractionDigits);
final digitMultiplier = power * _multiplier;
if (_isInfinite(number)) {
integerPart = number.toInt();
extraIntegerDigits = 0;
fractionPart = 0;
} else {
// We have three possible pieces. First, the basic integer part. If this
// is a percent or permille, the additional 2 or 3 digits. Finally the
// fractional part.
// We avoid multiplying the number because it might overflow if we have
// a fixed-size integer type, so we extract each of the three as an
// integer pieces.
integerPart = _floor(number);
var fraction = number - integerPart;
// Multiply out to the number of decimal places and the percent, then
// round. For fixed-size integer types this should always be zero, so
// multiplying is OK.
var remainingDigits = _round(fraction * digitMultiplier).toInt();
// However, in rounding we may overflow into the main digits.
if (remainingDigits >= digitMultiplier) {
integerPart++;
remainingDigits -= digitMultiplier;
}
// Separate out the extra integer parts from the fraction part.
extraIntegerDigits = remainingDigits ~/ power;
fractionPart = remainingDigits % power;
}
var fractionPresent = minimumFractionDigits > 0 || fractionPart > 0;
var integerDigits = _integerDigits(integerPart, extraIntegerDigits);
var digitLength = integerDigits.length;
if (_hasIntegerDigits(integerDigits)) {
_pad(minimumIntegerDigits - digitLength);
for (var i = 0; i < digitLength; i++) {
_addDigit(integerDigits.codeUnitAt(i));
_group(digitLength, i);
}
} else if (!fractionPresent) {
// If neither fraction nor integer part exists, just print zero.
_addZero();
}
_decimalSeparator(fractionPresent);
_formatFractionPart((fractionPart + power).toString());
}
/**
* Compute the raw integer digits which will then be printed with
* grouping and translated to localized digits.
*/
String _integerDigits(integerPart, extraIntegerDigits) {
// 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 = '';
if (1 is double && integerPart is num && integerPart > _maxInt) {
var howManyDigitsTooBig = (log(integerPart) / LN10).ceil() - 16;
var divisor = pow(10, howManyDigitsTooBig).round();
paddingDigits = symbols.ZERO_DIGIT * howManyDigitsTooBig.toInt();
integerPart = (integerPart / divisor).truncate();
}
var extra = extraIntegerDigits == 0 ? '' : extraIntegerDigits.toString();
var intDigits = _mainIntegerDigits(integerPart);
var paddedExtra =
intDigits.isEmpty ? extra : extra.padLeft(_multiplierDigits, '0');
return "${intDigits}${paddedExtra}${paddingDigits}";
}
/**
* The digit string of the integer part. This is the empty string if the
* integer part is zero and otherwise is the toString() of the integer
* part, stripping off any minus sign.
*/
String _mainIntegerDigits(integer) {
if (integer == 0) return '';
var digits = integer.toString();
// If we have a fixed-length int representation, it can have a negative
// number whose negation is also negative, e.g. 2^-63 in 64-bit.
// Remove the minus sign.
return digits.startsWith('-') ? digits.substring(1) : digits;
}
/**
* Format the part after the decimal place in a fixed point number.
*/
void _formatFractionPart(String fractionPart) {
var fractionCodes = fractionPart.codeUnits;
var fractionLength = fractionPart.length;
while (fractionCodes[fractionLength - 1] == _zero &&
fractionLength > minimumFractionDigits + 1) {
fractionLength--;
}
for (var i = 1; i < fractionLength; i++) {
_addDigit(fractionCodes[i]);
}
}
/** Print the decimal separator if appropriate. */
void _decimalSeparator(bool fractionPresent) {
if (_decimalSeparatorAlwaysShown || fractionPresent) {
_add(symbols.DECIMAL_SEP);
}
}
/**
* Return true if we have a main integer part which is printable, either
* because we have digits left of the decimal point (this may include digits
* which have been moved left because of percent or permille formatting),
* or because the minimum number of printable digits is greater than 1.
*/
bool _hasIntegerDigits(String digits) =>
digits.isNotEmpty || minimumIntegerDigits > 0;
/** A group of methods that provide support for writing digits and other
* required characters into [_buffer] easily.
*/
void _add(String x) { _buffer.write(x);}
void _addCharCode(int x) { _buffer.writeCharCode(x);}
void _addZero() { _buffer.write(symbols.ZERO_DIGIT);}
void _addDigit(int x) { _buffer.writeCharCode(_localeZero + x - _zero);}
/** Print padding up to [numberOfDigits] above what's included in [basic]. */
void _pad(int numberOfDigits, [String basic = '']) {
for (var i = 0; i < numberOfDigits - basic.length; i++) {
_add(symbols.ZERO_DIGIT);
}
for (var x in basic.codeUnits) {
_addDigit(x);
}
}
/**
* We are printing the digits of the number from left to right. We may need
* to print a thousands separator or other grouping character as appropriate
* to the locale. So we find how many places we are from the end of the number
* by subtracting our current [position] from the [totalLength] and printing
* the separator character every [_groupingSize] digits, with the final
* grouping possibly being of a different size, [_finalGroupingSize].
*/
void _group(int totalLength, int position) {
var distanceFromEnd = totalLength - position;
if (distanceFromEnd <= 1 || _groupingSize <= 0) return;
if (distanceFromEnd == _finalGroupingSize + 1) {
_add(symbols.GROUP_SEP);
} else if ((distanceFromEnd > _finalGroupingSize) &&
(distanceFromEnd - _finalGroupingSize) % _groupingSize == 1) {
_add(symbols.GROUP_SEP);
}
}
/** Returns the code point for the character '0'. */
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.
get _localeZero => symbols.ZERO_DIGIT.codeUnits.first;
/**
* Returns the prefix for [x] based on whether it's positive or negative.
* In en_US this would be '' and '-' respectively.
*/
String _signPrefix(x) => x.isNegative ? _negativePrefix : _positivePrefix;
/**
* Returns the suffix for [x] based on wether it's positive or negative.
* In en_US there are no suffixes for positive or negative.
*/
String _signSuffix(x) => 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, currencyName);
parser.parse();
}
String toString() => "NumberFormat($_locale, $_pattern)";
}
/**
* A one-time object for parsing a particular numeric string. One-time here
* means an instance can only parse one string. This is implemented by
* transforming from a locale-specific format to one that the system can parse,
* then calls the system parsing methods on it.
*/
class _NumberParser {
/** The format for which we are parsing. */
final NumberFormat format;
/** The text we are parsing. */
final String text;
/** What we use to iterate over the input text. */
final _Stream input;
/**
* The result of parsing [text] according to [format]. Automatically
* populated in the constructor.
*/
num value;
/** The symbols used by our format. */
NumberSymbols get symbols => format.symbols;
/** Where we accumulate the normalized representation of the number. */
final StringBuffer _normalized = new StringBuffer();
/**
* Did we see something that indicates this is, or at least might be,
* a positive number.
*/
bool gotPositive = false;
/**
* 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;
/** Should we stop parsing before hitting the end of the string. */
bool done = false;
/** Have we already skipped over any required prefixes. */
bool prefixesSkipped = false;
/** If the number is percent or permill, what do we divide by at the end. */
int scale = 1;
String get _positivePrefix => format._positivePrefix;
String get _negativePrefix => format._negativePrefix;
String get _positiveSuffix => format._positiveSuffix;
String get _negativeSuffix => format._negativeSuffix;
int get _zero => format._zero;
int get _localeZero => format._localeZero;
/**
* Create a new [_NumberParser] on which we can call parse().
*/
_NumberParser(this.format, text) : this.text = text,
this.input = new _Stream(text) {
value = parse();
}
/**
* The strings we might replace with functions that return the replacement
* values. They are functions because we might need to check something
* in the context. Note that the ordering is important here. For example,
* [symbols.PERCENT] might be " %", and we must handle that before we
* look at an individual space.
*/
Map<String, Function> get replacements => _replacements == null ?
_replacements = _initializeReplacements() : _replacements;
var _replacements;
Map _initializeReplacements() => {
symbols.DECIMAL_SEP: () => '.',
symbols.EXP_SYMBOL: () => 'E',
symbols.GROUP_SEP: handleSpace,
symbols.PERCENT: () {
scale = _NumberFormatParser._PERCENT_SCALE;
return '';
},
symbols.PERMILL: () {
scale = _NumberFormatParser._PER_MILLE_SCALE;
return '';
},
' ' : handleSpace,
'\u00a0' : handleSpace,
'+': () => '+',
'-': () => '-',
};
invalidFormat() =>
throw new FormatException("Invalid number: ${input.contents}");
/**
* Replace a space in the number with the normalized form. If space is not
* a significant character (normally grouping) then it's just invalid. If it
* is the grouping character, then it's only valid if it's followed by a
* digit. e.g. '$12 345.00'
*/
handleSpace() =>
groupingIsNotASpaceOrElseItIsSpaceFollowedByADigit ? '' : invalidFormat();
/**
* Determine if a space is a valid character in the number. See [handleSpace].
*/
bool get groupingIsNotASpaceOrElseItIsSpaceFollowedByADigit {
if (symbols.GROUP_SEP != '\u00a0' || symbols.GROUP_SEP != ' ') return true;
var peeked = input.peek(symbols.GROUP_SEP.length + 1);
return asDigit(peeked[peeked.length - 1]) != null;
}
/**
* Turn [char] into a number representing a digit, or null if it doesn't
* represent a digit in this locale.
*/
int asDigit(String char) {
var charCode = char.codeUnitAt(0);
var digitValue = charCode - _localeZero;
if (digitValue >= 0 && digitValue < 10) {
return digitValue;
} else {
return null;
}
}
/**
* Check to see if the input begins with either the positive or negative
* prefixes. Set the [gotPositive] and [gotNegative] variables accordingly.
*/
void checkPrefixes({bool skip: false}) {
bool checkPrefix(String prefix, skip) {
var matched = prefix.isNotEmpty && input.startsWith(prefix);
if (skip && matched) input.read(prefix.length);
return matched;
}
// TODO(alanknight): There's a faint possibility of a bug here where
// a positive prefix is followed by a negative prefix that's also a valid
// part of the number, but that seems very unlikely.
if (checkPrefix(_positivePrefix, skip)) gotPositive = true;
if (checkPrefix(_negativePrefix, skip)) gotNegative = true;
// Copied from Closure. It doesn't seem to be necessary to pass the test
// suite, so I'm not sure it's really needed.
if (gotPositive && gotNegative) {
if (_positivePrefix.length > _negativePrefix.length) {
gotNegative = false;
} else if (_negativePrefix.length > _positivePrefix.length) {
gotPositive = false;
}
}
}
/**
* If the rest of our input is either the positive or negative suffix,
* set [gotPositiveSuffix] or [gotNegativeSuffix] accordingly.
*/
void checkSuffixes() {
var remainder = input.rest();
if (remainder == _positiveSuffix) gotPositiveSuffix = true;
if (remainder == _negativeSuffix) gotNegativeSuffix = true;
}
/**
* We've encountered a character that's not a digit. Go through our
* replacement rules looking for how to handle it. If we see something
* that's not a digit and doesn't have a replacement, then we're done
* and the number is probably invalid.
*/
void processNonDigit() {
for (var key in replacements.keys) {
if (input.startsWith(key)) {
_normalized.write(replacements[key]());
input.read(key.length);
return;
}
}
// It might just be a prefix that we haven't skipped. We don't want to
// skip them initially because they might also be semantically meaningful,
// e.g. leading %. So we allow them through the loop, but only once.
if (input.index == 0 && !prefixesSkipped) {
prefixesSkipped = true;
checkPrefixes(skip: true);
} else {
done = true;
}
}
/**
* Parse [text] and return the resulting number. Throws [FormatException]
* if we can't parse it.
*/
num parse() {
if (text == symbols.NAN) return double.NAN;
if (text == "$_positivePrefix${symbols.INFINITY}$_positiveSuffix") {
return double.INFINITY;
}
if (text == "$_negativePrefix${symbols.INFINITY}$_negativeSuffix") {
return double.NEGATIVE_INFINITY;
}
checkPrefixes();
var parsed = parseNumber(input);
if (gotPositive && !gotPositiveSuffix) invalidNumber();
if (gotNegative && !gotNegativeSuffix) invalidNumber();
if (!input.atEnd()) invalidNumber();
return parsed;
}
/** The number is invalid, throw a [FormatException]. */
void invalidNumber() =>
throw new FormatException("Invalid Number: ${input.contents}");
/**
* Parse the number portion of the input, i.e. not any prefixes or suffixes,
* and assuming NaN and Infinity are already handled.
*/
num parseNumber(_Stream input) {
while (!done && !input.atEnd()) {
int digit = asDigit(input.peek());
if (digit != null) {
_normalized.writeCharCode(_zero + digit);
input.next();
} else {
processNonDigit();
}
checkSuffixes();
}
var normalizedText = _normalized.toString();
num parsed = int.parse(normalizedText, onError: (message) => null);
if (parsed == null) parsed = double.parse(normalizedText);
return parsed / scale;
}
}
/**
* 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 _PER_MILLE_SCALE = 1000;
static const _PATTERN_PERCENT = '%';
static const _PERCENT_SCALE = 100;
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;
/** We can be passed a specific currency symbol, regardless of the locale. */
String currencyName;
/**
* Create a new [_NumberFormatParser] for a particular [NumberFormat] and
* [input] pattern.
*/
_NumberFormatParser(this.format, input, this.currencyName) :
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 _iterable(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._negativePrefix + format._positivePrefix;
format._negativeSuffix = format._positiveSuffix + format._negativeSuffix;
}
}
/**
* 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;
while (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) {
if (pattern.peek == _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(currencyName);
break;
case _PATTERN_PERCENT:
if (format._multiplier != 1 && format._multiplier != _PERCENT_SCALE) {
throw new FormatException('Too many percent/permill');
}
format._multiplier = _PERCENT_SCALE;
affix.write(symbols.PERCENT);
break;
case _PATTERN_PER_MILLE:
if (format._multiplier != 1 &&
format._multiplier != _PER_MILLE_SCALE) {
throw new FormatException('Too many percent/permill');
}
format._multiplier = _PER_MILLE_SCALE;
affix.write(symbols.PERMILL);
break;
default:
affix.write(ch);
}
}
return true;
}
/** Variables used in [_parseTrunk] and [parseTrunkCharacter]. */
var decimalPos = -1;
var digitLeftCount = 0;
var zeroDigitCount = 0;
var digitRightCount = 0;
var groupingCount = -1;
/**
* Parse the "trunk" portion of the pattern, the piece that doesn't include
* positive or negative prefixes or suffixes.
*/
String _parseTrunk() {
var loop = true;
var trunk = new StringBuffer();
while (pattern.current != null && loop) {
loop = parseTrunkCharacter(trunk);
}
if (zeroDigitCount == 0 && digitLeftCount > 0 && decimalPos >= 0) {
// Handle '###.###' and '###.' and '.###'
// Handle '.###'
var n = decimalPos == 0 ? 1 : decimalPos;
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._finalGroupingSize = max(0, groupingCount);
if (!format._groupingSizeSetExplicitly) {
format._groupingSize = format._finalGroupingSize;
}
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(trunk) {
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:
if (groupingCount > 0) {
format._groupingSizeSetExplicitly = true;
format._groupingSize = groupingCount;
}
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;
}
}
/**
* Returns an [Iterable] on the string as a list of substrings.
*/
Iterable _iterable(String s) => new _StringIterable(s);
/**
* Return an iterator on the string as a list of substrings.
*/
Iterator _iterator(String s) => new _StringIterator(s);
// TODO(nweiz): remove this when issue 3780 is fixed.
/**
* Provides an Iterable that wraps [_iterator] so it can be used in a `for`
* loop.
*/
class _StringIterable extends IterableBase<String> {
final Iterator<String> iterator;
_StringIterable(String s) : iterator = _iterator(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> {
final String input;
int nextIndex = 0;
String _current = null;
_StringIterator(input) : input = _validate(input);
String get current => _current;
bool moveNext() {
if (nextIndex >= input.length) {
_current = null;
return false;
}
_current = input[nextIndex++];
return true;
}
String get peek => nextIndex >= input.length ? null : input[nextIndex];
Iterator<String> get iterator => this;
static String _validate(input) {
if (input is! String) throw new ArgumentError(input);
return input;
}
}