minting 1.7.0 → 1.7.3

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +80 -112
  3. data/Rakefile +13 -1
  4. data/bin/bench_check +46 -0
  5. data/doc/Mint/Currency.html +446 -46
  6. data/doc/Mint/CurrencyRegistry.html +7 -7
  7. data/doc/Mint/Money.html +203 -177
  8. data/doc/Mint/RangeStepPatch.html +277 -0
  9. data/doc/Mint/Registry.html +842 -0
  10. data/doc/Mint/UnknownCurrency.html +2 -2
  11. data/doc/Mint.html +385 -66
  12. data/doc/Minting.html +3 -3
  13. data/doc/_index.html +24 -9
  14. data/doc/agents/api_review-2026-06-15.md +342 -0
  15. data/doc/class_list.html +1 -1
  16. data/doc/file.README.html +97 -89
  17. data/doc/index.html +97 -89
  18. data/doc/method_list.html +97 -25
  19. data/doc/top-level-namespace.html +13 -5
  20. data/lib/minting/currency/currency.rb +75 -0
  21. data/lib/minting/mint/aliases.rb +3 -0
  22. data/lib/minting/mint/dsl/{refinements.rb → numeric.rb} +6 -5
  23. data/lib/minting/mint/dsl/range.rb +31 -18
  24. data/lib/minting/mint/dsl/string.rb +12 -0
  25. data/lib/minting/mint/dsl/top_level.rb +3 -0
  26. data/lib/minting/mint/locale_backend.rb +29 -0
  27. data/lib/minting/mint/mint.rb +28 -12
  28. data/lib/minting/mint/parser/parser.rb +62 -0
  29. data/lib/minting/mint/parser/separators.rb +39 -0
  30. data/lib/minting/mint/registry/registration.rb +33 -0
  31. data/lib/minting/mint/registry/registry.rb +38 -0
  32. data/lib/minting/mint/registry/symbols.rb +49 -0
  33. data/lib/minting/mint/registry/zeros.rb +18 -0
  34. data/lib/minting/mint.rb +13 -16
  35. data/lib/minting/money/allocation/allocation.rb +25 -0
  36. data/lib/minting/money/{allocation.rb → allocation/split.rb} +1 -19
  37. data/lib/minting/money/arithmetics/methods.rb +27 -0
  38. data/lib/minting/money/{arithmetics.rb → arithmetics/operators.rb} +0 -21
  39. data/lib/minting/money/clamp.rb +66 -0
  40. data/lib/minting/money/coercion.rb +10 -0
  41. data/lib/minting/money/comparable.rb +6 -0
  42. data/lib/minting/money/constructors.rb +14 -9
  43. data/lib/minting/money/format/formatting.rb +60 -0
  44. data/lib/minting/money/{formatting.rb → format/to_s.rb} +13 -36
  45. data/lib/minting/money/money.rb +12 -58
  46. data/lib/minting/version.rb +1 -1
  47. metadata +29 -19
  48. data/lib/minting/mint/currency/currency.rb +0 -36
  49. data/lib/minting/mint/currency/currency_registry.rb +0 -67
  50. data/lib/minting/mint/currency/world_currencies.rb +0 -16
  51. data/lib/minting/mint/parser.rb +0 -85
  52. /data/doc/agents/{AGENTS.md → expired/AGENTS.md} +0 -0
  53. /data/doc/agents/{copilot-instructions.md → expired/copilot-instructions.md} +0 -0
  54. /data/doc/agents/{gemini_gem_evaluation.md → expired/gemini_gem_evaluation.md} +0 -0
  55. /data/doc/agents/{recommendations.md → expired/recommendations.md} +0 -0
  56. /data/doc/agents/{rubocop-issues.md → expired/rubocop-issues.md} +0 -0
@@ -102,8 +102,7 @@
102
102
  <dt id="Money-constant" class="">Money =
103
103
  <div class="docstring">
104
104
  <div class="discussion">
105
- <p>Optional top‑level aliases for application use.
106
- Not required automatically.</p>
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>
107
106
 
108
107
  </div>
109
108
  </div>
@@ -115,7 +114,16 @@ Not required automatically.</p>
115
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>
116
115
 
117
116
  <dt id="Currency-constant" class="">Currency =
118
-
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>
119
127
  </dt>
120
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>
121
129
 
@@ -133,9 +141,9 @@ Not required automatically.</p>
133
141
  </div>
134
142
 
135
143
  <div id="footer">
136
- Generated on Wed Jun 10 01:35:30 2026 by
144
+ Generated on Mon Jun 15 19:57:57 2026 by
137
145
  <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
138
- 0.9.44 (ruby-4.0.1).
146
+ 0.9.44 (ruby-4.0.5).
139
147
  </div>
140
148
 
141
149
  </div>
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ module Mint
5
+ # Represents a specific currency unit, identified by ISO 4217 alphabetic code.
6
+ # Currency objects are immutable and define the properties of a monetary unit
7
+ # including its subunit precision, display symbol, and formatting rules.
8
+ #
9
+ # @see https://www.iso.org/iso-4217-currency-codes.html
10
+ # @attr_reader code [String] ISO 4217 currency code (e.g., "USD", "EUR")
11
+ # @attr_reader subunit [Integer] Number of decimal places (0 for JPY, 2 for USD, 3 for IQD)
12
+ # @attr_reader symbol [String] Display symbol (e.g., "$", "€", "R$")
13
+ # @attr_reader priority [Integer] Parser precedence for symbol detection
14
+ # @attr_reader country [String, nil] Associated country code
15
+ # @attr_reader name [String, nil] Currency name
16
+ # @attr_reader fractional_multiplier [Integer] 10^subunit, used for fractional conversions
17
+ # @attr_reader minimum_amount [Rational] Smallest representable amount (1/fractional_multiplier)
18
+ Currency = Data.define(:code, :subunit, :symbol, :priority, :country, :name,
19
+ :fractional_multiplier) do
20
+ # @param code [String] ISO 4217 currency code
21
+ # @param symbol [String] Display symbol
22
+ # @param subunit [Integer] Number of decimal places (default 0)
23
+ # @param priority [Integer] Parser precedence for symbol detection (default 0)
24
+ # @param country [String, nil] Associated country code (default nil)
25
+ # @param name [String, nil] Currency name (default nil)
26
+ def initialize(code:, symbol:, subunit: 0, priority: 0, country: nil, name: nil)
27
+ subunit = subunit.to_i
28
+ priority = priority.to_i
29
+ fractional_multiplier = 10**subunit
30
+ super(code:, subunit:, symbol:, priority:, country:, name:,
31
+ fractional_multiplier:)
32
+ end
33
+
34
+ # @return [String] debug representation
35
+ def inspect = "<Currency:(#{code} #{symbol} #{subunit} #{name})>"
36
+
37
+ # @return [Rational] smallest representable amount (1/fractional_multiplier)
38
+ def minimum_amount = Rational(1, fractional_multiplier)
39
+
40
+ # Normalizes numeric amounts for this currency
41
+ # 1. Converts to Rational
42
+ # 2. Rounds to respect currency subunit
43
+ def normalize_amount(amount) = amount.to_r.round(subunit)
44
+ end
45
+
46
+ # Resolves an object into a {Currency}, returning +nil+ when it can't.
47
+ #
48
+ # Accepts +nil+, +String+, {Currency}, or {Money}.
49
+ # Passing a {Money} extracts its currency
50
+ #
51
+ # @param object [String, Currency, Money, nil] a currency code, object, or +nil+
52
+ # @return [Currency, nil] the resolved Currency, or +nil+ if +object+ is +nil+
53
+ # or the code is not registered
54
+ # @raise [ArgumentError] if +object+ is an unsupported type (e.g. +Integer+)
55
+ def Currency.resolve(object)
56
+ case object
57
+ when NilClass then nil
58
+ when Currency then object
59
+ when Money then object.currency
60
+ when String then Mint.currency_for_code object
61
+ else raise ArgumentError, "currency must be [Currency], [Money], [String] or nil (#{object})"
62
+ end
63
+ end
64
+
65
+ # Resolves an object into a {Currency}, raising on failure.
66
+ #
67
+ # Like {.resolve} but raises when the result would be +nil+.
68
+ #
69
+ # @param object [String, Currency, Money, nil] a currency code, object, or +nil+
70
+ # @return [Currency] the resolved Currency
71
+ # @raise [ArgumentError] if +object+ cannot be resolved into a registered currency
72
+ def Currency.resolve!(object)
73
+ resolve(object) or raise ArgumentError, "Could not resolve (#{object}) into a currency"
74
+ end
75
+ end
@@ -3,5 +3,8 @@
3
3
  # Optional top‑level aliases for application use.
