minting 1.7.2 → 1.8.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -3
  3. data/doc/Mint/Currency.html +826 -55
  4. data/doc/Mint/Money.html +715 -218
  5. data/doc/Mint/RangeStepPatch.html +1 -1
  6. data/doc/Mint/Registry.html +859 -0
  7. data/doc/Mint/Rounding.html +495 -0
  8. data/doc/Mint/UnknownCurrency.html +1 -1
  9. data/doc/Mint.html +307 -225
  10. data/doc/Minting.html +2 -2
  11. data/doc/_index.html +15 -8
  12. data/doc/agents/api_review-2026-06-15.md +329 -0
  13. data/doc/agents/copilot-instructions.md +0 -5
  14. data/doc/agents/expired/copilot-instructions.md +75 -0
  15. data/doc/class_list.html +1 -1
  16. data/doc/file.README.html +25 -4
  17. data/doc/index.html +25 -4
  18. data/doc/method_list.html +177 -25
  19. data/doc/top-level-namespace.html +1 -1
  20. data/lib/minting/currency/currency.rb +71 -1
  21. data/lib/minting/mint/dsl/range.rb +1 -0
  22. data/lib/minting/mint/locale_backend.rb +29 -0
  23. data/lib/minting/mint/mint.rb +13 -38
  24. data/lib/minting/mint/parser/parser.rb +50 -19
  25. data/lib/minting/mint/parser/separators.rb +10 -8
  26. data/lib/minting/mint/registry/registration.rb +33 -0
  27. data/lib/minting/mint/registry/registry.rb +38 -0
  28. data/lib/minting/mint/registry/symbols.rb +49 -0
  29. data/lib/minting/mint/registry/zeros.rb +20 -0
  30. data/lib/minting/mint/rounding.rb +51 -0
  31. data/lib/minting/mint.rb +12 -23
  32. data/lib/minting/money/allocation/allocation.rb +1 -2
  33. data/lib/minting/money/allocation/split.rb +1 -1
  34. data/lib/minting/money/arithmetics/methods.rb +2 -2
  35. data/lib/minting/money/arithmetics/operators.rb +6 -6
  36. data/lib/minting/money/clamp.rb +1 -1
  37. data/lib/minting/money/coercion.rb +1 -1
  38. data/lib/minting/money/comparable.rb +6 -0
  39. data/lib/minting/money/constructors.rb +63 -20
  40. data/lib/minting/money/format/formatting.rb +16 -0
  41. data/lib/minting/money/format/to_s.rb +13 -4
  42. data/lib/minting/money/money.rb +12 -6
  43. data/lib/minting/version.rb +1 -1
  44. metadata +15 -7
  45. data/lib/minting/currency/currency_registry.rb +0 -67
  46. data/lib/minting/currency/world_currencies.rb +0 -16
  47. /data/doc/agents/{AGENTS.md → expired/AGENTS.md} +0 -0
  48. /data/doc/agents/{gemini_gem_evaluation.md → expired/gemini_gem_evaluation.md} +0 -0
  49. /data/doc/agents/{recommendations.md → expired/recommendations.md} +0 -0
  50. /data/doc/agents/{rubocop-issues.md → expired/rubocop-issues.md} +0 -0
@@ -12,46 +12,21 @@ module Mint
12
12
  # @param currency_code [Currency, String] Currency code
13
13
  # @return [Money] the instantiated Money object
14
14
  # @raise [ArgumentError] if the currency code is not registered
15
- def self.money(amount, currency_code) = Money.create(amount, currency_code)
15
+ def self.money(amount, currency_code) = Money.from(amount, currency_code)
16
16
 
