MAINTENANCE DISCONTINUED - LOOKING FOR A NEW MAINTAINER

This project has not seen any major updates since a year, and maintenance will not be continued. If you would like to take on maintenance of this gem, please tweet @beatrichartz

exchange <img src=“https://secure.travis-ci.org/beatrichartz/exchange.png?branch=master” /> <img src=“https://gemnasium.com/beatrichartz/exchange.png” alt=“Dependency Status” /> <img src=“https://codeclimate.com/github/beatrichartz/exchange.png” /> <img src=“https://d2weczhvl823v0.cloudfront.net/beatrichartz/exchange/trend.png”/>

The Exchange Gem gives you easy access to currency functions directly on your Numbers. It is tested against: ruby 2.0, 1.9, ree and rubinius (1.9 and 2.0 mode). Exchange will in future versions take advantage of 2.1 features like refinements, be sure to check this page for updates!

You can use it with just plain ruby projects, in Rails 2 and 3, Sinatra or whatever Framework you like.

<img src=“http://api.flattr.com/button/flattr-badge-large.png” alt=“Flattr this” title=“Flattr this” border=“0” />

Note when using with MRI 2.1.0-p0

MRI 2.1.0-p0 has a confirmed bug in BigDecimal causing it to return wrong results for BigDecimal division when the rvalue is lower than 1. Exchange includes a patch for this, and all tests are running for MRI 2.1.0.

Installation

Bundler / Rails

Add it to your Gemfile

gem "exchange", "~> 1.2.0"

Manually

Just install it as a gem

gem install exchange

Then require it

require 'exchange'

Production Ready

Exchange is production-ready. It is in use on rightclearing.com, a music licensing service worth noticing. If you use this gem in production, drop a note so your app can be listed here.

Features

Easy Conversion

Conversion of currencies does not get any easier

1.in(:eur).to(:usd)

or better for historic dates

1.in(:eur).to(:usd, :at => Time.now - 84600)

Fallbacks for Conversion APIs

Never worry if an API is shortly unavailable – Exchange provides the possibility to fall back to other conversion API’s if the chosen one is currently not available or does not provide a rate for the attempted conversion. Thus it is possible to combine different API’s with incomplete currency sets to have a more complete currency set.

The performance impact of a fallback if a rate is recognizably not provided by an API is minimal, whereas the performance impact on a http connection error can have a bigger impact on performance.

The default fallback mechanism used calls the ECB API if the Xavier Media API is unavailable. You can set your own fallback chain using the API configuration.

Precise Calculation

You may know the deal: Floating Point Errors can cost you money:

(0.29 * 50).round #=> 14, which is incorrect

Whereas

(BigDecimal("0.29") * 50).round #=> 15, which is correct

Exchange uses BigDecimal in all its currency and conversion operations, so you will not be a likely victim for floating point inaccuracies. It even does implicitly convert counterparts of basic operations like (* / - +) to calculate your values safely:

(50.in(:usd) * 0.29).round 0 #=> "USD 15.00"
(0.29 * 50.in(:usd)).round 0 #=> 15

BigDecimal? Sounds slow to me

If performance combined with conversion is your concern, don’t worry. With ruby 1.9.3, you’ll get about the following results for money instantiation (e.g. 1.in(:eur))

1000 operations

Normal Float Operation takes 	0.000196s
Big Decimal Operation takes 	0.001239s
Money gem Operation takes 	0.05348s
Exchange gem Operation takes 	0.035287s

You’re right that Big Decimal is slower than Float operations, but then again, the exchange gem outscores the money gem.

A mixin for typecasting

There may be a need for you to typecast an attribute of an object as money. Exchange features a typecasting mixin, which you can use with Rails, Ohm, Datamapper, or just plain ruby classes to typecast an attribute as money. Use it like this:

class Article
  # make the class method available
  #
  extend Exchange::Typecasting

  # Install the typecasting for price
  #
  money :price, :currency => :currency

  # let's say you have currency set in a unique place
  #
  def currency
    manager.currency
  end
end

Now you’re able to do this:

article = Article.new
article.price #=> will give you money in the currency defined on the manager
article.price = 3.45.in(:usd) #Will implicitly convert the price if manager.currency is not :usd

You can feed the currency option with a proc or a symbol representing the method. For in-depth information about typecasting visit the documentation here

Only one request per day to keep you up to date

