currency 0.3.3 → 0.4.0

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.
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
+