exchange 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. data/.travis.yml +1 -0
  2. data/README.rdoc +5 -3
  3. data/VERSION +1 -1
  4. data/exchange.gemspec +16 -2
  5. data/iso4217.yml +589 -0
  6. data/lib/core_extensions/conversability.rb +12 -7
  7. data/lib/exchange.rb +7 -1
  8. data/lib/exchange/cache.rb +2 -0
  9. data/lib/exchange/cache/base.rb +9 -5
  10. data/lib/exchange/cache/file.rb +65 -0
  11. data/lib/exchange/cache/memcached.rb +4 -4
  12. data/lib/exchange/cache/no_cache.rb +33 -0
  13. data/lib/exchange/cache/rails.rb +2 -2
  14. data/lib/exchange/cache/redis.rb +5 -5
  15. data/lib/exchange/configuration.rb +23 -7
  16. data/lib/exchange/currency.rb +94 -40
  17. data/lib/exchange/external_api.rb +1 -0
  18. data/lib/exchange/external_api/base.rb +11 -19
  19. data/lib/exchange/external_api/call.rb +10 -12
  20. data/lib/exchange/external_api/currency_bot.rb +2 -2
  21. data/lib/exchange/external_api/ecb.rb +68 -0
  22. data/lib/exchange/external_api/xavier_media.rb +4 -3
  23. data/lib/exchange/helper.rb +27 -0
  24. data/lib/exchange/iso_4217.rb +95 -0
  25. data/spec/core_extensions/conversability_spec.rb +40 -6
  26. data/spec/exchange/cache/base_spec.rb +4 -4
  27. data/spec/exchange/cache/file_spec.rb +70 -0
  28. data/spec/exchange/cache/memcached_spec.rb +5 -2
  29. data/spec/exchange/cache/no_cache_spec.rb +27 -0
  30. data/spec/exchange/cache/rails_spec.rb +6 -3
  31. data/spec/exchange/cache/redis_spec.rb +5 -2
  32. data/spec/exchange/currency_spec.rb +86 -23
  33. data/spec/exchange/external_api/base_spec.rb +8 -5
  34. data/spec/exchange/external_api/call_spec.rb +38 -29
  35. data/spec/exchange/external_api/currency_bot_spec.rb +8 -10
  36. data/spec/exchange/external_api/ecb_spec.rb +55 -0
  37. data/spec/exchange/external_api/xavier_media_spec.rb +8 -8
  38. data/spec/exchange/helper_spec.rb +30 -0
  39. data/spec/exchange/iso_4217_spec.rb +45 -0
  40. data/spec/support/api_responses/example_ecb_xml_90d.xml +64 -0
  41. data/spec/support/api_responses/example_ecb_xml_daily.xml +44 -0
  42. data/spec/support/api_responses/example_ecb_xml_history.xml +64 -0
  43. metadata +35 -21
@@ -6,7 +6,10 @@ module Exchange
6
6
  # @since 0.1
7
7
 
8
8
  module Conversability
9
- # Method missing is used here to allow instantiation and immediate conversion of Currency objects from a common Fixnum or Float
9
+ # Dynamic method generation is used here to allow instantiation and immediate conversion of Currency objects from
10
+ # a common Fixnum or Float or BigDecimal. Since ruby 1.9 handles certain type conversion of Fixnum, Float and others
11
+ # via method missing, this is not handled via method missing because it would seriously break down performance.
12
+ #
10
13
  # @example Instantiate from any type of number
11
14
  # 40.usd => #<Exchange::Currency @value=40 @currency=:usd>
12
15
  # -33.nok => #<Exchange::Currency @value=-33 @currency=:nok>
@@ -20,13 +23,15 @@ module Exchange
20
23
  # 1.nok.to_chf(:at => Time.now - 3600) => #<Exchange::Currency @value=6.57 @currency=:chf>
21
24
  # -3.5.dkk.to_huf(:at => Time.now - 172800) => #<Exchange::Currency @value=-337.40 @currency=:huf>
22
25
 
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
26
+ ISO4217.definitions.keys.each do |c|
27
+ define_method c.downcase.to_sym do |*args|
28
+ Currency.new(self, c, *args)
29
+ end
27
30
  end
31
+
28
32
  end
29
33
  end
30
34
 
31
- Fixnum.send :include, Exchange::Conversability
32
- Float.send :include, Exchange::Conversability
35
+ Fixnum.send :include, Exchange::Conversability
36
+ Float.send :include, Exchange::Conversability
37
+ BigDecimal.send :include, Exchange::Conversability
@@ -1,11 +1,17 @@
1
+ require 'bigdecimal'
1
2
  require 'open-uri'
