exchange 0.2.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +41 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +262 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/exchange.gemspec +102 -0
- data/lib/core_extensions/conversability.rb +32 -0
- data/lib/exchange.rb +11 -0
- data/lib/exchange/cache.rb +4 -0
- data/lib/exchange/cache/base.rb +51 -0
- data/lib/exchange/cache/memcached.rb +52 -0
- data/lib/exchange/cache/rails.rb +45 -0
- data/lib/exchange/cache/redis.rb +54 -0
- data/lib/exchange/configuration.rb +72 -0
- data/lib/exchange/currency.rb +237 -0
- data/lib/exchange/external_api.rb +4 -0
- data/lib/exchange/external_api/base.rb +114 -0
- data/lib/exchange/external_api/call.rb +76 -0
- data/lib/exchange/external_api/currency_bot.rb +47 -0
- data/lib/exchange/external_api/xavier_media.rb +47 -0
- data/spec/core_extensions/conversability_spec.rb +64 -0
- data/spec/exchange/cache/base_spec.rb +29 -0
- data/spec/exchange/cache/memcached_spec.rb +72 -0
- data/spec/exchange/cache/rails_spec.rb +67 -0
- data/spec/exchange/cache/redis_spec.rb +76 -0
- data/spec/exchange/configuration_spec.rb +47 -0
- data/spec/exchange/currency_spec.rb +219 -0
- data/spec/exchange/external_api/base_spec.rb +31 -0
- data/spec/exchange/external_api/call_spec.rb +68 -0
- data/spec/exchange/external_api/currency_bot_spec.rb +61 -0
- data/spec/exchange/external_api/xavier_media_spec.rb +59 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/api_responses/example_historic_json.json +167 -0
- data/spec/support/api_responses/example_json_api.json +167 -0
- data/spec/support/api_responses/example_xml_api.xml +156 -0
- metadata +191 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module Exchange
|
2
|
+
|
3
|
+
# The conversability module which will get included in Fixnum and Float, giving them the currency exchange methods
|
4
|
+
# @author Beat Richartz
|
5
|
+
# @version 0.2
|
6
|
+
# @since 0.1
|
7
|
+
|
8
|
+
module Conversability
|
9
|
+
# Method missing is used here to allow instantiation and immediate conversion of Currency objects from a common Fixnum or Float
|
10
|
+
# @example Instantiate from any type of number
|
11
|
+
# 40.usd => #<Exchange::Currency @value=40 @currency=:usd>
|
12
|
+
# -33.nok => #<Exchange::Currency @value=-33 @currency=:nok>
|
13
|
+
# 33.333.sek => #<Exchange::Currency @value=33.333 @currency=:sek>
|
14
|
+
# @example Instantiate and immediatly convert
|
15
|
+
# 1.usd.to_eur => #<Exchange::Currency @value=0.79 @currency=:eur>
|
16
|
+
# 1.nok.to_chf => #<Exchange::Currency @value=6.55 @currency=:chf>
|
17
|
+
# -3.5.dkk.to_huf => #<Exchange::Currency @value=-346.55 @currency=:huf>
|
18
|
+
# @example Instantiate and immediatly convert at a specific time in the past
|
19
|
+
# 1.usd.to_eur(:at => Time.now - 86400) => #<Exchange::Currency @value=0.80 @currency=:eur>
|
20
|
+
# 1.nok.to_chf(:at => Time.now - 3600) => #<Exchange::Currency @value=6.57 @currency=:chf>
|
21
|
+
# -3.5.dkk.to_huf(:at => Time.now - 172800) => #<Exchange::Currency @value=-337.40 @currency=:huf>
|
22
|
+
|
23
|
+
def method_missing method, *args, &block
|
24
|
+
return Exchange::Currency.new(self, method, *args) if method.to_s.length == 3 && Exchange::Configuration.api_class::CURRENCIES.include?(method.to_s)
|
25
|
+
|
26
|
+
super method, *args, &block
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Fixnum.send :include, Exchange::Conversability
|
32
|
+
Float.send :include, Exchange::Conversability
|
data/lib/exchange.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'bundler'
|
3
|
+
require 'json'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'redis'
|
6
|
+
require 'memcached'
|
7
|
+
require 'exchange/configuration'
|
8
|
+
require 'exchange/currency'
|
9
|
+
require 'exchange/external_api'
|
10
|
+
require 'exchange/cache'
|
11
|
+
require 'core_extensions/conversability'
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Exchange
|
2
|
+
# The cache module. All Classes handling caching for this gem have to be placed here. Allows easy extension with own caching solutions
|
3
|
+
# as shown in the example below.
|
4
|
+
# @example Write your own caching module
|
5
|
+
# module Cache
|
6
|
+
# class MyCustomCache < Base
|
7
|
+
# class << self
|
8
|
+
# # a cache class has to have the class method "cached"
|
9
|
+
# def cached api, opts={}, &block
|
10
|
+
# # generate the key with key(api, opts[:at]) and you will get a unique key to store in your cache
|
11
|
+
# # Your code goes here
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
# # Now, you can configure your Caching solution in the configuration. The Symbol will get camelcased and constantized
|
17
|
+
# Exchange::Configuration.cache = :my_custom_cache
|
18
|
+
# # Have fun, and don't forget to write tests.
|
19
|
+
|
20
|
+
module Cache
|
21
|
+
|
22
|
+
# The base Class for all Caching operations. Essentially generates a helper function for all cache classes to generate a key
|
23
|
+
# @author Beat Richartz
|
24
|
+
# @version 0.1
|
25
|
+
# @since 0.1
|
26
|
+
|
27
|
+
class Base
|
28
|
+
class << self
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
# A Cache Key generator for the API Classes and the time
|
33
|
+
# Generates a key which can handle expiration by itself
|
34
|
+
# @param [Exchange::ExternalAPI::Subclass] api_class The API to store the data for
|
35
|
+
# @param [optional, Time] time The time for which the data is valid
|
36
|
+
# @return [String] A string that can be used as cache key
|
37
|
+
# @example
|
38
|
+
# Exchange::Cache::Base.key(Exchange::ExternalAPI::CurrencyBot, Time.gm(2012,1,1)) #=> "Exchange_ExternalAPI_CurrencyBot_2012_1"
|
39
|
+
|
40
|
+
def key(api_class, time=nil)
|
41
|
+
time ||= Time.now
|
42
|
+
key_parts = [api_class.to_s.gsub(/::/, '_'), time.year, time.yday]
|
43
|
+
key_parts << time.hour if Exchange::Configuration.update == :hourly
|
44
|
+
key_parts.join('_')
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
CachingWithoutBlockError = Class.new(ArgumentError)
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Exchange
|
2
|
+
module Cache
|
3
|
+
# @author Beat Richartz
|
4
|
+
# A class that cooperates with the memcached gem to cache the data from the exchange api in memcached
|
5
|
+
#
|
6
|
+
# @version 0.1
|
7
|
+
# @since 0.1
|
8
|
+
# @example Activate caching via memcache by setting the cache in the configuration to :memcached
|
9
|
+
# Exchange::Configuration.define do |c|
|
10
|
+
# c.cache = :memcached
|
11
|
+
# c.cache_host = 'Your memcached host'
|
12
|
+
# c.cache_port = 'Your memcached port'
|
13
|
+
# end
|
14
|
+
class Memcached < Base
|
15
|
+
class << self
|
16
|
+
|
17
|
+
# instantiates a memcached client and memoizes it in a class variable.
|
18
|
+
# Use this client to access memcached data. For further explanation of use visit the memcached gem documentation
|
19
|
+
# @example
|
20
|
+
# Exchange::Cache::Memcached.client.set('FOO', 'BAR')
|
21
|
+
# @return [::Memcached] an instance of the memcached client gem class
|
22
|
+
|
23
|
+
def client
|
24
|
+
@@client ||= ::Memcached.new("#{Exchange::Configuration.cache_host}:#{Exchange::Configuration.cache_port}")
|
25
|
+
end
|
26
|
+
|
27
|
+
# returns either cached data from the memcached client or calls the block and caches it in memcached.
|
28
|
+
# This method has to be the same in all the cache classes in order for the configuration binding to work
|
29
|
+
# @param [Exchange::ExternalAPI::Subclass] api The API class the data has to be stored for
|
30
|
+
# @param [Hash] opts the options to cache with
|
31
|
+
# @option opts [Time] :at the historic time of the exchange rates to be cached
|
32
|
+
# @yield [] This method takes a mandatory block with an arity of 0 and calls it if no cached result is available
|
33
|
+
# @raise [CachingWithoutBlockError] an Argument Error when no mandatory block has been given
|
34
|
+
|
35
|
+
def cached api, opts={}, &block
|
36
|
+
raise CachingWithoutBlockError.new('Caching needs a block') unless block_given?
|
37
|
+
begin
|
38
|
+
result = JSON.load client.get(key(api, opts[:at]))
|
39
|
+
rescue ::Memcached::NotFound
|
40
|
+
result = block.call
|
41
|
+
if result && !result.empty?
|
42
|
+
client.set key(api, opts[:at]), result.to_json, Exchange::Configuration.update == :daily ? 86400 : 3600
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
result
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Exchange
|
2
|
+
module Cache
|
3
|
+
# @author Beat Richartz
|
4
|
+
# A class that cooperates with rails to cache the data from the exchange api in rails
|
5
|
+
#
|
6
|
+
# @version 0.1
|
7
|
+
# @since 0.1
|
8
|
+
# @example Activate caching via rails by setting the cache in the configuration to :rails
|
9
|
+
# Exchange::Configuration.define do |c|
|
10
|
+
# c.cache = :rails
|
11
|
+
# end
|
12
|
+
class Rails < Base
|
13
|
+
class << self
|
14
|
+
|
15
|
+
# returns a Rails cache client. This has not to be stored since rails already memoizes it.
|
16
|
+
# Use this client to access rails cache data. For further explanation of use visit the rails documentation
|
17
|
+
# @example
|
18
|
+
# Exchange::Cache::Rails.client.set('FOO', 'BAR')
|
19
|
+
# @return [ActiveSupport::Cache::Subclass] an instance of the rails cache class (presumably a subclass of ActiveSupport::Cache)
|
20
|
+
|
21
|
+
def client
|
22
|
+
::Rails.cache if defined?(::Rails)
|
23
|
+
end
|
24
|
+
|
25
|
+
# returns either cached data from the memcached client or calls the block and caches it in rails cache.
|
26
|
+
# This method has to be the same in all the cache classes in order for the configuration binding to work
|
27
|
+
# @param [Exchange::ExternalAPI::Subclass] api The API class the data has to be stored for
|
28
|
+
# @param [Hash] opts the options to cache with
|
29
|
+
# @option opts [Time] :at the historic time of the exchange rates to be cached
|
30
|
+
# @yield [] This method takes a mandatory block with an arity of 0 and calls it if no cached result is available
|
31
|
+
# @raise [CachingWithoutBlockError] an Argument Error when no mandatory block has been given
|
32
|
+
|
33
|
+
def cached api, opts={}, &block
|
34
|
+
raise CachingWithoutBlockError.new('Caching needs a block') unless block_given?
|
35
|
+
|
36
|
+
result = client.fetch key(api, opts[:at]), :expires_in => Exchange::Configuration.update == :daily ? 86400 : 3600, &block
|
37
|
+
client.delete(key(api, opts[:at])) unless result && !result.empty?
|
38
|
+
|
39
|
+
result
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Exchange
|
2
|
+
module Cache
|
3
|
+
# @author Beat Richartz
|
4
|
+
# A class that cooperates with the redis gem and the redis key value store to cache the data from the exchange api in redis
|
5
|
+
#
|
6
|
+
# @version 0.1
|
7
|
+
# @since 0.1
|
8
|
+
# @example Activate caching via redis by setting the cache in the configuration to :redis
|
9
|
+
# Exchange::Configuration.define do |c|
|
10
|
+
# c.cache = :redis
|
11
|
+
# c.cache_host = 'Your redis host'
|
12
|
+
# c.cache_port = 'Your redis port (an Integer)'
|
13
|
+
# end
|
14
|
+
class Redis < Base
|
15
|
+
class << self
|
16
|
+
|
17
|
+
# instantiates a redis client and memoizes it in a class variable.
|
18
|
+
# Use this client to access redis data. For further explanation of use visit the redis gem documentation
|
19
|
+
# @example
|
20
|
+
# Exchange::Cache::Redis.client.set('FOO', 'BAR')
|
21
|
+
# @return [::Redis] an instance of the redis client gem class
|
22
|
+
|
23
|
+
def client
|
24
|
+
@@client ||= ::Redis.new(:host => Exchange::Configuration.cache_host, :port => Exchange::Configuration.cache_port)
|
25
|
+
end
|
26
|
+
|
27
|
+
# returns either cached data from the redis client or calls the block and caches it in redis.
|
28
|
+
# This method has to be the same in all the cache classes in order for the configuration binding to work
|
29
|
+
# @param [Exchange::ExternalAPI::Subclass] api The API class the data has to be stored for
|
30
|
+
# @param [Hash] opts the options to cache with
|
31
|
+
# @option opts [Time] :at the historic time of the exchange rates to be cached
|
32
|
+
# @yield [] This method takes a mandatory block with an arity of 0 and calls it if no cached result is available
|
33
|
+
# @raise [CachingWithoutBlockError] an Argument Error when no mandatory block has been given
|
34
|
+
|
35
|
+
def cached api, opts={}, &block
|
36
|
+
raise CachingWithoutBlockError.new('Caching needs a block') unless block_given?
|
37
|
+
|
38
|
+
if result = client.get(key(api, opts[:at]))
|
39
|
+
result = JSON.load result
|
40
|
+
else
|
41
|
+
result = block.call
|
42
|
+
if result && !result.empty?
|
43
|
+
client.set key(api, opts[:at]), result.to_json
|
44
|
+
client.expire key(api, opts[:at]), Exchange::Configuration.update == :daily ? 86400 : 3600
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
result
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Exchange
|
2
|
+
# @author Beat Richartz
|
3
|
+
# A configuration class that stores the configuration of the gem. It allows to set the api from which the data gets retrieved,
|
4
|
+
# the cache in which the data gets cached, the regularity of updates for the currency rates, how many times the api calls should be
|
5
|
+
# retried on failure, and wether operations mixing currencies should raise errors or not
|
6
|
+
# @version 0.1
|
7
|
+
# @since 0.1
|
8
|
+
class Configuration
|
9
|
+
class << self
|
10
|
+
@@config ||= {:api => :currency_bot, :retries => 5, :allow_mixed_operations => true, :cache => :memcached, :cache_host => 'localhost', :cache_port => 11211, :update => :daily}
|
11
|
+
|
12
|
+
# A configuration method that stores the configuration of the gem. It allows to set the api from which the data gets retrieved,
|
13
|
+
# the cache in which the data gets cached, the regularity of updates for the currency rates, how many times the api calls should be
|
14
|
+
# retried on failure, and wether operations mixing currencies should raise errors or not
|
15
|
+
# @version 0.1
|
16
|
+
# @since 0.1
|
17
|
+
# @example Set a configuration by passing a block to the configuration
|
18
|
+
# Exchange::Configuration.define do |c|
|
19
|
+
# c.cache = :redis
|
20
|
+
# c.cache_host = '127.0.0.1'
|
21
|
+
# c.cache_port = 6547
|
22
|
+
# c.api = :currency_bot
|
23
|
+
# c.retries = 1
|
24
|
+
# c.allow_mixed_operations = false
|
25
|
+
# c.update = :hourly
|
26
|
+
# end
|
27
|
+
# @yield [Exchange::Configuration] yields a the configuration class
|
28
|
+
# @yieldparam [optional, Symbol] cache The cache type to use. Possible Values: :redis, :memcached or :rails or false to disable caching. Defaults to :memcached
|
29
|
+
# @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
|
30
|
+
# @yieldparam [optional, Integer] cache_port An integer for the cache port. Does not have to be set for Rails cache
|
31
|
+
# @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
|
32
|
+
# @yieldparam [optional, Integer] retries The number of times the gem should retry to connect to the api host. Defaults to 5.
|
33
|
+
# @yieldparam [optional, Boolean] If set to false, Operations with with different currencies raise errors. Defaults to true.
|
34
|
+
# @yieldparam [optional, Symbol] The regularity of updates for the API. Possible values: :daily, :hourly. Defaults to :daily.
|
35
|
+
# @example Set configuration values directly to the class
|
36
|
+
# Exchange::Configuration.cache = :redis
|
37
|
+
# Exchange::Configuration.api = :xavier_media
|
38
|
+
def define &blk
|
39
|
+
self.instance_eval(&blk)
|
40
|
+
end
|
41
|
+
|
42
|
+
[:api, :retries, :cache, :cache_host, :cache_port, :update, :allow_mixed_operations].each do |m|
|
43
|
+
define_method m do
|
44
|
+
@@config[m]
|
45
|
+
end
|
46
|
+
define_method :"#{m}=" do |data|
|
47
|
+
@@config.merge! m => data
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# The instantiated api class according to the configuration
|
52
|
+
# @example
|
53
|
+
# Exchange::Configuration.api = :currency_bot
|
54
|
+
# Exchange::Configuration.api_class #=> Exchange::ExternalAPI::CurrencyBot
|
55
|
+
# @return [Exchange::ExternalAPI::Subclass] A subclass of Exchange::ExternalAPI
|
56
|
+
|
57
|
+
def api_class
|
58
|
+
Exchange::ExternalAPI.const_get self.api.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }
|
59
|
+
end
|
60
|
+
|
61
|
+
# The instantiated cache class according to the configuration
|
62
|
+
# @example
|
63
|
+
# Exchange::Configuration.cache = :redis
|
64
|
+
# Exchange::Configuration.cache_class #=> Exchange::ExternalAPI::Redis
|
65
|
+
# @return [Exchange::Cache::Subclass] A subclass of Exchange::Cache (or nil if caching has been set to false)
|
66
|
+
|
67
|
+
def cache_class
|
68
|
+
Exchange::Cache.const_get self.cache.to_s.gsub(/(?:^|_)(.)/) { $1.upcase } if self.cache
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
# Top Level Module of the the gem.
|
2
|
+
# @author Beat Richartz
|
3
|
+
# @version 0.1
|
4
|
+
# @since 0.1
|
5
|
+
|
6
|
+
module Exchange
|
7
|
+
# @author Beat Richartz
|
8
|
+
# Currency Objects instantiated from the currency class can be used for basic mathematical operations and currency conversions
|
9
|
+
# @version 0.1
|
10
|
+
# @since 0.1
|
11
|
+
class Currency
|
12
|
+
include Comparable
|
13
|
+
|
14
|
+
# @attr_reader
|
15
|
+
# @return [Float] number The number the currency object has been instantiated from
|
16
|
+
attr_reader :value
|
17
|
+
|
18
|
+
# @attr_reader
|
19
|
+
# @return [Symbol, String] currency the currency of the currency object
|
20
|
+
attr_reader :currency
|
21
|
+
|
22
|
+
# @attr_reader
|
23
|
+
# @return [Time] The time at which the conversion has taken place or should take place if the object is involved in operations
|
24
|
+
attr_reader :time
|
25
|
+
|
26
|
+
# @attr_reader
|
27
|
+
# @return [Exchange::Currency] The original currency object this currency object was converted from
|
28
|
+
attr_reader :from
|
29
|
+
|
30
|
+
# Intialize the currency with a number and a currency
|
31
|
+
# @param [Integer, Float] number The number the currency is instantiated from
|
32
|
+
# @param [String, Symbol] currency The currency the currency object is in
|
33
|
+
# @param [Hash] opts Optional Parameters for instantiation
|
34
|
+
# @option opts [Time] :at The time at which conversion took place
|
35
|
+
# @option opts [String,Symbol] :from The currency object this currency object was converted from
|
36
|
+
# @version 0.2
|
37
|
+
#
|
38
|
+
# @example Instantiate a currency object of 40 US Dollars
|
39
|
+
# Exchange::Currency.new(40, :usd)
|
40
|
+
# #=> #<Exchange::Currency @number=40.0 @currency=:usd @time=#<Time>>
|
41
|
+
# @example Instantiate a currency object of 40 US Dollars and convert it to Euro. It shows the conversion date and the original currency
|
42
|
+
# Exchange::Currency.new(40, :usd).to_eur(:at => Time.gm(2012,9,1))
|
43
|
+
# #=> #<Exchange::Currency @number=37.0 @currency=:usd @time=#<Time> @from=#<Exchange::Currency @number=40.0 @currency=:usd>>
|
44
|
+
|
45
|
+
def initialize value, currency, opts={}
|
46
|
+
@value = value.to_f
|
47
|
+
@currency = currency
|
48
|
+
@time = assure_time(opts[:at], :default => :now)
|
49
|
+
@from = opts[:from] if opts[:from]
|
50
|
+
end
|
51
|
+
|
52
|
+
# Method missing is used to handle conversions from one currency object to another. It only handles currencies which are available in
|
53
|
+
# the API class set in the configuration.
|
54
|
+
# @example Calls convert_to with 'chf'
|
55
|
+
# Exchange::Currency.new(40,:usd).to_chf
|
56
|
+
# @example Calls convert_to with 'sek' and :at => Time.gm(2012,2,2)
|
57
|
+
# Exchange::Currency.new(40,:nok).to_sek(:at => Time.gm(2012,2,2))
|
58
|
+
|
59
|
+
def method_missing method, *args, &block
|
60
|
+
if method.to_s.match(/\Ato_(\w+)/) && Exchange::Configuration.api_class::CURRENCIES.include?($1)
|
61
|
+
args.first[:at] ||= time if args.first
|
62
|
+
return self.convert_to($1, args.first || {:at => self.time})
|
63
|
+
end
|
64
|
+
|
65
|
+
self.value.send method, *args, &block
|
66
|
+
end
|
67
|
+
|
68
|
+
# Converts this instance of currency into another currency
|
69
|
+
# @return [Exchange::Currency] An instance of Exchange::Currency with the converted number and the converted currency
|
70
|
+
# @param [Symbol, String] other The currency to convert the number to
|
71
|
+
# @param [Hash] opts An options hash
|
72
|
+
# @option [Time] :at The timestamp of the rate the conversion took place in
|
73
|
+
# @example convert to 'chf'
|
74
|
+
# Exchange::Currency.new(40,:usd).convert_to('chf')
|
75
|
+
# @example convert to 'sek' at a specific rate
|
76
|
+
# Exchange::Currency.new(40,:nok).convert_to('sek', :at => Time.gm(2012,2,2))
|
77
|
+
|
78
|
+
def convert_to other, opts={}
|
79
|
+
Exchange::Currency.new(Exchange::Configuration.api_class.new.convert(value, currency, other, opts), other, opts.merge(:from => self))
|
80
|
+
end
|
81
|
+
|
82
|
+
class << self
|
83
|
+
|
84
|
+
private
|
85
|
+
# @private
|
86
|
+
# @macro [attach] install_operations
|
87
|
+
|
88
|
+
def install_operation op
|
89
|
+
define_method op do
|
90
|
+
@value = self.value.send(op)
|
91
|
+
self
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# @private
|
96
|
+
# @macro [attach] base_operations
|
97
|
+
# @method $1(other)
|
98
|
+
|
99
|
+
def base_operation op
|
100
|
+
self.class_eval <<-EOV
|
101
|
+
def #{op}(other)
|
102
|
+
#{'raise Exchange::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?(Exchange::Currency) && other.currency != self.currency'}
|
103
|
+
@value #{op}= other.kind_of?(Exchange::Currency) ? other.convert_to(self.currency, :at => other.time) : other
|
104
|
+
self
|
105
|
+
end
|
106
|
+
EOV
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
# Round the currency (Equivalent to normal round)
|
112
|
+
# @return [Exchange::Currency] The currency you started with with a rounded value
|
113
|
+
# @example
|
114
|
+
# Exchange::Currency.new(40.5, :usd).round
|
115
|
+
# #=> #<Exchange::Currency @number=41 @currency=:usd>
|
116
|
+
|
117
|
+
install_operation :round
|
118
|
+
|
119
|
+
|
120
|
+
# Ceil the currency (Equivalent to normal ceil)
|
121
|
+
# @return [Exchange::Currency] The currency you started with with a ceiled value
|
122
|
+
# @example
|
123
|
+
# Exchange::Currency.new(40.4, :usd).ceil
|
124
|
+
# #=> #<Exchange::Currency @number=41 @currency=:usd>
|
125
|
+
|
126
|
+
install_operation :ceil
|
127
|
+
|
128
|
+
|
129
|
+
# Floor the currency (Equivalent to normal floor)
|
130
|
+
# @return [Exchange::Currency] The currency you started with with a floored value
|
131
|
+
# @example
|
132
|
+
# Exchange::Currency.new(40.7, :usd).floor
|
133
|
+
# #=> #<Exchange::Currency @number=40 @currency=:usd>
|
134
|
+
|
135
|
+
install_operation :floor
|
136
|
+
|
137
|
+
|
138
|
+
# Add value to the currency
|
139
|
+
# @param [Integer, Float, Exchange::Currency] other The value to be added to the currency. If an Exchange::Currency, it is converted to the instance's currency and then the converted value is added.
|
140
|
+
# @return [Exchange::Currency] The currency with the added value
|
141
|
+
# @raise [CurrencyMixError] If the configuration does not allow mixed operations, this method will raise an error if two different currencies are used in the operation
|
142
|
+
# @example Configuration disallows mixed operations
|
143
|
+
# Exchange::Configuration.allow_mixed_operations = false
|
144
|
+
# Exchange::Currency.new(20,:nok) + Exchange::Currency.new(20,:sek)
|
145
|
+
# #=> #<CurrencyMixError "You tried to mix currencies">
|
146
|
+
# @example Configuration allows mixed operations (default)
|
147
|
+
# Exchange::Currency.new(20,:nok) + Exchange::Currency.new(20,:sek)
|
148
|
+
# #=> #<Exchange::Currency @value=37.56 @currency=:nok>
|
149
|
+
|
150
|
+
base_operation '+'
|
151
|
+
|
152
|
+
# Subtract a value from the currency
|
153
|
+
# @param [Integer, Float, Exchange::Currency] other The value to be subtracted from the currency. If an Exchange::Currency, it is converted to the instance's currency and then subtracted from the converted value.
|
154
|
+
# @return [Exchange::Currency] The currency with the added value
|
155
|
+
# @raise [CurrencyMixError] If the configuration does not allow mixed operations, this method will raise an error if two different currencies are used in the operation
|
156
|
+
# @example Configuration disallows mixed operations
|
157
|
+
# Exchange::Configuration.allow_mixed_operations = false
|
158
|
+
# Exchange::Currency.new(20,:nok) - Exchange::Currency.new(20,:sek)
|
159
|
+
# #=> #<CurrencyMixError "You tried to mix currencies">
|
160
|
+
# @example Configuration allows mixed operations (default)
|
161
|
+
# Exchange::Currency.new(20,:nok) - Exchange::Currency.new(20,:sek)
|
162
|
+
# #=> #<Exchange::Currency @value=7.56 @currency=:nok>
|
163
|
+
|
164
|
+
base_operation '-'
|
165
|
+
|
166
|
+
# Multiply a value with the currency
|
167
|
+
# @param [Integer, Float, Exchange::Currency] other The value to be multiplied with the currency. If an Exchange::Currency, it is converted to the instance's currency and multiplied with the converted value.
|
168
|
+
# @return [Exchange::Currency] The currency with the multiplied value
|
169
|
+
# @raise [CurrencyMixError] If the configuration does not allow mixed operations, this method will raise an error if two different currencies are used in the operation
|
170
|
+
# @example Configuration disallows mixed operations
|
171
|
+
# Exchange::Configuration.allow_mixed_operations = false
|
172
|
+
# Exchange::Currency.new(20,:nok) * Exchange::Currency.new(20,:sek)
|
173
|
+
# #=> #<CurrencyMixError "You tried to mix currencies">
|
174
|
+
# @example Configuration allows mixed operations (default)
|
175
|
+
# Exchange::Currency.new(20,:nok) * Exchange::Currency.new(20,:sek)
|
176
|
+
# #=> #<Exchange::Currency @value=70.56 @currency=:nok>
|
177
|
+
|
178
|
+
base_operation '*'
|
179
|
+
|
180
|
+
# Divide the currency by a value
|
181
|
+
# @param [Integer, Float, Exchange::Currency] other The value to be divided by the currency. If an Exchange::Currency, it is converted to the instance's currency and divided by the converted value.
|
182
|
+
# @return [Exchange::Currency] The currency with the divided value
|
183
|
+
# @raise [CurrencyMixError] If the configuration does not allow mixed operations, this method will raise an error if two different currencies are used in the operation
|
184
|
+
# @example Configuration disallows mixed operations
|
185
|
+
# Exchange::Configuration.allow_mixed_operations = false
|
186
|
+
# Exchange::Currency.new(20,:nok) / Exchange::Currency.new(20,:sek)
|
187
|
+
# #=> #<CurrencyMixError "You tried to mix currencies">
|
188
|
+
# @example Configuration allows mixed operations (default)
|
189
|
+
# Exchange::Currency.new(20,:nok) / Exchange::Currency.new(20,:sek)
|
190
|
+
# #=> #<Exchange::Currency @value=1.56 @currency=:nok>
|
191
|
+
|
192
|
+
base_operation '/'
|
193
|
+
|
194
|
+
def == other
|
195
|
+
if other.is_a?(Exchange::Currency) && other.currency == self.currency
|
196
|
+
other.value == self.value
|
197
|
+
elsif other.is_a?(Exchange::Currency)
|
198
|
+
other.convert_to(self.currency, :at => other.time).value == self.value
|
199
|
+
else
|
200
|
+
self.value == other
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def <=> other
|
205
|
+
if other.is_a?(Exchange::Currency) && ((other.currency == self.currency && self.value < other.value) || (other.currency != self.currency && self.value < other.convert_to(self.currency, :at => other.time).value))
|
206
|
+
-1
|
207
|
+
elsif other.is_a?(Exchange::Currency) && ((other.currency == self.currency && self.value > other.value) || (other.currency != self.currency && self.value > other.convert_to(self.currency, :at => other.time).value))
|
208
|
+
1
|
209
|
+
elsif other.is_a?(Exchange::Currency)
|
210
|
+
0
|
211
|
+
else
|
212
|
+
self.value <=> other
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
protected
|
218
|
+
|
219
|
+
# A helper function to assure a value is an instance of time
|
220
|
+
# @param [Time, String, NilClass] The value to be asserted
|
221
|
+
# @param [Hash] opts Options for assertion
|
222
|
+
# @option opts [Symbol] :default If the argument is nil, you can define :default as :now to be delivered with Time.now instead of nil
|
223
|
+
# @version 0.2
|
224
|
+
|
225
|
+
def assure_time(arg=nil, opts={})
|
226
|
+
if arg
|
227
|
+
arg.kind_of?(Time) ? arg : Time.gm(*arg.split('-'))
|
228
|
+
elsif opts[:default] == :now
|
229
|
+
Time.now
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
|
235
|
+
# The error that will get thrown when currencies get mixed up in base operations
|
236
|
+
CurrencyMixError = Class.new(ArgumentError)
|
237
|
+
end
|