Industry guru Dave Taylor answers free tech support questions about a wide variety of business and technical topics, including blogging, Google AdSense, MySpace, Sony PSP, Apple iPod, Mp3 players, management, Linux, SEO, Mac OS X, Facebook, Twitter, LinkedIn and Microsoft Windows.

How many Friday the 13ths are there in a given year?

I'm studying Unix, and i need help writing a script. The script requires that it outputs the number of months and years from January 2007 to December 2017 inclusive that have a Friday the thirteenth.


Dave's Answer:

I generally don't help people with homework assignments (I get quite a few requests every week) but this one is pretty interesting, all in all, so let's have a shot at trying to figure out how to solve this problem with a Unix shell script.

The first step is to decide how complicated to make it: we can either use cal and parse the results or, if we're lucky, we'll have a version of the command-line date command that can take arbitrary "epoch values" and turn them into date strings. An epoch value, by the way, is the core date unit in Unix, the number of seconds since the beginning of "Unix time". That value? Well, we can let the command tell us:

$ date -r0
Wed Dec 31 18:00:00 CST 1969

So one way to solve this problem is to simply feed the "date" command epoch values for each day in the date range we need, testing each to see if it's a "Friday" and a "13".

The only hard part is figuring out the epoch date for, say, high noon on January 1st, 2007.

Here's how I'd solve that:

$ date 
Sat Apr 28 14:35:55 CDT 2007
$ date +%s
1177788958

Now we know that epoch date 1177788958 = 28 April, 2007 at 2:23pm

$ expr 60 \* 60 \* 24
86400

That's seconds in a day, the basic value to step from one day to another

$ date -r `expr 1177788958 - 86400`
Fri Apr 27 14:35:58 CDT 2007

Proof: you can see we now see the exact same time of day, one day earlier.
Now, we can also use "date" to figure out how many days into the year we are:

$ date +%j
118

Now, finally, we can just subtract 118 * 88400 to get to January 1st:

$ date -r `expr 1177702558 - 10195200`
Sat Dec 30 13:35:58 CST 2006

Nope. I'm guessing it's a timezone issue, so we can just recalculate...

$ expr 86400 \* 116
10022400
$ expr 1177702558 - 10022400
1167680158
$ date -r 1167680158
Mon Jan  1 13:35:58 CST 2007

Okay, so that's the tough part figured out. We now know the epoch date for 1 January, 2007, mid-day. It's not at noon, but since we'll be at exactly the same time of day for every day it'll work just fine...

Now the script is actually pretty straightforward, believe it or not. As a first stab, let's just see if we can produce a week's worth of dates in a loop:

starton=1167680158      # 1 jan, 2007, midday
oneday=86400
daystotest=7                    # test one week to start

span=$(( $oneday * $daystotest ))
endon=$(( $starton + $span ))

theday=$starton

while [ $theday -le $endon ]; do
  date -r $theday
  theday=$(( $theday + $oneday ))
done

Let's run those settings:

$ sh fridays.sh
Mon Jan  1 13:35:58 CST 2007
Tue Jan  2 13:35:58 CST 2007
Wed Jan  3 13:35:58 CST 2007
Thu Jan  4 13:35:58 CST 2007
Fri Jan  5 13:35:58 CST 2007
Sat Jan  6 13:35:58 CST 2007
Sun Jan  7 13:35:58 CST 2007
Mon Jan  8 13:35:58 CST 2007

Good. Now, let's do another interim solution by counting how many Fridays there are. This we can do by just modifying the while loop:

while [ $theday -le $endon ] 
do
  if [ ! -z "$(date -r $theday  | grep "^Fri ")" ] ; then
    echo "Match: $(date -r $theday)"
    matches=$(( $matches + 1 ))
  fi
  theday=$(( $theday + $oneday ))
done

Now this time I'll run for 60 days by changing "daystotest" to 60, and run it:

