minting 1.6.3 → 1.7.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.
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mint Range patch
4
+ # @api private
5
+ module Mint
6
+ if RUBY_VERSION < '4.0'
7
+ # Ruby < 4.0's Range#step calls rb_to_int on non-Numeric step arguments,
8
+ # which raises TypeError for Money objects. Ruby 4.0+ uses arithmetic
9
+ # iteration (+ / <=>) for non-numeric steps natively, so this patch is
10
+ # only needed on older Rubies.
11
+ module RangeStepPatch
12
+ def step(step_size = nil, &)
13
+ return super unless step_size.is_a?(Mint::Money)
14
+
15
+ raise TypeError, "can't iterate from NilClass" unless self.begin
16
+ raise ArgumentError, "step can't be 0" if step_size.zero?
17
+
18
+ if block_given?
19
+ each_money_step(step_size, &)
20
+ self
21
+ else
22
+ Enumerator.new do |yielder|
23
+ each_money_step(step_size) { |v| yielder << v }
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def each_money_step(step_amount)
31
+ 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
40
+ end
41
+
42
+ ascending = step_amount.positive?
43
+ loop do
44
+ break if ascending ? current > last : current < last
45
+ break if exclude_end? && current == last
46
+
47
+ yield current
48
+ current += step_amount
49
+ end
50
+ end
51
+ end
52
+ Range.prepend(Mint::RangeStepPatch)
53
+ end
54
+ end
@@ -19,14 +19,4 @@ module Mint
19
19
  refine String do
20
20
  def to_money(currency) = Mint.money(to_r, currency)
21
21
  end
22
-
23
- def self.use_top_level_constants!
24
- if !defined?(::Money) && !defined?(::Currency)
25
- require 'minting/mint/aliases'
26
- elsif ::Money == Mint::Money && ::Currency == Mint::Currency
27
- warn 'Warning: Money and Currency already defined as Mint aliases, skipping'
28
- else
29
- raise NameError, 'Cannot define top-level Money or Currency constants: already defined'
30
- end
31
- end
32
22
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mint refinements
4
+ module Mint
5
+ def self.use_top_level_constants!
6
+ if !defined?(::Money) && !defined?(::Currency)
7
+ require 'minting/mint/aliases'
8
+ elsif ::Money == Mint::Money && ::Currency == Mint::Currency
9
+ warn 'Warning: Money and Currency already defined as Mint aliases, skipping'
10
+ else
11
+ raise NameError, 'Cannot define top-level Money or Currency constants: already defined'
12
+ end
13
+ end
14
+ end
@@ -21,10 +21,10 @@ module Mint
21
21
  # @return [Currency, nil] the registered Currency instance or nil if not found
22
22
  def self.currency(currency)
23
23
  case currency
24
- when nil then nil
24
+ when NilClass then nil
25
25
  when Currency then currency
26
26
  when String then CurrencyRegistry.currencies[currency]
27
- else raise ArgumentError, "currency must be [Currency] ot [String] (#{currency})"
27
+ else raise ArgumentError, "currency must be [Currency] ot [String] (#{currency})"
28
28
  end
29
29
  end
30
30
 
@@ -42,27 +42,34 @@ module Mint
42
42
  Rational(numeric)
43
43
  end
44
44
 
45
+ def classify_separators(numeric)
46
+ case [numeric.count(','), numeric.count('.')]
47
+ in [0, 0] | [0, 1] then :decimal_period # e.g. "1500" or "34.21".
48
+ in [1, 0] then :decimal_comma # Only one comma: decimal (e.g. 19,99 or 1,234).
49
+ in [c, p] if c > 1 && p > 1 then :ambiguous # Both separators appear multiple times
50
+ in [c, p] if c > 0 && p > 0 then :mixed # Commas and dots: the rightmost one is the decimal separator.
51
+ else :thousands_only # Multiple of the same separator only (e.g. 1,234,567)
52
+ end
53
+ end
54
+
45
55
  # Converts locale-specific decimal/thousand separators into a plain decimal string.
46
56
  def normalize_separators(numeric)
