minting 1.7.2 → 1.8.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 +35 -3
- data/doc/Mint/Currency.html +826 -55
- data/doc/Mint/Money.html +715 -218
- data/doc/Mint/RangeStepPatch.html +1 -1
- data/doc/Mint/Registry.html +859 -0
- data/doc/Mint/Rounding.html +495 -0
- data/doc/Mint/UnknownCurrency.html +1 -1
- data/doc/Mint.html +307 -225
- data/doc/Minting.html +2 -2
- data/doc/_index.html +15 -8
- data/doc/agents/api_review-2026-06-15.md +329 -0
- data/doc/agents/copilot-instructions.md +0 -5
- data/doc/agents/expired/copilot-instructions.md +75 -0
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +25 -4
- data/doc/index.html +25 -4
- data/doc/method_list.html +177 -25
- data/doc/top-level-namespace.html +1 -1
- data/lib/minting/currency/currency.rb +71 -1
- data/lib/minting/mint/dsl/range.rb +1 -0
- data/lib/minting/mint/locale_backend.rb +29 -0
- data/lib/minting/mint/mint.rb +13 -38
- data/lib/minting/mint/parser/parser.rb +50 -19
- data/lib/minting/mint/parser/separators.rb +10 -8
- data/lib/minting/mint/registry/registration.rb +33 -0
- data/lib/minting/mint/registry/registry.rb +38 -0
- data/lib/minting/mint/registry/symbols.rb +49 -0
- data/lib/minting/mint/registry/zeros.rb +20 -0
- data/lib/minting/mint/rounding.rb +51 -0
- data/lib/minting/mint.rb +12 -23
- data/lib/minting/money/allocation/allocation.rb +1 -2
- data/lib/minting/money/allocation/split.rb +1 -1
- data/lib/minting/money/arithmetics/methods.rb +2 -2
- data/lib/minting/money/arithmetics/operators.rb +6 -6
- data/lib/minting/money/clamp.rb +1 -1
- data/lib/minting/money/coercion.rb +1 -1
- data/lib/minting/money/comparable.rb +6 -0
- data/lib/minting/money/constructors.rb +63 -20
- data/lib/minting/money/format/formatting.rb +16 -0
- data/lib/minting/money/format/to_s.rb +13 -4
- data/lib/minting/money/money.rb +12 -6
- data/lib/minting/version.rb +1 -1
- metadata +15 -7
- data/lib/minting/currency/currency_registry.rb +0 -67
- data/lib/minting/currency/world_currencies.rb +0 -16
- /data/doc/agents/{AGENTS.md → expired/AGENTS.md} +0 -0
- /data/doc/agents/{gemini_gem_evaluation.md → expired/gemini_gem_evaluation.md} +0 -0
- /data/doc/agents/{recommendations.md → expired/recommendations.md} +0 -0
- /data/doc/agents/{rubocop-issues.md → expired/rubocop-issues.md} +0 -0
data/lib/minting/mint/mint.rb
CHANGED
|
@@ -12,46 +12,21 @@ module Mint
|
|
|
12
12
|
# @param currency_code [Currency, String] Currency code
|
|
13
13
|
# @return [Money] the instantiated Money object
|
|
14
14
|
# @raise [ArgumentError] if the currency code is not registered
|
|
15
|
-
def self.money(amount, currency_code) = Money.
|
|
15
|
+
def self.money(amount, currency_code) = Money.from(amount, currency_code)
|
|
16
16
|
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
# @param currency [String, Currency] the currency identifier or object
|
|
21
|
-
# @return [Currency, nil] the registered Currency instance or nil if not found
|
|
22
|
-
def self.currency(currency)
|
|
23
|
-
case currency
|
|
24
|
-
when NilClass then nil
|
|
25
|
-
when Currency then currency
|
|
26
|
-
when String then CurrencyRegistry.currencies[currency]
|
|
27
|
-
else raise ArgumentError, "currency must be [Currency], [String] or nil (#{currency})"
|
|
28
|
-
end
|
|
29
|
-
end
|
|
17
|
+
# @return [Hash{String => Currency}] the frozen world-currencies hash
|
|
18
|
+
# @api private
|
|
19
|
+
def self.world_currencies = Registry.world_currencies
|
|
30
20
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
21
|
+
# Executes a block with a specific rounding mode applied to all money
|
|
22
|
+
# construction, parsing, change, allocation, and split operations.
|
|
33
23
|
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
# @raise [ArgumentError] if the currency is not registered
|
|
37
|
-
def self.zero(currency)
|
|
38
|
-
checked = Mint.currency(currency)
|
|
39
|
-
raise ArgumentError, "Invalid Currency: [#{currency}]" unless checked
|
|
40
|
-
|
|
41
|
-
@zeros ||= CurrencyRegistry.currencies.values.to_h { |currency| [currency, Mint::Money.send(:new, 0, currency)] }
|
|
42
|
-
@zeros[currency] ||= Money.send(:new, 0, currency)
|
|
43
|
-
@zeros[currency]
|
|
44
|
-
end
|
|
45
|
-
|
|
24
|
+
# Restores the previous mode (or default) when the block exits, even on
|
|
25
|
+
# exception.
|
|
46
26
|
#
|
|
47
|
-
# @param
|
|
48
|
-
#
|
|
49
|
-
# @
|
|
50
|
-
# @
|
|
51
|
-
|
|
52
|
-
# @raise [ArgumentError] if the code contains invalid characters
|
|
53
|
-
# @raise [KeyError] if the currency code is already registered
|
|
54
|
-
def self.register_currency(code:, subunit: 0, symbol: '', priority: 0)
|
|
55
|
-
CurrencyRegistry.register(code:, subunit:, symbol:, priority:)
|
|
56
|
-
end
|
|
27
|
+
# @param mode [Symbol] one of: +:half_up+, +:half_down+, +:floor+,
|
|
28
|
+
# +:ceil+, +:truncate+, +:down+
|
|
29
|
+
# @yield block to execute with the rounding mode active
|
|
30
|
+
# @raise [ArgumentError] if +mode+ is not a recognised rounding mode
|
|
31
|
+
def self.with_rounding(mode, &) = Rounding.with_mode(mode, &)
|
|
57
32
|
end
|
|
@@ -6,29 +6,58 @@ module Mint
|
|
|
6
6
|
|
|
7
7
|
# Parses a human-readable money string into a {Money} object.
|
|
8
8
|
#
|
|
9
|
+
# Returns +nil+ when the input is invalid or currency cannot be determined.
|
|
10
|
+
#
|
|
9
11
|
# @param input [String] Amount input, optionally including a currency symbol or code
|
|
10
12
|
# @param currency [String, Symbol, Currency, nil] ISO code when not present in +input+
|
|
11
|
-
# @return [Money]
|
|
12
|
-
# @raise [ArgumentError] when +input+ is invalid or currency cannot be determined
|
|
13
|
+
# @return [Money, nil]
|
|
13
14
|
#
|
|
14
15
|
# @example With explicit currency
|
|
15
|
-
#
|
|
16
|
-
#
|
|
16
|
+
# Mint.parse('19.99', 'USD') #=> [USD 19.99]
|
|
17
|
+
# Mint.parse('garbage', 'USD') #=> nil
|
|
17
18
|
#
|
|
18
19
|
# @example With symbol or code in the string
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# Money.parse('USD 1,234.56') #=> [USD 1234.56]
|
|
20
|
+
# Mint.parse('$19.99') #=> [USD 19.99]
|
|
21
|
+
# Mint.parse('USD 1,234.56') #=> [USD 1234.56]
|
|
22
22
|
def parse(input, currency = nil)
|
|
23
|
+
return nil unless input.is_a?(String)
|
|
24
|
+
|
|
25
|
+
input = input.strip
|
|
26
|
+
return nil if input.empty?
|
|
27
|
+
|
|
28
|
+
currency = parse_currency(input, currency)
|
|
29
|
+
return nil unless currency
|
|
30
|
+
|
|
31
|
+
amount = parse_amount(input)
|
|
32
|
+
return nil unless amount
|
|
33
|
+
|
|
34
|
+
amount = currency.normalize_amount(amount)
|
|
35
|
+
Mint::Money.new(amount, currency)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Like {.parse} but raises on failure.
|
|
39
|
+
#
|
|
40
|
+
# @param input [String] Amount input, optionally including a currency symbol or code
|
|
41
|
+
# @param currency [String, Symbol, Currency, nil] ISO code when not present in +input+
|
|
42
|
+
# @return [Money]
|
|
43
|
+
# @raise [ArgumentError] when +input+ is invalid or currency cannot be determined
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# Mint.parse!('19.99', 'USD') #=> [USD 19.99]
|
|
47
|
+
# Mint.parse!('garbage', 'USD') #=> ArgumentError
|
|
48
|
+
def parse!(input, currency = nil)
|
|
23
49
|
raise ArgumentError, 'input must be a String' unless input.is_a?(String)
|
|
24
50
|
|
|
25
51
|
input = input.strip
|
|
26
52
|
raise ArgumentError, 'input cannot be empty' if input.empty?
|
|
27
53
|
|
|
28
|
-
currency =
|
|
29
|
-
raise ArgumentError, "Currency [#{currency}] not
|
|
54
|
+
currency = parse_currency(input, currency)
|
|
55
|
+
raise ArgumentError, "Currency [#{currency}] not found" unless currency
|
|
30
56
|
|
|
31
|
-
amount =
|
|
57
|
+
amount = parse_amount(input)
|
|
58
|
+
raise ArgumentError, "Could not parse [#{input}]" unless amount
|
|
59
|
+
|
|
60
|
+
amount = currency.normalize_amount(amount)
|
|
32
61
|
Mint::Money.new(amount, currency)
|
|
33
62
|
end
|
|
34
63
|
|
|
@@ -40,8 +69,11 @@ module Mint
|
|
|
40
69
|
accounting_negative = input.start_with?('(') && input.end_with?(')')
|
|
41
70
|
|
|
42
71
|
# Remove any charater that is not a digit, comma or period
|
|
43
|
-
|
|
44
|
-
|
|
72
|
+
numeric_input = input.scan(/[\d.,-]/).join
|
|
73
|
+
numeric = parse_separators(numeric_input)
|
|
74
|
+
return nil unless numeric
|
|
75
|
+
|
|
76
|
+
amount = Rational(numeric)
|
|
45
77
|
accounting_negative ? -amount : amount
|
|
46
78
|
end
|
|
47
79
|
|
|
@@ -51,16 +83,15 @@ module Mint
|
|
|
51
83
|
# back to symbol matching. This correctly handles inputs like
|
|
52
84
|
# "MAX 10.00 USD" where the first uppercase word isn't a currency code.
|
|
53
85
|
# @private
|
|
54
|
-
def parse_currency(input)
|
|
86
|
+
def parse_currency(input, currency = nil)
|
|
87
|
+
currency = Currency.resolve(currency)
|
|
88
|
+
return currency if currency
|
|
89
|
+
|
|
55
90
|
input.scan(/\b([A-Z_]+)\b/) do |(code)|
|
|
56
|
-
currency =
|
|
91
|
+
currency = Currency.for_code(code)
|
|
57
92
|
return currency if currency
|
|
58
93
|
end
|
|
59
94
|
|
|
60
|
-
|
|
61
|
-
return currency if input.include?(symbol)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
raise ArgumentError, 'Currency could not be detected'
|
|
95
|
+
Registry.detect_currency(input)
|
|
65
96
|
end
|
|
66
97
|
end
|
|
@@ -10,24 +10,26 @@ module Mint
|
|
|
10
10
|
# @private
|
|
11
11
|
def classify_separators(numeric)
|
|
12
12
|
case [numeric.count('.'), numeric.count(',')]
|
|
13
|
-
in [0, 1] if numeric[-4] == ',' then :thousands_comma
|
|
14
|
-
in [0, 1] then :decimal_comma
|
|
15
|
-
in [0, 0] | [1, 0] then :decimal_period
|
|
16
|
-
in [p, c] if p > 1 && c > 1 then :
|
|
17
|
-
in [p, c] if p > 0 && c > 0 then :mixed
|
|
18
|
-
else :thousands
|
|
13
|
+
in [0, 1] if numeric[-4] == ',' then :thousands_comma # Comma is a thousand separator
|
|
14
|
+
in [0, 1] then :decimal_comma # Only one comma: decimal (e.g. 19,99 or 1,4 or 1,2345).
|
|
15
|
+
in [0, 0] | [1, 0] then :decimal_period # e.g. "1500" or "34.21".
|
|
16
|
+
in [p, c] if p > 1 && c > 1 then :invalid # Both separators appear multiple times
|
|
17
|
+
in [p, c] if p > 0 && c > 0 then :mixed # Commas and dots: the rightmost one is the decimal
|
|
18
|
+
else :thousands # Multiple of the same separator only (e.g. 1,234,567)
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# Converts locale-specific decimal/thousand separators into a plain decimal string.
|
|
23
23
|
# @private
|
|
24
|
-
def
|
|
24
|
+
def parse_separators(numeric)
|
|
25
|
+
return nil unless numeric.match?(/\d/)
|
|
26
|
+
|
|
25
27
|
case classify_separators(numeric)
|
|
26
28
|
when :decimal_period then numeric # Nothing to normalize (e.g. "1500" or "34.21").
|
|
27
29
|
when :decimal_comma then numeric.tr(',', '.') # Only one comma: decimal (e.g. 19,99 or 1,234).
|
|
28
30
|
when :thousands_comma then numeric.delete(',')
|
|
29
31
|
when :thousands then numeric.delete('.,')
|
|
30
|
-
when :
|
|
32
|
+
when :invalid then nil
|
|
31
33
|
when :mixed # Commas and dots: the rightmost one is the decimal separator.
|
|
32
34
|
if numeric.rindex(',') > numeric.rindex('.')
|
|
33
35
|
numeric.delete('.').tr(',', '.')
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mint
|
|
4
|
+
# :nodoc:
|
|
5
|
+
module Registry
|
|
6
|
+
# Registers a new currency, raising a KeyError if already registered.
|
|
7
|
+
#
|
|
8
|
+
# @param code [String] the unique currency code
|
|
9
|
+
# @param subunit [Integer] the decimal subunit precision, defaults to 0
|
|
10
|
+
# @param symbol [String] the display symbol
|
|
11
|
+
# @param priority [Integer] parser precedence priority
|
|
12
|
+
# @return [Currency] the newly registered Currency instance
|
|
13
|
+
# @raise [ArgumentError] if the code contains invalid characters
|
|
14
|
+
# @raise [KeyError] if the currency code is already registered
|
|
15
|
+
def self.register(code:, subunit: 0, symbol: '', priority: 0)
|
|
16
|
+
raise ArgumentError, 'Currency code must be String' unless code.is_a? String
|
|
17
|
+
unless code.match?(/^[A-Z_]+$/)
|
|
18
|
+
raise ArgumentError,
|
|
19
|
+
"Currency code must have only letters or '_' ('USD',, 'MY_COIN')"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
MUTEX.synchronize do
|
|
23
|
+
raise KeyError, "Currency: #{code} already registered" if currencies[code]
|
|
24
|
+
|
|
25
|
+
currency = Currency.new(code:, subunit:, symbol:, priority:)
|
|
26
|
+
@currencies = @currencies.merge(code => currency).freeze
|
|
27
|
+
@currency_symbols = nil
|
|
28
|
+
@currency_symbol_map = nil
|
|
29
|
+
currency
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require_relative 'symbols'
|
|
5
|
+
require_relative 'registration'
|
|
6
|
+
require_relative 'zeros'
|
|
7
|
+
|
|
8
|
+
# Mint registry: manages all cached state
|
|
9
|
+
module Mint
|
|
10
|
+
# Internal registry for currencies, symbols, and zero-money cache.
|
|
11
|
+
# All mutable shared state lives here.
|
|
12
|
+
module Registry
|
|
13
|
+
MUTEX = Monitor.new
|
|
14
|
+
|
|
15
|
+
private_constant :MUTEX
|
|
16
|
+
|
|
17
|
+
# Loads ISO world currencies from YAML file.
|
|
18
|
+
#
|
|
19
|
+
# @return [Hash{String => Currency}] ISO-4217 world currencies mapped by code
|
|
20
|
+
# @api private
|
|
21
|
+
def self.world_currencies
|
|
22
|
+
@world_currencies || MUTEX.synchronize do
|
|
23
|
+
@world_currencies = begin
|
|
24
|
+
path = File.join(File.expand_path('../../data', __dir__), 'world-currencies.yaml')
|
|
25
|
+
YAML.load_file(path).to_h { |entry| [entry['code'], Currency.new(**entry.transform_keys(&:to_sym))] }
|
|
26
|
+
end.freeze
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns the frozen hash of all registered currencies (world + custom).
|
|
31
|
+
#
|
|
32
|
+
# @return [Hash{String => Currency}] registered currencies mapped by code
|
|
33
|
+
# @api private
|
|
34
|
+
def self.currencies
|
|
35
|
+
@currencies || MUTEX.synchronize { @currencies = world_currencies.dup.freeze }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mint
|
|
4
|
+
# :nodoc:
|
|
5
|
+
module Registry
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
# Looks up a currency by its display symbol.
|
|
9
|
+
#
|
|
10
|
+
# @param symbol [String] the display symbol (e.g. "$", "R$")
|
|
11
|
+
# @return [Currency, nil] the highest-priority currency for the symbol
|
|
12
|
+
# @api private
|
|
13
|
+
def currency_for_symbol(symbol)
|
|
14
|
+
@currency_symbol_map || MUTEX.synchronize { @currency_symbol_map = currency_symbols.to_h.freeze }
|
|
15
|
+
@currency_symbol_map[symbol]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Scans +input+ for registered currency symbols and returns the first match.
|
|
19
|
+
#
|
|
20
|
+
# @param input [String] the string to scan
|
|
21
|
+
# @return [Currency, nil]
|
|
22
|
+
# @api private
|
|
23
|
+
def detect_currency(input)
|
|
24
|
+
currency_symbols.each do |symbol, currency|
|
|
25
|
+
return currency if input.include?(symbol)
|
|
26
|
+
end
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Registered symbols sorted for detection: longest match wins, then parser priority.
|
|
33
|
+
# Duplicate symbols are deduplicated — the highest-priority currency wins.
|
|
34
|
+
#
|
|
35
|
+
# @return [Array<Array<String, Currency>>] sorted symbol-to-currency mappings
|
|
36
|
+
# @api private
|
|
37
|
+
def currency_symbols
|
|
38
|
+
@currency_symbols || MUTEX.synchronize do
|
|
39
|
+
@currency_symbols =
|
|
40
|
+
currencies.values
|
|
41
|
+
.reject { |currency| currency.symbol.empty? }
|
|
42
|
+
.map { |currency| [currency.symbol, currency] }
|
|
43
|
+
.sort_by { |symbol, currency| [-symbol.length, -currency.priority] }
|
|
44
|
+
.uniq { |symbol, _| symbol }
|
|
45
|
+
.freeze
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mint
|
|
4
|
+
# :nodoc:
|
|
5
|
+
module Registry
|
|
6
|
+
# Returns the cached zero-Money for a currency, creating it if needed.
|
|
7
|
+
#
|
|
8
|
+
# @param currency [Currency] the currency object
|
|
9
|
+
# @return [Money] a frozen zero-Money
|
|
10
|
+
# @api private
|
|
11
|
+
def self.zero_for(currency)
|
|
12
|
+
raise ArgumentError, "Expect a Currency param. (#{currency})" unless currency.is_a?(Currency)
|
|
13
|
+
|
|
14
|
+
MUTEX.synchronize do
|
|
15
|
+
@zeros ||= {}
|
|
16
|
+
@zeros[currency] ||= Mint::Money.send(:new, 0, currency)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mint
|
|
4
|
+
# Rounding-mode dispatch table and block-scoped context.
|
|
5
|
+
# @api private
|
|
6
|
+
module Rounding
|
|
7
|
+
# Maps mode symbols to their corresponding +Rational+ rounding lambdas.
|
|
8
|
+
# @return [Hash{Symbol => Proc}]
|
|
9
|
+
# @api private
|
|
10
|
+
MODES = {
|
|
11
|
+
half_up: ->(amount, ndigits) { amount.round(ndigits, half: :up) },
|
|
12
|
+
half_down: ->(amount, ndigits) { amount.round(ndigits, half: :down) },
|
|
13
|
+
floor: ->(amount, ndigits) { amount.floor(ndigits) },
|
|
14
|
+
ceil: ->(amount, ndigits) { amount.ceil(ndigits) },
|
|
15
|
+
truncate: ->(amount, ndigits) { amount.truncate(ndigits) },
|
|
16
|
+
down: ->(amount, ndigits) { amount.truncate(ndigits) }
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
# Returns the currently active rounding mode, falling back to +:half_up+.
|
|
20
|
+
# @api private
|
|
21
|
+
# @return [Symbol]
|
|
22
|
+
def self.current_mode
|
|
23
|
+
Thread.current[:minting_rounding_mode] || :half_up
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Rounds +amount+ to +ndigits+ using the currently scoped rounding mode.
|
|
27
|
+
# @api private
|
|
28
|
+
# @param amount [Numeric]
|
|
29
|
+
# @param ndigits [Integer]
|
|
30
|
+
# @return [Rational]
|
|
31
|
+
def self.apply(amount, ndigits)
|
|
32
|
+
MODES.fetch(current_mode).call(amount.to_r, ndigits)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Sets a rounding mode for the duration of a block, restoring the
|
|
36
|
+
# previous mode on exit (even on exception).
|
|
37
|
+
# @api private
|
|
38
|
+
# @param mode [Symbol]
|
|
39
|
+
# @yield block to execute with the mode active
|
|
40
|
+
# @raise [ArgumentError] on unknown mode
|
|
41
|
+
def self.with_mode(mode)
|
|
42
|
+
raise ArgumentError, "Unknown rounding mode: #{mode}" unless MODES.key?(mode)
|
|
43
|
+
|
|
44
|
+
prev = Thread.current[:minting_rounding_mode]
|
|
45
|
+
Thread.current[:minting_rounding_mode] = mode
|
|
46
|
+
yield
|
|
47
|
+
ensure
|
|
48
|
+
Thread.current[:minting_rounding_mode] = prev
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/minting/mint.rb
CHANGED
|
@@ -1,27 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
require 'minting/currency/world_currencies'
|
|
3
|
+
require_relative 'mint/rounding'
|
|
4
|
+
require_relative 'currency/currency'
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
require_relative 'mint/dsl/numeric'
|
|
7
|
+
require_relative 'mint/dsl/range'
|
|
8
|
+
require_relative 'mint/dsl/string'
|
|
9
|
+
require_relative 'mint/dsl/top_level'
|
|
10
|
+
require_relative 'mint/locale_backend'
|
|
11
|
+
require_relative 'mint/mint'
|
|
12
|
+
require_relative 'mint/parser/parser'
|
|
13
|
+
require_relative 'mint/parser/separators'
|
|
14
|
+
require_relative 'mint/registry/registry'
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
require 'minting/mint/parser/parser'
|
|
14
|
-
require 'minting/mint/parser/separators'
|
|
15
|
-
|
|
16
|
-
require 'minting/money/allocation/allocation'
|
|
17
|
-
require 'minting/money/allocation/split'
|
|
18
|
-
require 'minting/money/arithmetics/methods'
|
|
19
|
-
require 'minting/money/arithmetics/operators'
|
|
20
|
-
require 'minting/money/clamp'
|
|
21
|
-
require 'minting/money/coercion'
|
|
22
|
-
require 'minting/money/comparable'
|
|
23
|
-
require 'minting/money/constructors'
|
|
24
|
-
require 'minting/money/conversion'
|
|
25
|
-
require 'minting/money/format/formatting'
|
|
26
|
-
require 'minting/money/format/to_s'
|
|
27
|
-
require 'minting/money/money'
|
|
16
|
+
require_relative 'money/money'
|
|
@@ -17,8 +17,7 @@ module Mint
|
|
|
17
17
|
raise ArgumentError, 'Need at least 1 proportion element' if proportions.empty?
|
|
18
18
|
raise ArgumentError, 'Proportions total must not be zero' if whole.zero?
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
amounts = proportions.map { |rate| Rational(amount * rate, whole).round(subunit) }
|
|
20
|
+
amounts = proportions.map { |rate| currency.normalize_amount(Rational(amount * rate, whole)) }
|
|
22
21
|
allocate_left_over(amounts: amounts, left_over: amount - amounts.sum)
|
|
23
22
|
end
|
|
24
23
|
end
|
|
@@ -17,7 +17,7 @@ module Mint
|
|
|
17
17
|
def split(slices)
|
|
18
18
|
raise ArgumentError, 'Slices quantity must be an poitive integer' unless slices.positive? && slices.integer?
|
|
19
19
|
|
|
20
|
-
fraction = (amount / slices)
|
|
20
|
+
fraction = currency.normalize_amount(amount / slices)
|
|
21
21
|
allocate_left_over(amounts: Array.new(slices, fraction),
|
|
22
22
|
left_over: amount - (fraction * slices))
|
|
23
23
|
end
|
|
@@ -6,7 +6,7 @@ module Mint
|
|
|
6
6
|
# Returns the absolute value of the monetary amount as a new {Money} instance.
|
|
7
7
|
#
|
|
8
8
|
# @return [Money] the absolute value
|
|
9
|
-
def abs =
|
|
9
|
+
def abs = copy_with(amount: amount.abs)
|
|
10
10
|
|
|
11
11
|
# Returns true if the monetary amount is less than zero.
|
|
12
12
|
#
|
|
@@ -22,6 +22,6 @@ module Mint
|
|
|
22
22
|
# Enables standard ranges and stepping (e.g. `1.dollar..10.dollars`).
|
|
23
23
|
#
|
|
24
24
|
# @return [Money] successor Money instance
|
|
25
|
-
def succ =
|
|
25
|
+
def succ = copy_with(amount: amount + currency.minimum_amount)
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -11,7 +11,7 @@ module Mint
|
|
|
11
11
|
def +(addend)
|
|
12
12
|
case addend
|
|
13
13
|
in 0 then self
|
|
14
|
-
in Money if same_currency?(addend) then
|
|
14
|
+
in Money if same_currency?(addend) then copy_with(amount: amount + addend.amount)
|
|
15
15
|
else raise TypeError, "#{addend} can't be added to #{self}"
|
|
16
16
|
end
|
|
17
17
|
end
|
|
@@ -24,7 +24,7 @@ module Mint
|
|
|
24
24
|
def -(subtrahend)
|
|
25
25
|
case subtrahend
|
|
26
26
|
when 0 then return self
|
|
27
|
-
when Money then return
|
|
27
|
+
when Money then return copy_with(amount: amount - subtrahend.amount) if same_currency?(subtrahend)
|
|
28
28
|
end
|
|
29
29
|
raise TypeError, "#{subtrahend} can't be subtracted from #{self}"
|
|
30
30
|
end
|
|
@@ -32,7 +32,7 @@ module Mint
|
|
|
32
32
|
# Unary negation operator. Returns a new {Money} instance with the inverted sign.
|
|
33
33
|
#
|
|
34
34
|
# @return [Money] negated Money instance
|
|
35
|
-
def -@ =
|
|
35
|
+
def -@ = copy_with(amount: -amount)
|
|
36
36
|
|
|
37
37
|
# Performs multiplication of the monetary value by a standard scalar Numeric.
|
|
38
38
|
#
|
|
@@ -42,7 +42,7 @@ module Mint
|
|
|
42
42
|
def *(multiplicand)
|
|
43
43
|
raise TypeError, "#{self} can't be multiplied by #{multiplicand}" unless multiplicand.is_a?(Numeric)
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
copy_with(amount: amount * multiplicand)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# Performs division of the monetary value by a scalar Numeric or identical currency {Money}.
|
|
@@ -53,7 +53,7 @@ module Mint
|
|
|
53
53
|
# @raise [ZeroDivisionError] if division by zero is attempted
|
|
54
54
|
def /(divisor)
|
|
55
55
|
case divisor
|
|
56
|
-
when Numeric then return
|
|
56
|
+
when Numeric then return copy_with(amount: amount / divisor)
|
|
57
57
|
when Money then return amount / divisor.amount if same_currency? divisor
|
|
58
58
|
end
|
|
59
59
|
raise TypeError, "#{self} can't be divided by #{divisor}"
|
|
@@ -65,7 +65,7 @@ module Mint
|
|
|
65
65
|
# @return [Money] reult of amount ** exponent
|
|
66
66
|
# @raise [TypeError] if exponent is not Numeric
|
|
67
67
|
def **(exponent)
|
|
68
|
-
return
|
|
68
|
+
return copy_with(amount: amount**exponent) if exponent.is_a?(Numeric)
|
|
69
69
|
|
|
70
70
|
raise TypeError, "#{self} can't be powered by #{exponent}"
|
|
71
71
|
end
|
data/lib/minting/money/clamp.rb
CHANGED
|
@@ -44,7 +44,7 @@ module Mint
|
|
|
44
44
|
# Multiplies a Money object by the wrapped numeric value.
|
|
45
45
|
# This is the standard coercion path for `Numeric * Money`.
|
|
46
46
|
def *(other)
|
|
47
|
-
other.
|
|
47
|
+
other.copy_with(amount: @value * other.amount)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
# @private
|
|
@@ -44,6 +44,12 @@ module Mint
|
|
|
44
44
|
# @return [self, nil] self if amount is non-zero, nil otherwise
|
|
45
45
|
def nonzero? = amount.nonzero?
|
|
46
46
|
|
|
47
|
+
# Helper method to verify if another Money has the identical currency.
|
|
48
|
+
#
|
|
49
|
+
# @param other [Money] the target currency to compare
|
|
50
|
+
# @return [Boolean] true if currencies match, false otherwise
|
|
51
|
+
def same_currency?(other) = other.currency == currency
|
|
52
|
+
|
|
47
53
|
# @return [Boolean] true if amount is zero
|
|
48
54
|
def zero? = amount.zero?
|
|
49
55
|
end
|