currency 0.1.2 → 0.2.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.
- 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:
|