exchange 0.2.6 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -2,3 +2,4 @@ require 'exchange/external_api/base'
2
2
  require 'exchange/external_api/call'
3
3
  require 'exchange/external_api/currency_bot'
4
4
  require 'exchange/external_api/xavier_media'
5
+ require 'exchange/external_api/ecb'
@@ -1,4 +1,5 @@
1
1
  module Exchange
2
+
2
3
  # The external API module. Every class Handling an API has to be placed here and inherit from base. It has to call an api and define
3
4
  # a rates hash, an exchange base and a unix timestamp. The call will get cached automatically with the right structure
4
5
  # Allows for easy extension with an own api, as shown below
@@ -15,7 +16,7 @@ module Exchange
15
16
  # # Every instance of ExternalAPI Class has to have an update function which gets the rates from the API
16
17
  # def update(opts={})
17
18
  # # assure that you will get a Time object for the historical dates
18
- # time = assure_time(opts[:at])
19
+ # time = Exchange::Helper.assure_time(opts[:at])
19
20
  #
20
21
  # # call your API (shown here with a helper function that builds your API URL). Like this, your calls will get cached.
21
22
  # Call.new(api_url(time), :at => time) do |result|
@@ -76,8 +77,14 @@ module Exchange
76
77
  # Exchange::ExternalAPI::Base.new.rate(:usd, :eur, :at => Time.gm(3,23,2009))
77
78
  # #=> 1.232231231
78
79
  def rate(from, to, opts={})
79
- update(opts)
80
- self.rates[to.to_s.upcase] / self.rates[from.to_s.upcase]
80
+ rate = Configuration.cache_class.cached(Exchange::Configuration.api, opts.merge(:key_for => [from, to])) do
81
+ update(opts)
82
+ rate_from = self.rates[to.to_s.upcase]
83
+ rate_to = self.rates[from.to_s.upcase]
84
+ raise NoRateError.new("No rates where found for #{from} to #{to} #{'at ' + opts[:at].to_s if opts[:at]}") unless rate_from && rate_to
85
+ rate_from / rate_to
86
+ end
87
+ BigDecimal.new(rate.to_s)
81
88
  end
82
89
 
83
90
  # Converts an amount of one currency into another
@@ -91,23 +98,8 @@ module Exchange
91
98
  # Exchange::ExternalAPI::Base.new.convert(23, :eur, :chf, :at => Time.gm(12,1,2011))
92
99
  # #=> 30.12
93
100
  def convert(amount, from, to, opts={})
94
- (amount.to_f * rate(from, to, opts) * 100).round.to_f / 100
101
+ amount * rate(from, to, opts)
95
102
  end
96
-
97
- protected
98
-
99
- # A helper function to assure a value is an instance of time
100
- # @param [Time, String, NilClass] The value to be asserted
101
- # @param [Hash] opts Options for assertion
102
- # @option opts [Symbol] :default If the argument is nil, you can define :default as :now to be delivered with Time.now instead of nil
103
-
104
- def assure_time(arg=nil, opts={})
105
- if arg
106
- arg.kind_of?(Time) ? arg : Time.gm(*arg.split('-'))
107
- elsif opts[:default] == :now
108
- Time.now
109
- end
110
- end
111
103
 
112
104
  end
113
105
  end
@@ -12,6 +12,7 @@ module Exchange
12
12
  # @param [String] url The url of the API to call
13
13
  # @param [Hash] options The options of the API call
14
14
  # @option options [Time] :at The time of the historical exchange rate file to get
15
+ # @option options [Class] :api The class to generate the key for
15
16
  # @option options [Integer] :retries The number of retries if the API Call should fail with a HTTP Error
16
17
  # @option options [Array] :retry_with an Array of urls to retry the call with (if the API does not have a file for the specified date). These values will be shifted until a call succeeds or the number of maximum retries is reached.
17
18
  # @option options [Symbol] :format The format to return / yield the API call result in, defaults to :json
@@ -30,19 +31,14 @@ module Exchange
30
31
  # # Do something with that result
31
32
 
32
33
  def initialize url, options={}, &block
