Class: Mint::Money

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/minting/money/parse.rb,
lib/minting/money/money.rb,
lib/minting/money/coercion.rb,
lib/minting/money/allocation.rb,
lib/minting/money/comparable.rb,
lib/minting/money/conversion.rb,
lib/minting/money/formatting.rb,
lib/minting/money/arithmetics.rb

Overview

Formatting functionality for Money objects

Constant Summary collapse

DEFAULT_FORMAT =

The default display format pattern for formatting monetary values. Uses ‘%<symbol>s` for the currency symbol and `%<amount>f` for the rounded amount.

'%<symbol>s%<amount>f'.freeze
SIGN_FORMAT_KEYS =

Keys accepted in the per-sign Hash form of ‘to_s(format:)`.

%i[positive negative zero].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#amountObject (readonly)

Returns the value of attribute amount.



7
8
9
# File 'lib/minting/money/money.rb', line 7

def amount
  @amount
end

#currencyObject (readonly)

Returns the value of attribute currency.



7
8
9
# File 'lib/minting/money/money.rb', line 7

def currency
  @currency
end

Class Method Details

.create(amount, currency) ⇒ Object

Creates a new Money immutable object with the specified amount and currency

Parameters:

  • amount (Numeric)

    The monetary amount

  • currency (Currency)

    The currency object

Raises:

  • (ArgumentError)

    If amount is not numeric or currency is invalid



13
14
15
16
17
18
19
20
# File 'lib/minting/money/money.rb', line 13

def self.create(amount, currency)
  raise ArgumentError, 'amount must be Numeric' unless amount.is_a?(Numeric)

  checked_currency = Mint.currency(currency)
  raise ArgumentError, "Currency not found (#{currency})" unless checked_currency

  new(checked_currency.normalize_amount(amount), checked_currency)
end

.from_fractional(fractional, currency) ⇒ Money

Builds a Money from a fractional (smallest-unit) Integer amount. This is the inverse of #fractional: for USD, the fractional unit is 1 cent; for JPY it is 1 yen; for IQD it is 1 dinar (subunit 3).

Examples:

USD cents

Money.from_fractional(123_456, 'USD') #=> [USD 1234.56]

JPY (subunit 0)

Money.from_fractional(1234, 'JPY')    #=> [JPY 1234]

Round trip

m = Mint.money(9.99, 'USD')
Money.from_fractional(m.fractional, 'USD') == m #=> true

Parameters:

  • fractional (Integer)

    the amount expressed in the currency’s smallest unit (e.g. cents). Must be an Integer to preserve exactness.

  • currency (String, Symbol, Currency)

    the currency identifier

Returns:

  • (Money)

    the resulting Money instance

Raises:

  • (ArgumentError)

    if fractional is not an Integer or currency is not registered



40
41
42
43
44
45
46
47
48
# File 'lib/minting/money/money.rb', line 40

def self.from_fractional(fractional, currency)
  raise ArgumentError, 'fractional must be an Integer' unless fractional.is_a?(Integer)

  checked_currency = Mint.currency(currency)
  raise ArgumentError, "Currency not found (#{currency})" unless checked_currency

  amount = Rational(fractional, checked_currency.fractional_multiplier)
  new(amount, checked_currency)
end

.parse(input, currency = nil) ⇒ Money

Parses a human-readable money string into a Mint::Money object.

Examples:

With explicit currency

Money.parse('19.99', 'USD')    #=> [USD 19.99]
Money.parse('1.234,56', 'EUR') #=> [EUR 1234.56]

With symbol or code in the string

Money.parse('$19.99')            #=> [USD 19.99]
Money.parse('19,99 €')         #=> [EUR 19.99]
Money.parse('USD 1,234.56')    #=> [USD 1234.56]

Parameters:

  • input (String)

    Amount input, optionally including a currency symbol or code

  • currency (String, Symbol, Currency, nil) (defaults to: nil)

    ISO code when not present in input

Returns:

Raises:

  • (ArgumentError)

    when input is invalid or currency cannot be determined



19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/minting/money/parse.rb', line 19

def self.parse(input, currency = nil)
  raise ArgumentError, 'input must be a String' unless input.is_a?(String)

  input = input.strip
  raise ArgumentError, 'input cannot be empty' if input.empty?

  currency = currency ? Mint.currency(currency) : parse_currency(input)
  raise ArgumentError, "Currency [#{currency}] not registered" unless currency

  amount = currency.normalize_amount(parse_amount(input))
  new(amount, currency)
