Class: Mint::Money

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

    The 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

.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



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

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 = parse_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



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

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



90
91
92
93
94
# File 'lib/minting/money/arithmetics.rb', line 90

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
57
58
# File 'lib/minting/money/arithmetics.rb', line 56

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



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

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]


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

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
# File 'lib/minting/money/comparable.rb', line 9

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



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| (amount * rate.to_r / 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



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

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.

Parameters:

  • other (Numeric)

    the left-hand operand to coerce

Returns:

  • (Array(CoercedNumber, Money))

    coerced operand array



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

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

#currency_codeString

Returns the ISO 3-letter currency code string.

Returns:

  • (String)

    the ISO currency code



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

def currency_code = currency.code

#eql?(other) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#fractionalObject



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

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

#hashInteger

Generates a stable hash key for Money instances.

Returns:

  • (Integer)

    the calculated hash value



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

def hash = [amount, currency_code].hash

#inspectString

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

Returns:

  • (String)

    the formatted inspect representation



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

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



50
51
52
53
# File 'lib/minting/money/constructors.rb', line 50

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)


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

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



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

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.

Returns:

  • (BigDecimal)

    the decimal representation of the money amount



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

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



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

def to_f = amount.to_f

#to_hashObject



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

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



25
26
27
28
29
# File 'lib/minting/money/conversion.rb', line 25

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



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

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



44
45
46
47
48
# File 'lib/minting/money/conversion.rb', line 44

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



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

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)


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

def zero? = amount.zero?