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.
@@ -0,0 +1,4 @@
1
+ class Money
2
+ class UnknownRate < StandardError
3
+ end
4
+ end
@@ -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
@@ -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 => '&euro;' },
51
+ "GBP" => { :delimiter => ",", :separator => ".", :symbol => '£', :html => '&pound;' },
52
+ "JPY" => { :delimiter => ".", :separator => ".", :symbol => '¥', :html => '&yen;' },
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