end

Instance Method Details

#*(multiplicand) ⇒ Money

Performs multiplication of the monetary value by a standard scalar Numeric.

Parameters:

  • multiplicand (Numeric)

    the scalar multiplier

Returns:

  • (Money)

    the multiplied Money instance

Raises:

  • (TypeError)

    if multiplier is not Numeric or is a Money object



62
63
64
65
66
# File 'lib/minting/money/arithmetics.rb', line 62

def *(multiplicand)
  return mint(amount * multiplicand) if multiplicand.is_a?(Numeric)

  raise TypeError, "#{self} can't be multiplied by #{multiplicand}"
end

#+(addend) ⇒ Money

Performs addition with another Mint::Money instance or standard zero Numeric.

Parameters:

  • addend (Money, Numeric)

    the value to add

Returns:

  • (Money)

    the sum of the addition

Raises:

  • (TypeError)

    if addition involves a different currency or incompatible types



29
30
31
32
33
34
# File 'lib/minting/money/arithmetics.rb', line 29

def +(addend)
  return self if addend.respond_to?(:zero?) && addend.zero?
  return mint(amount + addend.amount) if addend.is_a?(Money) && same_currency?(addend)

  raise TypeError, "#{addend} can't be added to #{self}"
end

#-(subtrahend) ⇒ Money

Performs subtraction with another Mint::Money instance or standard zero Numeric.

Parameters:

  • subtrahend (Money, Numeric)

    the value to subtract

Returns:

  • (Money)

    the difference of the subtraction

Raises:

  • (TypeError)

    if subtraction involves a different currency or incompatible types



41
42
43
44
45
46
47
48
# File 'lib/minting/money/arithmetics.rb', line 41

def -(subtrahend)
  return self if subtrahend.respond_to?(:zero?) && subtrahend.zero?
  if subtrahend.is_a?(Money) && same_currency?(subtrahend)
    return mint(amount - subtrahend.amount)
  end

  raise TypeError, "#{subtrahend} can't be subtracted from #{self}"
end

#-@Money

Unary negation operator. Returns a new Mint::Money instance with the inverted sign.

Returns:

  • (Money)

    negated Money instance



53
54
55
# File 'lib/minting/money/arithmetics.rb', line 53

def -@
  mint(-amount)
end

#/(divisor) ⇒ Money, Numeric

Performs division of the monetary value by a scalar Numeric or identical currency Mint::Money.

Parameters:

  • divisor (Numeric, Money)

    the divisor

Returns:

  • (Money, Numeric)

    a new Money (scalar division) or a numeric ratio (Money division)

Raises:

  • (TypeError)

    if divisor is of incompatible type or different currency

  • (ZeroDivisionError)

    if division by zero is attempted



74
75
76
77
78
79
# File 'lib/minting/money/arithmetics.rb', line 74

def /(divisor)
  return mint(amount / divisor) if divisor.is_a?(Numeric)
  return amount / divisor.amount if same_currency? divisor

  raise TypeError, "#{self} can't be divided by #{divisor}"
end

#<=>(other) ⇒ Object

Examples:

two_usd == Mint.money(2r, 'USD') #=> [$ 2.00]
two_usd > 0                      #=> true
two_usd > Mint.money(2, 'USD')   #=> false
two_usd > 1
=> TypeError: [$ 2.00] can't be compared to 1
two_usd > Mint.money(2, 'BRL')
=> TypeError: [$ 2.00] can't be compared to [R$ 2.00]

Raises:

  • (TypeError)


29
30
31
32
33
34
35
36
37
# File 'lib/minting/money/comparable.rb', line 29

def <=>(other)
  case other
  when Numeric
    return amount <=> other if other.zero?
  when Mint::Money
    return amount <=> other.amount if currency == other.currency
  end
  raise TypeError, "#{inspect} can't be compared to #{other.inspect}"
end

#==(other) ⇒ Object

Returns true if both are zero, or both have same amount and same currency.

Returns:

  • true if both are zero, or both have same amount and same currency



8
9
10
11
12
# File 'lib/minting/money/comparable.rb', line 8

def ==(other)
  return true if zero? && other.respond_to?(:zero?) && other.zero?

  eql?(other)
end

#absMoney

Returns the absolute value of the monetary amount as a new Mint::Money instance.

