currency 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,70 @@
1
+ require 'active_record/base'
2
+ require File.join(File.dirname(__FILE__), '..', 'currency')
3
+
4
+ module Currency
5
+ module ActiveRecord
6
+ def self.append_features(base) # :nodoc:
7
+ # $stderr.puts " Currency::ActiveRecord#append_features(#{base})"
8
+ super
9
+ base.extend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ def money(attr_name, *opts)
14
+ opts = Hash.[](*opts)
15
+
16
+ attr_name = attr_name.to_s
17
+
18
+ currency = opts[:currency]
19
+
20
+ currency_field = opts[:currency_field]
21
+ if currency_field
22
+ currency = "read_attribute(:#{currency_field})"
23
+ end
24
+
25
+ currency ||= ':USD'
26
+
27
+ validate = ''
28
+ validate = "validates_numericality_of :#{attr_name}" unless opts[:allow_nil]
29
+
30
+ module_eval (x = <<-"end_eval"), __FILE__, __LINE__
31
+ #{validate}
32
+ def #{attr_name}
33
+ # $stderr.puts " \#{self.class.name}##{attr_name}"
34
+ unless @#{attr_name}
35
+ #{attr_name}_rep = read_attribute(:#{attr_name})
36
+ @#{attr_name} = Money.new_rep(#{attr_name}_rep, #{currency}) unless #{attr_name}_rep.nil?
37
+ end
38
+ @#{attr_name}
39
+ end
40
+ def #{attr_name}=(value)
41
+ if value.nil?
42
+ ;
43
+ elsif value.kind_of?(Integer) || value.kind_of?(String) || value.kind_of?(Float)
44
+ #{attr_name}_money = Money.new(value, #{currency})
45
+ elsif value.kind_of?(Money)
46
+ #{attr_name}_money = value.convert(#{currency})
47
+ else
48
+ throw "Bad money format \#{value.inspect}"
49
+ end
50
+ @#{attr_name} = #{attr_name}_money
51
+ #{currency_field ? 'write_attribute(:#{currency_field}, #{attr_name}_money.nil? ? nil : #{attr_name}_money.currency.name)' : ''}
52
+ write_attribute(:#{attr_name}, #{attr_name}_money.nil? ? nil : #{attr_name}_money.rep)
53
+ value
54
+ end
55
+ def #{attr_name}_before_type_cast
56
+ x = #{attr_name}
57
+ x &&= x.format(:no_symbol, :no_currency, :no_thousands)
58
+ x
59
+ end
60
+ end_eval
61
+ # $stderr.puts " CODE = #{x}"
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+
68
+ ActiveRecord::Base.class_eval do
69
+ include Currency::ActiveRecord
70
+ end
@@ -0,0 +1,25 @@
1
+ #
2
+ # External representation mixins
3
+ #
4
+ class Integer # Exact
5
+ def Money_rep(currency)
6
+ Integer(self * currency.scale)
7
+ end
8
+ end
9
+
10
+
11
+ class Float # Inexact
12
+ def Money_rep(currency)
13
+ Integer(self * currency.scale)
14
+ end
15
+ end
16
+
17
+
18
+ class String # Exact
19
+ def Money_rep(currency)
20
+ x = currency.parse(self, :currency => currency)
21
+ x.rep if x.kind_of?(Currency::Money)
22
+ x
23
+ end
24
+ end
25
+
@@ -0,0 +1,203 @@
1
+ module Currency
2
+ #include Currency::Exceptions
3
+
4
+ class Currency
5
+ # Create a new currency
6
+ def initialize(code, symbol = nil, scale = 100)
7
+ self.code= code
8
+ self.symbol= symbol
9
+ self.scale= scale
10
+ end
11
+
12
+ def self.get(code)
13
+ CurrencyFactory.default.get_by_code(code)
14
+ end
15
+
16
+ def self.cast_code(x)
17
+ x = x.upcase.intern if x.kind_of?(String)
18
+ raise InvalidCurrencyCode.new(x) unless x.kind_of?(Symbol)
19
+ x
20
+ end
21
+
22
+ # Accessors
23
+ def code
24
+ @code
25
+ end
26
+ def code=(x)
27
+ x = self.class.cast_code(x) unless x.nil?
28
+ @code = x
29
+ #$stderr.puts "#{self}.code = #{@code}"; x
30
+ end
31
+
32
+ def scale
33
+ @scale
34
+ end
35
+ def scale=(x)
36
+ @scale = x
37
+ return x if x.nil?
38
+ @scale_exp = Integer(Math.log10(@scale));
39
+ @format_right = - @scale_exp
40
+ @format_left = @format_right - 1
41
+ x
42
+ end
43
+
44
+ def scale_exp
45
+ @scale_exp
46
+ end
47
+
48
+ def symbol
49
+ @symbol
50
+ end
51
+ def symbol=(x)
52
+ @symbol = x
53
+ end
54
+
55
+ # Parse a Money string
56
+ def parse(str, *opt)
57
+ x = str
58
+ opt = Hash.[](*opt)
59
+
60
+ md = nil # match data
61
+
62
+ # $stderr.puts "'#{x}'.Money_rep(#{self})"
63
+
64
+ # Handle currency code at front of string.
65
+ if (md = /^([A-Z][A-Z][A-Z])\s*(.*)$/.match(x)) != nil
66
+ curr = CurrencyFactory.default.get_by_code(md[1])
67
+ x = md[2]
68
+ if curr != self
69
+ if opt[:currency] && opt[:currency] != curr
70
+ raise IncompatibleCurrency.new("#{str} #{opt[:currency].code}")
71
+ end
72
+ return Money.new(x, curr);
73
+ end
74
+ # Handle currency code at end of string.
75
+ elsif (md = /^(.*)\s*([A-Z][A-Z][A-Z])$/.match(x)) != nil
76
+ curr = CurrencyFactory.default.get_by_code(md[2])
77
+ x = md[1]
78
+ if curr != self
79
+ if opt[:currency] && opt[:currency] != curr
80
+ raise IncompatibleCurrency.new("#{str} #{opt[:currency].code}")
81
+ end
82
+ return Money.new(x, curr);
83
+ end
84
+ end
85
+
86
+ # Remove placeholders and symbol.
87
+ x = x.gsub(/[, ]/, '')
88
+ x = x.sub(@symbol, '') if @symbol
89
+
90
+ # Match: whole Currency value.
91
+ if x =~ /^[-+]?(\d+)\.?$/
92
+ # $stderr.puts "'#{self}'.parse(#{str}) => EXACT"
93
+ x.to_i.Money_rep(self)
94
+
95
+ # Match: fractional Currency value.
96
+ elsif (md = /^([-+]?)(\d*)\.(\d+)$/.match(x)) != nil
97
+ sign = md[1]
98
+ whole = md[2]
99
+ part = md[3]
100
+
101
+ # $stderr.puts "'#{self}'.parse(#{str}) => DECIMAL (#{sign} #{whole} . #{part})"
102
+
103
+ if part.length != self.scale
104
+
105
+ # Pad decimal places with additional '0'
106
+ while part.length < self.scale_exp
107
+ part << '0'
108
+ end
109
+
110
+ # Truncate to Currency's decimal size.
111
+ part = part[0..(self.scale_exp - 1)]
112
+
113
+ # $stderr.puts " => INEXACT DECIMAL '#{whole}'"
114
+ end
115
+
116
+ # Put the string back together:
117
+ # #{sign}#{whole}#{part}
118
+ whole = sign + whole + part
119
+ # $stderr.puts " => REP = #{whole}"
120
+
121
+ x = whole.to_i
122
+
123
+ x = Money.new_rep(x, opt[:currency])
124
+ else
125
+ # $stderr.puts "'#{self}'.parse(#{str}) => ??? '#{x}'"
126
+ #x.to_f.Money_rep(self)
127
+ raise InvalidMoneyString.new("#{str} #{self}")
128
+ end
129
+ end
130
+
131
+ # Format a Money object.
132
+ def format(m, *opt)
133
+ opt = opt.flatten
134
+
135
+ # Get scaled integer representation for this Currency.
136
+ x = m.Money_rep(self)
137
+
138
+ # Remove sign.
139
+ x = - x if ( neg = x < 0 )
140
+
141
+ # Convert to String.
142
+ x = x.to_s
143
+
144
+ # Keep prefixing "0" until filled to scale.
145
+ while ( x.length <= @scale_exp )
146
+ x = "0" + x
147
+ end
148
+
149
+ # Insert decimal place
150
+ whole = x[0..@format_left]
151
+ decimal = x[@format_right..-1]
152
+
153
+ # Do commas
154
+ x = whole
155
+ unless opt.include?(:no_thousands)
156
+ x.reverse!
157
+ x.gsub!(/(\d\d\d)/) {|y| y + ','}
158
+ x.sub!(/,$/,'')
159
+ x.reverse!
160
+ end
161
+
162
+ x << '.' + decimal unless opt.include?(:no_cents) || opt.include?(:no_decimal)
163
+
164
+ # Put sign back.
165
+ x = "-" + x if neg
166
+
167
+ # Add symbol?
168
+ x = @symbol + x unless opt.include?(:no_symbol)
169
+
170
+ # Suffix currency code.
171
+ if opt.include?(:with_currency)
172
+ x << ' '
173
+ x << '<span class="currency">' if opt.include?(:html)
174
+ x << @code.to_s
175
+ x << '</span>' if opt.include?(:html)
176
+ end
177
+
178
+ x
179
+ end
180
+
181
+ #def to_s
182
+ # @code.to_s
183
+ #end
184
+
185
+ # Convience
186
+ # Default currency
187
+ def self.default
188
+ CurrencyFactory.default.currency
189
+ end
190
+
191
+ def self.USD
192
+ CurrencyFactory.default.USD
193
+ end
194
+
195
+ def self.CAD
196
+ CurrencyFactory.default.CAD
197
+ end
198
+ end
199
+
200
+
201
+ # END MODULE
202
+ end
203
+
@@ -0,0 +1,50 @@
1
+ module Currency
2
+ # Represents a method of converting between two currencies
3
+ # TODO:
4
+ # Create an ExchangeRateLoader class.
5
+ # Create an ExchangeRateLoader subclass that interfaces to xe.com or other FX quote source.
6
+ class CurrencyExchange
7
+ @@default = nil
8
+ def self.default
9
+ @@default ||= self.new
10
+ end
11
+ def self.default=(x)
12
+ @@default = x
13
+ end
14
+
15
+ def initialize(*opt)
16
+ @exchange_rate = { }
17
+ end
18
+
19
+ def convert(m, c2, c1 = nil)
20
+ c1 = m.currency if c1 == nil
21
+ if ( c1 == c2 )
22
+ m
23
+ else
24
+ Money.new(exchange_rate(c1, c2).convert(m, c1), c2)
25
+ end
26
+ end
27
+
28
+ def clear_exchange_rates
29
+ @exchange_rate.empty!
30
+ end
31
+
32
+ def clear_exchange_rate(c1, c2, recip = true)
33
+ @exchange_rate[c1.code.to_s + c2.code.to_s] = nil
34
+ @exchange_rate[c2.code.to_s + c1.code.to_s] = nil if recip
35
+ end
36
+
37
+ def exchange_rate(c1, c2)
38
+ (@exchange_rate[c1.code.to_s + c2.code.to_s] ||= load_exchange_rate(c1, c2)) ||
39
+ (@exchange_rate[c2.code.to_s + c1.code.to_s] ||= load_exchange_rate(c2, c1))
40
+ end
41
+
42
+
43
+ def load_exchange_rate(c1, c2)
44
+ raise "Subclass responsibility: load_exchange_rate"
45
+ end
46
+ end
47
+
48
+ # END MODULE
49
+ end
50
+
@@ -0,0 +1,27 @@
1
+ module Currency
2
+ # This can convert only between USD and CAD
3
+ class CurrencyExchangeTest < CurrencyExchange
4
+ @@instance = nil
5
+ def self.instance(*opts)
6
+ @@instance ||= self.new(*opts)
7
+ end
8
+
9
+ # Sample constant.
10
+ def self.USD_CAD; 1.1708; end
11
+
12
+ def load_exchange_rate(c1, c2)
13
+ # $stderr.puts "load_exchange_rate(#{c1}, #{c2})"
14
+ rate = 0.0
15
+ if ( c1.code == :USD && c2.code == :CAD )
16
+ rate = self.class.USD_CAD
17
+ end
18
+ rate > 0 ? ExchangeRate.new(c1, c2, rate, self.class.name) : nil
19
+ end
20
+ end
21
+
22
+ # END MODULE
23
+ end
24
+
25
+ # Install as current
26
+ Currency::CurrencyExchange.default = Currency::CurrencyExchangeTest.instance
27
+
@@ -0,0 +1,140 @@
1
+ require 'net/http'
2
+ require 'open-uri'
3
+
4
+ module Currency
5
+ # Represents connects to http://xe.com and groks "XE.com Quick Cross Rates"
6
+
7
+ class CurrencyExchangeXe < CurrencyExchange
8
+ @@instance = nil
9
+ def self.instance(*opts)
10
+ @@instance ||= self.new(*opts)
11
+ end
12
+
13
+ attr_accessor :uri
14
+
15
+ def initialize(*opt)
16
+ super(*opt)
17
+ self.uri = 'http://xe.com/'
18
+ @rates = nil
19
+ end
20
+
21
+ def rates
22
+ return @rates if @rates
23
+
24
+ @rates = parse_page_rates
25
+ return @rates unless @rates
26
+
27
+ @rates_usd_cur = @rates[:USD]
28
+ @rate_timestamp = Time.now
29
+
30
+ @rates
31
+ end
32
+
33
+ def get_page
34
+ data = open(uri) { |data| data.read }
35
+
36
+ data = data.split(/[\r\n]/)
37
+
38
+ data
39
+ end
40
+
41
+ def parse_page_rates(data = nil)
42
+ data = get_page unless data
43
+
44
+ # Chomp after
45
+ until data.empty?
46
+ line = data.pop
47
+ break if line =~ /Need More currencies\?/
48
+ end
49
+
50
+ # Chomp before
51
+ until data.empty?
52
+ line = data.shift
53
+ break if line =~ /XE.com Quick Cross Rates/
54
+ end
55
+
56
+ until data.empty?
57
+ line = data.shift
58
+ break if line =~ /Confused about how to use the rates/i
59
+ end
60
+
61
+ until data.empty?
62
+ line = data.shift
63
+ break if line =~ /^\s*<\/tr>/i
64
+ end
65
+ # $stderr.puts "#{data[0..4].inspect}"
66
+
67
+ # Read first table row to get position for each currency
68
+ currency = [ ]
69
+ until data.empty?
70
+ line = data.shift
71
+ break if line =~ /^\s*<\/tr>/i
72
+ if md = /<td><IMG .+ ALT="([A-Z][A-Z][A-Z])"/i.match(line) #"
73
+ cur = md[1].intern
74
+ cur_i = currency.size
75
+ currency.push(cur)
76
+ # $stderr.puts "Found currency header: #{cur.inspect} at #{cur_i}"
77
+ end
78
+ end
79
+
80
+ # $stderr.puts "#{data[0..4].inspect}"
81
+
82
+ # Skip blank <tr>
83
+ until data.empty?
84
+ line = data.shift
85
+ break if line =~ /^\s*<td>.+1.+USD.+=/
86
+ end
87
+
88
+ until data.empty?
89
+ line = data.shift
90
+ break if line =~ /^\s*<\/tr>/i
91
+ end
92
+
93
+ # $stderr.puts "#{data[0..4].inspect}"
94
+
95
+ # Read first row of 1 USD = ...
96
+
97
+ rate = { }
98
+ cur_i = -1
99
+ until data.empty?
100
+ line = data.shift
101
+ break if cur_i < 0 && line =~ /^\s*<\/tr>/i
102
+ if md = /<td>\s+(\d+\.\d+)\s+<\/td>/.match(line)
103
+ usd_to_cur = md[1].to_f
104
+ cur_i = cur_i + 1
105
+ cur = currency[cur_i]
106
+ (rate[:USD] ||= {})[cur] = usd_to_cur
107
+ end
108
+ end
109
+
110
+ rate
111
+ end
112
+
113
+ def load_exchange_rate(c1, c2)
114
+ rates # Load rates
115
+
116
+ # $stderr.puts "load_exchange_rate(#{c1}, #{c2})"
117
+ rate = 0.0
118
+ r1 = nil
119
+ r2 = nil
120
+
121
+ if ( c1.code == :USD && (r2 = @rates_usd_cur[c2.code]) )
122
+ rate = r2
123
+ elsif ( c2.code == :USD && (r1 = @rates_usd_cur[c2.code]) )
124
+ rate = 1.0 / r1
125
+ elsif ( (r1 = @rates_usd_cur[c1.code]) && (r2 = @rates_usd_cur[c2.code]) )
126
+ rate = r2 / r1
127
+ end
128
+
129
+ # $stderr.puts "XE Rate: #{c1.code} / #{c2.code} = #{rate}"
130
+
131
+ rate > 0 ? ExchangeRate.new(c1, c2, rate, self.class.name, @rate_timestamp) : nil
132
+ end
133
+ end
134
+
135
+ # END MODULE
136
+ end
137
+
138
+ # Install as current
139
+ Currency::CurrencyExchange.default = Currency::CurrencyExchangeXe.instance
140
+