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; |
+} |