Returns:

  • (Money)

    the absolute value



6
# File 'lib/minting/money/arithmetics.rb', line 6

def abs = mint(amount.abs)

#allocate(proportions) ⇒ Array<Money>

Proportionally allocates the monetary amount among a list of ratios. Disperses any subunit rounding amounts across the initial slots

Examples:

Proportional allocation

money = Mint.money(10.00, 'USD')
money.allocate([1, 2, 3]) #=> [[USD 1.67], [USD 3.33], [USD 5.00]]

Parameters:

  • proportions (Array<Numeric>)

    a list of numeric proportions/ratios to allocate by

Returns:

  • (Array<Money>)

    the list of newly allocated Money objects

Raises:

  • (ArgumentError)

    if the proportions list is empty or sums to zero



12
13
14
15
16
17
18
19
20
# File 'lib/minting/money/allocation.rb', line 12

def allocate(proportions)
  whole = proportions.sum.to_r
  raise ArgumentError, 'Need at least 1 proportion element' if proportions.empty?
  raise ArgumentError, 'Proportions total must not be zero' if whole.zero?

  subunit = currency.subunit
  amounts = proportions.map { |rate| (amount * rate.to_r / whole).round(subunit) }
  allocate_left_over!(amounts: amounts, left_over: amount - amounts.sum)
end

#clamp(min, max) ⇒ Money

Constrains self to the inclusive range [min, max].

Both bounds may be either a same-currency Mint::Money or a Numeric. A Numeric is interpreted as an amount in self‘s currency, so the common pricing idiom price.clamp(0, 100) reads as “0 to 100 in the same currency as price”.

When self is already in range the receiver is returned (no new object allocated). When out of range, the nearest bound is returned as a new frozen Mint::Money in self‘s currency.

Examples:

In range

Mint.money(5, 'USD').clamp(0, 10) #=> [USD 5.00]  (returns self)

Out of range, with Numeric bounds

Mint.money(50, 'USD').clamp(0, 10) #=> [USD 10.00]

Out of range, with Money bounds

loss  = Mint.money(-5, 'USD')
floor = Mint.money(0,  'USD')
ceil  = Mint.money(10, 'USD')
loss.clamp(floor, ceil) #=> [USD 0.00]

Subunit-0 currency (JPY)

Mint.money(500, 'JPY').clamp(0, 100) #=> [JPY 100]

Parameters:

  • min (Money, Numeric)

    lower bound (inclusive)

  • max (Money, Numeric)

    upper bound (inclusive)

Returns:

  • (Money)

    self if in range, otherwise the nearer bound

Raises:

  • (ArgumentError)

    if min or max is not a Money or Numeric; if a Money operand has a different currency; if min > max



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/minting/money/money.rb', line 114

def clamp(min, max)
  case min
  when Numeric
  when Money
    raise(ArgumentError, "min currency must be: #{currency_code}") unless same_currency?(min)

    min = min.amount
  else raise(ArgumentError, 'min must be Numeric or Money')
  end

  case max
  when Numeric
  when Money
    raise(ArgumentError, "max currency must be: #{currency_code}") unless same_currency?(max)

    max = max.amount
  else raise(ArgumentError, 'max must be Numeric or Money')
  end

  mint(amount.clamp(min, max))
end

#coerce(other) ⇒ Array(CoercedNumber, Money)

Implements the standard Ruby coercion protocol. Allows Mint::Money to interact seamlessly as the right-hand operand in Numeric arithmetic.

Parameters:

  • other (Numeric)

    the left-hand operand to coerce

Returns:

  • (Array(CoercedNumber, Money))

    coerced operand array



8
9
10
# File 'lib/minting/money/coercion.rb', line 8

def coerce(other)
  [CoercedNumber.new(other), self]
end

#currency_codeString

Returns the ISO 3-letter currency code string.

Returns:

  • (String)

    the ISO currency code



53
# File 'lib/minting/money/money.rb', line 53

def currency_code = currency.code

#eql?(other) ⇒ Boolean

Returns:

  • (Boolean)


14
15
16
17
18
# File 'lib/minting/money/comparable.rb', line 14

def eql?(other)
  other.is_a?(Mint::Money) &&
    amount == other.amount &&
    currency == other.currency
end

#fractionalObject



55
# File 'lib/minting/money/money.rb', line 55

def fractional = (amount * currency.fractional_multiplier).to_i

#hashInteger

