minting 1.9.7 → 2.1.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +298 -122
  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 +77 -94
  10. data/lib/minting/currency/registry.rb +134 -0
  11. data/lib/minting/currency/rounding.rb +47 -0
  12. data/lib/minting/data/crypto-currencies.yaml +126 -0
  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/registry.rb +9 -11
  18. data/lib/minting/mint/registry/symbols.rb +37 -30
  19. data/lib/minting/mint.rb +3 -2
  20. data/lib/minting/money/allocation/allocation.rb +2 -2
  21. data/lib/minting/money/allocation/split.rb +2 -2
  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 +22 -18
  28. data/lib/minting/money/format/format.rb +100 -0
  29. data/lib/minting/money/format/formatter.rb +110 -0
  30. data/lib/minting/money/format/to_s.rb +20 -102
  31. data/lib/minting/money/format/validator.rb +34 -0
  32. data/lib/minting/money/money.rb +25 -9
  33. data/lib/minting/money/parse.rb +127 -0
  34. data/lib/minting/money/rounding.rb +26 -0
  35. data/lib/minting/version.rb +1 -1
  36. data/lib/minting.rb +17 -8
  37. metadata +12 -31
  38. data/doc/Mint/Currency.html +0 -2032
  39. data/doc/Mint/Money.html +0 -5139
  40. data/doc/Mint/RangeStepPatch.html +0 -277
  41. data/doc/Mint/Registry.html +0 -863
  42. data/doc/Mint/Rounding.html +0 -506
  43. data/doc/Mint/UnknownCurrency.html +0 -138
  44. data/doc/Mint.html +0 -931
  45. data/doc/Minting.html +0 -142
  46. data/doc/Numeric.html +0 -479
  47. data/doc/String.html +0 -241
  48. data/doc/_index.html +0 -206
  49. data/doc/class_list.html +0 -54
  50. data/doc/css/common.css +0 -1
  51. data/doc/css/full_list.css +0 -206
  52. data/doc/css/style.css +0 -1089
  53. data/doc/file.README.html +0 -291
  54. data/doc/file_list.html +0 -59
  55. data/doc/frames.html +0 -22
  56. data/doc/index.html +0 -291
  57. data/doc/js/app.js +0 -801
  58. data/doc/js/full_list.js +0 -334
  59. data/doc/js/jquery.js +0 -4
  60. data/doc/method_list.html +0 -758
  61. data/doc/top-level-namespace.html +0 -135
  62. data/lib/minting/mint/aliases.rb +0 -16
  63. data/lib/minting/mint/parser/parser.rb +0 -97
  64. data/lib/minting/mint/parser/separators.rb +0 -41
  65. data/lib/minting/mint/rounding.rb +0 -65
  66. data/lib/minting/money/format/formatting.rb +0 -130
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ class Money
5
+ # Compiles and caches formatter lambdas for a fixed combination of format
6
+ # template, currency, and separator configuration.
7
+ #
8
+ # Use {.for} to obtain a cached instance; avoid +new+ directly unless you
9
+ # want an uncached formatter (typically only useful for testing).
10
+ #
11
+ # @api private
12
+ class Formatter
13
+ extend FormatterValidator
14
+
15
+ def self.cache = @cache ||= {}
16
+
17
+ # Returns a cached {Formatter} for the given configuration.
18
+ # @param format [Hash{Symbol => String}] per-sign templates
19
+ # @param decimal [String] decimal separator
20
+ # @param thousand [String, false] thousands delimiter (+false+ disables)
21
+ def self.for(format, decimal, thousand)
22
+ key = [format, decimal, thousand]
23
+ formatter = cache[key]
24
+ return formatter if formatter
25
+
26
+ validate_format!(format)
27
+ validate_separators!(decimal:, thousand:)
28
+
29
+ cache[key] = new(format, decimal, thousand)
30
+ end
31
+
32
+ def initialize(format, decimal, thousand)
33
+ @format = format
34
+ @decimal = decimal
35
+ @thousand = thousand
36
+ compile
37
+ end
38
+
39
+ SUBUNIT_PLACEHOLDER = "\uE000"
40
+ # Matches a digit followed by groups of exactly 3 digits that terminate
41
+ # at a non-digit or end-of-string. Used to insert thousand separators.
42
+ # e.g. "1234567" → "1" matches before "234" + "567" at string end.
43
+ THOUSAND_RE = /(\d)(?=(?:\d{3})+(?:[^\d]|$))/
44
+
45
+ def format(money)
46
+ amount = money.amount
47
+ currency = money.currency
48
+
49
+ templates = @has_placeholder ? @templates_by_subunit[currency.subunit] : @templates
50
+
51
+ template = templates[amount <=> 0] || templates[1]
52
+
53
+ display_amount = @has_negative_template && amount < 0 ? -amount : amount
54
+ integral = display_amount.to_i
55
+
56
+ result = Kernel.format(template,
57
+ currency: currency.code,
58
+ dsymbol: @needs_dsymbol && currency.dsymbol,
59
+ symbol: currency.symbol,
60
+ amount: display_amount,
61
+ integral: integral,
62
+ fractional: @needs_fractional ? money.fractional.abs : 0)
63
+ apply_separators(result, integral)
64
+ end
65
+
66
+ private
67
+
68
+ def apply_separators(result, integral)
69
+ unsigned_integral = integral.abs
70
+ int_str = unsigned_integral.to_s
71
+
72
+ result.sub!("#{int_str}.", "#{int_str}#{@decimal}") if @decimal != '.'
73
+
74
+ if @needs_thousand_substitution && unsigned_integral >= 1000
75
+ formatted_int = int_str.gsub(THOUSAND_RE, @thousand_replacement)
76
+ result.gsub!(int_str, formatted_int)
77
+ end
78
+ result
79
+ end
80
+
81
+ def compile
82
+ @templates = { -1 => @format[:negative], 0 => @format[:zero], 1 => @format[:positive] || Money::DEFAULT_FORMAT }
83
+ @templates.compact!
84
+ # Inject subunit precision into %<amount>f specs that lack an explicit
85
+ # precision. Matches "%<amount>f" or "%+10<amount>f" (with optional
86
+ # flags/width before the named ref) and appends a placeholder for the
87
+ # currency subunit digits — e.g. "%<amount>f" → "%<amount>\uE000f".
88
+ # The placeholder is later replaced with the actual subunit count at
89
+ # format time (e.g. "\uE000" → "2" for USD, "0" for JPY).
90
+ @templates.transform_values! { |f| f.gsub(/%<amount>(\s*\+?\d*)f/, "%<amount>\\1.#{SUBUNIT_PLACEHOLDER}f") }
91
+ @has_negative_template = @templates.key?(-1)
92
+
93
+ joined = @templates.values.join
94
+ @needs_fractional = joined.include?('%<fractional>')
95
+ @needs_dsymbol = joined.include?('%<dsymbol>')
96
+
97
+ @needs_thousand_substitution = @thousand && !@thousand.empty? &&
98
+ (joined.include?('%<amount>') || joined.include?('%<integral>'))
99
+ @thousand_replacement = "\\1#{@thousand}" if @needs_thousand_substitution
100
+
101
+ @has_placeholder = joined.include?(SUBUNIT_PLACEHOLDER)
102
+ return unless @has_placeholder
103
+
104
+ @templates_by_subunit = Hash.new do |h, subunit|
105
+ h[subunit] = @templates.transform_values { |f| f.gsub(SUBUNIT_PLACEHOLDER, subunit.to_s) }
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -7,119 +7,37 @@ module Mint
7
7
  # Uses `%<symbol>s` for the currency symbol and `%<amount>f` for the rounded amount.
