minting 1.9.6 → 1.9.7
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 +30 -25
- data/Rakefile +16 -20
- data/bin/bench_check +2 -2
- data/doc/Mint/Currency.html +183 -94
- data/doc/Mint/Money.html +866 -334
- data/doc/Mint/RangeStepPatch.html +1 -1
- data/doc/Mint/Registry.html +10 -10
- data/doc/Mint/Rounding.html +2 -2
- data/doc/Mint/UnknownCurrency.html +6 -4
- data/doc/Mint.html +33 -106
- data/doc/Minting.html +2 -2
- data/doc/Numeric.html +479 -0
- data/doc/String.html +241 -0
- data/doc/_index.html +27 -1
- data/doc/agents/copilot-instructions.md +3 -3
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +47 -33
- data/doc/index.html +47 -33
- data/doc/method_list.html +125 -53
- data/doc/top-level-namespace.html +5 -21
- data/lib/minting/currency/currency.rb +6 -3
- data/lib/minting/data/world-currencies.yaml +75 -23
- data/lib/minting/mint/i18n.rb +1 -1
- data/lib/minting/mint/rounding.rb +1 -0
- data/lib/minting/money/conversion.rb +1 -0
- data/lib/minting/money/format/formatting.rb +88 -55
- data/lib/minting/money/format/to_s.rb +20 -3
- data/lib/minting/money/money.rb +0 -4
- data/lib/minting/version.rb +1 -1
- metadata +3 -2
- data/doc/Mint/CurrencyRegistry.html +0 -511
|
@@ -3,24 +3,92 @@
|
|
|
3
3
|
module Mint
|
|
4
4
|
# :nodoc:
|
|
5
5
|
class Money
|
|
6
|
-
|
|
6
|
+
def self.compiled_formatters
|
|
7
|
+
@compiled_formatters ||= {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Build each sign template with subunit precision baked in, returning a
|
|
11
|
+
# frozen Hash of templates keyed by sign (:positive, :negative, :zero).
|
|
12
|
+
# Keeps %<symbol>s and %<currency>s as format args so width/padding specifiers work.
|
|
13
|
+
# @private
|
|
14
|
+
def self.compile_templates(format_hash, subunit)
|
|
15
|
+
[
|
|
16
|
+
format_hash[:positive] || DEFAULT_FORMAT,
|
|
17
|
+
format_hash[:negative],
|
|
18
|
+
format_hash[:zero]
|
|
19
|
+
].map do |sign_format|
|
|
20
|
+
next unless sign_format
|
|
21
|
+
|
|
22
|
+
# Injects the currency's subunit precision into %<amount>f specifiers
|
|
23
|
+
# (e.g. '%<amount>f' → '%<amount>.2f' for USD), preserving any
|
|
24
|
+
# existing width/alignment specifier (e.g. '%<amount>+10f' stays).
|
|
25
|
+
sign_format = sign_format.gsub(/%<amount>(\s*\+?\d*)f/, "%<amount>\\1.#{subunit}f")
|
|
26
|
+
|
|
27
|
+
# For zero-subunit currencies (JPY, KRW, etc.), strip %<fractional>d
|
|
28
|
+
# specifiers entirely since there is no fractional part to display.
|
|
29
|
+
sign_format.gsub!(/%<fractional>[^%]*?d/, '') if subunit.zero?
|
|
30
|
+
|
|
31
|
+
sign_format
|
|
32
|
+
end
|
|
33
|
+
end
|
|
7
34
|
|
|
8
|
-
#
|
|
9
|
-
#
|
|
35
|
+
# Builds and returns a lambda that formats amounts for a fixed
|
|
36
|
+
# [format_config, currency, decimal, thousand] combination.
|
|
37
|
+
# The lambda is cached by {.compiled_formatters}.
|
|
10
38
|
# @private
|
|
11
|
-
def
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
39
|
+
def self.compile_formatter(format_hash, currency, decimal, thousand) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
40
|
+
subunit = currency.subunit
|
|
41
|
+
has_decimal_substitution = decimal != '.'
|
|
42
|
+
escaped_decimal = Regexp.escape(decimal)
|
|
43
|
+
has_thousand_separator = thousand && !thousand.empty?
|
|
44
|
+
|
|
45
|
+
templates = compile_templates(format_hash, subunit)
|
|
46
|
+
positive_template, negative_template, zero_template = templates
|
|
47
|
+
|
|
48
|
+
# Detect whether templates use %<fractional> and/or %<d-symbol>
|
|
49
|
+
all_templates = templates.compact.join
|
|
50
|
+
needs_fractional = all_templates.include?('%<fractional>')
|
|
51
|
+
needs_integral = all_templates.include?('%<amount>') || all_templates.include?('%<integral>')
|
|
52
|
+
multiplier = currency.fractional_multiplier
|
|
53
|
+
symbol = currency.symbol
|
|
54
|
+
|
|
55
|
+
args = {
|
|
56
|
+
currency: currency.code,
|
|
57
|
+
dsymbol: currency.disambiguate_symbol || symbol,
|
|
58
|
+
symbol: symbol
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
lambda do |amount|
|
|
62
|
+
format_template = if negative_template && amount < 0
|
|
63
|
+
amount = -amount
|
|
64
|
+
negative_template
|
|
65
|
+
elsif zero_template && amount == 0
|
|
66
|
+
zero_template
|
|
67
|
+
else
|
|
68
|
+
positive_template
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
args[:amount] = amount
|
|
72
|
+
args[:integral] = amount.to_i
|
|
73
|
+
args[:fractional] = ((amount.abs % 1) * multiplier).to_i if needs_fractional
|
|
74
|
+
|
|
75
|
+
result = Kernel.format(format_template, **args)
|
|
76
|
+
result.gsub!(/(?<=\d)\.(?=\d)/, decimal) if has_decimal_substitution
|
|
77
|
+
|
|
78
|
+
if needs_integral && has_thousand_separator && (amount >= 1000 || amount <= -1000)
|
|
79
|
+
# Split on the decimal separator between digits only — symbols may contain '.' (e.g. د.إ).
|
|
80
|
+
parts = result.split(/(?<=\d)#{escaped_decimal}(?=\d)/, 2)
|
|
81
|
+
# Insert the thousands delimiter before each run of 3 digits from the right.
|
|
82
|
+
parts[0].gsub!(/(\d)(?=(?:\d{3})+(?:[^\d]|$))/) { Regexp.last_match(1) + thousand }
|
|
83
|
+
result = parts.join(decimal)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
result
|
|
21
87
|
end
|
|
22
88
|
end
|
|
23
89
|
|
|
90
|
+
private
|
|
91
|
+
|
|
24
92
|
# Validates that format hash contains only known keys.
|
|
25
93
|
# @private
|
|
26
94
|
def validate_format_hash(format)
|
|
@@ -46,52 +114,17 @@ module Mint
|
|
|
46
114
|
end
|
|
47
115
|
end
|
|
48
116
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
parts = string.split(/(?<=\d)#{Regexp.escape(decimal)}(?=\d)/, 2)
|
|
54
|
-
parts[0].gsub!(/(\d)(?=(?:\d{3})+(?:[^\d]|$))/, "\\1#{thousand}")
|
|
55
|
-
parts.join(decimal)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Applies a format template to produce a formatted string representation.
|
|
117
|
+
# Applies a format template to the money amount, returning a formatted string.
|
|
118
|
+
# Uses a cached compiled formatter lambda that pre-resolves currency-specific
|
|
119
|
+
# values (symbol, code, subunit) so per-call work is reduced to
|
|
120
|
+
# Kernel.format + optional separator substitutions.
|
|
59
121
|
# @private
|
|
60
|
-
#
|
|
61
122
|
def format_amount(format, decimal:, thousand:)
|
|
62
|
-
|
|
63
|
-
resolved_format, adjusted_amount = resolve_format(format)
|
|
64
|
-
|
|
65
|
-
resolved_format = inject_subunit_precision(resolved_format, subunit)
|
|
66
|
-
|
|
67
|
-
result = Kernel.format(resolved_format, {
|
|
68
|
-
amount: adjusted_amount,
|
|
69
|
-
currency: currency_code,
|
|
70
|
-
symbol: currency.symbol,
|
|
71
|
-
integral: adjusted_amount.to_i,
|
|
72
|
-
fractional: fractional
|
|
73
|
-
})
|
|
123
|
+
key = [format, currency_code, decimal, thousand].hash
|
|
74
124
|
|
|
75
|
-
|
|
76
|
-
result.gsub!(/(?<=\d)\.(?=\d)/, decimal) if decimal != '.'
|
|
125
|
+
formatter = Money.compiled_formatters[key] ||= Money.compile_formatter(format, currency, decimal, thousand)
|
|
77
126
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# Apply thousands only to the integral portion, using the decimal as boundary
|
|
81
|
-
apply_thousand_separator(result, decimal:, thousand:)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Injects the currency's subunit precision into %<amount>f specifiers
|
|
85
|
-
# (e.g. '%<amount>f' → '%<amount>.2f' for USD). For zero-subunit
|
|
86
|
-
# currencies (e.g. JPY), strips %<fractional>d specifiers.
|
|
87
|
-
# @private
|
|
88
|
-
def inject_subunit_precision(template, subunit)
|
|
89
|
-
cache ||= {}
|
|
90
|
-
cache[[template, subunit]] ||= begin
|
|
91
|
-
result = template.gsub(/%<amount>(\s*\+?\d*)f/, "%<amount>\\1.#{subunit}f")
|
|
92
|
-
result.gsub!(/%<fractional>[^%]*?d/, '') if subunit.zero?
|
|
93
|
-
result.freeze
|
|
94
|
-
end
|
|
127
|
+
formatter.call(amount)
|
|
95
128
|
end
|
|
96
129
|
end
|
|
97
130
|
end
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
module Mint
|
|
4
4
|
# :nodoc:
|
|
5
5
|
class Money
|
|
6
|
+
# The default display format pattern for formatting monetary values.
|
|
7
|
+
# Uses `%<symbol>s` for the currency symbol and `%<amount>f` for the rounded amount.
|
|
8
|
+
DEFAULT_FORMAT = '%<symbol>s%<amount>f'
|
|
9
|
+
|
|
6
10
|
PRESETS = {
|
|
7
11
|
amount: { format: '%<amount>f' },
|
|
8
12
|
accounting: { format: { negative: '(%<symbol>s%<amount>f)' } },
|
|
@@ -17,7 +21,7 @@ module Mint
|
|
|
17
21
|
# When provided, expands to the preset's format options and merges
|
|
18
22
|
# with any explicit keyword arguments (kwargs override the preset).
|
|
19
23
|
# @param format [String, Hash, nil] Either a Format string with placeholders
|
|
20
|
-
# (%<symbol>s, %<amount>f, %<currency>s, %<integral>d, %<fractional>d),
|
|
24
|
+
# (%<symbol>s, %<amount>f, %<currency>s, %<integral>d, %<fractional>d, %<dsymbol>s),
|
|
21
25
|
# or a Hash with per-sign keys (:positive, :negative, :zero) each
|
|
22
26
|
# holding a format string. A Hash is convenient for sign-aware formats
|
|
23
27
|
# such as accounting parentheses:
|
|
@@ -96,12 +100,25 @@ module Mint
|
|
|
96
100
|
else raise ArgumentError, 'Invalid format. Only String or Hash are accepted'
|
|
97
101
|
end
|
|
98
102
|
|
|
99
|
-
formatted = format_amount(format, decimal
|
|
103
|
+
formatted = format_amount(format, decimal:, thousand:)
|
|
100
104
|
|
|
101
105
|
width ? formatted.rjust(width) : formatted
|
|
102
106
|
end
|
|
103
107
|
|
|
104
|
-
|
|
108
|
+
THOUSAND_RE = /(\d)(?=(\d{3})+\z)/
|
|
109
|
+
|
|
110
|
+
def to_s
|
|
111
|
+
return format unless Mint.locale_backend.nil?
|
|
112
|
+
|
|
113
|
+
subunit = currency.subunit
|
|
114
|
+
integral = to_i.to_s
|
|
115
|
+
integral.gsub!(THOUSAND_RE, '\1,') if amount.abs >= 1000
|
|
116
|
+
if subunit > 0
|
|
117
|
+
"#{currency.symbol}#{integral}.#{fractional.to_s.rjust(subunit, '0')}"
|
|
118
|
+
else
|
|
119
|
+
"#{currency.symbol}#{integral}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
105
122
|
|
|
106
123
|
alias to_fs :format
|
|
107
124
|
end
|
data/lib/minting/money/money.rb
CHANGED
|
@@ -17,10 +17,6 @@ module Mint
|
|
|
17
17
|
# Money objects are immutable and support arithmetic, comparison,
|
|
18
18
|
# formatting, allocation, and parsing operations.
|
|
19
19
|
class Money
|
|
20
|
-
# The default display format pattern for formatting monetary values.
|
|
21
|
-
# Uses `%<symbol>s` for the currency symbol and `%<amount>f` for the rounded amount.
|
|
22
|
-
DEFAULT_FORMAT = '%<symbol>s%<amount>f'
|
|
23
|
-
|
|
24
20
|
attr_reader :amount, :currency
|
|
25
21
|
|
|
26
22
|
# Returns the ISO 3-letter currency code string.
|
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: 1.9.
|
|
4
|
+
version: 1.9.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gilson Ferraz
|
|
@@ -38,13 +38,14 @@ files:
|
|
|
38
38
|
- bin/setup
|
|
39
39
|
- doc/Mint.html
|
|
40
40
|
- doc/Mint/Currency.html
|
|
41
|
-
- doc/Mint/CurrencyRegistry.html
|
|
42
41
|
- doc/Mint/Money.html
|
|
43
42
|
- doc/Mint/RangeStepPatch.html
|
|
44
43
|
- doc/Mint/Registry.html
|
|
45
44
|
- doc/Mint/Rounding.html
|
|
46
45
|
- doc/Mint/UnknownCurrency.html
|
|
47
46
|
- doc/Minting.html
|
|
47
|
+
- doc/Numeric.html
|
|
48
|
+
- doc/String.html
|
|
48
49
|
- doc/_index.html
|
|
49
50
|
- doc/agents/api_review-2026-06-15.md
|
|
50
51
|
- doc/agents/copilot-instructions.md
|