blob: 4008e53ac9c72c492d5e299c0872a09afb96d128 [file] [log] [blame]
/*
* 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);
}
}