diff --git a/CHANGELOG.md b/CHANGELOG.md index cf9375d0..183bf935 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 c0e77a59..57fa07dc 100644 --- a/lib/src/intl/number_format.dart +++ b/lib/src/intl/number_format.dart @@ -81,6 +81,7 @@ class NumberFormat { _internalMultiplier = x; _multiplierDigits = (log(_multiplier) / LN10).round(); } + int _internalMultiplier = 1; /** How many digits are there in the [_multiplier]. */ @@ -264,11 +265,39 @@ class NumberFormat { */ _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 @@ class NumberFormat { } /** - * 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 @@ class NumberFormat { } /** - * 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 @@ class NumberFormat { * then calls the system parsing methods on it. */ class _NumberParser { - /** The format for which we are parsing. */ final NumberFormat format; @@ -557,22 +587,22 @@ class _NumberParser { 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 @@ class _NumberParser { * to parse a single pattern. */ class _NumberFormatParser { - /** * The special characters in the pattern language. All others are treated * as literals. @@ -1074,3 +1103,82 @@ class _StringIterator implements Iterator { 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 a4187d09..bbd71923 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 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 747a8bb0..ea634c8f 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 @@ import 'package:fixnum/fixnum.dart'; 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 @@ 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('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 @@ main() { 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 14c8b3f8..54c9114b 100644 --- a/test/number_format_test.dart +++ b/test/number_format_test.dart @@ -19,6 +19,7 @@ var testNumbersWeCanReadBack = { "-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 @@ var testNumbersWeCanReadBack = { }; /** 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 = {