47
- case [numeric.count(','), numeric.count('.')]
48
- in [0, 0] | [0, 1] then numeric # Nothing to normalize (e.g. "1500" or "34.21").
49
- in [1, 0] then numeric.tr(',', '.') # Only one comma: decimal (e.g. 19,99 or 1,234).
50
- in [c, p] if c > 1 && p > 1 # Both separators appear multiple times
51
- raise ArgumentError, "could not distinguish decimal and thousand separators in '#{numeric}'"
52
- in [c, p] if c > 0 && p > 0 # Commas and dots: the rightmost one is the decimal separator.
57
+ case classify_separators(numeric)
58
+ when :decimal_period then numeric # Nothing to normalize (e.g. "1500" or "34.21").
59
+ when :decimal_comma then numeric.tr(',', '.') # Only one comma: decimal (e.g. 19,99 or 1,234).
60
+ when :thousands_only then numeric.delete(',.')
61
+ when :ambiguous then raise ArgumentError, "could not distinguish decimal and thousand separators in '#{numeric}'"
62
+ when :mixed # Commas and dots: the rightmost one is the decimal separator.
53
63
  if numeric.rindex(',') > numeric.rindex('.')
54
64
  numeric.delete('.').tr(',', '.')
55
65
  else
56
66
  numeric.delete(',')
57
67
  end
58
- else # Multiple of the same separator only (e.g. 1,234,567) — all are thousands.
59
- numeric.delete(',.')
60
68
  end
61
69
  end
62
70
 
63
71
  def parse_currency(input)
64
72
  case input
65
- when nil then return nil
66
73
  when String
67
74
  # Prefer an explicit ISO 4217 code (e.g. "USD 1,234.56") over symbol matching.
68
75
  currency = Mint.currency(input[/\b([A-Z_]+)\b/, 1])
data/lib/minting/mint.rb CHANGED
@@ -3,7 +3,9 @@
3
3
  require 'minting/mint/currency/currency'
4
4
  require 'minting/mint/currency/currency_registry'
5
5
  require 'minting/mint/currency/world_currencies'
6
- require 'minting/mint/dsl'
6
+ require 'minting/mint/dsl/range'
7
+ require 'minting/mint/dsl/refinements'
8
+ require 'minting/mint/dsl/top_level'
7
9
  require 'minting/mint/mint'
8
10
  require 'minting/mint/parser'
9
11
  require 'minting/money/allocation'
@@ -19,41 +19,41 @@ module Mint
19
19
 
20
20
  subunit = currency.subunit
21
21
  amounts = proportions.map { |rate| Rational(amount * rate, whole).round(subunit) }
22
- allocate_left_over!(amounts: amounts, left_over: amount - amounts.sum)
22
+ allocate_left_over(amounts: amounts, left_over: amount - amounts.sum)
23
23
  end
24
24
 
25
25
  # Splits the monetary amount into a given quantity of equal parts.
26
26
  # Disperses any fractional subunit rounding differences across the initial slots
27
27
  # so that the sum is preserved.
28
28
  #
29
- # @param quantity [Integer] the number of equal parts to divide the money into (must be > 0)
29
+ # @param slices [Integer] the number of equal parts to divide the money into (must be > 0)
30
30
  # @return [Array<Money>] the list of newly split Money objects
31
31
  # @raise [ArgumentError] if quantity is not a positive integer
32
32
  #
33
33
  # @example Even split
34
34
  # money = Mint.money(10.00, 'USD')
35
35
  # money.split(3) #=> [[USD 3.34], [USD 3.33], [USD 3.33]]
36
- def split(quantity)
37
- unless quantity.positive? && quantity.integer?
38
- raise ArgumentError,
39
- 'quantity must be an integer > 0'
40
- end
36
+ def split(slices)
37
+ raise ArgumentError, 'Slices quantity must be an poitive integer' unless slices.positive? && slices.integer?
41
38
 
42
- fraction = (amount / quantity).round(currency.subunit)
43
- allocate_left_over!(amounts: Array.new(quantity, fraction),
44
- left_over: amount - (fraction * quantity))
39
+ fraction = (amount / slices).round(currency.subunit)
40
+ allocate_left_over(amounts: Array.new(slices, fraction),
41
+ left_over: amount - (fraction * slices))
45
42
  end
46
43
 
47
44
  private
48
45
 
49
- def allocate_left_over!(amounts:, left_over:)
46
+ # Distributes any leftover amount across the allocation slots by adjusting
47
+ # individual amounts by the currency's minimum unit, and converting to Money.
48
+ # Caution: amounts array is mutated by this method
49
+ def allocate_left_over(amounts:, left_over:)
50
50
  if left_over.nonzero?
51
51
  minimum = currency.minimum_amount
52
52
  minimum = -minimum if left_over.negative?
53
- last_slot = (left_over / minimum).to_i - 1
54
- (0..last_slot).each { |slot| amounts[slot] += minimum }
53
+ slots_to_adjust = (left_over / minimum).to_i
54
+ (0...slots_to_adjust).each { |slot| amounts[slot] += minimum }
55
55
  end
56
- amounts.map { |amount| Money.new(amount, currency) }
56
+ amounts.map! { |amount| Money.new(amount, currency) }
57
57
  end
58
58
  end
59
59
  end
@@ -31,10 +31,10 @@ module Mint
31
31
  # @raise [TypeError] if addition involves a different currency or incompatible types
32
32
  def +(addend)
33
33
  case addend
34
- when 0 then return self
35
- when Money then return mint(amount + addend.amount) if same_currency?(addend)
34
+ in 0 then self
35
+ in Money if same_currency?(addend) then mint(amount + addend.amount)
36
+ else raise TypeError, "#{addend} can't be added to #{self}"
36
37
  end
37
- raise TypeError, "#{addend} can't be added to #{self}"
38
38
  end
39
39
 
40
40
  # Performs subtraction with another {Money} instance or standard zero Numeric.
@@ -61,9 +61,9 @@ module Mint
61
61
  # @return [Money] the multiplied Money instance
62
62
  # @raise [TypeError] if multiplier is not Numeric or is a Money object
63
63
  def *(multiplicand)
64
- return mint(amount * multiplicand) if multiplicand.is_a?(Numeric)
64
+ raise TypeError, "#{self} can't be multiplied by #{multiplicand}" unless multiplicand.is_a?(Numeric)
65
65
 
66
- raise TypeError, "#{self} can't be multiplied by #{multiplicand}"
66
+ mint(amount * multiplicand)
67
67
  end
68
68
 
69
69
  # Performs division of the monetary value by a scalar Numeric or identical currency {Money}.
@@ -4,13 +4,13 @@ module Mint
4
4
  # Implements the standard Ruby coercion protocol.
5
5
  class Money
6
6
  # Allows {Money} to interact seamlessly as the right-hand operand in Numeric arithmetic.
7
- # This enables expressions like `5 + money` where `5` is a Numeric and `money` is a Money object.
7
+ # This enables expressions like `5 * money` where `5` is a Numeric and `money` is a Money object.
8
8
  #
9
9
  # @param other [Numeric] the left-hand operand to coerce
10
10
  # @return [Array(CoercedNumber, Money)] coerced operand array
11
11
  # @example
12
12
  # price = Mint.money(10, 'USD')
13
- # 5 + price #=> [USD 15.00] (via coercion)
13
+ # 5 * price #=> [USD 50.00] (via coercion)
14
14
  def coerce(other)
15
15
  [CoercedNumber.new(other), self]
16
16
  end
@@ -20,22 +20,20 @@ module Mint
20
20
  # @private
21
21
  class CoercedNumber
22
22
  # @private
23
- def initialize(value)
24
- @value = value
25
- end
23
+ def initialize(value) = @value = value
26
24
 
27
25
  # @private
28
26
  def +(other)
29
- return other if @value.zero?
27
+ raise_coercion_error(:+, other) unless @value.zero?
30
28
 
31
- raise_coercion_error(:+, other)
29
+ other
32
30
  end
33
31
 
34
32
  # @private
35
33
  def -(other)
36
- return -other if @value.zero?
34
+ raise_coercion_error(:-, other) unless @value.zero?
37
35
 
38
- raise_coercion_error(:-, other)
36
+ -other
39
37
  end
40
38
 
41
39
  # @private
@@ -59,8 +57,7 @@ module Mint
59
57
  private
