acvwilson-acvwilson-currency 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/COPYING.txt +339 -0
  2. data/ChangeLog +8 -0
  3. data/LICENSE.txt +65 -0
  4. data/Manifest.txt +58 -0
  5. data/README.txt +51 -0
  6. data/Releases.txt +155 -0
  7. data/TODO.txt +9 -0
  8. data/currency.gemspec +18 -0
  9. data/examples/ex1.rb +13 -0
  10. data/examples/xe1.rb +20 -0
  11. data/lib/currency.rb +143 -0
  12. data/lib/currency/active_record.rb +265 -0
  13. data/lib/currency/config.rb +91 -0
  14. data/lib/currency/core_extensions.rb +48 -0
  15. data/lib/currency/currency.rb +175 -0
  16. data/lib/currency/currency/factory.rb +121 -0
  17. data/lib/currency/currency_version.rb +6 -0
  18. data/lib/currency/exception.rb +119 -0
  19. data/lib/currency/exchange.rb +50 -0
  20. data/lib/currency/exchange/rate.rb +214 -0
  21. data/lib/currency/exchange/rate/deriver.rb +157 -0
  22. data/lib/currency/exchange/rate/source.rb +89 -0
  23. data/lib/currency/exchange/rate/source/base.rb +166 -0
  24. data/lib/currency/exchange/rate/source/failover.rb +63 -0
  25. data/lib/currency/exchange/rate/source/federal_reserve.rb +160 -0
  26. data/lib/currency/exchange/rate/source/historical.rb +79 -0
  27. data/lib/currency/exchange/rate/source/historical/rate.rb +184 -0
  28. data/lib/currency/exchange/rate/source/historical/rate_loader.rb +186 -0
  29. data/lib/currency/exchange/rate/source/historical/writer.rb +220 -0
  30. data/lib/currency/exchange/rate/source/new_york_fed.rb +127 -0
  31. data/lib/currency/exchange/rate/source/provider.rb +120 -0
  32. data/lib/currency/exchange/rate/source/test.rb +50 -0
  33. data/lib/currency/exchange/rate/source/the_financials.rb +191 -0
  34. data/lib/currency/exchange/rate/source/timed_cache.rb +198 -0
  35. data/lib/currency/exchange/rate/source/xe.rb +165 -0
  36. data/lib/currency/exchange/time_quantitizer.rb +111 -0
  37. data/lib/currency/formatter.rb +300 -0
  38. data/lib/currency/macro.rb +321 -0
  39. data/lib/currency/money.rb +296 -0
  40. data/lib/currency/money_helper.rb +13 -0
  41. data/lib/currency/parser.rb +193 -0
  42. data/spec/ar_base_spec.rb +140 -0
  43. data/spec/ar_column_spec.rb +69 -0
  44. data/spec/ar_core_spec.rb +64 -0
  45. data/spec/ar_simple_spec.rb +31 -0
  46. data/spec/config_spec.rb +29 -0
  47. data/spec/federal_reserve_spec.rb +75 -0
  48. data/spec/formatter_spec.rb +72 -0
  49. data/spec/historical_writer_spec.rb +187 -0
  50. data/spec/macro_spec.rb +109 -0
  51. data/spec/money_spec.rb +347 -0
  52. data/spec/new_york_fed_spec.rb +73 -0
  53. data/spec/parser_spec.rb +105 -0
  54. data/spec/spec_helper.rb +25 -0
  55. data/spec/time_quantitizer_spec.rb +115 -0
  56. data/spec/timed_cache_spec.rb +95 -0
  57. data/spec/xe_spec.rb +50 -0
  58. metadata +117 -0
@@ -0,0 +1,296 @@
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
+ @currency.format(self, *opt)
241
+ end
242
+
243
+ # Coerces the Money's value to a Float.
244
+ # May cause loss of precision.
245
+ def to_f
246
+ Float(@rep) / @currency.scale
247
+ end
248
+
249
+ # Coerces the Money's value to an Integer.
250
+ # May cause loss of precision.
251
+ def to_i
252
+ @rep / @currency.scale
253
+ end
254
+
255
+ # True if the Money's value is zero.
256
+ def zero?
257
+ @rep == 0
258
+ end
259
+
260
+ # True if the Money's value is greater than zero.
261
+ def positive?
262
+ @rep > 0
263
+ end
264
+
265
+ # True if the Money's value is less than zero.
266
+ def negative?
267
+ @rep < 0
268
+ end
269
+
270
+ # Returns the Money's value representation in another currency.
271
+ def Money_rep(currency, time = nil)
272
+ # Attempt conversion?
273
+ if @currency != currency || (time && @time != time)
274
+
275
+ self.convert(currency, time).rep
276
+ # raise ::Currency::Exception::Generic, "Incompatible Currency: #{@currency} != #{currency}"
277
+ else
278
+ @rep
279
+ end
280
+ end
281
+
282
+ # Basic inspection, with symbol, currency code and time.
283
+ # The standard #inspect method is available as #inspect_deep.
284
+ def inspect(*opts)
285
+ self.format(:symbol => true, :code => true, :time => true)
286
+ end
287
+
288
+ # How to alias a method defined in an object superclass in a different class:
289
+ define_method(:inspect_deep, Object.instance_method(:inspect))
290
+ # How call a method defined in a superclass from a method with a different name:
291
+ # def inspect_deep(*opts)
292
+ # self.class.superclass.instance_method(:inspect).bind(self).call
293
+ # end
294
+
295
+ end # class
296
+
@@ -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
+