currency 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -7,16 +7,50 @@
7
7
  require 'rubygems'
8
8
  require 'hoe'
9
9
 
10
+ PKG_Name = 'Currency'
11
+ PKG_DESCRIPTION = %{Currency models currencies, monetary values, foreign exchanges.}
12
+
13
+ #################################################################
14
+ # Release notes
15
+ #
16
+
17
+ def get_release_notes(relfile = "Releases")
18
+
19
+ release = nil
20
+ notes = [ ]
21
+
22
+ File.open(relfile) do |f|
23
+ while line = f.readline
24
+ if md = /^== Release ([\d\.]+)/i.match(line)
25
+ release = md[1]
26
+ notes << line
27
+ break
28
+ end
29
+ end
30
+
31
+ while line = f.readline
32
+ if md = /^== Release ([\d\.]+)/i.match(line)
33
+ break
34
+ end
35
+ notes << line
36
+ end
37
+ end
38
+
39
+ [ release, notes.join('') ]
40
+ end
41
+
10
42
  #################################################################
11
43
 
12
- PKG_Name = 'Currency'
13
44
  PKG_NAME = PKG_Name.gsub(/[a-z][A-Z]/) {|x| "#{x[0,1]}_#{x[1,1]}"}.downcase
14
45
 
15
- hoe = Hoe.new("currency", '0.3.1') do |p|
46
+ release, release_notes = get_release_notes
47
+
48
+ hoe = Hoe.new("currency", release) do |p|
16
49
  p.author = 'Kurt Stephens'
17
- p.description = %{Currency models currencies, monetary values, foreign exchanges.}
50
+ p.description = PKG_DESCRIPTION
18
51
  p.email = "ruby-#{PKG_NAME}@umleta.com"
19
52
  p.summary = p.description
53
+ p.changes = release_notes
20
54
  p.url = "http://rubyforge.org/projects/#{PKG_NAME}"
21
55
 
22
56
  p.test_globs = ['test/**/*.rb']
data/Releases CHANGED
@@ -1,5 +1,12 @@
1
1
  = Currency Release History
2
2
 
3
+ == Release 0.3.2: 2006/10/31
4
+
5
+ * BOO!
6
+ * Fixed Currency.symbol formatting when Currency.symbol.nil?
7
+ * Added expiration of rates in Xe.
8
+ * Added more Money tests.
9
+
3
10
  == Release 0.3.1: 2006/10/31
4
11
 
5
12
  * Remove debug puts.
@@ -1,6 +1,17 @@
1
+
2
+
3
+ # External representation mixin
4
+ class Object
5
+ # Exact conversion to Money representation value.
6
+ def money(*opts)
7
+ Currency::Money(self, *opts)
8
+ end
9
+ end
10
+
11
+
1
12
  # External representation mixin
2
13
  class Integer
3
- # Exact
14
+ # Exact conversion to Money representation value.
4
15
  def Money_rep(currency)
5
16
  Integer(self * currency.scale)
6
17
  end
@@ -9,7 +20,7 @@ end
9
20
 
10
21
  # External representation mixin
11
22
  class Float
12
- # Inexact
23
+ # Inexact conversion to Money representation value.
13
24
  def Money_rep(currency)
14
25
  Integer(self * currency.scale)
15
26
  end
@@ -18,7 +29,7 @@ end
18
29
 
19
30
  # External representation mixin
20
31
  class String
21
- # Exact
32
+ # Exact conversion to Money representation value.
22
33
  def Money_rep(currency)
23
34
  x = currency.parse(self, :currency => currency)
24
35
  x.rep if x.kind_of?(Currency::Money)
@@ -212,7 +212,7 @@ module Currency
212
212
  x = "-" + x if neg
213
213
 
214
214
  # Add symbol?
215
- x = @symbol + x unless opt.include?(:no_symbol)
215
+ x = (@symbol || '') + x unless opt.include?(:no_symbol)
216
216
 
217
217
  # Suffix currency code.
218
218
  if opt.include?(:with_currency)
@@ -233,6 +233,10 @@ module Currency
233
233
  def self.default
234
234
  CurrencyFactory.default.currency
235
235
  end
236
+ # Sets the default CurrencyFactory's currency.
237
+ def self.default=(x)
238
+ CurrencyFactory.default.currency = x
239
+ end
236
240
 
237
241
  # Returns the USD Currency.
238
242
  def self.USD
@@ -1,12 +1,17 @@
1
1
  module Currency
2
2
  class CurrencyFactory
3
- # Default factory.
4
3
  @@default = nil
