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.
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.
Add it to your Gemfile
gem "exchange", "~> 1.2.0"
Just install it as a gem
gem install exchange
Then require it
require 'exchange'
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.
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)
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.
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
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.
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
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)
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"
Three open APIs are already included:
-
Open Exchange Rates (Look for the free plan at the bottom of the page)
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 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
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 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)
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>
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
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>
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
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
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
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
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.
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.
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.