8
8
  DEFAULT_FORMAT = '%<symbol>s%<amount>f'
9
9
 
10
- PRESETS = {
11
- amount: { format: '%<amount>f' },
12
- accounting: { format: { negative: '(%<symbol>s%<amount>f)' } },
13
- european: { format: '%<amount>f %<symbol>s', decimal: ',', thousand: '.' },
14
- currency: { format: '%<currency>s %<amount>f' }
15
- }.freeze
10
+ # Match a digit followed by groups of 3 digits until end of string — inserts thousand separators.
11
+ THOUSAND_RE = /(\d)(?=(\d{3})+\z)/
16
12
 
17
- # Formats money as a string with customizable format, thousand delimiter, and decimal
18
- #
19
- # @param preset [Symbol, nil] Named format preset, one of:
20
- # +:accounting+, +:european+, +:amount+, +:currency+.
21
- # When provided, expands to the preset's format options and merges
22
- # with any explicit keyword arguments (kwargs override the preset).
23
- # @param format [String, Hash, nil] Either a Format string with placeholders
24
- # (%<symbol>s, %<amount>f, %<currency>s, %<integral>d, %<fractional>d, %<dsymbol>s),
25
- # or a Hash with per-sign keys (:positive, :negative, :zero) each
26
- # holding a format string. A Hash is convenient for sign-aware formats
27
- # such as accounting parentheses:
28
- #
29
- # money.format(format: { negative: '(%<symbol>s%<amount>f)' })
30
- #
31
- # Missing keys fall back to the module default, so a Hash with only
32
- # :negative will still format positives sensibly. The valid keys are
33
- # :positive, :negative, :zero; anything else raises ArgumentError.
34
- # When +nil+, falls back to +Mint.locale_backend+ if set, otherwise
35
- # +"%<symbol>s%<amount>f"+.
36
- # @param thousand [String, false, nil] Thousands delimiter (e.g., ',' for 1,000).
37
- # When +nil+, falls back to +Mint.locale_backend+ if set, otherwise +","+.
38
- # @param decimal [String, nil] Decimal separator (e.g., '.' or ',').
39
- # When +nil+, falls back to +Mint.locale_backend+ if set, otherwise +"."+.
40
- # @return [String] Formatted money string
41
- #
42
- # @raise [ArgumentError] if +preset+ is not a recognised name, or if
43
- # +format+ is not a String or Hash, the Hash is empty, or the Hash
44
- # contains an unrecognised key.
45
- #
46
- # @example Basic formatting
47
- # money = Mint.money(1234.56, 'USD')
48
- # money.format #=> "$1,234.56"
49
- # money.format(thousand: '.', decimal: ',') #=> "$1.234,56"
50
- # money.format(decimal: ',', thousand: '') #=> "$1234,56"
13
+ # Returns a string representation of the money amount.
51
14
  #