4
+
5
+ # Returns the default CurrencyFactory.
5
6
  def self.default
6
7
  @@default ||= CurrencyFactory.new
7
8
  end
9
+ # Sets the default CurrencyFactory.
10
+ def self.default=(x)
11
+ @@default = x
12
+ end
8
13
 
9
- def initialize
14
+ def initialize(*opts)
10
15
  @currency_by_code = { }
11
16
  @currency_by_symbol = { }
12
17
  @currency = nil
@@ -14,60 +19,82 @@ module Currency
14
19
  @CAD = nil
15
20
  end
16
21
 
17
- # Lookup table by code
22
+ # Lookup Currency by code.
18
23
  def get_by_code(x)
19
- x = Currency.cast_code(x)
20
- # $stderr.puts "get_by_code(#{x})"
21
- @currency_by_code[x] ||= load(Currency.new(x))
24
+ x = Currency.cast_code(x)
25
+ # $stderr.puts "get_by_code(#{x})"
26
+ @currency_by_code[x] ||= install(load(Currency.new(x)))
22
27
  end
23
28
 
24
- # Lookup table by symbol
29
+ # Lookup Currency by symbol.
25
30
  def get_by_symbol(symbol)
26
- @currency_by_symbol[symbol] ||= load(Currency.new(nil, symbol))
31
+ @currency_by_symbol[symbol] ||= install(load(Currency.new(nil, symbol)))
27
32
  end
28
33
 
34
+ # This method initializes a Currency object as
35
+ # requested from #get_by_code or #get_by_symbol.
36
+ #
37
+ # This method must initialize:
38
+ #
39
+ # currency.code
40
+ # currency.symbol
41
+ # currency.scale
42
+ #
43
+ # Subclasses that provide Currency metadata should override this method.
44
+ # For example, loading Currency metadata from a database or YAML file.
29
45
  def load(currency)
30
46
  # $stderr.puts "BEFORE: load(#{currency.code})"
31
47
 
32
- # SAMPLE CODE
48
+ # Basic
33
49
  if currency.code == :USD || currency.symbol == '$'
34
50
  # $stderr.puts "load('USD')"
35
- currency.code= :USD
36
- currency.symbol= '$'
37
- currency.scale= 100
51
+ currency.code = :USD
52
+ currency.symbol = '$'
53
+ currency.scale = 100
38
54
  elsif currency.code == :CAD
39
55
  # $stderr.puts "load('CAD')"
40
- currency.symbol= '$'
41
- currency.scale= 100
56
+ currency.symbol = '$'
57
+ currency.scale = 100
42
58
  else
43
- currency.symbol= nil
44
- currency.scale= 100
59
+ currency.symbol = nil
60
+ currency.scale = 100
45
61
  end
46
62
 
47
63
  # $stderr.puts "AFTER: load(#{currency.inspect})"
48
64
 
49
- install(currency)
65
+ currency
50
66
  end
51
67
 
68
+ # Installs a new Currency for #get_by_symbol and #get_by_code.
52
69
  def install(currency)
70
+ raise Exception::UnknownCurrency.new() unless currency
53
71
  @currency_by_symbol[currency.symbol] ||= currency unless currency.symbol.nil?
54
72
  @currency_by_code[currency.code] = currency
55
73
  end
56
74
 
57
- # Standard Currencys
75
+ # Standard Currency: US Dollars, :USD.
58
76
  def USD
59
77
  @USD ||= self.get_by_code(:USD)
60
78
  end
61
79
 
80
+ # Standard Currency: Canadian Dollars, :CAD.
62
81
  def CAD
63
82
  @CAD ||= self.get_by_code(:CAD)
64
83
  end
65
84
 
66
- # Default Currency
85
+ # Returns the default Currency.
86
+ # Defaults to self.USD.
67
87
  def currency
68
88
  @currency ||= self.USD
69
89
  end
70
- end
71
- # END MODULE
72
- end
90
+
91
+ # Sets the default Currency.
92
+ def currency=(x)
93
+ @currency = x
94
+ end
95
+
96
+ end # class
97
+ end # module
98
+
99
+
73
100
 
@@ -2,5 +2,5 @@
2
2
  # This file is auto-generated by build scripts.
3
3
  # See: rake update_version
4
4
  module Currency
5
- CurrencyVersion = '0.3.1'
5
+ CurrencyVersion = '0.3.2'
6
6
  end
@@ -14,11 +14,18 @@ module Exchange
14
14
 
15
15
  # Represents a method of converting between two currencies
16
16
  class Base
17
+
18
+ # The name of this Exchange.
17
19
  attr_accessor :name
