Exchange


Exchange is a library helping you to deal with money in your ruby application. It features an intuitive DSL, measures to prevent floating point errors, currency conversion, typecasting, formatting options and more. It's goal is to make your life easier by dealing with money in decimal format with the precision of your choice and relieving you of most of the pitfalls involved in money handling.

Exchange works with MRI 1.9, 2.0, 2.1 (including a Patch to fix a bug with Divisions for BigDecimal), REE as well as jRuby in 1.9 and 2.0 mode and Rubinius.

Installation

$ gem install exchange

Basics

Instantiate a currency

1.in(:usd)

You can also instantiate currencies from ISO 3166 country codes

1.in(:it) #=> same as 1.in(:eur)

Convert a currency to another

1.2.in(:eur).to(:jpy)

to_s converts the currency to a string in ISO4217-compatible format, complete with the right precision and the ISO standard currency abbreviation. You can also choose to only print the amount or to get a string with a currency symbol rather than the abbreviation by passing an argument.

220.2.in(:jpy).to_s #=> "JPY 220"
245.934.in(:usd).to_s(:amount) #=> "245.93"
245.934.in(:usd).to_s(:symbol) #=> "$245.93"

Apply any method you would apply to a normal instance of numeric. Method missing is routed to the currency value.

1.2.in(:eur).zero?

Conversion

Convert at today's rate

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

Convert at yesterday's rate

1.2.in(:eur).to(:jpy, at: Time.now - 86400)

Currencies can be defined with a timestamp which is applied whenever a conversion takes place. In the example, the defined currency is always converted using the rates of January 2, 2000

currency = 1.2.in(:eur, at: Time.gm(2000,1,2))
currency.to(:jpy)
currency.to(:omr)

Exchange keeps a the instance the currency was converted from for information purposes

1.in(:usd).to(:eur).from == 1.in(:usd) #=> true

Fallbacks

Never worry if an API is shortly unavailable – Exchange provides the possibility to fall back to other conversion API's if 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 caused by a http connection error is significantly larger.

The default fallback mechanism used calls the ECB API if the Xavier Media API is unavailable. If both are unavailable, an error is raised. You can set your own fallback chain using the API configuration.

Formatting

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 amount format will print out the unformatted amount only, using decimal notation

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"

Rounding

Round, ceil or floor without an argument always rounds to the precision defined by ISO4217. Apply an argument to get rounding to a certain amount of decimals.

10.345.in(:usd).round #=> 10.35
9.999.in(:eur).floor #=> 9.99
10.345.in(:usd).round(0) #=> 10
9.999.in(:eur).floor(1) #=> 9.9

You can apply psychological rounding 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

Arithmetics

Use basic arithmetic operations with different types of currencies. If two different currencies are used, the currency right of the expression is converted to the currency left of the expression.

1.in(:usd) + 3.in(:usd)
5.in(:usd) - 3.in(:eur)
1000.in(:jpy) / 30.in(:usd)
7.in(:jpy) * 3.in(:usd)

Comparing and Sorting

Currencies get converted implicitly, at the time defined when instantiated

2.in(:eur) > 2.in(:usd) #=> true
2.in(:nok) < 2.in(:sek) #=> false
50.in(:eur, at: '2011-1-1') == 50.in(:sek) #=> false

Sorting also implicitly converts the currencies

[5.in(:eur), 4.in(:usd), 4.in(:chf, at: '2010-01-01')].sort

Safe operations

Operations with money are safeguarded against most floating point errors through Exchange. Upon instantiation of a currency, Exchange always converts the value to a big decimal. Additional Measures are implemented when Exchange::Money instances are used with floats in normal operations. Although BigDecimal operations are a bit slower than Float operations, the Exchange gem performs operations very efficiently. Float Operations not involving instances of Exchange::Money are left untouched.

Floating Point errors in ruby

