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

Defined Under Namespace

Classes: CoercedNumber

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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(amount, currency) ⇒ Money

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 initialize(amount, currency)
  raise ArgumentError, 'amount must be Numeric' unless amount.is_a?(Numeric)
  raise ArgumentError, 'currency must be a Currency object' unless currency.is_a?(Currency)

  @amount = amount.to_r.round(currency.subunit)
  @currency = currency
  freeze
end

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

.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 = parse_amount(input)
  new(amount, currency)
end

.zeroObject

Returns default zero no currency money



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

def self.zero = @zero ||= new(0, Mint.currencies('XXX'))

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.to_r) 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
# 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?

  amounts = proportions.map { |rate| (amount * rate.to_r / whole).round(currency.subunit) }
  allocate_left_over!(amounts: amounts, left_over: amount - amounts.sum)
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:



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



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

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

#hashInteger

Generates a stable hash key for Money instances.

Returns:

  • (Integer)

    the calculated hash value



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

def hash = [amount, currency_code].hash

#inspectString

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

Returns:

  • (String)

    the formatted inspect representation



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

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



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

def mint(new_amount)
  new_amount.to_r == 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



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

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



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

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

Raises:

  • (NoMethodError)

    if the bigdecimal gem is not loaded/available



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

def to_d
  raise NoMethodError, 'decimal gem required' unless defined?(BigDecimal)

  amount.to_d 0
end

#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
21
22
# File 'lib/minting/money/conversion.rb', line 20

def to_f
  amount.to_f
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



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

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='#{ERB::Util.html_escape(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



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

def to_i
  amount.to_i
end

#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



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

def to_json(*_args)
  subunit = currency.subunit
  Kernel.format(
    %({"currency": "#{currency_code}", "amount": "%0.#{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



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

def to_r
  amount
end

#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"

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) (defaults to: '%<symbol>s%<amount>f')

    Format string with placeholders: %<symbol>s, %<amount>f, %<currency>s

  • 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)


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/minting/money/formatting.rb', line 27

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)

  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?