dev
dart 304 lines 9.12 KB
Raw
1 import "dart:math";
2
3 import "package:cw_core/amount/utils.dart";
4 import "package:cw_core/crypto_currency.dart";
5 import "package:cw_core/currency.dart";
6 import "package:cw_core/format_fixed.dart";
7 import "package:cw_core/parse_fixed.dart";
8
9 export "money_local.dart";
10
11 class Money implements Comparable<Money> {
12 const Money(this.amount, this.currency, [int? overrideDecimals])
13 : _overrideDecimals = overrideDecimals;
14
15 factory Money.zero(Currency currency) => Money(BigInt.zero, currency);
16
17 factory Money.fromInt(int amount, Currency currency) => Money(BigInt.from(amount), currency);
18
19 /// Parse the [source] and turn it into [Money] trimming trailing 0s
20 ///
21 /// Throws a [FormatException] if the [source] is not a valid decimal or
22 /// not in canonical representation or if it is a decimal when [isBaseUnit]
23 factory Money.parse(
24 String source,
25 Currency currency, {
26 bool isBaseUnit = false,
27 bool strictParsing = true,
28 }) {
29 if (!isBaseUnit) {
30 source = trimTrailingFractionZeros(source);
31 }
32 final decimals = strictParsing ? currency.decimals : _getActualDecimals(source, currency);
33 final amount = isBaseUnit ? BigInt.parse(source) : parseFixed(source, decimals);
34
35 return Money(amount, currency, decimals);
36 }
37
38 /// Parse the [source] and turn it into [Money] if possible trimming trailing 0s
39 ///
40 /// As [parse] except that this method returns `null` if the input is not
41 /// valid or if it is a decimal when [isBaseUnit]
42 static Money? tryParse(
43 String source,
44 Currency currency, {
45 bool isBaseUnit = false,
46 bool strictParsing = true,
47 }) {
48 try {
49 if (!isBaseUnit) {
50 source = trimTrailingFractionZeros(source);
51 }
52 final decimals = strictParsing ? currency.decimals : _getActualDecimals(source, currency);
53 final amount = isBaseUnit ? BigInt.tryParse(source) : tryParseFixed(source, decimals);
54
55 return amount != null ? Money(amount, currency, decimals) : null;
56 } catch (_) {
57 return null;
58 }
59 }
60
61 final BigInt amount;
62 final Currency currency;
63
64 final int? _overrideDecimals;
65
66 /// Returns the amount of decimals of [currency]
67 int get decimals => _overrideDecimals ?? currency.decimals;
68
69 /// Returns the sign of this [BigInt] amount.
70 /// Returns 0 for zero, -1 for values less than zero and +1 for values
71 /// greater than zero.
72 int get sign => amount.sign;
73
74 /// Returns `true` when amount of this money is zero.
75 bool get isZero => amount == BigInt.zero;
76
77 /// Returns `true` when amount of this money is negative.
78 bool get isNegative => amount.isNegative;
79
80 /// Compares this to [other].
81 ///
82 /// [other] has to be in same currency, [ArgumentError] will be thrown
83 /// otherwise.
84 @override
85 int compareTo(Money other) {
86 _assertSameCurrency(other);
87
88 final aligned = _align(other);
89 return aligned.a.compareTo(aligned.b);
90 }
91
92 /// Returns `true` if [other] is the same amount of money in
93 /// the same currency.
94 @override
95 bool operator ==(Object other) {
96 if (identical(this, other)) {
97 return true;
98 }
99
100 if (other is! Money || other.currency != currency) {
101 return false;
102 }
103
104 final aligned = _align(other);
105 return aligned.a == aligned.b;
106 }
107
108 /// Returns `true` when this money is less than [other].
109 ///
110 /// Both operands have to be in same currency, [ArgumentError] will be thrown
111 /// otherwise.
112 bool operator <(Money other) {
113 _assertSameCurrency(other, "Cannot compare money in different currencies.");
114
115 final aligned = _align(other);
116 return aligned.a < aligned.b;
117 }
118
119 /// Returns `true` when this money is less than or equal to [other].
120 ///
121 /// Both operands have to be in same currency, [ArgumentError] will be thrown
122 /// otherwise.
123 bool operator <=(Money other) {
124 _assertSameCurrency(other, "Cannot compare money in different currencies.");
125
126 final aligned = _align(other);
127 return aligned.a <= aligned.b;
128 }
129
130 /// Returns `true` when this money is greater than [other].
131 ///
132 /// Both operands have to be in same currency, [ArgumentError] will be thrown
133 /// otherwise.
134 bool operator >(Money other) {
135 _assertSameCurrency(other, "Cannot compare money in different currencies.");
136
137 final aligned = _align(other);
138 return aligned.a > aligned.b;
139 }
140
141 /// Returns `true` when this money is greater than or equal to [other].
142 ///
143 /// Both operands have to be in same currency, [ArgumentError] will be thrown
144 /// otherwise.
145 bool operator >=(Money other) {
146 _assertSameCurrency(other, "Cannot compare money in different currencies.");
147
148 final aligned = _align(other);
149 return aligned.a >= aligned.b;
150 }
151
152 /// Adds the amount of [other] to this amount.
153 ///
154 /// Both operands must be in same currency, [ArgumentError] will be thrown
155 /// otherwise.
156 Money operator +(Money other) {
157 _assertSameCurrency(other);
158
159 final aligned = _align(other);
160 return copyWith(amount: aligned.a + aligned.b, decimals: aligned.decimals);
161 }
162
163 /// unary minus operator.
164 Money operator -() => copyWith(amount: -amount);
165
166 /// Subtracts the amount of [other] from this amount.
167 ///
168 /// Both operands must be in same currency, [ArgumentError] will be thrown
169 /// otherwise.
170 Money operator -(Money other) {
171 _assertSameCurrency(other);
172
173 final aligned = _align(other);
174 return copyWith(amount: aligned.a - aligned.b, decimals: aligned.decimals);
175 }
176
177 /// Returns [Money] multiplied by [other].
178 ///
179 /// The result is again [Money].
180 Money operator *(BigInt other) => copyWith(amount: amount * other);
181
182 /// Returns [Money] divided by [other].
183 ///
184 /// The result is again [Money].
185 Money operator /(BigInt other) {
186 if (other == BigInt.zero) {
187 throw Exception("Division by zero.");
188 }
189
190 final neg = (amount.isNegative) ^ (other.isNegative);
191 final A = amount.abs();
192 final B = other.abs();
193
194 final q = A ~/ B;
195 final r = A % B;
196
197 // Compare 2*r with B to detect half/up/down
198 final twiceR = r << 1;
199 final mag = (twiceR >= B) ? q + BigInt.one : q;
200
201 return copyWith(amount: neg ? -mag : mag);
202 }
203
204 /// Creates a copy of this [Money] object with optional new values
205 /// for [amount], [currency].
206 ///
207 /// If just [currency] is provided and the decimals missmatch
208 /// the amount will be transformed to keep its canonical representation
209 Money copyWith({BigInt? amount, Currency? currency, int? decimals}) {
210 if (currency != null && amount == null && decimals == null &&
211 currency.decimals != this.decimals) {
212 return Money(
213 _transformAmount(this.amount, this.decimals, currency.decimals),
214 currency,
215 currency.decimals,
216 );
217 }
218
219 return Money(amount ?? this.amount, currency ?? this.currency, decimals ?? this.decimals);
220 }
221
222 void _assertSameCurrency(Money other, [String? message]) {
223 if (currency != other.currency) {
224 throw ArgumentError(message ?? "Cannot operate with money values in different currencies.");
225 }
226 }
227
228 static BigInt _transformAmount(BigInt source, int sourceDecimals, int targetDecimals) {
229 final diff = targetDecimals - sourceDecimals;
230
231 return diff == 0
232 ? source
233 : diff > 0
234 ? source * BigInt.from(10).pow(diff)
235 : source ~/ BigInt.from(10).pow(-diff);
236 }
237
238 ({BigInt a, BigInt b, int decimals}) _align(Money other) {
239 final decimals = max(this.decimals, other.decimals);
240 return (
241 a: _transformAmount(amount, this.decimals, decimals),
242 b: _transformAmount(other.amount, other.decimals, decimals),
243 decimals: decimals,
244 );
245 }
246
247 static int _getActualDecimals(String value, Currency currency) {
248 final comps = value.split(".");
249 if (comps.length > 2) {
250 throw FormatException("Money._getActualDecimals: too many decimal points, value, $value");
251 }
252
253 return max(comps.length == 2 ? comps[1].length : 0, currency.decimals);
254 }
255
256 @override
257 int get hashCode {
258 final value = formatFixed(amount, this.decimals, trimZeros: true);
259 final decimals = _getActualDecimals(value, currency);
260
261 return Object.hash(value, decimals, currency);
262 }
263
264 @override
265 String toString() => formatFixed(amount, decimals);
266
267 String toStringWithSymbol({
268 int? fractionalDigits,
269 bool trimZeros = true,
270 bool useBaseUnit = false,
271 bool withSymbolPrefix = false,
272 }) {
273 final amount = toStringWithPrecision(
274 fractionalDigits: fractionalDigits,
275 trimZeros: trimZeros,
276 useBaseUnit: useBaseUnit,
277 );
278 final symbol = getSymbol(useBaseUnit: useBaseUnit);
279
280 return withSymbolPrefix ? "$symbol $amount" : "$amount $symbol";
281 }
282
283 String toStringWithPrecision({
284 int? fractionalDigits,
285 bool trimZeros = true,
286 bool useBaseUnit = false,
287 }) =>
288 formatFixed(
289 decimals != currency.decimals
290 ? _transformAmount(amount, decimals, currency.decimals)
291 : amount,
292 useBaseUnit ? 0 : currency.decimals,
293 fractionalDigits: fractionalDigits,
294 trimZeros: trimZeros,
295 );
296
297 // To Override the symbol with the ticker of the base unit
298 String getSymbol({required bool useBaseUnit}) {
299 if (useBaseUnit && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) {
300 return "sats";
301 }
302 return currency.symbol;
303 }
304 }