Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library number_format; | 5 part of intl; |
| 6 | 6 /** |
| 7 import 'dart:math'; | 7 * Provides the ability to format a number in a locale-specific way. The |
| 8 | 8 * format is specified as a pattern using a subset of the ICU formatting |
| 9 import "intl.dart"; | 9 * patterns. |
| 10 import "number_symbols.dart"; | 10 * |
| 11 import "number_symbols_data.dart"; | 11 * 0 - A single digit |
| 12 | 12 * # - A single digit, omitted if the value is zero |
| 13 * . - Decimal separator | |
| 14 * - - Minus sign | |
| 15 * , - Grouping separator | |
| 16 * E - Separates mantissa and expontent | |
| 17 * + - Before an exponent, indicates it should be prefixed with a plus sign. | |
| 18 * % - In prefix or suffix, multiply by 100 and show as percentage | |
| 19 * \u2030 - In prefix or suffix, multiply by 1000 and show as per mille | |
| 20 * \u00A4 - Currency sign, replaced by currency name | |
| 21 * ' - Used to quote special characters | |
| 22 * ; - Used to separate the positive and negative patterns if both are present | |
| 23 * | |
| 24 * For example, | |
| 25 * var f = new NumberFormat("###.0#", "en_US"); | |
| 26 * print(f.format(12.345)); | |
| 27 * ==> 12.34 | |
| 28 * If the locale is not specified, it will default to the current locale. If | |
| 29 * the format is not specified it will print in a basic format with at least | |
| 30 * one integer digit and three fraction digits. | |
| 31 * | |
| 32 * There are also standard patterns available via the special constructors. e.g. | |
| 33 * var symbols = new NumberFormat.PERCENT_FORMAT("ar"); | |
| 34 * There are four such constructors: DECIMAL_FORMAT, PERCENT_FORMAT, | |
| 35 * SCIENTIFIC_FORMAT and CURRENCY_FORMAT. However, at the moment, | |
| 36 * SCIENTIFIC_FORMAT prints only as equivalent to "#E0" and does not take | |
| 37 * into account significant digits. CURRENCY_FORMAT will always use the name | |
| 38 * of the currency rather than the symbol. | |
| 39 */ | |
| 13 class NumberFormat { | 40 class NumberFormat { |
| 14 /** Variables to determine how number printing behaves. */ | 41 /** Variables to determine how number printing behaves. */ |
| 15 // TODO(alanknight): If these remain as variables and are set based on the | 42 // TODO(alanknight): If these remain as variables and are set based on the |
| 16 // pattern, can we make them final? | 43 // pattern, can we make them final? |
| 17 String _negativePrefix = '-'; | 44 String _negativePrefix = '-'; |
| 18 String _positivePrefix = ''; | 45 String _positivePrefix = ''; |
| 19 String _negativeSuffix = ''; | 46 String _negativeSuffix = ''; |
| 20 String _positiveSuffix = ''; | 47 String _positiveSuffix = ''; |
| 21 /** How many numbers in a group when using punctuation to group digits in | 48 /** |
| 49 * How many numbers in a group when using punctuation to group digits in | |
| 22 * large numbers. e.g. in en_US: "1,000,000" has a grouping size of 3 digits | 50 * large numbers. e.g. in en_US: "1,000,000" has a grouping size of 3 digits |
| 23 * between commas. | 51 * between commas. |
| 24 */ | 52 */ |
| 25 int _groupingSize = 3; | 53 int _groupingSize = 3; |
| 26 bool _decimalSeparatorAlwaysShown = false; | 54 bool _decimalSeparatorAlwaysShown = false; |
| 55 bool _useSignForPositiveExponent = false; | |
| 27 bool _useExponentialNotation = false; | 56 bool _useExponentialNotation = false; |
| 57 | |
| 28 int _maximumIntegerDigits = 40; | 58 int _maximumIntegerDigits = 40; |
| 29 int _minimumIntegerDigits = 1; | 59 int _minimumIntegerDigits = 1; |
| 30 int _maximumFractionDigits = 3; // invariant, >= minFractionDigits | 60 int _maximumFractionDigits = 3; |
| 31 int _minimumFractionDigits = 0; | 61 int _minimumFractionDigits = 0; |
| 32 int _minimumExponentDigits = 0; | 62 int _minimumExponentDigits = 0; |
| 33 bool _useSignForPositiveExponent = false; | 63 |
| 64 int _multiplier = 1; | |
| 65 | |
| 66 /** | |
| 67 * Stores the pattern used to create this format. This isn't used, but | |
| 68 * is helpful in debugging. | |
| 69 */ | |
| 70 String _pattern; | |
| 71 /** | |
| 72 * Set the maximum digits printed to the left of the decimal point. | |
| 73 * Normally this is computed from the pattern, but it's exposed here for | |
| 74 * testing purposes and for rare cases where you want to force it explicitly. | |
| 75 */ | |
| 76 void setMaximumIntegerDigits(int max) { | |
| 77 _maximumIntegerDigits = max; | |
| 78 } | |
| 79 | |
| 80 /** | |
| 81 * Set the minimum number of digits printed to the left of the decimal point. | |
| 82 * Normally this is computed from the pattern, but it's exposed here for | |
| 83 * testing purposes and for rare cases where you want to force it explicitly. | |
| 84 */ | |
| 85 void setMinimumIntegerDigits(int min) { | |
| 86 _minimumIntegerDigits = min; | |
| 87 } | |
| 88 | |
| 89 /** | |
| 90 * Set the maximum number of digits printed to the right of the decimal point. | |
| 91 * Normally this is computed from the pattern, but it's exposed here for | |
| 92 * testing purposes and for rare cases where you want to force it explicitly. | |
| 93 */ | |
| 94 void setMaximumFractionDigits(int max) { | |
| 95 _maximumFractionDigits = max; | |
| 96 } | |
| 97 | |
| 98 /** | |
| 99 * Set the minimum digits printed to the left of the decimal point. | |
| 100 * Normally this is computed from the pattern, but it's exposed here for | |
| 101 * testing purposes and for rare cases where you want to force it explicitly. | |
| 102 */ | |
| 103 void setMinimumFractionDigits(int max) { | |
| 104 _minimumFractionDigits = max; | |
| 105 } | |
| 34 | 106 |
| 35 /** The locale in which we print numbers. */ | 107 /** The locale in which we print numbers. */ |
| 36 final String _locale; | 108 final String _locale; |
| 37 | 109 |
| 38 /** Caches the symbols used for our locale. */ | 110 /** Caches the symbols used for our locale. */ |
| 39 NumberSymbols _symbols; | 111 NumberSymbols _symbols; |
| 40 | 112 |
| 41 /** | 113 /** |
| 42 * Transient internal state in which to build up the result of the format | 114 * Transient internal state in which to build up the result of the format |
| 43 * operation. We can have this be just an instance variable because Dart is | 115 * operation. We can have this be just an instance variable because Dart is |
| 44 * single-threaded and unless we do an asynchronous operation in the process | 116 * single-threaded and unless we do an asynchronous operation in the process |
| 45 * of formatting then there will only ever be one number being formatted | 117 * of formatting then there will only ever be one number being formatted |
| 46 * at a time. In languages with threads we'd need to pass this on the stack. | 118 * at a time. In languages with threads we'd need to pass this on the stack. |
| 47 */ | 119 */ |
| 48 StringBuffer _buffer; | 120 StringBuffer _buffer; |
| 49 | 121 |
| 50 /** | 122 /** |
| 51 * Create a number format that prints in [newPattern] as it applies in | 123 * Create a number format that prints using [newPattern] as it applies in |
| 52 * [locale]. | 124 * [locale]. |
| 53 */ | 125 */ |
| 54 NumberFormat([String newPattern, String locale]): | 126 factory NumberFormat([String newPattern, String locale]) { |
| 55 _locale = Intl.verifiedLocale(locale, localeExists) { | 127 return new NumberFormat._forPattern(locale, (x) => newPattern); |
| 56 // TODO(alanknight): There will need to be some kind of async setup | 128 } |
| 57 // operations so as not to bring along every locale in every program. | 129 |
| 130 /** Create a number format that prints as DECIMAL_PATTERN. */ | |
| 131 NumberFormat.DECIMAL_PATTERN([String locale]) : | |
|
Emily Fortuna
2013/04/09 19:39:15
personal preference: I'd prefer the name of these
| |
| 132 this._forPattern(locale, (x) => x.DECIMAL_PATTERN); | |
| 133 | |
| 134 /** Create a number format that prints as PERCENT_PATTERN. */ | |
| 135 NumberFormat.PERCENT_PATTERN([String locale]) : | |
| 136 this._forPattern(locale, (x) => x.PERCENT_PATTERN); | |
| 137 | |
| 138 /** Create a number format that prints as SCIENTIFIC_PATTERN. */ | |
| 139 NumberFormat.SCIENTIFIC_PATTERN([String locale]) : | |
| 140 this._forPattern(locale, (x) => x.SCIENTIFIC_PATTERN); | |
| 141 | |
| 142 /** Create a number format that prints as CURRENCY_PATTERN. */ | |
| 143 NumberFormat.CURRENCY_PATTERN([String locale]) : | |
| 144 this._forPattern(locale, (x) => x.CURRENCY_PATTERN); | |
| 145 | |
| 146 /** | |
| 147 * Create a number format that prints in a pattern we get from | |
| 148 * the [getPattern] function using the locale [locale]. | |
| 149 */ | |
| 150 NumberFormat._forPattern(String locale, Function getPattern) : | |
| 151 _locale = Intl.verifiedLocale(locale, localeExists) { | |
| 58 _symbols = numberFormatSymbols[_locale]; | 152 _symbols = numberFormatSymbols[_locale]; |
| 59 _setPattern(newPattern); | 153 _setPattern(getPattern(_symbols)); |
| 60 } | 154 } |
| 61 | 155 |
| 62 /** | 156 /** |
| 63 * Return the locale code in which we operate, e.g. 'en_US' or 'pt'. | 157 * Return the locale code in which we operate, e.g. 'en_US' or 'pt'. |
| 64 */ | 158 */ |
| 65 String get locale => _locale; | 159 String get locale => _locale; |
| 66 | 160 |
| 67 /** | 161 /** |
| 68 * Return true if the locale exists, or if it is null. The null case | 162 * Return true if the locale exists, or if it is null. The null case |
| 69 * is interpreted to mean that we use the default locale. | 163 * is interpreted to mean that we use the default locale. |
| 70 */ | 164 */ |
| 71 static bool localeExists(localeName) { | 165 static bool localeExists(localeName) { |
| 72 if (localeName == null) return false; | 166 if (localeName == null) return false; |
| 73 return numberFormatSymbols.containsKey(localeName); | 167 return numberFormatSymbols.containsKey(localeName); |
| 74 } | 168 } |
| 75 | 169 |
| 76 /** | 170 /** |
| 77 * Return the symbols which are used in our locale. Cache them to avoid | 171 * Return the symbols which are used in our locale. Cache them to avoid |
| 78 * repeated lookup. | 172 * repeated lookup. |
| 79 */ | 173 */ |
| 80 NumberSymbols get symbols { | 174 NumberSymbols get symbols { |
| 81 return _symbols; | 175 return _symbols; |
| 82 } | 176 } |
| 83 | 177 |
| 84 // TODO(alanknight): Actually use the pattern and locale. | |
| 85 _setPattern(String x) {} | |
| 86 | |
| 87 /** | 178 /** |
| 88 * Format [number] according to our pattern and return the formatted string. | 179 * Format [number] according to our pattern and return the formatted string. |
| 89 */ | 180 */ |
| 90 String format(num number) { | 181 String format(num number) { |
| 91 // TODO(alanknight): Do we have to do anything for printing numbers bidi? | 182 // TODO(alanknight): Do we have to do anything for printing numbers bidi? |
| 92 // Or are they always printed left to right? | 183 // Or are they always printed left to right? |
| 93 if (number.isNaN) return symbols.NAN; | 184 if (number.isNaN) return symbols.NAN; |
| 94 if (number.isInfinite) return "${_signPrefix(number)}${symbols.INFINITY}"; | 185 if (number.isInfinite) return "${_signPrefix(number)}${symbols.INFINITY}"; |
| 95 | 186 |
| 96 _newBuffer(); | 187 _newBuffer(); |
| 97 _add(_signPrefix(number)); | 188 _add(_signPrefix(number)); |
| 98 _formatNumber(number.abs()); | 189 _formatNumber(number.abs() * _multiplier); |
| 99 _add(_signSuffix(number)); | 190 _add(_signSuffix(number)); |
| 100 | 191 |
| 101 var result = _buffer.toString(); | 192 var result = _buffer.toString(); |
| 102 _buffer = null; | 193 _buffer = null; |
| 103 return result; | 194 return result; |
| 104 } | 195 } |
| 105 | 196 |
| 106 /** | 197 /** |
| 107 * Format the main part of the number in the form dictated by the pattern. | 198 * Format the main part of the number in the form dictated by the pattern. |
| 108 */ | 199 */ |
| 109 void _formatNumber(num number) { | 200 void _formatNumber(num number) { |
| 110 if (_useExponentialNotation) { | 201 if (_useExponentialNotation) { |
| 111 _formatExponential(number); | 202 _formatExponential(number); |
| 112 } else { | 203 } else { |
| 113 _formatFixed(number); | 204 _formatFixed(number); |
| 114 } | 205 } |
| 115 } | 206 } |
| 116 | 207 |
| 117 /** Format the number in exponential notation. */ | 208 /** Format the number in exponential notation. */ |
| 118 _formatExponential(num number) { | 209 void _formatExponential(num number) { |
| 119 if (number == 0.0) { | 210 if (number == 0.0) { |
| 120 _formatFixed(number); | 211 _formatFixed(number); |
| 121 _formatExponent(0); | 212 _formatExponent(0); |
| 122 return; | 213 return; |
| 123 } | 214 } |
| 124 | 215 |
| 125 var exponent = (log(number) / log(10)).floor(); | 216 var exponent = (log(number) / log(10)).floor(); |
| 126 var mantissa = number / pow(10, exponent); | 217 var mantissa = number / pow(10.0, exponent); |
| 127 | 218 |
| 128 if (_minimumIntegerDigits < 1) { | 219 var minIntDigits = _minimumIntegerDigits; |
| 129 exponent++; | 220 if (_maximumIntegerDigits > 1 && |
| 130 mantissa /= 10; | 221 _maximumIntegerDigits > _minimumIntegerDigits) { |
| 222 // A repeating range is defined; adjust to it as follows. | |
| 223 // If repeat == 3, we have 6,5,4=>3; 3,2,1=>0; 0,-1,-2=>-3; | |
| 224 // -3,-4,-5=>-6, etc. This takes into account that the | |
| 225 // exponent we have here is off by one from what we expect; | |
| 226 // it is for the format 0.MMMMMx10^n. | |
| 227 while ((exponent % _maximumIntegerDigits) != 0) { | |
| 228 mantissa *= 10; | |
| 229 exponent--; | |
| 230 } | |
| 231 minIntDigits = 1; | |
| 131 } else { | 232 } else { |
| 132 exponent -= _minimumIntegerDigits - 1; | 233 // No repeating range is defined, use minimum integer digits. |
| 133 mantissa *= pow(10, _minimumIntegerDigits - 1); | 234 if (_minimumIntegerDigits < 1) { |
| 235 exponent++; | |
| 236 mantissa /= 10; | |
| 237 } else { | |
| 238 exponent -= _minimumIntegerDigits - 1; | |
| 239 mantissa *= pow(10, _minimumIntegerDigits - 1); | |
| 240 } | |
| 134 } | 241 } |
| 135 _formatFixed(number); | 242 _formatFixed(mantissa); |
| 136 _formatExponent(exponent); | 243 _formatExponent(exponent); |
| 137 } | 244 } |
| 138 | 245 |
| 139 /** | 246 /** |
| 140 * Format the exponent portion, e.g. in "1.3e-5" the "e-5". | 247 * Format the exponent portion, e.g. in "1.3e-5" the "e-5". |
| 141 */ | 248 */ |
| 142 void _formatExponent(num exponent) { | 249 void _formatExponent(num exponent) { |
| 143 _add(symbols.EXP_SYMBOL); | 250 _add(symbols.EXP_SYMBOL); |
| 144 if (exponent < 0) { | 251 if (exponent < 0) { |
| 145 exponent = -exponent; | 252 exponent = -exponent; |
| 146 _add(symbols.MINUS_SIGN); | 253 _add(symbols.MINUS_SIGN); |
| 147 } else if (_useSignForPositiveExponent) { | 254 } else if (_useSignForPositiveExponent) { |
| 148 _add(symbols.PLUS_SIGN); | 255 _add(symbols.PLUS_SIGN); |
| 149 } | 256 } |
| 150 _pad(_minimumExponentDigits, exponent.toString()); | 257 _pad(_minimumExponentDigits, exponent.toString()); |
| 151 } | 258 } |
| 152 | 259 |
| 260 /** Used to test if we have exceeded Javascript integer limits. */ | |
| 261 final _maxInt = pow(2, 52); | |
| 262 | |
| 153 /** | 263 /** |
| 154 * Format the basic number portion, inluding the fractional digits. | 264 * Format the basic number portion, inluding the fractional digits. |
| 155 */ | 265 */ |
| 156 void _formatFixed(num number) { | 266 void _formatFixed(num number) { |
| 157 // Round the number. | 267 // Very fussy math to get integer and fractional parts. |
| 158 var power = pow(10, _maximumFractionDigits); | 268 var power = pow(10, _maximumFractionDigits); |
| 159 var intValue = number.truncate(); | 269 var shiftedNumber = (number * power); |
| 160 var multiplied = (number * power).round(); | 270 // We must not roundToDouble() an int or it will lose precision. We must not |
| 161 var fracValue = (multiplied - intValue * power).floor(); | 271 // round() a large double or it will take its loss of precision and |
| 272 // preserve it in an int, which we will then print to the right | |
| 273 // of the decimal place. Therefore, only roundToDouble if we are already | |
| 274 // a double. | |
| 275 if (shiftedNumber is double) { | |
| 276 shiftedNumber = shiftedNumber.roundToDouble(); | |
| 277 } | |
| 278 var intValue, fracValue; | |
| 279 if (shiftedNumber.isInfinite) { | |
| 280 intValue = number.toInt(); | |
| 281 fracValue = 0; | |
| 282 } else { | |
| 283 intValue = shiftedNumber.round() ~/ power; | |
| 284 fracValue = (shiftedNumber - intValue * power).floor(); | |
| 285 } | |
| 162 var fractionPresent = _minimumFractionDigits > 0 || fracValue > 0; | 286 var fractionPresent = _minimumFractionDigits > 0 || fracValue > 0; |
| 163 | 287 |
| 164 // On dartj2s the integer part may be large enough to be a floating | 288 // If the int part is larger than 2^52 and we're on Javascript (so it's |
| 165 // point value, in which case we reduce it until it is small enough | 289 // really a float) it will lose precision, so pad out the rest of it |
| 166 // to be printed as an integer and pad the remainder with zeros. | 290 // with zeros. Check for Javascript by seeing if an integer is double. |
| 167 var paddingDigits = new StringBuffer(); | 291 var paddingDigits = new StringBuffer(); |
| 168 while ((intValue & 0x7fffffff) != intValue) { | 292 if (1 is double && intValue > _maxInt) { |
| 169 paddingDigits.write(symbols.ZERO_DIGIT); | 293 var howManyDigitsTooBig = (log(intValue) / LN10).ceil() - 16; |
| 170 intValue = intValue ~/ 10; | 294 var divisor = pow(10, howManyDigitsTooBig).round(); |
| 295 for (var each in new List(howManyDigitsTooBig.toInt())) { | |
| 296 paddingDigits.write(symbols.ZERO_DIGIT); | |
| 297 } | |
| 298 intValue = (intValue / divisor).truncate(); | |
| 171 } | 299 } |
| 172 var integerDigits = "${intValue}${paddingDigits}".codeUnits; | 300 var integerDigits = "${intValue}${paddingDigits}".codeUnits; |
| 173 var digitLength = integerDigits.length; | 301 var digitLength = integerDigits.length; |
| 174 | 302 |
| 175 if (_hasPrintableIntegerPart(intValue)) { | 303 if (_hasPrintableIntegerPart(intValue)) { |
| 176 _pad(_minimumIntegerDigits - digitLength); | 304 _pad(_minimumIntegerDigits - digitLength); |
| 177 for (var i = 0; i < digitLength; i++) { | 305 for (var i = 0; i < digitLength; i++) { |
| 178 _addDigit(integerDigits[i]); | 306 _addDigit(integerDigits[i]); |
| 179 _group(digitLength, i); | 307 _group(digitLength, i); |
| 180 } | 308 } |
| 181 } else if (!fractionPresent) { | 309 } else if (!fractionPresent) { |
| 182 // If neither fraction nor integer part exists, just print zero. | 310 // If neither fraction nor integer part exists, just print zero. |
| 183 _addZero(); | 311 _addZero(); |
| 184 } | 312 } |
| 185 | 313 |
| 186 _decimalSeparator(fractionPresent); | 314 _decimalSeparator(fractionPresent); |
| 187 _formatFractionPart((fracValue + power).toString()); | 315 _formatFractionPart((fracValue + power).toString()); |
| 188 } | 316 } |
| 189 | 317 |
| 190 /** | 318 /** |
| 191 * Format the part after the decimal place in a fixed point number. | 319 * Format the part after the decimal place in a fixed point number. |
| 192 */ | 320 */ |
| 193 void _formatFractionPart(String fractionPart) { | 321 void _formatFractionPart(String fractionPart) { |
| 194 var fractionCodes = fractionPart.codeUnits; | 322 var fractionCodes = fractionPart.codeUnits; |
| 195 var fractionLength = fractionPart.length; | 323 var fractionLength = fractionPart.length; |
| 196 while (fractionPart[fractionLength - 1] == '0' && | 324 while(fractionCodes[fractionLength - 1] == _zero && |
| 197 fractionLength > _minimumFractionDigits + 1) { | 325 fractionLength > _minimumFractionDigits + 1) { |
| 198 fractionLength--; | 326 fractionLength--; |
| 199 } | 327 } |
| 200 for (var i = 1; i < fractionLength; i++) { | 328 for (var i = 1; i < fractionLength; i++) { |
| 201 _addDigit(fractionCodes[i]); | 329 _addDigit(fractionCodes[i]); |
| 202 } | 330 } |
| 203 } | 331 } |
| 204 | 332 |
| 205 /** Print the decimal separator if appropriate. */ | 333 /** Print the decimal separator if appropriate. */ |
| 206 void _decimalSeparator(bool fractionPresent) { | 334 void _decimalSeparator(bool fractionPresent) { |
| (...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 251 */ | 379 */ |
| 252 void _group(int totalLength, int position) { | 380 void _group(int totalLength, int position) { |
| 253 var distanceFromEnd = totalLength - position; | 381 var distanceFromEnd = totalLength - position; |
| 254 if (distanceFromEnd <= 1 || _groupingSize <= 0) return; | 382 if (distanceFromEnd <= 1 || _groupingSize <= 0) return; |
| 255 if (distanceFromEnd % _groupingSize == 1) { | 383 if (distanceFromEnd % _groupingSize == 1) { |
| 256 _add(symbols.GROUP_SEP); | 384 _add(symbols.GROUP_SEP); |
| 257 } | 385 } |
| 258 } | 386 } |
| 259 | 387 |
| 260 /** Returns the code point for the character '0'. */ | 388 /** Returns the code point for the character '0'. */ |
| 261 int get _zero => '0'.codeUnits.first; | 389 final _zero = '0'.codeUnits.first; |
| 262 | 390 |
| 263 /** Returns the code point for the locale's zero digit. */ | 391 /** Returns the code point for the locale's zero digit. */ |
| 264 // Note that there is a slight risk of a locale's zero digit not fitting | 392 // Note that there is a slight risk of a locale's zero digit not fitting |
| 265 // into a single code unit, but it seems very unlikely, and if it did, | 393 // into a single code unit, but it seems very unlikely, and if it did, |
| 266 // there's a pretty good chance that our assumptions about being able to do | 394 // there's a pretty good chance that our assumptions about being able to do |
| 267 // arithmetic on it would also be invalid. | 395 // arithmetic on it would also be invalid. |
| 268 int get _localeZero => symbols.ZERO_DIGIT.codeUnits.first; | 396 get _localeZero => symbols.ZERO_DIGIT.codeUnits.first; |
| 269 | 397 |
| 270 /** | 398 /** |
| 271 * Returns the prefix for [x] based on whether it's positive or negative. | 399 * Returns the prefix for [x] based on whether it's positive or negative. |
| 272 * In en_US this would be '' and '-' respectively. | 400 * In en_US this would be '' and '-' respectively. |
| 273 */ | 401 */ |
| 274 String _signPrefix(num x) { | 402 String _signPrefix(num x) { |
| 275 return x.isNegative ? _negativePrefix : _positivePrefix; | 403 return x.isNegative ? _negativePrefix : _positivePrefix; |
| 276 } | 404 } |
| 277 | 405 |
| 278 /** | 406 /** |
| 279 * Returns the suffix for [x] based on wether it's positive or negative. | 407 * Returns the suffix for [x] based on wether it's positive or negative. |
| 280 * In en_US there are no suffixes for positive or negative. | 408 * In en_US there are no suffixes for positive or negative. |
| 281 */ | 409 */ |
| 282 String _signSuffix(num x) { | 410 String _signSuffix(num x) { |
| 283 return x.isNegative ? _negativeSuffix : _positiveSuffix; | 411 return x.isNegative ? _negativeSuffix : _positiveSuffix; |
| 284 } | 412 } |
| 413 | |
| 414 void _setPattern(String newPattern) { | |
| 415 if (newPattern == null) return; | |
| 416 // Make spaces non-breaking | |
| 417 _pattern = newPattern.replaceAll(' ', '\u00a0'); | |
| 418 var parser = new _NumberFormatParser(this, newPattern); | |
| 419 parser.parse(); | |
| 420 } | |
| 421 | |
| 422 String toString() => "NumberFormat($_locale, $_pattern)"; | |
| 285 } | 423 } |
| 424 | |
| 425 /** | |
| 426 * Private class that parses the numeric formatting pattern and sets the | |
| 427 * variables in [format] to appropriate values. Instances of this are | |
| 428 * transient and store parsing state in instance variables, so can only be used | |
| 429 * to parse a single pattern. | |
| 430 */ | |
| 431 class _NumberFormatParser { | |
| 432 | |
| 433 /** | |
| 434 * The special characters in the pattern language. All others are treated | |
| 435 * as literals. | |
| 436 */ | |
| 437 static const _PATTERN_SEPARATOR = ';'; | |
| 438 static const _QUOTE = "'"; | |
| 439 static const _PATTERN_DIGIT = '#'; | |
| 440 static const _PATTERN_ZERO_DIGIT = '0'; | |
| 441 static const _PATTERN_GROUPING_SEPARATOR = ','; | |
| 442 static const _PATTERN_DECIMAL_SEPARATOR = '.'; | |
| 443 static const _PATTERN_CURRENCY_SIGN = '\u00A4'; | |
| 444 static const _PATTERN_PER_MILLE = '\u2030'; | |
| 445 static const _PATTERN_PERCENT = '%'; | |
| 446 static const _PATTERN_EXPONENT = 'E'; | |
| 447 static const _PATTERN_PLUS = '+'; | |
| 448 | |
| 449 /** The format whose state we are setting. */ | |
| 450 final NumberFormat format; | |
| 451 | |
| 452 /** The pattern we are parsing. */ | |
| 453 final _StringIterator pattern; | |
| 454 | |
| 455 /** | |
| 456 * Create a new [_NumberFormatParser] for a particular [NumberFormat] and | |
| 457 * [input] pattern. | |
| 458 */ | |
| 459 _NumberFormatParser(this.format, input) : pattern = _iterator(input) { | |
| 460 pattern.moveNext(); | |
| 461 } | |
| 462 | |
| 463 /** The [NumberSymbols] for the locale in which our [format] prints. */ | |
| 464 NumberSymbols get symbols => format.symbols; | |
| 465 | |
| 466 /** Parse the input pattern and set the values. */ | |
| 467 void parse() { | |
| 468 format._positivePrefix = _parseAffix(); | |
| 469 var trunk = _parseTrunk(); | |
| 470 format._positiveSuffix = _parseAffix(); | |
| 471 // If we have separate positive and negative patterns, now parse the | |
| 472 // the negative version. | |
| 473 if (pattern.current == _NumberFormatParser._PATTERN_SEPARATOR) { | |
| 474 pattern.moveNext(); | |
| 475 format._negativePrefix = _parseAffix(); | |
| 476 // Skip over the negative trunk, verifying that it's identical to the | |
| 477 // positive trunk. | |
| 478 for (var each in _iterator(trunk)) { | |
| 479 if (pattern.current != each && pattern.current != null) { | |
| 480 throw new FormatException( | |
| 481 "Positive and negative trunks must be the same"); | |
| 482 } | |
| 483 pattern.moveNext(); | |
| 484 } | |
| 485 format._negativeSuffix = _parseAffix(); | |
| 486 } else { | |
| 487 // If no negative affix is specified, they share the same positive affix. | |
| 488 format._negativePrefix = format._positivePrefix + format._negativePrefix; | |
| 489 format._negativeSuffix = format._negativeSuffix + format._positiveSuffix; | |
| 490 } | |
| 491 } | |
| 492 | |
| 493 /** Variable used in parsing prefixes and suffixes to keep track of | |
| 494 * whether or not we are in a quoted region. */ | |
| 495 bool inQuote = false; | |
| 496 | |
| 497 /** | |
| 498 * Parse a prefix or suffix and return the prefix/suffix string. Note that | |
| 499 * this also may modify the state of [format]. | |
| 500 */ | |
| 501 String _parseAffix() { | |
| 502 var affix = new StringBuffer(); | |
| 503 inQuote = false; | |
| 504 var loop = true; | |
| 505 while (loop) { | |
| 506 loop = parseCharacterAffix(affix) && pattern.moveNext(); | |
| 507 } | |
| 508 return affix.toString(); | |
| 509 } | |
| 510 | |
| 511 /** | |
| 512 * Parse an individual character as part of a prefix or suffix. Return true | |
| 513 * if we should continue to look for more affix characters, and false if | |
| 514 * we have reached the end. | |
| 515 */ | |
| 516 bool parseCharacterAffix(StringBuffer affix) { | |
| 517 var ch = pattern.current; | |
| 518 if (ch == null) return false; | |
| 519 if (ch == _QUOTE) { | |
| 520 var nextChar = pattern.peek; | |
| 521 if (nextChar == _QUOTE) { | |
| 522 pattern.moveNext(); | |
| 523 affix.write(_QUOTE); // 'don''t' | |
| 524 } else { | |
| 525 inQuote = !inQuote; | |
| 526 } | |
| 527 return true; | |
| 528 } | |
| 529 | |
| 530 if (inQuote) { | |
| 531 affix.write(ch); | |
| 532 } else { | |
| 533 switch (ch) { | |
| 534 case _PATTERN_DIGIT: | |
| 535 case _PATTERN_ZERO_DIGIT: | |
| 536 case _PATTERN_GROUPING_SEPARATOR: | |
| 537 case _PATTERN_DECIMAL_SEPARATOR: | |
| 538 case _PATTERN_SEPARATOR: | |
| 539 return false; | |
| 540 case _PATTERN_CURRENCY_SIGN: | |
| 541 // TODO(alanknight): Handle the local/global/portable currency signs | |
| 542 affix.write(symbols.DEF_CURRENCY_CODE); | |
| 543 break; | |
| 544 case _PATTERN_PERCENT: | |
| 545 if (format._multiplier != 1) { | |
| 546 throw new FormatException('Too many percent/permill'); | |
|
Emily Fortuna
2013/04/09 19:39:15
nit: can't you make these error messages specific
Alan Knight
2013/04/09 19:58:27
I think I can't, because one of the error cases is
| |
| 547 } | |
| 548 format._multiplier = 100; | |
| 549 affix.write(symbols.PERCENT); | |
| 550 break; | |
| 551 case _PATTERN_PER_MILLE: | |
| 552 if (format._multiplier != 1) { | |
| 553 throw new FormatException('Too many percent/permill'); | |
| 554 } | |
| 555 format._multiplier = 1000; | |
| 556 affix.write(symbols.PERMILL); | |
| 557 break; | |
| 558 default: | |
| 559 affix.write(ch); | |
| 560 } | |
| 561 } | |
| 562 return true; | |
| 563 } | |
| 564 | |
| 565 /** Variables used in [parseTrunk] and [parseTrunkCharacter]. */ | |
| 566 var decimalPos; | |
| 567 var digitLeftCount; | |
| 568 var zeroDigitCount; | |
| 569 var digitRightCount; | |
| 570 var groupingCount; | |
| 571 var trunk; | |
| 572 | |
| 573 /** | |
| 574 * Parse the "trunk" portion of the pattern, the piece that doesn't include | |
| 575 * positive or negative prefixes or suffixes. | |
| 576 */ | |
| 577 String _parseTrunk() { | |
| 578 decimalPos = -1; | |
| 579 digitLeftCount = 0; | |
| 580 zeroDigitCount = 0; | |
| 581 digitRightCount = 0; | |
| 582 groupingCount = -1; | |
| 583 | |
| 584 var loop = true; | |
| 585 trunk = new StringBuffer(); | |
| 586 while (pattern.current != null && loop) { | |
| 587 loop = parseTrunkCharacter(); | |
| 588 } | |
| 589 | |
| 590 if (zeroDigitCount == 0 && digitLeftCount > 0 && decimalPos >= 0) { | |
| 591 // Handle '###.###' and '###.' and '.###' | |
| 592 var n = decimalPos; | |
| 593 if (n == 0) { // Handle '.###' | |
| 594 n++; | |
| 595 } | |
| 596 digitRightCount = digitLeftCount - n; | |
| 597 digitLeftCount = n - 1; | |
| 598 zeroDigitCount = 1; | |
| 599 } | |
| 600 | |
| 601 // Do syntax checking on the digits. | |
| 602 if (decimalPos < 0 && digitRightCount > 0 || | |
| 603 decimalPos >= 0 && (decimalPos < digitLeftCount || | |
| 604 decimalPos > digitLeftCount + zeroDigitCount) || | |
| 605 groupingCount == 0) { | |
| 606 throw new FormatException('Malformed pattern "${pattern.input}"'); | |
| 607 } | |
| 608 var totalDigits = digitLeftCount + zeroDigitCount + digitRightCount; | |
| 609 | |
| 610 format._maximumFractionDigits = | |
| 611 decimalPos >= 0 ? totalDigits - decimalPos : 0; | |
| 612 if (decimalPos >= 0) { | |
| 613 format._minimumFractionDigits = | |
| 614 digitLeftCount + zeroDigitCount - decimalPos; | |
| 615 if (format._minimumFractionDigits < 0) { | |
| 616 format._minimumFractionDigits = 0; | |
| 617 } | |
| 618 } | |
| 619 | |
| 620 // The effectiveDecimalPos is the position the decimal is at or would be at | |
| 621 // if there is no decimal. Note that if decimalPos<0, then digitTotalCount | |
| 622 // == digitLeftCount + zeroDigitCount. | |
| 623 var effectiveDecimalPos = decimalPos >= 0 ? decimalPos : totalDigits; | |
| 624 format._minimumIntegerDigits = effectiveDecimalPos - digitLeftCount; | |
| 625 if (format._useExponentialNotation) { | |
| 626 format._maximumIntegerDigits = | |
| 627 digitLeftCount + format._minimumIntegerDigits; | |
| 628 | |
| 629 // In exponential display, we need to at least show something. | |
| 630 if (format._maximumFractionDigits == 0 && | |
| 631 format._minimumIntegerDigits == 0) { | |
| 632 format._minimumIntegerDigits = 1; | |
| 633 } | |
| 634 } | |
| 635 | |
| 636 format._groupingSize = max(0, groupingCount); | |
| 637 format._decimalSeparatorAlwaysShown = decimalPos == 0 || | |
| 638 decimalPos == totalDigits; | |
| 639 | |
| 640 return trunk.toString(); | |
| 641 } | |
| 642 | |
| 643 /** | |
| 644 * Parse an individual character of the trunk. Return true if we should | |
| 645 * continue to look for additional trunk characters or false if we have | |
| 646 * reached the end. | |
| 647 */ | |
| 648 bool parseTrunkCharacter() { | |
| 649 var ch = pattern.current; | |
| 650 switch (ch) { | |
| 651 case _PATTERN_DIGIT: | |
| 652 if (zeroDigitCount > 0) { | |
| 653 digitRightCount++; | |
| 654 } else { | |
| 655 digitLeftCount++; | |
| 656 } | |
| 657 if (groupingCount >= 0 && decimalPos < 0) { | |
| 658 groupingCount++; | |
| 659 } | |
| 660 break; | |
| 661 case _PATTERN_ZERO_DIGIT: | |
| 662 if (digitRightCount > 0) { | |
| 663 throw new FormatException('Unexpected "0" in pattern "' | |
| 664 + pattern.input + '"'); | |
| 665 } | |
| 666 zeroDigitCount++; | |
| 667 if (groupingCount >= 0 && decimalPos < 0) { | |
| 668 groupingCount++; | |
| 669 } | |
| 670 break; | |
| 671 case _PATTERN_GROUPING_SEPARATOR: | |
| 672 groupingCount = 0; | |
| 673 break; | |
| 674 case _PATTERN_DECIMAL_SEPARATOR: | |
| 675 if (decimalPos >= 0) { | |
| 676 throw new FormatException( | |
| 677 'Multiple decimal separators in pattern "$pattern"'); | |
| 678 } | |
| 679 decimalPos = digitLeftCount + zeroDigitCount + digitRightCount; | |
| 680 break; | |
| 681 case _PATTERN_EXPONENT: | |
| 682 trunk.write(ch); | |
| 683 if (format._useExponentialNotation) { | |
| 684 throw new FormatException( | |
| 685 'Multiple exponential symbols in pattern "$pattern"'); | |
| 686 } | |
| 687 format._useExponentialNotation = true; | |
| 688 format._minimumExponentDigits = 0; | |
| 689 | |
| 690 // exponent pattern can have a optional '+'. | |
| 691 pattern.moveNext(); | |
| 692 var nextChar = pattern.current; | |
| 693 if (nextChar == _PATTERN_PLUS) { | |
| 694 trunk.write(pattern.current); | |
| 695 pattern.moveNext(); | |
| 696 format._useSignForPositiveExponent = true; | |
| 697 } | |
| 698 | |
| 699 // Use lookahead to parse out the exponential part | |
| 700 // of the pattern, then jump into phase 2. | |
| 701 while (pattern.current == _PATTERN_ZERO_DIGIT) { | |
| 702 trunk.write(pattern.current); | |
| 703 pattern.moveNext(); | |
| 704 format._minimumExponentDigits++; | |
| 705 } | |
| 706 | |
| 707 if ((digitLeftCount + zeroDigitCount) < 1 || | |
| 708 format._minimumExponentDigits < 1) { | |
| 709 throw new FormatException( | |
| 710 'Malformed exponential pattern "$pattern"'); | |
| 711 } | |
| 712 return false; | |
| 713 default: | |
| 714 return false; | |
| 715 } | |
| 716 trunk.write(ch); | |
| 717 pattern.moveNext(); | |
| 718 return true; | |
| 719 } | |
| 720 } | |
| 721 | |
| 722 /** | |
| 723 * Return an iterator on the string as a list of substrings. | |
| 724 */ | |
| 725 Iterator _iterator(String s) => new _StringIterator(s); | |
| 726 | |
| 727 /** | |
| 728 * Provides an iterator over a string as a list of substrings, and also | |
| 729 * gives us a lookahead of one via the [peek] method. | |
| 730 */ | |
| 731 class _StringIterator implements Iterator<String> { | |
| 732 String input; | |
| 733 var index = -1; | |
| 734 inBounds(i) => i >= 0 && i < input.length; | |
| 735 _StringIterator(this.input); | |
| 736 String get current => inBounds(index) ? input[index] : null; | |
| 737 | |
| 738 bool moveNext() => inBounds(++index); | |
| 739 String get peek => inBounds(index + 1) ? input[index + 1] : null; | |
| 740 Iterator<String> get iterator => this; | |
| 741 } | |
| OLD | NEW |