| /* |
| * Copyright 2014 Google Inc. All rights reserved. |
| * |
| * Use of this source code is governed by a BSD-style |
| * license that can be found in the LICENSE file or at |
| * https://developers.google.com/open-source/licenses/bsd |
| */ |
| part of charted.locale.format; |
| |
| /** |
| * The number formatter of a given locale. Applying the locale specific |
| * number format, number grouping and currency symbol, etc.. The format |
| * function in the NumberFormat class is used to format a number by the given |
| * specifier with the number properties of the locale. |
| */ |
| class NumberFormat { |
| // [[fill]align][sign][symbol][0][width][,][.precision][type] |
| static final RegExp FORMAT_REGEX = new RegExp( |
| r'(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?' |
| r'(\.-?\d+)?([a-z%])?', |
| caseSensitive: false); |
| |
| final String _localeDecimal; |
| final List<int> _localeGrouping; |
| final String _localeThousands; |
| final List<String> _localeCurrency; |
| |
| NumberFormat(Locale locale) |
| : _localeDecimal = locale.decimal, |
| _localeGrouping = locale.grouping, |
| _localeThousands = locale.thousands, |
| _localeCurrency = locale.currency; |
| |
| /** |
| * Returns a new format function with the given string specifier. A format |
| * function takes a number as the only argument, and returns a string |
| * representing the formatted number. The format specifier is modeled after |
| * Python 3.1's built-in format specification mini-language. The general form |
| * of a specifier is: |
| * [[fill]align][sign][symbol][0][width][,][.precision][type]. |
| * |
| * @see <a href="http://docs.python.org/release/3.1.3/library/string.html#formatspec">format specification mini-language</a> |
| */ |
| FormatFunction format(String specifier) { |
| Match match = FORMAT_REGEX.firstMatch(specifier); |
| var fill = match.group(1) != null ? match.group(1) : ' ', |
| align = match.group(2) != null ? match.group(2) : '>', |
| sign = match.group(3) != null ? match.group(3) : '', |
| symbol = match.group(4) != null ? match.group(4) : '', |
| zfill = match.group(5), |
| width = match.group(6) != null ? int.parse(match.group(6)) : 0, |
| comma = match.group(7) != null, |
| precision = match.group(8) != null |
| ? int.parse(match.group(8).substring(1)) |
| : null, |
| type = match.group(9), |
| scale = 1, |
| prefix = '', |
| suffix = '', |
| integer = false; |
| |
| if (zfill != null || fill == '0' && align == '=') { |
| zfill = fill = '0'; |
| align = '='; |
| if (comma) { |
| width -= ((width - 1) / 4).floor(); |
| } |
| } |
| |
| switch (type) { |
| case 'n': |
| comma = true; |
| type = 'g'; |
| break; |
| case '%': |
| scale = 100; |
| suffix = '%'; |
| type = 'f'; |
| break; |
| case 'p': |
| scale = 100; |
| suffix = '%'; |
| type = 'r'; |
| break; |
| case 'b': |
| case 'o': |
| case 'x': |
| case 'X': |
| if (symbol == '#') prefix = '0' + type.toLowerCase(); |
| break; |
| case 'c': |
| case 'd': |
| integer = true; |
| precision = 0; |
| break; |
| case 's': |
| scale = -1; |
| type = 'r'; |
| break; |
| } |
| |
| if (symbol == '\$') { |
| prefix = _localeCurrency[0]; |
| suffix = _localeCurrency[1]; |
| } |
| |
| // If no precision is specified for r, fallback to general notation. |
| if (type == 'r' && precision == null) { |
| type = 'g'; |
| } |
| |
| // Ensure that the requested precision is in the supported range. |
| if (precision != null) { |
| if (type == 'g') { |
| precision = math.max(1, math.min(21, precision)); |
| } else if (type == 'e' || type == 'f') { |
| precision = math.max(0, math.min(20, precision)); |
| } |
| } |
| |
| NumberFormatFunction formatFunction = _getFormatFunction(type); |
| |
| var zcomma = (zfill != null) && comma; |
| |
| return (dynamic _value) { |
| var value = _value as num; |
| if (value == null) return '-'; |
| var fullSuffix = suffix; |
| |
| // Return the empty string for floats formatted as ints. |
| if (integer && (value % 1) > 0) return ''; |
| |
| // Convert negative to positive, and record the sign prefix. |
| String negative; |
| if (value < 0 || value == 0 && 1 / value < 0) { |
| value = -value; |
| negative = '-'; |
| } else { |
| negative = sign; |
| } |
| |
| // Apply the scale, computing it from the value's exponent for si |
| // format. Preserve the existing suffix, if any, such as the |
| // currency symbol. |
| if (scale < 0) { |
| FormatPrefix unit = |
| new FormatPrefix(value, (precision != null) ? precision : 0); |
| value = unit.scale(value); |
| fullSuffix = unit.symbol + suffix; |
| } else { |
| value *= scale; |
| } |
| |
| // Convert to the desired precision. |
| String stringValue; |
| if (precision != null) { |
| stringValue = formatFunction(value, precision); |
| } else { |
| stringValue = formatFunction(value); |
| } |
| |
| // Break the value into the integer part (before) and decimal part |
| // (after). |
| int i = stringValue.lastIndexOf('.'); |
| String before = i < 0 ? stringValue : stringValue.substring(0, i), |
| after = i < 0 ? '' : _localeDecimal + stringValue.substring(i + 1); |
| |
| // If the fill character is not '0', grouping is applied before |
| //padding. |
| if (zfill == null && comma) { |
| before = _formatGroup(before); |
| } |
| |
| int length = prefix.length + |
| before.length + |
| after.length + |
| (zcomma ? 0 : negative.length); |
| var padding = length < width |
| ? new List.filled((length = width - length + 1), '').join(fill) |
| : ''; |
| |
| // If the fill character is '0', grouping is applied after padding. |
| if (zcomma) { |
| before = _formatGroup(padding + before); |
| } |
| |
| // Apply prefix. |
| negative += prefix; |
| |
| // Rejoin integer and decimal parts. |
| stringValue = before + after; |
| |
| // Apply any padding and alignment attributes before returning the string. |
| return (align == '<' |
| ? negative + stringValue + padding |
| : align == '>' |
| ? padding + negative + stringValue |
| : align == '^' |
| ? padding.substring(0, length >>= 1) + |
| negative + |
| stringValue + |
| padding.substring(length) |
| : negative + |
| (zcomma ? stringValue : padding + stringValue)) + |
| fullSuffix; |
| }; |
| } |
| |
| // Gets the format function by given type. |
| NumberFormatFunction _getFormatFunction(String type) { |
| switch (type) { |
| case 'b': |
| return (num x, [int p = 0]) => x.toInt().toRadixString(2); |
| case 'c': |
| return (num x, [int p = 0]) => new String.fromCharCodes([x.toInt()]); |
| case 'o': |
| return (num x, [int p = 0]) => x.toInt().toRadixString(8); |
| case 'x': |
| return (num x, [int p = 0]) => x.toInt().toRadixString(16); |
| case 'X': |
| return (num x, [int p = 0]) => |
| x.toInt().toRadixString(16).toUpperCase(); |
| case 'g': |
| return (num x, [int p = 1]) => x.toStringAsPrecision(p); |
| case 'e': |
| return (num x, [int p = 0]) => x.toStringAsExponential(p); |
| case 'f': |
| return (num x, [int p = 0]) => x.toStringAsFixed(p); |
| case 'r': |
| default: |
| return (num x, [int p = 0]) => x.toString(); |
| } |
| } |
| |
| String _formatGroup(String value) { |
| if (_localeGrouping == null) { |
| return value; |
| } |
| int i = value.length, j = 0, g = _localeGrouping[0]; |
| var t = <String>[]; |
| while (i > 0 && g > 0) { |
| if (i - g >= 0) { |
| i = i - g; |
| } else { |
| g = i; |
| i = 0; |
| } |
| var length = (i + g) < value.length ? (i + g) : value.length; |
| t.add(value.substring(i, length)); |
| g = _localeGrouping[j = (j + 1) % _localeGrouping.length]; |
| } |
| return t.reversed.join(_localeThousands); |
| } |
| } |