minting 1.4.0 → 1.5.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.
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mint
4
+ # Money Arithmetics
2
5
  class Money
3
6
  # Returns the absolute value of the monetary amount as a new {Money} instance.
4
7
  #
@@ -27,9 +30,10 @@ module Mint
27
30
  # @return [Money] the sum of the addition
28
31
  # @raise [TypeError] if addition involves a different currency or incompatible types
29
32
  def +(addend)
30
- return self if addend.respond_to?(:zero?) && addend.zero?
31
- return mint(amount + addend.amount) if addend.is_a?(Money) && same_currency?(addend)
32
-
33
+ case addend
34
+ when 0 then return self
35
+ when Money then return mint(amount + addend.amount) if same_currency?(addend)
36
+ end
33
37
  raise TypeError, "#{addend} can't be added to #{self}"
34
38
  end
35
39
 
@@ -39,11 +43,10 @@ module Mint
39
43
  # @return [Money] the difference of the subtraction
40
44
  # @raise [TypeError] if subtraction involves a different currency or incompatible types
41
45
  def -(subtrahend)
42
- return self if subtrahend.respond_to?(:zero?) && subtrahend.zero?
43
- if subtrahend.is_a?(Money) && same_currency?(subtrahend)
44
- return mint(amount - subtrahend.amount)
46
+ case subtrahend
47
+ when 0 then return self
48
+ when Money then return mint(amount - subtrahend.amount) if same_currency?(subtrahend)
45
49
  end
46
-
47
50
  raise TypeError, "#{subtrahend} can't be subtracted from #{self}"
48
51
  end
49
52
 
@@ -72,10 +75,22 @@ module Mint
72
75
  # @raise [TypeError] if divisor is of incompatible type or different currency
73
76
  # @raise [ZeroDivisionError] if division by zero is attempted
74
77
  def /(divisor)
75
- return mint(amount / divisor) if divisor.is_a?(Numeric)
76
- return amount / divisor.amount if same_currency? divisor
77
-
78
+ case divisor
79
+ when Numeric then return mint(amount / divisor)
80
+ when Money then return amount / divisor.amount if same_currency? divisor
81
+ end
78
82
  raise TypeError, "#{self} can't be divided by #{divisor}"
79
83
  end
84
+
85
+ # Performs exponentiation of the monetary value by a standard scalar Numeric.
86
+ #
87
+ # @param exponent [Numeric]
88
+ # @return [Money] reult of amount ** exponent
89
+ # @raise [TypeError] if exponent is not Numeric
90
+ def **(exponent)
91
+ return mint(amount**exponent) if exponent.is_a?(Numeric)
92
+
93
+ raise TypeError, "#{self} can't be powered by #{exponent}"
94
+ end
80
95
  end
81
96
  end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mint
4
+ # Implements the standard Ruby coercion protocol.
2
5
  class Money
3
- # Implements the standard Ruby coercion protocol.
4
6
  # Allows {Money} to interact seamlessly as the right-hand operand in Numeric arithmetic.
5
7
  #
6
8
  # @param other [Numeric] the left-hand operand to coerce
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mint
2
- # :nodoc
3
4
  # Comparison methods
4
5
  class Money
5
6
  include Comparable
@@ -28,12 +29,10 @@ module Mint
28
29
  #
29
30
  def <=>(other)
30
31
  case other
31
- when Numeric
32
- return amount <=> other if other.zero?
33
- when Mint::Money
34
- return amount <=> other.amount if currency == other.currency
32
+ in 0 then amount <=> other
33
+ in Mint::Money if same_currency?(other) then amount <=> other.amount
34
+ else raise TypeError, "#{inspect} can't be compared to #{other.inspect}"
35
35
  end
36
- raise TypeError, "#{inspect} can't be compared to #{other.inspect}"
37
36
  end
38
37
 