33
- if Exchange::Configuration.cache
34
- result = Exchange::Configuration.cache_class.cached(Exchange::Configuration.api_class, :at => options[:at]) do
35
- load_url(url, options[:retries] || 5, options[:retry_with])
36
- end
37
- else
38
- result = load_url(url, options[:retries] || 5, options[:retry_with])
34
+ result = Configuration.cache_class(options).cached(options[:api] || Configuration.api, options) do
35
+ load_url(url, options[:retries] || Exchange::Configuration.retries, options[:retry_with])
39
36
  end
40
37
 
41
38
  parsed = options[:format] == :xml ? Nokogiri.parse(result) : JSON.load(result)
42
39
 
43
40
  return parsed unless block_given?
44
-
45
- yield parsed
41
+ yield parsed
46
42
  end
47
43
 
48
44
  private
@@ -52,20 +48,22 @@ module Exchange
52
48
  # @param [Integer] retries The number of retries to do if the API Call should fail with a HTTP Error
53
49
  # @param [Array] retry_with An array of urls to retry the API call with if the call to the original URL should fail. These values will be shifted until a call succeeds or the number of maximum retries is reached
54
50
 
55
- def load_url(url, retries, retry_with)
56
- begin
51
+ def load_url(url, retries, retry_with)
52
+ begin
57
53
  result = URI.parse(url).open.read
58
54
  rescue SocketError
55
+ puts 'SocketError'
59
56
  raise APIError.new("Calling API #{url} produced a socket error")
60
- rescue OpenURI::HTTPError
57
+ rescue OpenURI::HTTPError => e
58
+ retries -= 1
61
59
  if retries > 0
62
- retries -= 1
63
60
  url = retry_with.shift if retry_with && !retry_with.empty?
64
61
  retry
65
62
  else
66
63
  raise APIError.new("API #{url} was not reachable")
67
64
  end
68
65
  end
66
+ result
69
67
  end
70
68
 
71
69
  end
@@ -20,11 +20,11 @@ module Exchange
20
20
  # Exchange::ExternalAPI::CurrencyBot.new.update(:at => Time.gm(3,2,2010))
21
21
 
22
22
  def update(opts={})
23
- time = assure_time(opts[:at])
23
+ time = Exchange::Helper.assure_time(opts[:at])
24
24
 
25
25
  Call.new(api_url(time), :at => time) do |result|
26
26
  @base = result['base']
27
- @rates = result['rates']
27
+ @rates = Hash[*result['rates'].keys.zip(result['rates'].values.map{|v| BigDecimal.new(v.to_s) }).flatten]
28
28
  @timestamp = result['timestamp'].to_i
29
29
  end
30
30
  end
