currency 0.3.1 → 0.3.2

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.
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: