Skip to content

Commit

Permalink
Allow Intl to handle scaled fixed-point values
Browse files Browse the repository at this point in the history
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=110469094
  • Loading branch information
alan-knight committed Dec 17, 2015
1 parent 5f9d4d0 commit 1381300
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 39 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
178 changes: 143 additions & 35 deletions lib/src/intl/number_format.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class NumberFormat {
_internalMultiplier = x;
_multiplierDigits = (log(_multiplier) / LN10).round();
}

int _internalMultiplier = 1;

/** How many digits are there in the [_multiplier]. */
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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}");
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1074,3 +1103,82 @@ class _StringIterator implements Iterator<String> {
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';
}
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: intl
version: 0.12.5-dev
version: 0.12.5
author: Dart Team <[email protected]>
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
Expand Down
39 changes: 38 additions & 1 deletion test/fixnum_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
// 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';
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%"
Expand All @@ -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) {
Expand All @@ -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]);
});
});
}
9 changes: 8 additions & 1 deletion test/number_format_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = {
Expand Down

0 comments on commit 1381300

Please sign in to comment.