@@ -0,0 +1,68 @@
1
+ module Exchange
2
+ module ExternalAPI
3
+
4
+ # The ECB class, handling communication with the European Central Bank XML File API
5
+ # You can find further information on the European Central Bank XML API API here: http://www.ecb.int/stats/exchange/eurofxref/html/index.en.html
6
+ # @author Beat Richartz
7
+ # @version 0.3
8
+ # @since 0.3
9
+
10
+ class ECB < Base
11
+ # The base of the ECB API URL
12
+ API_URL = "http://www.ecb.europa.eu/stats/eurofxref"
13
+ # The currencies the ECB API URL can handle
14
+ CURRENCIES = %W(eur usd jpy bgn czk dkk gbp huf ltl lvl pln ron sek chf nok hrk rub try aud brl cad cny hkd idr ils inr krw mxn myr nzd php sgd thb zar)
15
+
16
+ attr_accessor :callresult
17
+
18
+ # Updates the rates by getting the information from ECB API for today or a defined historical date
19
+ # The call gets cached for a maximum of 24 hours. Getting history from ECB is a bit special, since they do not seem to have
20
+ # any smaller portion history than an epic 4MB XML history file and a 90 day recent history file. We get each of that once and cache it in smaller portions.
21
+ # @param [Hash] opts Options to define for the API Call
22
+ # @option opts [Time, String] :at a historical date to get the exchange rates for
23
+ # @example Update the currency bot API to use the file of March 2, 2010
24
+ # Exchange::ExternalAPI::XavierMedia.new.update(:at => Time.gm(3,2,2010))
25
+
26
+ def update(opts={})
27
+ time = Exchange::Helper.assure_time(opts[:at], :default => :now)
28
+ api_url = api_url(time)
29
+ times = Exchange::Configuration.retries.times.map{ |i| time - 86400 * (i+1) }
30
+
31
+ Kernel.warn "WARNING: Using the ECB API without caching can be very, very slow." unless Configuration.cache
32
+
33
+ Configuration.cache_class.cached(self.class, :at => time) do
34
+ Call.new(api_url, :format => :xml, :at => time, :cache => :file, :cache_period => time >= Time.now - 90 * 86400 ? :daily : :monthly) do |result|
35
+ t = time
36
+ while (r = result.css("Cube[time=\"#{t.strftime("%Y-%m-%d")}\"]")).empty? && !times.empty?
37
+ t = times.shift
38
+ end
39
+ @callresult = r.to_s
40
+ end
41
+ end
42
+
43
+ parsed = Nokogiri.parse(self.callresult)
44
+
45
+ @base = 'EUR' # We just have to assume, since it's the ECB
46
+ @rates = Hash[*(['EUR', BigDecimal.new("1")] + parsed.children.children.map {|c| c.attributes.values.map{|v| v.value.match(/\d/) ? BigDecimal.new(v.value) : v.value }.sort_by(&:to_s).reverse unless c.attributes.values.empty? }.compact.flatten)]
47
+ @timestamp = time.to_i
48
+ end
49
+
50
+ private
51
+
52
+ # A helper function which build a valid api url for the specified time
53
+ # If the date is today, get the small daily file. If it is less than 90 days ago, get the 90 days file.
54
+ # If it is more than 90 days ago, get the big file
55
+ # @param [Time] time The exchange rate date for which the URL should be built
56
+ # @return [String] An ECB API URL to get the xml from
57
+
58
+ def api_url(time)
59
+ border = Time.now - 90 * 86400
60
+ [
61
+ API_URL,
62
+ border <= time ? 'eurofxref-hist-90d.xml' : 'eurofxref-hist.xml'
63
+ ].join('/')
64
+ end
65
+
66
+ end
67
+ end
68
+ end
@@ -15,19 +15,20 @@ module Exchange
15
15
 
16
16
  # Updates the rates by getting the information from Xaviermedia API for today or a defined historical date
17
17
  # The call gets cached for a maximum of 24 hours.
18
+ # @version 0.3
18
19
  # @param [Hash] opts Options to define for the API Call
19
20
  # @option opts [Time, String] :at a historical date to get the exchange rates for
20
21
  # @example Update the currency bot API to use the file of March 2, 2010
21
22
  # Exchange::ExternalAPI::XavierMedia.new.update(:at => Time.gm(3,2,2010))
22
23
 
23
24
  def update(opts={})
24
- time = assure_time(opts[:at], :default => :now)
25
+ time = Exchange::Helper.assure_time(opts[:at], :default => :now)
25
26
  api_url = api_url(time)
26
- retry_urls = [api_url(time - 86400), api_url(time - 172800), api_url(time - 259200)]
27
+ retry_urls = Exchange::Configuration.retries.times.map{ |i| api_url(time - 86400 * (i+1)) }
27
28
 
28
29
  Call.new(api_url, :format => :xml, :at => time, :retry_with => retry_urls) do |result|
29
30
  @base = result.css('basecurrency').children[0].to_s
30
- @rates = Hash[*result.css('fx currency_code').children.map(&:to_s).zip(result.css('fx rate').children.map{|c| c.to_s.to_f }).flatten]
31
+ @rates = Hash[*result.css('fx currency_code').children.map(&:to_s).zip(result.css('fx rate').children.map{|c| BigDecimal.new(c.to_s) }).flatten]
31
32
  @timestamp = Time.gm(*result.css('fx_date').children[0].to_s.split('-')).to_i
32
33
  end
33
34
  end
