currency 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +1 -1
- data/lib/currency.rb +27 -18
- data/lib/currency/active_record.rb +59 -1
- data/lib/currency/core_extensions.rb +9 -6
- data/lib/currency/currency.rb +49 -14
- data/lib/currency/currency_version.rb +1 -1
- data/lib/currency/exception.rb +34 -8
- data/lib/currency/exchange.rb +26 -4
- data/lib/currency/exchange/base.rb +36 -13
- data/lib/currency/exchange/rate.rb +1 -0
- data/lib/currency/exchange/test.rb +11 -6
- data/lib/currency/exchange/xe.rb +21 -4
- data/lib/currency/money.rb +97 -26
- data/test/money_test.rb +11 -3
- data/test/xe_test.rb +0 -3
- metadata +4 -3
data/Rakefile
CHANGED
@@ -12,7 +12,7 @@ require 'hoe'
|
|
12
12
|
PKG_Name = 'Currency'
|
13
13
|
PKG_NAME = PKG_Name.gsub(/[a-z][A-Z]/) {|x| "#{x[0,1]}_#{x[1,1]}"}.downcase
|
14
14
|
|
15
|
-
hoe = Hoe.new("currency", '0.
|
15
|
+
hoe = Hoe.new("currency", '0.2.0') do |p|
|
16
16
|
p.author = 'Kurt Stephens'
|
17
17
|
p.description = %{Currency models currencies, monetary values, foreign exchanges.}
|
18
18
|
p.email = "ruby-#{PKG_NAME}@umleta.com"
|
data/lib/currency.rb
CHANGED
@@ -1,16 +1,7 @@
|
|
1
1
|
# -*- ruby -*-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
require 'currency/exception'
|
7
|
-
require 'currency/money'
|
8
|
-
require 'currency/currency_factory'
|
9
|
-
require 'currency/currency'
|
10
|
-
require 'currency/money'
|
11
|
-
require 'currency/exchange'
|
12
|
-
require 'currency/core_extensions'
|
13
|
-
|
2
|
+
#
|
3
|
+
# = Currency
|
4
|
+
#
|
14
5
|
# The Currency package provides an object-oriented model of:
|
15
6
|
#
|
16
7
|
# * currencies
|
@@ -22,10 +13,12 @@ require 'currency/core_extensions'
|
|
22
13
|
#
|
23
14
|
# * Currency::Money - uses a scaled integer representation of a monetary value and performs accurate conversions to and from string values.
|
24
15
|
# * Currency::Currency - provides an object-oriented representation of a currency.
|
25
|
-
# * Currency::Exchange::Base - the base class for a
|
16
|
+
# * Currency::Exchange::Base - the base class for a currency exchange rate provider.
|
26
17
|
# * Currency::Exchange::Rate - represents a exchange rate between two currencies.
|
27
18
|
#
|
28
|
-
#
|
19
|
+
#
|
20
|
+
# The example below uses Currency::Exchange::Xe to automatically get
|
21
|
+
# exchange rates from http://xe.com/ :
|
29
22
|
#
|
30
23
|
# require 'currency'
|
31
24
|
# require 'currency/exchange/xe'
|
@@ -35,8 +28,6 @@ require 'currency/core_extensions'
|
|
35
28
|
# cad = usd.convert(:CAD)
|
36
29
|
# puts "cad = #{cad.format}"
|
37
30
|
#
|
38
|
-
# The example above uses Currency::Exchange::Xe automatically get exchange rates from http://xe.com/.
|
39
|
-
#
|
40
31
|
# == ActiveRecord Suppport
|
41
32
|
#
|
42
33
|
# This package also contains ActiveRecord support for money values:
|
@@ -63,7 +54,25 @@ require 'currency/core_extensions'
|
|
63
54
|
#
|
64
55
|
# == Examples
|
65
56
|
#
|
66
|
-
#
|
67
|
-
# * The "test cases":http://rubyforge.org/cgi-bin/viewvc.cgi/currency/trunk/test/?root=currency
|
57
|
+
# See the examples/ and test/ directorys
|
68
58
|
#
|
59
|
+
# == Author
|
60
|
+
#
|
61
|
+
# Kurt Stephens http://kurtstephens.com
|
62
|
+
#
|
63
|
+
# == Support
|
64
|
+
#
|
65
|
+
# ruby-currency(at)umleta.com
|
66
|
+
#
|
67
|
+
|
68
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
69
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
70
|
+
|
71
|
+
require 'currency/exception'
|
72
|
+
require 'currency/money'
|
73
|
+
require 'currency/currency_factory'
|
74
|
+
require 'currency/currency'
|
75
|
+
require 'currency/money'
|
76
|
+
require 'currency/exchange'
|
77
|
+
require 'currency/core_extensions'
|
69
78
|
|
@@ -9,7 +9,53 @@ module Currency
|
|
9
9
|
base.extend(ClassMethods)
|
10
10
|
end
|
11
11
|
|
12
|
+
|
13
|
+
# == ActiveRecord Suppport
|
14
|
+
#
|
15
|
+
# This package also contains ActiveRecord support for money values:
|
16
|
+
#
|
17
|
+
# require 'currency'
|
18
|
+
# require 'currency/active_record'
|
19
|
+
#
|
20
|
+
# class Entry < ActiveRecord::Base
|
21
|
+
# money :amount
|
22
|
+
# end
|
23
|
+
#
|
12
24
|
module ClassMethods
|
25
|
+
|
26
|
+
# Defines a Money object attribute that is bound
|
27
|
+
# to a database column. The database column to store the
|
28
|
+
# Money value representation is assumed to be
|
29
|
+
# INTEGER.
|
30
|
+
#
|
31
|
+
# Options:
|
32
|
+
#
|
33
|
+
# :currency => :USD
|
34
|
+
#
|
35
|
+
# Defines the Currency to use for storing a normalized Money
|
36
|
+
# value. This allows SQL summary operations,
|
37
|
+
# like SUM(), MAX(), AVG(), etc., to produce meaningful results,
|
38
|
+
# regardless of the initial currency specified. If this
|
39
|
+
# option is used, subsequent reads will be in the specified
|
40
|
+
# normalization :currency. Defaults to :USD.
|
41
|
+
#
|
42
|
+
# :currency_field => undef
|
43
|
+
#
|
44
|
+
# Defines the name of the CHAR(3) column that is used to store and
|
45
|
+
# retrieve the Money's Currency code. If this option is used, each
|
46
|
+
# record may use a different Currency to store the result, such
|
47
|
+
# that SQL summary operations, like SUM(), MAX(), AVG(),
|
48
|
+
# may return meaningless results.
|
49
|
+
#
|
50
|
+
# :currency_preferred_field => undef
|
51
|
+
#
|
52
|
+
# Defines the name of a CHAR(3) column used to store and
|
53
|
+
# retrieve the Money's Currency code. This option can be used
|
54
|
+
# with normalize Money values to retrieve the Money value
|
55
|
+
# in its original Currency, while
|
56
|
+
# allowing SQL summary operations on a normalized Money value
|
57
|
+
# to still be valid.
|
58
|
+
#
|
13
59
|
def money(attr_name, *opts)
|
14
60
|
opts = Hash.[](*opts)
|
15
61
|
|
@@ -22,6 +68,12 @@ module Currency
|
|
22
68
|
currency = "read_attribute(:#{currency_field})"
|
23
69
|
end
|
24
70
|
|
71
|
+
currency_preferred_field = opts[:currency_preferred_field]
|
72
|
+
if currency_preferred_field
|
73
|
+
read_preferred_currency = "@#{attr_name} = @#{attr_name}.convert(read_attribute(:#{:currency_preferred_field}))"
|
74
|
+
write_preferred_currency = "write_attribute(:#{currency_preferred_field}, @#{attr_name}_money.currency.code)"
|
75
|
+
end
|
76
|
+
|
25
77
|
currency ||= ':USD'
|
26
78
|
|
27
79
|
validate = ''
|
@@ -33,7 +85,10 @@ def #{attr_name}
|
|
33
85
|
# $stderr.puts " \#{self.class.name}##{attr_name}"
|
34
86
|
unless @#{attr_name}
|
35
87
|
#{attr_name}_rep = read_attribute(:#{attr_name})
|
36
|
-
|
88
|
+
unless #{attr_name}_rep.nil?
|
89
|
+
@#{attr_name} = Money.new_rep(#{attr_name}_rep, #{currency})
|
90
|
+
#{read_preferred_currency}
|
91
|
+
end
|
37
92
|
end
|
38
93
|
@#{attr_name}
|
39
94
|
end
|
@@ -42,7 +97,9 @@ def #{attr_name}=(value)
|
|
42
97
|
;
|
43
98
|
elsif value.kind_of?(Integer) || value.kind_of?(String) || value.kind_of?(Float)
|
44
99
|
#{attr_name}_money = Money.new(value, #{currency})
|
100
|
+
#{write_preferred_currency}
|
45
101
|
elsif value.kind_of?(Money)
|
102
|
+
#{write_preferred_currency}
|
46
103
|
#{attr_name}_money = value.convert(#{currency})
|
47
104
|
else
|
48
105
|
throw "Bad money format \#{value.inspect}"
|
@@ -53,6 +110,7 @@ def #{attr_name}=(value)
|
|
53
110
|
value
|
54
111
|
end
|
55
112
|
def #{attr_name}_before_type_cast
|
113
|
+
# FIX ME, User cannot specify Currency
|
56
114
|
x = #{attr_name}
|
57
115
|
x &&= x.format(:no_symbol, :no_currency, :no_thousands)
|
58
116
|
x
|
@@ -1,21 +1,24 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
#
|
4
|
-
class Integer # Exact
|
1
|
+
# External representation mixin
|
2
|
+
class Integer
|
3
|
+
# Exact
|
5
4
|
def Money_rep(currency)
|
6
5
|
Integer(self * currency.scale)
|
7
6
|
end
|
8
7
|
end
|
9
8
|
|
10
9
|
|
11
|
-
|
10
|
+
# External representation mixin
|
11
|
+
class Float
|
12
|
+
# Inexact
|
12
13
|
def Money_rep(currency)
|
13
14
|
Integer(self * currency.scale)
|
14
15
|
end
|
15
16
|
end
|
16
17
|
|
17
18
|
|
18
|
-
|
19
|
+
# External representation mixin
|
20
|
+
class String
|
21
|
+
# Exact
|
19
22
|
def Money_rep(currency)
|
20
23
|
x = currency.parse(self, :currency => currency)
|
21
24
|
x.rep if x.kind_of?(Currency::Money)
|
data/lib/currency/currency.rb
CHANGED
@@ -1,49 +1,73 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#
|
3
|
+
# = Currency::Currency
|
4
|
+
#
|
5
|
+
# Represents a currency.
|
6
|
+
#
|
7
|
+
#
|
8
|
+
|
1
9
|
module Currency
|
2
10
|
#include Currency::Exceptions
|
3
11
|
|
4
12
|
class Currency
|
5
|
-
# Create a new currency
|
13
|
+
# Create a new currency.
|
14
|
+
# This should only be called from Currency::CurrencyFactory.
|
6
15
|
def initialize(code, symbol = nil, scale = 100)
|
7
16
|
self.code = code
|
8
17
|
self.symbol = symbol
|
9
18
|
self.scale = scale
|
10
19
|
end
|
11
20
|
|
21
|
+
# Returns the Currency object from the default CurrencyFactory
|
22
|
+
# by its 3-letter uppercase Symbol name, such as :USD, or :CAD.
|
12
23
|
def self.get(code)
|
13
24
|
CurrencyFactory.default.get_by_code(code)
|
14
25
|
end
|
15
26
|
|
27
|
+
# Internal method for converting currency codes to internal
|
28
|
+
# Symbol format.
|
16
29
|
def self.cast_code(x)
|
17
30
|
x = x.upcase.intern if x.kind_of?(String)
|
18
|
-
raise InvalidCurrencyCode.new(x) unless x.kind_of?(Symbol)
|
31
|
+
raise Exception::InvalidCurrencyCode.new(x) unless x.kind_of?(Symbol)
|
32
|
+
raise Exception::InvalidCurrencyCode.new(x) unless x.to_s.length == 3
|
19
33
|
x
|
20
34
|
end
|
21
35
|
|
36
|
+
# Returns the hash of the Currency's code.
|
22
37
|
def hash
|
23
38
|
@code.hash
|
24
39
|
end
|
25
40
|
|
41
|
+
# Returns true if the Currency's are equal.
|
26
42
|
def eql?(x)
|
27
43
|
self.class == x.class && @code == x.code
|
28
44
|
end
|
29
45
|
|
46
|
+
# Returns true if the Currency's are equal.
|
30
47
|
def ==(x)
|
31
48
|
self.class == x.class && @code == x.code
|
32
49
|
end
|
33
50
|
|
34
|
-
#
|
51
|
+
# Get the Currency's code.
|
35
52
|
def code
|
36
53
|
@code
|
37
54
|
end
|
55
|
+
|
56
|
+
|
57
|
+
# Client should never call this directly.
|
38
58
|
def code=(x)
|
39
59
|
x = self.class.cast_code(x) unless x.nil?
|
40
60
|
@code = x
|
41
61
|
#$stderr.puts "#{self}.code = #{@code}"; x
|
42
62
|
end
|
43
63
|
|
64
|
+
# Get the Currency's scale factor.
|
65
|
+
# E.g: the :USD scale factor is 100.
|
44
66
|
def scale
|
45
67
|
@scale
|
46
68
|
end
|
69
|
+
|
70
|
+
# Client should never call this directly.
|
47
71
|
def scale=(x)
|
48
72
|
@scale = x
|
49
73
|
return x if x.nil?
|
@@ -53,21 +77,32 @@ module Currency
|
|
53
77
|
x
|
54
78
|
end
|
55
79
|
|
80
|
+
# Get the Currency's scale factor.
|
81
|
+
# E.g: the :USD scale factor is 2, where 10 ^ 2 == 100.
|
56
82
|
def scale_exp
|
57
83
|
@scale_exp
|
58
84
|
end
|
59
85
|
|
86
|
+
# Get the Currency's symbol.
|
87
|
+
# E.g. :USD, :CAD, etc.
|
60
88
|
def symbol
|
61
89
|
@symbol
|
62
90
|
end
|
91
|
+
|
92
|
+
# Client should never call this directly.
|
63
93
|
def symbol=(x)
|
64
94
|
@symbol = x
|
65
95
|
end
|
66
96
|
|
67
|
-
# Parse a Money string
|
97
|
+
# Parse a Money string.
|
98
|
+
# Options:
|
99
|
+
# :currency => Currency object.
|
100
|
+
# Look for a matching currency code at the beginning or end of the string.
|
101
|
+
# If the currency does not match IncompatibleCurrency is raised.
|
102
|
+
#
|
68
103
|
def parse(str, *opt)
|
69
104
|
x = str
|
70
|
-
opt = Hash
|
105
|
+
opt = Hash[*opt]
|
71
106
|
|
72
107
|
md = nil # match data
|
73
108
|
|
@@ -79,7 +114,7 @@ module Currency
|
|
79
114
|
x = md[2]
|
80
115
|
if curr != self
|
81
116
|
if opt[:currency] && opt[:currency] != curr
|
82
|
-
raise IncompatibleCurrency.new("#{str} #{opt[:currency].code}")
|
117
|
+
raise Exception::IncompatibleCurrency.new("#{str} #{opt[:currency].code}")
|
83
118
|
end
|
84
119
|
return Money.new(x, curr);
|
85
120
|
end
|
@@ -89,7 +124,7 @@ module Currency
|
|
89
124
|
x = md[1]
|
90
125
|
if curr != self
|
91
126
|
if opt[:currency] && opt[:currency] != curr
|
92
|
-
raise IncompatibleCurrency.new("#{str} #{opt[:currency].code}")
|
127
|
+
raise Exception::IncompatibleCurrency.new("#{str} #{opt[:currency].code}")
|
93
128
|
end
|
94
129
|
return Money.new(x, curr);
|
95
130
|
end
|
@@ -136,7 +171,7 @@ module Currency
|
|
136
171
|
else
|
137
172
|
# $stderr.puts "'#{self}'.parse(#{str}) => ??? '#{x}'"
|
138
173
|
#x.to_f.Money_rep(self)
|
139
|
-
raise InvalidMoneyString.new("#{str} #{self}")
|
174
|
+
raise Exception::InvalidMoneyString.new("#{str} #{self}")
|
140
175
|
end
|
141
176
|
end
|
142
177
|
|
@@ -194,22 +229,22 @@ module Currency
|
|
194
229
|
# @code.to_s
|
195
230
|
#end
|
196
231
|
|
197
|
-
#
|
198
|
-
# Default currency
|
232
|
+
# Returns the default CurrencyFactory's currency.
|
199
233
|
def self.default
|
200
234
|
CurrencyFactory.default.currency
|
201
235
|
end
|
202
236
|
|
237
|
+
# Returns the USD Currency.
|
203
238
|
def self.USD
|
204
239
|
CurrencyFactory.default.USD
|
205
240
|
end
|
206
241
|
|
242
|
+
# Returns the CAD Currency.
|
207
243
|
def self.CAD
|
208
244
|
CurrencyFactory.default.CAD
|
209
245
|
end
|
210
|
-
end
|
211
|
-
|
212
246
|
|
213
|
-
#
|
214
|
-
|
247
|
+
end # class
|
248
|
+
|
249
|
+
end # module
|
215
250
|
|
data/lib/currency/exception.rb
CHANGED
@@ -1,15 +1,41 @@
|
|
1
1
|
module Currency
|
2
|
-
|
3
|
-
|
4
|
-
class InvalidMoneyString < Error
|
2
|
+
module Exception
|
5
3
|
end
|
4
|
+
end
|
6
5
|
|
7
|
-
|
8
|
-
|
6
|
+
# Base class for all Currency::Exception.
|
7
|
+
class Currency::Exception::Base < Exception
|
8
|
+
end
|
9
|
+
|
10
|
+
module Currency
|
11
|
+
module Exception
|
12
|
+
# Error during string parsing.
|
13
|
+
class InvalidMoneyString < Base
|
14
|
+
end
|
9
15
|
|
10
|
-
|
11
|
-
|
16
|
+
# Error in Currency code formeat.
|
17
|
+
class InvalidCurrencyCode < Base
|
18
|
+
end
|
19
|
+
|
20
|
+
# Error during conversion between currencies.
|
21
|
+
class IncompatibleCurrency < Base
|
22
|
+
end
|
23
|
+
|
24
|
+
# Error if an Exchange is not defined.
|
25
|
+
class UndefinedExchange < Base
|
26
|
+
end
|
27
|
+
|
28
|
+
# Error if a Currency is unknown.
|
29
|
+
class UnknownCurrency < Base
|
30
|
+
end
|
31
|
+
|
32
|
+
# Error if an Exchange cannot provide an Exchange::Rate.
|
33
|
+
class UnknownRate < Base
|
34
|
+
end
|
35
|
+
|
36
|
+
# Error if an Exchange::Rate is not valid.
|
37
|
+
class InvalidRate < Base
|
38
|
+
end
|
12
39
|
|
13
|
-
class UndefinedExchange < Error
|
14
40
|
end
|
15
41
|
end
|
data/lib/currency/exchange.rb
CHANGED
@@ -1,22 +1,44 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#
|
3
|
+
# = Currency::Exchange
|
4
|
+
#
|
5
|
+
# The Currency::Exchange package is responsible for
|
6
|
+
# the conversion between currencies.
|
7
|
+
#
|
8
|
+
|
1
9
|
module Currency
|
2
10
|
module Exchange
|
3
|
-
|
4
11
|
@@default = nil
|
12
|
+
@@current = nil
|
13
|
+
|
14
|
+
# Returns the default Currency::Exchange object.
|
15
|
+
#
|
16
|
+
# If one is not specfied an instance of Currency::Exchange::Base is
|
17
|
+
# created. Currency::Exchange::Base cannot service any
|
18
|
+
# conversion rate requests.
|
5
19
|
def self.default
|
6
20
|
@@default ||= Base.new
|
7
21
|
end
|
22
|
+
|
23
|
+
# Sets the default Currency::Exchange object.
|
8
24
|
def self.default=(x)
|
9
25
|
@@default = x
|
10
26
|
end
|
11
27
|
|
12
|
-
|
28
|
+
# Returns the current Currency::Exchange object used during
|
29
|
+
# explicit and implicit Money conversions.
|
30
|
+
#
|
31
|
+
# If #current= has not been called and #default= has not been called,
|
32
|
+
# then UndefinedExchange is raised.
|
13
33
|
def self.current
|
14
|
-
@@current || self.default || (raise UndefinedExchange
|
34
|
+
@@current || self.default || (raise Exception::UndefinedExchange.new("Currency::Exchange.current not defined"))
|
15
35
|
end
|
36
|
+
|
37
|
+
# Sets the current Currency::Exchange object used during
|
38
|
+
# explicit and implicit Money conversions.
|
16
39
|
def self.current=(x)
|
17
40
|
@@current = x
|
18
41
|
end
|
19
|
-
|
20
42
|
end
|
21
43
|
end
|
22
44
|
|
@@ -1,3 +1,14 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#
|
3
|
+
# = Currency::Exchange::Base
|
4
|
+
#
|
5
|
+
# The Currency::Exchange::Base class is the base class for
|
6
|
+
# currency exchange rate providers.
|
7
|
+
#
|
8
|
+
# Currency::Exchange::Base subclasses are Currency::Exchange::Rate
|
9
|
+
# factories.
|
10
|
+
#
|
11
|
+
|
1
12
|
module Currency
|
2
13
|
module Exchange
|
3
14
|
|
@@ -7,42 +18,54 @@ module Exchange
|
|
7
18
|
|
8
19
|
def initialize(*opt)
|
9
20
|
@name = nil
|
10
|
-
@
|
21
|
+
@rate = { }
|
11
22
|
end
|
12
23
|
|
24
|
+
# Converts Money m in Currency c1 to a new
|
25
|
+
# Money value in Currency c2.
|
13
26
|
def convert(m, c2, c1 = nil)
|
14
27
|
c1 = m.currency if c1 == nil
|
15
28
|
if ( c1 == c2 )
|
16
29
|
m
|
17
30
|
else
|
18
|
-
Money.new(
|
31
|
+
Money.new(rate(c1, c2).convert(m, c1), c2)
|
19
32
|
end
|
20
33
|
end
|
21
34
|
|
22
|
-
|
23
|
-
|
35
|
+
# Flush all cached Rate.
|
36
|
+
def clear_rates
|
37
|
+
@rate.empty!
|
24
38
|
end
|
25
39
|
|
26
|
-
|
27
|
-
|
28
|
-
@
|
40
|
+
# Flush any cached Rate between Currency c1 and c2.
|
41
|
+
def clear_rate(c1, c2, recip = true)
|
42
|
+
@rate[c1.code.to_s + c2.code.to_s] = nil
|
43
|
+
@rate[c2.code.to_s + c1.code.to_s] = nil if recip
|
29
44
|
end
|
30
45
|
|
31
|
-
|
32
|
-
|
33
|
-
|
46
|
+
# Returns the Rate between Currency c1 and c2.
|
47
|
+
#
|
48
|
+
# This will call #get_rate(c1, c2) if the
|
49
|
+
# Rate has not already been cached.
|
50
|
+
def rate(c1, c2)
|
51
|
+
(@rate[c1.code.to_s + c2.code.to_s] ||= get_rate(c1, c2)) ||
|
52
|
+
(@rate[c2.code.to_s + c1.code.to_s] ||= get_rate(c2, c1))
|
34
53
|
end
|
35
54
|
|
36
55
|
|
37
|
-
|
38
|
-
|
56
|
+
# Determines and creates the Rate between Currency c1 and c2.
|
57
|
+
#
|
58
|
+
# Subclasses are required to implement this method.
|
59
|
+
def get_rate(c1, c2)
|
60
|
+
raise Exception::UnknownRate.new("Subclass responsibility: get_rate")
|
39
61
|
end
|
40
62
|
|
63
|
+
# Returns a simple string rep of an Exchange object.
|
41
64
|
def to_s
|
42
65
|
"#<#{self.class.name} #{self.name && self.name.inspect}>"
|
43
66
|
end
|
44
67
|
|
45
|
-
end
|
68
|
+
end # class
|
46
69
|
|
47
70
|
|
48
71
|
end # module
|
@@ -1,10 +1,13 @@
|
|
1
|
+
# This class is a test Exchange.
|
2
|
+
# It can convert only between USD and CAD.
|
3
|
+
|
1
4
|
module Currency
|
2
5
|
module Exchange
|
3
6
|
|
4
|
-
# This class is a text Exchange.
|
5
|
-
# It can convert only between USD and CAD
|
6
7
|
class Test < Base
|
7
8
|
@@instance = nil
|
9
|
+
|
10
|
+
# Returns a singleton instance.
|
8
11
|
def self.instance(*opts)
|
9
12
|
@@instance ||= self.new(*opts)
|
10
13
|
end
|
@@ -13,10 +16,11 @@ module Exchange
|
|
13
16
|
super(*opts)
|
14
17
|
end
|
15
18
|
|
16
|
-
#
|
19
|
+
# Test rate from :USD to :CAD.
|
17
20
|
def self.USD_CAD; 1.1708; end
|
18
21
|
|
19
|
-
|
22
|
+
# Returns test Rate for USD and CAD pairs.
|
23
|
+
def get_rate(c1, c2)
|
20
24
|
# $stderr.puts "load_exchange_rate(#{c1}, #{c2})"
|
21
25
|
rate = 0.0
|
22
26
|
if ( c1.code == :USD && c2.code == :CAD )
|
@@ -24,11 +28,12 @@ module Exchange
|
|
24
28
|
end
|
25
29
|
rate > 0 ? Rate.new(c1, c2, rate, self) : nil
|
26
30
|
end
|
27
|
-
|
31
|
+
|
32
|
+
end # class
|
28
33
|
|
29
34
|
end # module
|
30
35
|
end # module
|
31
36
|
|
32
|
-
# Install as
|
37
|
+
# Install as default.
|
33
38
|
Currency::Exchange.default = Currency::Exchange::Test.instance
|
34
39
|
|
data/lib/currency/exchange/xe.rb
CHANGED
@@ -1,17 +1,24 @@
|
|
1
|
+
# Connects to http://xe.com and parses "XE.com Quick Cross Rates"
|
2
|
+
# from home page HTML.
|
3
|
+
|
1
4
|
require 'net/http'
|
2
5
|
require 'open-uri'
|
3
6
|
|
4
7
|
module Currency
|
5
8
|
module Exchange
|
6
|
-
# Represents connects to http://xe.com and groks "XE.com Quick Cross Rates"
|
7
9
|
|
8
10
|
class Xe < Base
|
9
11
|
@@instance = nil
|
12
|
+
|
13
|
+
# Returns a singleton instance.
|
10
14
|
def self.instance(*opts)
|
11
15
|
@@instance ||= self.new(*opts)
|
12
16
|
end
|
13
17
|
|
18
|
+
# Defaults to "http://xe.com/"
|
14
19
|
attr_accessor :uri
|
20
|
+
|
21
|
+
# This Exchange's name is the same as its #uri.
|
15
22
|
def name
|
16
23
|
uri
|
17
24
|
end
|
@@ -22,6 +29,10 @@ module Exchange
|
|
22
29
|
@rates = nil
|
23
30
|
end
|
24
31
|
|
32
|
+
# Returns a cached Hash of rates:
|
33
|
+
#
|
34
|
+
# xe.rates[:USD][:CAD] => 1.01
|
35
|
+
#
|
25
36
|
def rates
|
26
37
|
return @rates if @rates
|
27
38
|
|
@@ -34,6 +45,7 @@ module Exchange
|
|
34
45
|
@rates
|
35
46
|
end
|
36
47
|
|
48
|
+
# Returns the URI content.
|
37
49
|
def get_page
|
38
50
|
data = open(uri) { |data| data.read }
|
39
51
|
|
@@ -42,6 +54,8 @@ module Exchange
|
|
42
54
|
data
|
43
55
|
end
|
44
56
|
|
57
|
+
# Parses http://xe.com homepage HTML for
|
58
|
+
# quick rates of 10 currencies.
|
45
59
|
def parse_page_rates(data = nil)
|
46
60
|
data = get_page unless data
|
47
61
|
|
@@ -114,7 +128,9 @@ module Exchange
|
|
114
128
|
rate
|
115
129
|
end
|
116
130
|
|
117
|
-
|
131
|
+
# Loads cached rates from xe.com and creates Rate objects
|
132
|
+
# for 10 currencies.
|
133
|
+
def get_rate(c1, c2)
|
118
134
|
rates # Load rates
|
119
135
|
|
120
136
|
# $stderr.puts "load_exchange_rate(#{c1}, #{c2})"
|
@@ -134,12 +150,13 @@ module Exchange
|
|
134
150
|
|
135
151
|
rate > 0 ? Rate.new(c1, c2, rate, self, @rate_timestamp) : nil
|
136
152
|
end
|
137
|
-
|
153
|
+
|
154
|
+
end # class
|
138
155
|
|
139
156
|
end # module
|
140
157
|
end # module
|
141
158
|
|
142
159
|
|
143
|
-
# Install as
|
160
|
+
# Install as default.
|
144
161
|
Currency::Exchange.default = Currency::Exchange::Xe.instance
|
145
162
|
|
data/lib/currency/money.rb
CHANGED
@@ -1,17 +1,54 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#
|
3
|
+
# = Currency::Money
|
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
|
+
# TODO:
|
11
|
+
# * Need to store a time, so we can use historical FX rates.
|
12
|
+
#
|
13
|
+
|
1
14
|
module Currency
|
2
15
|
|
3
|
-
#
|
16
|
+
# Use this function instead of Money#new:
|
17
|
+
#
|
18
|
+
# Currency::Money("12.34", :CAD)
|
4
19
|
#
|
5
|
-
#
|
6
|
-
# historical FX rates to convert?
|
20
|
+
# not
|
7
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
|
+
|
8
29
|
class Money
|
9
30
|
include Comparable
|
10
31
|
|
11
|
-
|
12
|
-
# Construct
|
13
|
-
#
|
32
|
+
#
|
33
|
+
# Construct a Money value object
|
34
|
+
# from a pre-scaled external representation:
|
35
|
+
# where x is a Float, Integer, String, etc.
|
36
|
+
#
|
37
|
+
# If a currency is not specified, Currency#default is used.
|
38
|
+
#
|
39
|
+
# x.Money_rep(currency)
|
40
|
+
#
|
41
|
+
# is invoked to coerce x into a Money representation value.
|
42
|
+
#
|
43
|
+
# For example:
|
44
|
+
#
|
45
|
+
# 123.Money_rep(:USD) => 12300
|
46
|
+
#
|
47
|
+
# Because the USD Currency object has a #scale of 100
|
48
|
+
#
|
14
49
|
# See #Money_rep(currency) mixin.
|
50
|
+
# See Currency#Money() function.
|
51
|
+
#
|
15
52
|
def initialize(x, currency = nil)
|
16
53
|
# Xform currency
|
17
54
|
currency = Currency.default if currency.nil?
|
@@ -32,6 +69,7 @@ module Currency
|
|
32
69
|
def self.us_dollar(x)
|
33
70
|
self.new(x, :USD)
|
34
71
|
end
|
72
|
+
# Compatibility with Money package.
|
35
73
|
def cents
|
36
74
|
@rep
|
37
75
|
end
|
@@ -43,33 +81,42 @@ module Currency
|
|
43
81
|
x.set_rep(r)
|
44
82
|
x
|
45
83
|
end
|
84
|
+
# Construct from post-scaled internal representation
|
85
|
+
# using the same currency.
|
86
|
+
# x = Currency::Money.new("1.98", :USD)
|
87
|
+
# x.new_rep(100) => "$1.00 USD"
|
88
|
+
#
|
46
89
|
def new_rep(r)
|
47
90
|
x = self.class.new(0, @currency)
|
48
91
|
x.set_rep(r)
|
49
92
|
x
|
50
93
|
end
|
51
94
|
|
52
|
-
#
|
95
|
+
# Do not call this method directly
|
96
|
+
# CLIENTS SHOULD NEVER CALL set_rep DIRECTLY.
|
97
|
+
# You have been warned in ALL CAPS.
|
53
98
|
def set_rep(r)
|
54
99
|
r = r.to_i unless r.kind_of?(Integer)
|
55
100
|
@rep = r
|
56
101
|
end
|
57
102
|
|
103
|
+
# Returns the money representation for the requested currency.
|
58
104
|
def Money_rep(currency)
|
59
105
|
$stderr.puts "@currency != currency (#{@currency.inspect} != #{currency.inspect}" unless @currency == currency
|
60
106
|
@rep
|
61
107
|
end
|
62
108
|
|
109
|
+
# Returns the money representation (usually an Integer).
|
63
110
|
def rep
|
64
111
|
@rep
|
65
112
|
end
|
66
113
|
|
67
|
-
# Get the money's Currency
|
114
|
+
# Get the money's Currency.
|
68
115
|
def currency
|
69
116
|
@currency
|
70
117
|
end
|
71
118
|
|
72
|
-
# Convert Money to another Currency
|
119
|
+
# Convert Money to another Currency.
|
73
120
|
def convert(currency)
|
74
121
|
currency = Currency.default if currency.nil?
|
75
122
|
currency = Currency.get(currency) unless currency.kind_of?(Currency)
|
@@ -80,17 +127,24 @@ module Currency
|
|
80
127
|
end
|
81
128
|
end
|
82
129
|
|
83
|
-
#
|
130
|
+
# Hash for hash table: both value and currency.
|
131
|
+
# See #eql? below.
|
84
132
|
def hash
|
85
133
|
@rep.hash ^ @currency.hash
|
86
134
|
end
|
87
135
|
|
136
|
+
# True if money values have the same value and currency.
|
88
137
|
def eql?(x)
|
89
|
-
|
138
|
+
self.class == x.class &&
|
139
|
+
@rep == x.rep &&
|
140
|
+
@currency == x.currency
|
90
141
|
end
|
91
142
|
|
143
|
+
# True if money values have the same value and currency.
|
92
144
|
def ==(x)
|
93
|
-
|
145
|
+
self.class == x.class &&
|
146
|
+
@rep == x.rep &&
|
147
|
+
@currency == x.currency
|
94
148
|
end
|
95
149
|
|
96
150
|
def <=>(x)
|
@@ -104,70 +158,85 @@ module Currency
|
|
104
158
|
# Operations on Money values.
|
105
159
|
|
106
160
|
|
161
|
+
# - Money => Money
|
162
|
+
# Negates a Money value.
|
107
163
|
def -@
|
108
|
-
# - Money => Money
|
109
164
|
new_rep(- @rep)
|
110
165
|
end
|
111
166
|
|
167
|
+
# Money + (Number|Money) => Money
|
168
|
+
#
|
112
169
|
# Right side maybe coerced to Money.
|
113
170
|
def +(x)
|
114
|
-
# Money + (Number|Money) => Money
|
115
171
|
new_rep(@rep + x.Money_rep(@currency))
|
116
172
|
end
|
117
173
|
|
174
|
+
# Money - (Number|Money) => Money
|
175
|
+
#
|
118
176
|
# Right side maybe coerced to Money.
|
119
177
|
def -(x)
|
120
|
-
# Money - (Number|Money) => Money
|
121
178
|
new_rep(@rep - x.Money_rep(@currency))
|
122
179
|
end
|
123
180
|
|
124
|
-
#
|
181
|
+
# Money * Number => Money
|
182
|
+
#
|
183
|
+
# Right side must be number.
|
125
184
|
def *(x)
|
126
|
-
|
127
|
-
new_rep(@rep * x)
|
185
|
+
new_rep(@rep * x)
|
128
186
|
end
|
129
187
|
|
130
|
-
#
|
188
|
+
# Money / Money => Number (ratio)
|
189
|
+
# Money / Number => Money
|
190
|
+
#
|
191
|
+
# Right side must be a number or Money.
|
192
|
+
# Right side Integers are not coerced to Float before
|
193
|
+
# division.
|
131
194
|
def /(x)
|
132
195
|
if x.kind_of?(self.class)
|
133
|
-
# Money / Money => ratio
|
134
196
|
(@rep.to_f) / (x.Money_rep(@currency).to_f)
|
135
197
|
else
|
136
|
-
# Money / Number => Money
|
137
198
|
new_rep(@rep / x)
|
138
199
|
end
|
139
200
|
end
|
140
201
|
|
202
|
+
# Formats the Money value as a String.
|
141
203
|
def format(*opt)
|
142
204
|
@currency.format(self, *opt)
|
143
205
|
end
|
144
206
|
|
145
|
-
#
|
207
|
+
# Formats the Money value as a String.
|
146
208
|
def to_s(*opt)
|
147
209
|
@currency.format(self, *opt)
|
148
210
|
end
|
149
211
|
|
212
|
+
# Coerces the Money's value to a Float.
|
213
|
+
# May cause loss of precision.
|
150
214
|
def to_f
|
151
215
|
Float(@rep) / @currency.scale
|
152
216
|
end
|
153
217
|
|
218
|
+
# Coerces the Money's value to an Integer.
|
219
|
+
# May cause loss of precision.
|
154
220
|
def to_i
|
155
221
|
@rep / @currency.scale
|
156
222
|
end
|
157
223
|
|
224
|
+
# True if the Money's value is zero.
|
158
225
|
def zero?
|
159
226
|
@rep == 0
|
160
227
|
end
|
161
228
|
|
229
|
+
# True if the Money's value is greater than zero.
|
162
230
|
def positive?
|
163
231
|
@rep > 0
|
164
232
|
end
|
165
233
|
|
234
|
+
# True if the Money's value is less than zero.
|
166
235
|
def negative?
|
167
236
|
@rep < 0
|
168
237
|
end
|
169
238
|
|
170
|
-
#
|
239
|
+
# Returns the Money's value representation in another currency.
|
171
240
|
def Money_rep(currency)
|
172
241
|
# Attempt conversion?
|
173
242
|
if @currency != currency
|
@@ -178,6 +247,8 @@ module Currency
|
|
178
247
|
end
|
179
248
|
end
|
180
249
|
|
250
|
+
# Basic inspection, with symbol and currency code.
|
251
|
+
# The standard #inspect method is available as #inspect_deep.
|
181
252
|
def inspect(*opts)
|
182
253
|
self.format(:with_symbol, :with_currency).inspect
|
183
254
|
end
|
@@ -185,9 +256,9 @@ module Currency
|
|
185
256
|
# How to alias a method defined in an object superclass in a different class:
|
186
257
|
define_method(:inspect_deep, Object.instance_method(:inspect))
|
187
258
|
# How call a method defined in a superclass from a method with a different name:
|
188
|
-
#def inspect_deep(*opts)
|
189
|
-
#
|
190
|
-
#end
|
259
|
+
# def inspect_deep(*opts)
|
260
|
+
# self.class.superclass.instance_method(:inspect).bind(self).call
|
261
|
+
# end
|
191
262
|
|
192
263
|
end # class
|
193
264
|
|
data/test/money_test.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
|
1
|
+
|
2
2
|
#require File.dirname(__FILE__) + '/../test_helper'
|
3
3
|
|
4
4
|
require 'test/test_base'
|
5
|
-
require 'currency'
|
5
|
+
require 'currency'
|
6
6
|
|
7
7
|
module Currency
|
8
8
|
|
@@ -105,7 +105,7 @@ class MoneyTest < TestBase
|
|
105
105
|
end
|
106
106
|
|
107
107
|
def test_op
|
108
|
-
# Using default
|
108
|
+
# Using default get_rate
|
109
109
|
assert_not_nil usd = Money.new(123.45, :USD)
|
110
110
|
assert_not_nil cad = Money.new(123.45, :CAD)
|
111
111
|
|
@@ -160,6 +160,14 @@ class MoneyTest < TestBase
|
|
160
160
|
assert_equal_float Exchange::Test.USD_CAD, m, 0.0001
|
161
161
|
end
|
162
162
|
|
163
|
+
def test_invalid_currency_code
|
164
|
+
assert_raise Exception::InvalidCurrencyCode do
|
165
|
+
Money.new(123, :asdf)
|
166
|
+
end
|
167
|
+
assert_raise Exception::InvalidCurrencyCode do
|
168
|
+
Money.new(123, 5)
|
169
|
+
end
|
170
|
+
end
|
163
171
|
end
|
164
172
|
|
165
173
|
end # module
|
data/test/xe_test.rb
CHANGED
metadata
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
|
-
rubygems_version: 0.
|
2
|
+
rubygems_version: 0.9.0
|
3
3
|
specification_version: 1
|
4
4
|
name: currency
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.
|
7
|
-
date: 2006-10-
|
6
|
+
version: 0.2.0
|
7
|
+
date: 2006-10-31 00:00:00 -05:00
|
8
8
|
summary: Currency models currencies, monetary values, foreign exchanges.
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -26,6 +26,7 @@ required_ruby_version: !ruby/object:Gem::Version::Requirement
|
|
26
26
|
platform: ruby
|
27
27
|
signing_key:
|
28
28
|
cert_chain:
|
29
|
+
post_install_message:
|
29
30
|
authors:
|
30
31
|
- Kurt Stephens
|
31
32
|
files:
|