= exchange {}[http://travis-ci.org/beatrichartz/exchange] {Dependency Status}[https://gemnasium.com/beatrichartz/exchange] {}[https://codeclimate.com/github/beatrichartz/exchange] The Exchange Gem gives you easy access to currency functions directly on your Numbers. {It is tested against}[http://travis-ci.org/beatrichartz/exchange]: ruby 1.9, ruby 1.8.7, ree, rubinius (1.8 and 1.9 mode) and jruby You can use it with just plain ruby projects, in Rails 2 and 3, Sinatra or whatever Framework you like. == Installation === Bundler / Rails Add it to your Gemfile gem "exchange", ">=0.10.0" === Manually Just install it as a gem gem install exchange Then require it require 'exchange' === Deprecations in 0.10.0 The old gem API (3.eur, 5.usd.to_chf) has been abandoned in favor of the new (1.in(:eur), 5.in(:usd).to(:chf)). This resolves a conflict of the earlier versions with the active support "try" method, which also stands for turkish lira. It also makes dynamic instantiation of currencies easier (in(currency) is better than send(currency), to(currency) much better than send(:"to_#{currency}")) Version 0.9.0 still supports the old API, so if you don't have the time to change now, just continue to use the old version and change later. There are no other changes than the api and no bugfixes from 0.9.0 to 0.10.0. == 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) === 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, {head over to this benchmark}[https://github.com/beatrichartz/exchange/blob/master/benchmark/benchmark.rb]. With ruby 1.9.3, you'll get about the following results 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}[http://rubydoc.info/github/beatrichartz/exchange/Exchange/Typecasting] === 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 49.567.in(:usd).to_s #=> "USD 49.57" 45.in(:jpy).to_s #=> "JPY 45" 34.34.in(:omr).to_s #=> "OMR 34.340" === Use three great APIs or your own Three open APIs are already included: - {Xaviermedia}[http://www.xavierforum.com/viewtopic.php?f=5&t=10979&sid=671a685edbfa5dbec219fbc6793d5057] - {European Central Bank}[http://www.ecb.int/stats/exchange/eurofxref/html/index.en.html] - {Open Exchange Rates}[http://openexchangerates.org/] (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}[http://rubydoc.info/github/beatrichartz/exchange/Exchange/ExternalAPI] === 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}[http://rubydoc.info/github/beatrichartz/exchange/Exchange/Cache] == 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) #=> # 2.3.in(:dkk).to(:sek) #=> # 45.54.in(:nok).to(:sek) #=> # Easily convert one currency to another at a historical rate 1.52.in(:usd).to :eur, :at => '2011-01-01' #=> # 3.45.in(:eur).to :sek, :at => Time.gm(2011,3,3) #=> # 345.in(:sek).to :nok, :at => Time.gm(2011,3,3) #=> # Or even define an instance of currency as historic by adding a time. 1.52.in(:usd, :at => '2011-01-01').to(:eur) #=> # 3.45.in(:usd, :at => Time.gm(2011,3,3)).to(:sek) #=> # 345.in(:usd, :at => Time.gm(2011,3,3)).to(:nok) #=> # Do multiple conversion steps at once (if in any way useful) 3.in(:chf).to(:eur, :at => '2011-02-04').to(:usd) #=> # === 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 #=> [#, #, #] 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) #=> # 1.in(:usd) - 1.32.in(:eur) #=> # 1.in(:usd) * 1.32.in(:eur) #=> # 1.in(:usd) / 1.32.in(:eur) #=> # 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') #=> # You can just instantiate currencies and apply operations. Rounding will by default round the currency to its ISO4217 decimal precision: 3.123.in(:eur).round #=> # You can also pass the precision you wish for as an argument, round, ceil, floor act like normal: 3.1234.in(:eur).round(0) #=> # 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) #=> # 10.34.in(:usd).to(:nok).ceil(0) #=> # 5.34.in(:usd).to(:eur).floor(0) #=> # 5.34.in(:usd).to(:eur).floor.from #=> # === Retain Information Access the original currency and its value after conversion, even over multiple steps converted = 2.in(:eur).to(:usd) #=> # converted.from #=> # converted2 = converted.to(:nok) #=> # converted2.from #=> # == 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 :allow_mixed_opterations (default true) If set to false, Operations with with different currencies raise errors. If you're afraid of mixed currency operations, just don't allow them Exchange.configuration.allow_mixed_operations = false 1.in(:usd) + 1.in(:eur) #=> MixedCurrencyError === 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 class MyCustom < Base # Define here which currencies your API can handle CURRENCIES = %W(usd chf) # 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 = 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 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. === 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 Copyright (c) 2012 Beat Richartz. See LICENSE.txt for further details.