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