currency 0.3.3 → 0.4.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.
Files changed (54) hide show
  1. data/COPYING.txt +339 -0
  2. data/LICENSE.txt +62 -0
  3. data/Manifest.txt +37 -14
  4. data/README.txt +8 -0
  5. data/Rakefile +42 -8
  6. data/Releases.txt +26 -0
  7. data/TODO.txt +1 -0
  8. data/examples/ex1.rb +3 -3
  9. data/examples/xe1.rb +3 -2
  10. data/lib/currency.rb +71 -9
  11. data/lib/currency/active_record.rb +138 -21
  12. data/lib/currency/core_extensions.rb +7 -5
  13. data/lib/currency/currency.rb +94 -177
  14. data/lib/currency/{currency_factory.rb → currency/factory.rb} +46 -25
  15. data/lib/currency/currency_version.rb +3 -3
  16. data/lib/currency/exception.rb +14 -14
  17. data/lib/currency/exchange.rb +14 -12
  18. data/lib/currency/exchange/rate.rb +159 -28
  19. data/lib/currency/exchange/rate/deriver.rb +146 -0
  20. data/lib/currency/exchange/rate/source.rb +84 -0
  21. data/lib/currency/exchange/rate/source/base.rb +156 -0
  22. data/lib/currency/exchange/rate/source/failover.rb +57 -0
  23. data/lib/currency/exchange/rate/source/historical.rb +79 -0
  24. data/lib/currency/exchange/rate/source/historical/rate.rb +181 -0
  25. data/lib/currency/exchange/rate/source/historical/writer.rb +203 -0
  26. data/lib/currency/exchange/rate/source/new_york_fed.rb +91 -0
  27. data/lib/currency/exchange/rate/source/provider.rb +105 -0
  28. data/lib/currency/exchange/rate/source/test.rb +50 -0
  29. data/lib/currency/exchange/rate/source/the_financials.rb +190 -0
  30. data/lib/currency/exchange/rate/source/timed_cache.rb +144 -0
  31. data/lib/currency/exchange/rate/source/xe.rb +166 -0
  32. data/lib/currency/exchange/time_quantitizer.rb +111 -0
  33. data/lib/currency/formatter.rb +159 -0
  34. data/lib/currency/macro.rb +321 -0
  35. data/lib/currency/money.rb +90 -64
  36. data/lib/currency/money_helper.rb +6 -5
  37. data/lib/currency/parser.rb +153 -0
  38. data/test/ar_column_test.rb +6 -3
  39. data/test/ar_simple_test.rb +5 -2
  40. data/test/ar_test_base.rb +39 -33
  41. data/test/ar_test_core.rb +64 -0
  42. data/test/formatter_test.rb +81 -0
  43. data/test/historical_writer_test.rb +184 -0
  44. data/test/macro_test.rb +109 -0
  45. data/test/money_test.rb +72 -4
  46. data/test/new_york_fed_test.rb +57 -0
  47. data/test/parser_test.rb +60 -0
  48. data/test/test_base.rb +13 -3
  49. data/test/time_quantitizer_test.rb +136 -0
  50. data/test/xe_test.rb +29 -5
  51. metadata +41 -18
  52. data/lib/currency/exchange/base.rb +0 -84
  53. data/lib/currency/exchange/test.rb +0 -39
  54. data/lib/currency/exchange/xe.rb +0 -250
@@ -1,6 +1,6 @@
1
+ module Currency
2
+ CurrencyVersion = '0.4.0'
3
+ end
1
4
  # DO NOT EDIT
2
5
  # This file is auto-generated by build scripts.
3
6
  # See: rake update_version
4
- module Currency
5
- CurrencyVersion = '0.3.3'
6
- end
@@ -1,14 +1,11 @@
1
- module Currency
2
- module Exception
3
- end
4
- end
5
-
6
- # Base class for all Currency::Exception.
7
- class Currency::Exception::Base < Exception
8
- end
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ module Currency::Exception
5
+ # Base class for all Currency::Exception objects.
6
+ class Base < ::Exception
7
+ end
9
8
 
10
- module Currency
11
- module Exception
12
9
  # Error during string parsing.
13
10
  class InvalidMoneyString < Base
14
11
  end
@@ -33,13 +30,16 @@ module Currency
33
30
  class UnknownCurrency < Base
34
31
  end
35
32
 
36
- # Error if an Exchange cannot provide an Exchange::Rate.
33
+ # Error if an Exchange Rate Source cannot provide an Exchange::Rate.
37
34
  class UnknownRate < Base
38
35
  end
39
36
 
40
37
  # Error if an Exchange::Rate is not valid.
41
38
  class InvalidRate < Base
42
39
  end
