mumboe-currency 0.5

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.
Files changed (37) hide show
  1. data/bin/currency_historical_rate_load +105 -0
  2. data/examples/ex1.rb +13 -0
  3. data/examples/xe1.rb +20 -0
  4. data/lib/currency/active_record.rb +265 -0
  5. data/lib/currency/config.rb +91 -0
  6. data/lib/currency/core_extensions.rb +41 -0
  7. data/lib/currency/currency/factory.rb +228 -0
  8. data/lib/currency/currency.rb +175 -0
  9. data/lib/currency/currency_version.rb +6 -0
  10. data/lib/currency/exception.rb +119 -0
  11. data/lib/currency/exchange/rate/deriver.rb +157 -0
  12. data/lib/currency/exchange/rate/source/base.rb +166 -0
  13. data/lib/currency/exchange/rate/source/failover.rb +63 -0
  14. data/lib/currency/exchange/rate/source/federal_reserve.rb +160 -0
  15. data/lib/currency/exchange/rate/source/historical/rate.rb +184 -0
  16. data/lib/currency/exchange/rate/source/historical/rate_loader.rb +186 -0
  17. data/lib/currency/exchange/rate/source/historical/writer.rb +220 -0
  18. data/lib/currency/exchange/rate/source/historical.rb +79 -0
  19. data/lib/currency/exchange/rate/source/new_york_fed.rb +127 -0
  20. data/lib/currency/exchange/rate/source/provider.rb +120 -0
  21. data/lib/currency/exchange/rate/source/test.rb +50 -0
  22. data/lib/currency/exchange/rate/source/the_financials.rb +191 -0
  23. data/lib/currency/exchange/rate/source/timed_cache.rb +198 -0
  24. data/lib/currency/exchange/rate/source/xe.rb +165 -0
  25. data/lib/currency/exchange/rate/source.rb +89 -0
  26. data/lib/currency/exchange/rate.rb +214 -0
  27. data/lib/currency/exchange/time_quantitizer.rb +111 -0
  28. data/lib/currency/exchange.rb +50 -0
  29. data/lib/currency/formatter.rb +290 -0
  30. data/lib/currency/macro.rb +321 -0
  31. data/lib/currency/money.rb +295 -0
  32. data/lib/currency/money_helper.rb +13 -0
  33. data/lib/currency/parser.rb +151 -0
  34. data/lib/currency.rb +143 -0
  35. data/test/string_test.rb +54 -0
  36. data/test/test_base.rb +44 -0
  37. metadata +90 -0
