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.
- checksums.yaml +4 -4
- data/.rubocop.yml +37 -0
- data/CHANGELOG.md +142 -1
- data/Gemfile +4 -0
- data/README.md +218 -4
- data/Rakefile +47 -10
- data/lib/amount/active_record/amount_validator.rb +9 -18
- data/lib/amount/active_record/attribute_definition.rb +3 -1
- data/lib/amount/active_record/locale/en.yml +7 -0
- data/lib/amount/active_record/model.rb +9 -0
- data/lib/amount/active_record/railtie.rb +1 -1
- data/lib/amount/active_record.rb +12 -0
- data/lib/amount/compact.rb +137 -0
- data/lib/amount/{allocation.rb → concerns/allocatable.rb} +1 -1
- data/lib/amount/{arithmetic.rb → concerns/calculable.rb} +9 -9
- data/lib/amount/concerns/coercible.rb +65 -0
- data/lib/amount/concerns/constructible.rb +92 -0
- data/lib/amount/{conversion.rb → concerns/convertible.rb} +3 -3
- data/lib/amount/concerns/formattable.rb +112 -0
- data/lib/amount/concerns/localizable.rb +39 -0
- data/lib/amount/{comparison.rb → concerns/orderable.rb} +10 -10
- data/lib/amount/concerns/parsable.rb +58 -0
- data/lib/amount/concerns/registrable.rb +80 -0
- data/lib/amount/{serialization.rb → concerns/serializable.rb} +4 -4
- data/lib/amount/display.rb +86 -51
- data/lib/amount/locale.rb +102 -0
- data/lib/amount/registry/generated_constructors.rb +10 -5
- data/lib/amount/registry.rb +72 -9
- data/lib/amount/rspec.rb +4 -4
- data/lib/amount/ui_parser.rb +109 -0
- data/lib/amount/version.rb +1 -1
- data/lib/amount.rb +61 -205
- data/test/amount/active_record/amount_validator_test.rb +201 -0
- data/test/amount/active_record/attribute_definition_test.rb +234 -0
- data/test/amount/active_record/migration_methods_test.rb +60 -0
- data/test/amount/active_record/model_test.rb +40 -0
- data/test/amount/active_record/type_test.rb +80 -0
- data/test/amount/active_record_test.rb +12 -0
- data/test/amount/compact_test.rb +371 -0
- data/test/amount/concerns/allocatable_test.rb +166 -0
- data/test/amount/concerns/calculable_test.rb +152 -0
- data/test/amount/concerns/coercible_test.rb +49 -0
- data/test/amount/concerns/constructible_test.rb +113 -0
- data/test/amount/concerns/convertible_test.rb +54 -0
- data/test/amount/concerns/formattable_test.rb +336 -0
- data/test/amount/concerns/localizable_test.rb +59 -0
- data/test/amount/concerns/orderable_test.rb +83 -0
- data/test/amount/concerns/parsable_test.rb +98 -0
- data/test/amount/concerns/registrable_test.rb +67 -0
- data/test/amount/concerns/serializable_test.rb +98 -0
- data/test/amount/display_test.rb +181 -0
- data/test/amount/locale_test.rb +590 -0
- data/test/amount/parser_test.rb +29 -0
- data/test/amount/registry/generated_constructors_test.rb +94 -0
- data/test/amount/registry_test.rb +149 -0
- data/test/amount/ui_parser_test.rb +284 -0
- data/test/amount_test.rb +55 -0
- data/test/dummy/config/application.rb +3 -0
- data/test/generators/amount/active_record/registry_generator_test.rb +116 -0
- data/test/integration/postgresql_test.rb +94 -0
- data/test/support/active_record_test_support.rb +65 -0
- data/test/support/error_helpers.rb +22 -0
- data/test/support/locale_test_setup.rb +36 -0
- data/test/test_helper.rb +1 -0
- metadata +47 -11
- data/test/postgresql_integration_test.rb +0 -71
- data/test/test_active_record.rb +0 -375
- data/test/test_amount.rb +0 -663
- 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
|