39
38
  def nonzero? = amount.nonzero?
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # Money constructors
5
+ class Money
6
+ # Creates a new Money immutable object with the specified amount and currency
7
+ # @param amount [Numeric] The monetary amount
8
+ # @param currency [Currency] The currency object
9
+ # @raise [ArgumentError] If amount is not numeric or currency is invalid
10
+ def self.create(amount, currency)
11
+ raise ArgumentError, 'amount must be Numeric' unless amount.is_a?(Numeric)
12
+
13
+ checked_currency = Mint.currency(currency)
14
+ raise ArgumentError, "Currency not found (#{currency})" unless checked_currency
15
+
16
+ new(checked_currency.normalize_amount(amount), checked_currency)
17
+ end
18
+
19
+ # Builds a Money from a fractional (smallest-unit) Integer amount.
20
+ # This is the inverse of {#fractional}: for USD, the fractional unit is
21
+ # 1 cent; for JPY it is 1 yen; for IQD it is 1 dinar (subunit 3).
22
+ #
23
+ # @param fractional [Integer] the amount expressed in the currency's
24
+ # smallest unit (e.g. cents). Must be an Integer to preserve exactness.
25
+ # @param currency [String, Symbol, Currency] the currency identifier
26
+ # @return [Money] the resulting Money instance
27
+ # @raise [ArgumentError] if +fractional+ is not an Integer or +currency+
28
+ # is not registered
29
+ #
30
+ # @example USD cents
31
+ # Money.from_fractional(123_456, 'USD') #=> [USD 1234.56]
32
+ # @example JPY (subunit 0)
33
+ # Money.from_fractional(1234, 'JPY') #=> [JPY 1234]
34
+ # @example Round trip
35
+ # m = Mint.money(9.99, 'USD')
36
+ # Money.from_fractional(m.fractional, 'USD') == m #=> true
37
+ def self.from_fractional(fractional, currency)
38
+ raise ArgumentError, 'fractional must be an Integer' unless fractional.is_a?(Integer)
39
+
40
+ checked_currency = Mint.currency(currency)
41
+ raise ArgumentError, "Currency not found (#{currency})" unless checked_currency
42
+
43
+ amount = Rational(fractional, checked_currency.fractional_multiplier)
44
+ new(amount, checked_currency)
45
+ end
46
+
47
+ # Returns a new Money object with the specified amount, or self if unchanged
48
+ # @param new_amount [Numeric] The new amount
49
+ # @return [Money] A new Money object or self
50
+ def mint(new_amount)
51
+ new_amount = currency.normalize_amount(new_amount)
52
+ new_amount == amount ? self : Money.new(new_amount, currency)
53
+ end
54
+
55
+ private
56
+
57
+ # Initializes a new Money object with the given amount and currency.
58
+ # @param amount [Numeric] The monetary amount
59
+ # @param currency [Currency] The currency object
60
+ def initialize(amount, currency)
61
+ @amount = amount
62
+ @currency = currency
63
+ freeze
64
+ end
65
+ end
66
+ end
@@ -1,9 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bigdecimal'
1
4
  require 'erb'
2
5
 
3
6
  module Mint
4
7
  # Conversion and serialization logic for {Money} instances.
5
8
  class Money
6
- # Converts the monetary amount to a {BigDecimal} object.
9
+ # Converts the monetary amount to a BigDecimal object.
7
10
  #
8
11
  # @return [BigDecimal] the decimal representation of the money amount
9
12
  def to_d = amount.to_d 0
@@ -1,9 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mint
2
4
  # Formatting functionality for Money objects
3
5
  class Money
4
- # Keys accepted in the per-sign Hash form of `to_s(format:)`.
5
- SIGN_FORMAT_KEYS = %i[positive negative zero].freeze
6
-
7
6
  # Formats money as a string with customizable format, thousand delimiter, and decimal
8
7
  #
9
8
  # @param format [String, Hash] Either a Format string with placeholders