You’re hitting the internet only daily to get new rates (hourly updates are available if you’re eager to have the absolutely newest ones)

ISO 4217 Currency formatting

One of the issues with currencies is: You never know the format they should be in. With Exchange, you can just use the currencies to_s method, which takes care of the right format for you. You can either have a string with the currency code in front, or just the amount in the right format

Normal formatting via to_s includes the ISO4217-compatible currency code

1.in(:usd).to_s #=> "USD 1.00"

Specify the symbol format will print out the currency with a symbol. If no symbol is associated with a currency, the fallback used is the normally formatted string with the ISO4217-compatible currency code

1.2.in(:eur).to_s(:symbol) #=> "€1.40"

Specifying the amount format will print out the formatted amount only

1440.4.in(:usd).to_s(:amount) #=> "1,440.40"

Specifying the plain format will print out the amount formatted with just a dot separator

1440.4.in(:usd).to_s(:plain) #=> "1440.40"

As seen above, exchange takes care of the right format for the separators

155_000_000.in(:usd).to_s #=> "USD 155,000,000.00"

Use three great APIs or your own

Three open APIs are already included:

but if you have another API you like to use, it becomes as easy as writing one Class and two methods to use it. Example of a custom API Extension

Use great caches or your own great cache

Use one of three available caching solutions:

  • Memcached via the Dalli gem

  • Redis via the redis gem

  • Rails cache (This gem does however not depend on rails)

But, same here, if you don’t like any of these or want to use your own caching solution, it is as easy as writing one Class and two methods to use it. Example of a custom cache extension

Basic Operations

Convert

Converting one currency to another is as easy as 1,2,3. Don’t be afraid, even if it returns a currency object, all Fixed and Float operations can be applied as method missing routes to the value

1.in(:usd).to(:eur)                                  #=> #<Exchange::Money @value=0.93 @currency=:eur>
2.3.in(:dkk).to(:sek)                                #=> #<Exchange::Money @value=3.33 @currency=:sek>
45.54.in(:nok).to(:sek)                              #=> #<Exchange::Money @value=3.33 @currency=:sek>

Easily convert one currency to another at a historical rate

1.52.in(:usd).to :eur, :at => '2011-01-01'           #=> #<Exchange::Money @value=1.23 @currency=:eur>
3.45.in(:eur).to :sek, :at => Time.gm(2011,3,3)      #=> #<Exchange::Money @value=19.23 @currency=:sek>
345.in(:sek).to :nok, :at => Time.gm(2011,3,3)       #=> #<Exchange::Money @value=348 @currency=:nok>

Or even define an instance of currency as historic by adding a time.

1.52.in(:usd, :at => '2011-01-01').to(:eur)          #=> #<Exchange::Money @value=1.23 @currency=:eur>
3.45.in(:usd, :at => Time.gm(2011,3,3)).to(:sek)     #=> #<Exchange::Money @value=19.23 @currency=:sek>
345.in(:usd, :at => Time.gm(2011,3,3)).to(:nok)      #=> #<Exchange::Money @value=348 @currency=:nok>

Do multiple conversion steps at once (if in any way useful)

3.in(:chf).to(:eur, :at => '2011-02-04').to(:usd)      #=> #<Exchange::Money @value=5.3 @currency=:eur>

Compare

Compare Currencies, they will convert implicitly

2.in(:eur) > 2.in(:usd)                                 #=> true (2.in(:usd) get converted to eur and compared)
2.in(:nok) < 2.in(:sek)                                 #=> false (2.in(:sek) get converted to nok and compared)
5.in(:eur) == 4.34.in(:chf)                             #=> true
50.in(:eur) == 4.34.in(:chf)                            #=> false
50.in(:eur).to(:sek) == 50.in(:eur)                     #=> true
50.in(:eur, :at => '2011-1-1') == 50.in(:sek)           #=> false

Sort multiple currencies at once

[5.in(:eur), 4.in(:usd), 4.in(:chf, :at => '2010-01-01')].sort   #=> [#<Exchange::Money @value=4 @currency=:usd>, #<Exchange::Money @value=4 @currency=:chf>, #<Exchange::Money @value=5 @currency=:eur>]

This is true, because it uses the same historic conversion rate

3.in(:eur, :at => '201-01-01').to(:usd) == 3.in(:eur).to(:usd, :at => '201-01-01')

But this is false, obviously, because the second instance uses the present exchange rate which differs from the historic one (if the two rates match, this will be true again)

