currency 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/COPYING.txt +339 -0
  2. data/LICENSE.txt +62 -0
  3. data/Manifest.txt +37 -14
  4. data/README.txt +8 -0
  5. data/Rakefile +42 -8
  6. data/Releases.txt +26 -0
  7. data/TODO.txt +1 -0
  8. data/examples/ex1.rb +3 -3
  9. data/examples/xe1.rb +3 -2
  10. data/lib/currency.rb +71 -9
  11. data/lib/currency/active_record.rb +138 -21
  12. data/lib/currency/core_extensions.rb +7 -5
  13. data/lib/currency/currency.rb +94 -177
  14. data/lib/currency/{currency_factory.rb → currency/factory.rb} +46 -25
  15. data/lib/currency/currency_version.rb +3 -3
  16. data/lib/currency/exception.rb +14 -14
  17. data/lib/currency/exchange.rb +14 -12
  18. data/lib/currency/exchange/rate.rb +159 -28
  19. data/lib/currency/exchange/rate/deriver.rb +146 -0
  20. data/lib/currency/exchange/rate/source.rb +84 -0
  21. data/lib/currency/exchange/rate/source/base.rb +156 -0
  22. data/lib/currency/exchange/rate/source/failover.rb +57 -0
  23. data/lib/currency/exchange/rate/source/historical.rb +79 -0
  24. data/lib/currency/exchange/rate/source/historical/rate.rb +181 -0
  25. data/lib/currency/exchange/rate/source/historical/writer.rb +203 -0
  26. data/lib/currency/exchange/rate/source/new_york_fed.rb +91 -0
  27. data/lib/currency/exchange/rate/source/provider.rb +105 -0
  28. data/lib/currency/exchange/rate/source/test.rb +50 -0
  29. data/lib/currency/exchange/rate/source/the_financials.rb +190 -0
  30. data/lib/currency/exchange/rate/source/timed_cache.rb +144 -0
  31. data/lib/currency/exchange/rate/source/xe.rb +166 -0
  32. data/lib/currency/exchange/time_quantitizer.rb +111 -0
  33. data/lib/currency/formatter.rb +159 -0
  34. data/lib/currency/macro.rb +321 -0
  35. data/lib/currency/money.rb +90 -64
  36. data/lib/currency/money_helper.rb +6 -5
  37. data/lib/currency/parser.rb +153 -0
  38. data/test/ar_column_test.rb +6 -3
  39. data/test/ar_simple_test.rb +5 -2
  40. data/test/ar_test_base.rb +39 -33
  41. data/test/ar_test_core.rb +64 -0
  42. data/test/formatter_test.rb +81 -0
  43. data/test/historical_writer_test.rb +184 -0
  44. data/test/macro_test.rb +109 -0
  45. data/test/money_test.rb +72 -4
  46. data/test/new_york_fed_test.rb +57 -0
  47. data/test/parser_test.rb +60 -0
  48. data/test/test_base.rb +13 -3
  49. data/test/time_quantitizer_test.rb +136 -0
  50. data/test/xe_test.rb +29 -5
  51. metadata +41 -18
  52. data/lib/currency/exchange/base.rb +0 -84
  53. data/lib/currency/exchange/test.rb +0 -39
  54. data/lib/currency/exchange/xe.rb +0 -250
@@ -1,40 +1,39 @@
1
- # -*- ruby -*-
2
- #
3
- # = Currency::Money
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
4
  #
5
5
  # Represents an amount of money in a particular currency.
6
6
  #
7
7
  # A Money object stores its value using a scaled Integer representation
8
8
  # and a Currency object.
9
9
  #
10
- # TODO:
11
- # * Need to store a time, so we can use historical FX rates.
10
+ # A Money object also has a time, which is used in conversions
11
+ # against historical exchange rates.
12
12
  #
13
-
14
- module Currency
15
-
16
- # Use this function instead of Money#new:
17
- #
18
- # Currency::Money("12.34", :CAD)
19
- #
20
- # not
21
- #
22
- # Currency::Money.new("12.34", :CAD)
23
- #
24
- # See Money#new.
25
- def self.Money(*opts)
26
- Money.new(*opts)
27
- end
28
-
29
- class Money
13
+ class Currency::Money
30
14
  include Comparable
31
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.
32
31
  #
33
32
  # Construct a Money value object
34
33
  # from a pre-scaled external representation:
35
34
  # where x is a Float, Integer, String, etc.
36
35
  #
