minting 1.1.0 → 1.1.1

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.
@@ -3,7 +3,7 @@ module Mint
3
3
  #
4
4
  # @see https://www.iso.org/iso-4217-currency-codes.html
5
5
  class Currency
6
- attr_reader :code, :subunit, :symbol, :priority, :minimum_amount
6
+ attr_reader :code, :subunit, :symbol, :priority, :minimum_amount, :country, :name
7
7
 
8
8
  def inspect
9
9
  "<Currency:(#{code} #{symbol} #{subunit})>"
@@ -11,12 +11,14 @@ module Mint
11
11
 
12
12
  private
13
13
 
14
- def initialize(code, subunit:, symbol:, priority: 0)
14
+ def initialize(code:, symbol:, subunit: 0, priority: 0, country: nil, name: nil)
15
15
  @code = code.to_s
16
16
  @subunit = subunit.to_i
17
- @symbol = symbol.to_s
17
+ @symbol = symbol
18
18
  @priority = priority.to_i
19
- @minimum_amount = 10r**-subunit
19
+ @country = country
20
+ @name = name
21
+ @minimum_amount = 10r**-@subunit
20
22
  freeze
21
23
  end
22
24
  end
@@ -1,7 +1,12 @@
1
1
  require 'yaml'
2
2
 
3
- # :nodoc
4
3
  module Mint
4
+ # Creates a new {Money} instance with the given amount and currency code.
5
+ #
6
+ # @param amount [Numeric] the financial value
7
+ # @param currency_code [String, Symbol] the ISO currency code or symbol
8
+ # @return [Money] the instantiated Money object
9
+ # @raise [ArgumentError] if the currency code is not registered
5
10
  def self.money(amount, currency_code)
6
11
  currency = currency(currency_code)
7
12
  return Money.new(amount, currency) if currency
@@ -9,6 +14,11 @@ module Mint
9
14
  raise ArgumentError, "Currency [#{currency_code}] not registered. Check Mint.currencies"
10
15
  end
11
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, Symbol, Currency] the currency identifier or object
21
+ # @return [Currency, nil] the registered Currency instance or nil if not found
12
22
  def self.currency(currency)
13
23
  case currency
14
24
  when Currency
@@ -20,13 +30,29 @@ module Mint
20
30
  end
21
31
  end
22
32
 
23
- def self.register_currency(code, subunit: 2, symbol: '$', priority: 0)
33
+ # Registers a new currency if not already registered.
34
+ #
35
+ # @param code [String, Symbol] the unique currency code (e.g. 'USD', :EUR)
36
+ # @param subunit [Integer] the decimal subunit precision (defaults to 2)
37
+ # @param symbol [String] the display symbol (defaults to '$')
38
+ # @param priority [Integer] parser precedence priority (defaults to 0)
39
+ # @return [Currency] the registered or existing Currency instance
40
+ # @raise [ArgumentError] if the code layout is invalid or register throws an error
41
+ def self.register_currency(code:, subunit: 2, symbol: '$', priority: 0)
24
42
  code = code.to_s
25
- currencies[code] || register_currency!(code, subunit: subunit, symbol: symbol,
26
- priority: priority)
43
+ currencies[code] || register_currency!(code:, subunit:, symbol:, priority:)
27
44
  end
28
45
 
29
- def self.register_currency!(code, subunit:, symbol: '', priority: 0)
46
+ # Strictly registers a new currency, raising a KeyError if already registered.
47
+ #
48
+ # @param code [String, Symbol] the unique currency code
49
+ # @param subunit [Integer] the decimal subunit precision
50
+ # @param symbol [String] the display symbol
51
+ # @param priority [Integer] parser precedence priority
52
+ # @return [Currency] the newly registered Currency instance
53
+ # @raise [ArgumentError] if the code contains invalid characters
54
+ # @raise [KeyError] if the currency code is already registered
55
+ def self.register_currency!(code:, subunit:, symbol: '', priority: 0)
30
56
  code = code.to_s
31
57
  unless code.match?(/^[A-Z_]+$/)
32
58
  raise ArgumentError,
@@ -37,14 +63,19 @@ module Mint
37
63
  "Currency: #{code} already registered"
38
64
  end
39
65
 