2
3
  require 'bundler'
3
4
  require 'json'
4
5
  require 'nokogiri'
5
6
  require 'redis'
6
7
  require 'memcached'
8
+ require 'exchange/helper'
7
9
  require 'exchange/configuration'
10
+ require 'exchange/iso_4217'
8
11
  require 'exchange/currency'
9
12
  require 'exchange/external_api'
10
13
  require 'exchange/cache'
11
- require 'core_extensions/conversability'
14
+ require 'core_extensions/conversability'
15
+
16
+ # The error that gets thrown if no conversion rate is available
17
+ NoRateError = Class.new(StandardError)
@@ -2,3 +2,5 @@ require 'exchange/cache/base'
2
2
  require 'exchange/cache/memcached'
3
3
  require 'exchange/cache/redis'
4
4
  require 'exchange/cache/rails'
5
+ require 'exchange/cache/file'
6
+ require 'exchange/cache/no_cache'
@@ -37,11 +37,15 @@ module Exchange
37
37
  # @example
38
38
  # Exchange::Cache::Base.key(Exchange::ExternalAPI::CurrencyBot, Time.gm(2012,1,1)) #=> "Exchange_ExternalAPI_CurrencyBot_2012_1"
39
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('_')
40
+ def key(api, opts={})
41
+ time = Exchange::Helper.assure_time(opts[:at], :default => :now)
42
+ [ 'exchange',
43
+ api.to_s,
44
+ time.year.to_s,
45
+ time.yday.to_s,
46
+ Exchange::Configuration.update == :hourly ? time.hour.to_s : nil,
47
+ *(opts[:key_for] || [])
48
+ ].compact.join('_')
45
49
  end
46
50
 
47
51
  end
@@ -0,0 +1,65 @@
1
+ module Exchange
2
+ module Cache
3
+
4
+ # @author Beat Richartz
5
+ # A class that allows to store api call results in files. THIS NOT A RECOMMENDED CACHING OPTION!
6
+ # It just may be necessary to cache large files somewhere, this class allows you to do that
7
+ #
8
+ # @version 0.3
9
+ # @since 0.3
10
+
11
+ class File < Base
12
+ class << self
13
+
14
+ # returns either cached data from a stored file or stores a file.
15
+ # This method has to be the same in all the cache classes in order for the configuration binding to work
16
+ # @param [Exchange::ExternalAPI::Subclass] api The API class the data has to be stored for
17
+ # @param [Hash] opts the options to cache with
18
+ # @option opts [Time] :at IS IGNORED FOR FILECACHE
19
+ # @option opts [Symbol] :cache_period The period to cache the file for
20
+ # @yield [] This method takes a mandatory block with an arity of 0 and calls it if no cached result is available
21
+ # @raise [CachingWithoutBlockError] an Argument Error when no mandatory block has been given
22
+
23
+ def cached api, opts={}, &block
24
+ raise CachingWithoutBlockError.new('Caching needs a block') unless block_given?
25
+
26
+ today = Time.now
27
+ dir = Exchange::Configuration.filestore_path
28
+ path = ::File.join(dir, key(api, opts[:cache_period]))
29
+
30
+ if ::File.exists?(path)
31
+ result = ::File.read(path)
32
+ else
33
+ result = block.call
34
+ if result && !result.to_s.empty?
35
+ FileUtils.mkdir_p(dir) unless Dir.respond_to?(:exists?) && Dir.exists?(dir)
36
+ keep_files = [key(api, :daily), key(api, :monthly)]
37
+ Dir.entries(dir).each do |e|
38
+ ::File.delete(::File.join(dir, e)) unless keep_files.include?(e) || e.match(/\A\./)
39
+ end
40
+ ::File.open(path, 'w') {|f| f.write(result) }
41
+ end
42
+ end
43
+
44
+ result
45
+ end
46
+
47
+ private
48
+
49
+ # A Cache Key generator for the file Cache Class and the time
50
+ # Generates a key which can handle expiration by itself
51
+ # @param [Exchange::ExternalAPI::Subclass] api_class The API to store the data for
52
+ # @param [optional, Symbol] cache_period The time for which the data is valid
53
+ # @return [String] A string that can be used as cache key
54
+ # @example
55
+ # Exchange::Cache::Base.key(Exchange::ExternalAPI::CurrencyBot, :monthly) #=> "Exchange_ExternalAPI_CurrencyBot_monthly_2012_1"
56
+
57
+ def key(api_class, cache_period=:daily)
58
+ time = Time.now
59
+ [api_class.to_s.gsub(/::/, '_'), cache_period, time.year, time.send(cache_period == :monthly ? :month : :yday)].join('_')
60
+ end
61
+
62
+ end
63
+ end
64
+ end
65
+ end
@@ -21,7 +21,7 @@ module Exchange
21
21
  # @return [::Memcached] an instance of the memcached client gem class