@@ -45,9 +44,12 @@ module Mint
45
44
  # money.to_s(format: '%<symbol>s%<amount>010.2f') #=> "$0001234.56"
46
45
  #
47
46
  def to_s(format: '%<symbol>s%<amount>f', decimal: '.', thousand: ',', width: nil)
48
- raise ArgumentError, 'Invalid format' unless format.is_a?(String) || format.is_a?(Hash)
49
-
50
- validate_format_hash!(format) if format.is_a?(Hash)
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'
52
+ end
51
53
 
52
54
  formatted = format_amount(format)
53
55
 
@@ -66,27 +68,26 @@ module Mint
66
68
  private
67
69
 
68
70
  def validate_format_hash!(format)
69
- raise ArgumentError, 'format Hash must not be empty' if format.empty?
70
-
71
- unknown = format.keys - SIGN_FORMAT_KEYS
72
- return if unknown.empty?
71
+ unknown = format.keys - %i[positive negative zero]
73
72
 
74
- raise ArgumentError,
75
- "Unknown format Hash key(s): #{unknown.inspect}. " \
76
- "Valid keys are #{SIGN_FORMAT_KEYS.inspect}"
73
+ raise ArgumentError, "Unknown format parameter(s): #{unknown.inspect}. " unless unknown.empty?
77
74
  end
78
75
 
79
76
  def format_amount(format)
80
77
  format = { positive: format } if format.is_a?(String)
81
- value = amount
78
+ positive_format = format[:positive]
79
+ negative_format = format[:negative]
80
+ zero_format = format[:zero]
82
81
 
83
- if amount.negative? && format[:negative]
84
- format = format[:negative]
82
+ if amount.negative? && negative_format
83
+ format = negative_format
85
84
  value = -amount
86
- elsif amount.zero? && format[:zero]
87
- format = format[:zero]
85
+ elsif amount.zero? && zero_format
86
+ format = zero_format
87
+ value = amount
88
88
  else
89
- format = format[:positive]
89
+ format = positive_format
90
+ value = amount
90
91
  end
91
92
  format ||= '%<symbol>s%<amount>f'
92
93
 
@@ -1,52 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mint
4
+ # Money constructors
2
5
  class Money
3
6
  # The default display format pattern for formatting monetary values.
4
7
  # Uses `%<symbol>s` for the currency symbol and `%<amount>f` for the rounded amount.
5
- DEFAULT_FORMAT = '%<symbol>s%<amount>f'.freeze
8
+ DEFAULT_FORMAT = '%<symbol>s%<amount>f'
6
9
 
7
10
  attr_reader :amount, :currency
8
11
 
9
- # Creates a new Money immutable object with the specified amount and currency
10
- # @param amount [Numeric] The monetary amount
11
- # @param currency [Currency] The currency object
12
- # @raise [ArgumentError] If amount is not numeric or currency is invalid
13
- def self.create(amount, currency)
14
- raise ArgumentError, 'amount must be Numeric' unless amount.is_a?(Numeric)
15
-
16
- checked_currency = Mint.currency(currency)
17
- raise ArgumentError, "Currency not found (#{currency})" unless checked_currency
18
-
19
- new(checked_currency.normalize_amount(amount), checked_currency)
20
- end
21
-
22
- # Builds a Money from a fractional (smallest-unit) Integer amount.
23
- # This is the inverse of {#fractional}: for USD, the fractional unit is
24
- # 1 cent; for JPY it is 1 yen; for IQD it is 1 dinar (subunit 3).
25
- #
26
- # @param fractional [Integer] the amount expressed in the currency's
27
- # smallest unit (e.g. cents). Must be an Integer to preserve exactness.
28
- # @param currency [String, Symbol, Currency] the currency identifier
29
- # @return [Money] the resulting Money instance
30
- # @raise [ArgumentError] if +fractional+ is not an Integer or +currency+
31
- # is not registered
32
- #
33
- # @example USD cents
34
- # Money.from_fractional(123_456, 'USD') #=> [USD 1234.56]
35
- # @example JPY (subunit 0)
36
- # Money.from_fractional(1234, 'JPY') #=> [JPY 1234]
37
- # @example Round trip
38
- # m = Mint.money(9.99, 'USD')
39
- # Money.from_fractional(m.fractional, 'USD') == m #=> true
40
- def self.from_fractional(fractional, currency)
41
- raise ArgumentError, 'fractional must be an Integer' unless fractional.is_a?(Integer)
42
-
43
- checked_currency = Mint.currency(currency)
44
- raise ArgumentError, "Currency not found (#{currency})" unless checked_currency
45
-
46
- amount = Rational(fractional, checked_currency.fractional_multiplier)
47
- new(amount, checked_currency)
48
- end
49
-
50
12
  # Returns the ISO 3-letter currency code string.