$ sh fridays.sh
Match: Fri Jan  5 13:35:58 CST 2007
Match: Fri Jan 12 13:35:58 CST 2007
Match: Fri Jan 19 13:35:58 CST 2007
Match: Fri Jan 26 13:35:58 CST 2007
Match: Fri Feb  2 13:35:58 CST 2007
Match: Fri Feb  9 13:35:58 CST 2007
Match: Fri Feb 16 13:35:58 CST 2007
Match: Fri Feb 23 13:35:58 CST 2007
Match: Fri Mar  2 13:35:58 CST 2007
Matches: 9

Good!! So now let's add one more step: a test for "13" in the grep:

while [ $theday -le $endon ]
do
  if [ ! -z "$(date -r $theday  | grep -E "^Fri.* 13 ")" ] ; then
    echo "Match: $(date -r $theday)"
    matches=$(( $matches + 1 ))
  fi
  theday=$(( $theday + $oneday ))
done

As you can see, I'm using a bit of a fancy regular expression (hence the addition of the "-E" flag) to test for lines that start with "Fri" and contain space-13-space. Now we'll run this for the entire year of 2007 by changing "daystotest" to 365, and...

$ sh fridays.sh
Match: Fri Apr 13 14:35:58 CDT 2007
Match: Fri Jul 13 14:35:58 CDT 2007
Matches: 2

Just about done. Now we just need to figure out how many days are in 10 years. Hopefully you can do that in your head! 3650 days:

sh fridays.sh
Match: Fri Apr 13 14:35:58 CDT 2007
Match: Fri Jul 13 14:35:58 CDT 2007
Match: Fri Jun 13 14:35:58 CDT 2008
Match: Fri Feb 13 13:35:58 CST 2009
Match: Fri Mar 13 14:35:58 CDT 2009
Match: Fri Nov 13 13:35:58 CST 2009
Match: Fri Aug 13 14:35:58 CDT 2010
Match: Fri May 13 14:35:58 CDT 2011
Match: Fri Jan 13 13:35:58 CST 2012
Match: Fri Apr 13 14:35:58 CDT 2012
Match: Fri Jul 13 14:35:58 CDT 2012
Match: Fri Sep 13 14:35:58 CDT 2013
Match: Fri Dec 13 13:35:58 CST 2013
Match: Fri Jun 13 14:35:58 CDT 2014
Match: Fri Feb 13 13:35:58 CST 2015
Match: Fri Mar 13 14:35:58 CDT 2015
Match: Fri Nov 13 13:35:58 CST 2015
Match: Fri May 13 14:35:58 CDT 2016
last day tested was Fri Dec 30 13:35:58 CST 2016
Matches: 18

As you can see, I added a test to see the last day we tested, because we need to take into account leapyears. Not bad, though, we're only off one day in ten years, so I'll readjust it to 3651, but since that penultimate day is Friday, the 31st of December, 2016 can't be a Friday the 13th, so I'll stick with 18 as the answer.

Just for completeness, here's the full script "fridays.sh" for you:

#!/bin/sh

# count occurences of Friday the 13th in the next four years.

starton=1167680158              # 1 jan, 2007, midday
oneday=86400
daystotest=3651                     # a decade, compensating for leap years
matches=0

span=$(( $oneday * $daystotest ))
endon=$(( $starton + $span ))

theday=$starton

while [ $theday -le $endon ]
do
  if [ ! -z "$(date -r $theday  | grep -E "^Fri.* 13 ")" ] ; then
    echo "Match: $(date -r $theday)"
    matches=$(( $matches + 1 ))
  fi
  theday=$(( $theday + $oneday ))
done

echo "last day tested was $(date -r $theday)"
echo "Matches: $matches"

exit 0