22
22
 
23
23
  def client
24
- @@client ||= ::Memcached.new("#{Exchange::Configuration.cache_host}:#{Exchange::Configuration.cache_port}")
24
+ @@client ||= ::Memcached.new("#{Configuration.cache_host}:#{Configuration.cache_port}")
25
25
  end
26
26
 
27
27
  # returns either cached data from the memcached client or calls the block and caches it in memcached.
@@ -35,11 +35,11 @@ module Exchange
35
35
  def cached api, opts={}, &block
36
36
  raise CachingWithoutBlockError.new('Caching needs a block') unless block_given?
37
37
  begin
38
- result = JSON.load client.get(key(api, opts[:at]))
38
+ result = JSON.load client.get(key(api, opts))
39
39
  rescue ::Memcached::NotFound
40
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
41
+ if result && !result.to_s.empty?
42
+ client.set key(api, opts), result.to_json, Configuration.update == :daily ? 86400 : 3600
43
43
  end
44
44
  end
45
45
 
@@ -0,0 +1,33 @@
1
+ module Exchange
2
+ module Cache
3
+
4
+ # @author Beat Richartz
5
+ # A class that allows to store api call results in files. THIS NOT A RECOMMENDED CACHING OPTION!
6
+ # It just may be necessary to cache large files somewhere, this class allows you to do that
7
+ #
8
+ # @version 0.3
9
+ # @since 0.3
10
+
11
+ class NoCache < Base
12
+ class << self
13
+
14
+ # returns either cached data from a stored file or stores a file.
15
+ # This method has to be the same in all the cache classes in order for the configuration binding to work
16
+ # @param [Exchange::ExternalAPI::Subclass] api The API class the data has to be stored for
17
+ # @param [Hash] opts the options to cache with
18
+ # @option opts [Time] :at IS IGNORED FOR FILECACHE
19
+ # @option opts [Symbol] :cache_period The period to cache the file for
20
+ # @yield [] This method takes a mandatory block with an arity of 0 and calls it if no cached result is available
21
+ # @raise [CachingWithoutBlockError] an Argument Error when no mandatory block has been given
22
+
23
+ def cached api, opts={}, &block
24
+ raise CachingWithoutBlockError.new('Caching needs a block') unless block_given?
25
+
26
+ block.call
27
+ end
28
+
29
+
30
+ end
31
+ end
32
+ end
33
+ end
@@ -33,8 +33,8 @@ module Exchange
33
33
  def cached api, opts={}, &block
34
34
  raise CachingWithoutBlockError.new('Caching needs a block') unless block_given?
35
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?
36
+ result = client.fetch key(api, opts), :expires_in => Configuration.update == :daily ? 86400 : 3600, &block
37
+ client.delete(key(api, opts)) unless result && !result.to_s.empty?
38
38
 
39
39
  result
40
40
  end
@@ -21,7 +21,7 @@ module Exchange
21
21
  # @return [::Redis] an instance of the redis client gem class
22
22
 
23
23
  def client
24
- @@client ||= ::Redis.new(:host => Exchange::Configuration.cache_host, :port => Exchange::Configuration.cache_port)
24
+ @@client ||= ::Redis.new(:host => Configuration.cache_host, :port => Configuration.cache_port)
25
25
  end
26
26
 
27
27
  # returns either cached data from the redis client or calls the block and caches it in redis.
@@ -35,13 +35,13 @@ module Exchange
35
35
  def cached api, opts={}, &block
36
36
  raise CachingWithoutBlockError.new('Caching needs a block') unless block_given?
37
37
 
38
- if result = client.get(key(api, opts[:at]))
38
+ if result = client.get(key(api, opts))
39
39
  result = JSON.load result
40
40
  else
41
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
42
+ if result && !result.to_s.empty?
43
+ client.set key(api, opts), result.to_json
44
+ client.expire key(api, opts), Configuration.update == :daily ? 86400 : 3600
45
45
  end
46
46
  end
47
47
 