51
13
  #
52
14
  # @return [String] the ISO currency code
@@ -59,14 +21,6 @@ module Mint
59
21
  # @return [Integer] the calculated hash value
60
22
  def hash = [amount, currency_code].hash
61
23
 
62
- # Returns a new Money object with the specified amount, or self if unchanged
63
- # @param new_amount [Numeric] The new amount
64
- # @return [Money] A new Money object or self
65
- def mint(new_amount)
66
- new_amount = currency.normalize_amount(new_amount)
67
- new_amount == amount ? self : Money.new(new_amount, currency)
68
- end
69
-
70
24
  # Returns a standard developer-oriented string inspection of the Money object.
71
25
  #
72
26
  # @return [String] the formatted inspect representation
@@ -76,13 +30,17 @@ module Mint
76
30
 
77
31
  # Helper method to verify if another object has the identical currency.
78
32
  #
79
- # @param other [Object] the target object to compare
33
+ # @param other [Currency] the target currency to compare
80
34
  # @return [Boolean] true if currencies match, false otherwise
81
- def same_currency?(other) = other.respond_to?(:currency) && other.currency == currency
35
+ def same_currency?(other) = other.currency == currency
82
36
 
83
37
  # Constrains +self+ to the inclusive range [+min+, +max+].
84
38
  #
85
- # Both bounds may be either a same-currency {Money} or a {Numeric}. A
39
+ # Bounds may be:
40
+ # - nil meaning no boundary
41
+ # - same-currency {Money} or Range
42
+ # - Numeric amount, or Range
43
+ #
86
44
  # Numeric is interpreted as an amount in +self+'s currency, so the common
87
45
  # pricing idiom +price.clamp(0, 100)+ reads as "0 to 100 in the same
88
46
  # currency as +price+".
@@ -91,11 +49,12 @@ module Mint
91
49
  # allocated). When out of range, the nearest bound is returned as a new
92
50
  # frozen {Money} in +self+'s currency.
93
51
  #
94
- # @param min [Money, Numeric] lower bound (inclusive)
95
- # @param max [Money, Numeric] upper bound (inclusive)
52
+ # @param min_or_range [Money, Numeric, Range, nil] lower bound (inclusive), or range
53
+ # @param max [Money, Numeric, nil] upper bound (inclusive)
96
54
  # @return [Money] +self+ if in range, otherwise the nearer bound
97
- # @raise [ArgumentError] if +min+ or +max+ is not a Money or Numeric; if
98
- # a Money operand has a different currency; if +min+ > +max+
55
+ # @raise [ArgumentError] if +min+ or +max+ is not a Money, Numeric or nil; if
56
+ # a Money operand has a different currency; if +min+ > +max+;
57
+ # if min is a Range, and max is not nil
99
58
  #
100
59
  # @example In range
101
60
  # Mint.money(5, 'USD').clamp(0, 10) #=> [USD 5.00] (returns self)
@@ -111,37 +70,26 @@ module Mint
111
70
  #
