DEV Community

loading...
Cover image for Truth tables as Ruby Hashes

Truth tables as Ruby Hashes

redfred7 profile image Fred Heath ・3 min read

Truth tables are a common way of defining and testing code behaviour. A truth table treats a component or function of our system as a black box, with well-defined inputs and outputs. At the same time, Ruby Hashes are flexible enough that they can be used as decision objects, receiving an input (key) and determining its value. Hopefully you can see where I'm going with this ;) Let's look at an example. The problem in question is this:

"Write a method that receives the month and the year and outputs how many days there are in that month".

Sounds easy, doesn't it? We all know which months have 30 and which 31 days in them. Apart from February, that is. February usually has 28 days, except that in leap years it has 29.  How do we know which years are leap years? There are certain rules that allow us to determine leap years:

  • The year is evenly divisible by 4
  • If the year can be evenly divided by 100, it is NOT a leap year, unless
  • the year is also evenly divisible by 400. Then it is a leap year

To sum it up, a year is a leap year when

  1. it is evenly divided by 4 but NOT evenly divided by 100.
  2. It is evenly divided by 4, 100 AND 400.

The truth table for this problem would be:

Month  Year No of Days
jan, mar, may, jul, aug, oct, dec any 31
apr, jun, sep, nov any 30
feb year % 4 != 0 28
feb (year % 4 == 0) && (year % 100 != 0) 29
feb (year % 4 == 0) && (year % 100 == 0) && (year % 400 != 0) 28
feb (year % 4 == 0) && (year % 100 == 0) && (year % 400 == 0) 29

Now we could use a multi-branch conditional or maybe a Case statement to implement this truth table. But there's another way. We can leverage two powerful Ruby features to model our truth table as a Hash:

  1. Everything is an expression in Ruby.  I mean everything, and that includes Hash keys and values. Every statement gets evaluated to an object and that's a beautiful thing.

  2. Ruby is great for List Comprehensions. It offers some great ways of making lists out of lists, either in an iterative or a functional manner.

Knowing all this, we can write our method as follows:

def month_days(year, month)
    h = {
        %w(jan mar may jul aug oct dec) => 31,
        %w(apr jun sep nov) => 30,
        %w(feb) => ((year % 4 == 0) && (year % 400 == 0)) ||
                    ((year % 4 == 0) && (year % 100 != 0)) ?
                    29 : 28
    }
    # find the Hash key the includes the required month, return its value 
    h.select {|k, v| k.include? month}.values
end

We use Arrays for the Hash keys and we use the ternary operator as  a value for the february key.  Our returning object is the value of a Hash key that is generated by filtering the original Hash's keys (Arrays) based on the desired month. Let's run it:

$> puts month_days 1900, 'feb'
=> 28
$> puts month_days 2000, 'feb'
=> 29
$> puts month_days 1900, 'sep'
=> 30

Beautiful. This is much cleaner and elegant than a big If or Case statement. Moreover, a Hash can be easily memoized so that any intricate calculations become just a simple lookup and performance is boosted. Of course, Ruby being Ruby, there'll be a different or better way, so if you know of any feel free to share it with me by commenting below.

Originally published at Bootstrapped Thoughts

Discussion (4)

pic
Editor guide
Collapse
tadman profile image
Scott Tadman • Edited

While this is clever, be careful when doing this in production code. That temporary object is used once and thrown away, only to be generated again on the next instantiation. This creates a lot of “garbage” that needs to be cleaned up.

In situations like this you really want a constant, you can use that over and over to get a lot of mileage out of it. Since February is really the only “special” month you can have a look-up table with a tiny bit of logic built in, like in a lambda, which accounts for the leap year.

A Hash with a dynamic generator could also work, as it could fill in years as they’re referenced based on leap/non-leap and use just two look-up hashes internally.

Consider what you can do with:

CALENDAR = Hash.new do |h,y|
  h[y] = ((y % 4 == 0) && ((y % 100 != 0) || (y % 400 == 0))) ? LEAP : NON_LEAP
end

Where this does the leap calculation at most once per year and links to the same hashes repeatedly.

Collapse
redfred7 profile image
Fred Heath Author • Edited

That's a nice way of doing it Scott, it's cool to get different perspectives. You make a good point about re-generating objects too and this is certainly true in my example above. But, as I mention in the post, memoizing the Hash mitigates this problem :) gitlab.com/snippets/1791110

Collapse
tadman profile image
Scott Tadman

While that's better it still creates clutter in your object. A default inspect will show @h = ... even though that's not really relevant to that object. There's no reason to create this per-object, either, it doesn't change. That's why I recommend as a constant, out of the way and implicitly shared.

Also remember that months have commonly recognized numerical identifiers, there's no need to say "feb" when 2 will do.

Collapse
david_j_eddy profile image
David J Eddy

Nice article, thank you for this. Definatly something I can use in the future while I learn Ruby more.