minting 1.9.6 → 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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +244 -121
  3. data/Rakefile +17 -26
  4. data/bin/bench_check +2 -2
  5. data/doc/agents/api_review-2026-06-15.md +1 -1
  6. data/doc/agents/copilot-instructions.md +5 -5
  7. data/doc/agents/expired/copilot-instructions.md +2 -2
  8. data/doc/agents/expired/gemini_gem_evaluation.md +2 -2
  9. data/lib/minting/aliases.rb +22 -0
  10. data/lib/minting/currency/currency.rb +87 -12
  11. data/lib/minting/data/crypto-currencies.yaml +126 -0
  12. data/lib/minting/data/world-currencies.yaml +75 -23
  13. data/lib/minting/mint/i18n.rb +79 -29
  14. data/lib/minting/mint/mint.rb +1 -26
  15. data/lib/minting/mint/registry/crypto.rb +59 -0
  16. data/lib/minting/mint/registry/registration.rb +1 -2
  17. data/lib/minting/mint/registry/symbols.rb +37 -30
  18. data/lib/minting/mint/rounding.rb +10 -7
  19. data/lib/minting/mint.rb +1 -2
  20. data/lib/minting/money/allocation/allocation.rb +2 -2
  21. data/lib/minting/money/allocation/split.rb +1 -1
  22. data/lib/minting/money/arithmetics/operators.rb +10 -13
  23. data/lib/minting/money/clamp.rb +6 -6
  24. data/lib/minting/money/coercion.rb +1 -1
  25. data/lib/minting/money/comparable.rb +3 -3
  26. data/lib/minting/money/constructors.rb +3 -42
  27. data/lib/minting/money/conversion.rb +23 -18
  28. data/lib/minting/money/format/format.rb +100 -0
  29. data/lib/minting/money/format/formatter.rb +102 -0
  30. data/lib/minting/money/format/to_s.rb +34 -99
  31. data/lib/minting/money/format/validator.rb +34 -0
  32. data/lib/minting/money/money.rb +25 -13
  33. data/lib/minting/money/parse.rb +127 -0
  34. data/lib/minting/money/rounding.rb +27 -0
  35. data/lib/minting/version.rb +1 -1
  36. data/lib/minting.rb +17 -8
  37. metadata +9 -28
  38. data/doc/Mint/Currency.html +0 -1943
  39. data/doc/Mint/CurrencyRegistry.html +0 -511
  40. data/doc/Mint/Money.html +0 -4607
  41. data/doc/Mint/RangeStepPatch.html +0 -277
  42. data/doc/Mint/Registry.html +0 -863
  43. data/doc/Mint/Rounding.html +0 -506
  44. data/doc/Mint/UnknownCurrency.html +0 -136
  45. data/doc/Mint.html +0 -1004
  46. data/doc/Minting.html +0 -142
  47. data/doc/_index.html +0 -180
  48. data/doc/class_list.html +0 -54
  49. data/doc/css/common.css +0 -1
  50. data/doc/css/full_list.css +0 -206
  51. data/doc/css/style.css +0 -1089
  52. data/doc/file.README.html +0 -277
  53. data/doc/file_list.html +0 -59
  54. data/doc/frames.html +0 -22
  55. data/doc/index.html +0 -277
  56. data/doc/js/app.js +0 -801
  57. data/doc/js/full_list.js +0 -334
  58. data/doc/js/jquery.js +0 -4
  59. data/doc/method_list.html +0 -686
  60. data/doc/top-level-namespace.html +0 -151
  61. data/lib/minting/mint/aliases.rb +0 -16
  62. data/lib/minting/mint/parser/parser.rb +0 -97
  63. data/lib/minting/mint/parser/separators.rb +0 -41
  64. data/lib/minting/money/format/formatting.rb +0 -97
@@ -1,151 +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
-
92
- </p>
93
-
94
-
95
- <h2>
96
- Constant Summary
97
- <small><a href="#" class="constants_summary_toggle">collapse</a></small>
98
- </h2>
99
-
100
- <dl class="constants">
101
-
102
- <dt id="Money-constant" class="">Money =
103
- <div class="docstring">
104
- <div class="discussion">
105
- <p>Alias for <span class='object_link'><a href="Mint/Money.html" title="Mint::Money (class)">Mint::Money</a></span> — enables <code>Money.new(...)</code> shorthand.</p>
106
-
107
- </div>
108
- </div>
109
- <div class="tags">
110
-
111
-
112
- </div>
113
- </dt>
114
- <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>
115
-
116
- <dt id="Currency-constant" class="">Currency =
117
- <div class="docstring">
118
- <div class="discussion">
119
- <p>Alias for <span class='object_link'><a href="Mint/Currency.html" title="Mint::Currency (class)">Mint::Currency</a></span> — enables <code>Currency.new(...)</code> shorthand.</p>
120
-
121
- </div>
122
- </div>
123
- <div class="tags">
124
-
125
-
126
- </div>
127
- </dt>
128
- <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>
129
-
130
- </dl>
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
- </div>
142
-
143
- <div id="footer">
144
- Generated on Mon Jun 22 16:47:02 2026 by
145
- <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
146
- 0.9.44 (ruby-4.0.5).
147
- </div>
148
-
149
- </div>
150
- </body>
151
- </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,97 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mint
4
- # :nodoc:
5
- class Money
6
- private
7
-
8
- # Resolves the format template and amount based on the amount's sign
9
- # (negative_format/zero_format may override both the template and negate the value)
10
- # @private
11
- def resolve_format(format)
12
- negative_format = format[:negative]
13
- zero_format = format[:zero]
14
-
15
- if amount.negative? && negative_format
16
- [negative_format, -amount]
17
- elsif amount.zero? && zero_format
18
- [zero_format, amount]
19
- else
20
- [format[:positive] || '%<symbol>s%<amount>f', amount]
21
- end
22
- end
23
-
24
- # Validates that format hash contains only known keys.
25
- # @private
26
- def validate_format_hash(format)
27
- unknown = format.keys - %i[positive negative zero]
28
-
29
- raise ArgumentError, "Unknown format parameter(s): #{unknown.inspect}. " unless unknown.empty?
30
- end
31
-
32
- # Validates +decimal+ and +thousand+ separator arguments.
33
- # @private
34
- def validate_separators!(decimal:, thousand:)
35
- case decimal
36
- when nil # :noop
37
- when '' then raise ArgumentError, 'decimal must be a non-empty'
38
- when thousand then raise ArgumentError, "decimal and thousand cannot be identical: #{decimal.inspect}"
39
- when String # :noop
40
- else raise ArgumentError, "decimal must be a String, false, or nil, got #{decimal.inspect}"
41
- end
42
-
43
- case thousand
44
- when false, nil, String # :noop
45
- else raise ArgumentError, "thousand must be a String, false, or nil, got #{thousand.inspect}"
46
- end
47
- end
48
-
49
- def apply_thousand_separator(string, decimal:, thousand:)
50
- return string if !thousand || thousand.empty?
51
-
52
- # Apply thousands only to the integral portion, using the decimal as boundary
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.
59
- # @private
60
- #
61
- def format_amount(format, decimal:, thousand:)
62
- subunit = currency.subunit
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
- })
74
-
75
- # Substitute decimal first, while the dot is still unambiguous
76
- result.gsub!(/(?<=\d)\.(?=\d)/, decimal) if decimal != '.'
77
-
78
- return result if adjusted_amount.abs < 1000
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
95
- end
96
- end
97
- end