52
- # @example Preset formats
53
- # loss = Mint.money(-1234.56, 'USD')
54
- # loss.format(:accounting) #=> "($1,234.56)"
55
- # money.format(:european) #=> "1.234,56 €"
56
- # money.format(:amount) #=> "1234.56"
57
- # money.format(:currency) #=> "USD 1234.56"
15
+ # When no {Mint.locale_backend} is configured, uses +currency.symbol+,
16
+ # comma thousands separators for amounts >= 1000, and decimal for the
17
+ # fractional part. When a locale backend is set, delegates to {#format}
18
+ # so locale-aware formatting takes effect.
58
19
  #
59
- # @example Custom formats
60
- # money.format(format: '%<amount>f') #=> "1234.56"
61
- # money.format(format: '%<currency>s %<amount>f') #=> "USD 1234.56"
62
- # money.format(format: '%<amount>f %<symbol>s') #=> "1234.56 $"
63
- # money.format(format: '%<symbol>s%<amount>+f') #=> "$+1234.56"
20
+ # Unlike {#format}, this method takes **no arguments** — use
21
+ # {#format} (alias {#to_fs}) for custom formatting.
64
22
  #
65
- # @example Integral & fractional parts
66
- # money.format(format: '%<integral>d.%<fractional>02d') #=> "1234.56"
67
- # price = Mint.money(0.99, 'USD')
68
- # price.format(format: '%<integral>d dollars and %<fractional>02d cents')
69
- # #=> "0 dollars and 99 cents"
23
+ # @return [String] formatted money string
70
24
  #