@@ -7,7 +7,7 @@ module Exchange
7
7
  # @since 0.1
8
8
  class Configuration
9
9
  class << self
10
- @@config ||= {:api => :currency_bot, :retries => 5, :allow_mixed_operations => true, :cache => :memcached, :cache_host => 'localhost', :cache_port => 11211, :update => :daily}
10
+ @@config ||= {:api => :currency_bot, :retries => 5, :filestore_path => File.expand_path('exchange_filestore'), :allow_mixed_operations => true, :cache => :memcached, :cache_host => 'localhost', :cache_port => 11211, :update => :daily}
11
11
 
12
12
  # A configuration method that stores the configuration of the gem. It allows to set the api from which the data gets retrieved,
13
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
@@ -32,14 +32,16 @@ module Exchange
32
32
  # @yieldparam [optional, Integer] retries The number of times the gem should retry to connect to the api host. Defaults to 5.
33
33
  # @yieldparam [optional, Boolean] If set to false, Operations with with different currencies raise errors. Defaults to true.
34
34
  # @yieldparam [optional, Symbol] The regularity of updates for the API. Possible values: :daily, :hourly. Defaults to :daily.
35
+ # @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.
35
36
  # @example Set configuration values directly to the class
36
37
  # Exchange::Configuration.cache = :redis
37
38
  # Exchange::Configuration.api = :xavier_media
39
+
38
40
  def define &blk
39
41
  self.instance_eval(&blk)
40
42
  end
41
-
42
- [:api, :retries, :cache, :cache_host, :cache_port, :update, :allow_mixed_operations].each do |m|
43
+
44
+ [:api, :retries, :cache, :cache_host, :cache_port, :filestore_path, :update, :allow_mixed_operations].each do |m|
43
45
  define_method m do
44
46
  @@config[m]
45
47
  end
@@ -52,20 +54,34 @@ module Exchange
52
54
  # @example
53
55
  # Exchange::Configuration.api = :currency_bot
54
56
  # Exchange::Configuration.api_class #=> Exchange::ExternalAPI::CurrencyBot
57
+ # @param [Hash] options A hash of Options
58
+ # @option options [Class] :api A api to return instead of the api class (use for fallback)
55
59
  # @return [Exchange::ExternalAPI::Subclass] A subclass of Exchange::ExternalAPI
56
60
 
57
- def api_class
58
- Exchange::ExternalAPI.const_get self.api.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }
61
+ def api_class(options={})
62
+ @api_class ||= {}
63
+ return @api_class[options[:api] || self.api] if @api_class[options[:api] || self.api]
64
+
65
+ @api_class[options[:api] || self.api] = ExternalAPI.const_get((options[:api] || self.api).to_s.gsub(/(?:^|_)(.)/) { $1.upcase })
59
66
  end
60
67
 
61
68
  # The instantiated cache class according to the configuration
62
69
  # @example
63
70
  # Exchange::Configuration.cache = :redis
64
71
  # Exchange::Configuration.cache_class #=> Exchange::ExternalAPI::Redis
72
+ # @param [Hash] options A hash of Options
73
+ # @option options [Class] :api A api to return instead of the api class (use for fallback)
65
74
  # @return [Exchange::Cache::Subclass] A subclass of Exchange::Cache (or nil if caching has been set to false)
66
75
 
67
- def cache_class
68
- Exchange::Cache.const_get self.cache.to_s.gsub(/(?:^|_)(.)/) { $1.upcase } if self.cache
76
+ def cache_class(options={})
77
+ @cache_class ||= {}
78
+ return @cache_class[options[:cache] || self.cache] if @cache_class[options[:cache] || self.cache]
79
+
80
+ @cache_class[options[:cache] || self.cache] = if self.cache
81
+ Cache.const_get((options[:cache] || self.cache).to_s.gsub(/(?:^|_)(.)/) { $1.upcase })
82
+ else
83
+ Cache::NoCache
84
+ end
69
85
  end
70
86
  end
71
87
  end
@@ -43,10 +43,10 @@ module Exchange
43
43
  # #=> #<Exchange::Currency @number=37.0 @currency=:usd @time=#<Time> @from=#<Exchange::Currency @number=40.0 @currency=:usd>>
44
44
 
45
45
  def initialize value, currency, opts={}
46
- @value = value.to_f
46
+ @value = ISO4217.instantiate(value, currency)
47
47
  @currency = currency
48
- @time = assure_time(opts[:at], :default => :now)
49
- @from = opts[:from] if opts[:from]
48
+ @time = Helper.assure_time(opts[:at], :default => :now)
49
+ @from = opts[:from]
50
50
  end
