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
- it is evenly divided by 4 but NOT evenly divided by 100.
- 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:
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.
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
Top comments (4)
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:
Where this does the leap calculation at most once per year and links to the same hashes repeatedly.
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
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"
when2
will do.Nice article, thank you for this. Definatly something I can use in the future while I learn Ruby more.