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.
Files changed (57) 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 +83 -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 +48 -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 +310 -0
  38. data/lib/currency/macro.rb +321 -0
  39. data/lib/currency/money.rb +298 -0
  40. data/lib/currency/money_helper.rb +13 -0
  41. data/lib/currency/parser.rb +193 -0
  42. data/spec/ar_column_spec.rb +76 -0
  43. data/spec/ar_core_spec.rb +68 -0
  44. data/spec/ar_simple_spec.rb +23 -0
  45. data/spec/config_spec.rb +29 -0
  46. data/spec/federal_reserve_spec.rb +75 -0
  47. data/spec/formatter_spec.rb +72 -0
  48. data/spec/historical_writer_spec.rb +187 -0
  49. data/spec/macro_spec.rb +109 -0
  50. data/spec/money_spec.rb +355 -0
  51. data/spec/new_york_fed_spec.rb +73 -0
  52. data/spec/parser_spec.rb +105 -0
  53. data/spec/spec_helper.rb +25 -0
  54. data/spec/time_quantitizer_spec.rb +115 -0
  55. data/spec/timed_cache_spec.rb +95 -0
  56. data/spec/xe_spec.rb +50 -0
  57. 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
+