Class: Mint::Money

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

:nodoc Arithmetic functions for money objects

Defined Under Namespace

Classes: CoercedNumber

Constant Summary collapse

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

Instance Attribute 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



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

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

  unless currency.is_a?(Currency)
    raise ArgumentError,
          'currency must be a Currency object'
  end

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

Instance Attribute Details

#amountObject (readonly)

Returns the value of attribute amount.



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

def amount
  @amount
end

#currencyObject (readonly)

Returns the value of attribute currency.



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

def currency
  @currency
end

Instance Method Details

#*(multiplicand) ⇒ Object

Raises:

  • (TypeError)


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

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

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

#+(addend) ⇒ Object

Raises:

  • (TypeError)


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

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) ⇒ Object

Raises:

  • (TypeError)


20
21
22
23
24
25
26
27
# File 'lib/minting/money/arithmetics.rb', line 20

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

#-@Object



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

def -@
  mint(-amount)
end

#/(divisor) ⇒ Object

Raises:

  • (TypeError)


39
40
41
42
43
44
# File 'lib/minting/money/arithmetics.rb', line 39

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'])  #=> true
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)


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

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

def ==(other)
  return true if zero? && other.respond_to?(:zero?) && other.zero?
  return false unless other.is_a?(Mint::Money)

  amount == other.amount && currency == other.currency
end

#absObject



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

def abs =     mint(amount.abs)

#allocate(proportions) ⇒ Object

Raises:

  • (ArgumentError)


5
6
7
8
9
10
11
# File 'lib/minting/money/allocation.rb', line 5

def allocate(proportions)
  raise ArgumentError, 'Need at least 1 proportion element' if proportions.empty?

  whole = proportions.sum.to_r
  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) ⇒ Object



5
6
7
# File 'lib/minting/money/coercion.rb', line 5

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

#currency_codeObject



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

def currency_code
  currency.code
end

#eql?(other) ⇒ Boolean

Returns:

  • (Boolean)


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

def eql?(other)
  self == other
end

#format_amount(format) ⇒ Object



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

def format_amount(format)
  # binding.irb if format.is_a? Hash
  format = { positive: format } if format.is_a?(String)
  value = amount

  if amount.negative? && format[:negative]
    format = format[:negative]
    value = -amount
  elsif amount.zero? && format[:zero]
    format = format[:zero]
  else
    format = format[:positive]
  end
  format ||= '%<symbol>s%<amount>f'

  # Automatically adjust decimal places based on currency subunit
  adjusted_format = format.gsub(/%<amount>(\+?\d*)f/,
                                "%<amount>\\1.#{currency.subunit}f")

  Kernel.format(adjusted_format,
                amount: value,
                currency: currency_code,
                symbol: currency.symbol)
end

#hashObject



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

def hash
  zero? ? 0.hash : [amount, currency.code].hash
end

#inspectObject



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

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



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

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

#negative?Boolean

Returns:

  • (Boolean)


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

def negative? = amount.negative?

#nonzero?Boolean

Returns:

  • (Boolean)


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

def nonzero?
  amount.nonzero?
end

#positive?Boolean

Returns:

  • (Boolean)


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

def positive? = amount.positive?

#same_currency?(other) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#split(quantity) ⇒ Object



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

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

#succObject



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

def succ = mint(amount + currency.minimum_amount)

#to_dObject



4
5
6
# File 'lib/minting/money/conversion.rb', line 4

def to_d
  amount.to_d 0
end

#to_fObject



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

def to_f
  amount.to_f
end

#to_html(format = DEFAULT_FORMAT) ⇒ Object



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

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

#to_iObject



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

def to_i
  amount.to_i
end

#to_json(*_args) ⇒ Object



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

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

#to_rObject



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

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                           #=> "$1234.56"
money.to_s(thousand: ',')           #=> "$1,234.56"
money.to_s(decimal: ',')           #=> "$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)


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

def zero?
  amount.zero?
end