37
- # If a currency is not specified, Currency#default is used.
36
+ # If a currency is not specified, Currency.default is used.
38
37
  #
39
38
  # x.Money_rep(currency)
40
39
  #
@@ -47,63 +46,83 @@ module Currency
47
46
  # Because the USD Currency object has a #scale of 100
48
47
  #
49
48
  # See #Money_rep(currency) mixin.
50
- # See Currency#Money() function.
51
49
  #
52
- def initialize(x, currency = nil)
53
- # Xform currency
54
- currency = Currency.default if currency.nil?
55
- currency = Currency.get(currency) unless currency.kind_of?(Currency)
56
-
57
- # Set ivars
58
- @currency = currency;
59
- @rep = x.Money_rep(@currency)
60
-
61
- # Handle conversion of "USD 123.45"
62
- if @rep.kind_of?(Money)
63
- @currency = @rep.currency
64
- @rep = @rep.rep
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)
65
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
66
78
  end
67
79
 
68
80
  # Compatibility with Money package.
69
81
  def self.us_dollar(x)
70
82
  self.new(x, :USD)
71
83
  end
84
+
85
+
72
86
  # Compatibility with Money package.
73
87
  def cents
74
88
  @rep
75
89
  end
76
90
 
77
91
 
78
- # Construct from post-scaled internal representation
79
- def self.new_rep(r, currency = nil)
80
- x = self.new(0, currency)
92
+ # Construct from post-scaled internal representation.
93
+ def self.new_rep(r, currency = nil, time = nil)
94
+ x = self.new(0, currency, time)
81
95
  x.set_rep(r)
82
96
  x
83
97
  end
84
- # Construct from post-scaled internal representation
85
- # using the same currency.
86
- # x = Currency::Money.new("1.98", :USD)
98
+
99
+
100
+ # Construct from post-scaled internal representation.
101
+ # using the same currency:
102
+ #
103
+ # x = Currency.Money("1.98", :USD)
87
104
  # x.new_rep(100) => "$1.00 USD"
88
105
  #
89
- def new_rep(r)
90
- x = self.class.new(0, @currency)
106
+ def new_rep(r, time = nil)
107
+ time ||= @time
108
+ x = self.class.new(0, @currency, time)
91
109
  x.set_rep(r)
92
110
  x
93
111
  end
94
112
 
95
- # Do not call this method directly
113
+ # Do not call this method directly.
96
114
  # CLIENTS SHOULD NEVER CALL set_rep DIRECTLY.
97
115
  # You have been warned in ALL CAPS.
98
- def set_rep(r)
116
+ def set_rep(r) # :nodoc:
99
117
  r = r.to_i unless r.kind_of?(Integer)
100
118
  @rep = r
101
119
  end
102
120
 
103
- # Returns the money representation for the requested currency.
104
- def Money_rep(currency)
105
- $stderr.puts "@currency != currency (#{@currency.inspect} != #{currency.inspect}" unless @currency == currency
106
- @rep
121
+ # Do not call this method directly.
122
+ # CLIENTS SHOULD NEVER CALL set_time DIRECTLY.
123
+ # You have been warned in ALL CAPS.
124
+ def set_time(time) # :nodoc:
125
+ @time = time
107
126
  end
108
127
 
109
128
  # Returns the Money representation (usually an Integer).
@@ -116,25 +135,33 @@ module Currency
116
135
  @currency
117
136
  end
118
137
 
138
+ # Get the Money's time.
139
+ def time
140
+ @time
141
+ end
142
+
119
143
  # Convert Money to another Currency.
120
144
  # currency can be a Symbol or a Currency object.
121
145
  # If currency is nil, the Currency.default is used.
122
- def convert(currency)
123
- currency = Currency.default if currency.nil?
124
- currency = Currency.get(currency) unless currency.kind_of?(Currency)
146
+ def convert(currency, time = nil)
147
+ currency = ::Currency::Currency.default if currency.nil?
148
+ currency = ::Currency::Currency.get(currency) unless currency.kind_of?(Currency)
125
149
  if @currency == currency
126
150
  self
127
151
  else
128
- Exchange.current.convert(self, currency)
152
+ time = self.time if time == :money
153
+ Exchange::Rate::Source.current.convert(self, currency, time)
129
154
  end
130
155
  end
131
156
 
157
+
132
158
  # Hash for hash table: both value and currency.
133
159
  # See #eql? below.
134
160
  def hash
135
161
  @rep.hash ^ @currency.hash
136
162
  end
137
163
 