18
20
 
21
+ # If true, this Exchange will log information.
22
+ attr_accessor :verbose
23
+
19
24
  def initialize(*opt)
20
25
  @name = nil
21
26
  @rate = { }
27
+ opt = Hash[*opt]
28
+ opt.each{|k,v| self.send(k, v)}
22
29
  end
23
30
 
24
31
  # Converts Money m in Currency c1 to a new
@@ -34,7 +41,7 @@ module Exchange
34
41
 
35
42
  # Flush all cached Rate.
36
43
  def clear_rates
37
- @rate.empty!
44
+ @rate.clear
38
45
  end
39
46
 
40
47
  # Flush any cached Rate between Currency c1 and c2.
@@ -47,6 +54,10 @@ module Exchange
47
54
  #
48
55
  # This will call #get_rate(c1, c2) if the
49
56
  # Rate has not already been cached.
57
+ #
58
+ # Subclasses can override this method to implement
59
+ # rate expiration rules.
60
+ #
50
61
  def rate(c1, c2)
51
62
  (@rate[c1.code.to_s + c2.code.to_s] ||= get_rate(c1, c2)) ||
52
63
  (@rate[c2.code.to_s + c1.code.to_s] ||= get_rate(c2, c1))
@@ -9,7 +9,6 @@ module Exchange
9
9
 
10
10
  class Xe < Base
11
11
  @@instance = nil
12
-
13
12
  # Returns a singleton instance.
14
13
  def self.instance(*opts)
15
14
  @@instance ||= self.new(*opts)
@@ -18,33 +17,118 @@ module Exchange
18
17
  # Defaults to "http://xe.com/"
19
18
  attr_accessor :uri
20
19
 
20
+ # Defines the number of seconds rates until rates
21
+ # become invalid, causing a request of new rates.
22
+ #
23
+ # Defaults to 600 seconds.
24
+ attr_accessor :time_to_live
25
+
26
+ # Defines the number of random seconds to add before
27
+ # rates become invalid.
28
+ #
29
+ # Defaults to 30 seconds.
30
+ attr_accessor :time_to_live_fudge
31
+
21
32
  # This Exchange's name is the same as its #uri.
22
33
  def name
23
34
  uri
24
35
  end
25
36
 
26
37
  def initialize(*opt)
27
- super(*opt)
28
38
  self.uri = 'http://xe.com/'
29
- @rates = nil
39
+ self.time_to_live = 600
40
+ self.time_to_live_fudge = 30
41
+ @xe_rates = nil
42
+ super(*opt)
43
+ end
44
+
45
+ def clear_rates
46
+ @xe_rates && @xe_rates.clear
47
+ super
48
+ end
49
+
50
+ def expired?
51
+ if @time_to_live &&
52
+ @xe_rates_renew_time &&
53
+ (Time.now > @xe_rates_renew_time)
54
+
55
+ if @xe_rates
56
+ $stderr.puts "#{self}: rates expired on #{@xe_rates_renew_time}" if @verbose
57
+
58
+ @old_rates ||= @xe_rates
59
+
60
+ @xe_rates = nil
61
+ end
62
+
63
+ true
64
+ else
65
+ false
66
+ end
67
+ end
68
+
69
+ # Check expired? before returning a Rate.
70
+ def rate(c1, c2)
71
+ if expired?
72
+ clear_rates
73
+ end
74
+ super(c1, c2)
30
75
  end
31
76
 
32
77
  # Returns a cached Hash of rates:
33
78
  #
34
- # xe.rates[:USD][:CAD] => 1.01
79
+ # xe.xe_rates[:USD][:CAD] => 1.0134
35
80
  #
36
- def rates
37
- return @rates if @rates
81
+ def xe_rates
82
+ old_rates = nil
83
+ # Check expiration.
84
+ expired?
38
85
 
39
- @rates = parse_page_rates
40
- return @rates unless @rates
86
+ # Return rates, if cached.
87
+ return @xe_rates if @xe_rates
41
88
 
42
- @rates_usd_cur = @rates[:USD]
43
- @rate_timestamp = Time.now
89
+ # Force load of rates
90
+ @xe_rates = xe_rates_load
91
+
92
+ # Flush old rates.
93
+ @old_rates = nil
94
+
95
+ # Update expiration.
96
+ if time_to_live
97
+ @xe_rates_renew_time = @rate_timestamp + (time_to_live + (time_to_live_fudge || 0))
98
+ $stderr.puts "#{self}: rates expire on #{@xe_rates_renew_time}" if @verbose
99
+ end
100
+
101
+ @xe_rates
102
+ end
103
+
104
+ def xe_rates_load
105
+ # Do not allow re-entrancy
106
+ raise "Reentrant!" if @processing_rates
107
+
108
+ # Begin processing new rate request.
109
+ @processing_rates = true
110
+
111
+ # Clear cached Rates.
112
+ clear_rates
113
+
114
+ # Parse rates from HTML page.
115
+ rates = parse_page_rates
44
116
 