43
-
44
- end
45
- end
40
+
41
+ # Error if a subclass is responsible for implementing a method.
42
+ class SubclassResponsibility < Base
43
+ end
44
+
45
+ end # module
@@ -1,13 +1,14 @@
1
- # -*- ruby -*-
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ # The Currency::Exchange package is responsible for
5
+ # the buying and selling of currencies.
2
6
  #
3
- # = Currency::Exchange
7
+ # This feature is currently unimplemented.
4
8
  #
5
- # The Currency::Exchange package is responsible for
6
- # the conversion between currencies.
9
+ # Exchange rate sources are configured via Currency::Exchange::Rate::Source.
7
10
  #
8
-
9
- module Currency
10
- module Exchange
11
+ module Currency::Exchange
11
12
  @@default = nil
12
13
  @@current = nil
13
14
 
@@ -17,7 +18,7 @@ module Exchange
17
18
  # created. Currency::Exchange::Base cannot service any
18
19
  # conversion rate requests.
19
20
  def self.default
20
- @@default ||= Base.new
21
+ @@default ||= raise("UNIMPLEMENTED")
21
22
  end
22
23
 
23
24
  # Sets the default Currency::Exchange object.
@@ -31,7 +32,7 @@ module Exchange
31
32
  # If #current= has not been called and #default= has not been called,
32
33
  # then UndefinedExchange is raised.
33
34
  def self.current
34
- @@current || self.default || (raise Exception::UndefinedExchange.new("Currency::Exchange.current not defined"))
35
+ @@current || self.default || (raise ::Currency::Exception::UndefinedExchange.new("Currency::Exchange.current not defined"))
35
36
  end
36
37
 
37
38
  # Sets the current Currency::Exchange object used during
@@ -39,8 +40,9 @@ module Exchange
39
40
  def self.current=(x)
40
41
  @@current = x
41
42
  end
42
- end
43
- end
44
43
 
45
- require 'currency/exchange/base'
44
+ end # module
45
+
46
46
  require 'currency/exchange/rate'
47
+ require 'currency/exchange/rate/source'
48
+
@@ -1,40 +1,100 @@
1
- module Currency
2
- module Exchange
3
- # Represents a convertion rate between two currencies
4
- class Rate
5
- def initialize(c1, c2, c1_to_c2_rate, source = "UNKNOWN", date = nil, recip = true)
1
+ # Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
2
+ # See LICENSE.txt for details.
3
+
4
+ # Represents a convertion rate between two currencies at a given period of time.
5
+ class Currency::Exchange::Rate
6
+ # The first Currency.
7
+ attr_reader :c1
8
+
9
+ # The second Currency.
10
+ attr_reader :c2
11
+
12
+ # The rate between c1 and c2.
13
+ #
14
+ # To convert between m1 in c1 and m2 in c2:
15
+ #
16
+ # m2 = m1 * rate.
17
+ attr_reader :rate
18
+
19
+ # The source of the rate.
20
+ attr_reader :source
21
+
22
+ # The Time of the rate.
23
+ attr_reader :date
24
+
25
+ # If the rate is derived from other rates, this describes from where it was derived.
26
+ attr_reader :derived
27
+
28
+ # Average rate over rate samples in a time range.
29
+ attr_reader :rate_avg
30
+
31
+ # Number of rate samples used to calcuate _rate_avg_.
32
+ attr_reader :rate_samples
33
+
34
+ # The rate low between date_0 and date_1.
35
+ attr_reader :rate_lo
36
+
37
+ # The rate high between date_0 and date_1.
38
+ attr_reader :rate_hi
39
+
40
+ # The rate at date_0.
41
+ attr_reader :rate_date_0
42
+
43
+ # The rate at date_1.
44
+ attr_reader :rate_date_1
45
+
46
+ # The lowest date of sampled rates.
47
+ attr_reader :date_0
48
+
49
+ # The highest date of sampled rates.
50
+ # This is non-inclusive during searches to allow seamless tileings of
51
+ # time with rate buckets.
52
+ attr_reader :date_1
53
+
54
+ def initialize(c1, c2, c1_to_c2_rate, source = "UNKNOWN", date = nil, derived = nil, reciprocal = nil, opts = nil)
6
55
  @c1 = c1
7
56
  @c2 = c2
8
57
  @rate = c1_to_c2_rate
9
- raise Exception::InvalidRate.new(@rate) unless @rate > 0.0
58
+ raise ::Currency::Exception::InvalidRate.new(@rate) unless @rate && @rate >= 0.0
10
59
  @source = source
11
- @date = date || Time.now
12
- @reciprocal = recip
13
- end
14
-
15
- def c1
16
- @c1
17
- end
60
+ @date = date
61
+ @derived = derived
62
+ @reciprocal = reciprocal
18
63
 
