# -*- coding: utf-8 -*-
require 'money/exchange_bank'
# Represents an amount of money in a certain currency.
class Money
include Comparable
attr_reader :cents, :currency, :bank
alias :to_i :cents
class << self
# Each Money object is associated to a bank object, which is responsible
# for currency exchange. This property allows one to specify the default
# bank object.
#
# bank1 = MyBank.new
# bank2 = MyOtherBank.new
#
# Money.default_bank = bank1
# money1 = Money.new(10)
# money1.bank # => bank1
#
# Money.default_bank = bank2
# money2 = Money.new(10)
# money2.bank # => bank2
# money1.bank # => bank1
#
# The default value for this property is an instance if VariableExchangeBank.
# It allows one to specify custom exchange rates:
#
# Money.default_bank.add_rate("USD", "CAD", 1.24515)
# Money.default_bank.add_rate("CAD", "USD", 0.803115)
# Money.us_dollar(100).exchange_to("CAD") # => Money.ca_dollar(124)
# Money.ca_dollar(100).exchange_to("USD") # => Money.us_dollar(80)
attr_accessor :default_bank
# the default currency, which is used when Money.new is called
# without an explicit currency argument. The default value is "USD".
attr_accessor :default_currency
end
self.default_bank = ExchangeBank.instance
self.default_currency = "USD"
CURRENCIES = {
"USD" => { :delimiter => ",", :separator => ".", :symbol => "$" },
"CAD" => { :delimiter => ",", :separator => ".", :symbol => "$" },
"HKD" => { :delimiter => ",", :separator => ".", :symbol => "$" },
"SGD" => { :delimiter => ",", :separator => ".", :symbol => "$" },
"BRL" => { :delimiter => ".", :separator => ",", :symbol => "R$" },
"EUR" => { :delimiter => ",", :separator => ".", :symbol => '€', :html => '€' },
"GBP" => { :delimiter => ",", :separator => ".", :symbol => '£', :html => '£' },
"JPY" => { :delimiter => ".", :separator => ".", :symbol => '¥', :html => '¥' },
}
# Create a new money object with value 0.
def self.empty(currency = default_currency)
Money.new(0, currency)
end
# Creates a new Money object of the given value, using the Canadian dollar currency.
def self.ca_dollar(cents)
Money.new(cents, "CAD")
end
# Creates a new Money object of the given value, using the American dollar currency.
def self.us_dollar(cents)
Money.new(cents, "USD")
end
# Creates a new Money object of the given value, using the Euro currency.
def self.euro(cents)
Money.new(cents, "EUR")
end
# Creates a new Money object of the given value, using the Brazilian Real currency.
def self.real(cents)
Money.new(cents, "BRL")
end
def self.add_rate(*params)
Money.default_bank.add_rate(*params)
end
# Creates a new money object.
# Money.new(100)
#
# Alternativly you can use the convinience methods like
# Money.ca_dollar and Money.us_dollar
def initialize(cents, currency = nil, bank = nil)
@cents = cents.to_i
@currency = (currency || Money.default_currency).upcase
@bank = bank || Money.default_bank
end
# Do two money objects equal? Only works if both objects are of the same currency
def ==(other_money)
other_money.respond_to?(:cents) && cents == other_money.cents &&
other_money.respond_to?(:currency) && bank.same_currency?(currency, other_money.currency)
end
def <=>(other_money)
if bank.same_currency?(currency, other_money.currency)
cents <=> other_money.cents
else
cents <=> other_money.exchange_to(currency).cents
end
end
def +(other_money)
other_money = Money.new(other_money) unless other_money.is_a? Money
if currency == other_money.currency
Money.new(cents + other_money.cents, other_money.currency)
else
Money.new(cents + other_money.exchange_to(currency).cents,currency)
end
end
def -(other_money)
other_money = Money.new(other_money) unless other_money.is_a? Money
if currency == other_money.currency
Money.new(cents - other_money.cents, other_money.currency)
else
Money.new(cents - other_money.exchange_to(currency).cents, currency)
end
end
# multiply money by fixnum
def *(fixnum)
Money.new(cents * fixnum, currency)
end
# divide money by fixnum
# check out split_in_installments method too
def /(fixnum)
Money.new(cents / fixnum, currency)
end
def %(fixnum)
Money.new(cents % fixnum, currency)
end
# Test if the money amount is zero
def zero?
cents == 0
end
# Calculates compound interest
# Returns a money object with the sum of self + it
def compound_interest(rate, count = 1, period = 12)
Money.new(cents * ((1 + rate / 100.0 / period) ** count - 1))
end
# Calculate self + simple interest
def simple_interest(rate, count = 1, period = 12)
Money.new(rate / 100 / period * cents * count)
end
# Round to nearest coin value
# basically, we don't have coins for cents in CZK,
# our smallest fraction is 0.50CZK
#
# Money.new(14_58).round_to_coin(50) => 14.50
#
def round_to_coin(coin)
coef = 1.0/coin
val = (cents * coef).floor / coef
Money.new(val, currency)
end
# Returns array a where
# a[0] is price _after_ applying tax (tax base)
# a[1] is tax
def tax_breakdown(tax)
_tax = (cents * (tax / 100.0)).round
[Money.new(cents + _tax, currency), Money.new(_tax, currency)]
end
#Returns array a where
# a[0] is price _before_ applying tax (tax base)
# a[1] is tax
def tax_reverse_breakdown(tax)
coef = tax/100.0
[Money.new((cents / (1+coef)).round, currency),
Money.new((cents*coef/(1+coef)).round, currency) ]
end
# Just a helper if you got tax inputs in percentage.
# Ie. add_tax(20) => cents * 1.20
def add_tax(tax)
tax_breakdown(tax)[0]
end
# Split money in number of installments
#
# Money.new(10_00).split_in_installments(3)
# => [ 3.34, 3.33, 3.33 ] (All Money instances)
#
def split_in_installments(fixnum, order=false)
wallet = Wallet.new(fixnum, Money.new(cents/fixnum,currency))
to_add = cents % fixnum
to_add.times { |m| wallet[m] += Money.new(1) }
wallet.reverse! if order
wallet
end
# Split money in installments based on payment value
#
# Money.new(1000_00).split_in_installments(Money.new(300_00))
# => [ 334_00, 333_00, 333_00 ] (All Money instances)
#
def in_installments_of(other_money, order=false)
split_in_installments(cents/other_money.cents, order)
end
# Format the price according to several rules
# Currently supported are :with_currency, :no_cents, :symbol and :html
#
# with_currency:
#
# Money.ca_dollar(0).format => "free"
# Money.ca_dollar(100).format => "$1.00"
# Money.ca_dollar(100).format(:with_currency => true) => "$1.00 CAD"
# Money.us_dollar(85).format(:with_currency => true) => "$0.85 USD"
#
# no_cents:
#
# Money.ca_dollar(100).format(:no_cents) => "$1"
# Money.ca_dollar(599).format(:no_cents) => "$5"
#
# Money.ca_dollar(570).format(:no_cents, :with_currency) => "$5 CAD"
# Money.ca_dollar(39000).format(:no_cents) => "$390"
#
# symbol:
#
# Money.new(100, :currency => "GBP").format(:symbol => "£") => "£1.00"
#
# html:
#
# Money.ca_dollar(570).format(:html => true, :with_currency => true) => "$5.70 CAD"
def format(*rules)
# support for old format parameters
rules = normalize_formatting_rules(rules)
if cents == 0
if rules[:display_free].respond_to?(:to_str)
return rules[:display_free]
elsif rules[:display_free]
return "free"
end
end
if rules.has_key?(:symbol)
if rules[:symbol]
symbol = rules[:symbol]
else
symbol = ""
end
else
symbol = (CURRENCIES[currency] ? CURRENCIES[currency][:symbol] : "$")
end
self.currency
delimiter = (CURRENCIES[currency] ? CURRENCIES[currency][:delimiter] : "," )
separator = (CURRENCIES[currency] ? CURRENCIES[currency][:separator] : "." )
if rules[:no_cents]
formatted = sprintf("#{symbol}%d", cents.to_f / 100)
formatted.gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
else
formatted = sprintf("#{symbol}%.2f", cents.to_f / 100).split('.')
formatted[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
formatted = formatted.join(separator)
end
# Commify ("10000" => "10,000")
formatted.gsub!(/(\d)(?=\d{3}+(?:\.|$))(\d{3}\..*)?/,'\1,\2')
if rules[:with_currency]
formatted << " "
formatted << '' if rules[:html]
formatted << currency
formatted << '' if rules[:html]
end
formatted.gsub!(symbol,CURRENCIES[currency][:html]) if rules[:html]
formatted
end
def normalize_formatting_rules(rules)
if rules.size == 1
rules = rules.pop
rules = { rules => true } if rules.is_a?(Symbol)
else
rules = rules.inject({}) do |h,s|
h[s] = true
h
end
end
rules
end
# Money.ca_dollar(100).to_s => "1.00"
def to_s
sprintf("%.2f", cents / 100.0)
end
# Money.ca_dollar(100).to_f => "1.0"
def to_f
cents / 100.0
end
# Recieve the amount of this money object in another currency.
def exchange_to(other_currency)
Money.new(@bank.exchange(self.cents, currency, other_currency), other_currency)
end
# Conversation to self
def to_money
self
end
def method_missing(m,*x)
if m.to_s =~ /^as/
exchange_to(m.to_s.split("_").last.upcase)
else
super
end
end
end
#
# Represent a financial array.
# Investment/Time/Installments...TODO...
#
class Wallet < Array
def to_s
map &:to_s
end
def sum
Money.new(inject(0){ |sum,m| sum + m.cents })
end
end