Class: Mint::Money

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
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,
lib/minting/money/constructors.rb

Overview

Money constructors

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'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#amountObject (readonly)

Returns the value of attribute amount.



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

def amount
  @amount
end

#currencyObject (readonly)

Returns the value of attribute currency.



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

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, String)

    The currency code or currency object

Raises:

  • (ArgumentError)

    If amount is not numeric or currency is invalid



10
11
12
13
14
15
16
17
# File 'lib/minting/money/constructors.rb', line 10

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



37
38
39
40
41
42
43
44
45
# File 'lib/minting/money/constructors.rb', line 37

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

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



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

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

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

#**(exponent) ⇒ Money

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

Parameters:

  • exponent (Numeric)

Returns:

  • (Money)

    reult of amount ** exponent

Raises:

  • (TypeError)

    if exponent is not Numeric



88
89
90
91
92
# File 'lib/minting/money/arithmetics.rb', line 88

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

  raise TypeError, "#{self} can't be powered by #{exponent}"
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



32
33
34
35
36
37
38
# File 'lib/minting/money/arithmetics.rb', line 32

def +(addend)
  case addend
  when 0     then return self
  when Money then return mint(amount + addend.amount) if same_currency?(addend)
  end
  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



45
46
47
48
49
50
51
# File 'lib/minting/money/arithmetics.rb', line 45

def -(subtrahend)
  case subtrahend
  when 0     then return self
  when Money then return mint(amount - subtrahend.amount) if same_currency?(subtrahend)
  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



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

def -@ = mint(-amount)

#/(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



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

def /(divisor)
  case divisor
  when Numeric then return mint(amount / divisor)
  when Money   then return amount / divisor.amount if same_currency? divisor
  end
  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]


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

def <=>(other)
  case other
  in 0                                    then amount <=> other
  in Mint::Money if same_currency?(other) then amount <=> other.amount
  else                                    raise TypeError, "#{inspect} can't be compared to #{other.inspect}"
  end
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



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

def ==(other)
  case other
  when 0           then zero?
  when Mint::Money then amount == other.amount && currency == other.currency
  else                  false
  end
end

#absMoney

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

Returns:

  • (Money)

    the absolute value



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

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



15
16
17
18
19
20
21
22
23
# File 'lib/minting/money/allocation.rb', line 15

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| Rational(amount * rate, whole).round(subunit) }
  allocate_left_over!(amounts: amounts, left_over: amount - amounts.sum)
end

#clamp(min_or_range, max = nil) ⇒ Money

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

Bounds may be:

  • nil meaning no boundary

  • same-currency Mint::Money or Range

  • Numeric amount, or Range

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_or_range (Money, Numeric, Range, nil)

    lower bound (inclusive), or range

  • max (Money, Numeric, nil) (defaults to: nil)

    upper bound (inclusive)

Returns:

  • (Money)

    self if in range, otherwise the nearer bound

Raises:

  • (ArgumentError)

    if min or max is not a Money, Numeric or nil; if a Money operand has a different currency; if min > max; if min is a Range, and max is not nil



83
84
85
86
87
88
89
90
91
92
# File 'lib/minting/money/money.rb', line 83

def clamp(min_or_range, max = nil)
  if min_or_range.is_a?(Range)
    raise(ArgumentError, "Either amount range alone or two amounts accepted: #{max}") if max

    min, max = min_or_range.minmax
  else
    min = min_or_range
  end
  mint(amount.clamp(normalize_boundary(min), normalize_boundary(max)))
end

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

