amounts 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/lib/amount/display.rb +2 -2
- data/lib/amount/registry.rb +1 -1
- data/lib/amount/serializer.rb +2 -0
- data/lib/amount/version.rb +1 -1
- data/lib/amount.rb +19 -3
- data/test/test_amount.rb +39 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 95dbf17ecb45d86b36277c6ecdaf611bdcb99d12eb97a3fc42b5f3bf2cd1ec50
|
|
4
|
+
data.tar.gz: 67fcceae2bae4582f4c4e2866f453727a23f444703e47717ab9e7420e8ff20de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15833dd17e8b167cfc61812af65dc0addad29912459a3886df4bd54b01cd8c43d055654cd971838a1c9b5fc76bca8cf83e8eaa72e25eaa4901eef93177275e4a
|
|
7
|
+
data.tar.gz: 002e0c5539b2ed22551a87d0d696a8cbfd2b87565b3c991a91fbbacb3c81e722d3b8b1af62bf1aa0604e08691805bfb2375cf2015f06bd57c62228525b9454ca
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.3 - 2026-04-26
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Rational scalars and rates are now accepted everywhere they are documented to
|
|
8
|
+
work: `Amount#*`, `Amount#/`, `Amount#to(rate:)`, `Amount.register_default_rate`,
|
|
9
|
+
and `display_units[unit][:scale]`. Previously each of these called
|
|
10
|
+
`BigDecimal(value.to_s)`, which raises `ArgumentError` for `Rational#to_s`
|
|
11
|
+
(`"3/2"`). All five sites now go through a single internal helper,
|
|
12
|
+
`Amount.coerce_decimal`, that handles `BigDecimal`, `Rational`, and string-or-
|
|
13
|
+
numeric inputs uniformly.
|
|
14
|
+
- `Amount.load` wraps a missing `:atomic` or `:symbol` key as
|
|
15
|
+
`Amount::InvalidInput` (`"amount payload missing key: atomic"`) instead of
|
|
16
|
+
leaking a raw `KeyError`. This matches the AR adapter's `Type#cast_hash`
|
|
17
|
+
behavior so callers can rescue a single error class.
|
|
18
|
+
|
|
3
19
|
## 0.0.2 - 2026-04-26
|
|
4
20
|
|
|
5
21
|
### Fixed
|
data/lib/amount/display.rb
CHANGED
|
@@ -32,7 +32,7 @@ class Amount
|
|
|
32
32
|
# @return [BigDecimal]
|
|
33
33
|
def in_unit(unit)
|
|
34
34
|
unit_spec = fetch_display_unit(unit)
|
|
35
|
-
@amount.decimal *
|
|
35
|
+
@amount.decimal * Amount.coerce_decimal(unit_spec[:scale])
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
private
|
|
@@ -44,7 +44,7 @@ class Amount
|
|
|
44
44
|
|
|
45
45
|
def render_display_unit(unit, direction)
|
|
46
46
|
spec = fetch_display_unit(unit)
|
|
47
|
-
scaled = @amount.decimal *
|
|
47
|
+
scaled = @amount.decimal * Amount.coerce_decimal(spec[:scale])
|
|
48
48
|
decimals = spec[:ui_decimals] || @entry.ui_decimals
|
|
49
49
|
rounded = round(scaled, decimals, direction)
|
|
50
50
|
|
data/lib/amount/registry.rb
CHANGED
data/lib/amount/serializer.rb
CHANGED
data/lib/amount/version.rb
CHANGED
data/lib/amount.rb
CHANGED
|
@@ -102,6 +102,22 @@ class Amount
|
|
|
102
102
|
super
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
# Coerces a numeric input to BigDecimal in a way that preserves Rational
|
|
106
|
+
# values. `BigDecimal(value.to_s)` raises `ArgumentError` for Rational
|
|
107
|
+
# because `Rational#to_s` produces strings like `"3/2"`. This helper is
|
|
108
|
+
# the single place every call site should use to convert a scalar / rate /
|
|
109
|
+
# display-unit scale into a BigDecimal.
|
|
110
|
+
#
|
|
111
|
+
# @param value [Numeric, BigDecimal, Rational, String]
|
|
112
|
+
# @return [BigDecimal]
|
|
113
|
+
def coerce_decimal(value)
|
|
114
|
+
case value
|
|
115
|
+
when BigDecimal then value
|
|
116
|
+
when Rational then BigDecimal(value, Float::DIG + 4)
|
|
117
|
+
else BigDecimal(value.to_s)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
105
121
|
# Temporarily swaps the global registry. Intended for tests.
|
|
106
122
|
#
|
|
107
123
|
# @param registry [Amount::Registry]
|
|
@@ -281,7 +297,7 @@ class Amount
|
|
|
281
297
|
# Amount.usdc("1.25") * 2
|
|
282
298
|
def *(scalar)
|
|
283
299
|
ensure_scalar!(scalar)
|
|
284
|
-
build((BigDecimal(@atomic) *
|
|
300
|
+
build((BigDecimal(@atomic) * Amount.coerce_decimal(scalar)).to_i)
|
|
285
301
|
end
|
|
286
302
|
|
|
287
303
|
# @param other [Amount, Numeric]
|
|
@@ -302,7 +318,7 @@ class Amount
|
|
|
302
318
|
ensure_scalar!(other)
|
|
303
319
|
raise ZeroDivisionError if other.zero?
|
|
304
320
|
|
|
305
|
-
build((BigDecimal(@atomic) /
|
|
321
|
+
build((BigDecimal(@atomic) / Amount.coerce_decimal(other)).to_i)
|
|
306
322
|
end
|
|
307
323
|
end
|
|
308
324
|
|
|
@@ -411,7 +427,7 @@ class Amount
|
|
|
411
427
|
rate = resolve_rate(target_symbol, rate)
|
|
412
428
|
target_entry = self.class.registry.lookup(target_symbol)
|
|
413
429
|
|
|
414
|
-
decimal_result = decimal *
|
|
430
|
+
decimal_result = decimal * Amount.coerce_decimal(rate)
|
|
415
431
|
atomic_result = (decimal_result * (BigDecimal(10)**target_entry.decimals)).to_i
|
|
416
432
|
|
|
417
433
|
target_entry.amount_class.new(atomic_result, target_symbol, from: :atomic)
|
data/test/test_amount.rb
CHANGED
|
@@ -229,6 +229,8 @@ class AmountTest < Minitest::Test
|
|
|
229
229
|
assert_equal Amount.new("3", :USDC), Amount.new("1", :USDC) * 3
|
|
230
230
|
assert_equal Amount.new("1.5", :USDC), Amount.new("1", :USDC) * 1.5
|
|
231
231
|
assert_equal Amount.new("2", :USDC), Amount.new("-1", :USDC) * -2
|
|
232
|
+
assert_equal Amount.new("3", :USDC), Amount.new("2", :USDC) * BigDecimal("1.5")
|
|
233
|
+
assert_equal Amount.new("3", :USDC), Amount.new("2", :USDC) * Rational(3, 2)
|
|
232
234
|
end
|
|
233
235
|
|
|
234
236
|
def test_amount_times_amount_raises
|
|
@@ -240,6 +242,8 @@ class AmountTest < Minitest::Test
|
|
|
240
242
|
def test_scalar_division_returns_amount
|
|
241
243
|
assert_equal Amount.new("0.5", :USDC), Amount.new("1", :USDC) / 2
|
|
242
244
|
assert_equal Amount.new("2", :USDC), Amount.new("-1", :USDC) / -0.5
|
|
245
|
+
assert_equal Amount.new("2", :USDC), Amount.new("3", :USDC) / Rational(3, 2)
|
|
246
|
+
assert_equal Amount.new("2", :USDC), Amount.new("3", :USDC) / BigDecimal("1.5")
|
|
243
247
|
end
|
|
244
248
|
|
|
245
249
|
def test_amount_division_returns_ratio
|
|
@@ -353,6 +357,33 @@ class AmountTest < Minitest::Test
|
|
|
353
357
|
assert_equal :GOLD, gold.symbol
|
|
354
358
|
end
|
|
355
359
|
|
|
360
|
+
def test_to_with_rational_rate
|
|
361
|
+
converted = Amount.new("4", :USDC).to(:USD, rate: Rational(1, 2))
|
|
362
|
+
|
|
363
|
+
assert_equal BigDecimal("2"), converted.decimal
|
|
364
|
+
assert_equal :USD, converted.symbol
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def test_register_default_rate_accepts_rational
|
|
368
|
+
Amount.register_default_rate :USDC, :USD, Rational(1, 2)
|
|
369
|
+
|
|
370
|
+
assert_equal BigDecimal("0.5"), Amount.registry.default_rate(:USDC, :USD)
|
|
371
|
+
assert_equal BigDecimal("0.5"), Amount.new("1", :USDC).to(:USD).decimal
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def test_display_units_with_rational_scale
|
|
375
|
+
Amount.registry.clear!
|
|
376
|
+
Amount.register :METAL, decimals: 4, display_symbol: "x", display_position: :suffix,
|
|
377
|
+
ui_decimals: 2,
|
|
378
|
+
display_units: { half: { scale: Rational(1, 2), symbol: "h", ui_decimals: 2 } },
|
|
379
|
+
default_display: :half
|
|
380
|
+
|
|
381
|
+
metal = Amount.new("1", :METAL)
|
|
382
|
+
|
|
383
|
+
assert_equal BigDecimal("0.5"), metal.in_unit(:half)
|
|
384
|
+
assert_match(/\A0\.50/, metal.ui(unit: :half))
|
|
385
|
+
end
|
|
386
|
+
|
|
356
387
|
def test_to_without_rate_requires_default
|
|
357
388
|
assert_raises(Amount::Registry::NoDefaultRate) do
|
|
358
389
|
Amount.new("1", :USDC).to(:GOLD)
|
|
@@ -406,6 +437,14 @@ class AmountTest < Minitest::Test
|
|
|
406
437
|
assert_equal Amount.new("1.5", :USDC), amount
|
|
407
438
|
end
|
|
408
439
|
|
|
440
|
+
def test_load_wraps_missing_keys_as_invalid_input
|
|
441
|
+
error = assert_raises(Amount::InvalidInput) { Amount.load(v: 1, symbol: "USDC") }
|
|
442
|
+
assert_match(/missing key: atomic/, error.message)
|
|
443
|
+
|
|
444
|
+
error = assert_raises(Amount::InvalidInput) { Amount.load({}) }
|
|
445
|
+
assert_match(/missing key/, error.message)
|
|
446
|
+
end
|
|
447
|
+
|
|
409
448
|
def test_load_rejects_unknown_serialization_version
|
|
410
449
|
assert_raises(Amount::InvalidInput) do
|
|
411
450
|
Amount.load(v: 2, atomic: "1500000", symbol: "USDC")
|