Documentation Driven Development

CPANneocpanismdocDWIM Fri 7 March 2014

I've released Date::WeekNumber, which provides two functions for generating week numbers in the format 2014-W09. This is based on code I wrote while creating the CPAN new dist a month contest. I approached the creation of this module slightly differently from my norm, writing the documentation first.

The key thing I wanted is illustrated with this year boundary:

    December 2013            January 2014
 Su Mo Tu We Th Fr Sa    Su Mo Tu We Th Fr Sa
  1  2  3  4  5  6  7              1  2  3  4
  8  9 10 11 12 13 14     5  6  7  8  9 10 11
 15 16 17 18 19 20 21    12 13 14 15 16 17 18
 22 23 24 25 26 27 28    19 20 21 22 23 24 25
 29 30 31                26 27 28 29 30 31

The key requirement for a weeks function is:

week('2013-12-31') eq week(2014-01-01')

ISO 8601 defines weeks as starting on Monday, but the once a week contest has weeks starting on Sunday. So I decided my new module would provide two functions: one for ISO weeks, and one for 'CPAN weeks'.

I started by writing the section of documentation that described the week numbering schemes, including calendar extracts that showed the interesting year-boundary cases.

That naturally led to some test cases. I stubbed out the functions so I could run the tests and see them fail.

When writing my earlier code I'd had a quick look at CPAN, but now I had a more determined look for modules that had support for weeks, thinking I still might find a module that did what I wanted. None of them could do CPAN weeks any easier than my existing code, and most of them needed several lines to produce the CPAN week.

So, I was still going to create a new module. I created a SEE ALSO section based on the modules I'd found, and wrote a script to run a bake-off between them. This revealed that Date::WeekOfYear sometimes gets the date wrong (bug submitted).

I wrote the first versions of iso_week_number() and cpan_week_number() using POSIX (thanks BOOK for %U), and released the module to CPAN. The following is illustrative only (strftime's interface isn't nearly so sane, for a start):

sub iso_week_number {
    my ($year, $month, $day) = @_;
    return POSIX::strftime('%G-W%V', $year, $month, $day);
}

sub cpan_week_number {
    my ($year, $month, $day) = @_;
    $week = POSIX::strftime('%U', $year, $month, $day);

    # week 0 means the last week of the previous year
    if ($week == 0) {
        $year--;
        $week = POSIX::strftime('%U', $year, 12, 31);
    }
    return sprintf('%.4d-W%.2d', $year, $week);
}

I also decided to try for some DWIMmery, so you can pass the date in one of 5 different formats.

At first blush, CPAN Testers seemed happy. But the next day I discovered that all Windows tests were failing. Turns out strftime's %G and %V don't work on Windows. WTF?!

So the next release used POSIX for cpan_week_number() and Date::Calc for iso_week_number(), with each function requiring the module it uses. All looks good so far.

Next I'll benchmark the different modules I could have used, to see which introduces the most dependencies, takes up most memory etc. I could have used DateTime for both, but I think of it as a bit of a bruiser. I figure any given user will want either iso_week_number() or cpan_week_number(), and so will only pull in one of POSIX or Date::Calc.

comments powered by Disqus