51
51
 
52
52
  # Method missing is used to handle conversions from one currency object to another. It only handles currencies which are available in
@@ -57,9 +57,11 @@ module Exchange
57
57
  # Exchange::Currency.new(40,:nok).to_sek(:at => Time.gm(2012,2,2))
58
58
 
59
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})
60
+ match = method.to_s.match(/\Ato_(\w{3})/)
61
+ if match && Configuration.api_class::CURRENCIES.include?($1)
62
+ return self.convert_to($1, {:at => self.time}.merge(args.first || {}))
63
+ elsif match && ISO4217.definitions.keys.include?($1.upcase)
64
+ raise NoRateError.new("Cannot convert to #{$1} because the defined api does not provide a rate")
63
65
  end
64
66
 
65
67
  self.value.send method, *args, &block
@@ -76,7 +78,7 @@ module Exchange
76
78
  # Exchange::Currency.new(40,:nok).convert_to('sek', :at => Time.gm(2012,2,2))
77
79
 
78
80
  def convert_to other, opts={}
79
- Exchange::Currency.new(Exchange::Configuration.api_class.new.convert(value, currency, other, opts), other, opts.merge(:from => self))
81
+ Currency.new(Configuration.api_class.new.convert(value, currency, other, opts), other, opts.merge(:from => self))
80
82
  end
81
83
 
82
84
  class << self
@@ -86,8 +88,8 @@ module Exchange
86
88
  # @macro [attach] install_operations
87
89
 
88
90
  def install_operation op
89
- define_method op do
90
- @value = self.value.send(op)
91
+ define_method op do |*precision|
92
+ @value = ISO4217.send(op, self.value, self.currency, precision.first)
91
93
  self
92
94
  end
93
95
  end
@@ -99,8 +101,8 @@ module Exchange
99
101
  def base_operation op
100
102
  self.class_eval <<-EOV
101
103
  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
+ #{'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 !Configuration.allow_mixed_operations && other.kind_of?(Currency) && other.currency != self.currency'}
105
+ @value #{op}= other.kind_of?(Currency) ? other.convert_to(self.currency, :at => other.time) : other
104
106
  self
105
107
  end
106
108
  EOV
@@ -108,29 +110,50 @@ module Exchange
108
110
 
109
111
  end
110
112
 
111
- # Round the currency (Equivalent to normal round)
113
+ # Round the currency. Since this is a currency, it will round to the standard decimal value.
114
+ # If you want to round it to another precision, you have to specifically ask for it.
112
115
  # @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
+ # @param [Integer] precision The precision you want the rounding to have. Defaults to the ISO 4217 standard value for the currency
117
+ # @since 0.1
118
+ # @version 0.3
119
+ # @example Round your currency to the iso standard number of decimals
120
+ # Exchange::Currency.new(40.545, :usd).round
121
+ # #=> #<Exchange::Currency @value=40.55 @currency=:usd>
122
+ # @example Round your currency to another number of decimals
123
+ # Exchange::Currency.new(40.545, :usd).round(0)
124
+ # #=> #<Exchange::Currency @value=41 @currency=:usd>
116
125
 
117
126
  install_operation :round
118
127
 
119
128
 
120
- # Ceil the currency (Equivalent to normal ceil)
129
+ # Ceil the currency. Since this is a currency, it will ceil to the standard decimal value.
130
+ # If you want to ceil it to another precision, you have to specifically ask for it.
121
131
  # @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>
132
+ # @param [Integer] precision The precision you want the ceiling to have. Defaults to the ISO 4217 standard value for the currency
133
+ # @since 0.1
134
+ # @version 0.3
135
+ # @example Ceil your currency to the iso standard number of decimals
136
+ # Exchange::Currency.new(40.544, :usd).ceil
137
+ # #=> #<Exchange::Currency @value=40.55 @currency=:usd>
138
+ # @example Ceil your currency to another number of decimals
139
+ # Exchange::Currency.new(40.445, :usd).ceil(0)
140
+ # #=> #<Exchange::Currency @value=41 @currency=:usd>
125
141
 
126
142
  install_operation :ceil
127
143
 
128
144
 
129
- # Floor the currency (Equivalent to normal floor)
145
+ # Floor the currency. Since this is a currency, it will ceil to the standard decimal value.
146
+ # If you want to ceil it to another precision, you have to specifically ask for it.
130
147
  # @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>
