exchange 0.2.6
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.
- 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
|