@@ -0,0 +1,27 @@
1
+ module Exchange
2
+ # Helper Functions that get used throughout the gem can be placed here
3
+ # @author Beat Richartz
4
+ # @version 0.3
5
+ # @since 0.3
6
+
7
+ class Helper
8
+ class << self
9
+
10
+ # A helper function to assure a value is an instance of time
11
+ # @param [Time, String, NilClass] The value to be asserted
12
+ # @param [Hash] opts Options for assertion
13
+ # @option opts [Symbol] :default a method that can be sent to Time if the argument is nil (:now for example)
14
+
15
+ def assure_time(arg=nil, opts={})
16
+ if arg
17
+ arg.kind_of?(Time) ? arg : Time.gm(*arg.split('-'))
18
+ elsif opts[:default]
19
+ Time.send(opts[:default])
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,95 @@
1
+ module Exchange
2
+
3
+ # This class handles everything that has to do with certified formatting of the different currencies. The standard is stored in
4
+ # the iso4217 YAML file.
5
+ # @version 0.3
6
+ # @since 0.3
7
+ # @author Beat Richartz
8
+
9
+ class ISO4217
10
+ class << self
11
+
12
+ # The ISO 4217 that have to be loaded. Nothing much to say here. Just use this method to get to the definitions
13
+ # They are static, so they can be stored in a class variable without many worries
14
+ # @return [Hash] The iso427 Definitions with the currency code as keys
15
+
16
+ def definitions
17
+ @@definitions ||= YAML.load_file('iso4217.yml')
18
+ end
19
+
20
+ # Use this to instantiate a currency amount. For one, it is important that we use BigDecimal here so nothing gets lost because
21
+ # of floating point errors. For the other, This allows us to set the precision exactly according to the iso definition
22
+ # @param [BigDecimal, Fixed, Float, String] amount The amount of money you want to instantiate
23
+ # @param [String, Symbol] currency The currency you want to instantiate the money in
24
+ # @return [BigDecimal] The instantiated currency
25
+ # @example instantiate a currency from a string
26
+ # Exchange::ISO4217.instantiate("4523", "usd") #=> #<Bigdecimal 4523.00>
27
+
28
+ def instantiate(amount, currency)
29
+ BigDecimal.new(amount.to_s, definitions[currency.to_s.upcase]['minor_unit'])
30
+ end
31
+
32
+ # Converts the currency to a string in ISO 4217 standardized format, either with or without the currency. This leaves you
33
+ # with no worries how to display the currency.
34
+ # @param [BigDecimal, Fixed, Float] amount The amount of currency you want to stringify
35
+ # @param [String, Symbol] currency The currency you want to stringify
36
+ # @param [Hash] opts The options for formatting
37
+ # @option opts [Boolean] :amount_only Whether you want to have the currency in the string or not
38
+ # @return [String] The formatted string
39
+ # @example Convert a currency to a string
40
+ # Exchange::ISO4217.stringify(49.567, :usd) #=> "USD 49.57"
41
+ # @example Convert a currency without minor to a string
42
+ # Exchange::ISO4217.stringif(45, :jpy) #=> "JPY 45"
43
+ # @example Convert a currency with a three decimal minor to a string
44
+ # Exchange::ISO4217.stringif(34.34, :omr) #=> "OMR 34.340"
45
+ # @example Convert a currency to a string without the currency
46
+ # Exchange::ISO4217.stringif(34.34, :omr, :amount_only => true) #=> "34.340"
47
+
48
+ def stringify(amount, currency, opts={})
49
+ format = "%.#{definitions[currency.to_s.upcase]['minor_unit']}f"
50
+ "#{currency.to_s.upcase + ' ' unless opts[:amount_only]}#{format % amount}"
51
+ end
52
+
53
+ private
54
+ # @private
55
+ # @macro [attach] install_operations
56
+
57
+ def install_operation op
58
+ self.class_eval <<-EOV
59
+ def self.#{op}(amount, currency, precision=nil)
60
+ minor = definitions[currency.to_s.upcase]['minor_unit']
61
+ (amount.is_a?(BigDecimal) ? amount : BigDecimal.new(amount.to_s, minor)).#{op}(precision || minor)
62
+ end
63
+ EOV
64
+ end
65
+ end
66
+
67
+ # Use this to round a currency amount. This allows us to round exactly to the number of minors the currency has in the
68
+ # iso definition
69
+ # @param [BigDecimal, Fixed, Float, String] amount The amount of money you want to round
70
+ # @param [String, Symbol] currency The currency you want to round the money in
71
+ # @example Round a currency with 2 minors
72
+ # Exchange::ISO4217.round("4523.456", "usd") #=> #<Bigdecimal 4523.46>
73
+
74
+ install_operation :round
75
+
76
+ # Use this to ceil a currency amount. This allows us to ceil exactly to the number of minors the currency has in the
77
+ # iso definition
78
+ # @param [BigDecimal, Fixed, Float, String] amount The amount of money you want to ceil
79
+ # @param [String, Symbol] currency The currency you want to ceil the money in
80
+ # @example Ceil a currency with 2 minors
81
+ # Exchange::ISO4217.ceil("4523.456", "usd") #=> #<Bigdecimal 4523.46>
82
+
83
+ install_operation :ceil
84
+
85
+ # Use this to floor a currency amount. This allows us to floor exactly to the number of minors the currency has in the
86
+ # iso definition
87
+ # @param [BigDecimal, Fixed, Float, String] amount The amount of money you want to floor
88
+ # @param [String, Symbol] currency The currency you want to floor the money in
89
+ # @example Floor a currency with 2 minors
90
+ # Exchange::ISO4217.floor("4523.456", "usd") #=> #<Bigdecimal 4523.46>
91
+
92
+ install_operation :floor
93
+
94
+ end
95
+ end
@@ -7,6 +7,13 @@ describe "Exchange::Conversability" do
7
7
  after(:all) do
