In Progress
Unit 1, Lesson 21
In Progress

Pessimize

Video transcript & code

A note from the head chef: First off, sorry there was no episode on Monday. One of our video pressure cookers exploded and we've been cleaning greasy code snippets out of the ventilation ducts all week.

But speaking of the kitchen, I've got some exciting news today! For a while now, ever since Episode #413, I've been assisted in preparing episodes by station chef Federico Iachetti. In fact, lately when you've been watching me demonstrate concepts on the show, more often than not you've actually been watching him do the typing!

Federico is more than a just dexterous keyboard-wrangler, and today I'm excited and proud to feature his very first full episode! It's on the topic of the Pessimize gem. It's a great video about a very handy tool, and I hope you enjoy it. Bon appétit!

--- Avdi


Say some time ago we were on a project and we needed to read pricing quotes in order to find the total amount we'd need to pay.

As the avid programmers we are, we rolled out a tiny script that would perform this calculation for us.

./total data1.csv
Total: $255.00

We showed it to our coworkers and they loved it. It was the simplest solution for the simplest of problems. The team started using it for every quote file we received. In the process, we added a bunch of new features to cover different providers rules.

One provider used a different format for numeric values, another provided discounts for all the items in the list, another one just gave discounts if the total exceeded X amount of dollars and some others didn't even deal in US Dollars.

In the course of a couple of days, our tiny script grew to accept multiple quote files and compare them using every kind of comparison we can imagine. It parsed different file formats and dealt with a number of currencies. It became an all-singing, all-dancing God-script.

./get_prices data1.csv data2.xls data3.html
--------------------------------------------------
| Item       | Provider                 | Price  |
--------------------------------------------------
| Keyboard   | PC Universe              | $99.40 |
| Mouse      | My little computer store | $84.90 |
| Monitor    | PC Universe              | $53.00 |
| Web cam    | PC land                  | $45.98 |
| Microphone | PC Universe              | $91.00 |
| CPU        | PC land                  | $51.00 |
| Video Card | My little computer store | $11.43 |
--------------------------------------------------
| Total:                                 $436.71 |
--------------------------------------------------

As just one example of the kind of bells and whistles the script has grown to encompass: we've added the possibility to calculate the total amount excluding discounts

./get_prices -d data1.csv data2.xls data3.html
--------------------------------------------------
| Item       | Provider                 | Price  |
--------------------------------------------------
| Item 1     | PC Universe              | $98.20 |
| Item 2     | PC Universe              | $52.10 |
| Item 3     | My little computer store | $83.00 |
| Item 4     | PC land                  | $45.50 |
| Item 5     | PC Universe              | $91.00 |
| Item 6     | PC land                  | $48.30 |
| Item 7     | My little computer store | $11.43 |
--------------------------------------------------
| Total:                                 $429.53 |
--------------------------------------------------

At some point during the process, someone introduced a dependency to the money gem in order to help us deal with currency management and comparison.

source "https://rubygems.org"

gem "money"

And everything worked and everyone was happy.

Some time passed by and now we need to perform all the same calculations for another project. We use the same script.

We download the repo and run bundle in order to install all the gems we need

bundle
Fetching gem metadata from https://rubygems.org/
Fetching version metadata from https://rubygems.org/
Resolving dependencies...
Using i18n 0.7.0
Using sixarm_ruby_unaccent 1.1.1
Using bundler 1.12.5
Installing money 6.7.1
Bundle complete! 1 Gemfile dependency, 4 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.
Post-install message from money:
Please note the following API changes in Money version 6

 - Money#amount, Money#dollars methods now return instances of BigDecimal (rather than Float).

Please read the migration notes at https://github.com/RubyMoney/money#migration-notes
and choose the migration that best suits your application.

Test responsibly :-)

And then we proceed to run the script with the new data and everything works

./get_prices new_data1.csv new_data2.xls new_data3.html
--------------------------------------------------
| Item       | Provider                 | Price  |
--------------------------------------------------
| Keyboard   | Home of the Computer     | $99.20 |
| Mouse      | Benjamin Computers       | $84.30 |
| Monitor    | Home of the Computer     | $48.00 |
| Web cam    | Home of the Computer     | $50.77 |
| Microphone | Benjamin Computers       | $92.00 |
| CPU        | Benjamin Computers       | $54.00 |
| Video Card | My little computer store | $10.45 |
--------------------------------------------------
| Total:                                 $438.72 |
--------------------------------------------------

But when we try it without the discounts, Oops! something's broken