3.in(:eur, :at => '2001-01-01').to(:usd) == 3.in(:eur).to(:usd)

Operate

Add, Subtract, Multiply, Divide Currencies and don’t lose a dime. The result will get returned in the currency of the first argument

1.in(:usd) + 1.32.in(:eur)                              #=> #<Exchange::Money @value=2.54 @currency=:usd>
1.in(:usd) - 1.32.in(:eur)                              #=> #<Exchange::Money @value=-0.2 @currency=:usd>
1.in(:usd) * 1.32.in(:eur)                              #=> #<Exchange::Money @value=3.44 @currency=:usd>
1.in(:usd) / 1.32.in(:eur)                              #=> #<Exchange::Money @value=0.89 @currency=:usd>

If you define a currency object as historic. It will use historic conversion if it gets converted (in this example, the 1.32 eur will get converted to usd at the rate of January 1 2008)

1.in(:usd) - 1.32.in(:eur, :at => '2008-1-1')           #=> #<Exchange::Money @value=2.54 @currency=:usd>

You can just instantiate currencies and apply operations. Rounding will by default round the currency to its ISO decimal precision:

3.123.in(:eur).round                               #=> #<Exchange::Money @value=3.12 @currency=:eur>

You can also pass the precision you wish for as an argument, round, ceil, floor act like normal:

3.1234.in(:eur).round(0)                           #=> #<Exchange::Money @value=3 @currency=:eur>

Convert one currency to another and round, ceil or floor it, it still retains currency information of the actual and previous currency

1.34.in(:usd).to(:eur).round(0)                      #=> #<Exchange::Money @value=1 @currency=:eur>
10.34.in(:usd).to(:nok).ceil(0)                      #=> #<Exchange::Money @value=45 @currency=:nok>
5.34.in(:usd).to(:eur).floor(0)                      #=> #<Exchange::Money @value=4 @currency=:eur>
5.34.in(:usd).to(:eur).floor.from                    #=> #<Exchange::Money @value=5.34 @currency=:usd>

Psychological Pricing

You can apply psychological pricing by passing the psych argument to the rounding operation

10.345.in(:usd).round(:psych)                       #=> 9.99
9.999.in(:eur).floor(:psych)                        #=> 8.99
10.345.in(:omr).ceil(:psych)                        #=> 10.999
76.in(:jpy).floor(:psych)                           #=> 69

Retain Information

Access the original currency and its value after conversion, even over multiple steps

converted = 2.in(:eur).to(:usd)                      #=> #<Exchange::Money @value=2.12 @currency=:usd>
converted.from                                       #=> #<Exchange::Money @value=2 @currency=:eur>
converted2 = converted.to(:nok)                      #=> #<Exchange::Money @value=22.12 @currency=:nok>
converted2.from                                      #=> #<Exchange::Money @value=2.12 @currency=:usd>

Configuration

You can configure the exchange gem to a variety of options, allowing you to control restrictions on operations, caching and which API the gem uses. Just set the configuration with

Exchange.configuration = Exchange::Configuration.new do |c|
  # your configuration goes here
end

Options

The options available are

:cache                                    Takes the cache options as a hash, the options are:
  :subclass (default :memory)             The cache subclass to use for caching. Available: Memory, Rails cache, Redis, Memcached, File
  :host (default '127.0.0.1')             A string with the hostname or IP to set the cache host to. Does not have to be set for Rails cache
  :port (default 11211)                   An integer for the cache port. Does not have to be set for Rails cache
  :expire (default :daily)                Which period is used to expire the cache, :daily or :hourly are available

:api                                      Takes the conversion api as a hash, the options are:
  :subclass (default :xavier_media)         The api to use. Available: Xavier Media, ECB, Currency Bot
  :retries (default 5)                      The number of times the gem should retry to connect to the api host
  :app_id (default nil)                     The app id to use with your api request
  :protocol (default :http)                 The protocol to use with the request

:implicit_conversions (default true)      If set to false, Operations with with different currencies raise errors.

If you want to maintain control over when a currency is converted, turn implicit conversions off

Exchange.configuration.implicit_conversions = false
1.in(:usd) + 1.in(:eur)                                 #=> raises ImplicitConversionDenied

Caching Options

In Key/Value stores, exchange will cache the API files with a key starting with ‘exchange_’

Use Memory to cache the result (default).

