currency 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/COPYING.txt +339 -0
  2. data/LICENSE.txt +62 -0
  3. data/Manifest.txt +37 -14
  4. data/README.txt +8 -0
  5. data/Rakefile +42 -8
  6. data/Releases.txt +26 -0
  7. data/TODO.txt +1 -0
  8. data/examples/ex1.rb +3 -3
  9. data/examples/xe1.rb +3 -2
  10. data/lib/currency.rb +71 -9
  11. data/lib/currency/active_record.rb +138 -21
  12. data/lib/currency/core_extensions.rb +7 -5
  13. data/lib/currency/currency.rb +94 -177
  14. data/lib/currency/{currency_factory.rb → currency/factory.rb} +46 -25
  15. data/lib/currency/currency_version.rb +3 -3
  16. data/lib/currency/exception.rb +14 -14
  17. data/lib/currency/exchange.rb +14 -12
  18. data/lib/currency/exchange/rate.rb +159 -28
  19. data/lib/currency/exchange/rate/deriver.rb +146 -0
  20. data/lib/currency/exchange/rate/source.rb +84 -0
  21. data/lib/currency/exchange/rate/source/base.rb +156 -0
  22. data/lib/currency/exchange/rate/source/failover.rb +57 -0
  23. data/lib/currency/exchange/rate/source/historical.rb +79 -0
  24. data/lib/currency/exchange/rate/source/historical/rate.rb +181 -0
  25. data/lib/currency/exchange/rate/source/historical/writer.rb +203 -0
  26. data/lib/currency/exchange/rate/source/new_york_fed.rb +91 -0
  27. data/lib/currency/exchange/rate/source/provider.rb +105 -0
  28. data/lib/currency/exchange/rate/source/test.rb +50 -0
  29. data/lib/currency/exchange/rate/source/the_financials.rb +190 -0
  30. data/lib/currency/exchange/rate/source/timed_cache.rb +144 -0
  31. data/lib/currency/exchange/rate/source/xe.rb +166 -0
  32. data/lib/currency/exchange/time_quantitizer.rb +111 -0
  33. data/lib/currency/formatter.rb +159 -0
  34. data/lib/currency/macro.rb +321 -0
  35. data/lib/currency/money.rb +90 -64
  36. data/lib/currency/money_helper.rb +6 -5
  37. data/lib/currency/parser.rb +153 -0
  38. data/test/ar_column_test.rb +6 -3
  39. data/test/ar_simple_test.rb +5 -2
  40. data/test/ar_test_base.rb +39 -33
  41. data/test/ar_test_core.rb +64 -0
  42. data/test/formatter_test.rb +81 -0
  43. data/test/historical_writer_test.rb +184 -0
  44. data/test/macro_test.rb +109 -0
  45. data/test/money_test.rb +72 -4
  46. data/test/new_york_fed_test.rb +57 -0
  47. data/test/parser_test.rb +60 -0
  48. data/test/test_base.rb +13 -3
  49. data/test/time_quantitizer_test.rb +136 -0
  50. data/test/xe_test.rb +29 -5
  51. metadata +41 -18
  52. data/lib/currency/exchange/base.rb +0 -84
  53. data/lib/currency/exchange/test.rb +0 -39
  54. data/lib/currency/exchange/xe.rb +0 -250