71
- # @example Per-sign Hash format (accounting parentheses)
72
- # loss = Mint.money(-1234.56, 'USD')
73
- # loss.format(format: { negative: '(%<symbol>s%<amount>f)' }) #=> "($1,234.56)"
74
- # Mint.money(0, 'BRL').format(format: { zero: '--' }) #=> "--"
75
- #
76
- # @example Padding and alignment
77
- # money.format(format: '%<amount>10.2f') #=> " 1234.56"
78
- # money.format(format: '%<symbol>s%<amount>010.2f') #=> "$0001234.56"
79
- #
80
- # @example Locale-aware formatting (with Mint.locale_backend set)
81
- # money.format # decimal and thousand come from locale_backend
82
- #
83
- def format(preset = nil, format: nil, decimal: nil, thousand: nil, width: nil)
84
- if preset
85
- config = PRESETS.fetch(preset) { raise ArgumentError, "Unknown format preset: #{preset.inspect}" }
86
- format ||= config[:format]
87
- decimal ||= config[:decimal]
88
- thousand ||= config[:thousand]
89
- width ||= config[:width]
90
- end
91
-
92
- validate_separators!(decimal:, thousand:)
93
-
94
- format, decimal, thousand = resolve_locale_for(format, decimal, thousand)
95
-
96
- case format
97
- when {}, '' then raise ArgumentError, 'format must not be empty'
98
- when Hash then validate_format_hash(format)
99
- when String then format = { positive: format }
100
- else raise ArgumentError, 'Invalid format. Only String or Hash are accepted'
101
- end
102
-
103
- formatted = format_amount(format, decimal:, thousand:)
104
-
105
- width ? formatted.rjust(width) : formatted
106
- end
107
-
108
- THOUSAND_RE = /(\d)(?=(\d{3})+\z)/
109
-
25
+ # @example
26
+ # Money.from(1234.56, 'USD').to_s #=> "$1,234.56"
27
+ # Money.from(0.99, 'USD').to_s #=> "$0.99"
28
+ # Money.from(100, 'JPY').to_s #=> "¥100"
110
29
  def to_s
111
30
  return format unless Mint.locale_backend.nil?
112
31
 
113
- subunit = currency.subunit
114
- integral = to_i.to_s
115
- integral.gsub!(THOUSAND_RE, '\1,') if amount.abs >= 1000
32
+ subunit = currency.subunit
33
+ major = integral.to_s
34
+ major.gsub!(THOUSAND_RE, '\1,') if amount.abs >= 1000
116
35
  if subunit > 0
117
- "#{currency.symbol}#{integral}.#{fractional.to_s.rjust(subunit, '0')}"
36
+ minor = fractional.abs.to_s.rjust(subunit, '0')
37
+ "#{currency.symbol}#{major}.#{minor}"
118
38
  else
119
- "#{currency.symbol}#{integral}"
39
+ "#{currency.symbol}#{major}"
120
40
  end
121
41
  end
122
-
123
- alias to_fs :format
124
42
  end
