currency 0.3.3 → 0.4.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 (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
  ##################################################