| 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 | } |