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);