Here are three Ruby functions…
Here are three Ruby functions. Each solves this problem: “You are given a starting and ending date and an increment in days. Produce all incremental dates that don’t include the starting date but may include the ending date. More formally: produce a list of all the dates such that for some n >= 1, date = starting_date + (increment * n) && date < = ending_date
.
Solution 1:
Solution 2:
Solution 3 depends on a lazily
function that produces an unbounded list from a starting element and a next-element function. Here’s a use of lazily
:
As a lazy sequence, integers
both (1) doesn’t do the work of calculating the i
th element unless a client of integers
asks for it, and also (2) doesn’t waste effort calculationg any intermediate values more than once.
Solution 3:
The third solution seems “intuitively” better to me, but I’m having difficulty explaining why.
The first solution fails on three aesthetic grounds:
-
It lacks decomposability. There’s no piece that can be ripped out and used in isolation. (For example, the body of the loop both creates a new element and updates an intermediate value.)
-
It lacks flow. It’s pleasing when you can view a computation as flowing a data structure through a series of functions, each of which changes its “shape” to convert a lump of coal into a diamond.
-
It has wasted motion: it puts an element at the front of the array, then throws it away. (Note: you can eliminate that by having
current
start out asexclusive+increment
but that code duplicates the later +=increment. Arguably, that duplicated increment-date action is wasted (programmer) motion, in the sense that the same action is done twice. (Or: don’t repeat yourself / Eliminate duplication.))
The second solution has flow of values through functions, but it wastes a lot of motion. A bunch of dates are created, only to be thrown away in the next step of the computation. Also, in some way I cannot clearly express, it seems wrong to stick the inclusive_end
between the exclusive_start
and the increment
, given that the latter two are what was originally presented to the user and the inclusive_end
is a user choice. (Therefore shouldn’t the exclusive_start
and increment
be more visually bound together than this solution does?)
The third solution …
-
… is decomposable: the sequence of dates is distinct from the decision about which subset to use. (You could, for example, pass in the whole lazy sequence instead of a
exclusive_start/increment
pair, something that couldn’t be done with the other solutions.) -
… eliminates wasted work, in that only the dates that are required are generated. (Well, it does store away a first date —
excluded_start
— that is then dropped. But it doesn’t create an excess new date.) -
… has the same feel of a data structure flowing through functions that #2 has.
So: #3 seems best to me, but the advantages over the other two seem unconvincing (especially given that programmers of my generation are likely to see closure-evaluation-requiring-heap-allocation-because-of-who-knows-what-bound-variables as scarily expensive).
Have you better arguments? Can you refute my arguments?
I’m trying to show the virtues of a lazy functional style. Perhaps this is a bad example? [It’s a real one, though, where I really do prefer the third solution.]
December 27th, 2011 at 9:44 pm
My C# version, which i find much more “readable” (subjective, i know) than the 3rd option, but still is lazy and composed etc: (Not tested well or properly input tested etc. Do not use in production code.)
private static IEnumerable Range ( DateTime start, DateTime end, int offsetDays = 1 )
{
int count = (int)( ( end - start ).TotalDays / offsetDays );
return Enumerable.Range ( 1, count ).Select ( n => start.AddDays ( n * offsetDays ) );
}
December 28th, 2011 at 10:04 am
What about Python:
#built-in
def dates(start,end,offset):
return range(start+offset, end, offset)
# if you needed to do this by yourself
def dates(start, end, offset):
while True:
start += offset
if start > end
break
yield start
January 9th, 2012 at 11:15 am
I like the yield statement too. In C# I would do
private static IEnumerable Range ( DateTime start, DateTime end, int offsetDays = 1 )
{
var current = start;
while (current
(also not tested :))
January 9th, 2012 at 11:17 am
That got chewed up. I’ll try it again.
private static IEnumerable Range ( DateTime start, DateTime end, int offsetDays = 1 )
{
var current = start;
while (current < end)
{
yield return current;
current.AddDays(offsetDays);
}
}
February 5th, 2012 at 4:52 am
Hi,
Solution 2 is the best for me. Simply because it is the easiest and most readable one. In my opinion an easy solution is one that can easily be extended and errors can be spotted fast.
Solution 3 is good too, lazyness is a good thing for optimization. But in this case I am not sure if it works.
I am not so deep into ruby right now, but I could imagine that “take_while { | d | d