There ya go. 18 Friday the thirteenths in ten years means, logically, that they occur, on average, 1.8 times per year. Next, we can try to figure out how often a blue moon occurs! (actually, blue moons are the second full moon in a month, so we could actually solve that one as a shell script. But I'll skip that for now)



Help others find this article at Del.icio.us, Digg, Netscape, Reddit, and Simpy.

Subscribe!

Never miss another useful Q&A article again! Subscribe to AskDaveTaylor with Google Reader.

Comments

Couldn't you speed up the program and make it less processor intensive by starting on the first Friday of 2007, then advancing by a weekly interval instead of a daily interval (or changing the interval from daily to weekly the first time a Friday is hit)?

Posted by: Greg Bulmash at April 28, 2007 4:26 PM

You're right, Greg, that'd be a smarter way to solve this, far less processor intensive. As you say, simply step day by day until the first friday, then change the value of oneday from 60*60*24 to (oneday*7) so you have seconds in a week. That'd speed things up by, oh, 7x :-)

Posted by: Dave Taylor at April 29, 2007 7:34 AM

Actually the Gregorian calendar repeats every 12 years, not 10. Not that it matters. Plus using PL/SQL on an Oracle database would be a better way to calculate it. I'm not just saying it because I am a PL/SQL programmer. But the average is 1.833. ; )

SQL> DECLARE
v_current_date DATE := '13-JAN-2007';
v_ending_date DATE := '14-JAN-2019';
v_13_friday_count BINARY_INTEGER := 0;
BEGIN
dbms_output.enable(1000000);
WHILE v_current_date < v_ending_date
LOOP
IF TO_CHAR(v_current_date, 'D') = 6
THEN
v_13_friday_count := v_13_friday_count + 1;
DBMS_OUTPUT.PUT_LINE(v_current_date || ' is a Friday the 13th');
END IF;
v_current_date := ADD_MONTHS(v_current_date, 1);
END LOOP;
DBMS_OUTPUT.PUT_LINE('T 2 3 here are ' || v_13_friday_count || ' Friday the 13ths in a 12 year period.');
DBMS_OUTPUT.PUT_LINE('For an average of ' || ROUND(v_13_friday_count/12, 3) || ' per year.');
END;
/
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 13-APR-07 is a Friday the 13th
13-JUL-07 is a Friday the 13th
13-JUN-08 is a Friday the 13th
13-FEB-09 is a Friday the 13th
13-MAR-09 is a Friday the 13th
13-NOV-09 is a Friday the 13th
13-AUG-10 is a Friday the 13th
13-MAY-11 is a Friday the 13th
13-JAN-12 is a Friday the 13th
13-APR-12 is a Friday the 13th
13-JUL-12 is a Friday the 13th
13-SEP-13 is a Friday the 13th
13-DEC-13 is a Friday the 13th
13-JUN-14 is a Friday the 13th
13-FEB-15 is a Friday the 13th
13-MAR-15 is a Friday the 13th
13-NOV-15 is a Friday the 13th
13-MAY-16 is a Friday the 13th
13-JAN-17 is a Friday the 13th
13-OCT-17 is a Friday the 13th
13-APR-18 is a Friday the 13th
13-JUL-18 is a Friday the 13th
There are 22 Friday the 13ths in a 12 year period.
For an average of 1.833 per year.

PL/SQL procedure successfully completed.

Elapsed: 00:00:00.02

Posted by: brian at May 1, 2007 7:46 AM

I have a lot to say, but ...
Starbucks coffee cup I have a lot to say, and questions of my own for that matter, but most of all I'd like to say thank you for all your efforts on this Web site by buying you a chai!

I do have a comment, now that you mention it!









Remember personal info?


Please note that I will never send you any unsolicited commercial email. Ever.

While I'm at it, please note that by submitting a question or comment you're agreeing to my terms of service, which are: you relinquish any subsequent rights of ownership to your material by submitting it on this site.









Search
Find just the answers you seek from among our 1700+ free tech support articles by using our Lijit search engine.


Help!





Subscribe to
Ask Dave Taylor!

Add to Google Reader
Add to My Yahoo!
Subscribe in NewsGator Online

RDF   XML

Free Updates!
Sign up and get free weekly updates and special offers on books, seminars, workshops and more.


Recent Entries
Join the List!
Join my author info mailing list, where you'll learn about my upcoming books, speaking gigs, and more!


Book Links
© 2002 - 2008 by Dave Taylor. All Rights Reserved.

Note: This web site is for the purpose of disseminating information for educational purposes, free of charge, for the benefit of all visitors. We take great care to provide quality information. However, we do not guarantee, and accept no legal liability whatsoever arising from or connected to, the accuracy, reliability, currency or completeness of any material contained on this web site or on any linked site.

[whiteboard marker tray]