exchange 0.6.0 → 0.8.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.
Binary file
Binary file
@@ -1,107 +1,27 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
1
  # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
3
+ require 'exchange/base'
5
4
 
6
5
  Gem::Specification.new do |s|
7
- s.name = "exchange"
8
- s.version = "0.6.0"
9
-
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Beat Richartz"]
12
- s.date = "2012-10-04"
13
- s.description = "The Exchange Gem gives you easy access to currency functions directly on your Numbers. Imagine a conversion as easy as \n 1.eur.to_usd\n or even better \n 1.eur.to_usd(:at => Time.now - 84600)\n which gets you an exchange at the rates of yesterday."
14
- s.email = "exchange_gem@gmail.com"
15
- s.extra_rdoc_files = [
16
- "LICENSE.txt",
17
- "README.rdoc"
18
- ]
19
- s.files = [
20
- ".document",
21
- ".rspec",
22
- ".travis.yml",
23
- "Gemfile",
24
- "Gemfile.lock",
25
- "LICENSE.txt",
26
- "README.rdoc",
27
- "Rakefile",
28
- "VERSION",
29
- "changelog.rdoc",
30
- "exchange.gemspec",
31
- "iso4217.yml",
32
- "lib/core_extensions/conversability.rb",
33
- "lib/exchange.rb",
34
- "lib/exchange/base.rb",
35
- "lib/exchange/cache.rb",
36
- "lib/exchange/cache/base.rb",
37
- "lib/exchange/cache/file.rb",
38
- "lib/exchange/cache/memcached.rb",
39
- "lib/exchange/cache/no_cache.rb",
40
- "lib/exchange/cache/rails.rb",
41
- "lib/exchange/cache/redis.rb",
42
- "lib/exchange/configuration.rb",
43
- "lib/exchange/currency.rb",
44
- "lib/exchange/external_api.rb",
45
- "lib/exchange/external_api/base.rb",
46
- "lib/exchange/external_api/call.rb",
47
- "lib/exchange/external_api/currency_bot.rb",
48
- "lib/exchange/external_api/ecb.rb",
49
- "lib/exchange/external_api/json.rb",
50
- "lib/exchange/external_api/xavier_media.rb",
51
- "lib/exchange/external_api/xml.rb",
52
- "lib/exchange/gem_loader.rb",
53
- "lib/exchange/helper.rb",
54
- "lib/exchange/iso_4217.rb",
55
- "spec/core_extensions/conversability_spec.rb",
56
- "spec/exchange/cache/base_spec.rb",
57
- "spec/exchange/cache/file_spec.rb",
58
- "spec/exchange/cache/memcached_spec.rb",
59
- "spec/exchange/cache/no_cache_spec.rb",
60
- "spec/exchange/cache/rails_spec.rb",
61
- "spec/exchange/cache/redis_spec.rb",
62
- "spec/exchange/configuration_spec.rb",
63
- "spec/exchange/currency_spec.rb",
64
- "spec/exchange/external_api/base_spec.rb",
65
- "spec/exchange/external_api/call_spec.rb",
66
- "spec/exchange/external_api/currency_bot_spec.rb",
67
- "spec/exchange/external_api/ecb_spec.rb",
68
- "spec/exchange/external_api/xavier_media_spec.rb",
69
- "spec/exchange/gem_loader_spec.rb",
70
- "spec/exchange/helper_spec.rb",
71
- "spec/exchange/iso_4217_spec.rb",
72
- "spec/spec_helper.rb",
73
- "spec/support/api_responses/example_ecb_xml_90d.xml",
74
- "spec/support/api_responses/example_ecb_xml_daily.xml",
75
- "spec/support/api_responses/example_ecb_xml_history.xml",
76
- "spec/support/api_responses/example_historic_json.json",
77
- "spec/support/api_responses/example_json_api.json",
78
- "spec/support/api_responses/example_xml_api.xml"
79
- ]
80
- s.homepage = "http://github.com/beatrichartz/exchange"
81
- s.licenses = ["MIT"]
82
- s.require_paths = ["lib"]
83
- s.rubygems_version = "1.8.24"
84
- s.summary = "Simple Exchange Rate operations for your ruby app"
85
-
86
- if s.respond_to? :specification_version then
87
- s.specification_version = 3
88
-
89
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
90
- s.add_runtime_dependency(%q<exchange>, [">= 0"])
91
- s.add_development_dependency(%q<yard>, ["~> 0.7.4"])
92
- s.add_development_dependency(%q<bundler>, [">= 1.0.0"])
93
- s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
94
- else
95
- s.add_dependency(%q<exchange>, [">= 0"])
96
- s.add_dependency(%q<yard>, ["~> 0.7.4"])
97
- s.add_dependency(%q<bundler>, [">= 1.0.0"])
98
- s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
99
- end
100
- else
101
- s.add_dependency(%q<exchange>, [">= 0"])
102
- s.add_dependency(%q<yard>, ["~> 0.7.4"])
103
- s.add_dependency(%q<bundler>, [">= 1.0.0"])
104
- s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
105
- end
6
+ s.name = "exchange"
7
+ s.version = Exchange::VERSION
8
+ s.authors = ["Beat Richartz"]
9
+ s.date = "2012-10-09"
10
+ s.description = "The Exchange Gem gives you easy access to currency functions directly on your Numbers. Imagine a conversion as easy as \n 1.eur.to_usd\n or even better \n 1.eur.to_usd(:at => Time.now - 84600)\n which gets you an exchange at the rates of yesterday."
11
+ s.email = "exchange_gem@gmail.com"
12
+ s.homepage = "http://github.com/beatrichartz/exchange"
13
+ s.licenses = ["MIT"]
14
+ s.require_paths = ["lib"]
15
+ s.rubygems_version = "1.8.24"
16
+ s.summary = "Simple Exchange Rate operations for your ruby app"
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- spec/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+
23
+ s.add_dependency "json", ">= 1.0.0"
24
+ s.add_development_dependency "yard", ">= 0.7.4"
25
+ s.add_development_dependency "bundler", ">= 1.0.0"
106
26
  end
