minting 1.1.2 → 1.3.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/README.md +5 -0
- data/Rakefile +7 -2
- data/lib/minting/mint/currency.rb +10 -2
- data/lib/minting/mint/registry.rb +9 -6
- data/lib/minting/money/allocation.rb +5 -3
- data/lib/minting/money/arithmetics.rb +2 -2
- data/lib/minting/money/coercion.rb +6 -6
- data/lib/minting/money/conversion.rb +5 -3
- data/lib/minting/money/money.rb +22 -8
- data/lib/minting/money/parse.rb +2 -2
- data/lib/minting/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7530401c0a722596508aab5aa587bc53a548698308f9d09ae34fac2ec671784
|
|
4
|
+
data.tar.gz: abe0501b11d4f9c77e2054443c12e1d4a5e2707fabbfac71f5bd4b8a7f5a3fb5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 31db7b1c31b0f1329342162cb8f868e46d14e703df5dd36898dfeb74281d674bc0f7ba0b9b367d8568f0751c09f3a219dec172ed87cc8cc4d906606550492034
|
|
7
|
+
data.tar.gz: 7090942000722ce81b302237a6b71850ef8a8ec477ded927bb5914f1e198552512ae11b0b1f46d38dd44ae0d6b4a2b1e7d34dab3ec35ea107b800c38e60c7a94
|
data/README.md
CHANGED
|
@@ -91,6 +91,11 @@ price_in_euros.to_s(format: '%<symbol>2s%<amount>+10f') #=> " € +12.34"
|
|
|
91
91
|
|
|
92
92
|
price.to_json # => "{ "currency": "USD", "amount": "9.99" }"
|
|
93
93
|
|
|
94
|
+
# Hash conversion
|
|
95
|
+
|
|
96
|
+
price.to_hash #=> {currency: "USD", amount: "9.99"}
|
|
97
|
+
|
|
98
|
+
|
|
94
99
|
# Proportional allocation and split
|
|
95
100
|
|
|
96
101
|
ten.split(3) #=> [[USD 3.34], [USD 3.33], [USD 3.33]]
|
data/Rakefile
CHANGED
|
@@ -12,18 +12,23 @@ Rake::TestTask.new(:test) do |t|
|
|
|
12
12
|
t.ruby_opts << '-rtest_helper.rb'
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
Rake::TestTask.new(
|
|
15
|
+
Rake::TestTask.new('bench') do |t|
|
|
16
16
|
t.libs = %w[lib test]
|
|
17
17
|
t.pattern = 'test/performance/*_benchmark.rb'
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
Rake::TestTask.new('bench:parse') do |t|
|
|
21
|
+
t.libs = %w[lib test]
|
|
22
|
+
t.pattern = 'test/performance/parse_benchmark.rb'
|
|
23
|
+
t.ruby_opts << '-r test_helper.rb'
|
|
24
|
+
end
|
|
25
|
+
|
|
20
26
|
Rake::TestTask.new('bench:edge') do |t|
|
|
21
27
|
t.libs = %w[lib test]
|
|
22
28
|
t.pattern = 'test/performance/algorithm_benchmark.rb'
|
|
23
29
|
t.ruby_opts << '-r test_helper.rb'
|
|
24
30
|
end
|
|
25
31
|
|
|
26
|
-
|
|
27
32
|
Rake::TestTask.new('bench:regression') do |t|
|
|
28
33
|
t.libs = %w[lib test]
|
|
29
34
|
t.pattern = 'test/performance/regression_benchmark.rb'
|
|
@@ -3,12 +3,19 @@ module Mint
|
|
|
3
3
|
#
|
|
4
4
|
# @see https://www.iso.org/iso-4217-currency-codes.html
|
|
5
5
|
class Currency
|
|
6
|
-
attr_reader :code, :subunit, :symbol,
|
|
6
|
+
attr_reader :code, :subunit, :symbol,
|
|
7
|
+
:country,
|
|
8
|
+
:fractional_multiplier, :minimum_amount,
|
|
9
|
+
:name, :priority
|
|
7
10
|
|
|
8
11
|
def inspect
|
|
9
12
|
"<Currency:(#{code} #{symbol} #{subunit})>"
|
|
10
13
|
end
|
|
11
14
|
|
|
15
|
+
def normalize_amount(amount)
|
|
16
|
+
amount.to_r.round(subunit)
|
|
17
|
+
end
|
|
18
|
+
|
|
12
19
|
private
|
|
13
20
|
|
|
14
21
|
def initialize(code:, symbol:, subunit: 0, priority: 0, country: nil, name: nil)
|
|
@@ -18,7 +25,8 @@ module Mint
|
|
|
18
25
|
@priority = priority.to_i
|
|
19
26
|
@country = country
|
|
20
27
|
@name = name
|
|
21
|
-
@
|
|
28
|
+
@fractional_multiplier = 10**@subunit
|
|
29
|
+
@minimum_amount = 1r / fractional_multiplier
|
|
22
30
|
freeze
|
|
23
31
|
end
|
|
24
32
|
end
|
|
@@ -9,11 +9,14 @@ module Mint
|
|
|
9
9
|
# @raise [ArgumentError] if the currency code is not registered
|
|
10
10
|
def self.money(amount, currency_code)
|
|
11
11
|
currency = currency(currency_code)
|
|
12
|
-
return Money.
|
|
12
|
+
return Money.create(amount, currency) if currency
|
|
13
13
|
|
|
14
|
-
raise ArgumentError, "
|
|
14
|
+
raise ArgumentError, "[#{currency.inspect}] is not a registered currency. Check Mint.currencies"
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# Returns default zero, no currency money
|
|
18
|
+
def self.zero = @zero ||= Money.new(0, Mint.currency('XXX'))
|
|
19
|
+
|
|
17
20
|
# Finds a registered currency by its code, symbol,
|
|
18
21
|
# or retrieves it directly if already a Currency object.
|
|
19
22
|
#
|
|
@@ -34,11 +37,11 @@ module Mint
|
|
|
34
37
|
#
|
|
35
38
|
# @param code [String, Symbol] the unique currency code (e.g. 'USD', :EUR)
|
|
36
39
|
# @param subunit [Integer] the decimal subunit precision (defaults to 2)
|
|
37
|
-
# @param symbol [String] the display symbol (defaults to '
|
|
40
|
+
# @param symbol [String] the display symbol (defaults to '')
|
|
38
41
|
# @param priority [Integer] parser precedence priority (defaults to 0)
|
|
39
42
|
# @return [Currency] the registered or existing Currency instance
|
|
40
43
|
# @raise [ArgumentError] if the code layout is invalid or register throws an error
|
|
41
|
-
def self.register_currency(code:, subunit: 2, symbol: '
|
|
44
|
+
def self.register_currency(code:, subunit: 2, symbol: '', priority: 0)
|
|
42
45
|
code = code.to_s
|
|
43
46
|
currencies[code] || register_currency!(code:, subunit:, symbol:, priority:)
|
|
44
47
|
end
|
|
@@ -63,9 +66,9 @@ module Mint
|
|
|
63
66
|
"Currency: #{code} already registered"
|
|
64
67
|
end
|
|
65
68
|
|
|
66
|
-
currencies[code] = Currency.new(code:, subunit:, symbol:, priority:)
|
|
69
|
+
currency = currencies[code] = Currency.new(code:, subunit:, symbol:, priority:)
|
|
67
70
|
@currency_symbols = nil
|
|
68
|
-
|
|
71
|
+
currency
|
|
69
72
|
end
|
|
70
73
|
|
|
71
74
|
# Returns the hash of all registered currencies.
|
|
@@ -14,7 +14,8 @@ module Mint
|
|
|
14
14
|
raise ArgumentError, 'Need at least 1 proportion element' if proportions.empty?
|
|
15
15
|
raise ArgumentError, 'Proportions total must not be zero' if whole.zero?
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
subunit = currency.subunit
|
|
18
|
+
amounts = proportions.map { |rate| (amount * rate.to_r / whole).round(subunit) }
|
|
18
19
|
allocate_left_over!(amounts: amounts, left_over: amount - amounts.sum)
|
|
19
20
|
end
|
|
20
21
|
|
|
@@ -44,11 +45,12 @@ module Mint
|
|
|
44
45
|
|
|
45
46
|
def allocate_left_over!(amounts:, left_over:)
|
|
46
47
|
if left_over.nonzero?
|
|
47
|
-
minimum =
|
|
48
|
+
minimum = currency.minimum_amount
|
|
49
|
+
minimum = -minimum if left_over.negative?
|
|
48
50
|
last_slot = (left_over / minimum).to_i - 1
|
|
49
51
|
(0..last_slot).each { |slot| amounts[slot] += minimum }
|
|
50
52
|
end
|
|
51
|
-
amounts.map {
|
|
53
|
+
amounts.map { Money.new(it, currency) }
|
|
52
54
|
end
|
|
53
55
|
end
|
|
54
56
|
end
|
|
@@ -3,7 +3,7 @@ module Mint
|
|
|
3
3
|
# Returns the absolute value of the monetary amount as a new {Money} instance.
|
|
4
4
|
#
|
|
5
5
|
# @return [Money] the absolute value
|
|
6
|
-
def abs =
|
|
6
|
+
def abs = mint(amount.abs)
|
|
7
7
|
|
|
8
8
|
# Returns true if the monetary amount is less than zero.
|
|
9
9
|
#
|
|
@@ -60,7 +60,7 @@ module Mint
|
|
|
60
60
|
# @return [Money] the multiplied Money instance
|
|
61
61
|
# @raise [TypeError] if multiplier is not Numeric or is a Money object
|
|
62
62
|
def *(multiplicand)
|
|
63
|
-
return mint(amount * multiplicand
|
|
63
|
+
return mint(amount * multiplicand) if multiplicand.is_a?(Numeric)
|
|
64
64
|
|
|
65
65
|
raise TypeError, "#{self} can't be multiplied by #{multiplicand}"
|
|
66
66
|
end
|
|
@@ -13,8 +13,6 @@ module Mint
|
|
|
13
13
|
# Coerced Number contains the arithmetic logic for numeric compatible ops.
|
|
14
14
|
# @private
|
|
15
15
|
class CoercedNumber
|
|
16
|
-
include Comparable
|
|
17
|
-
|
|
18
16
|
# @private
|
|
19
17
|
def initialize(value)
|
|
20
18
|
@value = value
|
|
@@ -44,19 +42,21 @@ module Mint
|
|
|
44
42
|
raise_coercion_error(:/, other)
|
|
45
43
|
end
|
|
46
44
|
|
|
47
|
-
#
|
|
45
|
+
# Only zero is dimensionless and comparable to Money.
|
|
46
|
+
# e.g. 0 < price is meaningful; 0.5 < price is not (what currency is 0.5?).
|
|
48
47
|
def <=>(other)
|
|
49
|
-
return nil if @value.nil? || other.nil?
|
|
50
48
|
return @value <=> other.amount if @value.zero? || other.zero?
|
|
51
49
|
|
|
52
50
|
raise_coercion_error(:<=>, other)
|
|
53
51
|
end
|
|
54
52
|
|
|
55
|
-
|
|
53
|
+
private
|
|
54
|
+
|
|
56
55
|
def raise_coercion_error(operation, operand)
|
|
57
56
|
raise TypeError,
|
|
58
|
-
"#{
|
|
57
|
+
"#{@value} #{operation} #{operand} : incompatible operands"
|
|
59
58
|
end
|
|
60
59
|
end
|
|
60
|
+
private_constant :CoercedNumber
|
|
61
61
|
end
|
|
62
62
|
end
|
|
@@ -39,15 +39,17 @@ module Mint
|
|
|
39
39
|
amount.to_i
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
def to_hash
|
|
43
|
+
{ currency: currency_code, amount: Kernel.format("%0.#{currency.subunit}f", amount) }
|
|
44
|
+
end
|
|
45
|
+
|
|
42
46
|
# Serializes the money instance to a standard JSON object containing the amount and currency.
|
|
43
47
|
# Highly optimized to run without external dependencies.
|
|
44
48
|
#
|
|
45
49
|
# @return [String] the JSON serialized string representation
|
|
46
50
|
def to_json(*_args)
|
|
47
|
-
subunit = currency.subunit
|
|
48
51
|
Kernel.format(
|
|
49
|
-
%({"currency": "#{currency_code}", "amount": "%0.#{subunit}f"}),
|
|
50
|
-
amount
|
|
52
|
+
%({"currency": "#{currency_code}", "amount": "%0.#{currency.subunit}f"}), amount
|
|
51
53
|
)
|
|
52
54
|
end
|
|
53
55
|
|
data/lib/minting/money/money.rb
CHANGED
|
@@ -10,13 +10,16 @@ module Mint
|
|
|
10
10
|
# @param amount [Numeric] The monetary amount
|
|
11
11
|
# @param currency [Currency] The currency object
|
|
12
12
|
# @raise [ArgumentError] If amount is not numeric or currency is invalid
|
|
13
|
-
def
|
|
13
|
+
def self.create(amount, currency)
|
|
14
14
|
raise ArgumentError, 'amount must be Numeric' unless amount.is_a?(Numeric)
|
|
15
|
-
raise ArgumentError, 'currency must be a Currency object' unless currency.is_a?(Currency)
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
checked_currency = Mint.currency(currency)
|
|
17
|
+
unless checked_currency
|
|
18
|
+
raise ArgumentError,
|
|
19
|
+
"Currency not found (#{currency}). Check Mint.currencies"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
new(checked_currency.normalize_amount(amount), checked_currency)
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
# Returns the ISO 3-letter currency code string.
|
|
@@ -24,6 +27,8 @@ module Mint
|
|
|
24
27
|
# @return [String] the ISO currency code
|
|
25
28
|
def currency_code = currency.code
|
|
26
29
|
|
|
30
|
+
def fractional = (amount * currency.fractional_multiplier).to_i
|
|
31
|
+
|
|
27
32
|
# Generates a stable hash key for Money instances.
|
|
28
33
|
#
|
|
29
34
|
# @return [Integer] the calculated hash value
|
|
@@ -33,7 +38,8 @@ module Mint
|
|
|
33
38
|
# @param new_amount [Numeric] The new amount
|
|
34
39
|
# @return [Money] A new Money object or self
|
|
35
40
|
def mint(new_amount)
|
|
36
|
-
new_amount
|
|
41
|
+
new_amount = new_amount.to_r.round(currency.subunit)
|
|
42
|
+
new_amount == amount ? self : Money.new(new_amount, currency)
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
# Returns a standard developer-oriented string inspection of the Money object.
|
|
@@ -49,7 +55,15 @@ module Mint
|
|
|
49
55
|
# @return [Boolean] true if currencies match, false otherwise
|
|
50
56
|
def same_currency?(other) = other.respond_to?(:currency) && other.currency == currency
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Initializes a new Money object with the given amount and currency.
|
|
61
|
+
# @param amount [Numeric] The monetary amount
|
|
62
|
+
# @param currency [Currency] The currency object
|
|
63
|
+
def initialize(amount, currency)
|
|
64
|
+
@amount = amount
|
|
65
|
+
@currency = currency
|
|
66
|
+
freeze
|
|
67
|
+
end
|
|
54
68
|
end
|
|
55
69
|
end
|
data/lib/minting/money/parse.rb
CHANGED
|
@@ -25,14 +25,14 @@ module Mint
|
|
|
25
25
|
currency = currency ? Mint.currency(currency) : parse_currency(input)
|
|
26
26
|
raise ArgumentError, "Currency [#{currency}] not registered" unless currency
|
|
27
27
|
|
|
28
|
-
amount = parse_amount(input)
|
|
28
|
+
amount = currency.normalize_amount(parse_amount(input))
|
|
29
29
|
new(amount, currency)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# Extracts a numeric value from input that should only contain an amount.
|
|
33
33
|
def self.parse_amount(input)
|
|
34
34
|
# Remove any charater that is not a digit, comma or period
|
|
35
|
-
numeric = input.scan(/[\d
|
|
35
|
+
numeric = input.scan(/[\d.,-]/).join
|
|
36
36
|
numeric = normalize_separators(numeric)
|
|
37
37
|
Rational(numeric)
|
|
38
38
|
end
|
data/lib/minting/version.rb
CHANGED