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
@@ -0,0 +1,102 @@
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
+ template = @templates[amount <=> 0] || @positive_template
50
+ display_amount = template == @negative_template ? -amount : amount
51
+ integral = display_amount.to_i
52
+
53
+ template = template.gsub(SUBUNIT_PLACEHOLDER, currency.subunit.to_s) if @has_placeholder
54
+ result = Kernel.format(template,
55
+ currency: currency.code,
56
+ dsymbol: @needs_dsymbol && currency.dsymbol,
57
+ symbol: currency.symbol,
58
+ amount: display_amount,
59
+ integral: integral,
60
+ fractional: @needs_fractional ? money.fractional.abs : 0)
61
+ apply_separators(result, integral)
62
+ end
63
+
64
+ private
65
+
66
+ def apply_separators(result, integral)
67
+ unsigned_integral = integral.abs
68
+ int_str = unsigned_integral.to_s
69
+
70
+ result.sub!("#{int_str}.", "#{int_str}#{@decimal}") if @decimal != '.'
71
+
72
+ if @needs_thousand_substitution && unsigned_integral >= 1000
73
+ formatted_int = int_str.gsub(THOUSAND_RE, @thousand_replacement)
74
+ result.gsub!(int_str, formatted_int)
75
+ end
76
+ result
77
+ end
78
+
79
+ def compile
80
+ @templates = { -1 => @format[:negative], 0 => @format[:zero], 1 => @format[:positive] || Money::DEFAULT_FORMAT }
81
+ @templates.compact!
82
+ # Inject subunit precision into %<amount>f specs that lack an explicit
83
+ # precision. Matches "%<amount>f" or "%+10<amount>f" (with optional
84
+ # flags/width before the named ref) and appends a placeholder for the
85
+ # currency subunit digits — e.g. "%<amount>f" → "%<amount>\uE000f".
86
+ # The placeholder is later replaced with the actual subunit count at
87
+ # format time (e.g. "\uE000" → "2" for USD, "0" for JPY).
88
+ @templates.transform_values! { |f| f.gsub(/%<amount>(\s*\+?\d*)f/, "%<amount>\\1.#{SUBUNIT_PLACEHOLDER}f") }
89
+ @negative_template = @templates[-1]
90
+ @positive_template = @templates[1]
91
+
92
+ joined_template = @templates.values.join
93
+ @has_placeholder = joined_template.include?(SUBUNIT_PLACEHOLDER)
94
+ @needs_fractional = joined_template.include?('%<fractional>')
95
+ @needs_dsymbol = joined_template.include?('%<dsymbol>')
96
+ @needs_thousand_substitution = @thousand && !@thousand.empty? && (joined_template.include?('%<amount>') ||
97
+ joined_template.include?('%<integral>'))
98
+ @thousand_replacement = "\\1#{@thousand}" if @needs_thousand_substitution
99
+ end
100
+ end
101
+ end
102
+ end
@@ -3,106 +3,41 @@
3
3
  module Mint
4
4
  # :nodoc:
5
5
  class Money