107
27
 
@@ -24,6 +24,9 @@ module Exchange
24
24
  # 1.nok.to_chf(:at => Time.now - 3600) => #<Exchange::Currency @value=6.57 @currency=:chf>
25
25
  # -3.5.dkk.to_huf(:at => Time.now - 172800) => #<Exchange::Currency @value=-337.40 @currency=:huf>
26
26
  #
27
+ # @since 0.1
28
+ # @version 0.2
29
+ #
27
30
  ISO4217.definitions.keys.each do |c|
28
31
  define_method c.downcase.to_sym do |*args|
29
32
  Currency.new(self, c, *args)
@@ -33,6 +36,5 @@ module Exchange
33
36
  end
34
37
  end
35
38
 
36
- Fixnum.send :include, Exchange::Conversability
37
- Float.send :include, Exchange::Conversability
38
- BigDecimal.send :include, Exchange::Conversability
39
+ # include the Conversability methods in all number operations. Stack traces will indicate the module if something goes wrong.
40
+ Numeric.send :include, Exchange::Conversability
@@ -2,7 +2,7 @@ module Exchange
2
2
 
3
3
  # The current version of the exchange gem
4
4
  #
5
- VERSION = '0.6.0'
5
+ VERSION = '0.8.0'
6
6
 
7
7
  # The root installation path of the gem
8
8
  # @version 0.5
@@ -7,15 +7,23 @@ module Exchange
7
7
  # @example Write your own caching module
8
8
  # module Cache
9
9
  # class MyCustomCache < Base
10
- # # a cache class has to have the class method "cached"
10
+ # # A cache class has to have the method "cached".
11
+ # # The cache Base is a singleton and forwards the method "cached"
12
+ # # to the instance
13
+ # #
11
14
  # def cached api, opts={}, &block
12
- # # generate the key with key(api, opts[:at]) and you will get a unique key to store in your cache
15
+ # # generate the storage with key(api, opts[:at]) and you will get a
16
+ # # unique key to store in your cache
17
+ # #
13
18
  # # Your code goes here