4
4
  # Not required automatically.
5
5
 
6
+ # Alias for {Mint::Money} — enables `Money.new(...)` shorthand.
6
7
  Money = Mint::Money
8
+
9
+ # Alias for {Mint::Currency} — enables `Currency.new(...)` shorthand.
7
10
  Currency = Mint::Currency
@@ -1,22 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Mint refinements
3
+ # Mint Numeric refinements
4
4
  module Mint
5
5
  refine Numeric do
6
+ # @return [Money] self interpreted as BRL
6
7
  def reais = Mint.money(self, 'BRL')
7
8
 
9
+ # @return [Money] self interpreted as USD
8
10
  def dollars = Mint.money(self, 'USD')
9
11
 
12
+ # @return [Money] self interpreted as EUR
10
13
  def euros = Mint.money(self, 'EUR')
11
14
 
15
+ # @param currency [String, Symbol, Currency] target currency
16
+ # @return [Money] self interpreted as the given currency
12
17
  def to_money(currency) = Mint.money(self, currency)
13
18
 
14
19
  alias_method :dollar, :dollars
15
20
  alias_method :euro, :euros
16
21
  alias_method :mint, :to_money
17
22
  end
18
-
19
- refine String do
20
- def to_money(currency) = Mint.money(to_r, currency)
21
- end
22
23
  end
@@ -9,43 +9,56 @@ module Mint
9
9
  # iteration (+ / <=>) for non-numeric steps natively, so this patch is
10
10
  # only needed on older Rubies.
11
11
  module RangeStepPatch
12
- def step(step_size = nil, &)
12
+ # Iterates over the range using a Money step value.
13
+ # Overrides Range#step to handle Mint::Money step sizes on Ruby < 4.0.
14
+ #
15
+ # @param step_size [Mint::Money, nil] step amount
16
+ # @return [self, Enumerator] self if block given, Enumerator otherwise
17
+ def step(step_size = nil, &block)
13
18
  return super unless step_size.is_a?(Mint::Money)
14
19
 
15
20
  raise TypeError, "can't iterate from NilClass" unless self.begin
16
21
  raise ArgumentError, "step can't be 0" if step_size.zero?
17
22
 
18
- if block_given?
19
- each_money_step(step_size, &)
23
+ if block
24
+ each_money_step(step_size, &block)
20
25
  self
21
26
  else
22
- Enumerator.new do |yielder|
23
- each_money_step(step_size) { |v| yielder << v }
24
- end
27
+ Enumerator.new { |yielder| each_money_step(step_size) { |value| yielder << value } }
25
28
  end
26
29
  end
27
30
 
28
31
  private
29
32
 
30
- def each_money_step(step_amount)
33
+ # Dispatches to the appropriate iteration strategy based on range bounds.
34
+ # @private
35
+ def each_money_step(step, &)
36
+ self.end ? bounded_step(step, &) : unbounded_step(step, &)
37
+ end
38
+
39
+ # Iterates an open-ended range (no upper bound) with a Money step.
40
+ # @private
41
+ def unbounded_step(step)
31
42
  current = self.begin
32
- last = self.end
33
-
34
- unless last
35
- loop do
36
- yield current
37
- current += step_amount
38
- end
39
- return
43
+ loop do
44
+ yield current
45
+ current += step
40
46
  end
