# === Usage with ActiveRecord
#
# Use the compose_of helper to let active record deal with embedding the money
# object in your models. The following example requires a cents and a currency field.
#
# class ProductUnit < ActiveRecord::Base
# belongs_to :product
# composed_of :price, :class_name => "Money", :mapping => [ %w(cents cents), %w(currency currency) ]
#
# private
# validate :cents_not_zero
#
# def cents_not_zero
# errors.add("cents", "cannot be zero or less") unless cents > 0
# end
#
# validates_presence_of :sku, :currency
# validates_uniqueness_of :sku
# end
#
class Money
include Comparable
attr_reader :cents, :currency
class MoneyError < StandardError# :nodoc:
end
# Bank lets you exchange the object which is responsible for currency
# exchange.
# The default implementation just throws an exception. However money
# ships with a variable exchange bank implementation which supports
# custom excahnge rates:
#
# Money.bank = VariableExchangeBank.new
# Money.bank.add_rate("USD", "CAD", 1.24515)
# Money.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)
@@bank = NoExchangeBank.new
cattr_accessor :bank
@@default_currency = "USD"
cattr_accessor :default_currency
# 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 = default_currency)
@cents, @currency = cents, currency
end
# Do two money objects equal? Only works if both objects are of the same currency
def eql?(other_money)
cents == other_money.cents && currency == other_money.currency
end
def <=>(other_money)
if currency == other_money.currency
cents <=> other_money.cents
else
cents <=> other_money.exchange_to(currency).cents
end
end
def +(other_money)
if self.cents == 0 or 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)
if self.cents == 0 or 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
# get the cents value of the object
def cents
@cents.to_i
end
# multiply money by fixnum
def *(fixnum)
Money.new(cents * fixnum, currency)
end
# divide money by fixnum
def /(fixnum)
Money.new(cents / fixnum, currency)
end
# Format the price according to several rules
# Currently supported are :with_currency, :no_cents and :html
#
# with_currency:
#
# Money.ca_dollar(0).format => "free"
# Money.ca_dollar(100).format => "$1.00"
# Money.ca_dollar(100).format(:with_currency) => "$1.00 CAD"
# Money.us_dollar(85).format(:with_currency) => "$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"
#
# html:
#
# Money.ca_dollar(570).format(:html, :with_currency) => "$5.70 CAD"
def format(*rules)
return "free" if cents == 0
rules = rules.flatten
if rules.include?(:no_cents)
formatted = sprintf("$%d", cents.to_f / 100 )
else
formatted = sprintf("$%.2f", cents.to_f / 100 )
end
if rules.include?(:with_currency)
formatted << " "
formatted << '' if rules.include?(:html)
formatted << currency
formatted << '' if rules.include?(:html)
end
formatted
end
# Money.ca_dollar(100).to_s => "$1.00 CAD"
def to_s
format(:with_currency)
end
# Recieve the amount of this money object in another currency
def exchange_to(other_currency)
self.class.bank.reduce(self, other_currency)
end
# Create a new money object with value 0
def self.empty(currency = default_currency)
Money.new(0, currency)
end
# Create a new money object using the Canadian dollar currency
def self.ca_dollar(num)
Money.new(num, "CAD")
end
# Create a new money object using the American dollar currency
def self.us_dollar(num)
Money.new(num, "USD")
end
# Create a new money object using the Euro currency
def self.euro(num)
Money.new(num, "EUR")
end
# Recieve a money object with the same amount as the current Money object
# in american dollar
def as_us_dollar
exchange_to("USD")
end
# Recieve a money object with the same amount as the current Money object
# in canadian dollar
def as_ca_dollar
exchange_to("CAD")
end
# Recieve a money object with the same amount as the current Money object
# in euro
def as_ca_euro
exchange_to("EUR")
end
# Conversation to self
def to_money
self
end
end