40
- currencies[code] =
41
- Currency.new(code, subunit: subunit, symbol: symbol, priority: priority)
66
+ currencies[code] = Currency.new(code:, subunit:, symbol:, priority:)
42
67
  @currency_symbols = nil
43
68
  currencies[code]
44
69
  end
45
70
 
71
+ # Returns the hash of all registered currencies.
72
+ #
73
+ # @return [Hash{String => Currency}] registered currencies mapped by code
46
74
  def self.currencies
47
- @currencies ||= load_currencies
75
+ @currencies ||= begin
76
+ registry = { 'XXX' => Currency.new(code: 'XXX', name: 'No currency', symbol: '¤') }
77
+ load_currencies(registry)
78
+ end
48
79
  end
49
80
 
50
81
  # Registered symbols sorted for detection: longest match wins, then parser priority.
@@ -57,16 +88,23 @@ module Mint
57
88
  end.freeze
58
89
  end
59
90
 
60
- def self.load_currencies
61
- path = File.expand_path('../data/currencies.yaml', __dir__)
62
- YAML.load_file(path).each_with_object({}) do |(code, attrs), registry|
91
+ def self.load_currencies(registry)
92
+ base = File.expand_path('../data', __dir__)
93
+ path = File.join(base, 'currencies.yaml')
94
+
95
+ data = YAML.load_file(path)
96
+ data.each do |entry|
97
+ code = entry['code']
63
98
  registry[code] = Currency.new(
64
- code,
65
- subunit: attrs['subunit'],
66
- symbol: attrs['symbol'],
67
- priority: attrs['priority']
99
+ code: code,
100
+ subunit: entry['subunit'],
101
+ symbol: entry['symbol'],
102
+ priority: entry['priority'],
103
+ country: entry['country'],
104
+ name: entry['name']
68
105
  )
69
106
  end
107
+ registry
70
108
  end
71
109
 
72
110
  private_class_method :load_currencies
@@ -1,7 +1,14 @@
1
1
  module Mint
2
- # :nodoc
3
- # split and allocation methods
4
2
  class Money
3
+ # Proportionally allocates the monetary amount among a list of ratios.
4
+ # Disperses any subunit rounding amounts across the initial slots
5
+ # @param proportions [Array<Numeric>] a list of numeric proportions/ratios to allocate by
6
+ # @return [Array<Money>] the list of newly allocated Money objects
7
+ # @raise [ArgumentError] if the proportions list is empty or sums to zero
8
+ #
9
+ # @example Proportional allocation
10
+ # money = Mint.money(10.00, 'USD')
11
+ # money.allocate([1, 2, 3]) #=> [[USD 1.67], [USD 3.33], [USD 5.00]]
5
12
  def allocate(proportions)
6
13
  whole = proportions.sum.to_r
7
14
  raise ArgumentError, 'Need at least 1 proportion element' if proportions.empty?
@@ -11,6 +18,17 @@ module Mint
11
18
  allocate_left_over!(amounts: amounts, left_over: amount - amounts.sum)
12
19
  end
13
20
 
21
+ # Splits the monetary amount into a given quantity of equal parts.
22
+ # Disperses any fractional subunit rounding differences across the initial slots
23
+ # so that the sum is preserved.
24
+ #
25
+ # @param quantity [Integer] the number of equal parts to divide the money into (must be > 0)
26
+ # @return [Array<Money>] the list of newly split Money objects
27
+ # @raise [ArgumentError] if quantity is not a positive integer
28
+ #
29
+ # @example Even split
30
+ # money = Mint.money(10.00, 'USD')
31
+ # money.split(3) #=> [[USD 3.34], [USD 3.33], [USD 3.33]]
14
32
  def split(quantity)
15
33
  unless quantity.positive? && quantity.integer?
16
34
  raise ArgumentError,
@@ -1,15 +1,31 @@
1
1
  module Mint
2
- # :nodoc
3
- # Arithmetic functions for money objects
4
2
  class Money
3
+ # Returns the absolute value of the monetary amount as a new {Money} instance.
4
+ #
5
+ # @return [Money] the absolute value
5
6
  def abs = mint(amount.abs)
6
7
 
8
+ # Returns true if the monetary amount is less than zero.
9
+ #
10
+ # @return [Boolean] true if negative, false otherwise
7
11
  def negative? = amount.negative?
8
12
 
13
+ # Returns true if the monetary amount is greater than zero.
14
+ #
15
+ # @return [Boolean] true if positive, false otherwise
9
16
  def positive? = amount.positive?
10
17
 
18
+ # Returns the successor of the Money instance by adding the minimum possible subunit amount.
19
+ # Enables standard ranges and stepping (e.g. `1.dollar..10.dollars`).
20
+ #
21
+ # @return [Money] successor Money instance
11
22
  def succ = mint(amount + currency.minimum_amount)
12
23
 
24
+ # Performs addition with another {Money} instance or standard zero Numeric.
25
+ #
26
+ # @param addend [Money, Numeric] the value to add
27
+ # @return [Money] the sum of the addition
28
+ # @raise [TypeError] if addition involves a different currency or incompatible types
13
29
  def +(addend)
14
30
  return self if addend.respond_to?(:zero?) && addend.zero?
15
31
  return mint(amount + addend.amount) if addend.is_a?(Money) && same_currency?(addend)
@@ -17,6 +33,11 @@ module Mint
17
33
  raise TypeError, "#{addend} can't be added to #{self}"
18
34
  end
19
35
 
36
+ # Performs subtraction with another {Money} instance or standard zero Numeric.
37
+ #
38
+ # @param subtrahend [Money, Numeric] the value to subtract
39
+ # @return [Money] the difference of the subtraction
40
+ # @raise [TypeError] if subtraction involves a different currency or incompatible types
20
41
  def -(subtrahend)
21
42
  return self if subtrahend.respond_to?(:zero?) && subtrahend.zero?
22
43
  if subtrahend.is_a?(Money) && same_currency?(subtrahend)
@@ -26,16 +47,30 @@ module Mint
26
47
  raise TypeError, "#{subtrahend} can't be subtracted from #{self}"
27
48
  end
28
49
 
50
+ # Unary negation operator. Returns a new {Money} instance with the inverted sign.
51
+ #
52
+ # @return [Money] negated Money instance
29
53
  def -@
30
54
  mint(-amount)
31
55
  end
32
56
 
57
+ # Performs multiplication of the monetary value by a standard scalar Numeric.
58
+ #
59
+ # @param multiplicand [Numeric] the scalar multiplier
60
+ # @return [Money] the multiplied Money instance
61
+ # @raise [TypeError] if multiplier is not Numeric or is a Money object
33
62
  def *(multiplicand)
34
63
  return mint(amount * multiplicand.to_r) if multiplicand.is_a?(Numeric)
35
64
 
36
65
  raise TypeError, "#{self} can't be multiplied by #{multiplicand}"
37
66
  end
38
67
 
68
+ # Performs division of the monetary value by a scalar Numeric or identical currency {Money}.
69
+ #
70
+ # @param divisor [Numeric, Money] the divisor
71
+ # @return [Money, Numeric] a new Money (scalar division) or a numeric ratio (Money division)
72
+ # @raise [TypeError] if divisor is of incompatible type or different currency
73
+ # @raise [ZeroDivisionError] if division by zero is attempted
39
74
  def /(divisor)
40
75
  return mint(amount / divisor) if divisor.is_a?(Numeric)
41
76
  return amount / divisor.amount if same_currency? divisor
@@ -1,40 +1,50 @@
1
1
  module Mint
2
- # :nodoc
3
- # Coercion logic
4
2
  class Money
3
+ # Implements the standard Ruby coercion protocol.
4
+ # Allows {Money} to interact seamlessly as the right-hand operand in Numeric arithmetic.
5
+ #
6
+ # @param other [Numeric] the left-hand operand to coerce
7
+ # @return [Array(CoercedNumber, Money)] coerced operand array
5
8
  def coerce(other)
6
9
  [CoercedNumber.new(other), self]
7
10
  end
8
11
 
9
- # :nodoc
12
+ # @private
10
13
  # Coerced Number contains the arithmetic logic for numeric compatible ops.
14
+ # @private
11
15
  class CoercedNumber
12
16
  include Comparable
13
17
 
18
+ # @private
14
19
  def initialize(value)
15
20
  @value = value
16
21
  end
17
22
 
23
+ # @private
18
24
  def +(other)
19
25
  return other if @value.zero?
20
26
 
21
27
  raise_coercion_error(:+, other)
22
28
  end
23
29
 
30
+ # @private
24
31
  def -(other)
25
32
  return -other if @value.zero?
26
33
 
27
34
  raise_coercion_error(:-, other)
28
35
  end
29
36
 
37
+ # @private
30
38
  def *(other)
31
39
  other.mint(@value * other.amount)
32
40
  end
33
41
 
42
+ # @private
34
43
  def /(other)
35
44
  raise_coercion_error(:/, other)
36
45
  end
37
46
 
47
+ # @private
38
48
  def <=>(other)
39
49
  return nil if @value.nil? || other.nil?
40
50
  return @value <=> other.amount if @value.zero? || other.zero?
@@ -42,6 +52,7 @@ module Mint
42
52
  raise_coercion_error(:<=>, other)
43
53
  end
44
54
 
55
+ # @private
45
56
  def raise_coercion_error(operation, operand)
46
57
  raise TypeError,
47
58
  "#{self} #{operation} #{operand} : incompatible operands"
@@ -7,9 +7,14 @@ module Mint
7
7
  # @return true if both are zero, or both have same amount and same currency
8
8
  def ==(other)
9
9
  return true if zero? && other.respond_to?(:zero?) && other.zero?
10
- return false unless other.is_a?(Mint::Money)
11
10
 
12
- amount == other.amount && currency == other.currency
11
+ eql?(other)
12
+ end
13
+
14
+ def eql?(other)
15
+ other.is_a?(Mint::Money) &&
16
+ amount == other.amount &&
17
+ currency == other.currency
13
18
  end
14
19
 
15
20
  # @example
@@ -31,16 +36,8 @@ module Mint
31
36
  raise TypeError, "#{inspect} can't be compared to #{other.inspect}"
32
37
  end
33
38
 
34
- def eql?(other)
35
- self == other
36
- end
37
-
38
- def nonzero?
39
- amount.nonzero?
40
- end
39
+ def nonzero? = amount.nonzero?
41
40
 
42
- def zero?
43
- amount.zero?
44
- end
41
+ def zero? = amount.zero?
45
42
  end
46
43
  end
@@ -1,28 +1,48 @@
1
1
  require 'erb'
2
2
 
3
3
  module Mint
4
- # Conversion logic
4
+ # Conversion and serialization logic for {Money} instances.
5
5
  class Money
6
+ # Converts the monetary amount to a {BigDecimal} object.
7
+ #
8
+ # @return [BigDecimal] the decimal representation of the money amount
9
+ # @raise [NoMethodError] if the bigdecimal gem is not loaded/available
6
10
  def to_d
7
11
  raise NoMethodError, 'decimal gem required' unless defined?(BigDecimal)
8
12
 
9
13
  amount.to_d 0
10
14
  end
11
15
 
16
+ # Converts the monetary amount to a standard float.
17
+ # Note: Using float conversion loses precision guarantees.
18
+ #
19
+ # @return [Float] the floating-point representation of the money amount
12
20
  def to_f
13
21
  amount.to_f
14
22
  end
15
23
 
24
+ # Renders a safe HTML5 `<data>` element containing the formatted currency.
25
+ # Embeds the ISO currency description and raw value as the metadata `title` attribute.
26
+ #
27
+ # @param format [String] the display format to apply to the visible HTML text
28
+ # @return [String] HTML5 `<data>` representation
16
29
  def to_html(format = DEFAULT_FORMAT)
17
30
  title = Kernel.format("#{currency_code} %0.#{currency.subunit}f", amount)
18
31
  body = to_s(format: format)
19
32
  %(<data class='money' title='#{ERB::Util.html_escape(title)}'>#{ERB::Util.html_escape(body)}</data>)
20
33
  end
21
34
 
35
+ # Truncates and converts the monetary amount to an Integer.
36
+ #
37
+ # @return [Integer] the integer representation of the money amount
22
38
  def to_i
23
39
  amount.to_i
24
40
  end
25
41
 
42
+ # Serializes the money instance to a standard JSON object containing the amount and currency.
43
+ # Highly optimized to run without external dependencies.
44
+ #
45
+ # @return [String] the JSON serialized string representation
26
46
  def to_json(*_args)
27
47
  subunit = currency.subunit
28
48
  Kernel.format(
@@ -31,6 +51,9 @@ module Mint
31
51
  )
32
52
  end
33
53
 
54
+ # Returns the exact internal Rational representation of the monetary amount.
55
+ #
56
+ # @return [Rational] the rational representation of the money amount
34
57
  def to_r
35
58
  amount
36
59
  end
@@ -11,7 +11,7 @@ module Mint
11
11
  # @example Basic formatting
12
12
  # money = Mint.money(1234.56, 'USD')
13
13
  # money.to_s #=> "$1,234.56"
14
- # money.to_s(thousand: '.', decimal: ',') #=> "$.1234,56"
14
+ # money.to_s(thousand: '.', decimal: ',') #=> "$1.234,56"
15
15
  # money.to_s(decimal: ',', thousand: '') #=> "$1234,56"
16
16
  #
17
17
  # @example Custom formats
@@ -41,6 +41,8 @@ module Mint
41
41
  formatted
42
42
  end
43
43
 
44
+ private
45
+
44
46
  def format_amount(format)
45
47
  format = { positive: format } if format.is_a?(String)
46
48
  value = amount
@@ -1,5 +1,7 @@
1
1
  module Mint
2
2
  class Money
3
+ # The default display format pattern for formatting monetary values.
4
+ # Uses `%<symbol>s` for the currency symbol and `%<amount>f` for the rounded amount.
3
5
  DEFAULT_FORMAT = '%<symbol>s%<amount>f'.freeze
4
6
 
5
7
  attr_reader :amount, :currency
@@ -10,24 +12,22 @@ module Mint
10
12
  # @raise [ArgumentError] If amount is not numeric or currency is invalid
11
13
  def initialize(amount, currency)
12
14
  raise ArgumentError, 'amount must be Numeric' unless amount.is_a?(Numeric)
13
-
14
- unless currency.is_a?(Currency)
15
- raise ArgumentError,
16
- 'currency must be a Currency object'
17
- end
15
+ raise ArgumentError, 'currency must be a Currency object' unless currency.is_a?(Currency)
18
16
 
19
17
  @amount = amount.to_r.round(currency.subunit)
20
18
  @currency = currency
21
19
  freeze
22
20
  end
23
21
 
24
- def currency_code
25
- currency.code
26
- end
22
+ # Returns the ISO 3-letter currency code string.
23
+ #
24
+ # @return [String] the ISO currency code
25
+ def currency_code = currency.code
27
26
 
28
- def hash
29
- zero? ? 0.hash : [amount, currency.code].hash
30
- end
27
+ # Generates a stable hash key for Money instances.
28
+ #
29
+ # @return [Integer] the calculated hash value
30
+ def hash = [amount, currency_code].hash
31
31
 
32
32
  # Returns a new Money object with the specified amount, or self if unchanged
33
33
  # @param new_amount [Numeric] The new amount
@@ -36,12 +36,20 @@ module Mint
36
36
  new_amount.to_r == amount ? self : Money.new(new_amount, currency)
37
37
  end
38
38
 
39
+ # Returns a standard developer-oriented string inspection of the Money object.
40
+ #
41
+ # @return [String] the formatted inspect representation
39
42
  def inspect
40
43
  Kernel.format "[#{currency_code} %0.#{currency.subunit}f]", amount
41
44
  end
42
45
 
43
- def same_currency?(other)
44
- other.respond_to?(:currency) && other.currency == currency
45
- end
46
+ # Helper method to verify if another object has the identical currency.
47
+ #
48
+ # @param other [Object] the target object to compare
49
+ # @return [Boolean] true if currencies match, false otherwise
50
+ def same_currency?(other) = other.respond_to?(:currency) && other.currency == currency
51
+
52
+ # Returns default zero no currency money
53
+ def self.zero = @zero ||= new(0, Mint.currencies('XXX'))
46
54
  end
47
55
  end
@@ -1,3 +1,5 @@
1
+ # Root namespace for the Minting library.
1
2
  module Minting
2
- VERSION = '1.1.0'.freeze
3
+ # Current version of the Minting gem.
4
+ VERSION = '1.1.1'.freeze
3
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minting
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gilson Ferraz