47
+ end
48
+
49
+ # Iterates a bounded range with a Money step, respecting direction and exclude_end?.
50
+ # @private
51
+ def bounded_step(step)
52
+ current = self.begin
53
+ last = self.end
54
+ asc = step.positive?
41
55
 
42
- ascending = step_amount.positive?
43
56
  loop do
44
- break if ascending ? current > last : current < last
57
+ break if asc ? current > last : current < last
45
58
  break if exclude_end? && current == last
46
59
 
47
60
  yield current
48
- current += step_amount
61
+ current += step
49
62
  end
50
63
  end
51
64
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mint String refinement
4
+ module Mint
5
+ refine String do
6
+ # Parses self as a numeric string and creates a Money in the given currency.
7
+ #
8
+ # @param currency [String, Symbol, Currency] target currency
9
+ # @return [Money]
10
+ def to_money(currency) = Mint.money(to_r, currency)
11
+ end
12
+ end
@@ -2,6 +2,9 @@
2
2
 
3
3
  # Mint refinements
4
4
  module Mint
5
+ # Registers top-level ::Money and ::Currency constants as aliases for Mint's classes.
6
+ #
7
+ # @raise [NameError] if ::Money or ::Currency are already defined and differ
5
8
  def self.use_top_level_constants!
6
9
  if !defined?(::Money) && !defined?(::Currency)
7
10
  require 'minting/mint/aliases'
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ module Mint
5
+ class << self
6
+ # Optional callable that returns a Hash with locale-aware formatting defaults.
7
+ #
8
+ # The callable receives no arguments and returns a Hash with these keys:
9
+ # [+:decimal+] Decimal separator (e.g. +","+)
10
+ # [+:thousand+] Thousands delimiter (e.g. +"."+)
11
+ # [+:format+] Format template string (e.g. +"%<amount>f %<symbol>s"+)
12
+ #
13
+ # When set, +#to_s+ and +#format+ use these values as fallbacks when the
14
+ # corresponding parameter is not explicitly provided.
15
+ #
16
+ # @example Rails I18n integration (in minting-rails railtie)
17
+ # Mint.locale_backend = -> {
18
+ # fmt = I18n.t('number.currency.format')
19
+ # {
20
+ # decimal: fmt[:separator],
21
+ # thousand: fmt[:delimiter],
22
+ # format: fmt[:format] == '%n %u' ? '%<amount>f %<symbol>s' : '%<symbol>s%<amount>f'
23
+ # }
24
+ # }
25
+ #
26
+ # @return [Proc, #call, nil]
27
+ attr_accessor :locale_backend
28
+ end
29
+ end
@@ -14,20 +14,36 @@ module Mint
14
14
  # @raise [ArgumentError] if the currency code is not registered
15
15
  def self.money(amount, currency_code) = Money.create(amount, currency_code)
16
16
 
17
- # Finds a registered currency by its code, symbol,
18
- # or retrieves it directly if already a Currency object.
17
+ # @return [Hash{String => Currency}] the frozen world-currencies hash
18
+ # @api private
19
+ def self.world_currencies = Registry.world_currencies
20
+
21
+ # Looks up a registered currency by its alpha code.
22
+ #
23
+ # Unlike {.currency}, this performs a direct hash lookup and only accepts strings.
19
24
  #
20
- # @param currency [String, Currency] the currency identifier or object
21
- # @return [Currency, nil] the registered Currency instance or nil if not found
22
- def self.currency(currency)
23
- case currency
24
- when NilClass then nil
25
- when Currency then currency
26
- when String then CurrencyRegistry.currencies[currency]
27
- else raise ArgumentError, "currency must be [Currency] ot [String] (#{currency})"
28
- end
25
+ # @param code [String] the currency code
26
+ # @return [Currency, nil] the registered Currency, or +nil+ if not found
27
+ def self.currency_for_code(code)
28
+ Registry.currencies[code]
29
29
  end
30
30
 