148
+ # @param [Integer] precision The precision you want the flooring to have. Defaults to the ISO 4217 standard value for the currency
149
+ # @since 0.1
150
+ # @version 0.3
151
+ # @example Floor your currency to the iso standard number of decimals
152
+ # Exchange::Currency.new(40.545, :usd).floor
153
+ # #=> #<Exchange::Currency @value=40.54 @currency=:usd>
154
+ # @example Floor your currency to another number of decimals
155
+ # Exchange::Currency.new(40.545, :usd).floor(0)
156
+ # #=> #<Exchange::Currency @value=40 @currency=:usd>
134
157
 
135
158
  install_operation :floor
136
159
 
@@ -191,17 +214,42 @@ module Exchange
191
214
 
192
215
  base_operation '/'
193
216
 
217
+ # Compare a currency with another currency or another value. If the other is not an instance of Exchange::Currency, the value
218
+ # of the currency is compared
219
+ # @param [Whatever you want to throw at it] other The counterpart to compare
220
+ # @return [Boolean] true if the other is equal, false if not
221
+ # @example Compare two currencies
222
+ # Exchange::Currency.new(40, :usd) == Exchange::Currency.new(34, :usd) #=> true
223
+ # @example Compare two different currencies, the other will get converted for comparison
224
+ # Exchange::Currency.new(40, :usd) == Exchange::Currency.new(34, :eur) #=> true, will implicitly convert eur to usd at the actual rate
225
+ # @example Compare a currency with a number, the value of the currency will get compared
226
+ # Exchange::Currency.new(35, :usd) == 35 #=> true
227
+
194
228
  def == other
195
229
  if other.is_a?(Exchange::Currency) && other.currency == self.currency
196
- other.value == self.value
230
+ other.round.value == self.round.value
197
231
  elsif other.is_a?(Exchange::Currency)
198
- other.convert_to(self.currency, :at => other.time).value == self.value
232
+ other.convert_to(self.currency, :at => other.time).round.value == self.round.value
199
233
  else
200
234
  self.value == other
201
235
  end
202
236
  end
203
237
 
238
+ # Sortcompare a currency with another currency. If the other is not an instance of Exchange::Currency, the value
239
+ # of the currency is compared. Different currencies will be converted to the comparing instances currency
240
+ # @param [Whatever you want to throw at it] other The counterpart to compare
241
+ # @return [Fixed] a number which can be used for sorting
242
+ # @since 0.3
243
+ # @version 0.3
244
+ # @example Compare two currencies in terms of value
245
+ # Exchange::Currency.new(40, :usd) <=> Exchange::Currency.new(28, :usd) #=> -1
246
+ # @example Compare two different currencies, the other will get converted for comparison
247
+ # Exchange::Currency.new(40, :usd) <=> Exchange::Currency.new(28, :eur) #=> -1
248
+ # @example Sort multiple currencies in an array
249
+ # [1.usd, 1.eur, 1.chf].sort.map(&:currency) #=> [:usd, :chf, :eur]
250
+
204
251
  def <=> other
252
+ # TODO which historic conversion should be used when two are present?
205
253
  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
254
  -1
207
255
  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))
@@ -214,21 +262,27 @@ module Exchange
214
262
 
215
263
  end
216
264
 
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
265
+ # Converts the currency to a string in ISO 4217 standardized format, either with or without the currency. This leaves you
266
+ # with no worries how to display the currency.
267
+ # @since 0.3
268
+ # @version 0.3
269
+ # @param [Symbol] format :currency (default) if you want a string with currency, :amount if you want just the amount.
270
+ # @return [String] The formatted string
271
+ # @example Convert a currency to a string
272
+ # Exchange::Currency.new(49.567, :usd).to_s #=> "USD 49.57"
273
+ # @example Convert a currency without minor to a string
274
+ # Exchange::Currency.new(45, :jpy).to_s #=> "JPY 45"
275
+ # @example Convert a currency with a three decimal minor to a string
276
+ # Exchange::Currency.new(34.34, :omr).to_s #=> "OMR 34.340"
277
+ # @example Convert a currency to a string without the currency
278
+ # Exchange::ISO4217.stringif(34.34, :omr).to_s(:iso) #=> "34.340"
279
+
280
+ def to_s format=:currency
281
+ [
282
+ format == :currency && ISO4217.stringify(self.value, self.currency),
283
+ format == :amount && ISO4217.stringify(self.value, self.currency, :amount_only => true)
284
+ ].detect{|l| l.is_a?(String) }
285
+ end
232
286
 
233
287
  end
234
288