exchange 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.document +5 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +17 -0
  5. data/Gemfile.lock +41 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.rdoc +262 -0
  8. data/Rakefile +46 -0
  9. data/VERSION +1 -0
  10. data/exchange.gemspec +102 -0
  11. data/lib/core_extensions/conversability.rb +32 -0
  12. data/lib/exchange.rb +11 -0
  13. data/lib/exchange/cache.rb +4 -0
  14. data/lib/exchange/cache/base.rb +51 -0
  15. data/lib/exchange/cache/memcached.rb +52 -0
  16. data/lib/exchange/cache/rails.rb +45 -0
  17. data/lib/exchange/cache/redis.rb +54 -0
  18. data/lib/exchange/configuration.rb +72 -0
  19. data/lib/exchange/currency.rb +237 -0
  20. data/lib/exchange/external_api.rb +4 -0
  21. data/lib/exchange/external_api/base.rb +114 -0
  22. data/lib/exchange/external_api/call.rb +76 -0
  23. data/lib/exchange/external_api/currency_bot.rb +47 -0
  24. data/lib/exchange/external_api/xavier_media.rb +47 -0
  25. data/spec/core_extensions/conversability_spec.rb +64 -0
  26. data/spec/exchange/cache/base_spec.rb +29 -0
  27. data/spec/exchange/cache/memcached_spec.rb +72 -0
  28. data/spec/exchange/cache/rails_spec.rb +67 -0
  29. data/spec/exchange/cache/redis_spec.rb +76 -0
  30. data/spec/exchange/configuration_spec.rb +47 -0
  31. data/spec/exchange/currency_spec.rb +219 -0
  32. data/spec/exchange/external_api/base_spec.rb +31 -0
  33. data/spec/exchange/external_api/call_spec.rb +68 -0
  34. data/spec/exchange/external_api/currency_bot_spec.rb +61 -0
  35. data/spec/exchange/external_api/xavier_media_spec.rb +59 -0
  36. data/spec/spec_helper.rb +28 -0
  37. data/spec/support/api_responses/example_historic_json.json +167 -0
  38. data/spec/support/api_responses/example_json_api.json +167 -0
  39. data/spec/support/api_responses/example_xml_api.xml +156 -0
  40. 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
@@ -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,4 @@
1
+ require 'exchange/cache/base'
2
+ require 'exchange/cache/memcached'
3
+ require 'exchange/cache/redis'
4
+ require 'exchange/cache/rails'
@@ -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