31
+ # Looks up a currency by its display symbol.
32
+ #
33
+ # @param symbol [String] the display symbol (e.g. "$", "R$")
34
+ # @return [Currency, nil] the highest-priority currency for the symbol
35
+ def self.currency_for_symbol(symbol)
36
+ Registry.currency_for_symbol(symbol)
37
+ end
38
+
39
+ # Returns a zero {Money} in the given currency, useful as a default value
40
+ # for discounts, totals, or placeholders.
41
+ #
42
+ # @param currency [String, Currency] a currency code or object
43
+ # @return [Money] a frozen zero-Money
44
+ # @raise [ArgumentError] if the currency can't be resolved
45
+ def self.zero(currency) = Registry.zero_for(Currency.resolve!(currency))
46
+
31
47
  # Registers a new currency, raising a KeyError if already registered.
32
48
  #
33
49
  # @param code [String] the unique currency code
@@ -38,6 +54,6 @@ module Mint
38
54
  # @raise [ArgumentError] if the code contains invalid characters
39
55
  # @raise [KeyError] if the currency code is already registered
40
56
  def self.register_currency(code:, subunit: 0, symbol: '', priority: 0)
41
- CurrencyRegistry.register(code:, subunit:, symbol:, priority:)
57
+ Registry.register(code:, subunit:, symbol:, priority:)
42
58
  end
43
59
  end