112
71
  # @example Subunit-0 currency (JPY)
113
72
  # Mint.money(500, 'JPY').clamp(0, 100) #=> [JPY 100]
114
- def clamp(min, max)
115
- case min
116
- when Numeric
117
- when Money
118
- raise(ArgumentError, "min currency must be: #{currency_code}") unless same_currency?(min)
73
+ def clamp(min_or_range, max = nil)
74
+ if min_or_range.is_a?(Range)
75
+ raise(ArgumentError, "Either amount range alone or two amounts accepted: #{max}") if max
119
76
 
120
- min = min.amount
121
- else raise(ArgumentError, 'min must be Numeric or Money')
77
+ min, max = min_or_range.minmax
78
+ else
79
+ min = min_or_range
122
80
  end
123
-
124
- case max
125
- when Numeric
126
- when Money
127
- raise(ArgumentError, "max currency must be: #{currency_code}") unless same_currency?(max)
128
-
129
- max = max.amount
130
- else raise(ArgumentError, 'max must be Numeric or Money')
131
- end
132
-
133
- mint(amount.clamp(min, max))
81
+ mint(amount.clamp(normalize_boundary(min), normalize_boundary(max)))
134
82
  end
135
83
 
136
84
  private
137
85
 
138
- # Initializes a new Money object with the given amount and currency.
139
- # @param amount [Numeric] The monetary amount
140
- # @param currency [Currency] The currency object
141
- def initialize(amount, currency)
142
- @amount = amount
143
- @currency = currency
144
- freeze
86
+ def normalize_boundary(boundary)
87
+ case boundary
88
+ in NilClass | Numeric then boundary
89
+ in Money if same_currency?(boundary) then boundary.amount
90
+ in Money then raise ArgumentError, "oundary currency must be: #{currency_code}"
91
+ else raise ArgumentError, "Boundary must be Numeric or Money #{boundary}"
92
+ end
145
93
  end
146
94
  end
147
95
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mint
2
- # nodoc
4
+ # Money parser
3
5
  class Money
4
6
  # Parses a human-readable money string into a {Money} object.
5
7
  #
@@ -22,7 +24,7 @@ module Mint
22
24
  input = input.strip
23
25
  raise ArgumentError, 'input cannot be empty' if input.empty?
24
26
 
25
- currency = currency ? Mint.currency(currency) : parse_currency(input)
27
+ currency = parse_currency(currency) || parse_currency(input)
26
28
  raise ArgumentError, "Currency [#{currency}] not registered" unless currency
27
29
 
28
30
  amount = currency.normalize_amount(parse_amount(input))
@@ -40,10 +42,8 @@ module Mint
40
42
  # Converts locale-specific decimal/thousand separators into a plain decimal string.
41
43
  def self.normalize_separators(numeric)
42
44
  case [numeric.count(','), numeric.count('.')]
43
- in [0, 0] | [0, 1] # Nothing to normalize (e.g. "1500" or "34.21").
44
- numeric
45
- in [1, 0] # Only one comma: decimal (e.g. 19,99 or 1,234).
46
- numeric.tr(',', '.')
45
+ in [0, 0] | [0, 1] then numeric # Nothing to normalize (e.g. "1500" or "34.21").
46
+ in [1, 0] then numeric.tr(',', '.') # Only one comma: decimal (e.g. 19,99 or 1,234).
47
47
  in [c, p] if c > 1 && p > 1 # Both separators appear multiple times
48
48
  raise ArgumentError, "could not distinguish decimal and thousand separators in '#{numeric}'"
49
49
  in [c, p] if c > 0 && p > 0 # Commas and dots: the rightmost one is the decimal separator.
@@ -58,22 +58,19 @@ module Mint
58
58
  end
59
59
 
60
60
  def self.parse_currency(input)
