From 0e3fda76319cc65ed32aa2f495c58c707b8b7ea6 Mon Sep 17 00:00:00 2001 From: nesquikm Date: Tue, 18 Feb 2025 17:13:09 +0400 Subject: [PATCH 1/3] refactor: Money and Currency serialization, formatting, and large number handling - Updated Money JSON serialization format - Simplified Money formatting with default pattern - Improved currency precision calculation - Added tests for large scale and integer handling - Enhanced pattern encoding and formatting flexibility --- lib/src/currency.dart | 2 +- lib/src/money.dart | 12 +-- lib/src/pattern_encoder.dart | 23 +----- test/src/money_format_test.dart | 2 +- test/src/money_json_test.dart | 7 +- test/src/money_large_json_test.dart | 19 +++++ test/src/money_large_test.dart | 111 ++++++++++++++++++++++++++++ test/src/money_parse_test.dart | 2 +- 8 files changed, 144 insertions(+), 34 deletions(-) create mode 100644 test/src/money_large_json_test.dart create mode 100644 test/src/money_large_test.dart diff --git a/lib/src/currency.dart b/lib/src/currency.dart index 4fe1074..a5a4352 100644 --- a/lib/src/currency.dart +++ b/lib/src/currency.dart @@ -167,7 +167,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 diff --git a/lib/src/money.dart b/lib/src/money.dart index 8b70692..78e6808 100644 --- a/lib/src/money.dart +++ b/lib/src/money.dart @@ -367,10 +367,7 @@ class Money implements Comparable { /// The JSON representation follows the same format generated by the /// [toJson] method. factory Money.fromJson(Map json) => Money.fromFixed( - Fixed.parse( - '${json['integerPart']}.${json['decimalPart']}', - scale: json['decimals'] as int, - ), + Fixed.fromJson(json['amount'] as Map), isoCode: json['isoCode'] as String, ); @@ -488,7 +485,8 @@ class Money implements Comparable { /// > $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)); @@ -498,9 +496,7 @@ class Money implements Comparable { /// The JSON representation can be used to recreate this [Money] instance /// using the [Money.fromJson] factory. Map toJson() => { - 'integerPart': amount.integerPart.toInt(), - 'decimalPart': amount.decimalPart.toInt(), - 'decimals': amount.scale, + 'amount': amount.toJson(), 'isoCode': currency.isoCode, }; diff --git a/lib/src/pattern_encoder.dart b/lib/src/pattern_encoder.dart index 05bd6c2..4868175 100644 --- a/lib/src/pattern_encoder.dart +++ b/lib/src/pattern_encoder.dart @@ -78,10 +78,9 @@ class PatternEncoder implements MoneyEncoder { 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); @@ -118,22 +117,6 @@ class PatternEncoder implements MoneyEncoder { 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) { diff --git a/test/src/money_format_test.dart b/test/src/money_format_test.dart index 3e42900..4a75bba 100644 --- a/test/src/money_format_test.dart +++ b/test/src/money_format_test.dart @@ -84,7 +84,7 @@ void main() { decimalSeparator: '/', groupSeparator: ' ', symbol: '€', - pattern: '###,###.##S'); + pattern: '#,###,###.##S'); final amount = Money.fromIntWithCurrency(1234567890, euroCurrency, decimalDigits: 3); diff --git a/test/src/money_json_test.dart b/test/src/money_json_test.dart index 0cdd8a3..e870fae 100644 --- a/test/src/money_json_test.dart +++ b/test/src/money_json_test.dart @@ -11,9 +11,10 @@ import 'package:test/test.dart'; void main() { final money = Money.fromInt(1025, isoCode: 'USD'); final expectedJson = { - 'integerPart': 10, - 'decimalPart': 25, - 'decimals': 2, + 'amount': { + 'minorUnits': '1025', + 'scale': 2, + }, 'isoCode': 'USD', }; diff --git a/test/src/money_large_json_test.dart b/test/src/money_large_json_test.dart new file mode 100644 index 0000000..42fee0d --- /dev/null +++ b/test/src/money_large_json_test.dart @@ -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)); + }); +} diff --git a/test/src/money_large_test.dart b/test/src/money_large_test.dart new file mode 100644 index 0000000..a855e6c --- /dev/null +++ b/test/src/money_large_test.dart @@ -0,0 +1,111 @@ +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', + ); + } + } + }); +} diff --git a/test/src/money_parse_test.dart b/test/src/money_parse_test.dart index 954ca35..52de6e0 100644 --- a/test/src/money_parse_test.dart +++ b/test/src/money_parse_test.dart @@ -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(); From edac941a3bef61537cfda1ea22c442729204e1ed Mon Sep 17 00:00:00 2001 From: nesquikm Date: Tue, 18 Feb 2025 17:15:35 +0400 Subject: [PATCH 2/3] chore: remove unused dart:math import from currency.dart --- lib/src/currency.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/currency.dart b/lib/src/currency.dart index a5a4352..c77b5cb 100644 --- a/lib/src/currency.dart +++ b/lib/src/currency.dart @@ -4,8 +4,6 @@ * Written by Brett Sutton , Jan 2022 */ -import 'dart:math'; - import 'package:meta/meta.dart'; import '../money2.dart'; From d2952f60325bf82aa2311e28cb6a012f3907a314 Mon Sep 17 00:00:00 2001 From: nesquikm Date: Tue, 18 Feb 2025 17:21:59 +0400 Subject: [PATCH 3/3] test: add comprehensive money addition and subtraction test for large scales and integers --- test/src/money_large_test.dart | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/src/money_large_test.dart b/test/src/money_large_test.dart index a855e6c..7c30e73 100644 --- a/test/src/money_large_test.dart +++ b/test/src/money_large_test.dart @@ -108,4 +108,41 @@ void main() { } } }); + + 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', + ); + } + } + }); }