8
8
  Exchange::Configuration.cache = :memcached
9
9
  end
10
+ it "should define all currencies on Fixnum, Float and BigDecimal" do
11
+ Exchange::ISO4217.definitions.keys.each do |c|
12
+ 1.should be_respond_to(c.downcase.to_sym)
13
+ 1.1.should be_respond_to(c.downcase.to_sym)
14
+ BigDecimal.new("1").should be_respond_to(c.downcase.to_sym)
15
+ end
16
+ end
10
17
  context "with a fixnum" do
11
18
  it "should allow to convert to a currency" do
12
19
  3.eur.should be_kind_of Exchange::Currency
@@ -19,13 +26,13 @@ describe "Exchange::Conversability" do
19
26
  it "should allow to do full conversions" do
20
27
  mock_api("https://raw.github.com/currencybot/open-exchange-rates/master/latest.json", fixture('api_responses/example_json_api.json'), 3)
21
28
  3.eur.to_chf.should be_kind_of Exchange::Currency
22
- 3.eur.to_chf.value.should == 3.62
29
+ 3.eur.to_chf.value.round(2).should == 3.62
23
30
  3.eur.to_chf.currency.should == 'chf'
24
31
  end
25
32
  it "should allow to do full conversions with negative numbers" do
26
33
  mock_api("https://raw.github.com/currencybot/open-exchange-rates/master/latest.json", fixture('api_responses/example_json_api.json'), 3)
27
34
  -3.eur.to_chf.should be_kind_of Exchange::Currency
28
- -3.eur.to_chf.value.should == -3.62
35
+ -3.eur.to_chf.value.round(2).should == -3.62
29
36
  -3.eur.to_chf.currency.should == 'chf'
30
37
  end
31
38
  it "should allow to define a historic time in which the currency should be interpreted" do
@@ -37,22 +44,22 @@ describe "Exchange::Conversability" do
37
44
  context "with a float" do
38
45
  it "should allow to convert to a currency" do
39
46
  3.25.eur.should be_kind_of Exchange::Currency
40
- 3.25.eur.value.should == 3.25
47
+ 3.25.eur.value.round(2).should == 3.25
41
48
  end
42
49
  it "should allow to convert to a curreny with a negative number" do
43
50
  -3.25.eur.should be_kind_of Exchange::Currency
44
- -3.25.eur.value.should == -3.25
51
+ -3.25.eur.value.round(2).should == -3.25
45
52
  end
46
53
  it "should allow to do full conversions" do
47
54
  mock_api("https://raw.github.com/currencybot/open-exchange-rates/master/latest.json", fixture('api_responses/example_json_api.json'), 3)
48
55
  3.25.eur.to_chf.should be_kind_of Exchange::Currency
49
- 3.25.eur.to_chf.value.should == 3.92
56
+ 3.25.eur.to_chf.value.round(2).should == 3.92
50
57
  3.25.eur.to_chf.currency.should == 'chf'
51
58
  end
52
59
  it "should allow to do full conversions with negative numbers" do
53
60
  mock_api("https://raw.github.com/currencybot/open-exchange-rates/master/latest.json", fixture('api_responses/example_json_api.json'), 3)
54
61
  -3.25.eur.to_chf.should be_kind_of Exchange::Currency
