Generate pretty weekly schedule charts using HTML::Template

This is another post inspired by a question on Stackoverflow.

The task is to take textual schedule information such as

0 24000 97200
1 52200 95400
2 0 0
3 37800 180000
4 0 0
5 48000 95400
6 0 0

and turn it into a an overview of the busy and free times over the course of the week. The end result of my attempt looks like this:

In the input data, the first number is the day of the week (Sunday = 0), the second number is the beginning of busy time in seconds since midnight, and the third number is the duration of the task in seconds. If a task does not fit in a given day, it spills over to the following day(s) until it’s done.

I used div elements appropriately floated and given a height and width to contain free and busy blocks for each day. Clearly, this is not very semantic: Converting it to an ordered list with title attributes on each block displaying the time periods is left as an exercise to the reader (mostly because I am afraid I’ll end up wasting a lot of time styling those lis.

I used my old standby, HTML::Template to generate the HTML. It is pretty standard fare and nicely separates the data from presentation (however, I used tpage from Template-Toolkit to generate the escaped HTML included in this post :-)

<!doctype html>
<html>
<head>
<title>Pretty Schedule Example</title>

<style type="text/css">
    .row .day { margin:0; padding:0; padding-right:1%; width:9% }
    .row .container { width: 90% }

    .row,
    .container,
    .container .busy,
    .container .free
    {
        height:1.5em;
        margin:0;
        padding:0;
        overflow:hidden;
        white-space:nowrap;
    }

    .row .container,
    .row .day,
    .container .busy,
    .container .free { float:left }

    .container .free { background-color:#f0f0a0 }
    .container .busy { background-color:#70b070 }

    .row {
        margin-bottom:0.125em;
        max-width:600px;
        width:100%;
    }

    .clear { clear:both }
</style>
</head>
<body>
<TMPL_LOOP DAYS>
<div class="row clear">
<div class="day"><TMPL_VAR DAY></div>
<div class="container">
<TMPL_LOOP BLOCKS>
<div class="<TMPL_VAR CLASS>" style="width:<TMPL_VAR WIDTH>"></div>
</TMPL_LOOP>
</div>
<br class="clear">
</div>
</TMPL_LOOP>
</body>
</html>

The Perl side is also relatively straightforward (and, as a self-contained example, reads the schedule data from the __DATA__ section):

#!/usr/bin/env perl

use strict; use warnings;
use HTML::Template;

use constant ONE_MINUTE => 60;
use constant ONE_HOUR   => 60 * ONE_MINUTE;
use constant ONE_DAY    => 24 * ONE_HOUR;

my @days = qw(Sun Mon Tue Wed Thu Fri Sat);

my $remainder = 0;
my @rows;

while (my $line = <DATA>) {
    next unless $line =~ m{
        \A
        ( [0-6]  ) \s+
        ( [0-9]+ ) \s+
        ( [0-9]+ ) \s+
        \z
    }x;
    my ($daynum, $start, $duration) = (, , );

    my $dayrow = make_dayrow(
        $days[$daynum],
        $remainder,
        $start,
        $duration,
    );

    push @rows, $dayrow->[0];
    $remainder = $dayrow->[1];
}

my $tmpl = HTML::Template->new(filename => 'pretty-schedule.html');
$tmpl->param(
    DAYS => \@rows
);

print $tmpl->output;

sub make_dayrow {
    my ($day, $remainder, $start, $duration) = @_;
    my $row = {DAY => $day};

    if ($remainder > ONE_DAY) {
        $row->{BLOCKS} = [
            { CLASS => 'busy', WIDTH => '100%' }
        ];
        return [$row, $remainder - ONE_DAY];
    }

    my @blocks;
    my $hang = $start + $duration > ONE_DAY
             ? $duration - (ONE_DAY - $start)
             : 0
             ;

    push @blocks, {
        CLASS => 'busy',
        WIDTH => format_width($remainder),
    } if $remainder > 0;

    if ($start > $remainder) {
        push @blocks, {
            CLASS => 'free',
            WIDTH => format_width($start - $remainder),
        }, {
            CLASS => 'busy',
            WIDTH => format_width($duration - $hang),
        };
    }

    unless ($hang) {
        my $taken = $start > $remainder ? $start : $remainder;
        $taken += $duration;
        my $leftover = ONE_DAY - $taken;
        if ($leftover > 0) {
            push @blocks, {
                CLASS => 'free',
                WIDTH => format_width($leftover),
            };
        }
    }

    $row->{BLOCKS} = \@blocks;
    return [$row, $hang];
}

sub format_width {
    my ($width) = @_;
    return sprintf('%.6f%%', 100 * ($width / ONE_DAY));
}

__DATA__
0 24000 97200
1 52200 95400
2 0 0
3 37800 180000
4 0 0
5 48000 95400
6 0 0

Enjoy!