60
58
 
61
59
  def raise_coercion_error(operation, operand)
62
- raise TypeError,
63
- "#{@value} #{operation} #{operand} : incompatible operands"
60
+ raise TypeError, "#{@value} #{operation} #{operand} : incompatible operands"
64
61
  end
65
62
  end
66
63
  private_constant :CoercedNumber
@@ -45,10 +45,10 @@ module Mint
45
45
  #
46
46
  def to_s(format: '%<symbol>s%<amount>f', decimal: '.', thousand: ',', width: nil)
47
47
  case format
48
- when {}, '', nil then raise ArgumentError, 'format must not be empty or null'
49
- when Hash then validate_format_hash!(format)
50
- when String # noop
51
- else raise ArgumentError, 'Invalid format'
48
+ when {}, '' then raise ArgumentError, 'format must not be empty'
49
+ when Hash then validate_format_hash(format)
50
+ when String then format = { positive: format }
51
+ else raise ArgumentError, 'Invalid format. Only String or Hash are accepted'
52
52
  end
53
53
 
54
54
  formatted = format_amount(format)
@@ -61,44 +61,39 @@ module Mint
61
61
  formatted.gsub!(/(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/, "\\1#{thousand}")
62
62
  end
63
63
 
64
- formatted = formatted.rjust(width) if width
65
- formatted
64
+ width ? formatted.rjust(width) : formatted
66
65
  end
67
66
 
68
67
  private
69
68
 
70
- def validate_format_hash!(format)
71
- unknown = format.keys - %i[positive negative zero]
72
-
73
- raise ArgumentError, "Unknown format parameter(s): #{unknown.inspect}. " unless unknown.empty?
74
- end
75
-
76
- def format_amount(format)
77
- format = { positive: format } if format.is_a?(String)
78
- positive_format = format[:positive]
69
+ def select_format(format)
79
70
  negative_format = format[:negative]
80
71
  zero_format = format[:zero]
81
72
 
82
73
  if amount.negative? && negative_format
83
- format = negative_format
84
- value = -amount
74
+ [negative_format, -amount]
85
75
  elsif amount.zero? && zero_format
86
- format = zero_format
87
- value = amount
76
+ [zero_format, amount]
88
77
  else
89
- format = positive_format
90
- value = amount
78
+ [format[:positive], amount]
91
79
  end
92
- format ||= '%<symbol>s%<amount>f'
80
+ end
93
81
 
82
+ def validate_format_hash(format)
83
+ unknown = format.keys - %i[positive negative zero]
84
+
85
+ raise ArgumentError, "Unknown format parameter(s): #{unknown.inspect}. " unless unknown.empty?
86
+ end
87
+
88
+ def format_amount(format)
89
+ format, value = select_format(format)
90
+ format ||= '%<symbol>s%<amount>f'
94
91
  # Automatically adjust decimal places based on currency subunit if missing
95
- adjusted_format = format
96
- .gsub(/%<amount>(\s*\+?\d*)f/, "%<amount>\\1.#{currency.subunit}f")
92
+ format = format.gsub(/%<amount>(\s*\+?\d*)f/, "%<amount>\\1.#{currency.subunit}f")
97
93
 
98
- Kernel.format(adjusted_format,
99
- amount: value,
100
- currency: currency_code,
101
- symbol: currency.symbol)
94
+ refs = format.scan(/%<(\w+)>/).flatten.map(&:to_sym)
95
+ all_args = { amount: value, currency: currency_code, symbol: currency.symbol }
96
+ Kernel.format(format, **all_args.slice(*refs))
102
97
  end
103
98
  end
104
99
  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.6.3'
6
+ VERSION = '1.7.0'
7
7
  end
data/minting.gemspec CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  lib = File.expand_path('lib', __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require 'minting/version'
@@ -15,18 +17,19 @@ Gem::Specification.new do |s|
15
17
  # Prevent pushing this gem to RubyGems.org.
16
18
  # To allow pushes either set the 'allowed_push_host' to allow pushing to
17
19
  # a single host or delete this section to allow pushing to any host.
18
- raise 'RubyGems 3.2 or newer is required' unless s.respond_to?(:metadata)
20
+ raise 'RubyGems 3.3 or newer is required' unless s.respond_to?(:metadata)
19
21
 
20
22
  s.metadata = {
21
23
  'bug_tracker_uri' => "#{s.homepage}/issues",
22
24
  'changelog_uri' => "#{s.homepage}/blob/master/CHANGELOG.md",
25
+ 'documentation_uri' => 'https://www.rubydoc.info/gems/minting',
23
26
  'homepage_uri' => s.homepage,
24
27
  'source_code_uri' => s.homepage,
25
28
  'allowed_push_host' => 'https://rubygems.org',
26
29
  'rubygems_mfa_required' => 'true'
27
30
  }
28
31
 
29
- s.required_ruby_version = '>= 3.2.0'
32
+ s.required_ruby_version = '>= 3.3.0'
30
33
  s.add_dependency 'bigdecimal', '>= 4.0'
31
34
 
32
35
  s.files = Dir.glob('{bin,doc,lib}/**/*')
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minting
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.3
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gilson Ferraz
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-06-12 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: bigdecimal
@@ -35,11 +36,31 @@ files:
35
36
  - bin/check-currencies
36
37
  - bin/console
37
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/UnknownCurrency.html
44
+ - doc/Minting.html
45
+ - doc/_index.html
38
46
  - doc/agents/AGENTS.md
39
47
  - doc/agents/copilot-instructions.md
40
48
  - doc/agents/gemini_gem_evaluation.md
41
49
  - doc/agents/recommendations.md
42
50
  - doc/agents/rubocop-issues.md
51
+ - doc/class_list.html
52
+ - doc/css/common.css
53
+ - doc/css/full_list.css
54
+ - doc/css/style.css
55
+ - doc/file.README.html
56
+ - doc/file_list.html
57
+ - doc/frames.html
58
+ - doc/index.html
59
+ - doc/js/app.js
60
+ - doc/js/full_list.js
61
+ - doc/js/jquery.js
62
+ - doc/method_list.html
63
+ - doc/top-level-namespace.html
43
64
  - lib/minting.rb
44
65
  - lib/minting/data/world-currencies.yaml
45
66
  - lib/minting/mint.rb
@@ -47,7 +68,9 @@ files:
47
68
  - lib/minting/mint/currency/currency.rb
48
69
  - lib/minting/mint/currency/currency_registry.rb
49
70
  - lib/minting/mint/currency/world_currencies.rb
50
- - lib/minting/mint/dsl.rb
71
+ - lib/minting/mint/dsl/range.rb
72
+ - lib/minting/mint/dsl/refinements.rb
73
+ - lib/minting/mint/dsl/top_level.rb
51
74
  - lib/minting/mint/mint.rb
52
75
  - lib/minting/mint/parser.rb
53
76
  - lib/minting/money/allocation.rb
@@ -66,10 +89,12 @@ licenses:
66
89
  metadata:
67
90
  bug_tracker_uri: https://github.com/gferraz/minting/issues
68
91
  changelog_uri: https://github.com/gferraz/minting/blob/master/CHANGELOG.md
92
+ documentation_uri: https://www.rubydoc.info/gems/minting
69
93
  homepage_uri: https://github.com/gferraz/minting
70
94
  source_code_uri: https://github.com/gferraz/minting
71
95
  allowed_push_host: https://rubygems.org
72
96
  rubygems_mfa_required: 'true'
97
+ post_install_message:
73
98
  rdoc_options: []
74
99
  require_paths:
75
100
  - lib
@@ -77,14 +102,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
77
102
  requirements:
78
103
  - - ">="
79
104
  - !ruby/object:Gem::Version
80
- version: 3.2.0
105
+ version: 3.3.0
81
106
  required_rubygems_version: !ruby/object:Gem::Requirement
82
107
  requirements:
83
108
  - - ">="
84
109
  - !ruby/object:Gem::Version
85
110
  version: '0'
86
111
  requirements: []
87
- rubygems_version: 4.0.9
112
+ rubygems_version: 3.5.22
113
+ signing_key:
88
114
  specification_version: 4
89
115
  summary: Library to manipulate currency values
90
116
  test_files: []