Empact-money 2.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +9 -0
- data/History.txt +0 -0
- data/MIT-LICENSE +21 -0
- data/Manifest.txt +24 -0
- data/README.rdoc +132 -0
- data/Rakefile +48 -0
- data/VERSION +1 -0
- data/lib/money.rb +25 -0
- data/lib/money/acts_as_money.rb +41 -0
- data/lib/money/core_extensions.rb +139 -0
- data/lib/money/errors.rb +4 -0
- data/lib/money/exchange_bank.rb +116 -0
- data/lib/money/money.rb +344 -0
- data/money.gemspec +64 -0
- data/rails/init.rb +3 -0
- data/spec/db/database.yml +4 -0
- data/spec/db/schema.rb +14 -0
- data/spec/money/acts_as_money_spec.rb +95 -0
- data/spec/money/core_extensions_spec.rb +61 -0
- data/spec/money/exchange_bank_spec.rb +141 -0
- data/spec/money/money_spec.rb +277 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +20 -0
- data/tmp/.gitignore +0 -0
- metadata +81 -0
data/lib/money/errors.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'money/errors'
|
2
|
+
require 'net/http'
|
3
|
+
require 'rubygems'
|
4
|
+
begin
|
5
|
+
require "nokogiri"
|
6
|
+
Parser = Nokogiri
|
7
|
+
rescue
|
8
|
+
require "hpricot"
|
9
|
+
Parser = Hpricot
|
10
|
+
end
|
11
|
+
# Class for aiding in exchanging money between different currencies.
|
12
|
+
# By default, the Money class uses an object of this class (accessible through
|
13
|
+
# Money#bank) for performing currency exchanges.
|
14
|
+
#
|
15
|
+
# By default, ExchangeBank has no knowledge about conversion rates.
|
16
|
+
# One must manually specify them with +add_rate+, after which one can perform
|
17
|
+
# exchanges with +exchange+. For example:
|
18
|
+
#
|
19
|
+
# bank = Money::ExchangeBank.new
|
20
|
+
# bank.add_rate("CAD", 0.803115)
|
21
|
+
# bank.add_rate("USD", 1.24515)
|
22
|
+
#
|
23
|
+
# # Exchange 100 CAD to USD:
|
24
|
+
# bank.exchange(100_00, "CAD", "USD") # => 15504
|
25
|
+
# # Exchange 100 USD to CAD:
|
26
|
+
# bank.exchange(100_00, "USD", "CAD") # => 6450
|
27
|
+
class Money
|
28
|
+
class ExchangeBank
|
29
|
+
# Returns the singleton instance of ExchangeBank.
|
30
|
+
#
|
31
|
+
# By default, <tt>Money.default_bank</tt> returns the same object.
|
32
|
+
def self.instance
|
33
|
+
@@singleton
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@rates = {}
|
38
|
+
@mutex = Mutex.new
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_rate(*params)
|
42
|
+
if rate = params.delete_at(2)
|
43
|
+
parse_rate(rate, *params)
|
44
|
+
else
|
45
|
+
parse_rate(params[1], Money.default_currency, params[0])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse_rate(rate,from,to)
|
50
|
+
return if from.upcase == to.upcase
|
51
|
+
@mutex.synchronize do
|
52
|
+
@rates["#{from}<>#{to}".upcase] = rate
|
53
|
+
@rates["#{to}<>#{from}".upcase] = 1.0/rate
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_rate(from, to = nil)
|
58
|
+
from, to = Money.default_currency, from unless to
|
59
|
+
@mutex.synchronize do
|
60
|
+
@rates["#{from}<>#{to}".upcase]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Given two currency names, checks whether they're both the same currency.
|
65
|
+
#
|
66
|
+
# bank = ExchangeBank.new
|
67
|
+
# bank.same_currency?("usd", "USD") # => true
|
68
|
+
# bank.same_currency?("usd", "EUR") # => false
|
69
|
+
def same_currency?(currency1, currency2)
|
70
|
+
currency1.upcase == currency2.upcase
|
71
|
+
end
|
72
|
+
|
73
|
+
# Exchange the given amount of cents in +from_currency+ to +to_currency+.
|
74
|
+
# Returns the amount of cents in +to_currency+ as an integer, rounded down.
|
75
|
+
#
|
76
|
+
# If the conversion rate is unknown, then Money::UnknownRate will be raised.
|
77
|
+
def exchange(cents, from_currency, to_currency)
|
78
|
+
rate = get_rate(from_currency, to_currency)
|
79
|
+
if !rate
|
80
|
+
raise Money::UnknownRate, "No conversion rate known for '#{from_currency}' -> '#{to_currency}'"
|
81
|
+
end
|
82
|
+
(cents * rate).floor
|
83
|
+
end
|
84
|
+
|
85
|
+
# Fetch rates
|
86
|
+
def fetch_rates
|
87
|
+
xml = Parser::XML(Net::HTTP.get(URI.parse('http://www.ecb.int/stats/eurofxref/eurofxref-daily.xml')))
|
88
|
+
curr = (xml/:Cube).select { |r| r["currency"] == Money.default_currency }.first
|
89
|
+
diff = Money.default_currency == "EUR" || !curr ? 1 : curr["rate"].to_f
|
90
|
+
(xml/:Cube).each do |x|
|
91
|
+
parse_rate x['rate'].to_f / diff, curr ? Money.default_currency : "EUR", x['currency'].upcase if x['currency']
|
92
|
+
end
|
93
|
+
parse_rate diff, Money.default_currency, "EUR" if curr
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
# Auto fetch the currencies every X seconds
|
98
|
+
# if no time is give, will fetch every hour
|
99
|
+
def auto_fetch(time = 60*60)
|
100
|
+
@auto_fetch.kill if (@auto_fetch && @auto_fetch.alive?)
|
101
|
+
@auto_fetch = Thread.new {
|
102
|
+
loop do
|
103
|
+
self.fetch_rates
|
104
|
+
sleep time
|
105
|
+
end
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
# stop auto fetch
|
110
|
+
def stop_fetch
|
111
|
+
@auto_fetch.kill if (@auto_fetch && @auto_fetch.alive?)
|
112
|
+
end
|
113
|
+
|
114
|
+
@@singleton = ExchangeBank.new
|
115
|
+
end
|
116
|
+
end
|
data/lib/money/money.rb
ADDED
@@ -0,0 +1,344 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'money/exchange_bank'
|
3
|
+
|
4
|
+
# Represents an amount of money in a certain currency.
|
5
|
+
class Money
|
6
|
+
include Comparable
|
7
|
+
attr_reader :cents, :currency, :bank
|
8
|
+
alias :to_i :cents
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Each Money object is associated to a bank object, which is responsible
|
12
|
+
# for currency exchange. This property allows one to specify the default
|
13
|
+
# bank object.
|
14
|
+
#
|
15
|
+
# bank1 = MyBank.new
|
16
|
+
# bank2 = MyOtherBank.new
|
17
|
+
#
|
18
|
+
# Money.default_bank = bank1
|
19
|
+
# money1 = Money.new(10)
|
20
|
+
# money1.bank # => bank1
|
21
|
+
#
|
22
|
+
# Money.default_bank = bank2
|
23
|
+
# money2 = Money.new(10)
|
24
|
+
# money2.bank # => bank2
|
25
|
+
# money1.bank # => bank1
|
26
|
+
#
|
27
|
+
# The default value for this property is an instance if VariableExchangeBank.
|
28
|
+
# It allows one to specify custom exchange rates:
|
29
|
+
#
|
30
|
+
# Money.default_bank.add_rate("USD", "CAD", 1.24515)
|
31
|
+
# Money.default_bank.add_rate("CAD", "USD", 0.803115)
|
32
|
+
# Money.us_dollar(100).exchange_to("CAD") # => Money.ca_dollar(124)
|
33
|
+
# Money.ca_dollar(100).exchange_to("USD") # => Money.us_dollar(80)
|
34
|
+
attr_accessor :default_bank
|
35
|
+
|
36
|
+
# the default currency, which is used when <tt>Money.new</tt> is called
|
37
|
+
# without an explicit currency argument. The default value is "USD".
|
38
|
+
attr_accessor :default_currency
|
39
|
+
end
|
40
|
+
|
41
|
+
self.default_bank = ExchangeBank.instance
|
42
|
+
self.default_currency = "USD"
|
43
|
+
|
44
|
+
CURRENCIES = {
|
45
|
+
"USD" => { :delimiter => ",", :separator => ".", :symbol => "$" },
|
46
|
+
"CAD" => { :delimiter => ",", :separator => ".", :symbol => "$" },
|
47
|
+
"HKD" => { :delimiter => ",", :separator => ".", :symbol => "$" },
|
48
|
+
"SGD" => { :delimiter => ",", :separator => ".", :symbol => "$" },
|
49
|
+
"BRL" => { :delimiter => ".", :separator => ",", :symbol => "R$" },
|
50
|
+
"EUR" => { :delimiter => ",", :separator => ".", :symbol => '€', :html => '€' },
|
51
|
+
"GBP" => { :delimiter => ",", :separator => ".", :symbol => '£', :html => '£' },
|
52
|
+
"JPY" => { :delimiter => ".", :separator => ".", :symbol => '¥', :html => '¥' },
|
53
|
+
}
|
54
|
+
|
55
|
+
# Create a new money object with value 0.
|
56
|
+
def self.empty(currency = default_currency)
|
57
|
+
Money.new(0, currency)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Creates a new Money object of the given value, using the Canadian dollar currency.
|
61
|
+
def self.ca_dollar(cents)
|
62
|
+
Money.new(cents, "CAD")
|
63
|
+
end
|
64
|
+
|
65
|
+
# Creates a new Money object of the given value, using the American dollar currency.
|
66
|
+
def self.us_dollar(cents)
|
67
|
+
Money.new(cents, "USD")
|
68
|
+
end
|
69
|
+
|
70
|
+
# Creates a new Money object of the given value, using the Euro currency.
|
71
|
+
def self.euro(cents)
|
72
|
+
Money.new(cents, "EUR")
|
73
|
+
end
|
74
|
+
|
75
|
+
# Creates a new Money object of the given value, using the Brazilian Real currency.
|
76
|
+
def self.real(cents)
|
77
|
+
Money.new(cents, "BRL")
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.add_rate(*params)
|
81
|
+
Money.default_bank.add_rate(*params)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Creates a new money object.
|
85
|
+
# Money.new(100)
|
86
|
+
#
|
87
|
+
# Alternativly you can use the convinience methods like
|
88
|
+
# Money.ca_dollar and Money.us_dollar
|
89
|
+
def initialize(cents, currency = nil, bank = nil)
|
90
|
+
@cents = cents.to_i
|
91
|
+
@currency = (currency || Money.default_currency).upcase
|
92
|
+
@bank = bank || Money.default_bank
|
93
|
+
end
|
94
|
+
|
95
|
+
# Do two money objects equal? Only works if both objects are of the same currency
|
96
|
+
def ==(other_money)
|
97
|
+
other_money.respond_to?(:cents) && cents == other_money.cents &&
|
98
|
+
other_money.respond_to?(:currency) && bank.same_currency?(currency, other_money.currency)
|
99
|
+
end
|
100
|
+
|
101
|
+
def <=>(other_money)
|
102
|
+
if bank.same_currency?(currency, other_money.currency)
|
103
|
+
cents <=> other_money.cents
|
104
|
+
else
|
105
|
+
cents <=> other_money.exchange_to(currency).cents
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def +(other_money)
|
110
|
+
other_money = Money.new(other_money) unless other_money.is_a? Money
|
111
|
+
if currency == other_money.currency
|
112
|
+
Money.new(cents + other_money.cents, other_money.currency)
|
113
|
+
else
|
114
|
+
Money.new(cents + other_money.exchange_to(currency).cents,currency)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def -(other_money)
|
119
|
+
other_money = Money.new(other_money) unless other_money.is_a? Money
|
120
|
+
if currency == other_money.currency
|
121
|
+
Money.new(cents - other_money.cents, other_money.currency)
|
122
|
+
else
|
123
|
+
Money.new(cents - other_money.exchange_to(currency).cents, currency)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# multiply money by fixnum
|
128
|
+
def *(fixnum)
|
129
|
+
Money.new(cents * fixnum, currency)
|
130
|
+
end
|
131
|
+
|
132
|
+
# divide money by fixnum
|
133
|
+
# check out split_in_installments method too
|
134
|
+
def /(fixnum)
|
135
|
+
Money.new(cents / fixnum, currency)
|
136
|
+
end
|
137
|
+
|
138
|
+
def %(fixnum)
|
139
|
+
Money.new(cents % fixnum, currency)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Test if the money amount is zero
|
143
|
+
def zero?
|
144
|
+
cents == 0
|
145
|
+
end
|
146
|
+
|
147
|
+
# Calculates compound interest
|
148
|
+
# Returns a money object with the sum of self + it
|
149
|
+
def compound_interest(rate, count = 1, period = 12)
|
150
|
+
Money.new(cents * ((1 + rate / 100.0 / period) ** count - 1))
|
151
|
+
end
|
152
|
+
|
153
|
+
# Calculate self + simple interest
|
154
|
+
def simple_interest(rate, count = 1, period = 12)
|
155
|
+
Money.new(rate / 100 / period * cents * count)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Round to nearest coin value
|
159
|
+
# basically, we don't have coins for cents in CZK,
|
160
|
+
# our smallest fraction is 0.50CZK
|
161
|
+
#
|
162
|
+
# Money.new(14_58).round_to_coin(50) => 14.50
|
163
|
+
#
|
164
|
+
def round_to_coin(coin)
|
165
|
+
coef = 1.0/coin
|
166
|
+
val = (cents * coef).floor / coef
|
167
|
+
Money.new(val, currency)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Returns array a where
|
171
|
+
# a[0] is price _after_ applying tax (tax base)
|
172
|
+
# a[1] is tax
|
173
|
+
def tax_breakdown(tax)
|
174
|
+
_tax = (cents * (tax / 100.0)).round
|
175
|
+
[Money.new(cents + _tax, currency), Money.new(_tax, currency)]
|
176
|
+
end
|
177
|
+
|
178
|
+
#Returns array a where
|
179
|
+
# a[0] is price _before_ applying tax (tax base)
|
180
|
+
# a[1] is tax
|
181
|
+
def tax_reverse_breakdown(tax)
|
182
|
+
coef = tax/100.0
|
183
|
+
[Money.new((cents / (1+coef)).round, currency),
|
184
|
+
Money.new((cents*coef/(1+coef)).round, currency) ]
|
185
|
+
end
|
186
|
+
|
187
|
+
# Just a helper if you got tax inputs in percentage.
|
188
|
+
# Ie. add_tax(20) => cents * 1.20
|
189
|
+
def add_tax(tax)
|
190
|
+
tax_breakdown(tax)[0]
|
191
|
+
end
|
192
|
+
|
193
|
+
# Split money in number of installments
|
194
|
+
#
|
195
|
+
# Money.new(10_00).split_in_installments(3)
|
196
|
+
# => [ 3.34, 3.33, 3.33 ] (All Money instances)
|
197
|
+
#
|
198
|
+
def split_in_installments(fixnum, order=false)
|
199
|
+
wallet = Wallet.new(fixnum, Money.new(cents/fixnum,currency))
|
200
|
+
to_add = cents % fixnum
|
201
|
+
to_add.times { |m| wallet[m] += Money.new(1) }
|
202
|
+
wallet.reverse! if order
|
203
|
+
wallet
|
204
|
+
end
|
205
|
+
|
206
|
+
# Split money in installments based on payment value
|
207
|
+
#
|
208
|
+
# Money.new(1000_00).split_in_installments(Money.new(300_00))
|
209
|
+
# => [ 334_00, 333_00, 333_00 ] (All Money instances)
|
210
|
+
#
|
211
|
+
def in_installments_of(other_money, order=false)
|
212
|
+
split_in_installments(cents/other_money.cents, order)
|
213
|
+
end
|
214
|
+
|
215
|
+
# Format the price according to several rules
|
216
|
+
# Currently supported are :with_currency, :no_cents, :symbol and :html
|
217
|
+
#
|
218
|
+
# with_currency:
|
219
|
+
#
|
220
|
+
# Money.ca_dollar(0).format => "free"
|
221
|
+
# Money.ca_dollar(100).format => "$1.00"
|
222
|
+
# Money.ca_dollar(100).format(:with_currency => true) => "$1.00 CAD"
|
223
|
+
# Money.us_dollar(85).format(:with_currency => true) => "$0.85 USD"
|
224
|
+
#
|
225
|
+
# no_cents:
|
226
|
+
#
|
227
|
+
# Money.ca_dollar(100).format(:no_cents) => "$1"
|
228
|
+
# Money.ca_dollar(599).format(:no_cents) => "$5"
|
229
|
+
#
|
230
|
+
# Money.ca_dollar(570).format(:no_cents, :with_currency) => "$5 CAD"
|
231
|
+
# Money.ca_dollar(39000).format(:no_cents) => "$390"
|
232
|
+
#
|
233
|
+
# symbol:
|
234
|
+
#
|
235
|
+
# Money.new(100, :currency => "GBP").format(:symbol => "£") => "£1.00"
|
236
|
+
#
|
237
|
+
# html:
|
238
|
+
#
|
239
|
+
# Money.ca_dollar(570).format(:html => true, :with_currency => true) => "$5.70 <span class=\"currency\">CAD</span>"
|
240
|
+
def format(*rules)
|
241
|
+
# support for old format parameters
|
242
|
+
rules = normalize_formatting_rules(rules)
|
243
|
+
|
244
|
+
if cents == 0
|
245
|
+
if rules[:display_free].respond_to?(:to_str)
|
246
|
+
return rules[:display_free]
|
247
|
+
elsif rules[:display_free]
|
248
|
+
return "free"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
if rules.has_key?(:symbol)
|
253
|
+
if rules[:symbol]
|
254
|
+
symbol = rules[:symbol]
|
255
|
+
else
|
256
|
+
symbol = ""
|
257
|
+
end
|
258
|
+
else
|
259
|
+
symbol = (CURRENCIES[currency] ? CURRENCIES[currency][:symbol] : "$")
|
260
|
+
end
|
261
|
+
self.currency
|
262
|
+
|
263
|
+
delimiter = (CURRENCIES[currency] ? CURRENCIES[currency][:delimiter] : "," )
|
264
|
+
separator = (CURRENCIES[currency] ? CURRENCIES[currency][:separator] : "." )
|
265
|
+
|
266
|
+
if rules[:no_cents]
|
267
|
+
formatted = sprintf("#{symbol}%d", cents.to_f / 100)
|
268
|
+
formatted.gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
|
269
|
+
else
|
270
|
+
formatted = sprintf("#{symbol}%.2f", cents.to_f / 100).split('.')
|
271
|
+
formatted[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
|
272
|
+
formatted = formatted.join(separator)
|
273
|
+
end
|
274
|
+
|
275
|
+
# Commify ("10000" => "10,000")
|
276
|
+
formatted.gsub!(/(\d)(?=\d{3}+(?:\.|$))(\d{3}\..*)?/,'\1,\2')
|
277
|
+
|
278
|
+
if rules[:with_currency]
|
279
|
+
formatted << " "
|
280
|
+
formatted << '<span class="currency">' if rules[:html]
|
281
|
+
formatted << currency
|
282
|
+
formatted << '</span>' if rules[:html]
|
283
|
+
end
|
284
|
+
formatted.gsub!(symbol,CURRENCIES[currency][:html]) if rules[:html]
|
285
|
+
formatted
|
286
|
+
end
|
287
|
+
|
288
|
+
def normalize_formatting_rules(rules)
|
289
|
+
if rules.size == 1
|
290
|
+
rules = rules.pop
|
291
|
+
rules = { rules => true } if rules.is_a?(Symbol)
|
292
|
+
else
|
293
|
+
rules = rules.inject({}) do |h,s|
|
294
|
+
h[s] = true
|
295
|
+
h
|
296
|
+
end
|
297
|
+
end
|
298
|
+
rules
|
299
|
+
end
|
300
|
+
|
301
|
+
# Money.ca_dollar(100).to_s => "1.00"
|
302
|
+
def to_s
|
303
|
+
sprintf("%.2f", cents / 100.0)
|
304
|
+
end
|
305
|
+
|
306
|
+
# Money.ca_dollar(100).to_f => "1.0"
|
307
|
+
def to_f
|
308
|
+
cents / 100.0
|
309
|
+
end
|
310
|
+
|
311
|
+
# Recieve the amount of this money object in another currency.
|
312
|
+
def exchange_to(other_currency)
|
313
|
+
Money.new(@bank.exchange(self.cents, currency, other_currency), other_currency)
|
314
|
+
end
|
315
|
+
|
316
|
+
# Conversation to self
|
317
|
+
def to_money
|
318
|
+
self
|
319
|
+
end
|
320
|
+
|
321
|
+
def method_missing(m,*x)
|
322
|
+
if m.to_s =~ /^as/
|
323
|
+
exchange_to(m.to_s.split("_").last.upcase)
|
324
|
+
else
|
325
|
+
super
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
#
|
331
|
+
# Represent a financial array.
|
332
|
+
# Investment/Time/Installments...TODO...
|
333
|
+
#
|
334
|
+
class Wallet < Array
|
335
|
+
|
336
|
+
def to_s
|
337
|
+
map &:to_s
|
338
|
+
end
|
339
|
+
|
340
|
+
def sum
|
341
|
+
Money.new(inject(0){ |sum,m| sum + m.cents })
|
342
|
+
end
|
343
|
+
|
344
|
+
end
|