17
- # Finds a registered currency by its code, symbol,
18
- # or retrieves it directly if already a Currency object.
19
- #
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], [String] or nil (#{currency})"
28
- end
29
- end
17
+ # @return [Hash{String => Currency}] the frozen world-currencies hash
18
+ # @api private
19
+ def self.world_currencies = Registry.world_currencies
30
20
 
31
- # Returns a zero {Money} in the given currency, useful as a default value
32
- # for discounts, totals, or placeholders.
21
+ # Executes a block with a specific rounding mode applied to all money
22
+ # construction, parsing, change, allocation, and split operations.
33
23
  #
34
- # @param currency [String, Currency] a currency code or object
35
- # @return [Money] a frozen zero-Money
36
- # @raise [ArgumentError] if the currency is not registered
37
- def self.zero(currency)
38
- checked = Mint.currency(currency)
39
- raise ArgumentError, "Invalid Currency: [#{currency}]" unless checked
40
-
41
- @zeros ||= CurrencyRegistry.currencies.values.to_h { |currency| [currency, Mint::Money.send(:new, 0, currency)] }
42
- @zeros[currency] ||= Money.send(:new, 0, currency)
43
- @zeros[currency]
44
- end
45
-
24
+ # Restores the previous mode (or default) when the block exits, even on
25
+ # exception.
46
26
  #
47
- # @param code [String] the unique currency code
48
- # @param subunit [Integer] the decimal subunit precision, defaults to 0
49
- # @param symbol [String] the display symbol
50
- # @param priority [Integer] parser precedence priority
51
- # @return [Currency] the newly registered Currency instance
52
- # @raise [ArgumentError] if the code contains invalid characters
53
- # @raise [KeyError] if the currency code is already registered
54
- def self.register_currency(code:, subunit: 0, symbol: '', priority: 0)
55
- CurrencyRegistry.register(code:, subunit:, symbol:, priority:)
56
- end
27
+ # @param mode [Symbol] one of: +:half_up+, +:half_down+, +:floor+,
28
+ # +:ceil+, +:truncate+, +:down+
29
+ # @yield block to execute with the rounding mode active
30
+ # @raise [ArgumentError] if +mode+ is not a recognised rounding mode
31
+ def self.with_rounding(mode, &) = Rounding.with_mode(mode, &)
57
32
  end
@@ -6,29 +6,58 @@ module Mint
6
6
 
7
7
  # Parses a human-readable money string into a {Money} object.
8
8
  #
9
+ # Returns +nil+ when the input is invalid or currency cannot be determined.
10
+ #
9
11
  # @param input [String] Amount input, optionally including a currency symbol or code
10
12
  # @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
+ # @return [Money, nil]
13
14
  #
14
15
  # @example With explicit currency
15
- # Money.parse('19.99', 'USD') #=> [USD 19.99]
16
- # Money.parse('1.234,56', 'EUR') #=> [EUR 1234.56]
16
+ # Mint.parse('19.99', 'USD') #=> [USD 19.99]
17
+ # Mint.parse('garbage', 'USD') #=> nil
17
18
  #
18
19
  # @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]
20
+ # Mint.parse('$19.99') #=> [USD 19.99]
21
+ # Mint.parse('USD 1,234.56') #=> [USD 1234.56]
22
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)
23
49
  raise ArgumentError, 'input must be a String' unless input.is_a?(String)
24
50
 
25
51
  input = input.strip
26
52
  raise ArgumentError, 'input cannot be empty' if input.empty?
27
53
 
28
- currency = Mint.currency(currency) || parse_currency(input)
29
- raise ArgumentError, "Currency [#{currency}] not registered" unless currency
54
+ currency = parse_currency(input, currency)
55
+ raise ArgumentError, "Currency [#{currency}] not found" unless currency
30
56
 
31
- amount = currency.normalize_amount(parse_amount(input))
57
+ amount = parse_amount(input)
58
+ raise ArgumentError, "Could not parse [#{input}]" unless amount
59
+
60
+ amount = currency.normalize_amount(amount)
32
61
  Mint::Money.new(amount, currency)
33
62
  end
34
63
 
@@ -40,8 +69,11 @@ module Mint
40
69
  accounting_negative = input.start_with?('(') && input.end_with?(')')
41
70
 
42
71
  # Remove any charater that is not a digit, comma or period
43
- numeric = input.scan(/[\d.,-]/).join
44
- amount = Rational(normalize_separators(numeric))
72
+ numeric_input = input.scan(/[\d.,-]/).join
73
+ numeric = parse_separators(numeric_input)
74
+ return nil unless numeric
75
+
76
+ amount = Rational(numeric)
45
77
  accounting_negative ? -amount : amount