125
43
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ class Money
5
+ # Shared validation for format templates and separator configuration.
6
+ #
7
+ # @api private
8
+ module FormatterValidator
9
+ def validate_format!(format)
10
+ raise ArgumentError, 'template must not be empty' if format == {}
11
+
12
+ unknown = format.keys - %i[positive negative zero]
13
+ raise ArgumentError, "Unknown format parameter(s): #{unknown.inspect}. " unless unknown.empty?
14
+ end
15
+
16
+ def validate_separators!(decimal:, thousand:)
17
+ case decimal
18
+ when '' then raise ArgumentError, "decimal separator must be a non-empty - #{decimal.inspect}"
19
+ when /\d/ then raise ArgumentError, "decimal separator cannot be a numeral - #{decimal.inspect}"
20
+ when thousand then raise ArgumentError, "decimal and thousand cannot be identical - #{decimal.inspect}"
21
+ when String # :noop
22
+ else raise ArgumentError, "decimal must be a String, false, or nil, got #{decimal.inspect}"
23
+ end
24
+
25
+ case thousand
26
+ when false, nil # :noop
27
+ when /\d/ then raise ArgumentError, "decimal separator cannot be a numeral - #{decimal.inspect}"
28
+ when String # :noop
29
+ else raise ArgumentError, "thousand must be a String, false, or nil, got #{thousand.inspect}"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -8,9 +8,13 @@ require_relative 'clamp'
8
8
  require_relative 'coercion'
9
9
  require_relative 'comparable'
10
10
  require_relative 'constructors'
11
+ require_relative 'parse'
11
12
  require_relative 'conversion'
12
- require_relative 'format/formatting'
13
+ require_relative 'format/validator'
14
+ require_relative 'format/formatter'
13
15
  require_relative 'format/to_s'
16
+ require_relative 'format/format'
17
+ require_relative 'rounding'
14
18
 
15
19
  module Mint
16
20
  # Represents a monetary value paired with a currency.
@@ -19,11 +23,14 @@ module Mint
19
23
  class Money
20
24
  attr_reader :amount, :currency
21
25
 
26
+ # Money::Currency is the canonical way to access Currency class
27
+ Currency = Mint::Currency
28
+
22
29
  # Returns the ISO 3-letter currency code string.
23
30
  #
24
31
  # @return [String] the ISO currency code (e.g., "USD", "EUR", "BRL")
25
32
  # @example
26
- # Mint.money(100, 'USD').currency_code #=> "USD"
33
+ # Money.from(100, 'USD').currency_code #=> "USD"
27
34
  def currency_code = currency.code
28
35
 
29
36
  # Returns the monetary amount expressed in the currency's smallest unit (fractional units).
@@ -31,17 +38,26 @@ module Mint
31
38
  #
32
39
  # @return [Integer] the amount in fractional units
33
40
  # @example
34
- # Mint.money(1234.56, 'USD').subunits #=> 123456
35
- # Mint.money(1000, 'JPY').subunits #=> 1000
36
- # Mint.money(123.456, 'IQD').subunits #=> 123456
41
+ # Money.from(1234.56, 'USD').subunits #=> 123456
42
+ # Money.from(1000, 'JPY').subunits #=> 1000
43
+ # Money.from(123.456, 'IQD').subunits #=> 123456
37
44
  def subunits = (amount * currency.fractional_multiplier).to_i
38
45
 
46
+ # Returns the whole-unit (integral) part of the amount.
47
+ # @example
48
+ # Money.from(1234.56, 'USD').integral #=> 1234
49
+ # Money.from(1000, 'JPY').integral #=> 1000
50
+ # Money.from(-9.99, 'USD').integral #=> -9
51
+ def integral = amount.to_i
52
+
53
+ alias to_i integral
54
+
39
55
  # Returns the fractional part of the amount.
40
56
  # @example
41
- # Mint.money(1234.56, 'USD').fractional #=> 56
42
- # Mint.money(1000, 'JPY').fractional #=> 0
43
- # Mint.money(123.456, 'IQD').fractional #=> 456
44
- def fractional = ((amount.abs % 1) * currency.fractional_multiplier).to_i
57
+ # Money.from(1234.56, 'USD').fractional #=> 56
58
+ # Money.from(1000, 'JPY').fractional #=> 0
59
+ # Money.from(123.456, 'IQD').fractional #=> 456
60
+ def fractional = ((amount - amount.to_i) * currency.fractional_multiplier).to_i
45
61
 