@@ -0,0 +1,62 @@
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
+ # @param input [String] Amount input, optionally including a currency symbol or code
10
+ # @param currency [String, Symbol, Currency, nil] ISO code when not present in +input+
11
+ # @return [Money]
12
+ # @raise [ArgumentError] when +input+ is invalid or currency cannot be determined
13
+ #
14
+ # @example With explicit currency
15
+ # Money.parse('19.99', 'USD') #=> [USD 19.99]
16
+ # Money.parse('1.234,56', 'EUR') #=> [EUR 1234.56]
17
+ #
18
+ # @example With symbol or code in the string
19
+ # Money.parse('$19.99') #=> [USD 19.99]
20
+ # Money.parse('19,99 €') #=> [EUR 19.99]
21
+ # Money.parse('USD 1,234.56') #=> [USD 1234.56]
22
+ def parse(input, currency = nil)
23
+ raise ArgumentError, 'input must be a String' unless input.is_a?(String)
24
+
25
+ input = input.strip
26
+ raise ArgumentError, 'input cannot be empty' if input.empty?
27
+
28
+ currency = Currency.resolve(currency) || parse_currency(input)
29
+ raise ArgumentError, "Currency [#{currency}] not registered" unless currency
30
+
31
+ amount = currency.normalize_amount(parse_amount(input))
32
+ Mint::Money.new(amount, currency)
33
+ end
34
+
35
+ private
36
+
37
+ # Extracts a numeric value from input that should only contain an amount.
38
+ # @private
39
+ def parse_amount(input)
40
+ accounting_negative = input.start_with?('(') && input.end_with?(')')
41
+
42
+ # Remove any charater that is not a digit, comma or period
43
+ numeric = input.scan(/[\d.,-]/).join
44
+ amount = Rational(normalize_separators(numeric))
45
+ accounting_negative ? -amount : amount
46
+ end
47
+
48
+ # Extracts currency from a string by matching ISO code or symbol.
49
+ #
50
+ # Scans all uppercase words and returns the first registered code, falling
51
+ # back to symbol matching. This correctly handles inputs like
52
+ # "MAX 10.00 USD" where the first uppercase word isn't a currency code.
53
+ # @private
54
+ def parse_currency(input)
55
+ input.scan(/\b([A-Z_]+)\b/) do |(code)|
56
+ currency = Mint.currency_for_code(code)
57
+ return currency if currency
58
+ end
59
+
60
+ Registry.detect_currency(input) or raise ArgumentError, 'Currency could not be detected'
61
+ end
62
+ end
@@ -0,0 +1,39 @@
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 :ambiguous # 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 normalize_separators(numeric)
25
+ case classify_separators(numeric)
26
+ when :decimal_period then numeric # Nothing to normalize (e.g. "1500" or "34.21").
27
+ when :decimal_comma then numeric.tr(',', '.') # Only one comma: decimal (e.g. 19,99 or 1,234).
28
+ when :thousands_comma then numeric.delete(',')
29
+ when :thousands then numeric.delete('.,')
30
+ when :ambiguous then raise ArgumentError, "could not distinguish decimal and thousand separators in '#{numeric}'"
31
+ when :mixed # Commas and dots: the rightmost one is the decimal separator.
32
+ if numeric.rindex(',') > numeric.rindex('.')
33
+ numeric.delete('.').tr(',', '.')
34
+ else
35
+ numeric.delete(',')
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # :nodoc:
5
+ module Registry
6
+ # Registers a new currency, raising a KeyError if already registered.
7
+ #
8
+ # @param code [String] the unique currency code
9
+ # @param subunit [Integer] the decimal subunit precision, defaults to 0
10
+ # @param symbol [String] the display symbol
11
+ # @param priority [Integer] parser precedence priority
12
+ # @return [Currency] the newly registered Currency instance
13
+ # @raise [ArgumentError] if the code contains invalid characters
14
+ # @raise [KeyError] if the currency code is already registered
15
+ def self.register(code:, subunit: 0, symbol: '', priority: 0)
16
+ raise ArgumentError, 'Currency code must be String' unless code.is_a? String
17
+ unless code.match?(/^[A-Z_]+$/)
18
+ raise ArgumentError,
19
+ "Currency code must have only letters or '_' ('USD',, 'MY_COIN')"
20
+ end
21
+
22
+ MUTEX.synchronize do
23
+ raise KeyError, "Currency: #{code} already registered" if currencies[code]
24
+
25
+ currency = Currency.new(code:, subunit:, symbol:, priority:)
26
+ @currencies = @currencies.merge(code => currency).freeze
27
+ @currency_symbols = nil
28
+ @currency_symbol_map = nil
29
+ currency
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require_relative 'symbols'
5
+ require_relative 'registration'
6
+ require_relative 'zeros'
7
+
8
+ # Mint registry: manages all cached state
9
+ module Mint
10
+ # Internal registry for currencies, symbols, and zero-money cache.
11
+ # All mutable shared state lives here.
12
+ module Registry
13
+ MUTEX = Monitor.new
14
+
15
+ private_constant :MUTEX
16
+
17
+ # Loads ISO world currencies from YAML file.
18
+ #
19
+ # @return [Hash{String => Currency}] ISO-4217 world currencies mapped by code
20
+ # @api private
21
+ def self.world_currencies
22
+ @world_currencies || MUTEX.synchronize do
23
+ @world_currencies = begin
24
+ path = File.join(File.expand_path('../../data', __dir__), 'world-currencies.yaml')
25
+ YAML.load_file(path).to_h { |entry| [entry['code'], Currency.new(**entry.transform_keys(&:to_sym))] }
26
+ end.freeze
27
+ end
28
+ end
29
+
30
+ # Returns the frozen hash of all registered currencies (world + custom).
31
+ #
32
+ # @return [Hash{String => Currency}] registered currencies mapped by code
33
+ # @api private
34
+ def self.currencies
35
+ @currencies || MUTEX.synchronize { @currencies = world_currencies.dup.freeze }
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # :nodoc:
5
+ module Registry
6
+ extend self
7
+
8
+ # Looks up a currency by its display symbol.
9
+ #
10
+ # @param symbol [String] the display symbol (e.g. "$", "R$")
11
+ # @return [Currency, nil] the highest-priority currency for the symbol
12
+ # @api private
13
+ def currency_for_symbol(symbol)
14
+ @currency_symbol_map || MUTEX.synchronize { @currency_symbol_map = currency_symbols.to_h.freeze }
15
+ @currency_symbol_map[symbol]
16
+ end
17
+
18
+ # Scans +input+ for registered currency symbols and returns the first match.
19
+ #
20
+ # @param input [String] the string to scan
21
+ # @return [Currency, nil]
22
+ # @api private
23
+ def detect_currency(input)
24
+ currency_symbols.each do |symbol, currency|
25
+ return currency if input.include?(symbol)
26
+ end
27
+ nil
28
+ end
29
+
30
+ private
31
+
32
+ # Registered symbols sorted for detection: longest match wins, then parser priority.
33
+ # Duplicate symbols are deduplicated — the highest-priority currency wins.
34
+ #
35
+ # @return [Array<Array<String, Currency>>] sorted symbol-to-currency mappings
36
+ # @api private
37
+ def currency_symbols
38
+ @currency_symbols || MUTEX.synchronize do
39
+ @currency_symbols =
40
+ currencies.values
41
+ .reject { |currency| currency.symbol.empty? }
42
+ .map { |currency| [currency.symbol, currency] }
43
+ .sort_by { |symbol, currency| [-symbol.length, -currency.priority] }
44
+ .uniq { |symbol, _| symbol }
45
+ .freeze
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # :nodoc:
5
+ module Registry
6
+ # Returns the cached zero-Money for a currency, creating it if needed.
7
+ #
8
+ # @param currency [Currency] the currency object
9
+ # @return [Money] a frozen zero-Money
10
+ # @api private
11
+ def self.zero_for(currency)
12
+ MUTEX.synchronize do
13
+ @zeros ||= {}
14
+ @zeros[currency] ||= Mint::Money.send(:new, 0, currency)
15
+ end
16
+ end
17
+ end
18
+ end
data/lib/minting/mint.rb CHANGED
@@ -1,18 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'minting/mint/currency/currency'
4
- require 'minting/mint/currency/currency_registry'
5
- require 'minting/mint/currency/world_currencies'
6
- require 'minting/mint/dsl/range'
7
- require 'minting/mint/dsl/refinements'
8
- require 'minting/mint/dsl/top_level'
9
- require 'minting/mint/mint'
10
- require 'minting/mint/parser'
11
- require 'minting/money/allocation'
12
- require 'minting/money/arithmetics'
13
- require 'minting/money/coercion'
14
- require 'minting/money/comparable'
15
- require 'minting/money/constructors'
16
- require 'minting/money/conversion'
17
- require 'minting/money/formatting'
18
- require 'minting/money/money'
3
+ require_relative 'currency/currency'
4
+
5
+ require_relative 'mint/dsl/numeric'
6
+ require_relative 'mint/dsl/range'
7
+ require_relative 'mint/dsl/string'
8
+ require_relative 'mint/dsl/top_level'
9
+ require_relative 'mint/locale_backend'
10
+ require_relative 'mint/mint'
11
+ require_relative 'mint/parser/parser'
12
+ require_relative 'mint/parser/separators'
13
+ require_relative 'mint/registry/registry'
14
+
15
+ require_relative 'money/money'
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # Allocation and splitting
5
+ class Money
6
+ # Proportionally allocates the monetary amount among a list of ratios.
7
+ # Disperses any subunit rounding amounts across the initial slots
8
+ # @param proportions [Array<Numeric>] a list of numeric proportions/ratios to allocate by
9
+ # @return [Array<Money>] the list of newly allocated Money objects
10
+ # @raise [ArgumentError] if the proportions list is empty or sums to zero
11
+ #
12
+ # @example Proportional allocation
13
+ # money = Mint.money(10.00, 'USD')
14
+ # money.allocate([1, 2, 3]) #=> [[USD 1.67], [USD 3.33], [USD 5.00]]
15
+ def allocate(proportions)
16
+ whole = proportions.sum.to_r
17
+ raise ArgumentError, 'Need at least 1 proportion element' if proportions.empty?
18
+ raise ArgumentError, 'Proportions total must not be zero' if whole.zero?
19
+
20
+ subunit = currency.subunit
21
+ amounts = proportions.map { |rate| Rational(amount * rate, whole).round(subunit) }
22
+ allocate_left_over(amounts: amounts, left_over: amount - amounts.sum)
23
+ end
24
+ end
25
+ end