Ever had to work with money in more than one currency? Ever wish you could do stuff like add them to one another without getting murdered by exceptions? Here's the gem for you.
Add this line to your application's Gemfile:
And then execute:
$ bundle install
Or install it yourself as:
$ gem install cashify
Cashify is a money handling gem that's inherently nice to currencies. It lets you treat your money as mostly numbers, freeing you from a lot of headache around manipulating and handling it. Instantiate a new
Cash object like so:
puts Cash.new(SEK: 100_00) # 100 SEK
Add another currency? Sure.
puts Cash.new(EUR: 100_00, SEK: 100_00) # 100 EUR, 100 SEK
And then maybe do some addition or subtraction?
puts Cash.new(SEK: 100_00) + Cash.new(USD: 100_00) + Cash.new(USD: 50_00) # 100 SEK, 150 USD
Perhaps add a little discount here or there?
puts (Cash.new(SEK: 100_00) + Cash.new(USD: 100_00)) * 0.9 # 90 SEK, 90 USD
Increase the values a little? Why not!
puts (Cash.new(SEK: 100_00, USD: 100_00) + 20 # 120 SEK, 120 USD
What. You can't just add integers to currencies
I can, in fact, totally add integers to currencies. And now you can, too! Also subtract, divide, multiply... look: this probably sounds bad to you. Foreign, somehow. Your parents raised you well, and you've already used the ubiquitous Money gem and tried to add a shipping cost or something. The Money gem then told you to stop immediately and stomped off into Exception Land, leaving you empty-handed and full of regret.
Or maybe you wanted to work with zero? You'll get answers on the Internet saying things like "aha but what would zero even mean when using a currency" and my answer to that would be "well at least 0 USD is the same as 0 SEK, right?" and so you can casually do
Cash.new(SEK: 0) + Cash.new(USD: 100) and get
100 USD back and then imagine these pedants popping a vein and dying. A zero is a zero. Don't try to tell me otherwise.
Can you really compare currencies that way though?
Well, I guess we can! And we're not even done. Check this out:
Cash.new(SEK: 100_00) > Cash.new(USD: 50_00) # true
Is this right or wrong? Who even knows at this point. But it's useful, and that's all we care about! I'll agree that the comparison is super naive, and you can Freedom Patch that if you like, or go the Money Gem route and pipe those objects through your own functions. But you see where we're going, right? Wouldn't it be nice to just grab various sums of currencies from your database, cast them to Cash objects, and have them just sort of work without putting up guardrails everywhere? Yes it would. The default assumption of "when you try to add one money to another money that probably means you want a conversion somewhere" is wrong in most cases. Also, initialization matters! Cashify is very database friendly. Imagine a Rails app with
Purchase objects that all have a cost. Then look at this beauty:
That piece of code pulls however many objects in however many currencies and tallies them up in SQL, returning a useful Cash object containing something along the lines of
1100 EUR, 1400 SEK, 2155 USD. If you want, you can smack a multiplier on those and add a discount. Or add a processing fee of
100 SEK. You be the boss.
This sounds dangerous and illegal, shouldn't these operations throw exceptions?
No, they probably shouldn't. The Money Gem school of thought says that every time you do something potentially hazardous to your money — like try to add an integer, not caring which currency you're dealing with — there's an exception, and then you'll have to handle that exception in a way that makes sense for your use case. I do see the logic, but in practice it gets old REAL fast. You'll be stuck in a never-ending shouting match with your money objects, having to drag them kicking and screaming through a bunch of guard statements in order to do simple things like addition. If you're only dealing with a single currency it's not too bad, but as soon as you add another one to the mix the noise level is deafening. And honestly: if you're only dealing with a single currency, why are you even using a money gem? You could use fixed point integers instead, pushing any currency-specific stuff to the boundaries of your code.
This in mind, Cashify posits that if you want to get these things right, you should be treating your money as simple numbers as often as possible. Currencies are a necessary evil but they're not your overarching concern, and this gem will allow them to lurk in the background where they belong. Throw those Cash objects around, stuff them into arrays, collide them in interesting ways. Swim around in your money like you're Scrooge McDuck and let your database do most of the work for you! It'll feel great, I promise.
Do you handle displaying currencies nicely, too?
Oof, sounds hard. Out of scope. Write a view helper or something.
And when you actually do want to convert currencies?
There are plenty of gems for that and it's honestly pretty easy. But you'll have to write your own code for it.
Returns true if all currencies are positive/negative.
Cash.new(SEK: 100_00, USD: 100_00).positive? # true Cash.new(SEK: 100_00, USD: -1_00).positive? # false Cash.new(SEK: -100_00, USD: -100_00).negative? # true
Returns true if all currencies are zero.
Cash.new(SEK: 0).zero? # true Cash.new(SEK: 0, USD: 1_00).zero? # false
Makes all values positive.
Cash.new(SEK: -100_00, USD: 1_00).abs # 100 SEK, 1 USD
Shorthand for a zero currency. Use it instead of
Cash.new(DKK: 0) because having to specify currency for zero feels wordy and awkward.
Cash.zero # 0 USD
Splits all the currencies out into an array of
Cash.new(EUR: 100_00, SEK: 100_00, USD: 100_00).to_a # [Cash(EUR: 100_00), Cash(SEK: 100_00), Cash(USD: 100_00)]
Get a simple string back.
Cash.new(EUR: 100_00, SEK: 100_00, USD: 100_00).to_s # "100 EUR, 100 SEK, 100 USD"
You'll be working with arrays of Cash objects a lot. You should absolutely be using Ruby's excellent array methods on them, but unfortunately
cash_array.sum won't work. Ruby's
sum method injects a
0 as the first value, and even though
Cash.new(USD: 100) + 0 is super valid and nice,
0 + Cash.new(USD: 100) will try to coerce your Cash object into an integer which just won't work. So if you want to sum an array of Cash, you can instead use:
Cash.sum [Cash.new(EUR: 100_00), Cash.new(SEK: 100_00), Cash.new(EUR: 100_00)] # 200 EUR, 100 SEK
Returns true if there are no currencies in the object.
Cash.new.empty? # true Cash.new(NOK: 100_00).empty? # false
Rounds currencies to a power interval.
Cash.new(EUR: 100_50, SEK: 122_25).round(100) # 101 EUR, 122 SEK Cash.new(EUR: 100_50, SEK: 122_25).round(1000) # 100 EUR, 120 SEK
Returns the first currency in the Cash object as a symbol
Cash.new(NOK: 100).currency # :NOK
Returns the first value in the Cash object.
Cash.new(NOK: 100).value # 100
After checking out the repo, run
bin/setup to install dependencies. Then, run
rake test to run the tests. You can also run
bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run
bundle exec rake install. To release a new version, update the version number in
version.rb, and then run
bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the
.gem file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/johanhalse/cashify.
The gem is available as open source under the terms of the MIT License.