46
62
  # Generates a stable hash key for Money instances.
47
63
  #
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # :nodoc:
5
+ class Money
6
+ # Parses a human-readable money string into a {Money} object.
7
+ #
8
+ # Returns +nil+ when the input is invalid or currency cannot be determined.
9
+ #
10
+ # @param input [String] Amount input, optionally including a currency symbol or code
11
+ # @param currency [String, Symbol, Currency, nil] ISO code when not present in +input+
12
+ # @return [Money, nil]
13
+ #
14
+ # @example With explicit currency
15
+ # Money.parse('19.99', 'USD') #=> [USD 19.99]
16
+ # Money.parse('garbage', 'USD') #=> nil
17
+ #
18
+ # @example With symbol or code in the string
19
+ # Money.parse('$19.99') #=> [USD 19.99]
20
+ # Money.parse('USD 1,234.56') #=> [USD 1234.56]
21
+ def self.parse(input, currency = nil)
22
+ return nil unless input.is_a?(String)
23
+
24
+ input = input.strip
25
+ return nil if input.empty?
26
+
27
+ currency = parse_currency(input, currency)
28
+ return nil unless currency
29
+
30
+ amount = parse_amount(input)
31
+ return nil unless amount
32
+
33
+ amount = currency.normalize_amount(amount)
34
+ new(amount, currency)
35
+ end
36
+
37
+ # Like {.parse} but raises on failure.
38
+ #
39
+ # @param input [String] Amount input, optionally including a currency symbol or code
40
+ # @param currency [String, Symbol, Currency, nil] ISO code when not present in +input+
41
+ # @return [Money]
42
+ # @raise [ArgumentError] when +input+ is invalid or currency cannot be determined
43
+ #
44
+ # @example
45
+ # Money.parse!('19.99', 'USD') #=> [USD 19.99]
46
+ # Money.parse!('garbage', 'USD') #=> ArgumentError
47
+ def self.parse!(input, currency = nil)
48
+ raise ArgumentError, 'input must be a String' unless input.is_a?(String)
49
+
50
+ input = input.strip
51
+ raise ArgumentError, 'input cannot be empty' if input.empty?
52
+
53
+ currency = parse_currency(input, currency)
54
+ raise ArgumentError, "Currency [#{currency}] not found" unless currency
55
+
56
+ amount = parse_amount(input)
57
+ raise ArgumentError, "Could not parse [#{input}]" unless amount
58
+
59
+ amount = currency.normalize_amount(amount)
60
+ new(amount, currency)
61
+ end
62
+
63
+ class << self
64
+ private
65
+
66
+ # Extracts a numeric value from input that should only contain an amount.
67
+ def parse_amount(input)
68
+ accounting_negative = input.start_with?('(') && input.end_with?(')')
69
+
70
+ numeric_input = input.gsub(/[^\d.,-]/, '')
71
+ numeric = parse_separators(numeric_input)
72
+ return nil unless numeric
73
+
74
+ amount = Rational(numeric)
75
+ accounting_negative ? -amount : amount
76
+ end
77
+
78
+ # Extracts currency from a string by matching ISO code or symbol.
79
+ #
80
+ # Scans all uppercase words and returns the first registered code, falling
81
+ # back to symbol matching. This correctly handles inputs like
82
+ # "MAX 10.00 USD" where the first uppercase word isn't a currency code.
83
+ def parse_currency(input, currency = nil)
84
+ input.scan(/\b([A-Z_]+)\b/) do |(code)|
85
+ found = Currency.for_code(code)
86
+ return found if found
87
+ end
88
+
89
+ found = Registry.detect_currency(input)
90
+ return found if found
91
+
92
+ Currency.resolve(currency)
93
+ end
94
+
95
+ # Converts locale-specific decimal/thousand separators into a plain decimal string.
96
+ def parse_separators(numeric)
97
+ return nil unless numeric.match?(/\d/)
98
+
99
+ case classify_separators(numeric)
100
+ when :decimal_period then numeric
101
+ when :decimal_comma then numeric.tr(',', '.')
102
+ when :thousands_comma then numeric.delete(',')
103
+ when :thousands then numeric.delete('.,')
104
+ when :invalid then nil
105
+ when :mixed
106
+ if numeric.rindex(',') > numeric.rindex('.')
107
+ numeric.delete('.').tr(',', '.')
108
+ else
109
+ numeric.delete(',')
110
+ end
111
+ end
112
+ end
113
+
114
+ # Classifies the separator pattern in a numeric string.
115
+ def classify_separators(numeric)
116
+ case [numeric.count('.'), numeric.count(',')]
117
+ in [0, 1] if numeric[-4] == ',' then :thousands_comma
118
+ in [0, 1] then :decimal_comma
119
+ in [0, 0] | [1, 0] then :decimal_period
120
+ in [p, c] if p > 1 && c > 1 then :invalid
121
+ in [p, c] if p > 0 && c > 0 then :mixed
122
+ else :thousands
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # :nodoc:
5
+ class Money
6
+ # Executes a block with a specific rounding mode applied to all money
7
+ # construction, parsing, change, allocation, and split operations.
8
+ #
9
+ # Restores the previous mode (or default) when the block exits, even on
10
+ # exception.
11
+ #
12
+ # Rounding-mode support is activated on first call. Once activated,
13
+ # +Currency#normalize_amount+ dispatches through +Currency.rounding_mode+,
14
+ # adding ~10–35&ns of overhead to every money creation or mutation.
15
+ # When rounding modes are never used (the common case), the fast path
16
+ # incurs zero overhead.
17
+ #
18
+ # @param mode [Symbol] one of: +:up+, +:down+, +:even+
19
+ # @yield block to execute with the rounding mode active
20
+ # @raise [ArgumentError] if +mode+ is not a recognised rounding mode
21
+ def self.with_rounding(mode, &)
22
+ Currency.activate_custom_rounding!
23
+ Currency.rounding_mode(mode, &)
24
+ end
25
+ end
26
+ end
@@ -3,5 +3,5 @@
3
3
  # Root namespace for the Minting library.