6
- PRESETS = {
7
- amount: { format: '%<amount>f' },
8
- accounting: { format: { negative: '(%<symbol>s%<amount>f)' } },
9
- european: { format: '%<amount>f %<symbol>s', decimal: ',', thousand: '.' },
10
- currency: { format: '%<currency>s %<amount>f' }
11
- }.freeze
12
-
13
- # Formats money as a string with customizable format, thousand delimiter, and decimal
14
- #
15
- # @param preset [Symbol, nil] Named format preset, one of:
16
- # +:accounting+, +:european+, +:amount+, +:currency+.
17
- # When provided, expands to the preset's format options and merges
18
- # with any explicit keyword arguments (kwargs override the preset).
19
- # @param format [String, Hash, nil] Either a Format string with placeholders
20
- # (%<symbol>s, %<amount>f, %<currency>s, %<integral>d, %<fractional>d),
21
- # or a Hash with per-sign keys (:positive, :negative, :zero) each
22
- # holding a format string. A Hash is convenient for sign-aware formats
23
- # such as accounting parentheses:
24
- #
25
- # money.format(format: { negative: '(%<symbol>s%<amount>f)' })
26
- #
27
- # Missing keys fall back to the module default, so a Hash with only
28
- # :negative will still format positives sensibly. The valid keys are
29
- # :positive, :negative, :zero; anything else raises ArgumentError.
30
- # When +nil+, falls back to +Mint.locale_backend+ if set, otherwise
31
- # +"%<symbol>s%<amount>f"+.
32
- # @param thousand [String, false, nil] Thousands delimiter (e.g., ',' for 1,000).
33
- # When +nil+, falls back to +Mint.locale_backend+ if set, otherwise +","+.
34
- # @param decimal [String, nil] Decimal separator (e.g., '.' or ',').
35
- # When +nil+, falls back to +Mint.locale_backend+ if set, otherwise +"."+.
36
- # @return [String] Formatted money string
37
- #
38
- # @raise [ArgumentError] if +preset+ is not a recognised name, or if
39
- # +format+ is not a String or Hash, the Hash is empty, or the Hash
40
- # contains an unrecognised key.
41
- #
42
- # @example Basic formatting
43
- # money = Mint.money(1234.56, 'USD')
44
- # money.format #=> "$1,234.56"
45
- # money.format(thousand: '.', decimal: ',') #=> "$1.234,56"
46
- # money.format(decimal: ',', thousand: '') #=> "$1234,56"
47
- #
48
- # @example Preset formats
49
- # loss = Mint.money(-1234.56, 'USD')
50
- # loss.format(:accounting) #=> "($1,234.56)"
51
- # money.format(:european) #=> "1.234,56 €"
52
- # money.format(:amount) #=> "1234.56"
53
- # money.format(:currency) #=> "USD 1234.56"
54
- #
55
- # @example Custom formats
56
- # money.format(format: '%<amount>f') #=> "1234.56"
57
- # money.format(format: '%<currency>s %<amount>f') #=> "USD 1234.56"
58
- # money.format(format: '%<amount>f %<symbol>s') #=> "1234.56 $"
59
- # money.format(format: '%<symbol>s%<amount>+f') #=> "$+1234.56"
60
- #
61
- # @example Integral & fractional parts
62
- # money.format(format: '%<integral>d.%<fractional>02d') #=> "1234.56"
63
- # price = Mint.money(0.99, 'USD')
64
- # price.format(format: '%<integral>d dollars and %<fractional>02d cents')
65
- # #=> "0 dollars and 99 cents"
66
- #
67
- # @example Per-sign Hash format (accounting parentheses)
68
- # loss = Mint.money(-1234.56, 'USD')
69
- # loss.format(format: { negative: '(%<symbol>s%<amount>f)' }) #=> "($1,234.56)"
70
- # Mint.money(0, 'BRL').format(format: { zero: '--' }) #=> "--"
71
- #
72
- # @example Padding and alignment
73
- # money.format(format: '%<amount>10.2f') #=> " 1234.56"
74
- # money.format(format: '%<symbol>s%<amount>010.2f') #=> "$0001234.56"
75
- #
76
- # @example Locale-aware formatting (with Mint.locale_backend set)
77
- # money.format # decimal and thousand come from locale_backend
78
- #
79
- def format(preset = nil, format: nil, decimal: nil, thousand: nil, width: nil)
80
- if preset
81
- config = PRESETS.fetch(preset) { raise ArgumentError, "Unknown format preset: #{preset.inspect}" }
82
- format ||= config[:format]
83
- decimal ||= config[:decimal]
84
- thousand ||= config[:thousand]
85
- width ||= config[:width]
86
- end
87
-
88
- validate_separators!(decimal:, thousand:)
89
-
90
- format, decimal, thousand = resolve_locale_for(format, decimal, thousand)
91
-
92
- case format
93
- when {}, '' then raise ArgumentError, 'format must not be empty'
94
- when Hash then validate_format_hash(format)
95
- when String then format = { positive: format }
96
- else raise ArgumentError, 'Invalid format. Only String or Hash are accepted'
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
+
10
+ # Match a digit followed by groups of 3 digits until end of string — inserts thousand separators.
11
+ THOUSAND_RE = /(\d)(?=(\d{3})+\z)/
12
+
13
+ # Returns a string representation of the money amount.
14
+ #
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.
19
+ #
20
+ # Unlike {#format}, this method takes **no arguments** — use
21
+ # {#format} (alias {#to_fs}) for custom formatting.
22
+ #
23
+ # @return [String] formatted money string
24
+ #
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"
29
+ def to_s
30
+ return format unless Mint.locale_backend.nil?
31
+
32
+ subunit = currency.subunit
33
+ major = integral.to_s
34
+ major.gsub!(THOUSAND_RE, '\1,') if amount.abs >= 1000
35
+ if subunit > 0
36
+ minor = fractional.abs.to_s.rjust(subunit, '0')
37
+ "#{currency.symbol}#{major}.#{minor}"
38
+ else
39
+ "#{currency.symbol}#{major}"
97
40
  end
98
-
99
- formatted = format_amount(format, decimal: decimal, thousand: thousand)
100
-
101
- width ? formatted.rjust(width) : formatted
102
41
  end
103
-
104
- def to_s = format
105
-
106
- alias to_fs :format
107
42
  end
108
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,26 +8,29 @@ 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.
17
21
  # Money objects are immutable and support arithmetic, comparison,
18
22
  # formatting, allocation, and parsing operations.
19
23
  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
24
  attr_reader :amount, :currency
25
25
 
26
+ # Money::Currency is the canonical way to access Currency class
27
+ Currency = Mint::Currency
28
+
26
29
  # Returns the ISO 3-letter currency code string.
27
30
  #
28
31
  # @return [String] the ISO currency code (e.g., "USD", "EUR", "BRL")
29
32
  # @example