Generates a stable hash key for Money instances.

Returns:

  • (Integer)

    the calculated hash value



60
# File 'lib/minting/money/money.rb', line 60

def hash = [amount, currency_code].hash

#inspectString

Returns a standard developer-oriented string inspection of the Money object.

Returns:

  • (String)

    the formatted inspect representation



73
74
75
# File 'lib/minting/money/money.rb', line 73

def inspect
  Kernel.format "[#{currency_code} %0.#{currency.subunit}f]", amount
end

#mint(new_amount) ⇒ Money

Returns a new Money object with the specified amount, or self if unchanged

Parameters:

  • new_amount (Numeric)

    The new amount

Returns:

  • (Money)

    A new Money object or self



65
66
67
68
# File 'lib/minting/money/money.rb', line 65

def mint(new_amount)
  new_amount = currency.normalize_amount(new_amount)
  new_amount == amount ? self : Money.new(new_amount, currency)
end

#negative?Boolean

Returns true if the monetary amount is less than zero.

Returns:

  • (Boolean)

    true if negative, false otherwise



11
# File 'lib/minting/money/arithmetics.rb', line 11

def negative? = amount.negative?

#nonzero?Boolean

Returns:

  • (Boolean)


39
# File 'lib/minting/money/comparable.rb', line 39

def nonzero? = amount.nonzero?

#positive?Boolean

Returns true if the monetary amount is greater than zero.

Returns:

  • (Boolean)

    true if positive, false otherwise



16
# File 'lib/minting/money/arithmetics.rb', line 16

def positive? = amount.positive?

#same_currency?(other) ⇒ Boolean

Helper method to verify if another object has the identical currency.

Parameters:

  • other (Object)

    the target object to compare

Returns:

  • (Boolean)

    true if currencies match, false otherwise



81
# File 'lib/minting/money/money.rb', line 81

def same_currency?(other) = other.respond_to?(:currency) && other.currency == currency

#split(quantity) ⇒ Array<Money>

Splits the monetary amount into a given quantity of equal parts. Disperses any fractional subunit rounding differences across the initial slots so that the sum is preserved.

Examples:

Even split

money = Mint.money(10.00, 'USD')
money.split(3) #=> [[USD 3.34], [USD 3.33], [USD 3.33]]

Parameters:

  • quantity (Integer)

    the number of equal parts to divide the money into (must be > 0)

Returns:

  • (Array<Money>)

    the list of newly split Money objects

Raises:

  • (ArgumentError)

    if quantity is not a positive integer



33
34
35
36
37
38
39
40
41
42
# File 'lib/minting/money/allocation.rb', line 33

def split(quantity)
  unless  quantity.positive? && quantity.integer?
    raise ArgumentError,
          'quantity must be an integer > 0'
  end

  fraction = (amount / quantity).round(currency.subunit)
  allocate_left_over!(amounts: Array.new(quantity, fraction),
                      left_over: amount - (fraction * quantity))
end

#succMoney

