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

Side by Side 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: Don't always initialize _defaultLocale 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 unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « pkg/intl/lib/intl.dart ('k') | pkg/intl/lib/number_symbols.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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 }
OLDNEW
« 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