amounts 0.0.7 → 0.1.0

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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +37 -0
  3. data/CHANGELOG.md +142 -1
  4. data/Gemfile +4 -0
  5. data/README.md +218 -4
  6. data/Rakefile +47 -10
  7. data/lib/amount/active_record/amount_validator.rb +9 -18
  8. data/lib/amount/active_record/attribute_definition.rb +3 -1
  9. data/lib/amount/active_record/locale/en.yml +7 -0
  10. data/lib/amount/active_record/model.rb +9 -0
  11. data/lib/amount/active_record/railtie.rb +1 -1
  12. data/lib/amount/active_record.rb +12 -0
  13. data/lib/amount/compact.rb +137 -0
  14. data/lib/amount/{allocation.rb → concerns/allocatable.rb} +1 -1
  15. data/lib/amount/{arithmetic.rb → concerns/calculable.rb} +9 -9
  16. data/lib/amount/concerns/coercible.rb +65 -0
  17. data/lib/amount/concerns/constructible.rb +92 -0
  18. data/lib/amount/{conversion.rb → concerns/convertible.rb} +3 -3
  19. data/lib/amount/concerns/formattable.rb +112 -0
  20. data/lib/amount/concerns/localizable.rb +39 -0
  21. data/lib/amount/{comparison.rb → concerns/orderable.rb} +10 -10
  22. data/lib/amount/concerns/parsable.rb +58 -0
  23. data/lib/amount/concerns/registrable.rb +80 -0
  24. data/lib/amount/{serialization.rb → concerns/serializable.rb} +4 -4
  25. data/lib/amount/display.rb +86 -51
  26. data/lib/amount/locale.rb +102 -0
  27. data/lib/amount/registry/generated_constructors.rb +10 -5
  28. data/lib/amount/registry.rb +72 -9
  29. data/lib/amount/rspec.rb +4 -4
  30. data/lib/amount/ui_parser.rb +109 -0
  31. data/lib/amount/version.rb +1 -1
  32. data/lib/amount.rb +61 -205
  33. data/test/amount/active_record/amount_validator_test.rb +201 -0
  34. data/test/amount/active_record/attribute_definition_test.rb +234 -0
  35. data/test/amount/active_record/migration_methods_test.rb +60 -0
  36. data/test/amount/active_record/model_test.rb +40 -0
  37. data/test/amount/active_record/type_test.rb +80 -0
  38. data/test/amount/active_record_test.rb +12 -0
  39. data/test/amount/compact_test.rb +371 -0
  40. data/test/amount/concerns/allocatable_test.rb +166 -0
  41. data/test/amount/concerns/calculable_test.rb +152 -0
  42. data/test/amount/concerns/coercible_test.rb +49 -0
  43. data/test/amount/concerns/constructible_test.rb +113 -0
  44. data/test/amount/concerns/convertible_test.rb +54 -0
  45. data/test/amount/concerns/formattable_test.rb +336 -0
  46. data/test/amount/concerns/localizable_test.rb +59 -0
  47. data/test/amount/concerns/orderable_test.rb +83 -0
  48. data/test/amount/concerns/parsable_test.rb +98 -0
  49. data/test/amount/concerns/registrable_test.rb +67 -0
  50. data/test/amount/concerns/serializable_test.rb +98 -0
  51. data/test/amount/display_test.rb +181 -0
  52. data/test/amount/locale_test.rb +590 -0
  53. data/test/amount/parser_test.rb +29 -0
  54. data/test/amount/registry/generated_constructors_test.rb +94 -0
  55. data/test/amount/registry_test.rb +149 -0
  56. data/test/amount/ui_parser_test.rb +284 -0
  57. data/test/amount_test.rb +55 -0
  58. data/test/dummy/config/application.rb +3 -0
  59. data/test/generators/amount/active_record/registry_generator_test.rb +116 -0
  60. data/test/integration/postgresql_test.rb +94 -0
  61. data/test/support/active_record_test_support.rb +65 -0
  62. data/test/support/error_helpers.rb +22 -0
  63. data/test/support/locale_test_setup.rb +36 -0
  64. data/test/test_helper.rb +1 -0
  65. metadata +47 -11
  66. data/test/postgresql_integration_test.rb +0 -71
  67. data/test/test_active_record.rb +0 -375
  68. data/test/test_amount.rb +0 -663
  69. data/test/test_registry_generator.rb +0 -104