./get_prices -d new_data1.csv new_data2.xls new_data3.html
/home/fedex/.rvm/gems/ruby-2.3.1/gems/money-6.7.1/lib/money/money/arithmetic.rb:112:in `+': TypeError (TypeError)
    from get_prices.rb:19:in `block in <main>'
    from /home/fedex/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/csv.rb:1748:in `each'
    from /home/fedex/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/csv.rb:1131:in `block in foreach'
    from /home/fedex/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/csv.rb:1282:in `open'
    from /home/fedex/.rvm/rubies/ruby-2.3.1/lib/ruby/2.3.0/csv.rb:1130:in `foreach'
    from get_prices.rb:11:in `each'
    from get_prices.rb:11:in `inject'
    from get_prices.rb:11:in `<main>'

Let's ignore the specific exception for the moment, and just think about what are some possible reasons this might have failed.

  • Someone could have changed something in the code.
  • Any of the new files might have a different format for a value.
  • Even a tiny typo in one of the files could have triggered an exception.

We could open our script and try to figure out what happened in our code.

But instead, let's first try to identify exactly what changed using git

As with most modern Ruby projects, we've been managing our gem dependencies with Bundler and a Gemfile.

What we find is that the only change in our codebase, is the version of the money gem

Head:     master Everything works like a charm!

Unstaged changes (1)
modified   Gemfile.lock
@@ -2,7 +2,11 @@ GEM
   remote: https://rubygems.org/
   specs:
     awesome_print (1.7.0)
-    money (2.0.0)
+    i18n (0.7.0)
+    money (6.7.1)
+      i18n (>= 0.6.4, <= 0.7.0)
+      sixarm_ruby_unaccent (>= 1.1.1, < 2)
+    sixarm_ruby_unaccent (1.1.1)

 PLATFORMS
   ruby

What is this diff telling us? It's saying that in between our last project and this one, the money gem version went from 2.0.0 to 6.7.1. And since we didn't specify a version in our dependencies, Bundler has dutifully picked up the very latest one.

Unfortunately, this upgrade appears to have included some breaking changes.

The solution to this problem is to constrain the version of our gems.

How do we constrain a gem version?

It's easy enough. We just need to provide a version number as the second argument to the gem method

source "https://rubygems.org"

gem "money", "2.0.0"

This change will tell bundle to use version 2.0.0 for the money gem.

By not providing this information, we're actually telling bundle to install the latest possible version of the gem. It would be the same than prepending it with a >=.

source "https://rubygems.org"

gem "money", ">= 2.0.0"

This means we're taking an optimistic approach with our versioning. We assume that every future change to our dependencies will either not modify or even improve them.

Now, we don't want to constrain by hand every version of every new gem we add to the project! It's a tedious process and it interrupts our flow. We want to just quickly add the gem and get back to hacking on the code.

Can't we get the computer to do the hard work for us?

Yes, we can. For this very purpose there is a gem called pessimize .

gem install pessimize
Fetching: pessimize-0.3.0.gem (100%)
Successfully installed pessimize-0.3.0
1 gem installed

Let's try it out to see what it does. After running bundle, We run the pessimize command from the terminal

pessimize
Backing up Gemfile and Gemfile.lock
 + cp Gemfile Gemfile.backup
 + cp Gemfile.lock Gemfile.lock.backup

~> written 2 gems to Gemfile, constrained to minor version updates

Pessimize has told us that it made backups of the Gemfile and Gemfile.lock. Then it made some edits to the originals. Let's see what those edits are.

If we open the Gemfile, we see that version numbers for every gem have been filled out for us

We can also see the ~> operator prepended to the version number

source "https://rubygems.org"

gem "money", "~> 6.7"

This operator is officially known as the Pessimistic Versioning Operator. We're going to cover the various gem version specification operators in great detail in another episode. In this case, what these version specifiers mean is that the money gem is constrained to the 6.7 major version series.

I commonly lock my gems down to the patch version, which means only the number after the second dot may be incremented.

Now this is a pessimistic approach to gem versioning. With this approach, we embrace the assumption that a bump on the major version number will have changes that may break our code.

We can specify this policy to pessimize by passing the -c flag with patch as the value

Once we decide that we trust pessmize, we may find the Gemfile and Gemfile.lock backup files it generates by default to be annoying and superfluous.

We can avoid writing these by passing the --no-backup argument.

pessimize -c patch --no-backup

pessimize saves me work and cuts down on rude gem upgrade surprises. It's so useful, I always have it at hand for all of my projects. I hope you give it a try for your next project.

Happy hacking!

Responses