4
4
  module Minting
5
5
  # Current version of the Minting gem.
6
- VERSION = '1.9.7'
6
+ VERSION = '2.1.0'
7
7
  end
data/lib/minting.rb CHANGED
@@ -3,12 +3,21 @@
3
3
  require 'minting/mint'
4
4
  require 'minting/version'
5
5
 
6
- # By default, expose Mint::Money as the top-level Money constant for
7
- # convenience. If Money is already defined (e.g. by the `money` gem), warn
8
- # and skip so both libraries can coexist in the same process without
9
- # corrupting either class.
10
- if defined?(Money) && Money != Mint::Money
11
- warn "minting: top-level Money is already defined (#{Money}); skipping auto-bind. Use Mint::Money."
12
- else
13
- Money = Mint::Money unless defined?(Money)
6
+ # @!parse
7
+ # # Top-level constant auto-bound to {Mint::Money} for convenience.
8
+ # Money = Mint::Money
9
+
10
+ # Top-level constant auto-bound to {Mint::Money} for convenience.
11
+ #
12
+ # Set at require-time via `require 'minting'`. If {::Money} is already
13
+ # defined (e.g. by the `money` gem), a warning is emitted and the existing
14
+ # constant is preserved — use {Mint::Money} explicitly in that case.
15
+ #
16
+ # @see Mint::Money
17
+ # @note This is a **breaking change from v1.x** where both +Money+ and
18
+ # +Currency+ required explicit opt-in via +Mint.use_top_level_constants!+.
19
+ Money = Mint::Money unless defined?(Money)
20
+
21
+ if Money != Mint::Money
22
+ warn "minting: top-level Money was already defined (#{Money}); skipping auto-bind! Use Mint::Money."
14
23
  end
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.7
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gilson Ferraz
@@ -36,17 +36,6 @@ files:
36
36
  - bin/check-currencies