(0.29 * 50).round(0) #=> 14
(1.0e+25 - BigDecimal.new("9999999999999999900000000")) #=> 0

Correct operations with instances of Exchange::Money

(50.in(:usd) * 0.29).round(0) #=> "USD 15.00"
(0.29 * 50.in(:usd)).round(0) #=> 15
(1.0e+25 - BigDecimal.new("9999999999999999900000000").in(:usd)).round #=> 100000000

Keep control over every conversion of currency in your app: if you do not want implicit conversions in arithmetic operations and comparisons to take place, you can configure Exchange to raise Errors on operations and comparisons involving multiple currencies.

Exchange.configuration.implicit_conversions = false
1.in(:eur) + 3.in(:usd) #=> raises ImplicitConversionError
1.in(:jpy) == 3.in(:chf) #=> raises ImplicitConversionError

Typecasting

Exchange features a typecasting module for you to use with your ruby classes, which instantiates Exchange::Money from three attributes: the amount, the currency and the creation date of the price (which defaults to Time.now if not used). Using it is simple:

class Article
  # make the class method available
  #
  extend Exchange::Typecasting
  
  # any kind of attribute will work, let's have attr_accessor for demo purposes
  #
  attr_accessor :price
    
  # 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, this will work:

a = Article.new
a.manager = Manager.new(currency: :eur)
a.price = 2
a.price #=> 2.in(:eur)

And also this; Exchange implicitly converts to the used currency if another than the one set is used in the assignment:

a.price = 3.in(:usd)
a.price == 3.in(:usd).to(:eur) #=> true
          

For more information about how to use the typecasting module, you can always visit the full documentation

Conversion Rate APIs

Exchange has 3 APIs built in for you to choose from, or you can easily build a custom API Extension

There is also a :random option for development use. It will put out random exchange rates. This can be useful if you are not connected to the internet.

To connect to a custom / private API, writing a small class is needed: How to connect to a custom / private API using Exchange

Caching Exchange Rates

Exchange generally stores your exchange rates in memory using instance variables of a singleton instance. For more persistent caching, you can also use 3 other provided caching solutions

  • Memcached via the Dalli gem
  • Redis via the Redis Gem
  • Rails cache

Please note that this gem does not list the any of these gems as explicit dependencies. If configured to use one of these caching solutions, Exchange tries to load the appropriate gem / constants and produces an error if it fails to do so / installation of the gem is needed.

You can also write a caching extension supporting your custom caching solution: How to write your own caching extension

Configuring

You can configure Exchange to a number of options. Configuration is a setter on the exchange module. This allows you to have different configurations, which you can then assign easily to exchange. This is how to define a configuration:

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

And this is how to assign it as the current one

Exchange.configuration = config

These are the options available with their defaults

cache: { # Takes the cache configuration as a hash, the options are:
  subclass: :memory,
    # takes an underscored symbol which is constantized 
    # on first use: :your_cache -> Exchange::Cache::YourCache
  host: nil, # The cache host for the client
  port: nil, # The cache port for the client
  expire: :daily # :hourly or :daily is available
},
api: { # Takes the conversion api configuration as a hash, the options are:
  subclass: :xavier_media,
    # takes an underscored symbol which is constantized 
    # on first use: :your_api -> Exchange::ExternalAPI::YourApi
  retries: 5, # The number of times a call to the API is retried
  app_id: nil, 
    # Some services like Open Exchange Rates require an App Id for requests
    # This is where to put it
  protocol: :http # The protocol to use for the requests
  fallback: :ecb
    # Fallback APIs to use if the requested api in :subclass is not available or does not provide any rate for the given conversion
    # takes an underscored symbol which is constantized or an array of underscored symbols
    # on first use: :your_api -> [Exchange::ExternalAPI::YourApi]
}
implicit_conversions: true
  # If set to false, operations involving multiple currencies will
  # produce errors, and no implicit conversions will take place.

Ready for Production

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.

Contributing

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.