Make number formatting in Intl able to work with Int64 or other types.
BUG=
R=sra@google.com
Review URL: https://codereview.chromium.org//778293002
git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/intl@42411 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9dca66f..36bb584 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 0.11.12
+ * Number formatting now accepts "int-like" inputs that don't have to
+ conform to the num interface. In particular, you can now pass an Int64
+ from the fixnum package and format it. In addition, this no longer
+ multiplies the result, so it won't lose precision on a few additional
+ cases in JS.
+
## 0.11.11
* Add a -no-embedded-plurals flag to reject plurals and genders that
have either leading or trailing text around them. This follows the
diff --git a/lib/src/intl/number_format.dart b/lib/src/intl/number_format.dart
index 377db7d..2e23d35 100644
--- a/lib/src/intl/number_format.dart
+++ b/lib/src/intl/number_format.dart
@@ -71,7 +71,19 @@
int minimumFractionDigits = 0;
int minimumExponentDigits = 0;
- int _multiplier = 1;
+ /**
+ * For percent and permille, what are we multiplying by in order to
+ * get the printed value, e.g. 100 for percent.
+ */
+ int get _multiplier => _internalMultiplier;
+ set _multiplier(int x) {
+ _internalMultiplier = x;
+ _multiplierDigits = (log(_multiplier) / LN10).round();
+ }
+ int _internalMultiplier = 1;
+
+ /** How many digits are there in the [_multiplier]. */
+ int _multiplierDigits = 0;
/**
* Stores the pattern used to create this format. This isn't used, but
@@ -162,14 +174,12 @@
/**
* Format [number] according to our pattern and return the formatted string.
*/
- String format(num number) {
- // TODO(alanknight): Do we have to do anything for printing numbers bidi?
- // Or are they always printed left to right?
- if (number.isNaN) return symbols.NAN;
- if (number.isInfinite) return "${_signPrefix(number)}${symbols.INFINITY}";
+ String format(number) {
+ if (_isNaN(number)) return symbols.NAN;
+ if (_isInfinite(number)) return "${_signPrefix(number)}${symbols.INFINITY}";
_add(_signPrefix(number));
- _formatNumber(number.abs() * _multiplier);
+ _formatNumber(number.abs());
_add(_signSuffix(number));
var result = _buffer.toString();
@@ -186,7 +196,7 @@
/**
* Format the main part of the number in the form dictated by the pattern.
*/
- void _formatNumber(num number) {
+ void _formatNumber(number) {
if (_useExponentialNotation) {
_formatExponential(number);
} else {
@@ -250,48 +260,60 @@
final _maxInt = pow(2, 52);
/**
+ * Helpers to check numbers that don't conform to the [num] interface,
+ * e.g. Int64
+ */
+ _isInfinite(number) => number is num ? number.isInfinite : false;
+ _isNaN(number) => number is num ? number.isNaN : false;
+ _round(number) => number is num ? number.round() : number;
+ _floor(number) => number is num ? number.floor() : number;
+
+ /**
* Format the basic number portion, inluding the fractional digits.
*/
- void _formatFixed(num number) {
- // Very fussy math to get integer and fractional parts.
- var power = pow(10, maximumFractionDigits);
- 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;
+ void _formatFixed(number) {
+ var integerPart;
+ int fractionPart;
+ int extraIntegerDigits;
+
+ final power = pow(10, maximumFractionDigits);
+ final digitMultiplier = power * _multiplier;
+
+ if (_isInfinite(number)) {
+ integerPart = number.toInt();
+ extraIntegerDigits = 0;
+ fractionPart = 0;
} else {
- intValue = shiftedNumber.round() ~/ power;
- fracValue = (shiftedNumber - intValue * power).floor();
+ // We have three possible pieces. First, the basic integer part. If this
+ // is a percent or permille, the additional 2 or 3 digits. Finally the
+ // fractional part.
+ // We avoid multiplying the number because it might overflow if we have
+ // a fixed-size integer type, so we extract each of the three as an
+ // integer pieces.
+ integerPart = _floor(number);
+ var fraction = number - integerPart;
+ // Multiply out to the number of decimal places and the percent, then
+ // round. For fixed-size integer types this should always be zero, so
+ // multiplying is OK.
+ var remainingDigits = _round(fraction * digitMultiplier).toInt();
+ // However, in rounding we may overflow into the main digits.
+ if (remainingDigits >= digitMultiplier) {
+ integerPart++;
+ remainingDigits -= digitMultiplier;
+ }
+ // Separate out the extra integer parts from the fraction part.
+ extraIntegerDigits = remainingDigits ~/ power;
+ fractionPart = remainingDigits % power;
}
- var fractionPresent = minimumFractionDigits > 0 || fracValue > 0;
+ var fractionPresent = minimumFractionDigits > 0 || fractionPart > 0;
- // 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 = '';
- if (1 is double && intValue > _maxInt) {
- var howManyDigitsTooBig = (log(intValue) / LN10).ceil() - 16;
- var divisor = pow(10, howManyDigitsTooBig).round();
- paddingDigits = symbols.ZERO_DIGIT * howManyDigitsTooBig.toInt();
-
- intValue = (intValue / divisor).truncate();
- }
- var integerDigits = "${intValue}${paddingDigits}".codeUnits;
+ var integerDigits = _integerDigits(integerPart, extraIntegerDigits);
var digitLength = integerDigits.length;
- if (_hasPrintableIntegerPart(intValue)) {
+ if (_hasPrintableIntegerPart(integerPart)) {
_pad(minimumIntegerDigits - digitLength);
for (var i = 0; i < digitLength; i++) {
- _addDigit(integerDigits[i]);
+ _addDigit(integerDigits.codeUnitAt(i));
_group(digitLength, i);
}
} else if (!fractionPresent) {
@@ -300,7 +322,44 @@
}
_decimalSeparator(fractionPresent);
- _formatFractionPart((fracValue + power).toString());
+ _formatFractionPart((fractionPart + power).toString());
+ }
+
+ /**
+ * Compute the raw integer digits which will then be printed with
+ * grouping and translated to localized digits.
+ */
+ String _integerDigits(integerPart, extraIntegerDigits) {
+ // 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 = '';
+ if (1 is double && integerPart is num && integerPart > _maxInt) {
+ var howManyDigitsTooBig = (log(integerPart) / LN10).ceil() - 16;
+ var divisor = pow(10, howManyDigitsTooBig).round();
+ paddingDigits = symbols.ZERO_DIGIT * howManyDigitsTooBig.toInt();
+ integerPart = (integerPart / divisor).truncate();
+ }
+
+ var extra = extraIntegerDigits == 0 ? '' : extraIntegerDigits.toString();
+ var intDigits = _mainIntegerDigits(integerPart);
+ var paddedExtra =
+ intDigits.isEmpty ? extra : extra.padLeft(_multiplierDigits, '0');
+ return "${intDigits}${paddedExtra}${paddingDigits}";
+ }
+
+ /**
+ * The digit string of the integer part. This is the empty string if the
+ * integer part is zero and otherwise is the toString() of the integer
+ * part, stripping off any minus sign.
+ */
+ String _mainIntegerDigits(integer) {
+ if (integer == 0) return '';
+ var digits = integer.toString();
+ // If we have a fixed-length int representation, it can have a negative
+ // number whose negation is also negative, e.g. 2^-63 in 64-bit.
+ // Remove the minus sign.
+ return digits.startsWith('-') ? digits.substring(1) : digits;
}
/**
@@ -330,8 +389,8 @@
* because we have digits left of the decimal point, or because there are
* a minimum number of printable digits greater than 1.
*/
- bool _hasPrintableIntegerPart(int intValue) =>
- intValue > 0 || minimumIntegerDigits > 0;
+ bool _hasPrintableIntegerPart(x) =>
+ x > 0 || minimumIntegerDigits > 0;
/** A group of methods that provide support for writing digits and other
* required characters into [_buffer] easily.
@@ -384,13 +443,13 @@
* Returns the prefix for [x] based on whether it's positive or negative.
* In en_US this would be '' and '-' respectively.
*/
- String _signPrefix(num x) => x.isNegative ? _negativePrefix : _positivePrefix;
+ String _signPrefix(x) => x.isNegative ? _negativePrefix : _positivePrefix;
/**
* Returns the suffix for [x] based on wether it's positive or negative.
* In en_US there are no suffixes for positive or negative.
*/
- String _signSuffix(num x) => x.isNegative ? _negativeSuffix : _positiveSuffix;
+ String _signSuffix(x) => x.isNegative ? _negativeSuffix : _positiveSuffix;
void _setPattern(String newPattern) {
if (newPattern == null) return;
diff --git a/pubspec.yaml b/pubspec.yaml
index c2bcbef..9d039c4 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: intl
-version: 0.11.11
+version: 0.11.12
author: Dart Team <misc@dartlang.org>
description: Contains code to deal with internationalized/localized messages, date and number formatting and parsing, bi-directional text, and other internationalization issues.
homepage: https://www.dartlang.org
@@ -12,3 +12,4 @@
petitparser: '>=1.1.3 <2.0.0'
dev_dependencies:
unittest: '>=0.10.0 <0.12.0'
+ fixnum: '>=0.9.0 <0.10.0'
diff --git a/test/fixnum_test.dart b/test/fixnum_test.dart
new file mode 100644
index 0000000..953fe00
--- /dev/null
+++ b/test/fixnum_test.dart
@@ -0,0 +1,47 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// 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 intl_test;
+
+import 'package:intl/intl.dart';
+import 'package:unittest/unittest.dart';
+import 'package:fixnum/fixnum.dart';
+
+var int64Values = {
+ new Int64(12345) :
+ ["USD12,345.00", "1,234,500%"],
+ new Int64(0x7FFFFFFFFFFFF) :
+ ["USD2,251,799,813,685,247.00", "225,179,981,368,524,700%"],
+ Int64.parseHex('7FFFFFFFFFFFFFF') :
+ ["USD576,460,752,303,423,487.00", "57,646,075,230,342,348,700%"],
+ Int64.parseHex('8000000000000000') :
+ ["-USD9,223,372,036,854,775,808.00", "-922,337,203,685,477,580,800%"]
+};
+
+var int32Values = {
+ new Int32(12345) : ["USD12,345.00", "1,234,500%"],
+ new Int32(0x7FFFF) : ["USD524,287.00", "52,428,700%"],
+ Int32.parseHex('7FFFFFF') : ["USD134,217,727.00", "13,421,772,700%"],
+ Int32.parseHex('80000000') : ["-USD2,147,483,648.00", "-214,748,364,800%"]
+};
+
+main() {
+ test('int64', () {
+ int64Values.forEach((number, expected) {
+ var currency = new NumberFormat.currencyPattern().format(number);
+ expect(currency, expected.first);
+ var percent = new NumberFormat.percentPattern().format(number);
+ expect(percent, expected[1]);
+ });
+ });
+
+ test('int32', () {
+ int32Values.forEach((number, expected) {
+ var currency = new NumberFormat.currencyPattern().format(number);
+ expect(currency, expected.first);
+ var percent = new NumberFormat.percentPattern().format(number);
+ expect(percent, expected[1]);
+ });
+ });
+}
diff --git a/test/number_closure_test.dart b/test/number_closure_test.dart
index 523ca8c..d715d7e 100644
--- a/test/number_closure_test.dart
+++ b/test/number_closure_test.dart
@@ -40,7 +40,7 @@
fmt = new NumberFormat.decimalPattern();
str = fmt.format(1.3456E20);
- expect(veryBigNumberCompare('134,559,999,999,999,000,000', str), isTrue);
+ expect(veryBigNumberCompare('134,560,000,000,000,000,000', str), isTrue);
fmt = new NumberFormat.percentPattern();
str = fmt.format(1.3456E20);