Yet Another Friday the 13th

I'm an avid horror fan, big surprise! I like horror movies of all types: zombies, slasher, B-grade, C-grade, gore and even oldschool thriller horror movies like Hitchcock. To this day, I host a Friday the 13th event at my house every time for my friends and I. We run a marathon of as many movies as we can. Sometimes we make it through two, sometimes five. It's not always easy to stay up! :)

These past few months have been pretty difficult and busy. At 10pm I got a message from a friend in Canada: "happy Friday the 13th!" - Shit! I missed one! Well, no matter. The question is: how do I make sure not to miss one again? This is something I considered more than once and was always too lazy to actually try out.

So, I jotted down this code to accomplish it. I'd be happy to hear of better ways you can come up with:

I got the following output:
Friday the 13th on: 13/04/2012
Friday the 13th on: 13/07/2012
Friday the 13th on: 13/09/2013
Friday the 13th on: 13/12/2013
Friday the 13th on: 13/06/2014
Friday the 13th on: 13/02/2015
Friday the 13th on: 13/03/2015
Friday the 13th on: 13/11/2015
Friday the 13th on: 13/05/2016
Friday the 13th on: 13/01/2017

It seems as though I missed the last Friday the 13th this year, but there are two next year and it seems like 2014 should be a big event because there's just one! 2015 has three!

Meanwhile, maybe I should go for a one-time Saturday the 14th?

18 Comments

Here's my solution:

use Date::Calc qw(Day_of_Week);

foreach my $year (2012 .. 2017) {
foreach my $month (1 .. 12) {
if (Day_of_Week($year,$month,13) == 5) {
printf("%d-%.2d-13\n", $year, $month);
}
}
}

I've used Date::Calc in production code at two different employers, and never had a problem with it. I've always felt it to be rock solid, and it has copious documentation, which is usually a good indicator.

I like its whipupitude as well, given the large number of utility functions.

Also take notice of Date::Calc's motto: "Keep it small, fast and simple". Try holding 10k or 100k DateTime objects in memory and see your process balloon (of course, not everyday you'll need to do that).

use DateTime ();
use Set::CrossProduct ();
say $_->ymd for
  grep { 5 == $_->day_of_week }
  map { DateTime->new( ( qw( year month day ), @$_ )[0,3,1,4,2,5] ) }
  Set::CrossProduct
    ->new( [ [2012 .. 2020], [1 .. 12], [13] ] )
    ->combinations;

Sawyer! How could we missed Friday the 13th again?
Liked your thought about this little script, but can you add a way that it will add a reminder to your calendar? That way we really won't forget about Friday the 13th (:

Not at all, go ahead.

How exactly do you expect leap seconds to make a difference as to whether a particular Friday falls on the 13th?

Leap years, certainly, but I can't imagine that any unexpected additional leap years will be proclaimed in my lifetime.

Here's a Perl 6 one-liner: http://irclog.perlgeek.de/perl6/2012-07-18#i_5825055

say (2012..2017 X 1..12).map(-> $y, $m { Date.new($y, $m, 13) }).grep(*.day-of-week == 5)

That Perl 6 one-liner is, of course, the 6ification of the approach I took. :-)

It is notable insofar as it is pretty much a no-frills direct transcription of how I thought of the solution I tried to code. Perl 5 does not provide sufficiently high-level primitives to express the same intent directly.

Now I wonder what a corresponding transcription in Haskell would look like.

I cannot think of many other languages that would make this easy in a similar way. Probably APL would, but, err.

SQL does unexpectedly well on this job! Of course this is exactly the sort of set operation that SQL is designed for, so it is not that unexpected, but I was still surprised at how straightforward it is to say this in SQL… except for the surprise lack of expressiveness that makes it all go badly wrong. The surprise is where it falls down: there is no concise way to generate a range like 2012..2020 or 1..12. Here it is in MySQL dialect:

SELECT CAST(CONCAT_WS("-",y,m,13) AS DATE) AS date
FROM (
        SELECT 2012 AS y
  UNION SELECT 2013
  UNION SELECT 2014
  UNION SELECT 2015
  UNION SELECT 2016
  UNION SELECT 2017
  UNION SELECT 2018
  UNION SELECT 2019
  UNION SELECT 2020 ) AS yseq
CROSS JOIN (
        SELECT 1 AS m
  UNION SELECT 2
  UNION SELECT 3
  UNION SELECT 4
  UNION SELECT 5
  UNION SELECT 6
  UNION SELECT 7
  UNION SELECT 8
  UNION SELECT 9
  UNION SELECT 10
  UNION SELECT 11
  UNION SELECT 12 ) AS mseq
GROUP BY date
HAVING DAYOFWEEK(date) = 6
ORDER BY date

If you disregard the huge copypasta necessary to supply the source data tables inline, the query itself is expressed almost directly. (Does Postgres manage to one-up the other databases on this? I hope so. Anyone? David?)

The only trick is the use of GROUP BY to be able to use HAVING instead of WHERE, because WHERE cannot refer to values computed inside the SELECT clause whereas HAVING can. One could use a subselect instead to defer the WHERE by another layer, but that would be more verbose.

So imagine prepopulated month and year tables, then it could be written like this:

SELECT CAST(CONCAT_WS("-",year,month,13) AS DATE) AS date
FROM year CROSS JOIN month
WHERE year BETWEEN 2012 AND 2020
GROUP BY date
HAVING DAYOFWEEK(date) = 6
ORDER BY date

That is almost novice-level SQL. If a novice can’t write this, that same novice will at least almost certainly be able to follow it.

Actually, scratch the GROUP BY clauses. They are unnecessary. A HAVING clause can stand by itself. I.e. this is the short form of the query:

SELECT CAST(CONCAT_WS("-",year,month,13) AS DATE) AS date
FROM year CROSS JOIN month
WHERE year BETWEEN 2012 AND 2020
HAVING DAYOFWEEK(date) = 6
ORDER BY date

Leave a comment

About Sawyer X

user-pic Gots to do the bloggingz