46
78
  end
47
79
 
@@ -51,16 +83,15 @@ module Mint
51
83
  # back to symbol matching. This correctly handles inputs like
52
84
  # "MAX 10.00 USD" where the first uppercase word isn't a currency code.
53
85
  # @private
54
- def parse_currency(input)
86
+ def parse_currency(input, currency = nil)
87
+ currency = Currency.resolve(currency)
88
+ return currency if currency
89
+
55
90
  input.scan(/\b([A-Z_]+)\b/) do |(code)|
56
- currency = Mint.currency(code)
91
+ currency = Currency.for_code(code)
57
92
  return currency if currency
58
93
  end
59
94
 
60
- CurrencyRegistry.currency_symbols.each do |symbol, currency|
61
- return currency if input.include?(symbol)
62
- end
63
-
64
- raise ArgumentError, 'Currency could not be detected'
95
+ Registry.detect_currency(input)
65
96
  end
66
97
  end
@@ -10,24 +10,26 @@ module Mint
10
10
  # @private
11
11
  def classify_separators(numeric)
12
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)
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
19
  end
20
20
  end
21
21
 
22
22
  # Converts locale-specific decimal/thousand separators into a plain decimal string.
23
23
  # @private
24
- def normalize_separators(numeric)
24
+ def parse_separators(numeric)
25
+ return nil unless numeric.match?(/\d/)
26
+
25
27
  case classify_separators(numeric)
26
28
  when :decimal_period then numeric # Nothing to normalize (e.g. "1500" or "34.21").
27
29
  when :decimal_comma then numeric.tr(',', '.') # Only one comma: decimal (e.g. 19,99 or 1,234).
28
30
  when :thousands_comma then numeric.delete(',')
29
31
  when :thousands then numeric.delete('.,')
30
- when :ambiguous then raise ArgumentError, "could not distinguish decimal and thousand separators in '#{numeric}'"
32
+ when :invalid then nil
31
33
  when :mixed # Commas and dots: the rightmost one is the decimal separator.
32
34
  if numeric.rindex(',') > numeric.rindex('.')
33
35
  numeric.delete('.').tr(',', '.')
@@ -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,20 @@
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
+ raise ArgumentError, "Expect a Currency param. (#{currency})" unless currency.is_a?(Currency)
13
+
14
+ MUTEX.synchronize do
15
+ @zeros ||= {}
16
+ @zeros[currency] ||= Mint::Money.send(:new, 0, currency)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # Rounding-mode dispatch table and block-scoped context.
5
+ # @api private
6
+ module Rounding
7
+ # Maps mode symbols to their corresponding +Rational+ rounding lambdas.
8
+ # @return [Hash{Symbol => Proc}]
9
+ # @api private
10
+ MODES = {
11
+ half_up: ->(amount, ndigits) { amount.round(ndigits, half: :up) },
12
+ half_down: ->(amount, ndigits) { amount.round(ndigits, half: :down) },
13
+ floor: ->(amount, ndigits) { amount.floor(ndigits) },
14
+ ceil: ->(amount, ndigits) { amount.ceil(ndigits) },
15
+ truncate: ->(amount, ndigits) { amount.truncate(ndigits) },
16
+ down: ->(amount, ndigits) { amount.truncate(ndigits) }
17
+ }.freeze
18
+
19
+ # Returns the currently active rounding mode, falling back to +:half_up+.
20
+ # @api private
21
+ # @return [Symbol]
22
+ def self.current_mode
23
+ Thread.current[:minting_rounding_mode] || :half_up
24
+ end
25
+
26
+ # Rounds +amount+ to +ndigits+ using the currently scoped rounding mode.
27
+ # @api private
28
+ # @param amount [Numeric]
29
+ # @param ndigits [Integer]
30
+ # @return [Rational]
31
+ def self.apply(amount, ndigits)
32
+ MODES.fetch(current_mode).call(amount.to_r, ndigits)
33
+ end
34
+
35
+ # Sets a rounding mode for the duration of a block, restoring the
36
+ # previous mode on exit (even on exception).
37
+ # @api private
38
+ # @param mode [Symbol]
39
+ # @yield block to execute with the mode active
40
+ # @raise [ArgumentError] on unknown mode
41
+ def self.with_mode(mode)
42
+ raise ArgumentError, "Unknown rounding mode: #{mode}" unless MODES.key?(mode)
43
+
44
+ prev = Thread.current[:minting_rounding_mode]
45
+ Thread.current[:minting_rounding_mode] = mode
46
+ yield
47
+ ensure
48
+ Thread.current[:minting_rounding_mode] = prev
49
+ end
50
+ end
51
+ end
data/lib/minting/mint.rb CHANGED
@@ -1,27 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'minting/currency/currency'
4
- require 'minting/currency/currency_registry'
5
- require 'minting/currency/world_currencies'
3
+ require_relative 'mint/rounding'
4
+ require_relative 'currency/currency'
6
5
 
