acvwilson-currency 0.5.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.
- 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,298 @@
|
|
1
|
+
# Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
|
2
|
+
# See LICENSE.txt for details.
|
3
|
+
|
4
|
+
#
|
5
|
+
# Represents an amount of money in a particular currency.
|
6
|
+
#
|
7
|
+
# A Money object stores its value using a scaled Integer representation
|
8
|
+
# and a Currency object.
|
9
|
+
#
|
10
|
+
# A Money object also has a time, which is used in conversions
|
11
|
+
# against historical exchange rates.
|
12
|
+
#
|
13
|
+
class Currency::Money
|
14
|
+
include Comparable
|
15
|
+
|
16
|
+
@@default_time = nil
|
17
|
+
def self.default_time
|
18
|
+
@@default_time
|
19
|
+
end
|
20
|
+
def self.default_time=(x)
|
21
|
+
@@default_time = x
|
22
|
+
end
|
23
|
+
|
24
|
+
@@empty_hash = { }
|
25
|
+
@@empty_hash.freeze
|
26
|
+
|
27
|
+
#
|
28
|
+
# DO NOT CALL THIS DIRECTLY:
|
29
|
+
#
|
30
|
+
# See Currency.Money() function.
|
31
|
+
#
|
32
|
+
# Construct a Money value object
|
33
|
+
# from a pre-scaled external representation:
|
34
|
+
# where x is a Float, Integer, String, etc.
|
35
|
+
#
|
36
|
+
# If a currency is not specified, Currency.default is used.
|
37
|
+
#
|
38
|
+
# x.Money_rep(currency)
|
39
|
+
#
|
40
|
+
# is invoked to coerce x into a Money representation value.
|
41
|
+
#
|
42
|
+
# For example:
|
43
|
+
#
|
44
|
+
# 123.Money_rep(:USD) => 12300
|
45
|
+
#
|
46
|
+
# Because the USD Currency object has a #scale of 100
|
47
|
+
#
|
48
|
+
# See #Money_rep(currency) mixin.
|
49
|
+
#
|
50
|
+
def initialize(x, currency = nil, time = nil)
|
51
|
+
opts ||= @@empty_hash
|
52
|
+
|
53
|
+
# Set ivars.
|
54
|
+
currency = ::Currency::Currency.get(currency)
|
55
|
+
@currency = currency
|
56
|
+
@time = time || ::Currency::Money.default_time
|
57
|
+
@time = ::Currency::Money.now if @time == :now
|
58
|
+
if x.kind_of?(String)
|
59
|
+
if currency
|
60
|
+
m = currency.parser_or_default.parse(x, :currency => currency)
|
61
|
+
else
|
62
|
+
m = ::Currency::Parser.default.parse(x)
|
63
|
+
end
|
64
|
+
@currency = m.currency unless @currency
|
65
|
+
@time = m.time if m.time
|
66
|
+
@rep = m.rep
|
67
|
+
else
|
68
|
+
@currency = ::Currency::Currency.default unless @currency
|
69
|
+
@rep = x.Money_rep(@currency)
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns a Time.new
|
75
|
+
# Can be modifed for special purposes.
|
76
|
+
def self.now
|
77
|
+
Time.new
|
78
|
+
end
|
79
|
+
|
80
|
+
# Compatibility with Money package.
|
81
|
+
def self.us_dollar(x)
|
82
|
+
self.new(x, :USD)
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
# Compatibility with Money package.
|
87
|
+
def cents
|
88
|
+
@rep
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
# Construct from post-scaled internal representation.
|
93
|
+
def self.new_rep(r, currency = nil, time = nil)
|
94
|
+
x = self.new(0, currency, time)
|
95
|
+
x.set_rep(r)
|
96
|
+
x
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
# Construct from post-scaled internal representation.
|
101
|
+
# using the same currency.
|
102
|
+
#
|
103
|
+
# x = Currency.Money("1.98", :USD)
|
104
|
+
# x.new_rep(123) => USD $1.23
|
105
|
+
#
|
106
|
+
# time defaults to self.time.
|
107
|
+
def new_rep(r, time = nil)
|
108
|
+
time ||= @time
|
109
|
+
x = self.class.new(0, @currency, time)
|
110
|
+
x.set_rep(r)
|
111
|
+
x
|
112
|
+
end
|
113
|
+
|
114
|
+
# Do not call this method directly.
|
115
|
+
# CLIENTS SHOULD NEVER CALL set_rep DIRECTLY.
|
116
|
+
# You have been warned in ALL CAPS.
|
117
|
+
def set_rep(r) # :nodoc:
|
118
|
+
r = r.to_i unless r.kind_of?(Integer)
|
119
|
+
@rep = r
|
120
|
+
end
|
121
|
+
|
122
|
+
# Do not call this method directly.
|
123
|
+
# CLIENTS SHOULD NEVER CALL set_time DIRECTLY.
|
124
|
+
# You have been warned in ALL CAPS.
|
125
|
+
def set_time(time) # :nodoc:
|
126
|
+
@time = time
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns the Money representation (usually an Integer).
|
130
|
+
def rep
|
131
|
+
@rep
|
132
|
+
end
|
133
|
+
|
134
|
+
# Get the Money's Currency.
|
135
|
+
def currency
|
136
|
+
@currency
|
137
|
+
end
|
138
|
+
|
139
|
+
# Get the Money's time.
|
140
|
+
def time
|
141
|
+
@time
|
142
|
+
end
|
143
|
+
|
144
|
+
# Convert Money to another Currency.
|
145
|
+
# currency can be a Symbol or a Currency object.
|
146
|
+
# If currency is nil, the Currency.default is used.
|
147
|
+
def convert(currency, time = nil)
|
148
|
+
currency = ::Currency::Currency.default if currency.nil?
|
149
|
+
currency = ::Currency::Currency.get(currency) unless currency.kind_of?(Currency)
|
150
|
+
if @currency == currency
|
151
|
+
self
|
152
|
+
else
|
153
|
+
time = self.time if time == :money
|
154
|
+
::Currency::Exchange::Rate::Source.current.convert(self, currency, time)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
# Hash for hash table: both value and currency.
|
160
|
+
# See #eql? below.
|
161
|
+
def hash
|
162
|
+
@rep.hash ^ @currency.hash
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
# True if money values have the same value and currency.
|
167
|
+
def eql?(x)
|
168
|
+
self.class == x.class &&
|
169
|
+
@rep == x.rep &&
|
170
|
+
@currency == x.currency
|
171
|
+
end
|
172
|
+
|
173
|
+
# True if money values have the same value and currency.
|
174
|
+
def ==(x)
|
175
|
+
self.class == x.class &&
|
176
|
+
@rep == x.rep &&
|
177
|
+
@currency == x.currency
|
178
|
+
end
|
179
|
+
|
180
|
+
# Compares Money values.
|
181
|
+
# Will convert x to self.currency before comparision.
|
182
|
+
def <=>(x)
|
183
|
+
if @currency == x.currency
|
184
|
+
@rep <=> x.rep
|
185
|
+
else
|
186
|
+
@rep <=> convert(@currency, @time).rep
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
# - Money => Money
|
192
|
+
#
|
193
|
+
# Negates a Money value.
|
194
|
+
def -@
|
195
|
+
new_rep(- @rep)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Money + (Money | Number) => Money
|
199
|
+
#
|
200
|
+
# Right side may be coerced to left side's Currency.
|
201
|
+
def +(x)
|
202
|
+
new_rep(@rep + x.Money_rep(@currency))
|
203
|
+
end
|
204
|
+
|
205
|
+
# Money - (Money | Number) => Money
|
206
|
+
#
|
207
|
+
# Right side may be coerced to left side's Currency.
|
208
|
+
def -(x)
|
209
|
+
new_rep(@rep - x.Money_rep(@currency))
|
210
|
+
end
|
211
|
+
|
212
|
+
# Money * Number => Money
|
213
|
+
#
|
214
|
+
# Right side must be Number.
|
215
|
+
def *(x)
|
216
|
+
new_rep(@rep * x)
|
217
|
+
end
|
218
|
+
|
219
|
+
# Money / Money => Float (ratio)
|
220
|
+
# Money / Number => Money
|
221
|
+
#
|
222
|
+
# Right side must be Money or Number.
|
223
|
+
# Right side Integers are not coerced to Float before
|
224
|
+
# division.
|
225
|
+
def /(x)
|
226
|
+
if x.kind_of?(self.class)
|
227
|
+
(@rep.to_f) / (x.Money_rep(@currency).to_f)
|
228
|
+
else
|
229
|
+
new_rep(@rep / x)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Formats the Money value as a String using the Currency's Formatter.
|
234
|
+
def format(*opt)
|
235
|
+
@currency.format(self, *opt)
|
236
|
+
end
|
237
|
+
|
238
|
+
# Formats the Money value as a String.
|
239
|
+
def to_s(*opt)
|
240
|
+
# raise "huh: #{opt.inspect}"
|
241
|
+
|
242
|
+
@currency.format(self, *opt)
|
243
|
+
end
|
244
|
+
|
245
|
+
# Coerces the Money's value to a Float.
|
246
|
+
# May cause loss of precision.
|
247
|
+
def to_f
|
248
|
+
Float(@rep) / @currency.scale
|
249
|
+
end
|
250
|
+
|
251
|
+
# Coerces the Money's value to an Integer.
|
252
|
+
# May cause loss of precision.
|
253
|
+
def to_i
|
254
|
+
@rep / @currency.scale
|
255
|
+
end
|
256
|
+
|
257
|
+
# True if the Money's value is zero.
|
258
|
+
def zero?
|
259
|
+
@rep == 0
|
260
|
+
end
|
261
|
+
|
262
|
+
# True if the Money's value is greater than zero.
|
263
|
+
def positive?
|
264
|
+
@rep > 0
|
265
|
+
end
|
266
|
+
|
267
|
+
# True if the Money's value is less than zero.
|
268
|
+
def negative?
|
269
|
+
@rep < 0
|
270
|
+
end
|
271
|
+
|
272
|
+
# Returns the Money's value representation in another currency.
|
273
|
+
def Money_rep(currency, time = nil)
|
274
|
+
# Attempt conversion?
|
275
|
+
if @currency != currency || (time && @time != time)
|
276
|
+
|
277
|
+
self.convert(currency, time).rep
|
278
|
+
# raise ::Currency::Exception::Generic, "Incompatible Currency: #{@currency} != #{currency}"
|
279
|
+
else
|
280
|
+
@rep
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Basic inspection, with symbol, currency code and time.
|
285
|
+
# The standard #inspect method is available as #inspect_deep.
|
286
|
+
def inspect(*opts)
|
287
|
+
self.format(:symbol => true, :code => true, :time => true)
|
288
|
+
end
|
289
|
+
|
290
|
+
# How to alias a method defined in an object superclass in a different class:
|
291
|
+
define_method(:inspect_deep, Object.instance_method(:inspect))
|
292
|
+
# How call a method defined in a superclass from a method with a different name:
|
293
|
+
# def inspect_deep(*opts)
|
294
|
+
# self.class.superclass.instance_method(:inspect).bind(self).call
|
295
|
+
# end
|
296
|
+
|
297
|
+
end # class
|
298
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
|
2
|
+
# See LICENSE.txt for details.
|
3
|
+
|
4
|
+
module ActionView::Helpers::MoneyHelper
|
5
|
+
# Creates a suitable HTML element for a Money value field.
|
6
|
+
def money_field(object, method, options = {})
|
7
|
+
InstanceTag.new(object, method, self).to_input_field_tag("text", options)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
ActionView::Base.load_helper(File.dirname(__FILE__))
|
13
|
+
|
@@ -0,0 +1,193 @@
|
|
1
|
+
# Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
|
2
|
+
# See LICENSE.txt for details.
|
3
|
+
|
4
|
+
|
5
|
+
require 'rss/rss' # Time#xmlschema
|
6
|
+
|
7
|
+
|
8
|
+
# This class parses a Money value from a String.
|
9
|
+
# Each Currency has a default Parser.
|
10
|
+
class Currency::Parser
|
11
|
+
|
12
|
+
# The default Currency to use if no Currency is specified.
|
13
|
+
attr_accessor :currency
|
14
|
+
|
15
|
+
# If true and a parsed string contains a ISO currency code
|
16
|
+
# that is not the same as currency,
|
17
|
+
# #parse() will raise IncompatibleCurrency.
|
18
|
+
# Defaults to false.
|
19
|
+
attr_accessor :enforce_currency
|
20
|
+
|
21
|
+
# The default Time to use if no Time is specified in the string.
|
22
|
+
# If :now, time is set to Time.new.
|
23
|
+
attr_accessor :time
|
24
|
+
|
25
|
+
@@default = nil
|
26
|
+
# Get the default Formatter.
|
27
|
+
def self.default
|
28
|
+
@@default ||=
|
29
|
+
self.new
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# Set the default Formatter.
|
34
|
+
def self.default=(x)
|
35
|
+
@@default = x
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def initialize(opt = { })
|
40
|
+
@time =
|
41
|
+
@enforce_currency =
|
42
|
+
@currency =
|
43
|
+
nil
|
44
|
+
opt.each_pair{ | k, v | self.send("#{k}=", v) }
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def _parse(str) # :nodoc:
|
49
|
+
x = str
|
50
|
+
|
51
|
+
# Get currency.
|
52
|
+
# puts "str = #{str.inspect}, @currency = #{@currency}"
|
53
|
+
|
54
|
+
md = nil # match data
|
55
|
+
|
56
|
+
# $stderr.puts "'#{x}'.Money_rep(#{self})"
|
57
|
+
|
58
|
+
# Parse time.
|
59
|
+
time = nil
|
60
|
+
if (md = /(\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?Z)/.match(x))
|
61
|
+
time = Time.xmlschema(md[1])
|
62
|
+
unless time
|
63
|
+
raise Currency::Exception::InvalidMoneyString,
|
64
|
+
[
|
65
|
+
"time: #{str.inspect} #{currency} #{x.inspect}",
|
66
|
+
:string, str,
|
67
|
+
:currency, currency,
|
68
|
+
:state, x,
|
69
|
+
]
|
70
|
+
end
|
71
|
+
x = md.pre_match + md.post_match
|
72
|
+
end
|
73
|
+
# Default time
|
74
|
+
time ||= @time
|
75
|
+
time = Time.new if time == :now
|
76
|
+
|
77
|
+
# $stderr.puts "x = #{x}"
|
78
|
+
convert_currency = nil
|
79
|
+
# Handle currency code in string.
|
80
|
+
if (md = /([A-Z][A-Z][A-Z])/.match(x))
|
81
|
+
curr = ::Currency::Currency.get(md[1])
|
82
|
+
x = md.pre_match + md.post_match
|
83
|
+
if @currency && @currency != curr
|
84
|
+
if @enforce_currency
|
85
|
+
raise ::Currency::Exception::IncompatibleCurrency,
|
86
|
+
[
|
87
|
+
"currency: #{str.inspect} #{@currency.code}",
|
88
|
+
:string, str,
|
89
|
+
:default_currency, @currency,
|
90
|
+
:parsed_currency, curr,
|
91
|
+
]
|
92
|
+
end
|
93
|
+
convert_currency = @currency
|
94
|
+
end
|
95
|
+
currency = curr
|
96
|
+
else
|
97
|
+
currency = @currency || ::Currency::Currency.default
|
98
|
+
currency = ::Currency::Currency.get(currency)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Remove placeholders and symbol.
|
102
|
+
x = x.gsub(/[, ]/, '')
|
103
|
+
symbol = currency.symbol # FIXME
|
104
|
+
x = x.gsub(symbol, '') if symbol
|
105
|
+
|
106
|
+
# $stderr.puts "x = #{x.inspect}"
|
107
|
+
# Match: whole Currency value.
|
108
|
+
if md = /^([-+]?\d+)\.?$/.match(x)
|
109
|
+
# $stderr.puts "'#{self}'.parse(#{str}) => EXACT"
|
110
|
+
x = ::Currency::Money.new_rep(md[1].to_i * currency.scale, currency, @time)
|
111
|
+
|
112
|
+
# Match: fractional Currency value.
|
113
|
+
elsif md = /^([-+]?)(\d*)\.(\d+)$/.match(x)
|
114
|
+
sign = md[1]
|
115
|
+
whole = md[2]
|
116
|
+
part = md[3]
|
117
|
+
|
118
|
+
# $stderr.puts "'#{self}'.parse(#{str}) => DECIMAL (#{sign} #{whole} . #{part})"
|
119
|
+
|
120
|
+
if part.length != currency.scale
|
121
|
+
|
122
|
+
# Pad decimal places with additional '0'
|
123
|
+
while part.length < currency.scale_exp
|
124
|
+
part << '0'
|
125
|
+
end
|
126
|
+
|
127
|
+
# Truncate to Currency's decimal size.
|
128
|
+
part = part[0 ... currency.scale_exp]
|
129
|
+
|
130
|
+
# $stderr.puts " => INEXACT DECIMAL '#{whole}'"
|
131
|
+
end
|
132
|
+
|
133
|
+
# Put the string back together:
|
134
|
+
# #{sign}#{whole}#{part}
|
135
|
+
whole = sign + whole + part
|
136
|
+
# $stderr.puts " => REP = #{whole}"
|
137
|
+
|
138
|
+
x = whole.to_i
|
139
|
+
|
140
|
+
x = ::Currency::Money.new_rep(x, currency, time)
|
141
|
+
else
|
142
|
+
# $stderr.puts "'#{self}'.parse(#{str}) => ??? '#{x}'"
|
143
|
+
#x.to_f.Money_rep(self)
|
144
|
+
raise ::Currency::Exception::InvalidMoneyString,
|
145
|
+
[
|
146
|
+
"#{str.inspect} #{currency} #{x.inspect}",
|
147
|
+
:string, str,
|
148
|
+
:currency, currency,
|
149
|
+
:state, x,
|
150
|
+
]
|
151
|
+
end
|
152
|
+
|
153
|
+
# Do conversion.
|
154
|
+
if convert_currency
|
155
|
+
x = x.convert(convert_currency)
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
x
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
@@empty_hash = { }
|
164
|
+
@@empty_hash.freeze
|
165
|
+
|
166
|
+
# Parse a Money string in this Currency.
|
167
|
+
#
|
168
|
+
# "123.45".money # Using default Currency.
|
169
|
+
# => USD $123.45
|
170
|
+
#
|
171
|
+
# "$123.45 USD".money # Explicit Currency.
|
172
|
+
# => USD $123.45
|
173
|
+
#
|
174
|
+
# "CAD 123.45".money
|
175
|
+
# => CAD $123.45
|
176
|
+
#
|
177
|
+
# "123.45 CAD".money(:USD) # Incompatible explicit Currency.
|
178
|
+
# !!! "123.45 CAD" USD (Currency::Exception::IncompatibleCurrency)
|
179
|
+
#
|
180
|
+
def parse(str, opt = @@empty_hash)
|
181
|
+
prs = self
|
182
|
+
|
183
|
+
unless opt.empty?
|
184
|
+
prs = prs.clone
|
185
|
+
opt.each_pair{ | k, v | prs.send("#{k}=", v) }
|
186
|
+
end
|
187
|
+
|
188
|
+
prs._parse(str)
|
189
|
+
end
|
190
|
+
|
191
|
+
end # class
|
192
|
+
|
193
|
+
|