164
+
138
165
  # True if money values have the same value and currency.
139
166
  def eql?(x)
140
167
  self.class == x.class &&
@@ -155,7 +182,7 @@ module Currency
155
182
  if @currency == x.currency
156
183
  @rep <=> x.rep
157
184
  else
158
- @rep <=> convert(@currency).rep
185
+ @rep <=> convert(@currency, @time).rep
159
186
  end
160
187
  end
161
188
 
@@ -188,7 +215,7 @@ module Currency
188
215
  new_rep(@rep * x)
189
216
  end
190
217
 
191
- # Money / Money => Number (ratio)
218
+ # Money / Money => Float (ratio)
192
219
  # Money / Number => Money
193
220
  #
194
221
  # Right side must be Money or Number.
@@ -202,7 +229,7 @@ module Currency
202
229
  end
203
230
  end
204
231
 
205
- # Formats the Money value as a String.
232
+ # Formats the Money value as a String using the Currency's Formatter.
206
233
  def format(*opt)
207
234
  @currency.format(self, *opt)
208
235
  end
@@ -240,10 +267,10 @@ module Currency
240
267
  end
241
268
 
242
269
  # Returns the Money's value representation in another currency.
243
- def Money_rep(currency)
270
+ def Money_rep(currency, time = nil)
244
271
  # Attempt conversion?
245
- if @currency != currency
246
- self.convert(currency).rep
272
+ if @currency != currency || (time && @time != time)
273
+ self.convert(currency, time).rep
247
274
  # raise("Incompatible Currency: #{@currency} != #{currency}")
248
275
  else
249
276
  @rep
@@ -253,7 +280,7 @@ module Currency
253
280
  # Basic inspection, with symbol and currency code.
254
281
  # The standard #inspect method is available as #inspect_deep.
255
282
  def inspect(*opts)
256
- self.format(:with_symbol, :with_currency).inspect
283
+ self.format(:symbol => true, :code => true)
257
284
  end
258
285
 
259
286
  # How to alias a method defined in an object superclass in a different class:
@@ -263,6 +290,5 @@ module Currency
263
290
  # self.class.superclass.instance_method(:inspect).bind(self).call
264
291
  # end
265
292
 
266
- end # class
293
+ end # class
267
294
 
268
- end # module
@@ -1,12 +1,13 @@
1
- module ActionView
2
- module Helpers
3
- module MoneyHelper
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.
4
6
  def money_field(object, method, options = {})
5
7
  InstanceTag.new(object, method, self).to_input_field_tag("text", options)
6
8
  end
7
- end
8
- end
9
9
  end
10
10
 
11
11
 
12
12
  ActionView::Base.load_helper(File.dirname(__FILE__))