30
- # Mint.money(100, 'USD').currency_code #=> "USD"
33
+ # Money.from(100, 'USD').currency_code #=> "USD"
31
34
  def currency_code = currency.code
32
35
 
33
36
  # Returns the monetary amount expressed in the currency's smallest unit (fractional units).
@@ -35,17 +38,26 @@ module Mint
35
38
  #
36
39
  # @return [Integer] the amount in fractional units
37
40
  # @example
38
- # Mint.money(1234.56, 'USD').subunits #=> 123456
39
- # Mint.money(1000, 'JPY').subunits #=> 1000
40
- # 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
41
44
  def subunits = (amount * currency.fractional_multiplier).to_i
42
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
+
43
55
  # Returns the fractional part of the amount.
44
56
  # @example
45
- # Mint.money(1234.56, 'USD').fractional #=> 56
46
- # Mint.money(1000, 'JPY').fractional #=> 0
47
- # Mint.money(123.456, 'IQD').fractional #=> 456
48
- 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
49
61
 
50
62
  # Generates a stable hash key for Money instances.
51
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,27 @@
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 loaded lazily on first call. Once loaded,
13
+ # +Currency#normalize_amount+ is patched to dispatch through the
14
+ # rounding module, adding ~10–35&ns of overhead to every money creation
15
+ # or mutation. When rounding modes are never used (the common case),
16
+ # the fast path incurs zero overhead.
17
+ #
18
+ # @param mode [Symbol] one of: +:half_up+, +:half_down+, +:floor+,
19
+ # +:ceil+, +:truncate+, +:down+
20
+ # @yield block to execute with the rounding mode active
21
+ # @raise [ArgumentError] if +mode+ is not a recognised rounding mode
22
+ def self.with_rounding(mode, &)
23
+ require_relative '../mint/rounding' unless defined?(Mint::Rounding)
24
+ Mint::Rounding.with_mode(mode, &)
25
+ end
26
+ end
27
+ 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.6'
6
+ VERSION = '2.0.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.6
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gilson Ferraz
@@ -36,16 +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/CurrencyRegistry.html
42
- - doc/Mint/Money.html
43
- - doc/Mint/RangeStepPatch.html
44
- - doc/Mint/Registry.html
45
- - doc/Mint/Rounding.html
46
- - doc/Mint/UnknownCurrency.html
47
- - doc/Minting.html
48
- - doc/_index.html
49
39
  - doc/agents/api_review-2026-06-15.md
50
40
  - doc/agents/copilot-instructions.md
51
41
  - doc/agents/expired/AGENTS.md
@@ -53,31 +43,18 @@ files:
53
43
  - doc/agents/expired/gemini_gem_evaluation.md
54
44
  - doc/agents/expired/recommendations.md
55
45
  - doc/agents/expired/rubocop-issues.md
56
- - doc/class_list.html
57
- - doc/css/common.css
58
- - doc/css/full_list.css
59
- - doc/css/style.css
60
- - doc/file.README.html
61
- - doc/file_list.html
62
- - doc/frames.html
63
- - doc/index.html
64
- - doc/js/app.js
65
- - doc/js/full_list.js
66
- - doc/js/jquery.js
67
- - doc/method_list.html
68
- - doc/top-level-namespace.html
69
46
  - lib/minting.rb
47
+ - lib/minting/aliases.rb
70
48
  - lib/minting/currency/currency.rb
49
+ - lib/minting/data/crypto-currencies.yaml
71
50
  - lib/minting/data/world-currencies.yaml
72
51
  - lib/minting/mint.rb
73
- - lib/minting/mint/aliases.rb
74
52
  - lib/minting/mint/dsl/numeric.rb
75
53
  - lib/minting/mint/dsl/range.rb
76
54
  - lib/minting/mint/dsl/string.rb
77
55
  - lib/minting/mint/i18n.rb
78
56
  - lib/minting/mint/mint.rb
79
- - lib/minting/mint/parser/parser.rb
80
- - lib/minting/mint/parser/separators.rb
57
+ - lib/minting/mint/registry/crypto.rb
81
58
  - lib/minting/mint/registry/registration.rb
82
59
  - lib/minting/mint/registry/registry.rb
83
60
  - lib/minting/mint/registry/symbols.rb
@@ -92,9 +69,13 @@ files:
92
69
  - lib/minting/money/comparable.rb
93
70
  - lib/minting/money/constructors.rb
94
71
  - lib/minting/money/conversion.rb
95
- - lib/minting/money/format/formatting.rb
72
+ - lib/minting/money/format/format.rb
73
+ - lib/minting/money/format/formatter.rb
96
74
  - lib/minting/money/format/to_s.rb
75
+ - lib/minting/money/format/validator.rb
97
76
  - lib/minting/money/money.rb
77
+ - lib/minting/money/parse.rb
78
+ - lib/minting/money/rounding.rb
98
79
  - lib/minting/version.rb
99
80
  - minting.gemspec
100
81
  homepage: https://github.com/gferraz/minting