Simplicity made easy

The second task of last week's Perl Weekly Challenge
was to list the dates of the last Friday in every month of a given year.

Many of the participants went to great lengths to create efficient and accurate solutions:
tracking the last day of each month, detecting leap years correctly,
working out formulas for the day of the month for a particular date,
managing the complex date arithmetic required.

But, given the powerful Date class built into Perl 6, most of those
admirable exertions were not actually necessary.

The entire task could be accomplished simply by walking through
from 1 January to 31 December of the given year, first checking
if each date is a Friday (i.e. the "day of the week" value is 5), and then
checking if the following Friday (i.e. the date exactly one week later)
happens to fall in a different month.

If both of those criteria are true, then we have a "last Friday of the month",
so we simply print it.

In other words:

for Date.new($year,1,1) .. Date.new($year,12,31) -> $date {
if $date.day-of-week == 5
&& $date.later(:1week).month != $date.month {
say $date;
}
}

We don't need to explicitly worry how long each month is, whether the year is a leap
year, or how to work out whether a given day is a Friday; the Date class takes
care of all that for us.

And that's not luck; it's entirely by design. Perl 6 has
a huge number of built-in datatypes like this;
they're yet another kind of "right tool, right at hand".

Of course, because Perl 6 is multiparadigm, there are plenty of other ways
to wrap a solution around the conveniences of the Date class,
depending on how you prefer to think about the world.

If you like pure functional solutions, write it like so:

say join "\n",
grep { .later(:1week).month != .month },
grep { .day-of-week == 5 },
Date.new($year,1,1) .. Date.new($year,12,31);

Or you'd prefer a (potentially) parallel pipeline,
then filter the sequence of dates through a series of
independent filters and processors:

Date.new($year,1,1) .. Date.new($year,12,31)
==> grep( {.day-of-week == 5} )
==> grep( {.later(:1week).month != .month} )
==> join( "\n" )
==> say();

Or you'd rather use a purely OO approach, just call the appropriate methods
on the list of dates:

(Date.new($year,1,1) .. Date.new($year,12,31))
.grep( {.day-of-week == 5} )
.grep( {.later(:1week).month != .month} )
.join( "\n" )
.say;

You could even write it as an old-school imperative Perl one-liner:

.say if .day-of-week == 5
&& .later(:1week).month != .month
for Date.new($year,1,1) .. Date.new($year,12,31);


So, whether you prefer an imperative, functional, object-oriented,
or pipelined approach, you can solve this problem simply and
cleanly. Because Perl 6 has the necessary built-in data types
to do the heavy lifting for you.

Damian

Leave a comment

About Damian Conway

user-pic I blog about Perl.