@@ -0,0 +1,590 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "i18n"
4
+ require_relative "../test_helper"
5
+ require_relative "../support/locale_test_setup"
6
+
7
+ class AmountLocaleTest < Minitest::Test
8
+ include LocaleTestSetup
9
+
10
+ # Not localizing
11
+
12
+ def test_not_localizing_keeps_the_plain_number
13
+ assert_equal "$122500.00", Amount.of_usd("122500").ui
14
+ end
15
+
16
+ def test_not_localizing_keeps_a_negative_number_plain
17
+ assert_equal "-$1234.50".sub("$", ""), Amount.of_usd("-1234.50").ui.sub("$", "")
18
+ end
19
+
20
+ def test_not_localizing_keeps_a_zero_decimal_type_plain
21
+ assert_equal "1500 logs", Amount.of_logs(1500).ui
22
+ end
23
+
24
+ def test_localize_is_off_by_default
25
+ refute_predicate Amount, :localize?
26
+ end
27
+
28
+ # The locale: option
29
+
30
+ {
31
+ en: "$122,500.00",
32
+ de: "$122.500,00",
33
+ fr: "$122 500,00"
34
+ }.each do |locale, expected|
35
+ define_method("test_locale_option_formats_#{locale}") do
36
+ assert_equal expected, Amount.of_usd("122500").ui(locale:)
37
+ end
38
+ end
39
+
40
+ def test_locale_under_one_thousand_has_no_delimiter
41
+ assert_equal "$999,99", Amount.of_usd("999.99").ui(locale: :de)
42
+ end
43
+
44
+ def test_locale_negative_millions_in_de
45
+ assert_equal "$-1.234.567,50", Amount.of_usd("-1234567.50").ui(locale: :de)
46
+ end
47
+
48
+ def test_locale_negative_thousands_in_en
49
+ assert_equal "$-1,234.50", Amount.of_usd("-1234.50").ui(locale: :en)
50
+ end
51
+
52
+ def test_locale_negative_under_one_thousand_in_en
53
+ assert_equal "$-123.00", Amount.of_usd("-123").ui(locale: :en)
54
+ end
55
+
56
+ def test_locale_with_decimals_zero_type_in_en
57
+ assert_equal "1,500 logs", Amount.of_logs(1500).ui(locale: :en)
58
+ end
59
+
60
+ def test_locale_with_decimals_zero_type_in_de
61
+ assert_equal "1.500.000 logs", Amount.of_logs(1_500_000).ui(locale: :de)
62
+ end
63
+
64
+ def test_locale_with_trim_zeros_keeps_significant_decimals
65
+ assert_equal "1.234,5 SOL", Amount.of_sol("1234.5").ui(locale: :de, trim_zeros: true)
66
+ end
67
+
68
+ def test_locale_with_trim_zeros_drops_all_zero_decimals
69
+ assert_equal "1.000 SOL", Amount.of_sol("1000").ui(locale: :de, trim_zeros: true)
70
+ end
71
+
72
+ def test_locale_rounds_down_by_default
73
+ assert_equal "$1.234,56", Amount.of_usdc("1234.5678").ui(locale: :de)
74
+ end
75
+
76
+ def test_locale_with_ceil_direction
77
+ assert_equal "$1.234,57", Amount.of_usdc("1234.5678").ui(locale: :de, direction: :ceil)
78
+ end
79
+
80
+ def test_locale_with_display_unit
81
+ assert_equal "31.103,50 g", Amount.of_gold("1000").ui(unit: :gram, locale: :de)
82
+ end
83
+
84
+ def test_locale_with_display_unit_undecorated
85
+ assert_equal "31,103.50", Amount.of_gold("1000").ui(unit: :gram, decorated: false, locale: :en)
86
+ end
87
+
88
+ def test_locale_undecorated
89
+ assert_equal "1.000,0000", Amount.of_gold("1000").ui(decorated: false, locale: :de)
90
+ end
91
+
92
+ # Amount.localize
93
+
94
+ def test_localize_uses_i18n_locale
95
+ localize_in_german
96
+
97
+ assert_equal "$1.500,00", Amount.of_usd("1500").ui
98
+ end
99
+
100
+ def test_locale_option_overrides_i18n_locale_when_localizing
101
+ localize_in_german
102
+
103
+ assert_equal "$1,500.00", Amount.of_usd("1500").ui(locale: :en)
104
+ end
105
+
106
+ def test_missing_number_format_falls_back_to_defaults
107
+ assert_equal "$1,500.00", Amount.of_usd("1500").ui(locale: :xx)
108
+ end
109
+
110
+ def test_missing_delimiter_falls_back_while_separator_is_used
111
+ I18n.backend.store_translations(:xx, number: { format: { separator: "," } })
112
+
113
+ assert_equal "$1,500,00", Amount.of_usd("1500").ui(locale: :xx)
114
+ end
115
+
116
+ def test_wire_to_s_does_not_change_when_localizing
117
+ localize_in_german
118
+
119
+ assert_equal "USD|122500.50", Amount.of_usd("122500.5").to_s
120
+ end
121
+
122
+ def test_wire_formatted_does_not_change_when_localizing
123
+ localize_in_german
124
+
125
+ assert_equal "122500.50", Amount.of_usd("122500.5").formatted
126
+ end
127
+
128
+ def test_wire_parse_round_trips_when_localizing
129
+ localize_in_german
130
+
131
+ assert_equal Amount.of_usd("122500.5"), Amount.parse(Amount.of_usd("122500.5").to_s)
132
+ end
133
+
134
+ # Type labels
135
+
136
+ def test_type_label_fallback_ui_uses_display_symbol_when_nothing_is_translated
137
+ register_label_fallback_types
138
+
139
+ assert_equal "3 searches-sym", Amount.new(3, :SEARCHES).ui(locale: :en)
140
+ end
141
+
142
+ def test_type_label_fallback_unit_label_uses_display_symbol_when_nothing_is_translated
143
+ register_label_fallback_types
144
+
145
+ assert_equal "searches-sym", Amount.new(3, :SEARCHES).unit_label(locale: :en)
146
+ end
147
+
148
+ def test_type_label_fallback_ui_uses_the_conventional_key
149
+ register_label_fallback_types
150
+ store_conventional_unit_labels
151
+
152
+ assert_equal "3 searches", Amount.new(3, :SEARCHES).ui(locale: :en)
153
+ end
154
+
155
+ def test_type_label_fallback_unit_label_uses_the_conventional_key
156
+ register_label_fallback_types
157
+ store_conventional_unit_labels
158
+
159
+ assert_equal "searches", Amount.new(3, :SEARCHES).unit_label(locale: :en)
160
+ end
161
+
162
+ def test_type_label_fallback_missing_label_key_skips_the_conventional_key
163
+ register_label_fallback_types
164
+ store_conventional_unit_labels
165
+
166
+ assert_equal "3 cr", Amount.new(3, :CREDITS).ui(locale: :en)
167
+ end
168
+
169
+ def test_type_label_fallback_label_key_wins_over_the_conventional_key
170
+ register_label_fallback_types
171
+ store_conventional_unit_labels
172
+ I18n.backend.store_translations(:en, custom: { credits: "credits" })
173
+
174
+ assert_equal "3 credits", Amount.new(3, :CREDITS).ui(locale: :en)
175
+ end
176
+
177
+ def test_two_locales_two_labels_in_en
178
+ register_searches_in_en_and_de
179
+
180
+ assert_equal "1,500 searches", Amount.new(1500, :SEARCHES).ui(locale: :en)
181
+ end
182
+
183
+ def test_two_locales_two_labels_in_de
184
+ register_searches_in_en_and_de
185
+
186
+ assert_equal "1.500 Suchen", Amount.new(1500, :SEARCHES).ui(locale: :de)
187
+ end
188
+
189
+ def test_two_locales_two_labels_unit_label_in_de
190
+ register_searches_in_en_and_de
191
+
192
+ assert_equal "Suchen", Amount.new(1500, :SEARCHES).unit_label(locale: :de)
193
+ end
194
+
195
+ def test_labels_unchanged_in_ui_when_not_localizing
196
+ register_translated_searches
197
+
198
+ assert_equal "3 SEARCHES", Amount.new(3, :SEARCHES).ui
199
+ end
200
+
201
+ def test_unit_label_unchanged_when_not_localizing
202
+ register_translated_searches
203
+
204
+ assert_equal "SEARCHES", Amount.new(3, :SEARCHES).unit_label
205
+ end
206
+
207
+ def test_display_unit_label_unchanged_when_not_localizing
208
+ register_translated_searches
209
+
210
+ assert_equal "g", Amount.of_gold("1").unit_label(unit: :gram)
211
+ end
212
+
213
+ def test_localize_uses_i18n_locale_for_labels_in_ui
214
+ register_german_searches_and_localize
215
+
216
+ assert_equal "3 Suchen", Amount.new(3, :SEARCHES).ui
217
+ end
218
+
219
+ def test_localize_uses_i18n_locale_for_unit_label
220
+ register_german_searches_and_localize
221
+
222
+ assert_equal "Suchen", Amount.new(3, :SEARCHES).unit_label
223
+ end
224
+
225
+ # Display unit labels
226
+
227
+ def test_display_unit_label_fallback_ui_uses_the_unit_symbol
228
+ assert_equal "31.103,50 g", Amount.of_gold("1000").ui(unit: :gram, locale: :de)
229
+ end
230
+
231
+ def test_display_unit_label_fallback_unit_label_uses_the_unit_symbol
232
+ assert_equal "g", Amount.of_gold("1000").unit_label(unit: :gram, locale: :de)
233
+ end
234
+
235
+ def test_display_unit_label_fallback_ui_uses_the_conventional_key
236
+ store_german_gram_label
237
+
238
+ assert_equal "31.103,50 Gramm", Amount.of_gold("1000").ui(unit: :gram, locale: :de)
239
+ end
240
+
241
+ def test_display_unit_label_fallback_unit_label_uses_the_conventional_key
242
+ store_german_gram_label
243
+
244
+ assert_equal "Gramm", Amount.of_gold("1000").unit_label(unit: :gram, locale: :de)
245
+ end
246
+
247
+ def test_display_unit_label_fallback_other_locale_keeps_the_unit_symbol
248
+ store_german_gram_label
249
+
250
+ assert_equal "g", Amount.of_gold("1000").unit_label(unit: :gram, locale: :en)
251
+ end
252
+
253
+ def test_display_unit_without_symbol_or_key_uses_the_type_label
254
+ register_mass_with_label_key
255
+
256
+ assert_equal "mass", Amount.new("1", :MASS).unit_label(unit: :plain, locale: :en)
257
+ end
258
+
259
+ def test_display_unit_with_missing_label_key_uses_the_type_label
260
+ register_mass_with_label_key
261
+
262
+ assert_equal "mass", Amount.new("1", :MASS).unit_label(unit: :kilo, locale: :en)
263
+ end
264
+
265
+ def test_display_unit_without_symbol_uses_the_translated_type_label
266
+ register_mass_with_label_key
267
+ I18n.backend.store_translations(:en, amounts: { units: { mass: "Mass" } })
268
+
269
+ assert_equal "Mass", Amount.new("1", :MASS).unit_label(unit: :plain, locale: :en)
270
+ end
271
+
272
+ def test_display_unit_label_key_is_used_when_translated
273
+ register_mass_with_label_key
274
+ I18n.backend.store_translations(:en, amounts: { units: { mass: "Mass" } })
275
+ I18n.backend.store_translations(:en, my: { kilo: "kg" })
276
+
277
+ assert_equal "1.00 kg", Amount.new("1", :MASS).ui(unit: :kilo, locale: :en)
278
+ end
279
+
280
+ # Plural labels
281
+
282
+ def test_hash_label_without_i18n_singular
283
+ without_i18n do
284
+ register_plural_searches
285
+
286
+ assert_equal "1 search", Amount.new(1, :SEARCHES).ui
287
+ end
288
+ end
289
+
290
+ def test_hash_label_without_i18n_zero_is_plural
291
+ without_i18n do
292
+ register_plural_searches
293
+
294
+ assert_equal "0 searches", Amount.new(0, :SEARCHES).ui
295
+ end
296
+ end
297
+
298
+ def test_hash_label_without_i18n_plural
299
+ without_i18n do
300
+ register_plural_searches
301
+
302
+ assert_equal "2 searches", Amount.new(2, :SEARCHES).ui
303
+ end
304
+ end
305
+
306
+ def test_hash_label_without_i18n_unit_label_with_count
307
+ without_i18n do
308
+ register_plural_searches
309
+
310
+ assert_equal "search", Amount.new(2, :SEARCHES).unit_label(count: 1)
311
+ end
312
+ end
313
+
314
+ def test_hash_label_without_i18n_unit_label_defaults_to_other
315
+ without_i18n do
316
+ register_plural_searches
317
+
318
+ assert_equal "searches", Amount.new(1, :SEARCHES).unit_label
319
+ end
320
+ end
321
+
322
+ def test_hash_label_counts_the_displayed_one
323
+ register_plural_searches(decimals: 2)
324
+
325
+ assert_equal "1.00 search", Amount.new(100, :SEARCHES).ui
326
+ end
327
+
328
+ def test_hash_label_counts_a_trimmed_fraction_as_plural
329
+ register_plural_searches(decimals: 2)
330
+
331
+ assert_equal "1.5 searches", Amount.new(150, :SEARCHES).ui(trim_zeros: true)
332
+ end
333
+
334
+ def test_hash_label_counts_the_displayed_one_in_a_locale
335
+ register_plural_searches(decimals: 2)
336
+
337
+ assert_equal "1.00 search", Amount.new(100, :SEARCHES).ui(locale: :fr).sub(",", ".")
338
+ end
339
+
340
+ def test_hash_label_counts_the_floored_displayed_number
341
+ register_rated
342
+
343
+ assert_equal "1.0 star", Amount.new(104, :RATED).ui
344
+ end
345
+
346
+ def test_hash_label_counts_the_ceiled_displayed_number
347
+ register_rated
348
+
349
+ assert_equal "1.1 stars", Amount.new(104, :RATED).ui(direction: :ceil)
350
+ end
351
+
352
+ def test_hash_display_unit_symbol_singular
353
+ register_plural_kilo
354
+
355
+ assert_equal "1 kilo", Amount.new(1, :MASS).ui(unit: :kilo)
356
+ end
357
+
358
+ def test_hash_display_unit_symbol_plural
359
+ register_plural_kilo
360
+
361
+ assert_equal "3 kilos", Amount.new(3, :MASS).ui(unit: :kilo)
362
+ end
363
+
364
+ def test_hash_display_unit_symbol_unit_label_with_count
365
+ register_plural_kilo
366
+
367
+ assert_equal "kilo", Amount.new(3, :MASS).unit_label(unit: :kilo, count: 1)
368
+ end
369
+
370
+ def test_hash_display_unit_symbol_unit_label_defaults_to_other
371
+ register_plural_kilo
372
+
373
+ assert_equal "kilos", Amount.new(1, :MASS).unit_label(unit: :kilo, locale: :en)
374
+ end
375
+
376
+ def test_i18n_plural_labels_singular
377
+ register_i18n_plural_searches
378
+
379
+ assert_equal "1 search", Amount.new(1, :SEARCHES).ui(locale: :en)
380
+ end
381
+
382
+ def test_i18n_plural_labels_zero_is_plural
383
+ register_i18n_plural_searches
384
+
385
+ assert_equal "0 searches", Amount.new(0, :SEARCHES).ui(locale: :en)
386
+ end
387
+
388
+ def test_i18n_plural_labels_plural
389
+ register_i18n_plural_searches
390
+
391
+ assert_equal "2 searches", Amount.new(2, :SEARCHES).ui(locale: :en)
392
+ end
393
+
394
+ def test_i18n_plural_labels_unit_label_with_count
395
+ register_i18n_plural_searches
396
+
397
+ assert_equal "search", Amount.new(2, :SEARCHES).unit_label(locale: :en, count: 1)
398
+ end
399
+
400
+ def test_i18n_plural_labels_unit_label_defaults_to_other
401
+ register_i18n_plural_searches
402
+
403
+ assert_equal "searches", Amount.new(1, :SEARCHES).unit_label(locale: :en)
404
+ end
405
+
406
+ def test_i18n_plural_labels_unused_when_not_localizing
407
+ register_i18n_plural_searches
408
+
409
+ assert_equal "1 SEARCHES", Amount.new(1, :SEARCHES).ui
410
+ end
411
+
412
+ def test_i18n_plural_hash_default_singular_uses_locale_rules
413
+ register_plural_searches
414
+
415
+ assert_equal "1 search", Amount.new(1, :SEARCHES).ui(locale: :en)
416
+ end
417
+
418
+ def test_i18n_plural_hash_default_plural_uses_locale_rules
419
+ register_plural_searches
420
+
421
+ assert_equal "2 searches", Amount.new(2, :SEARCHES).ui(locale: :en)
422
+ end
423
+
424
+ {
425
+ 1 => "1 hledani",
426
+ 3 => "3 hledani-few",
427
+ 5 => "5 hledani-other"
428
+ }.each do |count, expected|
429
+ define_method("test_i18n_plural_with_few_form_for_#{count}") do
430
+ store_few_form_plural_rule
431
+
432
+ assert_equal expected, Amount.new(count, :SEARCHES).ui(locale: :xx)
433
+ end
434
+ end
435
+
436
+ def test_missing_plural_form_falls_back_to_other
437
+ I18n.backend.class.include(I18n::Backend::Pluralization)
438
+ I18n.backend.store_translations(:xx, i18n: { plural: { rule: ->(n) { n == 3 ? :few : :other } } },
439
+ amounts: { units: { searches: { other: "o" } } })
440
+ Amount.register :SEARCHES, decimals: 0
441
+
442
+ assert_equal "3 o", Amount.new(3, :SEARCHES).ui(locale: :xx)
443
+ end
444
+
445
+ # Invalid labels and errors
446
+
447
+ def test_hash_display_symbol_without_other_is_rejected
448
+ assert_raises(Amount::Registry::InvalidDisplaySymbol) do
449
+ Amount.register :SEARCHES, decimals: 0, display_symbol: { one: "search" }
450
+ end
451
+ end
452
+
453
+ def test_hash_display_symbol_without_other_names_other_in_the_message
454
+ error = error_from(Amount::Registry::InvalidDisplaySymbol) do
455
+ Amount.register :SEARCHES, decimals: 0, display_symbol: { one: "search" }
456
+ end
457
+
458
+ assert_match(/:other/, error.message)
459
+ end
460
+
461
+ def test_hash_display_symbol_with_string_keys_is_rejected
462
+ assert_raises(Amount::Registry::InvalidDisplaySymbol) do
463
+ Amount.register :SEARCHES, decimals: 0, display_symbol: { "other" => "x" }
464
+ end
465
+ end
466
+
467
+ def test_hash_display_unit_symbol_without_other_is_rejected
468
+ assert_raises(Amount::Registry::InvalidDisplayUnit) do
469
+ Amount.register :MASS, decimals: 0, display_units: { kilo: { scale: 1, symbol: { one: "kilo" } } }
470
+ end
471
+ end
472
+
473
+ def test_hash_display_unit_symbol_without_other_names_other_in_the_message
474
+ error = error_from(Amount::Registry::InvalidDisplayUnit) do
475
+ Amount.register :MASS, decimals: 0, display_units: { kilo: { scale: 1, symbol: { one: "kilo" } } }
476
+ end
477
+
478
+ assert_match(/:other/, error.message)
479
+ end
480
+
481
+ def test_rejected_hash_labels_leave_the_type_unregistered
482
+ error_from(Amount::Registry::InvalidDisplaySymbol) do
483
+ Amount.register :SEARCHES, decimals: 0, display_symbol: { one: "search" }
484
+ end
485
+ error_from(Amount::Registry::InvalidDisplaySymbol) do
486
+ Amount.register :SEARCHES, decimals: 0, display_symbol: { "other" => "x" }
487
+ end
488
+
489
+ refute Amount.registry.registered?(:SEARCHES)
490
+ end
491
+
492
+ def test_unit_label_for_an_unknown_display_unit_raises
493
+ assert_raises(Amount::Registry::InvalidDisplayUnit) { Amount.of_gold("1").unit_label(unit: :nope) }
494
+ end
495
+
496
+ def test_unit_label_with_a_locale_without_i18n_raises
497
+ without_i18n do
498
+ assert_raises(ArgumentError) { Amount.of_usd("1").unit_label(locale: :de) }
499
+ end
500
+ end
501
+
502
+ def test_locale_without_i18n_raises
503
+ without_i18n do
504
+ assert_raises(ArgumentError) { Amount.of_usd("1").ui(locale: :de) }
505
+ end
506
+ end
507
+
508
+ def test_locale_without_i18n_names_i18n_in_the_message
509
+ without_i18n do
510
+ assert_match(/i18n/, error_from(ArgumentError) { Amount.of_usd("1").ui(locale: :de) }.message)
511
+ end
512
+ end
513
+
514
+ private
515
+
516
+ def localize_in_german
517
+ Amount.localize = true
518
+ I18n.locale = :de
519
+ end
520
+
521
+ def register_label_fallback_types
522
+ Amount.register :SEARCHES, decimals: 0, display_symbol: "searches-sym"
523
+ Amount.register :CREDITS, decimals: 0, display_symbol: "cr", label_key: :'custom.credits'
524
+ end
525
+
526
+ def store_conventional_unit_labels
527
+ I18n.backend.store_translations(:en, amounts: { units: { searches: "searches", credits: "conv" } })
528
+ end
529
+
530
+ def register_searches_in_en_and_de
531
+ Amount.register :SEARCHES, decimals: 0
532
+ I18n.backend.store_translations(:en, amounts: { units: { searches: "searches" } })
533
+ I18n.backend.store_translations(:de, amounts: { units: { searches: "Suchen" } })
534
+ end
535
+
536
+ def register_translated_searches
537
+ Amount.register :SEARCHES, decimals: 0
538
+ I18n.backend.store_translations(:en, amounts: { units: { searches: "translated" } })
539
+ end
540
+
541
+ def register_german_searches_and_localize
542
+ Amount.register :SEARCHES, decimals: 0
543
+ I18n.backend.store_translations(:de, amounts: { units: { searches: "Suchen" } })
544
+ localize_in_german
545
+ end
546
+
547
+ def store_german_gram_label
548
+ I18n.backend.store_translations(:de, amounts: { display_units: { gold: { gram: "Gramm" } } })
549
+ end
550
+
551
+ def register_mass_with_label_key
552
+ Amount.register :MASS, decimals: 2, display_symbol: "mass", display_units: {
553
+ kilo: { scale: 1, label_key: "my.kilo" },
554
+ plain: { scale: 1 }
555
+ }
556
+ end
557
+
558
+ def register_plural_searches(decimals: 0)
559
+ Amount.register :SEARCHES, decimals:, display_symbol: { one: "search", other: "searches" }
560
+ end
561
+
562
+ def register_rated
563
+ Amount.register :RATED, decimals: 2, ui_decimals: 1, display_symbol: { one: "star", other: "stars" }
564
+ end
565
+
566
+ def register_plural_kilo
567
+ Amount.register :MASS, decimals: 0, display_units: {
568
+ kilo: { scale: 1, symbol: { one: "kilo", other: "kilos" }, ui_decimals: 0 }
569
+ }
570
+ end
571
+
572
+ def register_i18n_plural_searches
573
+ Amount.register :SEARCHES, decimals: 0
574
+ I18n.backend.store_translations(:en, amounts: { units: { searches: { one: "search", other: "searches" } } })
575
+ end
576
+
577
+ def store_few_form_plural_rule
578
+ I18n.backend.class.include(I18n::Backend::Pluralization)
579
+ I18n.backend.store_translations(:xx, i18n: { plural: { rule: lambda { |n|
580
+ case n
581
+ when 1 then :one
582
+ when 2..4 then :few
583
+ else :other
584
+ end
585
+ } } })
586
+ I18n.backend.store_translations(:xx, number: { format: { separator: ".", delimiter: "," } },
587
+ amounts: { units: { searches: { one: "hledani", few: "hledani-few", other: "hledani-other" } } })
588
+ Amount.register :SEARCHES, decimals: 0
589
+ end
590
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_helper"
4
+
5
+ class AmountParserTest < 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_parse_accepts_explicit_v1_prefix
15
+ assert_equal Amount.new("1.5", :USDC), Amount.parse("v1:USDC|1.5")
16
+ end
17
+
18
+ def test_parse_rejects_unknown_version_prefix
19
+ assert_raises(Amount::InvalidInput) { Amount.parse("v2:USDC|1.5") }
20
+ end
21
+
22
+ def test_parse_rejects_missing_value
23
+ assert_raises(Amount::InvalidInput) { Amount.parse("USDC|") }
24
+ end
25
+
26
+ def test_parse_rejects_missing_symbol
27
+ assert_raises(Amount::InvalidInput) { Amount.parse("|1.5") }
28
+ end
29
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../test_helper"
4
+
5
+ class AmountRegistryGeneratedConstructorsTest < Minitest::Test
6
+ class GoldAmount < Amount
7
+ def purity_estimate
8
+ "24k"
9
+ end
10
+ end
11
+
12
+ def setup
13
+ AmountTestSupport.register_default_types!
14
+ end
15
+
16
+ def teardown
17
+ Amount.singleton_class.send(:remove_method, :of_collide) if Amount.singleton_class.method_defined?(:of_collide)
18
+ Amount.registry.clear!
19
+ end
20
+
21
+ def test_generated_constructor_matches_new
22
+ assert_equal Amount.new("1.50", :USDC), Amount.of_usdc("1.50")
23
+ end
24
+
25
+ def test_generated_constructor_accepts_from_keyword
26
+ assert_equal 1_500_000, Amount.of_usdc(1_500_000, from: :atomic).atomic
27
+ end
28
+
29
+ def test_multi_word_symbol_generates_constructor
30
+ Amount.register :OIL_WTI_BBL, decimals: 4
31
+
32
+ assert_equal Amount.new("1.5", :OIL_WTI_BBL), Amount.of_oil_wti_bbl("1.5")
33
+ end
34
+
35
+ def test_non_method_safe_symbol_skips_constructor_generation
36
+ Amount.register :'USDC.e', decimals: 6
37
+
38
+ refute Amount.respond_to?(:of_usdc_e)
39
+ end
40
+
41
+ # Without the `of_` prefix the generated method would shadow
42
+ # `Object#try` once ActiveSupport is loaded — see the :TRY regression.
43
+ def test_generated_constructor_uses_of_prefix
44
+ Amount.register :TRY, decimals: 2
45
+
46
+ assert Amount.respond_to?(:of_try)
47
+ end
48
+
49
+ def test_of_prefixed_constructor_matches_new
50
+ Amount.register :TRY, decimals: 2
51
+
52
+ assert_equal Amount.new("100", :TRY), Amount.of_try("100")
53
+ end
54
+
55
+ def test_generated_constructor_exists_before_clear
56
+ assert Amount.respond_to?(:of_usdc)
57
+ end
58
+
59
+ def test_clear_removes_generated_constructor_methods
60
+ Amount.registry.clear!
61
+
62
+ refute Amount.respond_to?(:of_usdc)
63
+ end
64
+
65
+ def test_constructor_collision_raises_at_registration_time
66
+ Amount.singleton_class.send(:define_method, :of_collide) { :existing }
67
+
68
+ assert_raises(Amount::Registry::AlreadyRegistered) do
69
+ Amount.register :COLLIDE, decimals: 2
70
+ end
71
+ end
72
+
73
+ def test_generated_constructor_returns_subclass
74
+ use_gold_amount_registry!
75
+
76
+ assert_instance_of GoldAmount, Amount.of_gold("1", from: :ui)
77
+ end
78
+
79
+ def test_generated_constructor_returns_subclass_with_its_behavior
80
+ use_gold_amount_registry!
81
+
82
+ assert_equal "24k", Amount.of_gold("1", from: :ui).purity_estimate
83
+ end
84
+
85
+ private
86
+
87
+ # Replaces the default types with GOLD registered to {GoldAmount}.
88
+ def use_gold_amount_registry!
89
+ Amount.registry.clear!
90
+ Amount.register :GOLD, decimals: 8, display_symbol: "oz t",
91
+ display_position: :suffix, ui_decimals: 4,
92
+ class: GoldAmount
93
+ end
94
+ end