minting 1.9.7 → 2.0.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -116
  3. data/Rakefile +2 -7
  4. data/doc/agents/api_review-2026-06-15.md +1 -1
  5. data/doc/agents/copilot-instructions.md +2 -2
  6. data/doc/agents/expired/copilot-instructions.md +2 -2
  7. data/doc/agents/expired/gemini_gem_evaluation.md +2 -2
  8. data/lib/minting/aliases.rb +22 -0
  9. data/lib/minting/currency/currency.rb +81 -9
  10. data/lib/minting/data/crypto-currencies.yaml +126 -0
  11. data/lib/minting/mint/i18n.rb +79 -29
  12. data/lib/minting/mint/mint.rb +1 -26
  13. data/lib/minting/mint/registry/crypto.rb +59 -0
  14. data/lib/minting/mint/registry/registration.rb +1 -2
  15. data/lib/minting/mint/registry/symbols.rb +37 -30
  16. data/lib/minting/mint/rounding.rb +9 -7
  17. data/lib/minting/mint.rb +1 -2
  18. data/lib/minting/money/allocation/allocation.rb +2 -2
  19. data/lib/minting/money/allocation/split.rb +1 -1
  20. data/lib/minting/money/arithmetics/operators.rb +10 -13
  21. data/lib/minting/money/clamp.rb +6 -6
  22. data/lib/minting/money/coercion.rb +1 -1
  23. data/lib/minting/money/comparable.rb +3 -3
  24. data/lib/minting/money/constructors.rb +3 -42
  25. data/lib/minting/money/conversion.rb +22 -18
  26. data/lib/minting/money/format/format.rb +100 -0
  27. data/lib/minting/money/format/formatter.rb +102 -0
  28. data/lib/minting/money/format/to_s.rb +20 -102
  29. data/lib/minting/money/format/validator.rb +34 -0
  30. data/lib/minting/money/money.rb +25 -9
  31. data/lib/minting/money/parse.rb +127 -0
  32. data/lib/minting/money/rounding.rb +27 -0
  33. data/lib/minting/version.rb +1 -1
  34. data/lib/minting.rb +17 -8
  35. metadata +9 -29
  36. data/doc/Mint/Currency.html +0 -2032
  37. data/doc/Mint/Money.html +0 -5139
  38. data/doc/Mint/RangeStepPatch.html +0 -277
  39. data/doc/Mint/Registry.html +0 -863
  40. data/doc/Mint/Rounding.html +0 -506
  41. data/doc/Mint/UnknownCurrency.html +0 -138
  42. data/doc/Mint.html +0 -931
  43. data/doc/Minting.html +0 -142
  44. data/doc/Numeric.html +0 -479
  45. data/doc/String.html +0 -241
  46. data/doc/_index.html +0 -206
  47. data/doc/class_list.html +0 -54
  48. data/doc/css/common.css +0 -1
  49. data/doc/css/full_list.css +0 -206
  50. data/doc/css/style.css +0 -1089
  51. data/doc/file.README.html +0 -291
  52. data/doc/file_list.html +0 -59
  53. data/doc/frames.html +0 -22
  54. data/doc/index.html +0 -291
  55. data/doc/js/app.js +0 -801
  56. data/doc/js/full_list.js +0 -334
  57. data/doc/js/jquery.js +0 -4
  58. data/doc/method_list.html +0 -758
  59. data/doc/top-level-namespace.html +0 -135
  60. data/lib/minting/mint/aliases.rb +0 -16
  61. data/lib/minting/mint/parser/parser.rb +0 -97
  62. data/lib/minting/mint/parser/separators.rb +0 -41
  63. data/lib/minting/money/format/formatting.rb +0 -130