14
19
  # end
15
20
  # end
16
21
  # end
22
+ #
17
23
  # # Now, you can configure your Caching solution in the configuration. The Symbol will get camelcased and constantized
18
- # configuration.cache.subclass = :my_custom_cache
24
+ # #
25
+ # Exchange.configuration.cache.subclass = :my_custom_cache
26
+ #
19
27
  # # Have fun, and don't forget to write tests.
20
28
 
21
29
  module Cache
@@ -30,11 +30,12 @@ module Exchange
30
30
  # @param [Exchange::ExternalAPI::Subclass] api The API class the data has to be stored for
31
31
  # @param [Hash] opts the options to cache with
32
32
  # @option opts [Time] :at the historic time of the exchange rates to be cached
33
- # @yield [] This method takes a mandatory block with an arity of 0 and calls it if no cached result is available
33
+ # @yield [] This method takes a mandatory block with an arity of 0 for caching
34
34
  # @raise [CachingWithoutBlockError] an Argument Error when no mandatory block has been given
35
-
35
+ #
36
36
  def cached api, opts={}, &block
37
- result = opts[:plain] ? client.get(key(api, opts)).to_s.gsub(/["\s+]/, '') : JSON.load(client.get(key(api, opts)))
37
+ stored = client.get(key(api, opts))
38
+ result = opts[:plain] ? stored.to_s.gsub(/["\s+]/, '') : JSON.load(stored) if stored && !stored.empty?
38
39
 
39
40
  unless result
40
41
  result = super
@@ -52,9 +52,10 @@ module Exchange
52
52
  #
53
53
  def install_getter key, parent_module
54
54
  define_method key do
55
- @config[key] = OpenStruct.new(@config[key]) unless @config[key].is_a?(OpenStruct)
56
- @config[key].subclass = parent_module.const_get camelize(@config[key].subclass) unless @config[key].subclass.is_a?(Class)
57
- @config[key]
55
+ config_part = @config[key]
56
+ config_part = OpenStruct.new(config_part) unless config_part.is_a?(OpenStruct)
57
+ config_part.subclass = parent_module.const_get camelize(config_part.subclass) unless config_part.subclass.is_a?(Class)
58
+ @config[key] = config_part
58
59
  end
59
60
  end
60
61
 
@@ -89,20 +90,10 @@ module Exchange
89
90
  # Exchange::Configuration.new do |c|
90
91
  # c.allow_mixed_operations = false
91
92
  # c.cache = {
92
- # :subclass => Cache::Redis,
93
+ # :subclass => Exhange::Cache::Redis,
93
94
  # :expire => :hourly
94
95
  # }
95
96
  #
96
- # @yield [Exchange::Configuration] yields an instance of the configuration class
97
- # @yieldparam [optional, Symbol] cache The cache type to use. Possible Values: :redis, :memcached or :rails or false to disable caching. Defaults to :memcached
98
- # @yieldparam [optional, String] cache_host A string with the hostname or IP to set the cache host to. Does not have to be set for Rails cache
99
- # @yieldparam [optional, Integer] cache_port An integer for the cache port. Does not have to be set for Rails cache
100
- # @yieldparam [optional, Symbol] api The API to use. Possible Values: :currency_bot (Open Source currency bot API) or :xavier_media (Xavier Media API). Defaults to :currency_bot
101
- # @yieldparam [optional, Integer] retries The number of times the gem should retry to connect to the api host. Defaults to 5.
102
- # @yieldparam [optional, Boolean] If set to false, Operations with with different currencies raise errors. Defaults to true.
103
- # @yieldparam [optional, Symbol] The regularity of updates for the API. Possible values: :daily, :hourly. Defaults to :daily.
104
- # @yieldparam [optional, String] The path where files can be stored for the gem (used for large files from ECB). Make sure ruby has write access.
105
- #
106
97
  def initialize configuration={}, &block
107
98
  @config = DEFAULTS.merge(configuration)
108
99
  self.instance_eval(&block) if block_given?
@@ -1,6 +1,6 @@
1
1
  # Top Level Module of the the gem.
2
2
  # @author Beat Richartz
3
- # @version 0.1
3
+ # @version 0.7
4
4
  # @since 0.1
5
5
 
6
6
  module Exchange
@@ -32,6 +32,10 @@ module Exchange
32
32
  #
33
33
  attr_reader :from
34
34
 
35
+ # @attr_reader
36
+ # @return [Exchange::ExternalAPI] The current api subclass
37
+ attr_reader :api
38
+
35
39
  # Intialize the currency with a number and a currency
36
40
  # @param [Integer, Float] number The number the currency is instantiated from
37
41
  # @param [String, Symbol] currency The currency the currency object is in
@@ -52,6 +56,7 @@ module Exchange
52
56
  @currency = currency
53
57
  @time = Helper.assure_time(opts[:at], :default => :now)
54
58
  @from = opts[:from]
59
+ @api = Exchange.configuration.api.subclass
55
60
  end
56
61
 
57
62
  # Method missing is used to handle conversions from one currency object to another. It only handles currencies which are available in
@@ -62,14 +67,16 @@ module Exchange
62
67
  # Exchange::Currency.new(40,:nok).to_sek(:at => Time.gm(2012,2,2))
63
68
  #
64
69
  def method_missing method, *args, &block
65
- match = method.to_s.match(/\Ato_(\w{3})/)
66
- if match && Exchange.configuration.api.subclass::CURRENCIES.include?($1)
67
- return self.convert_to($1, {:at => self.time}.merge(args.first || {}))
68
- elsif match && ISO4217.definitions.keys.include?($1.upcase)
69
- raise NoRateError.new("Cannot convert to #{$1} because the defined api does not provide a rate")
70
+ match = method.to_s.match /\Ato_(\w{3})$/
71
+ currency = $1
72
+
73
+ if match && api_supports_currency?(currency)
74
+ return convert_to currency, { :at => time }.merge(args.first || {})
75
+ elsif match
76
+ test_for_no_rate_error(currency)
70
77
  end
71
78
 
72
- self.value.send method, *args, &block
79
+ value.send method, *args, &block
73
80
  end
74
81
 
75
82
  # Converts this instance of currency into another currency
@@ -95,21 +102,20 @@ module Exchange
95
102
  #
96
103
  def install_operation op
97
104
  define_method op do |*precision|
98
- @value = ISO4217.send(op, self.value, self.currency, precision.first)
99
- self
105
+ Exchange::Currency.new(ISO4217.send(op, self.value, self.currency, precision.first), currency, :at => time, :from => self)
100
106
  end
101
107
  end
102
108
 
103
109
  # @private
104
110
  # @macro [attach] base_operations
105
111
  # @method $1(other)
106
- #
112
+ #
107
113
  def base_operation op
108
114
  self.class_eval <<-EOV
109
115
  def #{op}(other)
110
- #{'raise CurrencyMixError.new("You\'re trying to mix up #{self.currency} with #{other.currency}. You denied mixing currencies in the configuration, allow it or convert the currencies before mixing") if !Exchange.configuration.allow_mixed_operations && other.kind_of?(Currency) && other.currency != self.currency'}
111
- @value #{op}= other.kind_of?(Currency) ? other.convert_to(self.currency, :at => other.time) : other
112
- self
116
+ test_for_currency_mix_error(other)
117
+ new_value = value #{op} (other.kind_of?(Currency) ? other.convert_to(self.currency, :at => other.time) : other)
118
+ Exchange::Currency.new(new_value, currency, :at => time, :from => self)
113
119
  end
114
120
  EOV
115
121
  end
@@ -121,7 +127,7 @@ module Exchange
121
127
  # @return [Exchange::Currency] The currency you started with with a rounded value
122
128
  # @param [Integer] precision The precision you want the rounding to have. Defaults to the ISO 4217 standard value for the currency
123
129
  # @since 0.1
124
- # @version 0.3
130
+ # @version 0.7.1
125
131
  # @example Round your currency to the iso standard number of decimals
126
132
  # Exchange::Currency.new(40.545, :usd).round
127
133
  # #=> #<Exchange::Currency @value=40.55 @currency=:usd>
@@ -137,7 +143,7 @@ module Exchange
137
143
  # @return [Exchange::Currency] The currency you started with with a ceiled value
138
144
  # @param [Integer] precision The precision you want the ceiling to have. Defaults to the ISO 4217 standard value for the currency
139
145
  # @since 0.1
140
- # @version 0.3
146
+ # @version 0.7.1
141
147
  # @example Ceil your currency to the iso standard number of decimals
142
148
  # Exchange::Currency.new(40.544, :usd).ceil
143
149
  # #=> #<Exchange::Currency @value=40.55 @currency=:usd>
@@ -153,7 +159,7 @@ module Exchange
153
159
  # @return [Exchange::Currency] The currency you started with with a floored value
154
160
  # @param [Integer] precision The precision you want the flooring to have. Defaults to the ISO 4217 standard value for the currency
155
161
  # @since 0.1
156
- # @version 0.3
162
+ # @version 0.7.1
157
163
  # @example Floor your currency to the iso standard number of decimals
158
164
  # Exchange::Currency.new(40.545, :usd).floor
159
165
  # #=> #<Exchange::Currency @value=40.54 @currency=:usd>
@@ -175,6 +181,8 @@ module Exchange
175
181
  # @example Configuration allows mixed operations (default)
176
182
  # Exchange::Currency.new(20,:nok) + Exchange::Currency.new(20,:sek)
177
183
  # #=> #<Exchange::Currency @value=37.56 @currency=:nok>
184
+ # @since 0.1
185
+ # @version 0.7
178
186
  #
179
187
  base_operation '+'
180
188
 
@@ -189,6 +197,8 @@ module Exchange
189
197
  # @example Configuration allows mixed operations (default)
190
198
  # Exchange::Currency.new(20,:nok) - Exchange::Currency.new(20,:sek)
191
199
  # #=> #<Exchange::Currency @value=7.56 @currency=:nok>
200
+ # @since 0.1
201
+ # @version 0.7
192
202
  #
193
203
  base_operation '-'
194
204
 
@@ -203,6 +213,8 @@ module Exchange
203
213
  # @example Configuration allows mixed operations (default)
204
214
  # Exchange::Currency.new(20,:nok) * Exchange::Currency.new(20,:sek)
205
215
  # #=> #<Exchange::Currency @value=70.56 @currency=:nok>
216
+ # @since 0.1
217
+ # @version 0.7
206
218
  #
207
219
  base_operation '*'
208
220
 
@@ -217,6 +229,8 @@ module Exchange
217
229
  # @example Configuration allows mixed operations (default)
218
230
  # Exchange::Currency.new(20,:nok) / Exchange::Currency.new(20,:sek)
219
231
  # #=> #<Exchange::Currency @value=1.56 @currency=:nok>
232
+ # @since 0.1
233
+ # @version 0.7
220
234
  #
221
235
  base_operation '/'
222
236
 
@@ -230,6 +244,8 @@ module Exchange
230
244
  # Exchange::Currency.new(40, :usd) == Exchange::Currency.new(34, :eur) #=> true, will implicitly convert eur to usd at the actual rate
231
245
  # @example Compare a currency with a number, the value of the currency will get compared
232
246
  # Exchange::Currency.new(35, :usd) == 35 #=> true
247
+ # @since 0.1
248
+ # @version 0.6
233
249
  #
234
250
  def == other
235
251
  if is_same_currency?(other)
@@ -246,7 +262,7 @@ module Exchange
246
262
  # @param [Whatever you want to throw at it] other The counterpart to compare
247
263
  # @return [Fixed] a number which can be used for sorting
248
264
  # @since 0.3
249
- # @version 0.3
265
+ # @version 0.6
250
266
  # @todo which historic conversion should be used when two are present?
251
267
  # @example Compare two currencies in terms of value
252
268
  # Exchange::Currency.new(40, :usd) <=> Exchange::Currency.new(28, :usd) #=> -1
@@ -254,7 +270,7 @@ module Exchange
254
270
  # Exchange::Currency.new(40, :usd) <=> Exchange::Currency.new(28, :eur) #=> -1
255
271
  # @example Sort multiple currencies in an array
256
272
  # [1.usd, 1.eur, 1.chf].sort.map(&:currency) #=> [:usd, :chf, :eur]
257
-
273
+ #
258
274
  def <=> other
259
275
  if is_same_currency?(other)
260
276
  self.value <=> other.value
@@ -279,7 +295,7 @@ module Exchange
279
295
  # Exchange::Currency.new(34.34, :omr).to_s #=> "OMR 34.340"
280
296
  # @example Convert a currency to a string without the currency
281
297
  # Exchange::ISO4217.stringif(34.34, :omr).to_s(:iso) #=> "34.340"
282
-
298
+ #
283
299
  def to_s format=:currency
284
300
  [
285
301
  format == :currency && ISO4217.stringify(self.value, self.currency),
@@ -292,6 +308,8 @@ module Exchange
292
308
  # determine if another given object is an instance of Exchange::Currency
293
309
  # @param [Object] other The object to be tested against
294
310
  # @return [Boolean] true if the other is an instance of Exchange::Currency, false if not
311
+ # @since 0.6
312
+ # @version 0.6
295
313
  #
296
314
  def is_currency? other
297
315
  other.is_a?(Exchange::Currency)
@@ -300,6 +318,8 @@ module Exchange
300
318
  # determine if another given object is an instance of Exchange::Currency and the same currency
301
319
  # @param [Object] other The object to be tested against
302
320
  # @return [Boolean] true if the other is an instance of Exchange::Currency and has the same currency as self, false if not
321
+ # @since 0.6
322
+ # @version 0.6
303
323
  #
304
324
  def is_same_currency? other
305
325
  is_currency?(other) && other.currency == self.currency
@@ -308,10 +328,40 @@ module Exchange
308
328
  # determine if another given object is an instance of Exchange::Currency and has another currency
309
329
  # @param [Object] other The object to be tested against
310
330
  # @return [Boolean] true if the other is an instance of Exchange::Currency and has another currency as self, false if not
331
+ # @since 0.6
332
+ # @version 0.6
311
333
  #
312
334
  def is_other_currency? other
313
335
  is_currency?(other) && other.currency != self.currency
314
336
  end
337
+
338
+ # determine wether the chosen api supports converting the given currency
339
+ # @param [String] currency The currency to test the api for
340
+ # @return [Boolean] True if the api supports the given currency, false if not
341
+ #
342
+ def api_supports_currency? currency
343
+ api::CURRENCIES.include?(currency)
344
+ end
345
+
346
+ # Test if another currency is used in an operation, and if so, if the operation is allowed
347
+ # @param [Numeric, Exchange::Currency] other The counterpart in the operation
348
+ # @raise [CurrencyMixError] an error if mixing currencies is not allowed and currencies where mixed
349
+ # @since 0.6
350
+ # @version 0.6
351
+ #
352
+ def test_for_currency_mix_error other
353
+ raise CurrencyMixError.new("You\'re trying to mix up #{self.currency} with #{other.currency}. You denied mixing currencies in the configuration, allow it or convert the currencies before mixing") if !Exchange.configuration.allow_mixed_operations && other.kind_of?(Currency) && other.currency != self.currency
354
+ end
355
+
356
+ # Helper method to raise a no rate error for a given currency if no rate is given
357
+ # @param [String] currency a possible currency
358
+ # @raise [NoRateError] an error indicating that the given string is a currency, but no rate is present
359
+ # @since 0.7.2
360
+ # @version 0.7.2
361
+ #
362
+ def test_for_no_rate_error currency
363
+ raise NoRateError.new("Cannot convert to #{currency} because the defined api does not provide a rate") if ISO4217.definitions.keys.include?(currency.upcase)
364
+ end
315
365
 
316
366
  end
317
367