Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add very large amount support #91

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions lib/src/currency.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
* Written by Brett Sutton <[email protected]>, Jan 2022
*/

import 'dart:math';

import 'package:meta/meta.dart';

import '../money2.dart';
Expand Down Expand Up @@ -167,7 +165,7 @@ class Currency {
throw ArgumentError.value(
precision, 'precision', 'Must be a non-negative value.');
}
return BigInt.from(pow(10, precision));
return BigInt.from(10).pow(precision);
}

/// Takes a [majorUnits] and a [minorUnits] and returns
Expand Down
12 changes: 4 additions & 8 deletions lib/src/money.dart
Original file line number Diff line number Diff line change
Expand Up @@ -367,10 +367,7 @@ class Money implements Comparable<Money> {
/// The JSON representation follows the same format generated by the
/// [toJson] method.
factory Money.fromJson(Map<String, dynamic> json) => Money.fromFixed(
Fixed.parse(
'${json['integerPart']}.${json['decimalPart']}',
scale: json['decimals'] as int,
),
Fixed.fromJson(json['amount'] as Map<String, dynamic>),
isoCode: json['isoCode'] as String,
);

Expand Down Expand Up @@ -488,7 +485,8 @@ class Money implements Comparable<Money> {
/// > $AUD11
/// ```
///
String format(String pattern) => encodedBy(PatternEncoder(this, pattern));
String format([String? pattern]) =>
encodedBy(PatternEncoder(this, pattern ?? currency.pattern));

@override
String toString() => encodedBy(PatternEncoder(this, currency.pattern));
Expand All @@ -498,9 +496,7 @@ class Money implements Comparable<Money> {
/// The JSON representation can be used to recreate this [Money] instance
/// using the [Money.fromJson] factory.
Map<String, dynamic> toJson() => {
'integerPart': amount.integerPart.toInt(),
'decimalPart': amount.decimalPart.toInt(),
'decimals': amount.scale,
'amount': amount.toJson(),
'isoCode': currency.isoCode,
};

Expand Down
23 changes: 3 additions & 20 deletions lib/src/pattern_encoder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,9 @@ class PatternEncoder implements MoneyEncoder<String> {
final moneyPattern = _getMoneyPattern(majorPattern);
_checkZeros(moneyPattern, patternGroupSeparator, minor: false);

final integerPart = data.integerPart;

final formattedMajorUnits =
_getFormattedMajorUnits(data, moneyPattern, integerPart);
final formattedMajorUnits = data.amount
.format(moneyPattern)
.replaceAll(patternGroupSeparator, data.currency.groupSeparator);

// replace the the money components with a single #
var compressedMajorPattern = _compressMoney(majorPattern);
Expand Down Expand Up @@ -118,22 +117,6 @@ class PatternEncoder implements MoneyEncoder<String> {
return formatted;
}

///
String _getFormattedMajorUnits(
MoneyData data, String moneyPattern, BigInt majorUnits) {
// format the no. into that pattern.
var formattedMajorUnits =
NumberFormat(moneyPattern).format(majorUnits.toInt());

if (!majorUnits.isNegative && data.amount.isNegative) {
formattedMajorUnits = '-$formattedMajorUnits';
}

// Convert to the MoneyData's preferred group separator
return formattedMajorUnits.replaceAll(
patternGroupSeparator, data.currency.groupSeparator);
}

/// returns the currency isoCode from [data] using the
/// supplied [pattern] to find the isoCode.
String _getIsoCode(MoneyData data, String pattern) {
Expand Down
2 changes: 1 addition & 1 deletion test/src/money_format_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ void main() {
decimalSeparator: '/',
groupSeparator: ' ',
symbol: '€',
pattern: '###,###.##S');
pattern: '#,###,###.##S');

final amount =
Money.fromIntWithCurrency(1234567890, euroCurrency, decimalDigits: 3);
Expand Down
7 changes: 4 additions & 3 deletions test/src/money_json_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import 'package:test/test.dart';
void main() {
final money = Money.fromInt(1025, isoCode: 'USD');
final expectedJson = <String, dynamic>{
'integerPart': 10,
'decimalPart': 25,
'decimals': 2,
'amount': {
'minorUnits': '1025',
'scale': 2,
},
'isoCode': 'USD',
};

Expand Down
19 changes: 19 additions & 0 deletions test/src/money_large_json_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'package:money2/money2.dart';
import 'package:test/test.dart';

void main() {
final amount = '${'9' * 100}.${'9' * 100}';
final minorUnits = '9' * 200;

test('Money serialization', () {
final c = Currency.create('c100', 100, symbol: '=100=');
Currencies().register(c);
final m0 = Money.parseWithCurrency(amount, c);
final json = m0.toJson();
final m1 = Money.fromJson(json);
expect(m0.integerPart, m1.integerPart);
expect(m0.decimalPart, m1.decimalPart);
expect(m1.compareTo(m0), 0);
expect(m1.minorUnits, BigInt.parse(minorUnits));
});
}
148 changes: 148 additions & 0 deletions test/src/money_large_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import 'package:money2/money2.dart';
import 'package:test/test.dart';

void main() {
const maxScale = 100;
const maxInts = 100;

setUp(() {
for (var scale = 0; scale <= maxScale; scale++) {
final c = Currency.create('C$scale', scale, symbol: '=$scale=');
Currencies().register(c);
}
});

test('test default currency formatting', () {
final roflWithDefaultFormatting =
Currency.create('ROFL', 9, symbol: 'ROFL');
expect(
Money.parseWithCurrency('2.0', roflWithDefaultFormatting).format(),
'ROFL2.00',
reason: 'Failed default formatting',
);
});

test('test custom currency formatting', () {
final roflWithDefaultFormatting =
Currency.create('ROFL', 9, symbol: 'ROFL', pattern: '0.000000000 S');
expect(
Money.parseWithCurrency('2.0', roflWithDefaultFormatting).format(),
'2.000000000 ROFL',
reason: 'Failed custom formatting',
);
});

test('test custom currency formatting 2', () {
final roflWithDefaultFormatting =
Currency.create('ROFL', 9, symbol: 'ROFL', pattern: '0.000000000 S');
expect(
Money.parseWithCurrency('2.01', roflWithDefaultFormatting).format(),
'2.010000000 ROFL',
reason: 'Failed custom formatting',
);
});

test('test custom currency formatting 3 (trim zeros)', () {
final roflWithDefaultFormatting =
Currency.create('ROFL', 9, symbol: 'ROFL', pattern: '0.######### S');
expect(
Money.parseWithCurrency('2.01', roflWithDefaultFormatting).format(),
'2.01 ROFL',
reason: 'Failed custom formatting',
);
});

test('test custom explicit formatting', () {
final roflWithDefaultFormatting =
Currency.create('ROFL', 9, symbol: 'ROFL', pattern: '0.######### S');
expect(
Money.parseWithCurrency('2.01', roflWithDefaultFormatting)
.format('S 0.#########'),
'ROFL 2.01',
reason: 'Failed custom formatting',
);
});

test('scale 0-$maxScale test', () {
for (var scale = 0; scale <= maxScale; scale++) {
final c = Currencies().find('C$scale');
expect(c, isNotNull);
final str = scale == 0 ? '0' : '0.${'0' * (scale - 1)}1';
final fmt = scale == 0 ? '0' : '0.${'#' * scale}';
expect(
Money.parseWithCurrency(str, c!).format(fmt),
str,
reason: 'Failed with $scale scale',
);
}
});

test('integers 0-$maxInts test', () {
for (var ints = 0; ints <= maxInts; ints++) {
final c = Currencies().find('C0');
expect(c, isNotNull);
final str = ints == 0 ? '0' : '9' * ints;
const fmt = '0';
expect(
Money.parseWithCurrency(str, c!).format(fmt),
str,
reason: 'Failed with $ints ints',
);
}
});

test('scale 0-$maxScale and integers 0-$maxInts test', () {
for (var scale = 19; scale <= maxScale; scale++) {
for (var ints = 1; ints <= maxInts; ints++) {
final c = Currencies().find('C$scale');
expect(c, isNotNull);
final intsStr = ints == 0 ? '0' : '9' * ints;
final str = scale == 0 ? intsStr : '$intsStr.${'0' * (scale - 1)}1';
final fmt = scale == 0 ? '0' : '0.${'0' * scale}';

expect(
Money.parseWithCurrency(str, c!).format(fmt),
str,
reason: 'Failed with $scale scale, $ints ints',
);
}
}
});

test(
'scale 0-$maxScale and integers 0-$maxInts addition and subtraction test',
() {
for (var scale = 0; scale <= maxScale; scale++) {
for (var ints = 0; ints <= maxInts; ints++) {
final c = Currencies().find('C$scale');
expect(c, isNotNull);
final intsStr = ints == 0 ? '1' : '9' * ints;
const str0 = '1';
final str1 =
scale == 0 ? '${intsStr}1' : '${intsStr}1.${'0' * (scale - 1)}1';
final str2 =
scale == 0 ? '${intsStr}2' : '${intsStr}2.${'0' * (scale - 1)}1';

final m0 =
Money.fromFixedWithCurrency(Fixed.parse(str0, scale: scale), c!);
final m1 =
Money.fromFixedWithCurrency(Fixed.parse(str1, scale: scale), c);
final m2 =
Money.fromFixedWithCurrency(Fixed.parse(str2, scale: scale), c);
final mDiff = m2 - m1;
final mSum = m1 + m0;

expect(
mDiff.amount,
Fixed.one,
reason: 'Failed with $scale scale, $ints ints',
);
expect(
mSum.amount,
m2.amount,
reason: 'Failed with $scale scale, $ints ints',
);
}
}
});
}
2 changes: 1 addition & 1 deletion test/src/money_parse_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ void main() {
decimalSeparator: '/',
groupSeparator: ' ',
symbol: '€',
pattern: '###,###.##S');
pattern: '#,###,###.##S');

final amount = Money.parseWithCurrency('1 234 567/89€', euroCurrency);
final formatted = amount.toString();
Expand Down