exchange 0.6.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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