@@ -0,0 +1,111 @@
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ # = Currency::Exchange::TimeQuantitizer
5
+ #
6
+ # The Currency::Exchange::TimeQuantitizer quantitizes time values
7
+ # such that money values and rates at a given time
8
+ # can be turned into a hash key, depending
9
+ # on the rate source's temporal accuracy.
10
+ #
11
+ class Currency::Exchange::TimeQuantitizer
12
+
13
+ def self.current; @current ||= self.new; end
14
+ def self.current=(x); @current = x; end
15
+
16
+ # Time quantitization size.
17
+ # Defaults to 1 day.
18
+ attr_accessor :time_quant_size
19
+
20
+ # Time quantization offset in seconds.
21
+ # This is applied to epoch time before quantization.
22
+ # If nil, uses Time#utc_offset.
23
+ # Defaults to nil.
24
+ attr_accessor :time_quant_offset
25
+
26
+ def initialize(*opt)
27
+ @time_quant_size ||= 60 * 60 * 24
28
+ @time_quant_offset ||= nil
29
+ opt = Hash[*opt]
30
+ opt.each_pair{|k,v| self.send("#{k}=", v)}
31
+ end
32
+
33
+
34
+ # Normalizes time to a quantitized value.
35
+ # For example: a time_quant_size of 60 * 60 * 24 will
36
+ # truncate a rate time to a particular day.
37
+ #
38
+ # Subclasses can override this method.
39
+ def quantitize_time(time)
40
+ # If nil, then nil.
41
+ return time unless time
42
+
43
+ # Get bucket parameters.
44
+ was_utc = time.utc?
45
+ quant_offset = time_quant_offset
46
+ quant_offset ||= time.utc_offset
47
+ # $stderr.puts "quant_offset = #{quant_offset}"
48
+ quant_size = time_quant_size.to_i
49
+
50
+ # Get offset from epoch.
51
+ time = time.tv_sec
52
+
53
+ # Remove offset (timezone)
54
+ time += quant_offset
55
+
56
+ # Truncate to quantitize size.
57
+ time = (time.to_i / quant_size) * quant_size
58
+
59
+ # Add offset (timezone)
60
+ time -= quant_offset
61
+
62
+ # Convert back to Time object.
63
+ time = Time.at(time)
64
+
65
+ # Quant to day?
66
+ # NOTE: is this due to a Ruby bug, or
67
+ # some wierd UTC time-flow issue, like leap-seconds.
68
+ if quant_size == 60 * 60 * 24
69
+ time = time + 60 * 60
70
+ if was_utc
71
+ time = time.getutc
72
+ time = Time.utc(time.year, time.month, time.day, 0, 0, 0, 0)
73
+ else
74
+ time = Time.local(time.year, time.month, time.day, 0, 0, 0, 0)
75
+ end
76
+ end
77
+
78
+ # Convert back to UTC?
79
+ time = time.getutc if was_utc
80
+
81
+ time
82
+ end
83
+
84
+ # Returns a Range of Time such that:
85
+ #
86
+ # range.include?(time)
87
+ # ! range.include?(time + time_quant_size)
88
+ # ! range.include?(time - time_quant_size)
89
+ # range.exclude_end?
90
+ #
91
+ # The range.max is end-exclusive to avoid precision issues:
92
+ #
93
+ # t = Time.now
94
+ # => Thu Feb 15 15:32:34 EST 2007
95
+ # x.quantitize_time_range(t)
96
+ # => Thu Feb 15 00:00:00 EST 2007...Fri Feb 16 00:00:00 EST 2007
97
+ #
98
+ def quantitize_time_range(time)
99
+ time_0 = quantitize_time(time)
100
+ time_1 = time_0 + time_quant_size.to_i
101
+ time_0 ... time_1
102
+ end
103
+
104
+ # Returns a simple string rep.
105
+ def to_s
106
+ "#<#{self.class.name} #{quant_offset} #{quant_size}>"
107
+ end
108
+
109
+ end # class
110
+
111
+
@@ -0,0 +1,159 @@
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+
5
+ # This class formats a Money value as a String.
6
+ # Each Currency has a default Formatter.
7
+ class Currency::Formatter
8
+ # Defaults to ','
9
+ attr_accessor :thousands_separator
10
+
11
+ # Defaults to '.'
12
+ attr_accessor :decimal_separator
13
+
14
+ # If true, insert _thousands_separator_ between each 3 digits in the whole value.
15
+ attr_accessor :thousands
16
+
17
+ # If true, append _decimal_separator_ and decimal digits after whole value.
18
+ attr_accessor :cents
19
+
20
+ # If true, prefix value with currency symbol.
21
+ attr_accessor :symbol
22
+
23
+ # If true, append currency code.
24
+ attr_accessor :code
25
+
26
+ # If true, use html formatting.
27
+ #
28
+ # Currency::Money(12.45, :EUR).to_s(:html => true; :code => true)
29
+ # => "&#8364;12.45 <span class=\"currency_code\">EUR</span>"
30
+
31
+ attr_accessor :html
32
+
33
+
34
+ # If passed true, formats for an input field (i.e.: as a number).
35
+ def as_input_value=(x)
36
+ if x
37
+ self.thousands_separator = ''
38
+ self.decimal_separator = '.'
39
+ self.thousands = false
40
+ self.cents = true
41
+ self.symbol = false
42
+ self.code = false
43
+ self.html = false
44
+ end
45
+ end
46
+
47
+
48
+ @@default = nil
49
+ # Get the default Formatter.
50
+ def self.default
51
+ @@default || self.new
52
+ end
53
+
54
+
55
+ # Set the default Formatter.
56
+ def self.default=(x)
57
+ @@default = x
58
+ end
59
+
60
+
61
+ def initialize(opt = { })
62
+ @thousands_separator = ','
63
+ @decimal_separator = '.'
64
+ @thousands = true
65
+ @cents = true
66
+ @symbol = true
67
+ @code = false
68
+ @html = false
69
+
70
+ opt.each_pair{ | k, v | self.send("#{k}=", v) }
71
+ end
72
+
73
+
74
+ def currency=(x) # :nodoc:
75
+ # DO NOTHING!
76
+ end
77
+
78
+
79
+ def _format(m, currency = nil) # :nodoc:
80
+ # Get currency.
81
+ currency ||= m.currency
82
+
83
+ # Get scaled integer representation for this Currency.
84
+ # $stderr.puts "m.currency = #{m.currency}, currency => #{currency}"
85
+ x = m.Money_rep(currency)
86
+
87
+ # Remove sign.
88
+ x = - x if ( neg = x < 0 )
89
+
90
+ # Convert to String.
91
+ x = x.to_s
92
+
93
+ # Keep prefixing "0" until filled to scale.
94
+ while ( x.length <= currency.scale_exp )
95
+ x = "0" + x
96
+ end
97
+
98
+ # Insert decimal place.
99
+ whole = x[0 .. currency.format_left]
100
+ decimal = x[currency.format_right .. -1]
101
+
102
+ # Do commas
103
+ x = whole
104
+ if @thousands && (@thousands_separator && ! @thousands_separator.empty?)
105
+ x.reverse!
106
+ x.gsub!(/(\d\d\d)/) {|y| y + @thousands_separator}
107
+ x.sub!(/#{@thousands_separator}$/,'')
108
+ x.reverse!
109
+ end
110
+
111
+ x << @decimal_separator + decimal if @cents && @decimal_separator
112
+
113
+ # Put sign back.
114
+ x = '-' + x if neg
115
+
116
+ # Add symbol?
117
+ x = ((@html && currency.symbol_html) || currency.symbol || '') + x if @symbol
118
+
119
+ # Suffix with currency code.
120
+ if @code
121
+ x << (' ' + _format_Currency(currency))
122
+ end
123
+
124
+ x
125
+ end
126
+
127
+
128
+ def _format_Currency(c) # :nodoc:
129
+ x = ''
130
+ x << '<span class="currency_code">' if @html
131
+ x << c.code.to_s
132
+ x << '</span>' if @html
133
+ x
134
+ end
135
+
136
+
137
+ @@empty_hash = { }
138
+ @@empty_hash.freeze
139
+
140
+ # Format a Money object as a String.
141
+ #
142
+ # m = Money.new("1234567.89")
143
+ # m.to_s(:code => true, :symbol => false)
144
+ # => "1,234,567.89 USD"
145
+ #
146
+ def format(m, opt = @@empty_hash)
147
+ fmt = self
148
+
149
+ unless opt.empty?
150
+ fmt = fmt.clone
151
+ opt.each_pair{ | k, v | fmt.send("#{k}=", v) }
152
+ end
153
+
154
+ # $stderr.puts "format(opt = #{opt.inspect})"
155
+ fmt._format(m, opt[:currency]) # Allow override of current currency.
156
+ end
157
+
158
+ end # class
159
+
@@ -0,0 +1,321 @@
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ class Currency::Money
5
+ @@money_attributes = { }
6
+
7
+ # Called by money macro when a money attribute
8
+ # is created.
9
+ def self.register_money_attribute(attr_opts)
10
+ (@@money_attributes[attr_opts[:class]] ||= { })[attr_opts[:attr_name]] = attr_opts
11
+ end
12
+
13
+ # Returns an array of option hashes for all the money attributes of
14
+ # this class.
15
+ #
16
+ # Superclass attributes are not included.
17
+ def self.money_attributes_for_class(cls)
18
+ (@@money_atttributes[cls] || { }).values
19
+ end
20
+
21
+ # Iterates through all known money attributes in all classes.
22
+ #
23
+ # each_money_attribute { | money_opts |
24
+ # ...
25
+ # }
26
+ def self.each_money_attribute(&blk)
27
+ @@money_attributes.each do | cls, hash |
28
+ hash.each do | attr_name, attr_opts |
29
+ yield attr_opts
30
+ end
31
+ end
32
+ end
33
+
34
+ end # class
35
+
36
+
37
+ module Currency::Macro
38
+ def self.append_features(base) # :nodoc:
39
+ # $stderr.puts " Currency::ActiveRecord#append_features(#{base})"
40
+ super
41
+ base.extend(ClassMethods)
42
+ end
43
+
44
+
45
+
46
+ # == Macro Suppport
47
+ #
48
+ # Support for Money attributes.
49
+ #
50
+ # require 'currency'
51
+ # require 'currency/macro'
52
+ #
53
+ # class SomeClass
54
+ # include ::Currency::Macro
55
+ # attr_accessor :amount
56
+ # attr_money :amount_money, :value => :amount, :currency_fixed => :USD, :rep => :float
57
+ # end
58
+ #
59
+ # x = SomeClass.new
60
+ # x.amount = 123.45
61
+ # x.amount
62
+ # # => 123.45
63
+ # x.amount_money
64
+ # # => $123.45 USD
65
+ # x.amount_money = x.amount_money + "12.45"
66
+ # # => $135.90 USD
67
+ # x.amount
68
+ # # => 135.9
69
+ # x.amount = 45.951
70
+ # x.amount_money
71
+ # # => $45.95 USD
72
+ # x.amount
73
+ # # => 45.951
74
+ #
75
+ module ClassMethods
76
+
77
+ # Defines a Money object attribute that is bound
78
+ # to other attributes.
79
+ #
80
+ # Options:
81
+ #
82
+ # :value => undef
83
+ #
84
+ # Defines the value attribute to use for storing the money value.
85
+ # Defaults to the attribute name.
86
+ #
87
+ # If this attribute is different from the attribute name,
88
+ # the money object will intercept #{value}=(x) to flush
89
+ # any cached Money object.
90
+ #
91
+ # :readonly => false
92
+ #
93
+ # If true, the underlying attribute is readonly. Thus the Money object
94
+ # cannot be cached. This is useful for computed money values.
95
+ #
96
+ # :rep => :float
97
+ #
98
+ # This option specifies how the value attribute stores Money values.
99
+ # if :rep is :rep, then the value is stored as a scaled integer as
100
+ # defined by the Currency.
101
+ # If :rep is :float, or :integer the corresponding #to_f or #to_i
102
+ # method is used.
103
+ # Defaults to :float.
104
+ #
105
+ # :currency => undef
106
+ #
107
+ # Defines the attribute used to store and
108
+ # retrieve the Money's Currency 3-letter ISO code.
109
+ #
110
+ # :currency_fixed => currency_code (e.g.: :USD)
111
+ #
112
+ # Defines the Currency to use for storing a normalized Money
113
+ # value.
114
+ #
115
+ # All Money values will be converted to this Currency before
116
+ # storing. This allows SQL summary operations,
117
+ # like SUM(), MAX(), AVG(), etc., to produce meaningful results,
118
+ # regardless of the initial currency specified. If this
119
+ # option is used, subsequent reads will be in the specified
120
+ # normalization :currency_fixed.
121
+ #
122
+ # :currency_preferred => undef
123
+ #
124
+ # Defines the name of attribute used to store and
125
+ # retrieve the Money's Currency ISO code. This option can be used
126
+ # with normalized Money values to retrieve the Money value
127
+ # in its original Currency, while
128
+ # allowing SQL summary operations on the normalized Money values
129
+ # to still be valid.
130
+ #
131
+ # :currency_update => undef
132
+ #
133
+ # If true, the currency attribute is updated upon setting the
134
+ # money attribute.
135
+ #
136
+ # :time => undef
137
+ #
138
+ # Defines the attribute used to
139
+ # retrieve the Money's time. If this option is used, each
140
+ # Money value will use this attribute during historical Currency
141
+ # conversions.
142
+ #
143
+ # Money values can share a time value with other attributes
144
+ # (e.g. a created_on column in ActiveRecord::Base).
145
+ #
146
+ # If this option is true, the money time attribute will be named
147
+ # "#{attr_name}_time" and :time_update will be true.
148
+ #
149
+ # :time_update => undef
150
+ #
151
+ # If true, the Money time value is updated upon setting the
152
+ # money attribute.
153
+ #
154
+ def attr_money(attr_name, *opts)
155
+ opts = Hash[*opts]
156
+
157
+ attr_name = attr_name.to_s
158
+ opts[:class] = self
159
+ opts[:name] = attr_name.intern
160
+ ::Currency::Money.register_money_attribute(opts)
161
+
162
+ value = opts[:value] || opts[:name]
163
+ opts[:value] = value
164
+ write_value = opts[:write_value] ||= "self.#{value} = "
165
+
166
+ # Intercept value setter?
167
+ if ! opts[:readonly] && value.to_s != attr_name.to_s
168
+ alias_accessor = <<-"end_eval"
169
+ alias :before_money_#{value}= :#{value}=
170
+
171
+ def #{value}=(__value)
172
+ @#{attr_name} = nil # uncache
173
+ self.before_money_#{value} = __value
174
+ end
175
+
176
+ end_eval
177
+ end
178
+
179
+ # How to convert between numeric representation and Money.
180
+ rep = opts[:rep] ||= :float
181
+ to_rep = opts[:to_rep]
182
+ from_rep = opts[:from_rep]
183
+ if rep == :rep
184
+ to_rep = 'rep'
185
+ from_rep = '::Currency::Money.new_rep'
186
+ else
187
+ case rep
188
+ when :float
189
+ to_rep = 'to_f'
190
+ when :integer
191
+ to_rep = 'to_i'
192
+ else
193
+ throw ::Currency::Exception::InvalidMoneyValue.new("Cannot use value representation: #{rep.inspect}")
194
+ end
195
+ from_rep = '::Currency::Money.new'
196
+ end
197
+ to_rep = to_rep.to_s
198
+ from_rep = from_rep.to_s
199
+
200
+ # Money time values.
201
+ time = opts[:time]
202
+ write_time = ''
203
+ if time
204
+ if time == true
205
+ time = "#{attr_name}_time"
206
+ opts[:time_update] = true
207
+ end
208
+ read_time = "self.#{time}"
209
+ end
210
+ opts[:time] = time
211
+ if opts[:time_update]
212
+ write_time = "self.#{time} = #{attr_name}_money && #{attr_name}_money.time"
213
+ end
214
+ time ||= 'nil'
215
+ read_time ||= time
216
+
217
+ currency_fixed = opts[:currency_fixed]
218
+ currency_fixed &&= ":#{currency_fixed}"
219
+
220
+ currency = opts[:currency]
221
+ if currency == true
222
+ currency = currency.to_s
223
+ currency = "self.#{attr_name}_currency"
224
+ end
225
+ if currency
226
+ read_currency = "self.#{currency}"
227
+ if opts[:currency_update]
228
+ write_currency = "self.#{currency} = #{attr_name}_money.nil? ? nil : #{attr_name}_money.currency.code"
229
+ else
230
+ convert_currency = "#{attr_name}_money = #{attr_name}_money.convert(#{read_currency}, #{read_time})"
231
+ end
232
+ end
233
+ opts[:currency] = currency
234
+ write_currency ||= ''
235
+ convert_currency ||= ''
236
+
237
+ currency_preferred = opts[:currency_preferred]
238
+ if currency_preferred
239
+ currency_preferred = currency_preferred.to_s
240
+ read_preferred_currency = "@#{attr_name} = @#{attr_name}.convert(#{currency_preferred}, #{read_time})"
241
+ write_preferred_currency = "self.#{currency_preferred} = @#{attr_name}_money.currency.code"
242
+ end
243
+
244
+ currency ||= currency_fixed
245
+ read_currency ||= currency
246
+
247
+ alias_accessor ||= ''
248
+
249
+ validate ||= ''
250
+
251
+ if opts[:readonly]
252
+ eval_opts = [ (opts[:module_eval] = x = <<-"end_eval"), __FILE__, __LINE__ ]
253
+ #{validate}
254
+
255
+ def #{attr_name}
256
+ #{attr_name}_rep = #{value}
257
+ if #{attr_name}_rep != nil
258
+ #{attr_name} = #{from_rep}(#{attr_name}_rep, #{read_currency} || #{currency}, #{read_time} || #{time})
259
+ #{read_preferred_currency}
260
+ else
261
+ #{attr_name} = nil
262
+ end
263
+ #{attr_name}
264
+ end
265
+
266
+ end_eval
267
+ else
268
+ eval_opts = [ (opts[:module_eval] = x = <<-"end_eval"), __FILE__, __LINE__ ]
269
+ #{validate}
270
+
271
+ #{alias_accessor}
272
+
273
+ def #{attr_name}
274
+ unless @#{attr_name}
275
+ #{attr_name}_rep = #{value}
276
+ if #{attr_name}_rep != nil
277
+ @#{attr_name} = #{from_rep}(#{attr_name}_rep, #{read_currency} || #{currency}, #{read_time} || #{time})
278
+ #{read_preferred_currency}
279
+ end
280
+ end
281
+ @#{attr_name}
282
+ end
283
+
284
+
285
+ def #{attr_name}=(value)
286
+ if value == nil
287
+ #{attr_name}_money = nil
288
+ elsif value.kind_of?(Integer) || value.kind_of?(Float) || value.kind_of?(String)
289
+ #{attr_name}_money = ::Currency.Money(value, #{read_currency}, #{read_time})
290
+ #{write_preferred_currency}
291
+ elsif value.kind_of?(::Currency::Money)
292
+ #{attr_name}_money = value
293
+ #{write_preferred_currency}
294
+ #{convert_currency}
295
+ else
296
+ throw ::Currency::Exception::InvalidMoneyValue.new(value)
297
+ end
298
+
299
+ @#{attr_name} = #{attr_name}_money
300
+ #{write_value}(#{attr_name}_money.nil? ? nil : #{attr_name}_money.#{to_rep})
301
+ #{write_currency}
302
+ #{write_time}
303
+
304
+ value
305
+ end
306
+
307
+ end_eval
308
+ end
309
+
310
+ # $stderr.puts " CODE = #{x}"
311
+ module_eval(*eval_opts)
312
+ end
313
+ end # module
314
+ end # module
315
+
316
+
317
+ # Use include ::Currency::Macro
318
+ #::Object.class_eval do
319
+ # include Currency::Macro
320
+ #end
321
+