45
- @rates
117
+ unless rates
118
+ # FIXME: raise Exception::???
119
+ return rates
120
+ end
121
+
122
+ # Compute new rate timeps
123
+ @rate_timestamp = Time.now # TODO: Extract this from HTML page!
124
+
125
+ # End processsing new rate request.
126
+ @processing_rates = false
127
+
128
+ rates
46
129
  end
47
130
 
131
+
48
132
  # Returns the URI content.
49
133
  def get_page
50
134
  data = open(uri) { |data| data.read }
@@ -131,18 +215,22 @@ module Exchange
131
215
  # Loads cached rates from xe.com and creates Rate objects
132
216
  # for 10 currencies.
133
217
  def get_rate(c1, c2)
134
- rates # Load rates
218
+ rates = xe_rates # Load rates
135
219
 
136
220
  # $stderr.puts "load_exchange_rate(#{c1}, #{c2})"
137
221
  rate = 0.0
138
222
  r1 = nil
139
223
  r2 = nil
140
224
 
141
- if ( c1.code == :USD && (r2 = @rates_usd_cur[c2.code]) )
225
+ rates_usd = rates[:USD]
226
+
227
+ raise Exception::UnknownRate.new("#{self}: base rate :USD") unless rates_usd
228
+
229
+ if ( c1.code == :USD && (r2 = rates_usd[c2.code]) )
142
230
  rate = r2
143
- elsif ( c2.code == :USD && (r1 = @rates_usd_cur[c2.code]) )
231
+ elsif ( c2.code == :USD && (r1 = rates_usd[c2.code]) )
144
232
  rate = 1.0 / r1
145
- elsif ( (r1 = @rates_usd_cur[c1.code]) && (r2 = @rates_usd_cur[c2.code]) )
233
+ elsif ( (r1 = rates_usd[c1.code]) && (r2 = rates_usd[c2.code]) )
146
234
  rate = r2 / r1
147
235
  end
148
236
 
@@ -106,17 +106,19 @@ module Currency
106
106
  @rep
107
107
  end
108
108
 
109
- # Returns the money representation (usually an Integer).
109
+ # Returns the Money representation (usually an Integer).
110
110
  def rep
111
111
  @rep
112
112
  end
113
113
 
114
- # Get the money's Currency.
114
+ # Get the Money's Currency.
115
115
  def currency
116
116
  @currency
117
117
  end
118
118
 
119
119
  # Convert Money to another Currency.
120
+ # currency can be a Symbol or a Currency object.
121
+ # If currency is nil, the Currency.default is used.
120
122
  def convert(currency)
121
123
  currency = Currency.default if currency.nil?
122
124
  currency = Currency.get(currency) unless currency.kind_of?(Currency)
@@ -147,6 +149,8 @@ module Currency
147
149
  @currency == x.currency
148
150
  end
149
151
 
152
+ # Compares Money values.
153
+ # Will convert x currency before comparision.
150
154
  def <=>(x)
151
155
  if @currency == x.currency
152
156
  @rep <=> x.rep
@@ -155,32 +159,31 @@ module Currency
155
159
  end
156
160
  end
157
161
 
158
- # Operations on Money values.
159
-
160
162
 
161
163
  # - Money => Money
164
+ #
162
165
  # Negates a Money value.
163
166
  def -@
164
167
  new_rep(- @rep)
165
168
  end
166
169
 
167
- # Money + (Number|Money) => Money
170
+ # Money + (Money | Number) => Money
168
171
  #
169
- # Right side maybe coerced to Money.
172
+ # Right side may be coerced to left side's Currency.
170
173
  def +(x)
171
174
  new_rep(@rep + x.Money_rep(@currency))
172
175
  end
173
176
 
174
- # Money - (Number|Money) => Money
177
+ # Money - (Money | Number) => Money
175
178
  #
176
- # Right side maybe coerced to Money.
179
+ # Right side may be coerced to left side's Currency.
177
180
  def -(x)
178
181
  new_rep(@rep - x.Money_rep(@currency))
179
182
  end
180
183
 
181
184
  # Money * Number => Money
182
185
  #
183
- # Right side must be number.
186
+ # Right side must be Number.
184
187
  def *(x)