@@ -0,0 +1,89 @@
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ require 'currency/exchange/rate'
5
+
6
+ #
7
+ # The Currency::Exchange::Rate::Source package is responsible for
8
+ # providing rates between currencies at a given time.
9
+ #
10
+ # It is not responsible for purchasing or selling actual money.
11
+ # See Currency::Exchange.
12
+ #
13
+ # Currency::Exchange::Rate::Source::Provider subclasses are true rate data
14
+ # providers. See the #load_rates method. They provide groups of rates
15
+ # at a given time.
16
+ #
17
+ # Other Currency::Exchange::Rate::Source::Base subclasses
18
+ # are chained to provide additional rate source behavior,
19
+ # such as caching and derived rates. They provide individual rates between
20
+ # currencies at a given time. See the #rate method. An application
21
+ # will interface directly to a Currency::Exchange::Rate::Source::Base.
22
+ # A rate aggregator like Currency::Exchange::Rate::Historical::Writer will
23
+ # interface directly to a Currency::Exchange::Rate::Source::Provider.
24
+ #
25
+ # == IMPORTANT
26
+ #
27
+ # Rates sources should *never* install themselves
28
+ # as a Currency::Exchange::Rate::Source.current or
29
+ # Currency::Exchange::Rate::Source.default. The application itself is
30
+ # responsible setting up the default rate source.
31
+ # The old auto-installation behavior of rate sources,
32
+ # like Currency::Exchange::Xe, is no longer supported.
33
+ #
34
+ # == Initialization of Rate Sources
35
+ #
36
+ # A typical application will use the following rate source chain:
37
+ #
38
+ # * Currency::Exchange::Rate::Source::TimedCache
39
+ # * Currency::Exchange::Rate::Deriver
40
+ # * a Currency::Exchange::Rate::Source::Provider subclass, like Currency::Exchange::Rate::Source::Xe.
41
+ #
42
+ # Somewhere at initialization of application:
43
+ #
44
+ # require 'currency'
45
+ # require 'currency/exchange/rate/deriver'
46
+ # require 'currency/exchange/rate/source/xe'
47
+ # require 'currency/exchange/rate/source/timed_cache'
48
+ #
49
+ # provider = Currency::Exchange::Rate::Source::Xe.new
50
+ # deriver = Currency::Exchange::Rate::Deriver.new(:source => provider)
51
+ # cache = Currency::Exchange::Rate::Source::TimedCache.new(:source => deriver)
52
+ # Currency::Exchange::Rate::Source.default = cache
53
+ #
54
+ module Currency::Exchange::Rate::Source
55
+
56
+ @@default = nil
57
+ @@current = nil
58
+
59
+ # Returns the default Currency::Exchange::Rate::Source::Base object.
60
+ #
61
+ # If one is not specfied an instance of Currency::Exchange::Rate::Source::Base is
62
+ # created. Currency::Exchange::Rate::Source::Base cannot service any
63
+ # conversion rate requests.
64
+ def self.default
65
+ @@default ||= Base.new
66
+ end
67
+
68
+ # Sets the default Currency::Exchange object.
69
+ def self.default=(x)
70
+ @@default = x
71
+ end
72
+
73
+ # Returns the current Currency::Exchange object used during
74
+ # explicit and implicit Money conversions.
75
+ #
76
+ # If #current= has not been called and #default= has not been called,
77
+ # then UndefinedExchange is raised.
78
+ def self.current
79
+ @@current || self.default || (raise ::Currency::Exception::UndefinedExchange, "Currency::Exchange.current not defined")
80
+ end
81
+
82
+ # Sets the current Currency::Exchange object used during
83
+ # explicit and implicit Money conversions.
84
+ def self.current=(x)
85
+ @@current = x
86
+ end
87
+ end
88
+
89
+ require 'currency/exchange/rate/source/base'
@@ -0,0 +1,214 @@
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ require 'currency/exchange'
5
+
6
+ # Represents a convertion rate between two currencies at a given period of time.
7
+ class Currency::Exchange::Rate
8
+ # The first Currency.
9
+ attr_reader :c1
10
+
11
+ # The second Currency.
12
+ attr_reader :c2
13
+
14
+ # The rate between c1 and c2.
15
+ #
16
+ # To convert between m1 in c1 and m2 in c2:
17
+ #
18
+ # m2 = m1 * rate.
19
+ attr_reader :rate
20
+
21
+ # The source of the rate.
22
+ attr_reader :source
23
+
24
+ # The Time of the rate.
25
+ attr_reader :date
26
+
27
+ # If the rate is derived from other rates, this describes from where it was derived.
28
+ attr_reader :derived
29
+
30
+ # Average rate over rate samples in a time range.
31
+ attr_reader :rate_avg
32
+
33
+ # Number of rate samples used to calcuate _rate_avg_.
34
+ attr_reader :rate_samples
35
+
36
+ # The rate low between date_0 and date_1.
37
+ attr_reader :rate_lo
38
+
39
+ # The rate high between date_0 and date_1.
40
+ attr_reader :rate_hi
41
+
42
+ # The rate at date_0.
43
+ attr_reader :rate_date_0
44
+
45
+ # The rate at date_1.
46
+ attr_reader :rate_date_1
47
+
48
+ # The lowest date of sampled rates.
49
+ attr_reader :date_0
50
+
51
+ # The highest date of sampled rates.
52
+ # This is non-inclusive during searches to allow seamless tileings of
53
+ # time with rate buckets.
54
+ attr_reader :date_1
55
+
56
+ def initialize(c1, c2, c1_to_c2_rate, source = "UNKNOWN", date = nil, derived = nil, reciprocal = nil, opts = nil)
57
+ @c1 = c1
58
+ @c2 = c2
59
+ @rate = c1_to_c2_rate
60
+ raise ::Currency::Exception::InvalidRate,
61
+ [
62
+ "rate is not positive",
63
+ :rate, @rate,
64
+ :c1, c1,
65
+ :c2, c2,
66
+ ] unless @rate && @rate >= 0.0
67
+ @source = source
68
+ @date = date
69
+ @derived = derived
70
+ @reciprocal = reciprocal
71
+
72
+ #
73
+ @rate_avg =
74
+ @rate_samples =
75
+ @rate_lo =
76
+ @rate_hi =
77
+ @rate_date_0 =
78
+ @rate_date_1 =
79
+ @date_0 =
80
+ @date_1 =
81
+ nil
82
+
83
+ if opts
84
+ opts.each_pair do | k, v |
85
+ self.instance_variable_set("@#{k}", v)
86
+ end
87
+ end
88
+ end
89
+
90
+
91
+ # Returns a cached reciprocal Rate object from c2 to c1.
92
+ def reciprocal
93
+ @reciprocal ||= @rate == 1.0 ? self :
94
+ self.class.new(@c2, @c1,
95
+ 1.0 / @rate,
96
+ @source,
97
+ @date,
98
+ "reciprocal(#{derived || "#{c1.code}#{c2.code}"})", self,
99
+ {
100
+ :rate_avg => @rate_avg && 1.0 / @rate_avg,
101
+ :rate_samples => @rate_samples,
102
+ :rate_lo => @rate_lo && 1.0 / @rate_lo,
103
+ :rate_hi => @rate_hi && 1.0 / @rate_hi,
104
+ :rate_date_0 => @rate_date_0 && 1.0 / @rate_date_0,
105
+ :rate_date_1 => @rate_date_1 && 1.0 / @rate_date_1,
106
+ :date_0 => @date_0,
107
+ :date_1 => @date_1,
108
+ }
109
+ )
110
+ end
111
+
112
+
113
+ # Converts from _m_ in Currency _c1_ to the opposite currency.
114
+ def convert(m, c1)
115
+ m = m.to_f
116
+ if @c1 == c1
117
+ # $stderr.puts "Converting #{@c1} #{m} to #{@c2} #{m * @rate} using #{@rate}"
118
+ m * @rate
119
+ else
120
+ # $stderr.puts "Converting #{@c2} #{m} to #{@c1} #{m / @rate} using #{1.0 / @rate}; recip"
121
+ m / @rate
122
+ end
123
+ end
124
+
125
+
126
+ # Collect rate samples into rate_avg, rate_hi, rate_lo, rate_date_0, rate_date_1, date_0, date_1.
127
+ def collect_rates(rates)
128
+ rates = [ rates ] unless rates.kind_of?(Enumerable)
129
+ rates.each do | r |
130
+ collect_rate(r)
131
+ end
132
+ self
133
+ end
134
+
135
+ # Collect rates samples in to this Rate.
136
+ def collect_rate(rate)
137
+ # Initial.
138
+ @rate_samples ||= 0
139
+ @rate_sum ||= 0
140
+ @source ||= rate.source
141
+ @c1 ||= rate.c1
142
+ @c2 ||= rate.c2
143
+
144
+ # Reciprocal?
145
+ if @c1 == rate.c2 && @c2 == rate.c1
146
+ collect_rate(rate.reciprocal)
147
+ elsif ! (@c1 == rate.c1 && @c2 == rate.c2)
148
+ raise ::Currency::Exception::InvalidRate,
149
+ [
150
+ "Cannot collect rates between different currency pairs",
151
+ :rate1, self,
152
+ :rate2, rate,
153
+ ]
154
+ else
155
+ # Multisource?
156
+ @source = "<<multiple-sources>>" unless @source == rate.source
157
+
158
+ # Calculate rate average.
159
+ @rate_samples += 1
160
+ @rate_sum += rate.rate || (rate.rate_lo + rate.rate_hi) * 0.5
161
+ @rate_avg = @rate_sum / @rate_samples
162
+
163
+ # Calculate rates ranges.
164
+ r = rate.rate_lo || rate.rate
165
+ unless @rate_lo && @rate_lo < r
166
+ @rate_lo = r
167
+ end
168
+ r = rate.rate_hi || rate.rate
169
+ unless @rate_hi && @rate_hi > r
170
+ @rate_hi = r
171
+ end
172
+
173
+ # Calculate rates on date range boundaries
174
+ r = rate.rate_date_0 || rate.rate
175
+ d = rate.date_0 || rate.date
176
+ unless @date_0 && @date_0 < d
177
+ @date_0 = d
178
+ @rate_date_0 = r
179
+ end
180
+
181
+ r = rate.rate_date_1 || rate.rate
182
+ d = rate.date_1 || rate.date
183
+ unless @date_1 && @date_1 > d
184
+ @date_1 = d
185
+ @rate_date_1 = r
186
+ end
187
+
188
+ @date ||= rate.date || rate.date0 || rate.date1
189
+ end
190
+ self
191
+ end
192
+
193
+
194
+ def to_s(extended = false)
195
+ extended = "#{date_0} #{rate_date_0} |< #{rate_lo} #{rate} #{rate_hi} >| #{rate_date_1} #{date_1}" if extended
196
+ extended ||= ''
197
+ "#<#{self.class.name} #{c1.code} #{c2.code} #{rate} #{source.inspect} #{date ? date.strftime('%Y/%m/%d-%H:%M:%S') : 'nil'} #{derived && derived.inspect} #{extended}>"
198
+ end
199
+
200
+ def inspect; to_s; end
201
+
202
+ end # class
203
+
204
+
205
+ class Currency::Exchange::Rate::Writable < Currency::Exchange::Rate
206
+ attr_writer :source
207
+ attr_writer :derived
208
+ attr_writer :rate
209
+ def initialize(c1 = nil, c2 = nil, rate = nil, *opts)
210
+ super(c1, c2, 0.0, *opts)
211
+ @rate = rate
212
+ end
213
+ end # class
214
+
@@ -0,0 +1,111 @@
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ # = Currency::Exchange::TimeQuantitizer
5
+ #
6
+ # The Currency::Exchange::TimeQuantitizer quantitizes time values
7
+ # such that money values and rates at a given time
8
+ # can be turned into a hash key, depending
9
+ # on the rate source's temporal accuracy.
10
+ #
11
+ class Currency::Exchange::TimeQuantitizer
12
+
13
+ def self.current; @current ||= self.new; end
14
+ def self.current=(x); @current = x; end
15
+
16
+ # Time quantitization size.
17
+ # Defaults to 1 day.
18
+ attr_accessor :time_quant_size
19
+
20
+ # Time quantization offset in seconds.
21
+ # This is applied to epoch time before quantization.
22
+ # If nil, uses Time#utc_offset.
23
+ # Defaults to nil.
24
+ attr_accessor :time_quant_offset
25
+
26
+ def initialize(*opt)
27
+ @time_quant_size ||= 60 * 60 * 24
28
+ @time_quant_offset ||= nil
29
+ opt = Hash[*opt]
30
+ opt.each_pair{|k,v| self.send("#{k}=", v)}
31
+ end
32
+
33
+
34
+ # Normalizes time to a quantitized value.
35
+ # For example: a time_quant_size of 60 * 60 * 24 will
36
+ # truncate a rate time to a particular day.
37
+ #
38
+ # Subclasses can override this method.
39
+ def quantitize_time(time)
40
+ # If nil, then nil.
41
+ return time unless time
42
+
43
+ # Get bucket parameters.
44
+ was_utc = time.utc?
45
+ quant_offset = time_quant_offset
46
+ quant_offset ||= time.utc_offset
47
+ # $stderr.puts "quant_offset = #{quant_offset}"
48
+ quant_size = time_quant_size.to_i
49
+
50
+ # Get offset from epoch.
51
+ time = time.tv_sec
52
+
53
+ # Remove offset (timezone)
54
+ time += quant_offset
55
+
56
+ # Truncate to quantitize size.
57
+ time = (time.to_i / quant_size) * quant_size
58
+
59
+ # Add offset (timezone)
60
+ time -= quant_offset
61
+
62
+ # Convert back to Time object.
63
+ time = Time.at(time)
64
+
65
+ # Quant to day?
66
+ # NOTE: is this due to a Ruby bug, or
67
+ # some wierd UTC time-flow issue, like leap-seconds.
68
+ if quant_size == 60 * 60 * 24
69
+ time = time + 60 * 60
70
+ if was_utc
71
+ time = time.getutc
72
+ time = Time.utc(time.year, time.month, time.day, 0, 0, 0, 0)
73
+ else
74
+ time = Time.local(time.year, time.month, time.day, 0, 0, 0, 0)
75
+ end
76
+ end
77
+
78
+ # Convert back to UTC?
79
+ time = time.getutc if was_utc
80
+
81
+ time
82
+ end
83
+
84
+ # Returns a Range of Time such that:
85
+ #
86
+ # range.include?(time)
87
+ # ! range.include?(time + time_quant_size)
88
+ # ! range.include?(time - time_quant_size)
89
+ # range.exclude_end?
90
+ #
91
+ # The range.max is end-exclusive to avoid precision issues:
92
+ #
93
+ # t = Time.now
94
+ # => Thu Feb 15 15:32:34 EST 2007
95
+ # x.quantitize_time_range(t)
96
+ # => Thu Feb 15 00:00:00 EST 2007...Fri Feb 16 00:00:00 EST 2007
97
+ #
98
+ def quantitize_time_range(time)
99
+ time_0 = quantitize_time(time)
100
+ time_1 = time_0 + time_quant_size.to_i
101
+ time_0 ... time_1
102
+ end
103
+
104
+ # Returns a simple string rep.
105
+ def to_s
106
+ "#<#{self.class.name} #{quant_offset} #{quant_size}>"
107
+ end
108
+
109
+ end # class
110
+
111
+
@@ -0,0 +1,50 @@
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ require 'currency'
5
+
6
+ # The Currency::Exchange package is responsible for
7
+ # the buying and selling of currencies.
8
+ #
9
+ # This feature is currently unimplemented.
10
+ #
11
+ # Exchange rate sources are configured via Currency::Exchange::Rate::Source.
12
+ #
13
+ module Currency::Exchange
14
+ @@default = nil
15
+ @@current = nil
16
+
17
+ # Returns the default Currency::Exchange object.
18
+ #
19
+ # If one is not specfied an instance of Currency::Exchange::Base is
20
+ # created. Currency::Exchange::Base cannot service any
21
+ # conversion rate requests.
22
+ def self.default
23
+ @@default ||= raise :Currency::Exception::Unimplemented, :default
24
+ end
25
+
26
+ # Sets the default Currency::Exchange object.
27
+ def self.default=(x)
28
+ @@default = x
29
+ end
30
+
31
+ # Returns the current Currency::Exchange object used during
32
+ # explicit and implicit Money trading.
33
+ #
34
+ # If #current= has not been called and #default= has not been called,
35
+ # then UndefinedExchange is raised.
36
+ def self.current
37
+ @@current || self.default || (raise ::Currency::Exception::UndefinedExchange, "Currency::Exchange.current not defined")
38
+ end
39
+
40
+ # Sets the current Currency::Exchange object used during
41
+ # explicit and implicit Money conversions.
42
+ def self.current=(x)
43
+ @@current = x
44
+ end
45
+
46
+ end # module
47
+
48
+ require 'currency/exchange/rate'
49
+ require 'currency/exchange/rate/source'
50
+