55
- -3.25.eur.to_chf.value.should == -3.92
62
+ -3.25.eur.to_chf.value.round(2).should == -3.92
56
63
  -3.25.eur.to_chf.currency.should == 'chf'
57
64
  end
58
65
  it "should allow to define a historic time in which the currency should be interpreted" do
@@ -61,4 +68,31 @@ describe "Exchange::Conversability" do
61
68
  3.25.chf(:at => '2010-01-01').time.year.should == 2010
62
69
  end
63
70
  end
71
+ context "with a big decimal" do
72
+ it "should allow to convert to a currency" do
73
+ BigDecimal.new("3.25").eur.should be_kind_of Exchange::Currency
74
+ BigDecimal.new("3.25").eur.value.round(2).should == 3.25
75
+ end
76
+ it "should allow to convert to a curreny with a negative number" do
77
+ BigDecimal.new("-3.25").eur.should be_kind_of Exchange::Currency
78
+ BigDecimal.new("-3.25").eur.value.round(2).should == -3.25
79
+ end
80
+ it "should allow to do full conversions" do
81
+ mock_api("https://raw.github.com/currencybot/open-exchange-rates/master/latest.json", fixture('api_responses/example_json_api.json'), 3)
82
+ BigDecimal.new("3.25").eur.to_chf.should be_kind_of Exchange::Currency
83
+ BigDecimal.new("3.25").eur.to_chf.value.round(2).should == 3.92
84
+ BigDecimal.new("3.25").eur.to_chf.currency.should == 'chf'
85
+ end
86
+ it "should allow to do full conversions with negative numbers" do
87
+ mock_api("https://raw.github.com/currencybot/open-exchange-rates/master/latest.json", fixture('api_responses/example_json_api.json'), 3)
88
+ BigDecimal.new("-3.25").eur.to_chf.should be_kind_of Exchange::Currency
89
+ BigDecimal.new("-3.25").eur.to_chf.value.round(2).should == -3.92
90
+ BigDecimal.new("-3.25").eur.to_chf.currency.should == 'chf'
91
+ end
92
+ it "should allow to define a historic time in which the currency should be interpreted" do
93
+ BigDecimal.new("3.25").chf(:at => Time.gm(2010,1,1)).time.yday.should == 1
94
+ BigDecimal.new("3.25").chf(:at => Time.gm(2010,1,1)).time.year.should == 2010
95
+ BigDecimal.new("3.25").chf(:at => '2010-01-01').time.year.should == 2010
96
+ end
97
+ end
64
98
  end
@@ -9,8 +9,8 @@ describe "Exchange::Cache::Base" do
9
9
  end
10
10
  context "with a daily cache" do
11
11
  it "should build a timestamped key with the class given, the yearday and the year" do
12
- Exchange::Cache::Base.send(:key, Exchange::ExternalAPI::XavierMedia).should == 'Exchange_ExternalAPI_XavierMedia_2012_61'
13
- Exchange::Cache::Base.send(:key, Exchange::ExternalAPI::CurrencyBot).should == 'Exchange_ExternalAPI_CurrencyBot_2012_61'
12
+ Exchange::Cache::Base.send(:key, :xavier_media).should == 'exchange_xavier_media_2012_61'
13
+ Exchange::Cache::Base.send(:key, :currency_bot).should == 'exchange_currency_bot_2012_61'
14
14
  end
15
15
  end
16
16
  context "with an hourly cache" do
@@ -21,8 +21,8 @@ describe "Exchange::Cache::Base" do
21
21
  Exchange::Configuration.update = :daily
22
22
  end
23
23
  it "should build a timestamped key with the class given, the yearday, the year and the hour" do
24
- Exchange::Cache::Base.send(:key, Exchange::ExternalAPI::XavierMedia).should == 'Exchange_ExternalAPI_XavierMedia_2012_61_23'
25
- Exchange::Cache::Base.send(:key, Exchange::ExternalAPI::CurrencyBot).should == 'Exchange_ExternalAPI_CurrencyBot_2012_61_23'
24
+ Exchange::Cache::Base.send(:key, :xavier_media).should == 'exchange_xavier_media_2012_61_23'
25
+ Exchange::Cache::Base.send(:key, :currency_bot).should == 'exchange_currency_bot_2012_61_23'
26
26
  end
27
27
  end
28
28
  end