37
37
  - bin/console
38
38
  - bin/setup
39
- - doc/Mint.html
40
- - doc/Mint/Currency.html
41
- - doc/Mint/Money.html
42
- - doc/Mint/RangeStepPatch.html
43
- - doc/Mint/Registry.html
44
- - doc/Mint/Rounding.html
45
- - doc/Mint/UnknownCurrency.html
46
- - doc/Minting.html
47
- - doc/Numeric.html
48
- - doc/String.html
49
- - doc/_index.html
50
39
  - doc/agents/api_review-2026-06-15.md
51
40
  - doc/agents/copilot-instructions.md
52
41
  - doc/agents/expired/AGENTS.md
@@ -54,36 +43,24 @@ files:
54
43
  - doc/agents/expired/gemini_gem_evaluation.md
55
44
  - doc/agents/expired/recommendations.md
56
45
  - doc/agents/expired/rubocop-issues.md
57
- - doc/class_list.html
58
- - doc/css/common.css
59
- - doc/css/full_list.css
60
- - doc/css/style.css
61
- - doc/file.README.html
62
- - doc/file_list.html
63
- - doc/frames.html
64
- - doc/index.html
65
- - doc/js/app.js
66
- - doc/js/full_list.js
67
- - doc/js/jquery.js
68
- - doc/method_list.html
69
- - doc/top-level-namespace.html
70
46
  - lib/minting.rb
47
+ - lib/minting/aliases.rb
71
48
  - lib/minting/currency/currency.rb
49
+ - lib/minting/currency/registry.rb
50
+ - lib/minting/currency/rounding.rb
51
+ - lib/minting/data/crypto-currencies.yaml
72
52
  - lib/minting/data/world-currencies.yaml
73
53
  - lib/minting/mint.rb
74
- - lib/minting/mint/aliases.rb
75
54
  - lib/minting/mint/dsl/numeric.rb
76
55
  - lib/minting/mint/dsl/range.rb
77
56
  - lib/minting/mint/dsl/string.rb
78
57
  - lib/minting/mint/i18n.rb
79
58
  - lib/minting/mint/mint.rb
80
- - lib/minting/mint/parser/parser.rb
81
- - lib/minting/mint/parser/separators.rb
59
+ - lib/minting/mint/registry/crypto.rb
82
60
  - lib/minting/mint/registry/registration.rb
83
61
  - lib/minting/mint/registry/registry.rb
84
62
  - lib/minting/mint/registry/symbols.rb
85
63
  - lib/minting/mint/registry/zeros.rb
86
- - lib/minting/mint/rounding.rb
87
64
  - lib/minting/money/allocation/allocation.rb
88
65
  - lib/minting/money/allocation/split.rb
89
66
  - lib/minting/money/arithmetics/methods.rb
@@ -93,9 +70,13 @@ files:
93
70
  - lib/minting/money/comparable.rb
94
71
  - lib/minting/money/constructors.rb
95
72
  - lib/minting/money/conversion.rb
96
- - lib/minting/money/format/formatting.rb
73
+ - lib/minting/money/format/format.rb
74
+ - lib/minting/money/format/formatter.rb
97
75
  - lib/minting/money/format/to_s.rb
76
+ - lib/minting/money/format/validator.rb
98
77
  - lib/minting/money/money.rb
78
+ - lib/minting/money/parse.rb
79
+ - lib/minting/money/rounding.rb
99
80
  - lib/minting/version.rb
100
81
  - minting.gemspec
101
82
  homepage: https://github.com/gferraz/minting
@@ -123,7 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
104
  - !ruby/object:Gem::Version
124
105
  version: '0'
125
106
  requirements: []
126
- rubygems_version: 4.0.10
107
+ rubygems_version: 4.0.17
127
108
  specification_version: 4
128
109
  summary: Library to manipulate currency values
129
110
  test_files: []