61
- # Prefer an explicit ISO 4217 code (e.g. "USD 1,234.56") over symbol matching.
62
- code = input[/\b([A-Z]{3})\b/, 1]
63
- if code
64
- currency = Mint.currency(code)
61
+ case input
62
+ when NilClass, Mint::Currency then return input
63
+ when String
64
+ # Prefer an explicit ISO 4217 code (e.g. "USD 1,234.56") over symbol matching.
65
+ currency = Mint.currency(input[/\b([A-Z]+)\b/, 1])
65
66
  return currency if currency
66
- end
67
-
68
- # Fall back to registered symbols, longest first (HK$ before $).
69
- Mint.currency_symbols.each do |symbol, currency|
70
- next if symbol.empty?
71
67
 
72
- return currency if input.include?(symbol)
68
+ # Fall back to registered symbols, longest first (HK$ before $).
69
+ Mint.currency_symbols.each do |symbol, currency|
70
+ return currency if input.include?(symbol)
71
+ end
73
72
  end
74
-
75
- raise ArgumentError,
76
- 'currency could not be detected; pass a currency code as the second argument'
73
+ raise ArgumentError, 'currency could not be detected; pass a currency code as the second argument'
77
74
  end
78
75
 
79
76
  private_class_method :parse_amount, :normalize_separators,
data/lib/minting/money.rb CHANGED
@@ -1,8 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'minting/money/parse'
2
4
  require 'minting/money/allocation'
3
5
  require 'minting/money/arithmetics'
4
6
  require 'minting/money/coercion'
5
7
  require 'minting/money/comparable'
8
+ require 'minting/money/constructors'
6
9
  require 'minting/money/conversion'
7
10
  require 'minting/money/formatting'
8
11
  require 'minting/money/money'
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Root namespace for the Minting library.
2
4
  module Minting
3
5
  # Current version of the Minting gem.
4
- VERSION = '1.4.0'.freeze
6
+ VERSION = '1.5.0'
5
7
  end
data/lib/minting.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'minting/mint'
2
4
  require 'minting/money'
3
5
  require 'minting/version'
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.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gilson Ferraz
@@ -34,32 +34,16 @@ files:
34
34
  - Rakefile
35
35
  - bin/console
36
36
  - bin/setup
37
- - doc/Mint.html
38
- - doc/Mint/Currency.html
39
- - doc/Mint/Money.html
40
- - doc/Minting.html
41
- - doc/_index.html
42
37
  - doc/agents/AGENTS.md
43
38
  - doc/agents/copilot-instructions.md
44
39
  - doc/agents/gemini_gem_evaluation.md
45
40
  - doc/agents/recommendations.md
46
- - doc/class_list.html
47
- - doc/css/common.css
48
- - doc/css/full_list.css
49
- - doc/css/style.css
50
- - doc/file.README.html
51
- - doc/file_list.html
52
- - doc/frames.html
53
- - doc/index.html
54
- - doc/js/app.js
55
- - doc/js/full_list.js
56
- - doc/js/jquery.js
57
- - doc/method_list.html
58
- - doc/top-level-namespace.html
41
+ - doc/agents/rubocop-issues.md
59
42
  - lib/minting.rb
60
43
  - lib/minting/data/currencies.yaml
61
44
  - lib/minting/mint.rb
62
45
  - lib/minting/mint/currency.rb
46
+ - lib/minting/mint/currency_store.rb
63
47
  - lib/minting/mint/refinements.rb
64
48
  - lib/minting/mint/registry.rb
65
49
  - lib/minting/money.rb
@@ -67,6 +51,7 @@ files:
67
51
  - lib/minting/money/arithmetics.rb
68
52
  - lib/minting/money/coercion.rb
69
53
  - lib/minting/money/comparable.rb
54
+ - lib/minting/money/constructors.rb
70
55
  - lib/minting/money/conversion.rb
71
56
  - lib/minting/money/formatting.rb
72
57
  - lib/minting/money/money.rb