Allows Mint::Money to interact seamlessly as the right-hand operand in Numeric arithmetic. This enables expressions like ‘5 + money` where `5` is a Numeric and `money` is a Money object.

Examples:

price = Mint.money(10, 'USD')
5 + price  #=> [USD 15.00] (via coercion)

Parameters:

  • other (Numeric)

    the left-hand operand to coerce

Returns:

  • (Array(CoercedNumber, Money))

    coerced operand array



14
15
16
# File 'lib/minting/money/coercion.rb', line 14

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

#currency_codeString

Returns the ISO 3-letter currency code string.

Examples:

Mint.money(100, 'USD').currency_code  #=> "USD"

Returns:

  • (String)

    the ISO currency code (e.g., “USD”, “EUR”, “BRL”)



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

def currency_code = currency.code

#eql?(other) ⇒ Boolean

Returns:

  • (Boolean)


17
18
19
20
21
# File 'lib/minting/money/comparable.rb', line 17

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

#fractionalInteger

Returns the monetary amount expressed in the currency’s smallest unit (fractional units). For example, cents for USD (subunit 2), yen for JPY (subunit 0), fils for IQD (subunit 3).

Examples:

Mint.money(1234.56, 'USD').fractional  #=> 123456
Mint.money(1000, 'JPY').fractional     #=> 1000
Mint.money(123.456, 'IQD').fractional  #=> 123456

Returns:

  • (Integer)

    the amount in fractional units



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

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

#hashInteger

Generates a stable hash key for Money instances.

Returns:

  • (Integer)

    the calculated hash value



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

def hash = [amount, currency_code].hash

#inspectString

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

Returns:

  • (String)

    the formatted inspect representation



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

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. This is the primary method for creating a modified copy of a Money instance while preserving immutability.

Examples:

price = Mint.money(10.00, 'USD')
price.mint(15.00)  #=> [USD 15.00]
price.mint(10.00)  #=> [USD 10.00] (returns self)

Parameters:

  • new_amount (Numeric)

    The new monetary amount

Returns:

  • (Money)

    A new Money object with the new amount, or self if the amount is unchanged



57
58
59
60
# File 'lib/minting/money/constructors.rb', line 57

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



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

def negative? = amount.negative?

#nonzero?Boolean

Returns:

  • (Boolean)


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

def nonzero? = amount.nonzero?

#positive?Boolean

Returns true if the monetary amount is greater than zero.

Returns:

  • (Boolean)

    true if positive, false otherwise



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

def positive? = amount.positive?

#same_currency?(other) ⇒ Boolean

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

Parameters:

  • other (Currency)

    the target currency to compare

Returns:

  • (Boolean)

    true if currencies match, false otherwise



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

def same_currency?(other) = 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



36
37
38
39
40
41
42
43
44
45
# File 'lib/minting/money/allocation.rb', line 36

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



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

def succ = mint(amount + currency.minimum_amount)

#to_dBigDecimal

Converts the monetary amount to a BigDecimal object.

Examples:

Mint.money(9.99, 'USD').to_d  #=> 0.999e1

Returns:

  • (BigDecimal)

    the decimal representation of the money amount



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

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



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

def to_f = amount.to_f

#to_hashHash

Returns a Hash representation of the money instance.

Examples:

Mint.money(134120, 'BRL').to_hash
#=> { currency: "BRL", amount: "134120.00" }

Returns:

  • (Hash)

    hash with :currency (String) and :amount (String) keys



47
48
49
# File 'lib/minting/money/conversion.rb', line 47

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



27
28
29
30
31
# File 'lib/minting/money/conversion.rb', line 27

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.

Examples:

Mint.money(9.99, 'USD').to_i  #=> 9
Mint.money(-9.99, 'USD').to_i #=> -9

Returns:

  • (Integer)

    the integer representation of the money amount



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

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



55
56
57
58
59
# File 'lib/minting/money/conversion.rb', line 55

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



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

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.



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

def to_s(format: '%<symbol>s%<amount>f', decimal: '.', thousand: ',', width: nil)
  case format
  when {}, '', nil then raise ArgumentError, 'format must not be empty or null'
  when Hash        then validate_format_hash!(format)
  when String # noop
  else raise ArgumentError, 'Invalid format'
  end

  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)


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

def zero? = amount.zero?