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.
- checksums.yaml +4 -4
- data/.rubocop.yml +37 -0
- data/CHANGELOG.md +113 -0
- data/Gemfile +4 -0
- data/README.md +215 -1
- 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.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} +1 -1
- data/lib/amount/concerns/coercible.rb +65 -0
- data/lib/amount/concerns/constructible.rb +92 -0
- data/lib/amount/{conversion.rb → concerns/convertible.rb} +1 -1
- data/lib/amount/concerns/formattable.rb +112 -0
- data/lib/amount/concerns/localizable.rb +39 -0
- data/lib/amount/{comparison.rb → concerns/orderable.rb} +2 -2
- data/lib/amount/concerns/parsable.rb +58 -0
- data/lib/amount/concerns/registrable.rb +80 -0
- data/lib/amount/{serialization.rb → concerns/serializable.rb} +3 -3
- data/lib/amount/display.rb +81 -46
- data/lib/amount/locale.rb +102 -0
- data/lib/amount/registry.rb +69 -7
- data/lib/amount/ui_parser.rb +109 -0
- data/lib/amount/version.rb +1 -1
- data/lib/amount.rb +55 -199
- 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 -675
- 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
|