| 1 |
module Globalize |
|---|
| 2 |
|
|---|
| 3 |
=begin rdoc |
|---|
| 4 |
This is what you use for representing money in your ActiveRecord models. |
|---|
| 5 |
It stores values as integers internally and in the database, to safeguard |
|---|
| 6 |
precision and rounding. |
|---|
| 7 |
|
|---|
| 8 |
More importantly for globalization freaks, it prints out the amount correctly |
|---|
| 9 |
in the current locale, via the handy #format method. Try it! |
|---|
| 10 |
|
|---|
| 11 |
Example: |
|---|
| 12 |
|
|---|
| 13 |
class Product < ActiveRecord::Base |
|---|
| 14 |
composed_of :price, :class_name => "Globalize::Currency", |
|---|
| 15 |
:mapping => [ %w(price cents) ] |
|---|
| 16 |
end |
|---|
| 17 |
|
|---|
| 18 |
product.price -> "SFr. 483'232.43" |
|---|
| 19 |
=end |
|---|
| 20 |
class Currency |
|---|
| 21 |
include Comparable |
|---|
| 22 |
|
|---|
| 23 |
attr_reader :cents |
|---|
| 24 |
|
|---|
| 25 |
class CurrencyError < StandardError# :nodoc: |
|---|
| 26 |
end |
|---|
| 27 |
|
|---|
| 28 |
# Creates a new Currency object, given cents. This is used to tie into |
|---|
| 29 |
# the ActiveRecord aggregation stuff. |
|---|
| 30 |
def initialize(cents) |
|---|
| 31 |
@cents = cents.nil? ? nil : cents.to_i |
|---|
| 32 |
end |
|---|
| 33 |
|
|---|
| 34 |
@@no_cents = false |
|---|
| 35 |
def self.no_cents; @@no_cents end |
|---|
| 36 |
def self.no_cents=(val); @@no_cents = val end |
|---|
| 37 |
|
|---|
| 38 |
@@free = Currency.new(0) |
|---|
| 39 |
@@free.freeze |
|---|
| 40 |
|
|---|
| 41 |
# Returns the Currency object representing a price of 0. |
|---|
| 42 |
def self.free |
|---|
| 43 |
@@free |
|---|
| 44 |
end |
|---|
| 45 |
|
|---|
| 46 |
@@na = Currency.new(nil) |
|---|
| 47 |
@@na.freeze |
|---|
| 48 |
|
|---|
| 49 |
# Returns the Currency object representing an unknown price. |
|---|
| 50 |
def self.na |
|---|
| 51 |
@@na |
|---|
| 52 |
end |
|---|
| 53 |
|
|---|
| 54 |
def <=>(other) |
|---|
| 55 |
if other.respond_to? :cents |
|---|
| 56 |
if na? |
|---|
| 57 |
other.na? ? 0 : 1 |
|---|
| 58 |
else |
|---|
| 59 |
other.na? ? -1 : cents <=> other.cents |
|---|
| 60 |
end |
|---|
| 61 |
elsif other.kind_of? Integer |
|---|
| 62 |
na? ? 1 : cents <=> other |
|---|
| 63 |
else |
|---|
| 64 |
raise "can only compare with money or integer" |
|---|
| 65 |
end |
|---|
| 66 |
end |
|---|
| 67 |
|
|---|
| 68 |
def +(other_money) |
|---|
| 69 |
raise TypeError, "parameter to Currency#+ must be Currency object" unless |
|---|
| 70 |
other_money.kind_of? Currency |
|---|
| 71 |
(na? || other_money.na?) ? Currency.na : |
|---|
| 72 |
Currency.new(cents + other_money.cents) |
|---|
| 73 |
end |
|---|
| 74 |
|
|---|
| 75 |
def -(other_money) |
|---|
| 76 |
raise TypeError, "parameter to Currency#+ must be Currency object" unless |
|---|
| 77 |
other_money.kind_of? Currency |
|---|
| 78 |
(na? || other_money.na?) ? Currency.na : |
|---|
| 79 |
Currency.new(cents - other_money.cents) |
|---|
| 80 |
end |
|---|
| 81 |
|
|---|
| 82 |
# Multiply money by amount |
|---|
| 83 |
def *(amount) |
|---|
| 84 |
return Currency.new(nil) if na? |
|---|
| 85 |
new_cents = amount * cents; |
|---|
| 86 |
new_cents = new_cents.round if new_cents.respond_to? :round |
|---|
| 87 |
Currency.new(new_cents) |
|---|
| 88 |
end |
|---|
| 89 |
|
|---|
| 90 |
# Divide money by amount |
|---|
| 91 |
def /(amount) |
|---|
| 92 |
return Currency.new(nil) if na? |
|---|
| 93 |
new_cents = cents / amount; |
|---|
| 94 |
new_cents = new_cents.round if new_cents.respond_to? :round |
|---|
| 95 |
Currency.new(new_cents) |
|---|
| 96 |
end |
|---|
| 97 |
|
|---|
| 98 |
# Returns the formatted version of the amount in local currency. |
|---|
| 99 |
# If <tt>:code => true</tt> is specified, format using international |
|---|
| 100 |
# 3-letter currency code. If <tt>:country</tt> is specified as well, |
|---|
| 101 |
# use that country's currency code for formatting. |
|---|
| 102 |
def format(options = {}) |
|---|
| 103 |
return :no_price_available.t("call for price") if na? |
|---|
| 104 |
|
|---|
| 105 |
force_cents = options[:force_cents] |
|---|
| 106 |
if options[:code] |
|---|
| 107 |
currency_code = options[:country] ? |
|---|
| 108 |
options[:country].currency_code : |
|---|
| 109 |
( Locale.active? ? Locale.active.currency_code : nil ) |
|---|
| 110 |
currency_code ? |
|---|
| 111 |
self.amount(false, force_cents) + " " + currency_code : |
|---|
| 112 |
self.amount(false, force_cents) |
|---|
| 113 |
else |
|---|
| 114 |
if Locale.active? |
|---|
| 115 |
fmt = Locale.active.currency_format || '$%n' |
|---|
| 116 |
fmt.sub('%n', self.amount(false, force_cents)) |
|---|
| 117 |
else |
|---|
| 118 |
self.amount false, force_cents |
|---|
| 119 |
end |
|---|
| 120 |
end |
|---|
| 121 |
end |
|---|
| 122 |
|
|---|
| 123 |
# Returns the formatted version of the amount, but without the currency symbol. |
|---|
| 124 |
# If +unlocalized+ is true, do not format the number according to the current locale, |
|---|
| 125 |
# so <tt>Currency.new(1234567) -> 12345.67</tt>. This is useful for sending the |
|---|
| 126 |
# amount to payment gateways. |
|---|
| 127 |
def amount(unlocalized = false, force_cents = false) |
|---|
| 128 |
return nil if na? |
|---|
| 129 |
decimal_sep = unlocalized ? |
|---|
| 130 |
'.' : |
|---|
| 131 |
(Locale.active? ? |
|---|
| 132 |
(Locale.active.currency_decimal_sep || Locale.active.decimal_sep || '.') : |
|---|
| 133 |
'.') |
|---|
| 134 |
dollar_str = unlocalized ? dollar_part.to_s : dollar_part.localize |
|---|
| 135 |
result = dollar_str |
|---|
| 136 |
result << decimal_sep + sprintf("%02d", cent_part) unless |
|---|
| 137 |
self.class.no_cents && !force_cents |
|---|
| 138 |
return result |
|---|
| 139 |
end |
|---|
| 140 |
|
|---|
| 141 |
# Same as #format with no arguments. |
|---|
| 142 |
def to_s |
|---|
| 143 |
self.format |
|---|
| 144 |
end |
|---|
| 145 |
|
|---|
| 146 |
# Parse a string or number into a currency object. Easier to use than #new. |
|---|
| 147 |
def self.parse(num) |
|---|
| 148 |
case |
|---|
| 149 |
when num.is_a?(String) |
|---|
| 150 |
raise ArgumentError, "Not an amount (#{num})" if num.delete("^0-9").empty? |
|---|
| 151 |
_dollars, _cents = num.delete("^0-9.").split('.', 2) |
|---|
| 152 |
_cents = _cents ? _cents[0,2] : 0 |
|---|
| 153 |
Currency.new(_dollars.to_i * 100 + _cents.to_i) |
|---|
| 154 |
when num.is_a?(Numeric) |
|---|
| 155 |
Currency.new(num * 100) |
|---|
| 156 |
when num.is_a?(NilClass) |
|---|
| 157 |
Currency.na |
|---|
| 158 |
else |
|---|
| 159 |
raise ArgumentError, "Unrecognized object #{num.class.name} for Currency" |
|---|
| 160 |
end |
|---|
| 161 |
end |
|---|
| 162 |
|
|---|
| 163 |
# Conversion to self |
|---|
| 164 |
def to_currency |
|---|
| 165 |
self |
|---|
| 166 |
end |
|---|
| 167 |
|
|---|
| 168 |
# Is the value 0? Is it free? |
|---|
| 169 |
def empty? |
|---|
| 170 |
cents == 0 |
|---|
| 171 |
end |
|---|
| 172 |
|
|---|
| 173 |
# Is the value unknown? |
|---|
| 174 |
def na? |
|---|
| 175 |
cents.nil? |
|---|
| 176 |
end |
|---|
| 177 |
|
|---|
| 178 |
private |
|---|
| 179 |
def dollar_part |
|---|
| 180 |
na? ? nil : cents / 100 |
|---|
| 181 |
end |
|---|
| 182 |
|
|---|
| 183 |
def cent_part |
|---|
| 184 |
na? ? nil : cents % 100 |
|---|
| 185 |
end |
|---|
| 186 |
|
|---|
| 187 |
end |
|---|
| 188 |
end |
|---|