19
- def c2
20
- @c2
64
+ if opts
65
+ opts.each_pair do | k, v |
66
+ self.instance_variable_set("@#{k}", v)
67
+ end
68
+ end
21
69
  end
22
70
 
23
- def rate
24
- @rate
25
- end
26
71
 
27
- def source
28
- @source
72
+ # Returns a cached reciprocal Rate object from c2 to c1.
73
+ def reciprocal
74
+ @reciprocal ||= @rate == 1.0 ? self :
75
+ self.class.new(@c2, @c1,
76
+ 1.0 / @rate,
77
+ @source,
78
+ @date,
79
+ "reciprocal(#{derived || "#{c1.code}#{c2.code}"})", self,
80
+ {
81
+ :rate_avg => @rate_avg && 1.0 / @rate_avg,
82
+ :rate_samples => @rate_samples,
83
+ :rate_lo => @rate_lo && 1.0 / @rate_lo,
84
+ :rate_hi => @rate_hi && 1.0 / @rate_hi,
85
+ :rate_date_0 => @rate_date_0 && 1.0 / @rate_date_0,
86
+ :rate_date_1 => @rate_date_1 && 1.0 / @rate_date_1,
87
+ :date_0 => @date_0,
88
+ :date_1 => @date_1,
89
+ }
90
+ )
29
91
  end
30
92
 
31
- def date
32
- @date
33
- end
34
93
 
94
+ # Converts from _m_ in Currency _c1_ to the opposite currency.
35
95
  def convert(m, c1)
36
96
  m = m.to_f
37
- if ( @c1 == c1 )
97
+ if @c1 == c1
38
98
  # $stderr.puts "Converting #{@c1} #{m} to #{@c2} #{m * @rate} using #{@rate}"
39
99
  m * @rate
40
100
  else
@@ -42,10 +102,81 @@ module Exchange
42
102
  m / @rate
43
103
  end
44
104
  end
45
- end
46
105
 
47
-
48
- end # module
49
- end # module
106
+
107
+ # Collect rate samples into rate_avg, rate_hi, rate_lo, rate_0, rate_1, date_0, date_1.
108
+ def collect_rates(rates)
109
+ rates = [ rates ] unless rates.kind_of?(Enumerable)
110
+ rates.each do | r |
111
+ collect_rate(r)
112
+ end
113
+ self
114
+ end
115
+
116
+ # Collect rates samples in to this Rate.
117
+ def collect_rate(rate)
118
+ # Initial.
119
+ @rate_samples ||= 0
120
+ @rate_sum ||= 0
121
+ @src ||= rate
122
+ @c1 ||= rate.c1
123
+ @c2 ||= rate.c2
124
+ @date ||= rate.date
125
+ @src ||= rate.src
126
+
127
+ # Reciprocal?
128
+ if @c1 == rate.c2 && @c2 == rate.c1
129
+ collect_rate(rate.reciprocal)
130
+ elsif ! (@c1 == rate.c1 && @c2 == rate.c2)
131
+ raise("Cannot collect rates between different currency pairs")
132
+ else
133
+ # Multisource?
134
+ @src = "<<multiple-sources>>" unless @src == rate.source
135
+
136
+ # Calculate rate average.
137
+ @rate_samples += 1
138
+ @rate_sum += rate.rate || (rate.rate_lo + rate.rate_hi) * 0.5
139
+ @rate_avg = @rate_sum / @rate_samples
140
+
141
+ # Calculate rates ranges.
142
+ r = rate.rate_lo || rate.rate
143
+ unless @rate_lo && @rate_lo < r
144
+ @rate_lo = r
145
+ end
146
+ r = rate.rate_hi || rate.rate
147
+ unless @rate_hi && @rate_hi > r
148
+ @rate_hi = r
149
+ end
150
+
151
+ # Calculate rates on date range boundaries
152
+ r = rate.rate_date_0 || rate.rate
153
+ d = rate.date_0 || rate.date
154
+ unless @date_0 && @date_0 < d
155
+ @date_0 = d
156
+ @rate_0 = r
157
+ end
158
+
159
+ r = rate.rate_date_1 || rate.rate
160
+ d = rate.date_1 || rate.date
161
+ unless @date_1 && @date_1 > d
162
+ @date_1 = d
163
+ @rate_0 = r
164
+ end
165
+
166
+ @date ||= rate.date || rate.date0 || rate.date1
167
+ end
168
+ self
169
+ end
170
+
171
+
172
+ def to_s(extended = false)
173
+ extended = "#{date_0} #{rate_0} |< #{rate_lo} #{rate} #{rate_hi} >| #{rate_1} #{date_1}" if extended
174
+ extended ||= ''
175
+ "#<#{self.class.name} #{c1.code} #{c2.code} #{rate} #{source.inspect} #{date ? date.strftime('%Y/%m/%d-%H:%M:%S') : 'nil'} #{derived && derived.inspect} #{extended}>"
176
+ end
177
+
178
+ def inspect; to_s; end
179
+
180
+ end # class
50
181
 
