Allow Intl to handle scaled fixed-point values
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=110469094
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf9375d..183bf93 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,12 @@
## 0.12.5
* Parse Eras in DateFormat.
- * Update pubspec.yaml to allow newer version of fixnum.
+ * Update pubspec.yaml to allow newer version of fixnum and analyzer.
* Improvements to the compiled size of generated messages code with dart2js.
* Allow adjacent literal strings to be used for message names/descriptions.
* Provide a better error message for some cases of bad parameters
to plural/gender/select messages.
+ * Introduce a simple MicroMoney class that can represent currency values
+ scaled by a constant factor.
## 0.12.4+3
* update analyzer to '<0.28.0' and fixnum to '<0.11.0'
diff --git a/lib/src/intl/number_format.dart b/lib/src/intl/number_format.dart
index c0e77a5..57fa07d 100644
--- a/lib/src/intl/number_format.dart
+++ b/lib/src/intl/number_format.dart
@@ -81,6 +81,7 @@
_internalMultiplier = x;
_multiplierDigits = (log(_multiplier) / LN10).round();
}
+
int _internalMultiplier = 1;
/** How many digits are there in the [_multiplier]. */
@@ -264,11 +265,39 @@
*/
_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.
+ * Helper to get the floor of a number which might not be num. This should
+ * only ever be called with an argument which is positive, or whose abs()
+ * is negative. The second case is the maximum negative value on a
+ * fixed-length integer. Since they are integers, they are also their own
+ * floor.
+ */
+ _floor(number) {
+ if (number.isNegative && !(number.abs().isNegative)) {
+ throw new ArgumentError(
+ "Internal error: expected positive number, got $number");
+ }
+ return (number is num) ? number.floor() : number ~/ 1;
+ }
+
+ /** Helper to round a number which might not be num.*/
+ _round(number) {
+ if (number is num) {
+ return number.round();
+ } else if (number.remainder(1) == 0) {
+ // Not a normal number, but int-like, e.g. Int64
+ return number;
+ } else {
+ // TODO(alanknight): Do this more efficiently. If IntX had floor and round we could avoid this.
+ var basic = _floor(number);
+ var fraction = (number - basic).toDouble().round();
+ return fraction == 0 ? number : number + fraction;
+ }
+ }
+
+ /**
+ * Format the basic number portion, including the fractional digits.
*/
void _formatFixed(number) {
var integerPart;
@@ -384,23 +413,25 @@
}
/**
- * Return true if we have a main integer part which is printable, either
- * because we have digits left of the decimal point (this may include digits
- * which have been moved left because of percent or permille formatting),
- * or because the minimum number of printable digits is greater than 1.
- */
+ * Return true if we have a main integer part which is printable, either
+ * because we have digits left of the decimal point (this may include digits
+ * which have been moved left because of percent or permille formatting),
+ * or because the minimum number of printable digits is greater than 1.
+ */
bool _hasIntegerDigits(String digits) =>
digits.isNotEmpty || minimumIntegerDigits > 0;
/** A group of methods that provide support for writing digits and other
- * required characters into [_buffer] easily.
- */
+ * required characters into [_buffer] easily.
+ */
void _add(String x) {
_buffer.write(x);
}
+
void _addZero() {
_buffer.write(symbols.ZERO_DIGIT);
}
+
void _addDigit(int x) {
_buffer.writeCharCode(_localeZero + x - _zero);
}
@@ -416,13 +447,13 @@
}
/**
- * We are printing the digits of the number from left to right. We may need
- * to print a thousands separator or other grouping character as appropriate
- * to the locale. So we find how many places we are from the end of the number
- * by subtracting our current [position] from the [totalLength] and printing
- * the separator character every [_groupingSize] digits, with the final
- * grouping possibly being of a different size, [_finalGroupingSize].
- */
+ * We are printing the digits of the number from left to right. We may need
+ * to print a thousands separator or other grouping character as appropriate
+ * to the locale. So we find how many places we are from the end of the number
+ * by subtracting our current [position] from the [totalLength] and printing
+ * the separator character every [_groupingSize] digits, with the final
+ * grouping possibly being of a different size, [_finalGroupingSize].
+ */
void _group(int totalLength, int position) {
var distanceFromEnd = totalLength - position;
if (distanceFromEnd <= 1 || _groupingSize <= 0) return;
@@ -474,7 +505,6 @@
* then calls the system parsing methods on it.
*/
class _NumberParser {
-
/** The format for which we are parsing. */
final NumberFormat format;
@@ -557,22 +587,22 @@
var _replacements;
Map _initializeReplacements() => {
- symbols.DECIMAL_SEP: () => '.',
- symbols.EXP_SYMBOL: () => 'E',
- symbols.GROUP_SEP: handleSpace,
- symbols.PERCENT: () {
- scale = _NumberFormatParser._PERCENT_SCALE;
- return '';
- },
- symbols.PERMILL: () {
- scale = _NumberFormatParser._PER_MILLE_SCALE;
- return '';
- },
- ' ': handleSpace,
- '\u00a0': handleSpace,
- '+': () => '+',
- '-': () => '-',
- };
+ symbols.DECIMAL_SEP: () => '.',
+ symbols.EXP_SYMBOL: () => 'E',
+ symbols.GROUP_SEP: handleSpace,
+ symbols.PERCENT: () {
+ scale = _NumberFormatParser._PERCENT_SCALE;
+ return '';
+ },
+ symbols.PERMILL: () {
+ scale = _NumberFormatParser._PER_MILLE_SCALE;
+ return '';
+ },
+ ' ': handleSpace,
+ '\u00a0': handleSpace,
+ '+': () => '+',
+ '-': () => '-',
+ };
invalidFormat() =>
throw new FormatException("Invalid number: ${input.contents}");
@@ -729,7 +759,6 @@
* to parse a single pattern.
*/
class _NumberFormatParser {
-
/**
* The special characters in the pattern language. All others are treated
* as literals.
@@ -1074,3 +1103,82 @@
return input;
}
}
+
+/// Used primarily for currency formatting, this number-like class stores
+/// millionths of a currency unit, typically as an Int64.
+///
+/// It supports no operations other than being used for Intl number formatting.
+abstract class MicroMoney {
+ factory MicroMoney(micros) => new _MicroMoney(micros);
+}
+
+/// Used primarily for currency formatting, this stores millionths of a
+/// currency unit, typically as an Int64.
+///
+/// This private class provides the operations needed by the formatting code.
+class _MicroMoney implements MicroMoney {
+ var _micros;
+ _MicroMoney(this._micros);
+ static const _multiplier = 1000000;
+
+ get _integerPart => _micros ~/ _multiplier;
+ int get _fractionPart => (this - _integerPart)._micros.toInt().abs();
+
+ bool get isNegative => _micros.isNegative;
+
+ _MicroMoney abs() => isNegative ? new _MicroMoney(_micros.abs()) : this;
+
+ // Note that if this is done in a general way there's a risk of integer
+ // overflow on JS when multiplying out the [other] parameter, which may be
+ // an Int64. In formatting we only ever subtract out our own integer part.
+ _MicroMoney operator -(other) {
+ if (other is MicroMoney) return new _MicroMoney(_micros - other._micros);
+ return new _MicroMoney(_micros - (other * _multiplier));
+ }
+
+ _MicroMoney operator +(other) {
+ if (other is MicroMoney) return new _MicroMoney(_micros + other._micros);
+ return new _MicroMoney(_micros + (other * _multiplier));
+ }
+
+ _MicroMoney operator ~/(divisor) {
+ if (divisor is! int) {
+ throw new ArgumentError.value(divisor, 'divisor',
+ '_MicroMoney ~/ only supports int arguments.');
+ }
+ return new _MicroMoney((_integerPart ~/ divisor) * _multiplier);
+ }
+
+ _MicroMoney operator *(other) {
+ if (other is! int) {
+ throw new ArgumentError.value(other, 'other',
+ '_MicroMoney * only supports int arguments.');
+ }
+ return new _MicroMoney(
+ (_integerPart * other) * _multiplier + (_fractionPart * other));
+ }
+
+ /// Note that this only really supports remainder from an int,
+ /// not division by another MicroMoney
+ _MicroMoney remainder(other) {
+ if (other is! int) {
+ throw new ArgumentError.value(other, 'other',
+ '_MicroMoney.remainder only supports int arguments.');
+ }
+ return new _MicroMoney(_micros.remainder(other * _multiplier));
+ }
+
+ double toDouble() => _micros.toDouble() / _multiplier;
+
+ int toInt() => _integerPart.toInt();
+
+ String toString() {
+ var beforeDecimal = _integerPart.toString();
+ var decimalPart = '';
+ var fractionPart = _fractionPart;
+ if (fractionPart != 0) {
+ decimalPart = '.' + fractionPart.toString();
+ }
+ return '$beforeDecimal$decimalPart';
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index a4187d0..bbd7192 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: intl
-version: 0.12.5-dev
+version: 0.12.5
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://github.com/dart-lang/intl
diff --git a/test/fixnum_test.dart b/test/fixnum_test.dart
index 747a8bb..ea634c8 100644
--- a/test/fixnum_test.dart
+++ b/test/fixnum_test.dart
@@ -2,7 +2,7 @@
// 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;
+library fixnum_test;
import 'package:intl/intl.dart';
import 'package:unittest/unittest.dart';
@@ -10,6 +10,7 @@
var int64Values = {
new Int64(12345): ["USD12,345.00", "1,234,500%"],
+ 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%"
@@ -28,9 +29,36 @@
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('7FFFFFFF'): ["USD2,147,483,647.00", "214,748,364,700%"],
Int32.parseHex('80000000'): ["-USD2,147,483,648.00", "-214,748,364,800%"]
};
+var microMoneyValues = {
+ new MicroMoney(new Int64(12345670000)): ["USD12,345.67", "1,234,567%"],
+ new MicroMoney(new Int64(12345671000)): ["USD12,345.67", "1,234,567%"],
+ new MicroMoney(new Int64(12345678000)): ["USD12,345.68", "1,234,568%"],
+ new MicroMoney(new Int64(-12345670000)): ["-USD12,345.67", "-1,234,567%"],
+ new MicroMoney(new Int64(-12345671000)): ["-USD12,345.67", "-1,234,567%"],
+ new MicroMoney(new Int64(-12345678000)): ["-USD12,345.68", "-1,234,568%"],
+ new MicroMoney(new Int64(12340000000)): ["USD12,340.00", "1,234,000%"],
+ new MicroMoney(new Int64(0x7FFFFFFFFFFFF)): [
+ "USD2,251,799,813.69",
+ "225,179,981,369%"
+ ],
+ new MicroMoney(Int64.parseHex('7FFFFFFFFFFFFFF')): [
+ "USD576,460,752,303.42",
+ "57,646,075,230,342%"
+ ],
+ new MicroMoney(Int64.parseHex('7FFFFFFFFFFFFFFF')): [
+ "USD9,223,372,036,854.78",
+ "922,337,203,685,478%"
+ ],
+ new MicroMoney(Int64.parseHex('8000000000000000')): [
+ "-USD9,223,372,036,854.78",
+ "-922,337,203,685,478%"
+ ]
+};
+
main() {
test('int64', () {
int64Values.forEach((number, expected) {
@@ -49,4 +77,13 @@
expect(percent, expected[1]);
});
});
+
+ test('micro money', () {
+ microMoneyValues.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_format_test.dart b/test/number_format_test.dart
index 14c8b3f..54c9114 100644
--- a/test/number_format_test.dart
+++ b/test/number_format_test.dart
@@ -19,6 +19,7 @@
"-1": -1,
"-2": -2.0,
"-0.01": -0.01,
+ "-1.23": -1.23,
"0.001": 0.001,
"0.01": 0.01,
"0.1": 0.1,
@@ -38,7 +39,13 @@
};
/** Test numbers that we can't parse because we lose precision in formatting.*/
-var testNumbersWeCannotReadBack = {"3.142": PI,};
+var testNumbersWeCannotReadBack = {
+ "3.142": PI,
+ "-1.234": -1.2342,
+ "-1.235": -1.2348,
+ "1.234": 1.2342,
+ "1.235": 1.2348
+};
/** Test numbers that won't work in Javascript because they're too big. */
var testNumbersOnlyForTheVM = {