currency 0.1.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.
@@ -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
+