Returns the successor of the Money instance by adding the minimum possible subunit amount. Enables standard ranges and stepping (e.g. ‘1.dollar..10.dollars`).

Returns:

  • (Money)

    successor Money instance



22
# File 'lib/minting/money/arithmetics.rb', line 22

def succ = mint(amount + currency.minimum_amount)

#to_dBigDecimal

Converts the monetary amount to a BigDecimal object.

Returns:

  • (BigDecimal)

    the decimal representation of the money amount



9
# File 'lib/minting/money/conversion.rb', line 9

def to_d = amount.to_d 0

#to_fFloat

Converts the monetary amount to a standard float. Note: Using float conversion loses precision guarantees.

Returns:

  • (Float)

    the floating-point representation of the money amount



15
# File 'lib/minting/money/conversion.rb', line 15

def to_f = amount.to_f

#to_hashObject



33
34
35
# File 'lib/minting/money/conversion.rb', line 33

def to_hash
  { currency: currency_code, amount: Kernel.format("%0.#{currency.subunit}f", amount) }
end

#to_html(format = DEFAULT_FORMAT) ⇒ String

Renders a safe HTML5 ‘<data>` element containing the formatted currency. Embeds the ISO currency description and raw value as the metadata `title` attribute.

Parameters:

  • format (String) (defaults to: DEFAULT_FORMAT)

    the display format to apply to the visible HTML text

Returns:

  • (String)

    HTML5 ‘<data>` representation



22
23
24
25
26
# File 'lib/minting/money/conversion.rb', line 22

def to_html(format = DEFAULT_FORMAT)
  title = Kernel.format("#{currency_code} %0.#{currency.subunit}f", amount)
  body = to_s(format: format)
  %(<data class='money' title='#{title}'>#{ERB::Util.html_escape(body)}</data>)
end

#to_iInteger

Truncates and converts the monetary amount to an Integer.

Returns:

  • (Integer)

    the integer representation of the money amount



31
# File 'lib/minting/money/conversion.rb', line 31

def to_i = amount.to_i

#to_json(*_args) ⇒ String

Serializes the money instance to a standard JSON object containing the amount and currency. Highly optimized to run without external dependencies.

Returns:

  • (String)

    the JSON serialized string representation



41
42
43
44
45
# File 'lib/minting/money/conversion.rb', line 41

def to_json(*_args)
  Kernel.format(
    %({"currency": "#{currency_code}", "amount": "%0.#{currency.subunit}f"}), amount
  )
end

#to_rRational

Returns the exact internal Rational representation of the monetary amount.

Returns:

  • (Rational)

    the rational representation of the money amount



50
# File 'lib/minting/money/conversion.rb', line 50

def to_r = amount

#to_s(format: '%<symbol>s%<amount>f', decimal: '.', thousand: ',', width: nil) ⇒ String

Formats money as a string with customizable format, thousand delimiter, and decimal

Examples:

Basic formatting

money = Mint.money(1234.56, 'USD')
money.to_s                               #=> "$1,234.56"
money.to_s(thousand: '.', decimal: ',')  #=> "$1.234,56"
money.to_s(decimal: ',', thousand: '')   #=> "$1234,56"

Custom formats

money.to_s(format: '%<amount>f')                    #=> "1234.56"
money.to_s(format: '%<currency>s %<amount>f')       #=> "USD 1234.56"
money.to_s(format: '%<amount>f %<symbol>s')         #=> "1234.56 $"
money.to_s(format: '%<symbol>s%<amount>+f')         #=> "$+1234.56"

Per-sign Hash format (accounting parentheses)

loss = Mint.money(-1234.56, 'USD')
loss.to_s(format: { negative: '(%<symbol>s%<amount>f)' }) #=> "($1,234.56)"
Mint.money(0, 'BRL').to_s(format: { zero: '--' })        #=> "--"

Padding and alignment

money.to_s(format: '%<amount>10.2f')                #=> "   1234.56"
money.to_s(format: '%<symbol>s%<amount>010.2f')     #=> "$0001234.56"

Parameters:

  • format (String, Hash) (defaults to: '%<symbol>s%<amount>f')

    Either a Format string with placeholders (%<symbol>s, %<amount>f, %<currency>s), or a Hash with per-sign keys (:positive, :negative, :zero) each holding a format string. A Hash is convenient for sign-aware formats such as accounting parentheses:

    money.to_s(format: { negative: '(%<symbol>s%<amount>f)' })
    

    Missing keys fall back to the module default, so a Hash with only :negative will still format positives sensibly. The valid keys are :positive, :negative, :zero; anything else raises ArgumentError.

  • thousand (String, false) (defaults to: ',')

    Thousands delimiter (e.g., ‘,’ for 1,000)

  • decimal (String) (defaults to: '.')

    Decimal separator (e.g., ‘.’ or ‘,’)

Returns:

  • (String)

    Formatted money string

Raises:

  • (ArgumentError)

    if format is not a String or Hash, the Hash is empty, or the Hash contains an unrecognised key.



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/minting/money/formatting.rb', line 47

def to_s(format: '%<symbol>s%<amount>f', decimal: '.', thousand: ',', width: nil)
  raise ArgumentError, 'Invalid format' unless format.is_a?(String) || format.is_a?(Hash)

  validate_format_hash!(format) if format.is_a?(Hash)

  formatted = format_amount(format)

  formatted.tr!('.', decimal) if decimal != '.'

  unless thousand.empty?
    # Regular expression courtesy of Money gem
    # Matches digits followed by groups of 3 digits until non-digit or end
    formatted.gsub!(/(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/, "\\1#{thousand}")
  end

  formatted = formatted.rjust(width) if width
  formatted
end

#zero?Boolean

Returns:

  • (Boolean)


41
# File 'lib/minting/money/comparable.rb', line 41

def zero? = amount.zero?