185
188
  new_rep(@rep * x)
186
189
  end
@@ -188,7 +191,7 @@ module Currency
188
191
  # Money / Money => Number (ratio)
189
192
  # Money / Number => Money
190
193
  #
191
- # Right side must be a number or Money.
194
+ # Right side must be Money or Number.
192
195
  # Right side Integers are not coerced to Float before
193
196
  # division.
194
197
  def /(x)
data/test/money_test.rb CHANGED
@@ -16,12 +16,27 @@ class MoneyTest < TestBase
16
16
  #
17
17
 
18
18
  def test_create
19
- assert_kind_of Money, m = Money.
20
- new(1.99)
19
+ assert_kind_of Money, m = Money.new(1.99)
20
+ assert_equal m.currency, Currency.default
21
+ assert_equal m.currency.code, :USD
21
22
 
22
23
  m
23
24
  end
24
25
 
26
+ def test_object_money_method
27
+ assert_kind_of Money, m = 1.99.money(:USD)
28
+ assert_equal m.currency.code, :USD
29
+ assert_equal m.rep, 199
30
+
31
+ assert_kind_of Money, m = 199.money(:CAD)
32
+ assert_equal m.currency.code, :CAD
33
+ assert_equal m.rep, 19900
34
+
35
+ assert_kind_of Money, m = "13.98".money(:CAD)
36
+ assert_equal m.currency.code, :CAD
37
+ assert_equal m.rep, 1398
38
+ end
39
+
25
40
  def test_zero
26
41
  m = Money.new(0)
27
42
  assert ! m.negative?
@@ -70,6 +85,20 @@ class MoneyTest < TestBase
70
85
  assert z == test_zero
71
86
  end
72
87
 
88
+ def test_compare
89
+ n = test_negative
90
+ z = test_zero
91
+ p = test_positive
92
+
93
+ assert (n <=> p) == -1
94
+ assert (p <=> n) == 1
95
+ assert (p <=> z) == 1
96
+
97
+ assert (n <=> n) == 0
98
+ assert (z <=> z) == 0
99
+ assert (p <=> p) == 0
100
+ end
101
+
73
102
  def test_rep
74
103
  assert_not_nil m = Money.new(123, :USD)
75
104
  assert m.rep == 12300
@@ -91,6 +120,34 @@ class MoneyTest < TestBase
91
120
  end
92
121
 
93
122
  def test_eql
123
+ assert_not_nil usd1 = Money.new(123, :USD)
124
+ assert_not_nil usd2 = Money.new("123", :USD)
125
+
126
+ assert_equal :USD, usd1.currency.code
127
+ assert_equal :USD, usd2.currency.code
128
+
129
+ assert_equal usd1.rep, usd2.rep
130
+
131
+ assert usd1 == usd2
132
+
133
+ end
134
+
135
+ def test_not_eql
136
+
137
+ assert_not_nil usd1 = Money.new(123, :USD)
138
+ assert_not_nil usd2 = Money.new("123.01", :USD)
139
+
140
+ assert_equal :USD, usd1.currency.code
141
+ assert_equal :USD, usd2.currency.code
142
+
143
+ assert_not_equal usd1.rep, usd2.rep
144
+
145
+ assert usd1 != usd2
146
+
147
+ ################
148
+ # currency !=
149
+ # rep ==
150
+
94
151
  assert_not_nil usd = Money.new(123, :USD)
95
152
  assert_not_nil cad = Money.new(123, :CAD)
96
153
 
@@ -101,7 +158,7 @@ class MoneyTest < TestBase
101
158
  assert usd.currency != cad.currency
102
159
 
103
160
  assert usd != cad
104
-
161
+
105
162
  end
106
163
 
107
164
  def test_op
data/test/xe_test.rb CHANGED
@@ -12,7 +12,7 @@ class XeTest < TestBase
12
12
  end
13
13
 
14
14
  def test_xe_usd_cad
15
- assert_not_nil rates = Exchange.default.rates
15
+ assert_not_nil rates = Exchange.default.xe_rates
16
16
  assert_not_nil rates[:USD]
17
17
  assert_not_nil usd_cad = rates[:USD][:CAD]
18
18
 
metadata CHANGED
@@ -3,7 +3,7 @@ rubygems_version: 0.9.0
3
3
  specification_version: 1
4
4
  name: currency
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.3.1
6
+ version: 0.3.2
7
7
  date: 2006-10-31 00:00:00 -05:00
8
8
  summary: Currency models currencies, monetary values, foreign exchanges.
9
9
  require_paths: