acvwilson-currency 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING.txt +339 -0
- data/ChangeLog +8 -0
- data/LICENSE.txt +65 -0
- data/Manifest.txt +58 -0
- data/README.txt +51 -0
- data/Releases.txt +155 -0
- data/TODO.txt +9 -0
- data/currency.gemspec +18 -0
- data/examples/ex1.rb +13 -0
- data/examples/xe1.rb +20 -0
- data/lib/currency.rb +143 -0
- data/lib/currency/active_record.rb +265 -0
- data/lib/currency/config.rb +91 -0
- data/lib/currency/core_extensions.rb +83 -0
- data/lib/currency/currency.rb +175 -0
- data/lib/currency/currency/factory.rb +121 -0
- data/lib/currency/currency_version.rb +6 -0
- data/lib/currency/exception.rb +119 -0
- data/lib/currency/exchange.rb +48 -0
- data/lib/currency/exchange/rate.rb +214 -0
- data/lib/currency/exchange/rate/deriver.rb +157 -0
- data/lib/currency/exchange/rate/source.rb +89 -0
- data/lib/currency/exchange/rate/source/base.rb +166 -0
- data/lib/currency/exchange/rate/source/failover.rb +63 -0
- data/lib/currency/exchange/rate/source/federal_reserve.rb +160 -0
- data/lib/currency/exchange/rate/source/historical.rb +79 -0
- data/lib/currency/exchange/rate/source/historical/rate.rb +184 -0
- data/lib/currency/exchange/rate/source/historical/rate_loader.rb +186 -0
- data/lib/currency/exchange/rate/source/historical/writer.rb +220 -0
- data/lib/currency/exchange/rate/source/new_york_fed.rb +127 -0
- data/lib/currency/exchange/rate/source/provider.rb +120 -0
- data/lib/currency/exchange/rate/source/test.rb +50 -0
- data/lib/currency/exchange/rate/source/the_financials.rb +191 -0
- data/lib/currency/exchange/rate/source/timed_cache.rb +198 -0
- data/lib/currency/exchange/rate/source/xe.rb +165 -0
- data/lib/currency/exchange/time_quantitizer.rb +111 -0
- data/lib/currency/formatter.rb +310 -0
- data/lib/currency/macro.rb +321 -0
- data/lib/currency/money.rb +298 -0
- data/lib/currency/money_helper.rb +13 -0
- data/lib/currency/parser.rb +193 -0
- data/spec/ar_column_spec.rb +76 -0
- data/spec/ar_core_spec.rb +68 -0
- data/spec/ar_simple_spec.rb +23 -0
- data/spec/config_spec.rb +29 -0
- data/spec/federal_reserve_spec.rb +75 -0
- data/spec/formatter_spec.rb +72 -0
- data/spec/historical_writer_spec.rb +187 -0
- data/spec/macro_spec.rb +109 -0
- data/spec/money_spec.rb +355 -0
- data/spec/new_york_fed_spec.rb +73 -0
- data/spec/parser_spec.rb +105 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/time_quantitizer_spec.rb +115 -0
- data/spec/timed_cache_spec.rb +95 -0
- data/spec/xe_spec.rb +50 -0
- metadata +117 -0
@@ -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,310 @@
|
|
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
|
+
# => "€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
|
+
# Set the decimal_places
|
116
|
+
# Defaults to: nil
|
117
|
+
attr_accessor :decimals
|
118
|
+
|
119
|
+
# If passed true, formats for an input field (i.e.: as a number).
|
120
|
+
def as_input_value=(x)
|
121
|
+
if x
|
122
|
+
self.thousands_separator = ''
|
123
|
+
self.decimal_separator = '.'
|
124
|
+
self.thousands = false
|
125
|
+
self.cents = true
|
126
|
+
self.symbol = false
|
127
|
+
self.code = false
|
128
|
+
self.html = false
|
129
|
+
self.time = false
|
130
|
+
self.time_fractional_digits = nil
|
131
|
+
end
|
132
|
+
x
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
@@default = nil
|
137
|
+
# Get the default Formatter.
|
138
|
+
def self.default
|
139
|
+
@@default || self.new
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
# Set the default Formatter.
|
144
|
+
def self.default=(x)
|
145
|
+
@@default = x
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
def initialize(opt = { })
|
150
|
+
@thousands_separator = ','
|
151
|
+
@decimal_separator = '.'
|
152
|
+
@thousands = true
|
153
|
+
@cents = true
|
154
|
+
@symbol = true
|
155
|
+
@code = false
|
156
|
+
@html = false
|
157
|
+
@time = false
|
158
|
+
@time_fractional_digits = 4
|
159
|
+
@template = '#{code}#{code && " "}#{symbol}#{sign}#{whole}#{fraction}#{time && " "}#{time}'
|
160
|
+
@template_object = nil
|
161
|
+
@decimals = nil
|
162
|
+
|
163
|
+
opt.each_pair{ | k, v | self.send("#{k}=", v) }
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
def currency=(x) # :nodoc:
|
168
|
+
# DO NOTHING!
|
169
|
+
end
|
170
|
+
|
171
|
+
|
172
|
+
# Sets the template and the Template#template.
|
173
|
+
def template=(x)
|
174
|
+
if @template_object
|
175
|
+
@template_object.template = x
|
176
|
+
end
|
177
|
+
@template = x
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
# Returns the Template object.
|
182
|
+
def template_object
|
183
|
+
return @template_object if @template_object
|
184
|
+
|
185
|
+
@template_object = Template.new
|
186
|
+
@template_object.template = @template if @template
|
187
|
+
# $stderr.puts "template.template = #{@template_object.template.inspect}"
|
188
|
+
@template_object.template_proc # pre-cache before clone.
|
189
|
+
|
190
|
+
@template_object
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
def _format(m, currency = nil, time = nil) # :nodoc:
|
195
|
+
# Get currency.
|
196
|
+
currency ||= m.currency
|
197
|
+
|
198
|
+
# Get time.
|
199
|
+
time ||= m.time
|
200
|
+
|
201
|
+
# set decimal places
|
202
|
+
@decimals ||= currency.scale_exp
|
203
|
+
|
204
|
+
# Setup template
|
205
|
+
tmpl = self.template_object.clone
|
206
|
+
# $stderr.puts "template.template = #{tmpl.template.inspect}"
|
207
|
+
tmpl.money = m
|
208
|
+
tmpl.currency = currency
|
209
|
+
|
210
|
+
# Get scaled integer representation for this Currency.
|
211
|
+
# $stderr.puts "m.currency = #{m.currency}, currency => #{currency}"
|
212
|
+
x = m.Money_rep(currency)
|
213
|
+
|
214
|
+
# Remove sign.
|
215
|
+
x = - x if ( neg = x < 0 )
|
216
|
+
tmpl.sign = neg ? '-' : nil
|
217
|
+
|
218
|
+
# Convert to String.
|
219
|
+
x = x.to_s
|
220
|
+
|
221
|
+
# Keep prefixing "0" until filled to scale.
|
222
|
+
while ( x.length <= currency.scale_exp )
|
223
|
+
x = "0" + x
|
224
|
+
end
|
225
|
+
|
226
|
+
# Insert decimal place.
|
227
|
+
whole = x[0 .. currency.format_left]
|
228
|
+
fraction = x[currency.format_right .. -1]
|
229
|
+
|
230
|
+
# Round the fraction to the supplied number of decimal places
|
231
|
+
fraction = (fraction.to_f / currency.scale).round(@decimals).to_s[2..-1]
|
232
|
+
# raise "decimals: #{@decimals}, scale_exp: #{currency.scale_exp}, x is: #{x.inspect}, currency.scale_exp is #{currency.scale_exp.inspect}, fraction: #{fraction.inspect}"
|
233
|
+
while ( fraction.length < @decimals )
|
234
|
+
fraction = fraction + "0"
|
235
|
+
end
|
236
|
+
|
237
|
+
|
238
|
+
# raise "x is: #{x.inspect}, currency.scale_exp is #{currency.scale_exp.inspect}, fraction: #{fraction.inspect}"
|
239
|
+
# fraction = ((fraction.to_f / currency.scale).round(decimals) * (10 ** decimals)).to_i.to_s
|
240
|
+
|
241
|
+
# Do thousands.
|
242
|
+
x = whole
|
243
|
+
if @thousands && (@thousands_separator && ! @thousands_separator.empty?)
|
244
|
+
x.reverse!
|
245
|
+
x.gsub!(/(\d\d\d)/) {|y| y + @thousands_separator}
|
246
|
+
x.sub!(/#{@thousands_separator}$/,'')
|
247
|
+
x.reverse!
|
248
|
+
end
|
249
|
+
|
250
|
+
# Put whole and fractional parts.
|
251
|
+
tmpl.whole = x
|
252
|
+
tmpl.fraction = @cents && @decimal_separator ? @decimal_separator + fraction : nil
|
253
|
+
|
254
|
+
|
255
|
+
# Add symbol?
|
256
|
+
tmpl.symbol = @symbol ? ((@html && currency.symbol_html) || currency.symbol) : nil
|
257
|
+
|
258
|
+
|
259
|
+
# Add currency code.
|
260
|
+
tmpl.code = @code ? _format_Currency(currency) : nil
|
261
|
+
|
262
|
+
# Add time.
|
263
|
+
tmpl.time = @time && time ? _format_Time(time) : nil
|
264
|
+
|
265
|
+
# Ask template to format the components.
|
266
|
+
tmpl.format
|
267
|
+
end
|
268
|
+
|
269
|
+
|
270
|
+
def _format_Currency(c) # :nodoc:
|
271
|
+
x = ''
|
272
|
+
x << '<span class="currency_code">' if @html
|
273
|
+
x << c.code.to_s
|
274
|
+
x << '</span>' if @html
|
275
|
+
x
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
def _format_Time(t) # :nodoc:
|
280
|
+
x = ''
|
281
|
+
x << t.getutc.xmlschema(@time_fractional_digits) if t
|
282
|
+
x
|
283
|
+
end
|
284
|
+
|
285
|
+
|
286
|
+
@@empty_hash = { }
|
287
|
+
@@empty_hash.freeze
|
288
|
+
|
289
|
+
# Format a Money object as a String.
|
290
|
+
#
|
291
|
+
# m = Money.new("1234567.89")
|
292
|
+
# m.to_s(:code => true, :symbol => false)
|
293
|
+
# => "1,234,567.89 USD"
|
294
|
+
#
|
295
|
+
def format(m, opt = @@empty_hash)
|
296
|
+
# raise "huh: #{opt.inspect}"
|
297
|
+
|
298
|
+
fmt = self
|
299
|
+
|
300
|
+
unless opt.empty?
|
301
|
+
fmt = fmt.clone
|
302
|
+
opt.each_pair{ | k, v | fmt.send("#{k}=", v) }
|
303
|
+
end
|
304
|
+
|
305
|
+
# $stderr.puts "format(opt = #{opt.inspect})"
|
306
|
+
fmt._format(m, opt[:currency]) # Allow override of current currency.
|
307
|
+
end
|
308
|
+
|
309
|
+
end # class
|
310
|
+
|
@@ -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
|
+
|