7
- require 'minting/mint/dsl/numeric'
8
- require 'minting/mint/dsl/range'
9
- require 'minting/mint/dsl/string'
10
- require 'minting/mint/dsl/top_level'
6
+ require_relative 'mint/dsl/numeric'
7
+ require_relative 'mint/dsl/range'
8
+ require_relative 'mint/dsl/string'
9
+ require_relative 'mint/dsl/top_level'
10
+ require_relative 'mint/locale_backend'
11
+ require_relative 'mint/mint'
12
+ require_relative 'mint/parser/parser'
13
+ require_relative 'mint/parser/separators'
14
+ require_relative 'mint/registry/registry'
11
15
 
12
- require 'minting/mint/mint'
13
- require 'minting/mint/parser/parser'
14
- require 'minting/mint/parser/separators'
15
-
16
- require 'minting/money/allocation/allocation'
17
- require 'minting/money/allocation/split'
18
- require 'minting/money/arithmetics/methods'
19
- require 'minting/money/arithmetics/operators'
20
- require 'minting/money/clamp'
21
- require 'minting/money/coercion'
22
- require 'minting/money/comparable'
23
- require 'minting/money/constructors'
24
- require 'minting/money/conversion'
25
- require 'minting/money/format/formatting'
26
- require 'minting/money/format/to_s'
27
- require 'minting/money/money'
16
+ require_relative 'money/money'
@@ -17,8 +17,7 @@ module Mint
17
17
  raise ArgumentError, 'Need at least 1 proportion element' if proportions.empty?
18
18
  raise ArgumentError, 'Proportions total must not be zero' if whole.zero?
19
19
 
20
- subunit = currency.subunit
21
- amounts = proportions.map { |rate| Rational(amount * rate, whole).round(subunit) }
20
+ amounts = proportions.map { |rate| currency.normalize_amount(Rational(amount * rate, whole)) }
22
21
  allocate_left_over(amounts: amounts, left_over: amount - amounts.sum)
23
22
  end
24
23
  end
@@ -17,7 +17,7 @@ module Mint
17
17
  def split(slices)
18
18
  raise ArgumentError, 'Slices quantity must be an poitive integer' unless slices.positive? && slices.integer?
19
19
 
20
- fraction = (amount / slices).round(currency.subunit)
20
+ fraction = currency.normalize_amount(amount / slices)
21
21
  allocate_left_over(amounts: Array.new(slices, fraction),
22
22
  left_over: amount - (fraction * slices))
23
23
  end
@@ -6,7 +6,7 @@ module Mint
6
6
  # Returns the absolute value of the monetary amount as a new {Money} instance.
7
7
  #
8
8
  # @return [Money] the absolute value
9
- def abs = mint(amount.abs)
9
+ def abs = copy_with(amount: amount.abs)
10
10
 
11
11
  # Returns true if the monetary amount is less than zero.
12
12
  #
@@ -22,6 +22,6 @@ module Mint
22
22
  # Enables standard ranges and stepping (e.g. `1.dollar..10.dollars`).
23
23
  #
24
24
  # @return [Money] successor Money instance
25
- def succ = mint(amount + currency.minimum_amount)
25
+ def succ = copy_with(amount: amount + currency.minimum_amount)
26
26
  end