51
182
 
@@ -0,0 +1,146 @@
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
+ require 'currency/exchange/rate/source/base'
6
+
7
+ # The Currency::Exchange::Rate::Deriver class calculates derived rates
8
+ # from base Rates from a rate Source by pivoting against a pivot currency or by
9
+ # generating reciprocals.
10
+ #
11
+ class Currency::Exchange::Rate::Deriver < Currency::Exchange::Rate::Source::Base
12
+
13
+ # The source for base rates.
14
+ attr_accessor :source
15
+
16
+
17
+ def name
18
+ source.name
19
+ end
20
+
21
+
22
+ def initialize(opt = { })
23
+ @source = nil
24
+ @pivot_currency = nil
25
+ @derived_rates = { }
26
+ @all_rates = { }
27
+ super
28
+ end
29
+
30
+
31
+ def pivot_currency
32
+ @pivot_currency || @source.pivot_currency || :USD
33
+ end
34
+
35
+
36
+ # Return all currencies.
37
+ def currencies
38
+ @source.currencies
39
+ end
40
+
41
+
42
+ # Flush all cached Rates.
43
+ def clear_rates
44
+ @derived_rate.clear
45
+ @all_rates.clear
46
+ @source.clear
47
+ super
48
+ end
49
+
50
+
51
+ # Returns all combinations of rates except identity rates.
52
+ def rates(time = nil)
53
+ time = time && normalize_time(time)
54
+ all_rates(time)
55
+ end
56
+
57
+
58
+ # Computes all rates.
59
+ # time is assumed to be normalized.
60
+ def all_rates(time = nil)
61
+ if x = @all_rates["#{time}"]
62
+ return x
63
+ end
64
+
65
+ x = @all_rates["#{time}"] = [ ]
66
+
67
+ currencies = self.currencies
68
+
69
+ currencies.each do | c1 |
70
+ currencies.each do | c2 |
71
+ next if c1 == c2
72
+ c1 = ::Currency::Currency.get(c1)
73
+ c2 = ::Currency::Currency.get(c2)
74
+ rate = rate(c1, c2, time)
75
+ x << rate
76
+ end
77
+ end
78
+
79
+ x
80
+ end
81
+
82
+
83
+ # Determines and creates the Rate between Currency c1 and c2.
84
+ #
85
+ # May attempt to use a pivot currency to bridge between
86
+ # rates.
87
+ #
88
+ def get_rate(c1, c2, time)
89
+ rate = get_rate_reciprocal(c1, c2, time)
90
+
91
+ # Attempt to use pivot_currency to bridge
92
+ # between Rates.
93
+ unless rate
94
+ pc = ::Currency::Currency.get(pivot_currency)
95
+
96
+ if pc &&
97
+ (rate_1 = get_rate_reciprocal(c1, pc, time)) &&
98
+ (rate_2 = get_rate_reciprocal(pc, c2, time))
99
+ c1_to_c2_rate = rate_1.rate * rate_2.rate
100
+ rate = new_rate(c1, c2,
101
+ c1_to_c2_rate,
102
+ rate_1.date || rate_2.date || time,
103
+ "pivot(#{pc.code},#{rate_1.derived || "#{rate_1.c1.code}#{rate_1.c2.code}"},#{rate_2.derived || "#{rate_2.c1}#{rate_2.c2}"})")
104
+ end
105
+ end
106
+
107
+ rate
108
+ end
109
+
110
+
111
+ # Get a matching base rate or its reciprocal.
112
+ def get_rate_reciprocal(c1, c2, time)
113
+ rate = get_rate_base_cached(c1, c2, time)
114
+ unless rate
115
+ if rate = get_rate_base_cached(c2, c1, time)
116
+ rate = (@rate["#{c1}:#{c2}:#{time}"] ||= rate.reciprocal)
117
+ end
118
+ end
119
+
120
+ rate
121
+ end
122
+
123
+
124
+ # Returns a cached base Rate.
125
+ #
126
+ def get_rate_base_cached(c1, c2, time)
127
+ rate = (@rate["#{c1}:#{c2}:#{time}"] ||= get_rate_base(c1, c2, time))
128
+ rate
129
+ end
130
+
131
+
132
+ # Returns a base Rate from the Source.
133
+ def get_rate_base(c1, c2, time)
134
+ if c1 == c2
135
+ # Identity rates are timeless.
136
+ new_rate(c1, c2, 1.0, nil, "identity")
137
+ else
138
+ source.rate(c1, c2, time)
139
+ end
140
+ end
141
+
142
+
143
+ end # class
144
+
145
+
146
+