amounts 0.0.1
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 +7 -0
- data/.rubocop.yml +89 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +402 -0
- data/Rakefile +18 -0
- data/bin/console +8 -0
- data/bin/setup +4 -0
- data/lib/amount/active_record/amount_validator.rb +115 -0
- data/lib/amount/active_record/attribute_definition.rb +192 -0
- data/lib/amount/active_record/migration_methods.rb +74 -0
- data/lib/amount/active_record/model.rb +140 -0
- data/lib/amount/active_record/rspec.rb +8 -0
- data/lib/amount/active_record/type.rb +106 -0
- data/lib/amount/active_record.rb +44 -0
- data/lib/amount/display.rb +82 -0
- data/lib/amount/parser.rb +50 -0
- data/lib/amount/registry/generated_constructors.rb +62 -0
- data/lib/amount/registry.rb +236 -0
- data/lib/amount/rspec.rb +27 -0
- data/lib/amount/rspec_matchers.rb +105 -0
- data/lib/amount/rspec_support.rb +47 -0
- data/lib/amount/serializer.rb +35 -0
- data/lib/amount/version.rb +5 -0
- data/lib/amount.rb +494 -0
- data/test/dummy/app/models/holding.rb +7 -0
- data/test/dummy/bin/rails +8 -0
- data/test/dummy/config/application.rb +15 -0
- data/test/dummy/config/database.yml +11 -0
- data/test/dummy/config/environment.rb +6 -0
- data/test/dummy/db/schema.rb +9 -0
- data/test/dummy/log/development.log +0 -0
- data/test/dummy/log/test.log +0 -0
- data/test/postgresql_integration_test.rb +71 -0
- data/test/support/amount_test_support.rb +38 -0
- data/test/test_active_record.rb +312 -0
- data/test/test_amount.rb +472 -0
- data/test/test_helper.rb +4 -0
- metadata +105 -0
data/test/test_amount.rb
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class AmountTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
AmountTestSupport.register_default_types!
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def teardown
|
|
11
|
+
Amount.registry.clear!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_construct_from_integer_treats_as_atomic
|
|
15
|
+
amount = Amount.new(1_000_000, :USDC)
|
|
16
|
+
|
|
17
|
+
assert_equal 1_000_000, amount.atomic
|
|
18
|
+
assert_equal BigDecimal("1"), amount.decimal
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_construct_from_string_treats_as_ui
|
|
22
|
+
assert_equal 1_500_000, Amount.new("1.50", :USDC).atomic
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_construct_from_float_treats_as_decimal
|
|
26
|
+
assert_equal 1_500_000, Amount.new(1.5, :USDC).atomic
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_explicit_from_overrides_inference
|
|
30
|
+
amount = Amount.new("1500000", :USDC, from: :atomic)
|
|
31
|
+
|
|
32
|
+
assert_equal 1_500_000, amount.atomic
|
|
33
|
+
assert_equal BigDecimal("1.5"), amount.decimal
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_parse_roundtrips_to_s
|
|
37
|
+
amount = Amount.parse("USDC|1.5")
|
|
38
|
+
|
|
39
|
+
assert_equal 1_500_000, amount.atomic
|
|
40
|
+
assert_equal "USDC|1.5", amount.to_s
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_parse_accepts_explicit_v1_prefix
|
|
44
|
+
amount = Amount.parse("v1:USDC|1.5")
|
|
45
|
+
|
|
46
|
+
assert_equal Amount.new("1.5", :USDC), amount
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_parse_rejects_unknown_version_prefix
|
|
50
|
+
assert_raises(Amount::InvalidInput) { Amount.parse("v2:USDC|1.5") }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_generated_constructor_matches_new
|
|
54
|
+
assert_equal Amount.new("1.50", :USDC), Amount.usdc("1.50")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_generated_constructor_accepts_from_keyword
|
|
58
|
+
assert_equal 1_500_000, Amount.usdc(1_500_000, from: :atomic).atomic
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_multi_word_symbol_generates_constructor
|
|
62
|
+
Amount.register :OIL_WTI_BBL, decimals: 4
|
|
63
|
+
|
|
64
|
+
assert_equal Amount.new("1.5", :OIL_WTI_BBL), Amount.oil_wti_bbl("1.5")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_non_method_safe_symbol_skips_constructor_generation
|
|
68
|
+
Amount.register :'USDC.e', decimals: 6
|
|
69
|
+
|
|
70
|
+
refute Amount.respond_to?(:usdc_e)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_clear_removes_generated_constructor_methods
|
|
74
|
+
assert Amount.respond_to?(:usdc)
|
|
75
|
+
|
|
76
|
+
Amount.registry.clear!
|
|
77
|
+
|
|
78
|
+
refute Amount.respond_to?(:usdc)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_registry_lock_prevents_mutation
|
|
82
|
+
Amount.registry.lock!
|
|
83
|
+
|
|
84
|
+
assert Amount.registry.locked?
|
|
85
|
+
assert_raises(Amount::Registry::RegistryLocked) { Amount.register(:DOGE, decimals: 8) }
|
|
86
|
+
assert_raises(Amount::Registry::RegistryLocked) { Amount.register_default_rate(:USD, :USDC, 1) }
|
|
87
|
+
assert_raises(Amount::Registry::RegistryLocked) { Amount.registry.clear! }
|
|
88
|
+
ensure
|
|
89
|
+
Amount.send(:replace_registry, Amount::Registry.new)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_constructor_collision_raises_at_registration_time
|
|
93
|
+
assert_raises(Amount::Registry::AlreadyRegistered) do
|
|
94
|
+
Amount.register :NEW, decimals: 2
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_inspect_uses_default_ui_representation
|
|
99
|
+
assert_equal "#<Amount USDC $1.50>", Amount.new("1.5", :USDC).inspect
|
|
100
|
+
assert_equal "#<Amount GOLD 1.0000 oz t>", Amount.new("1", :GOLD).inspect
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_parse_rejects_missing_value
|
|
104
|
+
assert_raises(Amount::InvalidInput) { Amount.parse("USDC|") }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def test_parse_rejects_missing_symbol
|
|
108
|
+
assert_raises(Amount::InvalidInput) { Amount.parse("|1.5") }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def test_unregistered_type_raises
|
|
112
|
+
assert_raises(Amount::Registry::UnknownType) { Amount.new(1, :DOGE) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def test_formatted_respects_storage_decimals
|
|
116
|
+
assert_equal "1.500000", Amount.new("1.5", :USDC).formatted
|
|
117
|
+
assert_equal "1.500000000", Amount.new("1.5", :SOL).formatted
|
|
118
|
+
assert_equal "1", Amount.new(1, :LOGS).formatted
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_ui_respects_ui_decimals_and_position
|
|
122
|
+
assert_equal "$1.50", Amount.new("1.5", :USDC).ui
|
|
123
|
+
assert_equal "$1.56", Amount.new("1.567", :USDC).ui
|
|
124
|
+
assert_equal "1.5000 SOL", Amount.new("1.5", :SOL).ui
|
|
125
|
+
assert_equal "1 logs", Amount.new(1, :LOGS).ui
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def test_ui_ceil_direction
|
|
129
|
+
assert_equal "$1.57", Amount.new("1.561", :USDC).ui(direction: :ceil)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def test_display_units_scale_output
|
|
133
|
+
gold = Amount.new("1.5", :GOLD)
|
|
134
|
+
|
|
135
|
+
assert_equal "1.5000 oz t", gold.ui
|
|
136
|
+
assert_equal "46.65 g", gold.ui(unit: :gram)
|
|
137
|
+
assert_equal "0.04665 kg", gold.ui(unit: :kg)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def test_in_unit_returns_raw_bigdecimal
|
|
141
|
+
gold = Amount.new("1.0", :GOLD)
|
|
142
|
+
|
|
143
|
+
assert_in_delta 31.1035, gold.in_unit(:gram).to_f, 0.0001
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def test_unknown_display_unit_raises
|
|
147
|
+
assert_raises(Amount::Registry::InvalidDisplayUnit) do
|
|
148
|
+
Amount.new("1", :GOLD).ui(unit: :pound)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def test_predicates
|
|
153
|
+
assert Amount.new(0, :USDC).zero?
|
|
154
|
+
assert Amount.new(1, :USDC).positive?
|
|
155
|
+
assert Amount.new(-1, :USDC).negative?
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def test_same_type
|
|
159
|
+
assert Amount.new(1, :USDC).same_type?(Amount.new(2, :USDC))
|
|
160
|
+
refute Amount.new(1, :USDC).same_type?(Amount.new(1, :SOL))
|
|
161
|
+
refute Amount.new(1, :USDC).same_type?(1)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def test_addition_same_type
|
|
165
|
+
assert_equal Amount.new("2.0", :USDC), Amount.new("1.5", :USDC) + Amount.new("0.5", :USDC)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def test_addition_cross_type_uses_registered_rate
|
|
169
|
+
Amount.register_default_rate :USD, :USDC, 1
|
|
170
|
+
|
|
171
|
+
result = Amount.new("10.00", :USDC) + Amount.new("5.00", :USD)
|
|
172
|
+
|
|
173
|
+
assert_equal Amount.new("15.00", :USDC), result
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def test_addition_different_type_raises_without_rate
|
|
177
|
+
assert_raises(Amount::TypeMismatch) do
|
|
178
|
+
Amount.new(1, :USDC) + Amount.new(1, :SOL)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def test_subtraction_cross_type_uses_registered_rate
|
|
183
|
+
Amount.register_default_rate :USD, :USDC, 1
|
|
184
|
+
|
|
185
|
+
result = Amount.new("10.00", :USDC) - Amount.new("5.00", :USD)
|
|
186
|
+
|
|
187
|
+
assert_equal Amount.new("5.00", :USDC), result
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def test_subtraction_different_type_raises_without_rate
|
|
191
|
+
assert_raises(Amount::TypeMismatch) do
|
|
192
|
+
Amount.new(1, :USDC) - Amount.new(1, :USD)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def test_asymmetric_rate_registration_only_allows_configured_direction
|
|
197
|
+
Amount.register_default_rate :USD, :USDC, 1
|
|
198
|
+
|
|
199
|
+
assert_equal Amount.new("15.00", :USDC), Amount.new("10.00", :USDC) + Amount.new("5.00", :USD)
|
|
200
|
+
|
|
201
|
+
assert_raises(Amount::TypeMismatch) do
|
|
202
|
+
Amount.new("5.00", :USD) + Amount.new("10.00", :USDC)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def test_unary_minus
|
|
207
|
+
assert_equal Amount.new("-1", :USDC), -Amount.new("1", :USDC)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def test_abs
|
|
211
|
+
assert_equal Amount.new("1", :USDC), Amount.new("-1", :USDC).abs
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def test_scalar_multiplication
|
|
215
|
+
assert_equal Amount.new("3", :USDC), Amount.new("1", :USDC) * 3
|
|
216
|
+
assert_equal Amount.new("1.5", :USDC), Amount.new("1", :USDC) * 1.5
|
|
217
|
+
assert_equal Amount.new("2", :USDC), Amount.new("-1", :USDC) * -2
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def test_amount_times_amount_raises
|
|
221
|
+
assert_raises(Amount::TypeMismatch) do
|
|
222
|
+
Amount.new("1", :USDC) * Amount.new("1", :USDC)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def test_scalar_division_returns_amount
|
|
227
|
+
assert_equal Amount.new("0.5", :USDC), Amount.new("1", :USDC) / 2
|
|
228
|
+
assert_equal Amount.new("2", :USDC), Amount.new("-1", :USDC) / -0.5
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def test_amount_division_returns_ratio
|
|
232
|
+
ratio = Amount.new("10", :USDC) / Amount.new("2", :USDC)
|
|
233
|
+
|
|
234
|
+
assert_equal BigDecimal(5), ratio
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def test_division_by_zero
|
|
238
|
+
assert_raises(ZeroDivisionError) { Amount.new(1, :USDC) / 0 }
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def test_split_preserves_total_exactly_with_remainder
|
|
242
|
+
amount = Amount.new(10, :LOGS)
|
|
243
|
+
parts, remainder = amount.split(3)
|
|
244
|
+
|
|
245
|
+
assert_equal [3, 3, 3], parts.map(&:atomic)
|
|
246
|
+
assert_equal 1, remainder.atomic
|
|
247
|
+
assert_equal amount.atomic, parts.sum(&:atomic) + remainder.atomic
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def test_split_even_division_returns_zero_remainder
|
|
251
|
+
parts, remainder = Amount.new(9, :LOGS).split(3)
|
|
252
|
+
|
|
253
|
+
assert_equal [3, 3, 3], parts.map(&:atomic)
|
|
254
|
+
assert_equal 0, remainder.atomic
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def test_split_one_returns_same_amount_and_zero_remainder
|
|
258
|
+
parts, remainder = Amount.new(9, :LOGS).split(1)
|
|
259
|
+
|
|
260
|
+
assert_equal [9], parts.map(&:atomic)
|
|
261
|
+
assert_equal 0, remainder.atomic
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def test_split_negative_amount_rounds_toward_zero_with_negative_remainder
|
|
265
|
+
parts, remainder = Amount.new(-10, :LOGS).split(3)
|
|
266
|
+
|
|
267
|
+
assert_equal [-3, -3, -3], parts.map(&:atomic)
|
|
268
|
+
assert_equal(-1, remainder.atomic)
|
|
269
|
+
assert_equal(-10, parts.sum(&:atomic) + remainder.atomic)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def test_allocate_by_weights
|
|
273
|
+
amount = Amount.new(100, :LOGS)
|
|
274
|
+
parts, remainder = amount.allocate([1, 1, 2])
|
|
275
|
+
|
|
276
|
+
assert_equal [25, 25, 50], parts.map(&:atomic)
|
|
277
|
+
assert_equal 0, remainder.atomic
|
|
278
|
+
assert_equal amount.atomic, parts.sum(&:atomic) + remainder.atomic
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def test_allocate_remainder_is_explicit
|
|
282
|
+
parts, remainder = Amount.new(10, :LOGS).allocate([1, 1, 1])
|
|
283
|
+
|
|
284
|
+
assert_equal [3, 3, 3], parts.map(&:atomic)
|
|
285
|
+
assert_equal 1, remainder.atomic
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def test_allocate_allows_zero_weights
|
|
289
|
+
parts, remainder = Amount.new(10, :LOGS).allocate([0, 1])
|
|
290
|
+
|
|
291
|
+
assert_equal [0, 10], parts.map(&:atomic)
|
|
292
|
+
assert_equal 0, remainder.atomic
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def test_allocate_negative_amount_rounds_toward_zero_with_negative_remainder
|
|
296
|
+
parts, remainder = Amount.new(-10, :LOGS).allocate([1, 1, 1])
|
|
297
|
+
|
|
298
|
+
assert_equal [-3, -3, -3], parts.map(&:atomic)
|
|
299
|
+
assert_equal(-1, remainder.atomic)
|
|
300
|
+
assert_equal(-10, parts.sum(&:atomic) + remainder.atomic)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def test_comparison_same_type
|
|
304
|
+
left = Amount.new("1", :USDC)
|
|
305
|
+
right = Amount.new("2", :USDC)
|
|
306
|
+
|
|
307
|
+
assert left < right
|
|
308
|
+
assert right > left
|
|
309
|
+
assert_equal Amount.new("1", :USDC), left
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def test_comparison_cross_type_uses_registered_rate
|
|
313
|
+
Amount.register_default_rate :USD, :USDC, 1
|
|
314
|
+
|
|
315
|
+
assert_operator Amount.new("10.00", :USDC), :>, Amount.new("5.00", :USD)
|
|
316
|
+
assert_equal 0, Amount.new("5.00", :USDC) <=> Amount.new("5.00", :USD)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def test_comparison_different_type_nil_without_rate
|
|
320
|
+
assert_nil(Amount.new(1, :USDC) <=> Amount.new(1, :SOL))
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def test_sorting
|
|
324
|
+
amounts = [Amount.new("3", :USDC), Amount.new("1", :USDC), Amount.new("2", :USDC)]
|
|
325
|
+
|
|
326
|
+
assert_equal ["1.0", "2.0", "3.0"], amounts.sort.map { |amount| amount.decimal.to_s("F") }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def test_to_same_symbol_is_identity
|
|
330
|
+
amount = Amount.new("1", :USDC)
|
|
331
|
+
|
|
332
|
+
assert_equal amount, amount.to(:USDC)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def test_to_with_explicit_rate
|
|
336
|
+
gold = Amount.new("1000", :USDC).to(:GOLD, rate: "0.00042")
|
|
337
|
+
|
|
338
|
+
assert_equal BigDecimal("0.42"), gold.decimal
|
|
339
|
+
assert_equal :GOLD, gold.symbol
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def test_to_without_rate_requires_default
|
|
343
|
+
assert_raises(Amount::Registry::NoDefaultRate) do
|
|
344
|
+
Amount.new("1", :USDC).to(:GOLD)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def test_to_uses_registered_default_rate
|
|
349
|
+
Amount.register_default_rate :USDC, :USD, 1
|
|
350
|
+
|
|
351
|
+
usd = Amount.new("1.5", :USDC).to(:USD)
|
|
352
|
+
|
|
353
|
+
assert_equal BigDecimal("1.5"), usd.decimal
|
|
354
|
+
assert_equal :USD, usd.symbol
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def test_cross_type_addition_via_explicit_conversion
|
|
358
|
+
gold = Amount.new("0.0001", :GOLD)
|
|
359
|
+
usdc = Amount.new("100", :USDC)
|
|
360
|
+
result = gold + usdc.to(:GOLD, rate: "0.00000042")
|
|
361
|
+
|
|
362
|
+
assert_equal BigDecimal("0.000142"), result.decimal
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def test_hash_equality_semantics
|
|
366
|
+
one = Amount.new("1.5", :USDC)
|
|
367
|
+
two = Amount.new("1.5", :USDC)
|
|
368
|
+
three = Amount.new("1.5", :USD)
|
|
369
|
+
|
|
370
|
+
data = { one => :first }
|
|
371
|
+
|
|
372
|
+
assert_equal :first, data[two]
|
|
373
|
+
assert_nil data[three]
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def test_to_h_and_load_round_trip_for_large_negative_amount
|
|
377
|
+
amount = Amount.new(-(10**30), :USDC, from: :atomic)
|
|
378
|
+
|
|
379
|
+
assert_equal amount, Amount.load(amount.to_h)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def test_to_h_returns_versioned_string_safe_payload
|
|
383
|
+
assert_equal(
|
|
384
|
+
{ v: 1, atomic: "1500000", symbol: "USDC" },
|
|
385
|
+
Amount.new("1.5", :USDC).to_h
|
|
386
|
+
)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def test_load_accepts_legacy_unversioned_payload
|
|
390
|
+
amount = Amount.load(atomic: 1_500_000, symbol: :USDC)
|
|
391
|
+
|
|
392
|
+
assert_equal Amount.new("1.5", :USDC), amount
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def test_load_rejects_unknown_serialization_version
|
|
396
|
+
assert_raises(Amount::InvalidInput) do
|
|
397
|
+
Amount.load(v: 2, atomic: "1500000", symbol: "USDC")
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def test_very_large_atomic_value_preserves_precision
|
|
402
|
+
amount = Amount.new(10**30, :SOL, from: :atomic)
|
|
403
|
+
|
|
404
|
+
assert_equal 10**30, amount.atomic
|
|
405
|
+
assert_equal "1000000000000000000000.0", amount.decimal.to_s("F")
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def test_registry_can_be_read_from_multiple_threads
|
|
409
|
+
errors = Queue.new
|
|
410
|
+
|
|
411
|
+
threads = 10.times.map do
|
|
412
|
+
Thread.new do
|
|
413
|
+
100.times do
|
|
414
|
+
Amount.registry.lookup(:USDC)
|
|
415
|
+
Amount.registry.default_rate?(:USDC, :USD)
|
|
416
|
+
rescue StandardError => error
|
|
417
|
+
errors << error
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
threads.each(&:join)
|
|
423
|
+
|
|
424
|
+
assert errors.empty?, "expected no thread errors, got #{errors.size}"
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
class AmountCustomClassTest < Minitest::Test
|
|
429
|
+
class GoldAmount < Amount
|
|
430
|
+
def purity_estimate
|
|
431
|
+
"24k"
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def setup
|
|
436
|
+
Amount.registry.clear!
|
|
437
|
+
Amount.register :GOLD, decimals: 8, display_symbol: "oz t",
|
|
438
|
+
display_position: :suffix, ui_decimals: 4,
|
|
439
|
+
class: GoldAmount
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def teardown
|
|
443
|
+
Amount.registry.clear!
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def test_subclass_instance_from_own_new
|
|
447
|
+
gold = GoldAmount.new("1", :GOLD)
|
|
448
|
+
|
|
449
|
+
assert_instance_of GoldAmount, gold
|
|
450
|
+
assert_equal "24k", gold.purity_estimate
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def test_arithmetic_returns_subclass
|
|
454
|
+
gold = GoldAmount.new("1", :GOLD)
|
|
455
|
+
|
|
456
|
+
assert_instance_of GoldAmount, gold + gold
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def test_generated_constructor_returns_subclass
|
|
460
|
+
gold = Amount.gold("1", from: :ui)
|
|
461
|
+
|
|
462
|
+
assert_instance_of GoldAmount, gold
|
|
463
|
+
assert_equal "24k", gold.purity_estimate
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def test_split_returns_subclass_parts_and_remainder
|
|
467
|
+
parts, remainder = GoldAmount.new("3", :GOLD).split(2)
|
|
468
|
+
|
|
469
|
+
assert parts.all? { |part| part.instance_of?(GoldAmount) }
|
|
470
|
+
assert_instance_of GoldAmount, remainder
|
|
471
|
+
end
|
|
472
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: amounts
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Seb Scholl
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-25 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bigdecimal
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
description: |
|
|
28
|
+
Amounts provides a precise Amount value object with atomic integer storage,
|
|
29
|
+
safe registered-type arithmetic, explicit conversion rates, display helpers,
|
|
30
|
+
and an optional ActiveRecord integration layer.
|
|
31
|
+
email:
|
|
32
|
+
- opensource@example.com
|
|
33
|
+
executables:
|
|
34
|
+
- console
|
|
35
|
+
- setup
|
|
36
|
+
extensions: []
|
|
37
|
+
extra_rdoc_files: []
|
|
38
|
+
files:
|
|
39
|
+
- ".rubocop.yml"
|
|
40
|
+
- CHANGELOG.md
|
|
41
|
+
- Gemfile
|
|
42
|
+
- LICENSE.txt
|
|
43
|
+
- README.md
|
|
44
|
+
- Rakefile
|
|
45
|
+
- bin/console
|
|
46
|
+
- bin/setup
|
|
47
|
+
- lib/amount.rb
|
|
48
|
+
- lib/amount/active_record.rb
|
|
49
|
+
- lib/amount/active_record/amount_validator.rb
|
|
50
|
+
- lib/amount/active_record/attribute_definition.rb
|
|
51
|
+
- lib/amount/active_record/migration_methods.rb
|
|
52
|
+
- lib/amount/active_record/model.rb
|
|
53
|
+
- lib/amount/active_record/rspec.rb
|
|
54
|
+
- lib/amount/active_record/type.rb
|
|
55
|
+
- lib/amount/display.rb
|
|
56
|
+
- lib/amount/parser.rb
|
|
57
|
+
- lib/amount/registry.rb
|
|
58
|
+
- lib/amount/registry/generated_constructors.rb
|
|
59
|
+
- lib/amount/rspec.rb
|
|
60
|
+
- lib/amount/rspec_matchers.rb
|
|
61
|
+
- lib/amount/rspec_support.rb
|
|
62
|
+
- lib/amount/serializer.rb
|
|
63
|
+
- lib/amount/version.rb
|
|
64
|
+
- test/dummy/app/models/holding.rb
|
|
65
|
+
- test/dummy/bin/rails
|
|
66
|
+
- test/dummy/config/application.rb
|
|
67
|
+
- test/dummy/config/database.yml
|
|
68
|
+
- test/dummy/config/environment.rb
|
|
69
|
+
- test/dummy/db/schema.rb
|
|
70
|
+
- test/dummy/log/development.log
|
|
71
|
+
- test/dummy/log/test.log
|
|
72
|
+
- test/postgresql_integration_test.rb
|
|
73
|
+
- test/support/amount_test_support.rb
|
|
74
|
+
- test/test_active_record.rb
|
|
75
|
+
- test/test_amount.rb
|
|
76
|
+
- test/test_helper.rb
|
|
77
|
+
homepage: https://github.com/zarpay/amounts
|
|
78
|
+
licenses:
|
|
79
|
+
- MIT
|
|
80
|
+
metadata:
|
|
81
|
+
bug_tracker_uri: https://github.com/zarpay/amounts/issues
|
|
82
|
+
changelog_uri: https://github.com/zarpay/amounts/blob/main/CHANGELOG.md
|
|
83
|
+
documentation_uri: https://github.com/zarpay/amounts#readme
|
|
84
|
+
source_code_uri: https://github.com/zarpay/amounts
|
|
85
|
+
post_install_message:
|
|
86
|
+
rdoc_options: []
|
|
87
|
+
require_paths:
|
|
88
|
+
- lib
|
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '3.1'
|
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '0'
|
|
99
|
+
requirements: []
|
|
100
|
+
rubygems_version: 3.5.22
|
|
101
|
+
signing_key:
|
|
102
|
+
specification_version: 4
|
|
103
|
+
summary: Precise registered quantities for money, tokens, commodities, and other fungible
|
|
104
|
+
things.
|
|
105
|
+
test_files: []
|