Exchange.configuration = Exchange::Configuration.new do |c|
  c.cache = {
    :subclass => :memory
  }
end

Use Memcached to cache the result.

Exchange.configuration = Exchange::Configuration.new do |c|
  c.cache = {
    :subclass => :memcached,
    :host     => 'yourhost',
    :port     => 2434, #yourport
  }
end

Use Redis to cache the result.

Exchange.configuration = Exchange::Configuration.new do |c|
  c.cache = {
    :subclass => :redis,
    :host     => 'yourhost',
    :port     => 2434, #yourport
  }
end

Use Rails to cache the result.

Exchange.configuration = Exchange::Configuration.new do |c|
  c.cache = {
    :subclass => :rails
  }
end

API Options

Use the Xaviermedia API

Exchange.configuration = Exchange::Configuration.new do |c|
  c.api = {
    :subclass => :xavier_media
  }
end

Use the open exchange rates Open Source API

Exchange.configuration = Exchange::Configuration.new do |c|
  c.api = {
    :subclass => :open_exchange_rates,
    :app_id => "Your open exchange rates app id"
  }
end

Use https as request protocol of your api requests:

Exchange.configuration = Exchange::Configuration.new do |c|
  c.api = {
    :protocol => :https
  }
end

Connect your own API and Cache

Your own API

Easily connect to your custom API by writing an ExternalAPI Class, or use your own caching solution to cache. Please note that only open source APIs can be accepted as contributions to this gem. Private / Premium APIs have to be written as your own.

module Exchange
  module ExternalAPI

    # Inherit from Json to write for a json api, and the json gem is automatically loaded
    # Inherit from XML to write for an xml api, and nokogiri is automatically loaded
    #
    class MyCustom < Json

      # Define here which currencies your API can handle
      #
      CURRENCIES = %W(usd chf).map(&:to_sym)

      # Every instance of ExternalAPI Class has to have an update function which
      # gets the rates from the API
      #
      def update(opts={})

        # assure that you will get a Time object for the historical dates
        #
        time = helper.assure_time(opts[:at])

        # Call your API (shown here with a helper function that builds your API URL).
        # Like this, your calls will get cached.
        #
        Call.new(api_url(time), :at => time) do |result|

        # Assign the currency conversion base.
        # Attention, this is readonly, self.base= won't work
        #
        @base                 = result['base']

        # assign the rates, this has to be a hash with the following format:
        # {'USD' => 1.23242, 'CHF' => 1.34323}.
        #
        # Attention, this is readonly, self.rates= won't work
        #
        @rates                = result['rates']

        # Timestamp the api call result. This may come in handy to assure you have
        # the right result.
        #
        # Attention, this is readonly, self.timestamp= won't work
        #
        @timestamp            = result['timestamp'].to_i

      end

      private

        def api_url(time)
          # code a helper function that builds your api url for the specified time
        end

    end
  end
end

Now, you can configure your API in the configuration. The Symbol will get camelcased and constantized

Exchange::Configuration.api.subclass = :my_custom

# Have fun, and don’t forget to write tests.

Have fun, and don’t forget to write tests.

Your own Cache

Write your own caching module to use the gem with your own custom caching solution.

module Cache
  class MyCustomCache < Base
    # A cache class has to have the method "cached".
    # The cache Base is a singleton and forwards the method "cached"
    # to the instance
    #
    def cached api, opts={}, &block
      # generate the storage with key(api, opts[:at]) and you will get a
      # unique key to store in your cache
      #
      # Your code goes here
    end
  end
end

Now, you can configure your Caching solution in the configuration. The Symbol will get camelcased and constantized

Exchange.configuration.cache.subclass = :my_custom_cache

Have fun, and don’t forget to write tests.

Contributing to exchange

Please note that only open source APIs can be accepted as contributions to this gem. Private / Premium APIs have to be written as your own extension and will not be added to the gem code.

  • Check out the latest master to make sure the feature hasn’t been implemented or the bug hasn’t been fixed yet.

  • Check out the issue tracker to make sure someone already hasn’t requested it and/or contributed it.

  • Fork the project.

  • Start a feature/bugfix branch.

  • Commit and push until you are happy with your contribution.

  • Make sure to add tests for it. This is important so I don’t break it in a future version unintentionally.

  • Make sure to add documentation for it. This is important so everyone else can see what your code can do.

  • Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.

Copyright © 2013 Beat Richartz. See LICENSE.txt for further details.