minting 2.1.1 → 2.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 +142 -40
- data/Rakefile +2 -7
- data/doc/agents/AGENTS.md +382 -0
- data/doc/api_review-2026-08-13.md +144 -0
- data/doc/security-report.md +137 -0
- data/lib/minting/aliases.rb +8 -8
- data/lib/minting/currency/currency.rb +1 -7
- data/lib/minting/currency/registry.rb +6 -5
- data/lib/minting/currency/rounding.rb +14 -1
- data/lib/minting/mint/dsl/numeric.rb +7 -2
- data/lib/minting/mint/dsl/string.rb +2 -2
- data/lib/minting/mint/mint.rb +6 -1
- data/lib/minting/mint/registry/crypto.rb +7 -3
- data/lib/minting/mint/registry/registration.rb +6 -2
- data/lib/minting/mint/registry/registry.rb +1 -1
- data/lib/minting/money/allocation/allocation.rb +6 -1
- data/lib/minting/money/clamp.rb +6 -16
- data/lib/minting/money/constructors.rb +1 -1
- data/lib/minting/money/conversion.rb +3 -0
- data/lib/minting/money/format/format.rb +10 -1
- data/lib/minting/money/format/formatter.rb +50 -22
- data/lib/minting/money/format/to_s.rb +4 -3
- data/lib/minting/money/money.rb +2 -1
- data/lib/minting/money/parse/separator_parser.rb +60 -0
- data/lib/minting/money/parse.rb +74 -14
- data/lib/minting/money/rounding.rb +4 -5
- data/lib/minting/version.rb +1 -1
- metadata +6 -2
data/lib/minting/money/parse.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'parse/separator_parser'
|
|
4
|
+
|
|
3
5
|
module Mint
|
|
4
6
|
# :nodoc:
|
|
5
7
|
class Money
|
|
@@ -8,7 +10,15 @@ module Mint
|
|
|
8
10
|
# Returns +nil+ when the input is invalid or currency cannot be determined.
|
|
9
11
|
#
|
|
10
12
|
# @param input [String] Amount input, optionally including a currency symbol or code
|
|
11
|
-
# @param
|
|
13
|
+
# @param positional_currency [String, Currency, nil] deprecated positional
|
|
14
|
+
# fallback currency when none is present in +input+
|
|
15
|
+
# @param default_currency [String, Currency, nil] fallback currency when none is present in +input+
|
|
16
|
+
# An embedded currency code or symbol takes precedence over this argument.
|
|
17
|
+
# The positional form is deprecated; use +default_currency:+ instead.
|
|
18
|
+
# @param decimal [String, nil] decimal separator used by the input source.
|
|
19
|
+
# When omitted, it is inferred from +thousand:+ or separator positions.
|
|
20
|
+
# @param thousand [String, nil] thousands separator used by the input source.
|
|
21
|
+
# When omitted, it is inferred from +decimal:+ or separator positions.
|
|
12
22
|
# @return [Money, nil]
|
|
13
23
|
#
|
|
14
24
|
# @example With explicit currency
|
|
@@ -18,63 +28,95 @@ module Mint
|
|
|
18
28
|
# @example With symbol or code in the string
|
|
19
29
|
# Money.parse('$19.99') #=> [USD 19.99]
|
|
20
30
|
# Money.parse('USD 1,234.56') #=> [USD 1234.56]
|
|
21
|
-
|
|
31
|
+
# Money.parse('123,456', 'EUR', decimal: ',') #=> [EUR 123.46]
|
|
32
|
+
# Money.parse('123,456', 'USD', thousand: ',') #=> [USD 123456.00]
|
|
33
|
+
# @deprecated Pass the fallback currency as +default_currency:+.
|
|
34
|
+
def self.parse(input, positional_currency = nil, default_currency: nil, decimal: nil, thousand: nil)
|
|
35
|
+
if positional_currency
|
|
36
|
+
warn 'DEPRECATION: Money.parse positional currency is deprecated; use default_currency: instead.', uplevel: 1
|
|
37
|
+
end
|
|
22
38
|
return nil unless input.is_a?(String)
|
|
23
39
|
|
|
24
40
|
input = input.strip
|
|
25
41
|
return nil if input.empty?
|
|
26
42
|
|
|
43
|
+
currency = default_currency || positional_currency
|
|
27
44
|
currency = parse_currency(input, currency)
|
|
28
45
|
return nil unless currency
|
|
29
46
|
|
|
30
|
-
amount = parse_amount(input)
|
|
47
|
+
amount = parse_amount(input, currency, decimal:, thousand:)
|
|
31
48
|
return nil unless amount
|
|
32
49
|
|
|
33
50
|
amount = currency.normalize_amount(amount)
|
|
34
|
-
new(amount, currency)
|
|
51
|
+
amount.zero? ? currency.zero : new(amount, currency)
|
|
35
52
|
end
|
|
36
53
|
|
|
37
54
|
# Like {.parse} but raises on failure.
|
|
38
55
|
#
|
|
39
56
|
# @param input [String] Amount input, optionally including a currency symbol or code
|
|
40
|
-
# @param
|
|
57
|
+
# @param positional_currency [String, Currency, nil] deprecated positional
|
|
58
|
+
# fallback currency when none is present in +input+
|
|
59
|
+
# @param default_currency [String, Currency, nil] fallback currency when none is present in +input+
|
|
60
|
+
# An embedded currency code or symbol takes precedence over this argument.
|
|
61
|
+
# The positional form is deprecated; use +default_currency:+ instead.
|
|
62
|
+
# @param decimal [String, nil] decimal separator used by the input source.
|
|
63
|
+
# When omitted, it is inferred from +thousand:+ or separator positions.
|
|
64
|
+
# @param thousand [String, nil] thousands separator used by the input source.
|
|
65
|
+
# When omitted, it is inferred from +decimal:+ or separator positions.
|
|
41
66
|
# @return [Money]
|
|
42
67
|
# @raise [ArgumentError] when +input+ is invalid or currency cannot be determined
|
|
43
68
|
#
|
|
44
69
|
# @example
|
|
45
70
|
# Money.parse!('19.99', 'USD') #=> [USD 19.99]
|
|
46
71
|
# Money.parse!('garbage', 'USD') #=> ArgumentError
|
|
47
|
-
|
|
72
|
+
# @deprecated Pass the fallback currency as +default_currency:+.
|
|
73
|
+
def self.parse!(input, positional_currency = nil, default_currency: nil, decimal: nil, thousand: nil)
|
|
74
|
+
if positional_currency
|
|
75
|
+
warn 'DEPRECATION: Money.parse! positional currency is deprecated; use default_currency: instead.', uplevel: 1
|
|
76
|
+
end
|
|
48
77
|
raise ArgumentError, 'input must be a String' unless input.is_a?(String)
|
|
49
78
|
|
|
50
79
|
input = input.strip
|
|
51
80
|
raise ArgumentError, 'input cannot be empty' if input.empty?
|
|
52
81
|
|
|
82
|
+
currency = default_currency || positional_currency
|
|
53
83
|
currency = parse_currency(input, currency)
|
|
54
84
|
raise ArgumentError, "Currency [#{currency}] not found" unless currency
|
|
55
85
|
|
|
56
|
-
amount = parse_amount(input)
|
|
86
|
+
amount = parse_amount(input, currency, decimal:, thousand:)
|
|
57
87
|
raise ArgumentError, "Could not parse [#{input}]" unless amount
|
|
58
88
|
|
|
59
89
|
amount = currency.normalize_amount(amount)
|
|
60
|
-
new(amount, currency)
|
|
90
|
+
amount.zero? ? currency.zero : new(amount, currency)
|
|
61
91
|
end
|
|
62
92
|
|
|
63
93
|
class << self
|
|
64
94
|
private
|
|
65
95
|
|
|
66
|
-
# Extracts
|
|
67
|
-
|
|
96
|
+
# Extracts one valid numeric value, allowing surrounding currency markers
|
|
97
|
+
# and uppercase annotation words (for example, "MAX 10.00 USD").
|
|
98
|
+
def parse_amount(input, currency, decimal: nil, thousand: nil)
|
|
68
99
|
accounting_negative = input.start_with?('(') && input.end_with?(')')
|
|
100
|
+
return nil if (input.include?('(') || input.include?(')')) && !accounting_negative
|
|
69
101
|
|
|
70
|
-
numeric_input = input
|
|
71
|
-
|
|
102
|
+
numeric_input = accounting_negative ? input[1...-1] : input
|
|
103
|
+
numeric_input = remove_currency_markers(numeric_input, currency)
|
|
104
|
+
numeric_input = numeric_input.gsub(/\b[A-Z_]+\b/, ' ').delete('[]').strip
|
|
105
|
+
numeric_input.sub!(/\A([+-])\s+/, '\\1')
|
|
106
|
+
return nil unless numeric_input.match?(/\A[+-]?\d[\d.,]*\z/)
|
|
107
|
+
|
|
108
|
+
numeric = parse_separators(numeric_input, decimal:, thousand:)
|
|
72
109
|
return nil unless numeric
|
|
73
110
|
|
|
74
111
|
amount = Rational(numeric)
|
|
75
112
|
accounting_negative ? -amount : amount
|
|
76
113
|
end
|
|
77
114
|
|
|
115
|
+
def remove_currency_markers(input, currency)
|
|
116
|
+
markers = [currency.symbol, currency.disambiguate_symbol].compact.uniq
|
|
117
|
+
markers.reduce(input) { |result, marker| result.gsub(marker, ' ') }
|
|
118
|
+
end
|
|
119
|
+
|
|
78
120
|
# Extracts currency from a string by matching ISO code or symbol.
|
|
79
121
|
#
|
|
80
122
|
# Scans all uppercase words and returns the first registered code, falling
|
|
@@ -92,9 +134,17 @@ module Mint
|
|
|
92
134
|
Currency.resolve(currency)
|
|
93
135
|
end
|
|
94
136
|
|
|
95
|
-
# Converts
|
|
96
|
-
|
|
137
|
+
# Converts decimal/thousand separators into a plain decimal string.
|
|
138
|
+
# An explicit decimal separator resolves otherwise ambiguous values.
|
|
139
|
+
def parse_separators(numeric, decimal: nil, thousand: nil)
|
|
140
|
+
return SeparatorParser.parse(numeric, decimal, thousand) if decimal || thousand
|
|
141
|
+
|
|
142
|
+
parse_heuristic_separators(numeric)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def parse_heuristic_separators(numeric)
|
|
97
146
|
return nil unless numeric.match?(/\d/)
|
|
147
|
+
return nil unless valid_numeric_syntax?(numeric)
|
|
98
148
|
|
|
99
149
|
case classify_separators(numeric)
|
|
100
150
|
when :decimal_period then numeric
|
|
@@ -122,6 +172,16 @@ module Mint
|
|
|
122
172
|
else :thousands
|
|
123
173
|
end
|
|
124
174
|
end
|
|
175
|
+
|
|
176
|
+
def valid_numeric_syntax?(numeric)
|
|
177
|
+
unsigned = numeric.delete_prefix('-').delete_prefix('+')
|
|
178
|
+
unsigned.match?(/\A\d+\z/) ||
|
|
179
|
+
unsigned.match?(/\A\d+[.,]\d+\z/) ||
|
|
180
|
+
unsigned.match?(/\A\d+(?:,\d{3})+\.\d+\z/) ||
|
|
181
|
+
unsigned.match?(/\A\d+(?:\.\d{3})+,\d+\z/) ||
|
|
182
|
+
unsigned.match?(/\A\d+(?:,\d{3})+\z/) ||
|
|
183
|
+
unsigned.match?(/\A\d+(?:\.\d{3})+\z/)
|
|
184
|
+
end
|
|
125
185
|
end
|
|
126
186
|
end
|
|
127
187
|
end
|
|
@@ -9,11 +9,10 @@ module Mint
|
|
|
9
9
|
# Restores the previous mode (or default) when the block exits, even on
|
|
10
10
|
# exception.
|
|
11
11
|
#
|
|
12
|
-
# Rounding-mode support is activated on first call.
|
|
13
|
-
# +Currency#normalize_amount+
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# incurs zero overhead.
|
|
12
|
+
# Rounding-mode support is activated on first call. Before activation,
|
|
13
|
+
# +Currency#normalize_amount+ uses a direct +Rational#round+ fast path with
|
|
14
|
+
# no thread-local lookup. Once activated, normalization reads the
|
|
15
|
+
# thread-local mode so custom rounding remains isolated per thread.
|
|
17
16
|
#
|
|
18
17
|
# @param mode [Symbol] one of: +:up+, +:down+, +:even+
|
|
19
18
|
# @yield block to execute with the rounding mode active
|
data/lib/minting/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: minting
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gilson Ferraz
|
|
@@ -36,6 +36,7 @@ files:
|
|
|
36
36
|
- bin/check-currencies
|
|
37
37
|
- bin/console
|
|
38
38
|
- bin/setup
|
|
39
|
+
- doc/agents/AGENTS.md
|
|
39
40
|
- doc/agents/api_review-2026-06-15.md
|
|
40
41
|
- doc/agents/copilot-instructions.md
|
|
41
42
|
- doc/agents/expired/AGENTS.md
|
|
@@ -43,6 +44,8 @@ files:
|
|
|
43
44
|
- doc/agents/expired/gemini_gem_evaluation.md
|
|
44
45
|
- doc/agents/expired/recommendations.md
|
|
45
46
|
- doc/agents/expired/rubocop-issues.md
|
|
47
|
+
- doc/api_review-2026-08-13.md
|
|
48
|
+
- doc/security-report.md
|
|
46
49
|
- lib/minting.rb
|
|
47
50
|
- lib/minting/aliases.rb
|
|
48
51
|
- lib/minting/currency/currency.rb
|
|
@@ -76,6 +79,7 @@ files:
|
|
|
76
79
|
- lib/minting/money/format/validator.rb
|
|
77
80
|
- lib/minting/money/money.rb
|
|
78
81
|
- lib/minting/money/parse.rb
|
|
82
|
+
- lib/minting/money/parse/separator_parser.rb
|
|
79
83
|
- lib/minting/money/rounding.rb
|
|
80
84
|
- lib/minting/version.rb
|
|
81
85
|
- minting.gemspec
|
|
@@ -104,7 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
104
108
|
- !ruby/object:Gem::Version
|
|
105
109
|
version: '0'
|
|
106
110
|
requirements: []
|
|
107
|
-
rubygems_version: 4.0.
|
|
111
|
+
rubygems_version: 4.0.18
|
|
108
112
|
specification_version: 4
|
|
109
113
|
summary: Library to manipulate currency values
|
|
110
114
|
test_files: []
|