mumboe-currency 0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/bin/currency_historical_rate_load +105 -0
  2. data/examples/ex1.rb +13 -0
  3. data/examples/xe1.rb +20 -0
  4. data/lib/currency/active_record.rb +265 -0
  5. data/lib/currency/config.rb +91 -0
  6. data/lib/currency/core_extensions.rb +41 -0
  7. data/lib/currency/currency/factory.rb +228 -0
  8. data/lib/currency/currency.rb +175 -0
  9. data/lib/currency/currency_version.rb +6 -0
  10. data/lib/currency/exception.rb +119 -0
  11. data/lib/currency/exchange/rate/deriver.rb +157 -0
  12. data/lib/currency/exchange/rate/source/base.rb +166 -0
  13. data/lib/currency/exchange/rate/source/failover.rb +63 -0
  14. data/lib/currency/exchange/rate/source/federal_reserve.rb +160 -0
  15. data/lib/currency/exchange/rate/source/historical/rate.rb +184 -0
  16. data/lib/currency/exchange/rate/source/historical/rate_loader.rb +186 -0
  17. data/lib/currency/exchange/rate/source/historical/writer.rb +220 -0
  18. data/lib/currency/exchange/rate/source/historical.rb +79 -0
  19. data/lib/currency/exchange/rate/source/new_york_fed.rb +127 -0
  20. data/lib/currency/exchange/rate/source/provider.rb +120 -0
  21. data/lib/currency/exchange/rate/source/test.rb +50 -0
  22. data/lib/currency/exchange/rate/source/the_financials.rb +191 -0
  23. data/lib/currency/exchange/rate/source/timed_cache.rb +198 -0
  24. data/lib/currency/exchange/rate/source/xe.rb +165 -0
  25. data/lib/currency/exchange/rate/source.rb +89 -0
  26. data/lib/currency/exchange/rate.rb +214 -0
  27. data/lib/currency/exchange/time_quantitizer.rb +111 -0
  28. data/lib/currency/exchange.rb +50 -0
  29. data/lib/currency/formatter.rb +290 -0
  30. data/lib/currency/macro.rb +321 -0
  31. data/lib/currency/money.rb +295 -0
  32. data/lib/currency/money_helper.rb +13 -0
  33. data/lib/currency/parser.rb +151 -0
  34. data/lib/currency.rb +143 -0
  35. data/test/string_test.rb +54 -0
  36. data/test/test_base.rb +44 -0
  37. metadata +90 -0
@@ -0,0 +1,290 @@
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ require 'rss/rss' # Time#xmlschema
5
+
6
+
7
+ # This class formats a Money value as a String.
8
+ # Each Currency has a default Formatter.
9
+ class Currency::Formatter
10
+ # The underlying object for Currency::Formatter#format.
11
+ # This object is cloned and initialized with strings created
12
+ # from Formatter#format.
13
+ # It handles the Formatter#format string interpolation.
14
+ class Template
15
+ @@empty_hash = { }
16
+ @@empty_hash.freeze
17
+
18
+ # The template string.
19
+ attr_accessor :template
20
+
21
+ # The Currency::Money object being formatted.
22
+ attr_accessor :money
23
+ # The Currency::Currency object being formatted.
24
+ attr_accessor :currency
25
+
26
+ # The sign: '-' or nil.
27
+ attr_accessor :sign
28
+ # The whole part of the value, with thousands_separator or nil.
29
+ attr_accessor :whole
30
+ # The fraction part of the value, with decimal_separator or nil.
31
+ attr_accessor :fraction
32
+ # The currency symbol or nil.
33
+ attr_accessor :symbol
34
+ # The currency code or nil.
35
+ attr_accessor :code
36
+
37
+ # The time or nil.
38
+ attr_accessor :time
39
+
40
+ def initialize(opts = @@empty_hash)
41
+ @template =
42
+ @template_proc =
43
+ nil
44
+
45
+ opts.each_pair{ | k, v | self.send("#{k}=", v) }
46
+ end
47
+
48
+
49
+ # Sets the template string and uncaches the template_proc.
50
+ def template=(x)
51
+ if @template != x
52
+ @template_proc = nil
53
+ end
54
+ @template = x
55
+ end
56
+
57
+
58
+ # Defines a the self._format template procedure using
59
+ # the template as a string to be interpolated.
60
+ def template_proc(template = @template)
61
+ return @template_proc if @template_proc
62
+ @template_proc = template || ''
63
+ # @template_proc = @template_proc.gsub(/[\\"']/) { | x | "\\" + x }
64
+ @template_proc = "def self._format; \"#{@template_proc}\"; end"
65
+ self.instance_eval @template_proc
66
+ @template_proc
67
+ end
68
+
69
+
70
+ # Formats the current state using the template.
71
+ def format
72
+ template_proc
73
+ _format
74
+ end
75
+ end
76
+
77
+
78
+ # Defaults to ','
79
+ attr_accessor :thousands_separator
80
+
81
+ # Defaults to '.'
82
+ attr_accessor :decimal_separator
83
+
84
+ # If true, insert _thousands_separator_ between each 3 digits in the whole value.
85
+ attr_accessor :thousands
86
+
87
+ # If true, append _decimal_separator_ and decimal digits after whole value.
88
+ attr_accessor :cents
89
+
90
+ # If true, prefix value with currency symbol.
91
+ attr_accessor :symbol
92
+
93
+ # If true, append currency code.
94
+ attr_accessor :code
95
+
96
+ # If true, append the time.
97
+ attr_accessor :time
98
+
99
+ # The number of fractional digits in the time.
100
+ # Defaults to 4.
101
+ attr_accessor :time_fractional_digits
102
+
103
+ # If true, use html formatting.
104
+ #
105
+ # Currency::Money(12.45, :EUR).to_s(:html => true; :code => true)
106
+ # => "&#8364;12.45 <span class=\"currency_code\">EUR</span>"
107
+ attr_accessor :html
108
+
109
+ # A template string used to format a money value.
110
+ # Defaults to:
111
+ #
112
+ # '#{code}#{code && " "}#{symbol}#{sign}#{whole}#{fraction}#{time && " "}#{time}'
113
+ attr_accessor :template
114
+
115
+
116
+ # If passed true, formats for an input field (i.e.: as a number).
117
+ def as_input_value=(x)
118
+ if x
119
+ self.thousands_separator = ''
120
+ self.decimal_separator = '.'
121
+ self.thousands = false
122
+ self.cents = true
123
+ self.symbol = false
124
+ self.code = false
125
+ self.html = false
126
+ self.time = false
127
+ self.time_fractional_digits = nil
128
+ end
129
+ x
130
+ end
131
+
132
+
133
+ @@default = nil
134
+ # Get the default Formatter.
135
+ def self.default
136
+ @@default || self.new
137
+ end
138
+
139
+
140
+ # Set the default Formatter.
141
+ def self.default=(x)
142
+ @@default = x
143
+ end
144
+
145
+
146
+ def initialize(opt = { })
147
+ @thousands_separator = ','
148
+ @decimal_separator = '.'
149
+ @thousands = true
150
+ @cents = true
151
+ @symbol = true
152
+ @code = false
153
+ @html = false
154
+ @time = false
155
+ @time_fractional_digits = 4
156
+ @template = '#{code}#{code && " "}#{symbol}#{sign}#{whole}#{fraction}#{time && " "}#{time}'
157
+ @template_object = nil
158
+
159
+ opt.each_pair{ | k, v | self.send("#{k}=", v) }
160
+ end
161
+
162
+
163
+ def currency=(x) # :nodoc:
164
+ # DO NOTHING!
165
+ end
166
+
167
+
168
+ # Sets the template and the Template#template.
169
+ def template=(x)
170
+ if @template_object
171
+ @template_object.template = x
172
+ end
173
+ @template = x
174
+ end
175
+
176
+
177
+ # Returns the Template object.
178
+ def template_object
179
+ return @template_object if @template_object
180
+
181
+ @template_object = Template.new
182
+ @template_object.template = @template if @template
183
+ # $stderr.puts "template.template = #{@template_object.template.inspect}"
184
+ @template_object.template_proc # pre-cache before clone.
185
+
186
+ @template_object
187
+ end
188
+
189
+
190
+ def _format(m, currency = nil, time = nil) # :nodoc:
191
+ # Get currency.
192
+ currency ||= m.currency
193
+
194
+ # Get time.
195
+ time ||= m.time
196
+
197
+ # Setup template
198
+ tmpl = self.template_object.clone
199
+ # $stderr.puts "template.template = #{tmpl.template.inspect}"
200
+ tmpl.money = m
201
+ tmpl.currency = currency
202
+
203
+ # Get scaled integer representation for this Currency.
204
+ # $stderr.puts "m.currency = #{m.currency}, currency => #{currency}"
205
+ x = m.Money_rep(currency)
206
+
207
+ # Remove sign.
208
+ x = - x if ( neg = x < 0 )
209
+ tmpl.sign = neg ? '-' : nil
210
+
211
+ # Convert to String.
212
+ x = x.to_s
213
+
214
+ # Keep prefixing "0" until filled to scale.
215
+ while ( x.length <= currency.scale_exp )
216
+ x = "0" + x
217
+ end
218
+
219
+ # Insert decimal place.
220
+ whole = x[0 .. currency.format_left]
221
+ fraction = x[currency.format_right .. -1]
222
+
223
+ # Do thousands.
224
+ x = whole
225
+ if @thousands && (@thousands_separator && ! @thousands_separator.empty?)
226
+ x.reverse!
227
+ x.gsub!(/(\d\d\d)/) {|y| y + @thousands_separator}
228
+ x.sub!(/#{@thousands_separator}$/,'')
229
+ x.reverse!
230
+ end
231
+
232
+ # Put whole and fractional parts.
233
+ tmpl.whole = x
234
+ tmpl.fraction = @cents && @decimal_separator ? @decimal_separator + fraction : nil
235
+
236
+
237
+ # Add symbol?
238
+ tmpl.symbol = @symbol ? ((@html && currency.symbol_html) || currency.symbol) : nil
239
+
240
+
241
+ # Add currency code.
242
+ tmpl.code = @code ? _format_Currency(currency) : nil
243
+
244
+ # Add time.
245
+ tmpl.time = @time && time ? _format_Time(time) : nil
246
+
247
+ # Ask template to format the components.
248
+ tmpl.format
249
+ end
250
+
251
+
252
+ def _format_Currency(c) # :nodoc:
253
+ x = ''
254
+ x << '<span class="currency_code">' if @html
255
+ x << c.code.to_s
256
+ x << '</span>' if @html
257
+ x
258
+ end
259
+
260
+
261
+ def _format_Time(t) # :nodoc:
262
+ x = ''
263
+ x << t.getutc.xmlschema(@time_fractional_digits) if t
264
+ x
265
+ end
266
+
267
+
268
+ @@empty_hash = { }
269
+ @@empty_hash.freeze
270
+
271
+ # Format a Money object as a String.
272
+ #
273
+ # m = Money.new("1234567.89")
274
+ # m.to_s(:code => true, :symbol => false)
275
+ # => "1,234,567.89 USD"
276
+ #
277
+ def format(m, opt = @@empty_hash)
278
+ fmt = self
279
+
280
+ unless opt.empty?
281
+ fmt = fmt.clone
282
+ opt.each_pair{ | k, v | fmt.send("#{k}=", v) }
283
+ end
284
+
285
+ # $stderr.puts "format(opt = #{opt.inspect})"
286
+ fmt._format(m, opt[:currency]) # Allow override of current currency.
287
+ end
288
+
289
+ end # class
290
+
@@ -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
+ raise ::Currency::Exception::InvalidMoneyValue, "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
+ raise ::Currency::Exception::InvalidMoneyValue, 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
+