@@ -1,135 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>
7
- Top Level Namespace
8
-
9
- &mdash; Documentation by YARD 0.9.44
10
-
11
- </title>
12
-
13
- <link rel="stylesheet" href="css/style.css" type="text/css">
14
-
15
- <link rel="stylesheet" href="css/common.css" type="text/css">
16
-
17
- <script type="text/javascript">
18
- pathId = "";
19
- relpath = '';
20
- </script>
21
-
22
-
23
- <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
24
-
25
- <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
26
-
27
-
28
- </head>
29
- <body>
30
- <div id="main_progress" aria-hidden="true"></div>
31
-
32
- <div class="nav_wrap">
33
- <iframe id="nav" src="class_list.html?1"></iframe>
34
- <div id="resizer"></div>
35
- </div>
36
-
37
- <div id="main" tabindex="-1">
38
- <div id="header">
39
- <div id="menu">
40
-
41
- <a href="_index.html">Index</a> &raquo;
42
-
43
-
44
- <span class="title">Top Level Namespace</span>
45
-
46
- </div>
47
-
48
- <div id="search">
49
-
50
- <a class="full_list_link" id="class_list_link"
51
- href="class_list.html">
52
-
53
- <svg width="24" height="24">
54
- <rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
55
- <rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
56
- <rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
57
- </svg>
58
- </a>
59
-
60
- </div>
61
- <div class="clear"></div>
62
- </div>
63
-
64
- <div id="content"><h1>Top Level Namespace
65
-
66
-
67
-
68
- </h1>
69
- <div class="box_info">
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
- </div>
82
-
83
- <h2>Defined Under Namespace</h2>
84
- <p class="children">
85
-
86
-
87
- <strong class="modules">Modules:</strong> <span class='object_link'><a href="Mint.html" title="Mint (module)">Mint</a></span>, <span class='object_link'><a href="Minting.html" title="Minting (module)">Minting</a></span>
88
-
89
-
90
-
91
- <strong class="classes">Classes:</strong> <span class='object_link'><a href="Numeric.html" title="Numeric (class)">Numeric</a></span>, <span class='object_link'><a href="String.html" title="String (class)">String</a></span>
92
-
93
-
94
- </p>
95
-
96
-
97
- <h2>
98
- Constant Summary
99
- <small><a href="#" class="constants_summary_toggle">collapse</a></small>
100
- </h2>
101
-
102
- <dl class="constants">
103
-
104
- <dt id="Money-constant" class="">Money =
105
-
106
- </dt>
107
- <dd><pre class="code"><span class='const'><span class='object_link'><a href="Mint.html" title="Mint (module)">Mint</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Mint/Money.html" title="Mint::Money (class)">Money</a></span></span></pre></dd>
108
-
109
- <dt id="Currency-constant" class="">Currency =
110
-
111
- </dt>
112
- <dd><pre class="code"><span class='const'><span class='object_link'><a href="Mint.html" title="Mint (module)">Mint</a></span></span><span class='op'>::</span><span class='const'><span class='object_link'><a href="Mint/Currency.html" title="Mint::Currency (class)">Currency</a></span></span></pre></dd>
113
-
114
- </dl>
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
- </div>
126
-
127
- <div id="footer">
128
- Generated on Wed Jul 1 01:11:52 2026 by
129
- <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
130
- 0.9.44 (ruby-4.0.5).
131
- </div>
132
-
133
- </div>
134
- </body>
135
- </html>
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Optional top-level alias for Mint::Currency.
4
- #
5
- # Mint::Money is auto-bound as the top-level Money constant by
6
- # `require 'minting'` (see lib/minting.rb). Currency is not auto-bound
7
- # because application domain models are commonly named Currency (e.g. a
8
- # Rails model). Require this file to opt in:
9
- #
10
- # require 'minting/mint/aliases'
11
- #
12
- if defined?(Currency) && Currency != Mint::Currency
13
- warn "minting: top-level Currency is already defined (#{Currency}); skipping alias."
14
- else
15
- Currency = Mint::Currency unless defined?(Currency)
16
- end
@@ -1,97 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Mint Money parsing
4
- module Mint
5
- extend self
6
-
7
- # Parses a human-readable money string into a {Money} object.
8
- #
9
- # Returns +nil+ when the input is invalid or currency cannot be determined.
10
- #
11
- # @param input [String] Amount input, optionally including a currency symbol or code
12
- # @param currency [String, Symbol, Currency, nil] ISO code when not present in +input+
13
- # @return [Money, nil]
14
- #
15
- # @example With explicit currency
16
- # Mint.parse('19.99', 'USD') #=> [USD 19.99]
17
- # Mint.parse('garbage', 'USD') #=> nil
18
- #
19
- # @example With symbol or code in the string
20
- # Mint.parse('$19.99') #=> [USD 19.99]
21
- # Mint.parse('USD 1,234.56') #=> [USD 1234.56]
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)
49
- raise ArgumentError, 'input must be a String' unless input.is_a?(String)
50
-
51
- input = input.strip
52
- raise ArgumentError, 'input cannot be empty' if input.empty?
53
-
54
- currency = parse_currency(input, currency)
55
- raise ArgumentError, "Currency [#{currency}] not found" unless currency
56
-
57
- amount = parse_amount(input)
58
- raise ArgumentError, "Could not parse [#{input}]" unless amount
59
-
60
- amount = currency.normalize_amount(amount)
61
- Mint::Money.new(amount, currency)
62
- end
63
-
64
- private
65
-
66
- # Extracts a numeric value from input that should only contain an amount.
67
- # @private
68
- def parse_amount(input)
69
- accounting_negative = input.start_with?('(') && input.end_with?(')')
70
-
71
- # Remove any charater that is not a digit, comma or period
72
- numeric_input = input.scan(/[\d.,-]/).join
73
- numeric = parse_separators(numeric_input)
74
- return nil unless numeric
75
-
76
- amount = Rational(numeric)
77
- accounting_negative ? -amount : amount
78
- end
79
-
80
- # Extracts currency from a string by matching ISO code or symbol.
81
- #
82
- # Scans all uppercase words and returns the first registered code, falling
83
- # back to symbol matching. This correctly handles inputs like
84
- # "MAX 10.00 USD" where the first uppercase word isn't a currency code.
85
- # @private
86
- def parse_currency(input, currency = nil)
87
- input.scan(/\b([A-Z_]+)\b/) do |(code)|
88
- found = Currency.for_code(code)
89
- return found if found
90
- end
91
-
92
- found = Registry.detect_currency(input)
93
- return found if found
94
-
95
- Currency.resolve(currency)
96
- end
97
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Mint Money parsing
4
- module Mint
5
- extend self
6
-
7
- private
8
-
9
- # Classifies the separator pattern in a numeric string.
10
- # @private
11
- def classify_separators(numeric)
12
- case [numeric.count('.'), numeric.count(',')]
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
- end
20
- end
21
-
22
- # Converts locale-specific decimal/thousand separators into a plain decimal string.
23
- # @private
24
- def parse_separators(numeric)
25
- return nil unless numeric.match?(/\d/)
26
-
27
- case classify_separators(numeric)
28
- when :decimal_period then numeric # Nothing to normalize (e.g. "1500" or "34.21").
29
- when :decimal_comma then numeric.tr(',', '.') # Only one comma: decimal (e.g. 19,99 or 1,234).
30
- when :thousands_comma then numeric.delete(',')
31
- when :thousands then numeric.delete('.,')
32
- when :invalid then nil
33
- when :mixed # Commas and dots: the rightmost one is the decimal separator.
34
- if numeric.rindex(',') > numeric.rindex('.')
35
- numeric.delete('.').tr(',', '.')
36
- else
37
- numeric.delete(',')
38
- end
39
- end
40
- end
41
- end
@@ -1,130 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mint
4
- # :nodoc:
5
- class Money
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
34
-
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}.
38
- # @private
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
87
- end
88
- end
89
-
90
- private
91
-
92
- # Validates that format hash contains only known keys.
93
- # @private
94
- def validate_format_hash(format)
95
- unknown = format.keys - %i[positive negative zero]
96
-
97
- raise ArgumentError, "Unknown format parameter(s): #{unknown.inspect}. " unless unknown.empty?
98
- end
99
-
100
- # Validates +decimal+ and +thousand+ separator arguments.
101
- # @private
102
- def validate_separators!(decimal:, thousand:)
103
- case decimal
104
- when nil # :noop
105
- when '' then raise ArgumentError, 'decimal must be a non-empty'
106
- when thousand then raise ArgumentError, "decimal and thousand cannot be identical: #{decimal.inspect}"
107
- when String # :noop
108
- else raise ArgumentError, "decimal must be a String, false, or nil, got #{decimal.inspect}"
109
- end
110
-
111
- case thousand
112
- when false, nil, String # :noop
113
- else raise ArgumentError, "thousand must be a String, false, or nil, got #{thousand.inspect}"
114
- end
115
- end
116
-
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.
121
- # @private
122
- def format_amount(format, decimal:, thousand:)
123
- key = [format, currency_code, decimal, thousand].hash
124
-
125
- formatter = Money.compiled_formatters[key] ||= Money.compile_formatter(format, currency, decimal, thousand)
126
-
127
- formatter.call(amount)
128
- end
129
- end
130
- end