13
+
@@ -0,0 +1,153 @@
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+
5
+ # This class parses a Money value from a String.
6
+ # Each Currency has a default Parser.
7
+ class Currency::Parser
8
+
9
+ # The default Currency to use if no Currency is specified.
10
+ attr_accessor :currency
11
+
12
+ # If true and a parsed string contains a ISO currency code
13
+ # that is not the same as currency,
14
+ # #parse() will raise IncompatibleCurrency.
15
+ # Defaults to false.
16
+ attr_accessor :enforce_currency
17
+
18
+ # The default Time to use if no Time is specified in the string.
19
+ attr_accessor :time
20
+
21
+ @@default = nil
22
+ # Get the default Formatter.
23
+ def self.default
24
+ @@default || self.new
25
+ end
26
+
27
+
28
+ # Set the default Formatter.
29
+ def self.default=(x)
30
+ @@default = x
31
+ end
32
+
33
+
34
+ def initialize(opt = { })
35
+ opt.each_pair{ | k, v | self.send("#{k}=", v) }
36
+ end
37
+
38
+
39
+ def _parse(str) # :nodoc:
40
+ x = str
41
+
42
+ # Get currency.
43
+ # puts "str = #{str.inspect}, @currency = #{@currency}"
44
+
45
+ md = nil # match data
46
+
47
+ # $stderr.puts "'#{x}'.Money_rep(#{self})"
48
+
49
+ # $stderr.puts "x = #{x}"
50
+ convert_currency = nil
51
+ # Handle currency code at front of string.
52
+ if (md = /([A-Z][A-Z][A-Z])/.match(x))
53
+ curr = ::Currency::Currency.get(md[1])
54
+ x = md.pre_match + md.post_match
55
+ if @currency && @currency != curr
56
+ if @enforce_currency
57
+ raise ::Currency::Exception::IncompatibleCurrency.new("#{str.inspect} #{@currency.code}")
58
+ end
59
+ convert_currency = @currency
60
+ end
61
+ currency = curr
62
+ else
63
+ currency = @currency || ::Currency::Currency.default
64
+ currency = ::Currency::Currency.get(currency)
65
+ end
66
+
67
+ # Remove placeholders and symbol.
68
+ x = x.gsub(/[, ]/, '')
69
+ symbol = currency.symbol # FIXME
70
+ x = x.gsub(symbol, '') if symbol
71
+
72
+ # $stderr.puts "x = #{x.inspect}"
73
+ # Match: whole Currency value.
74
+ if md = /^([-+]?\d+)\.?$/.match(x)
75
+ # $stderr.puts "'#{self}'.parse(#{str}) => EXACT"
76
+ x = ::Currency::Money.new_rep(md[1].to_i * currency.scale, currency, @time)
77
+
78
+ # Match: fractional Currency value.
79
+ elsif md = /^([-+]?)(\d*)\.(\d+)$/.match(x)
80
+ sign = md[1]
81
+ whole = md[2]
82
+ part = md[3]
83
+
84
+ # $stderr.puts "'#{self}'.parse(#{str}) => DECIMAL (#{sign} #{whole} . #{part})"
85
+
86
+ if part.length != currency.scale
87
+
88
+ # Pad decimal places with additional '0'
89
+ while part.length < currency.scale_exp
90
+ part << '0'
91
+ end
92
+
93
+ # Truncate to Currency's decimal size.
94
+ part = part[0 ... currency.scale_exp]
95
+
96
+ # $stderr.puts " => INEXACT DECIMAL '#{whole}'"
97
+ end
98
+
99
+ # Put the string back together:
100
+ # #{sign}#{whole}#{part}
101
+ whole = sign + whole + part
102
+ # $stderr.puts " => REP = #{whole}"
103
+
104
+ x = whole.to_i
105
+
106
+ x = ::Currency::Money.new_rep(x, currency, @time)
107
+ else
108
+ # $stderr.puts "'#{self}'.parse(#{str}) => ??? '#{x}'"
109
+ #x.to_f.Money_rep(self)
110
+ raise ::Currency::Exception::InvalidMoneyString.new("#{str.inspect} #{currency} #{x.inspect}")
111
+ end
112
+
113
+ # Do conversion.
114
+ if convert_currency
115
+ x = x.convert(convert_currency)
116
+ end
117
+
118
+
119
+ x
120
+ end
121
+
122
+
123
+ @@empty_hash = { }
124
+ @@empty_hash.freeze
125
+
126
+ # Parse a Money string in this Currency.
127
+ #
128
+ # "123.45".money # Using default Currency.
129
+ # => $123.45 USD
130
+ #
131
+ # "123.45 USD".money # Explicit Currency.
132
+ # => $123.45 USD
133
+ #
134
+ # "123.45 CAD".money
135
+ # => $123.45 CAD
136
+ #
137
+ # "123.45 CAD".money(:USD) # Incompatible explicit Currency.
138
+ # !!! "123.45 CAD" USD (Currency::Exception::IncompatibleCurrency)
139
+ #
140
+ def parse(str, opt = @@empty_hash)
141
+ prs = self
142
+
143
+ unless opt.empty?
144
+ prs = prs.clone
145
+ opt.each_pair{ | k, v | prs.send("#{k}=", v) }
146
+ end
147
+
148
+ prs._parse(str)
149
+ end
150
+
151
+ end # class
152
+
153
+
@@ -1,4 +1,7 @@
1
- require 'test/ar_test_base'
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ require 'test/ar_test_core'
2
5
  require 'currency'
3
6
 
4
7
  require 'rubygems'
@@ -8,7 +11,7 @@ require 'currency/active_record'
8
11
 
9
12
  module Currency
10
13
 
11
- class ArFieldTest < ArTestBase
14
+ class ArFieldTest < ArTestCore
12
15
 
13
16
  ##################################################
14
17
  # Basic CurrenyTest AR::B class
@@ -32,7 +35,7 @@ class ArFieldTest < ArTestBase
32
35
 
33
36
  class CurrencyColumnTest < AR_B
34
37
  set_table_name TABLE_NAME
35
- money :amount, :currency_column => true
38
+ attr_money :amount, :currency_column => true
36
39
  end
37
40
 
38
41
  ##################################################