27
27
  end
@@ -11,7 +11,7 @@ module Mint
11
11
  def +(addend)
12
12
  case addend
13
13
  in 0 then self
14
- in Money if same_currency?(addend) then mint(amount + addend.amount)
14
+ in Money if same_currency?(addend) then copy_with(amount: amount + addend.amount)
15
15
  else raise TypeError, "#{addend} can't be added to #{self}"
16
16
  end
17
17
  end
@@ -24,7 +24,7 @@ module Mint
24
24
  def -(subtrahend)
25
25
  case subtrahend
26
26
  when 0 then return self
27
- when Money then return mint(amount - subtrahend.amount) if same_currency?(subtrahend)
27
+ when Money then return copy_with(amount: amount - subtrahend.amount) if same_currency?(subtrahend)
28
28
  end
29
29
  raise TypeError, "#{subtrahend} can't be subtracted from #{self}"
30
30
  end
@@ -32,7 +32,7 @@ module Mint
32
32
  # Unary negation operator. Returns a new {Money} instance with the inverted sign.
33
33
  #
34
34
  # @return [Money] negated Money instance
35
- def -@ = mint(-amount)
35
+ def -@ = copy_with(amount: -amount)
36
36
 
37
37
  # Performs multiplication of the monetary value by a standard scalar Numeric.
38
38
  #
@@ -42,7 +42,7 @@ module Mint
42
42
  def *(multiplicand)
43
43
  raise TypeError, "#{self} can't be multiplied by #{multiplicand}" unless multiplicand.is_a?(Numeric)
44
44
 
45
- mint(amount * multiplicand)
45
+ copy_with(amount: amount * multiplicand)
46
46
  end
47
47
 
48
48
  # Performs division of the monetary value by a scalar Numeric or identical currency {Money}.
@@ -53,7 +53,7 @@ module Mint
53
53
  # @raise [ZeroDivisionError] if division by zero is attempted
54
54
  def /(divisor)
55
55
  case divisor
56
- when Numeric then return mint(amount / divisor)
56
+ when Numeric then return copy_with(amount: amount / divisor)
57
57
  when Money then return amount / divisor.amount if same_currency? divisor
58
58
  end
59
59
  raise TypeError, "#{self} can't be divided by #{divisor}"
@@ -65,7 +65,7 @@ module Mint
65
65
  # @return [Money] reult of amount ** exponent
66
66
  # @raise [TypeError] if exponent is not Numeric
67
67
  def **(exponent)
68
- return mint(amount**exponent) if exponent.is_a?(Numeric)
68
+ return copy_with(amount: amount**exponent) if exponent.is_a?(Numeric)
69
69
 
70
70
  raise TypeError, "#{self} can't be powered by #{exponent}"
71
71
  end
@@ -47,7 +47,7 @@ module Mint
47
47
  else
48
48
  min = min_or_range
49
49
  end
50
- mint(amount.clamp(normalize_boundary(min), normalize_boundary(max)))
50
+ copy_with(amount: amount.clamp(normalize_boundary(min), normalize_boundary(max)))
51
51
  end
52
52
 
53
53
  private
@@ -44,7 +44,7 @@ module Mint
44
44
  # Multiplies a Money object by the wrapped numeric value.
45
45
  # This is the standard coercion path for `Numeric * Money`.
46
46
  def *(other)
47
- other.mint(@value * other.amount)
47
+ other.copy_with(amount: @value * other.amount)
48
48
  end
49
49
 
50
50
  # @private
@@ -44,6 +44,12 @@ module Mint
44
44
  # @return [self, nil] self if amount is non-zero, nil otherwise
45
45
  def nonzero? = amount.nonzero?
46
46
 
47
+ # Helper method to verify if another Money has the identical currency.
48
+ #
49
+ # @param other [Money] the target currency to compare
50
+ # @return [Boolean] true if currencies match, false otherwise
51
+ def same_currency?(other) = other.currency == currency
52
+
47
53
  # @return [Boolean] true if amount is zero
48
54
  def zero? = amount.zero?
49
55
  end