amounts 0.0.8 → 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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +37 -0
  3. data/CHANGELOG.md +113 -0
  4. data/Gemfile +4 -0
  5. data/README.md +215 -1
  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.rb +12 -0
  12. data/lib/amount/compact.rb +137 -0
  13. data/lib/amount/{allocation.rb → concerns/allocatable.rb} +1 -1
  14. data/lib/amount/{arithmetic.rb → concerns/calculable.rb} +1 -1
  15. data/lib/amount/concerns/coercible.rb +65 -0
  16. data/lib/amount/concerns/constructible.rb +92 -0
  17. data/lib/amount/{conversion.rb → concerns/convertible.rb} +1 -1
  18. data/lib/amount/concerns/formattable.rb +112 -0
  19. data/lib/amount/concerns/localizable.rb +39 -0
  20. data/lib/amount/{comparison.rb → concerns/orderable.rb} +2 -2
  21. data/lib/amount/concerns/parsable.rb +58 -0
  22. data/lib/amount/concerns/registrable.rb +80 -0
  23. data/lib/amount/{serialization.rb → concerns/serializable.rb} +3 -3
  24. data/lib/amount/display.rb +81 -46
  25. data/lib/amount/locale.rb +102 -0
  26. data/lib/amount/registry.rb +69 -7
  27. data/lib/amount/ui_parser.rb +109 -0
  28. data/lib/amount/version.rb +1 -1
  29. data/lib/amount.rb +55 -199
  30. data/test/amount/active_record/amount_validator_test.rb +201 -0
  31. data/test/amount/active_record/attribute_definition_test.rb +234 -0
  32. data/test/amount/active_record/migration_methods_test.rb +60 -0
  33. data/test/amount/active_record/model_test.rb +40 -0
  34. data/test/amount/active_record/type_test.rb +80 -0
  35. data/test/amount/active_record_test.rb +12 -0
  36. data/test/amount/compact_test.rb +371 -0
  37. data/test/amount/concerns/allocatable_test.rb +166 -0
  38. data/test/amount/concerns/calculable_test.rb +152 -0
  39. data/test/amount/concerns/coercible_test.rb +49 -0
  40. data/test/amount/concerns/constructible_test.rb +113 -0
  41. data/test/amount/concerns/convertible_test.rb +54 -0
  42. data/test/amount/concerns/formattable_test.rb +336 -0
  43. data/test/amount/concerns/localizable_test.rb +59 -0
  44. data/test/amount/concerns/orderable_test.rb +83 -0
  45. data/test/amount/concerns/parsable_test.rb +98 -0
  46. data/test/amount/concerns/registrable_test.rb +67 -0
  47. data/test/amount/concerns/serializable_test.rb +98 -0
  48. data/test/amount/display_test.rb +181 -0
  49. data/test/amount/locale_test.rb +590 -0
  50. data/test/amount/parser_test.rb +29 -0
  51. data/test/amount/registry/generated_constructors_test.rb +94 -0
  52. data/test/amount/registry_test.rb +149 -0
  53. data/test/amount/ui_parser_test.rb +284 -0
  54. data/test/amount_test.rb +55 -0
  55. data/test/dummy/config/application.rb +3 -0
  56. data/test/generators/amount/active_record/registry_generator_test.rb +116 -0
  57. data/test/integration/postgresql_test.rb +94 -0
  58. data/test/support/active_record_test_support.rb +65 -0
  59. data/test/support/error_helpers.rb +22 -0
  60. data/test/support/locale_test_setup.rb +36 -0
  61. data/test/test_helper.rb +1 -0
  62. metadata +47 -11
  63. data/test/postgresql_integration_test.rb +0 -71
  64. data/test/test_active_record.rb +0 -375
  65. data/test/test_amount.rb +0 -675
  66. data/test/test_registry_generator.rb +0 -104
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../test_helper"
4
+
5
+ class AmountCoercibleTest < 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_coerce_decimal_returns_big_decimal_unchanged
15
+ value = BigDecimal("1.25")
16
+
17
+ assert_same value, Amount.coerce_decimal(value)
18
+ end
19
+
20
+ def test_coerce_decimal_converts_integer
21
+ assert_equal BigDecimal("3"), Amount.coerce_decimal(3)
22
+ end
23
+
24
+ def test_coerce_decimal_converts_string
25
+ assert_equal BigDecimal("1.25"), Amount.coerce_decimal("1.25")
26
+ end
27
+
28
+ def test_coerce_decimal_converts_rational
29
+ assert_equal BigDecimal("1.5"), Amount.coerce_decimal(Rational(3, 2))
30
+ end
31
+
32
+ def test_coerce_decimal_converts_float_through_its_string_form
33
+ assert_equal BigDecimal("0.1"), Amount.coerce_decimal(0.1)
34
+ end
35
+
36
+ def test_cross_type_addition_without_rate_names_both_types
37
+ error = error_from(Amount::TypeMismatch) { Amount.of_usdc("1") + Amount.of_sol("1") }
38
+
39
+ assert_equal "type mismatch: USDC vs SOL", error.message
40
+ end
41
+
42
+ def test_adding_a_non_amount_raises_type_mismatch
43
+ assert_raises(Amount::TypeMismatch) { Amount.of_usdc("1") + 1 }
44
+ end
45
+
46
+ def test_adding_a_non_amount_names_its_class
47
+ assert_match(/vs Integer\z/, error_from(Amount::TypeMismatch) { Amount.of_usdc("1") + 1 }.message)
48
+ end
49
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../test_helper"
4
+
5
+ class AmountConstructibleTest < 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.registry.clear!
18
+ end
19
+
20
+ def test_construct_from_integer_treats_as_atomic
21
+ assert_equal 1_000_000, Amount.new(1_000_000, :USDC).atomic
22
+ end
23
+
24
+ def test_construct_from_integer_gives_matching_decimal
25
+ assert_equal BigDecimal("1"), Amount.new(1_000_000, :USDC).decimal
26
+ end
27
+
28
+ def test_construct_from_string_treats_as_ui
29
+ assert_equal 1_500_000, Amount.new("1.50", :USDC).atomic
30
+ end
31
+
32
+ def test_construct_from_float_treats_as_decimal
33
+ assert_equal 1_500_000, Amount.new(1.5, :USDC).atomic
34
+ end
35
+
36
+ def test_construct_from_rational_treats_as_ui
37
+ assert_equal 1_500_000, Amount.new(Rational(3, 2), :USDC).atomic
38
+ end
39
+
40
+ def test_construct_from_rational_is_exact_for_repeating_fractions
41
+ # 1/3 has no finite decimal expansion. With six decimals of storage
42
+ # the atomic representation is 333333 (truncated toward zero).
43
+ assert_equal 333_333, Amount.new(Rational(1, 3), :USDC).atomic
44
+ end
45
+
46
+ def test_construct_from_rational_with_explicit_ui_from
47
+ assert_equal 1_500_000, Amount.new(Rational(3, 2), :USDC, from: :ui).atomic
48
+ end
49
+
50
+ def test_explicit_from_overrides_inference_for_atomic
51
+ assert_equal 1_500_000, Amount.new("1500000", :USDC, from: :atomic).atomic
52
+ end
53
+
54
+ def test_explicit_from_overrides_inference_for_decimal
55
+ assert_equal BigDecimal("1.5"), Amount.new("1500000", :USDC, from: :atomic).decimal
56
+ end
57
+
58
+ def test_subclass_instance_from_own_new
59
+ use_gold_amount_registry!
60
+
61
+ assert_instance_of GoldAmount, GoldAmount.new("1", :GOLD)
62
+ end
63
+
64
+ def test_subclass_instance_from_own_new_has_subclass_behavior
65
+ use_gold_amount_registry!
66
+
67
+ assert_equal "24k", GoldAmount.new("1", :GOLD).purity_estimate
68
+ end
69
+
70
+ def test_amount_new_dispatches_to_registered_subclass_from_ui
71
+ use_gold_amount_registry!
72
+
73
+ assert_instance_of GoldAmount, Amount.new("1", :GOLD)
74
+ end
75
+
76
+ def test_amount_new_dispatches_to_registered_subclass_from_atomic
77
+ use_gold_amount_registry!
78
+
79
+ assert_instance_of GoldAmount, Amount.new(100_000_000, :GOLD, from: :atomic)
80
+ end
81
+
82
+ def test_explicit_wrong_subclass_still_raises
83
+ use_gold_amount_registry!
84
+ other = Class.new(Amount)
85
+
86
+ assert_raises(Amount::InvalidInput) { other.new("1", :GOLD) }
87
+ end
88
+
89
+ def test_explicit_wrong_subclass_message_names_registered_class
90
+ use_gold_amount_registry!
91
+ other = Class.new(Amount)
92
+
93
+ assert_match(/use AmountConstructibleTest::GoldAmount\.new for GOLD/,
94
+ error_from(Amount::InvalidInput) { other.new("1", :GOLD) }.message)
95
+ end
96
+
97
+ def test_amount_new_with_default_class_is_unchanged
98
+ use_gold_amount_registry!
99
+ Amount.register :USDC, decimals: 6
100
+
101
+ assert_instance_of Amount, Amount.new("1", :USDC)
102
+ end
103
+
104
+ private
105
+
106
+ # Replaces the default types with GOLD registered to {GoldAmount}.
107
+ def use_gold_amount_registry!
108
+ Amount.registry.clear!
109
+ Amount.register :GOLD, decimals: 8, display_symbol: "oz t",
110
+ display_position: :suffix, ui_decimals: 4,
111
+ class: GoldAmount
112
+ end
113
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require_relative "../../test_helper"
5
+
6
+ class AmountConvertibleTest < Minitest::Test
7
+ def setup
8
+ AmountTestSupport.register_default_types!
9
+ end
10
+
11
+ def teardown
12
+ Amount.registry.clear!
13
+ end
14
+
15
+ def test_to_same_symbol_is_identity
16
+ amount = Amount.new("1", :USDC)
17
+
18
+ assert_equal amount, amount.to(:USDC)
19
+ end
20
+
21
+ def test_to_with_explicit_rate_converts_value
22
+ assert_equal BigDecimal("0.42"), Amount.new("1000", :USDC).to(:GOLD, rate: "0.00042").decimal
23
+ end
24
+
25
+ def test_to_with_explicit_rate_sets_target_symbol
26
+ assert_equal :GOLD, Amount.new("1000", :USDC).to(:GOLD, rate: "0.00042").symbol
27
+ end
28
+
29
+ def test_to_with_rational_rate_converts_value
30
+ assert_equal BigDecimal("2"), Amount.new("4", :USDC).to(:USD, rate: Rational(1, 2)).decimal
31
+ end
32
+
33
+ def test_to_with_rational_rate_sets_target_symbol
34
+ assert_equal :USD, Amount.new("4", :USDC).to(:USD, rate: Rational(1, 2)).symbol
35
+ end
36
+
37
+ def test_to_without_rate_requires_default
38
+ assert_raises(Amount::Registry::NoDefaultRate) do
39
+ Amount.new("1", :USDC).to(:GOLD)
40
+ end
41
+ end
42
+
43
+ def test_to_uses_registered_default_rate_value
44
+ Amount.register_default_rate :USDC, :USD, 1
45
+
46
+ assert_equal BigDecimal("1.5"), Amount.new("1.5", :USDC).to(:USD).decimal
47
+ end
48
+
49
+ def test_to_uses_registered_default_rate_symbol
50
+ Amount.register_default_rate :USDC, :USD, 1
51
+
52
+ assert_equal :USD, Amount.new("1.5", :USDC).to(:USD).symbol
53
+ end
54
+ end
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "i18n"
4
+ require_relative "../../test_helper"
5
+ require_relative "../../support/locale_test_setup"
6
+
7
+ class AmountFormattableTest < Minitest::Test
8
+ include LocaleTestSetup
9
+
10
+ NBSP = " "
11
+
12
+ # Template primitives
13
+
14
+ def test_fill_places_number_before_label
15
+ assert_equal "12.50 €", Amount::Formattable.fill("%n %u", number: "12.50", label: "€")
16
+ end
17
+
18
+ def test_fill_places_label_before_number
19
+ assert_equal "€12.50", Amount::Formattable.fill("%u%n", number: "12.50", label: "€")
20
+ end
21
+
22
+ def test_fill_does_not_expand_placeholders_inside_the_values
23
+ assert_equal "5 %n", Amount::Formattable.fill("%n %u", number: "5", label: "%n")
24
+ end
25
+
26
+ {
27
+ prefix: "%u%n",
28
+ suffix: "%n %u",
29
+ nil => "%n %u"
30
+ }.each do |position, template|
31
+ define_method("test_template_for_#{position.inspect.delete(':')}_matches_the_legacy_layout") do
32
+ assert_equal template, Amount::Formattable.template_for(position)
33
+ end
34
+ end
35
+
36
+ def test_valid_template_accepts_a_string_with_the_number
37
+ assert Amount::Formattable.valid_template?("%n")
38
+ end
39
+
40
+ {
41
+ "string_without_the_number" => "%u",
42
+ "symbol" => :'%n',
43
+ "nil" => nil
44
+ }.each do |name, template|
45
+ define_method("test_valid_template_rejects_a_#{name}") do
46
+ refute Amount::Formattable.valid_template?(template)
47
+ end
48
+ end
49
+
50
+ # Amount#display_format
51
+
52
+ def test_display_format_is_part_of_the_amount_api
53
+ assert_includes Amount.ancestors, Amount::Formattable
54
+ end
55
+
56
+ def test_display_format_is_part_of_an_amount_subclass_api
57
+ assert_includes Class.new(Amount).ancestors, Amount::Formattable
58
+ end
59
+
60
+ def test_display_format_returns_the_registered_prefix_template
61
+ assert_equal "%u%n", Amount.of_usd("1").display_format
62
+ end
63
+
64
+ def test_display_format_returns_the_registered_suffix_template
65
+ assert_equal "%n %u", Amount.of_sol("1").display_format
66
+ end
67
+
68
+ def test_display_format_for_a_display_unit_with_a_format
69
+ register_mass
70
+
71
+ assert_equal "%n#{NBSP}%u", Amount.new("1", :MASS).display_format(unit: :kilo)
72
+ end
73
+
74
+ def test_display_format_for_a_display_unit_with_a_position
75
+ register_mass
76
+
77
+ assert_equal "%u%n", Amount.new("1", :MASS).display_format(unit: :pre)
78
+ end
79
+
80
+ def test_display_format_for_a_display_unit_named_by_a_string_without_a_template
81
+ register_mass
82
+
83
+ assert_equal "[%u] %n", Amount.new("1", :MASS).display_format(unit: "plain")
84
+ end
85
+
86
+ def test_display_format_for_a_locale_without_a_template
87
+ register_eur_with_german_template
88
+
89
+ assert_equal "%u%n", Amount.of_eur("1").display_format(locale: :en)
90
+ end
91
+
92
+ def test_display_format_for_a_locale_with_a_template
93
+ register_eur_with_german_template
94
+
95
+ assert_equal "%n#{NBSP}%u", Amount.of_eur("1").display_format(locale: :de)
96
+ end
97
+
98
+ def test_display_format_without_a_locale_ignores_locale_templates
99
+ register_eur_with_german_template
100
+
101
+ assert_equal "%u%n", Amount.of_eur("1").display_format
102
+ end
103
+
104
+ def test_display_format_for_an_unknown_display_unit_raises
105
+ register_mass
106
+
107
+ assert_raises(Amount::Registry::InvalidDisplayUnit) { Amount.new("1", :MASS).display_format(unit: :ton) }
108
+ end
109
+
110
+ def test_display_format_for_a_display_unit_of_a_type_without_units_raises
111
+ assert_raises(Amount::Registry::InvalidDisplayUnit) { Amount.of_usd("1").display_format(unit: :cents) }
112
+ end
113
+
114
+ def test_display_format_with_a_locale_requires_i18n
115
+ without_i18n do
116
+ assert_raises(ArgumentError) { Amount.of_usd("1").display_format(locale: :de) }
117
+ end
118
+ end
119
+
120
+ # Rendering without templates
121
+
122
+ def test_output_without_templates_prefix
123
+ assert_equal "$1.50", Amount.of_usdc("1.5").ui
124
+ end
125
+
126
+ def test_output_without_templates_negative_prefix
127
+ assert_equal "$-5.00", Amount.of_usd("-5").ui
128
+ end
129
+
130
+ def test_output_without_templates_suffix
131
+ assert_equal "1.5000 oz t", Amount.of_gold("1.5").ui
132
+ end
133
+
134
+ def test_output_without_templates_display_unit
135
+ assert_equal "46.65 g", Amount.of_gold("1.5").ui(unit: :gram)
136
+ end
137
+
138
+ def test_output_without_templates_in_en
139
+ assert_equal "$1,500.00", Amount.of_usd("1500").ui(locale: :en)
140
+ end
141
+
142
+ def test_output_without_templates_in_de
143
+ assert_equal "$1.500,00", Amount.of_usd("1500").ui(locale: :de)
144
+ end
145
+
146
+ # Rendering a type's template
147
+
148
+ def test_display_format_places_number_and_label
149
+ Amount.register :EUR, decimals: 2, display_symbol: "€", display_format: "%u %n"
150
+
151
+ assert_equal "€ 12.50", Amount.of_eur("12.5").ui
152
+ end
153
+
154
+ def test_display_format_keeps_the_sign_in_the_number
155
+ Amount.register :EUR, decimals: 2, display_symbol: "€", display_format: "%u%n"
156
+
157
+ assert_equal "€-12.50", Amount.of_eur("-12.5").ui
158
+ end
159
+
160
+ def test_display_format_keeps_non_breaking_spaces
161
+ Amount.register :EUR, decimals: 2, display_symbol: "€", display_format: "%n#{NBSP}%u"
162
+
163
+ assert_equal "12.50#{NBSP}€", Amount.of_eur("12.5").ui
164
+ end
165
+
166
+ def test_display_format_places_a_singular_label
167
+ register_plural_searches_with_template
168
+
169
+ assert_equal "search: 1", Amount.of_searches(1).ui
170
+ end
171
+
172
+ def test_display_format_places_a_plural_label
173
+ register_plural_searches_with_template
174
+
175
+ assert_equal "searches: 3", Amount.of_searches(3).ui
176
+ end
177
+
178
+ def test_display_format_works_without_i18n
179
+ Amount.register :EUR, decimals: 2, display_symbol: "€", display_format: "%n#{NBSP}%u"
180
+
181
+ without_i18n { assert_equal "12.50#{NBSP}€", Amount.of_eur("12.5").ui }
182
+ end
183
+
184
+ def test_undecorated_output_is_the_number_alone
185
+ Amount.register :EUR, decimals: 2, display_symbol: "€", display_format: "[%u] %n"
186
+
187
+ assert_equal "12.50", Amount.of_eur("12.5").ui(decorated: false)
188
+ end
189
+
190
+ def test_empty_label_renders_the_number_alone
191
+ Amount.register :BARE, decimals: 0, display_symbol: "", display_format: "%n %u"
192
+
193
+ assert_equal "7", Amount.of_bare(7).ui
194
+ end
195
+
196
+ def test_wire_format_ignores_display_format
197
+ Amount.register :EUR, decimals: 2, display_symbol: "€", display_format: "%u%n"
198
+
199
+ assert_equal "EUR|12.50", Amount.of_eur("12.5").to_s
200
+ end
201
+
202
+ # Locale templates for a type
203
+
204
+ def test_locale_without_a_template_uses_the_types_template
205
+ register_eur_with_german_template
206
+
207
+ assert_equal "€1,234.50", Amount.of_eur("1234.5").ui(locale: :en)
208
+ end
209
+
210
+ def test_locale_template_replaces_the_types_template
211
+ register_eur_with_german_template
212
+
213
+ assert_equal "1.234,50#{NBSP}€", Amount.of_eur("1234.5").ui(locale: :de)
214
+ end
215
+
216
+ def test_locale_template_keeps_the_sign_in_the_number
217
+ register_eur_with_german_template
218
+
219
+ assert_equal "-1.234,50#{NBSP}€", Amount.of_eur("-1234.5").ui(locale: :de)
220
+ end
221
+
222
+ def test_locale_template_is_ignored_when_not_localizing
223
+ register_eur_with_german_template
224
+
225
+ assert_equal "€1234.50", Amount.of_eur("1234.5").ui
226
+ end
227
+
228
+ def test_locale_template_follows_i18n_locale_when_localize_is_on
229
+ register_eur_with_german_template
230
+ Amount.localize = true
231
+ I18n.locale = :de
232
+
233
+ assert_equal "1.234,50#{NBSP}€", Amount.of_eur("1234.5").ui
234
+ end
235
+
236
+ def test_locale_template_overrides_display_position
237
+ I18n.backend.store_translations(:de, amounts: { formats: { usd: "%n %u" } })
238
+
239
+ assert_equal "1.500,00 $", Amount.of_usd("1500").ui(locale: :de)
240
+ end
241
+
242
+ def test_locale_template_uses_the_translated_label
243
+ Amount.register :SEARCHES, decimals: 0
244
+ I18n.backend.store_translations(:de, amounts: { units: { searches: "Suchen" }, formats: { searches: "%u: %n" } })
245
+
246
+ assert_equal "Suchen: 1.500", Amount.new(1500, :SEARCHES).ui(locale: :de)
247
+ end
248
+
249
+ def test_locale_template_without_number_is_ignored
250
+ register_eur_with_german_template
251
+ I18n.backend.store_translations(:de, amounts: { formats: { eur: "%u" } })
252
+
253
+ assert_equal "€1.234,50", Amount.of_eur("1234.5").ui(locale: :de)
254
+ end
255
+
256
+ # Display units
257
+
258
+ def test_display_unit_format_places_number_and_label
259
+ register_mass
260
+
261
+ assert_equal "1.00#{NBSP}kg", Amount.new("1", :MASS).ui(unit: :kilo)
262
+ end
263
+
264
+ def test_display_unit_position_still_works
265
+ register_mass
266
+
267
+ assert_equal "p1.00", Amount.new("1", :MASS).ui(unit: :pre)
268
+ end
269
+
270
+ def test_display_unit_without_template_uses_the_types
271
+ register_mass
272
+
273
+ assert_equal "[x] 1.00", Amount.new("1", :MASS).ui(unit: :plain)
274
+ end
275
+
276
+ def test_display_unit_locale_template_overrides_its_format
277
+ register_mass_with_german_kilo_template
278
+
279
+ assert_equal "kg 1,00", Amount.new("1", :MASS).ui(unit: :kilo, locale: :de)
280
+ end
281
+
282
+ def test_display_unit_locale_template_is_only_used_for_its_locale
283
+ register_mass_with_german_kilo_template
284
+
285
+ assert_equal "1.00#{NBSP}kg", Amount.new("1", :MASS).ui(unit: :kilo, locale: :en)
286
+ end
287
+
288
+ def test_display_unit_without_template_uses_the_types_locale_template
289
+ register_mass_with_german_type_template
290
+
291
+ assert_equal "x: 1,00", Amount.new("1", :MASS).ui(unit: :plain, locale: :de)
292
+ end
293
+
294
+ def test_display_unit_position_wins_over_the_types_locale_template
295
+ register_mass_with_german_type_template
296
+
297
+ assert_equal "p1,00", Amount.new("1", :MASS).ui(unit: :pre, locale: :de)
298
+ end
299
+
300
+ def test_display_unit_undecorated_output_is_the_number_alone
301
+ register_mass
302
+
303
+ assert_equal "1.00", Amount.new("1", :MASS).ui(unit: :kilo, decorated: false)
304
+ end
305
+
306
+ private
307
+
308
+ def register_eur_with_german_template
309
+ Amount.register :EUR, decimals: 2, display_symbol: "€", display_format: "%u%n"
310
+ I18n.backend.store_translations(:de, amounts: { formats: { eur: "%n#{NBSP}%u" } })
311
+ end
312
+
313
+ def register_mass
314
+ Amount.register :MASS, decimals: 2, display_symbol: "mass", display_format: "[%u] %n", display_units: {
315
+ kilo: { scale: 1, symbol: "kg", format: "%n#{NBSP}%u" },
316
+ pre: { scale: 1, symbol: "p", position: :prefix },
317
+ plain: { scale: 1, symbol: "x" }
318
+ }
319
+ end
320
+
321
+ def register_mass_with_german_kilo_template
322
+ register_mass
323
+ I18n.backend.store_translations(:de, amounts: { display_unit_formats: { mass: { kilo: "%u %n" } } })
324
+ end
325
+
326
+ def register_mass_with_german_type_template
327
+ register_mass
328
+ I18n.backend.store_translations(:de, amounts: { formats: { mass: "%u: %n" } })
329
+ end
330
+
331
+ def register_plural_searches_with_template
332
+ Amount.register :SEARCHES, decimals: 0,
333
+ display_symbol: { one: "search", other: "searches" },
334
+ display_format: "%u: %n"
335
+ end
336
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../test_helper"
4
+ require_relative "../../support/locale_test_setup"
5
+
6
+ class AmountLocalizableTest < Minitest::Test
7
+ include LocaleTestSetup
8
+
9
+ def test_localize_defaults_to_false
10
+ refute_predicate Amount, :localize?
11
+ end
12
+
13
+ def test_setting_localize_true_makes_localize_predicate_true
14
+ Amount.localize = true
15
+
16
+ assert_predicate Amount, :localize?
17
+ end
18
+
19
+ def test_localize_is_an_alias_of_localize_predicate
20
+ assert_equal :localize?, Amount.method(:localize).original_name
21
+ end
22
+
23
+ { "false" => false, "nil" => nil }.each do |name, value|
24
+ define_method("test_setting_localize_#{name}_makes_localize_predicate_false") do
25
+ Amount.localize = true
26
+ Amount.localize = value
27
+
28
+ assert_equal false, Amount.localize?
29
+ end
30
+ end
31
+
32
+ def test_localize_true_without_i18n_raises_when_set
33
+ without_i18n do
34
+ assert_raises(ArgumentError) { Amount.localize = true }
35
+ end
36
+ end
37
+
38
+ def test_localize_true_without_i18n_message_names_i18n
39
+ without_i18n do
40
+ assert_match(/i18n/, error_from(ArgumentError) { Amount.localize = true }.message)
41
+ end
42
+ end
43
+
44
+ def test_localize_true_without_i18n_leaves_localize_off
45
+ without_i18n do
46
+ error_from(ArgumentError) { Amount.localize = true }
47
+
48
+ refute_predicate Amount, :localize?
49
+ end
50
+ end
51
+
52
+ def test_localize_true_without_i18n_leaves_ui_unlocalized
53
+ without_i18n do
54
+ error_from(ArgumentError) { Amount.localize = true }
55
+
56
+ assert_equal "$1500.00", Amount.of_usd("1500").ui
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../test_helper"
4
+
5
+ class AmountOrderableTest < 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_zero_predicate
15
+ assert Amount.new(0, :USDC).zero?
16
+ end
17
+
18
+ def test_positive_predicate
19
+ assert Amount.new(1, :USDC).positive?
20
+ end
21
+
22
+ def test_negative_predicate
23
+ assert Amount.new(-1, :USDC).negative?
24
+ end
25
+
26
+ def test_same_type_true_for_same_symbol
27
+ assert Amount.new(1, :USDC).same_type?(Amount.new(2, :USDC))
28
+ end
29
+
30
+ def test_same_type_false_for_different_symbol
31
+ refute Amount.new(1, :USDC).same_type?(Amount.new(1, :SOL))
32
+ end
33
+
34
+ def test_same_type_false_for_non_amount
35
+ refute Amount.new(1, :USDC).same_type?(1)
36
+ end
37
+
38
+ def test_less_than_same_type
39
+ assert Amount.new("1", :USDC) < Amount.new("2", :USDC)
40
+ end
41
+
42
+ def test_greater_than_same_type
43
+ assert Amount.new("2", :USDC) > Amount.new("1", :USDC)
44
+ end
45
+
46
+ def test_equality_same_type
47
+ assert_equal Amount.new("1", :USDC), Amount.new("1", :USDC)
48
+ end
49
+
50
+ def test_greater_than_cross_type_uses_registered_rate
51
+ Amount.register_default_rate :USD, :USDC, 1
52
+
53
+ assert_operator Amount.new("10.00", :USDC), :>, Amount.new("5.00", :USD)
54
+ end
55
+
56
+ def test_spaceship_cross_type_uses_registered_rate
57
+ Amount.register_default_rate :USD, :USDC, 1
58
+
59
+ assert_equal 0, Amount.new("5.00", :USDC) <=> Amount.new("5.00", :USD)
60
+ end
61
+
62
+ def test_comparison_different_type_nil_without_rate
63
+ assert_nil(Amount.new(1, :USDC) <=> Amount.new(1, :SOL))
64
+ end
65
+
66
+ def test_sorting
67
+ amounts = [Amount.new("3", :USDC), Amount.new("1", :USDC), Amount.new("2", :USDC)]
68
+
69
+ assert_equal ["1.0", "2.0", "3.0"], amounts.sort.map { |amount| amount.decimal.to_s("F") }
70
+ end
71
+
72
+ def test_equal_amounts_are_the_same_hash_key
73
+ data = { Amount.new("1.5", :USDC) => :first }
74
+
75
+ assert_equal :first, data[Amount.new("1.5", :USDC)]
76
+ end
77
+
78
+ def test_different_symbol_is_a_different_hash_key
79
+ data = { Amount.new("1.5", :USDC) => :first }
80
+
81
+ assert_nil data[Amount.new("1.5", :USD)]
82
+ end
83
+ end