#!/usr/local/bin/perl -w

# showcrontab
#
# Displays the contents of a crontab file in a human readable form.
# Copyright (C) November 1997, William R. Ward <wrw@bayview.com>.
#
# Copying and distribution permitted under GNU public license, or the
# Perl "Artistic License".  See GNU and Perl documentation for more info.

require 5.003;                  # minimum Perl version
use strict;                     # strict syntax checking
use Text::Wrap;                 # use wrap function

# Set the margin for wrapping
$Text::Wrap::columns = 72;

# Text for days of the week
my @weekdays = qw(Sunday Monday Tuesday Wednesday Thursday Friday Saturday);

# If no command line arguments given, use output of 'crontab -l'
unshift (@ARGV, "crontab -l |") unless @ARGV;

# For each line of the input, parse and display.
while (<>)
{
    chomp;                      # remove trailing newline
    next if /^#/;               # skip comments

    # The crontab format is six fields separated by spaces.  The first
    # five are the minute, hour, day of month, day of year, and day of
    # week, respectively.  Each is either '*' which means 'all', or a
    # list of ranges of numbers.  Example values include '6', '1,3,7',
    # '20-40', '1-5,7', etc.  No space may be included in any field
    # except for the sixth, which is the command to run.

    my ($min, $hour, $mday, $yday, $wday, $cmd) = split(/\s+/, $_, 6);

    # Display first the command that is to be run.
    print wrap('', "\t\t ", "Run the command: $cmd\n");

    # Display the time.  If the hour is given...
    if ($hour ne '*')
    {
        # ...and the minute is given...
        if ($min ne '*')
        {
            # Split the hours and minutes into lists of times using
            # str2list() and display a list of times such as '9:00 am, 5:30 pm'
            my @hours = str2list($hour);
            my @mins  = str2list($min);
            my ($i, $j, @times);
            foreach $i (@hours)
            {
                # convert $i (the hour cursor) to am/pm style
                my $ampm = ($i >= 12 ? 'pm' : 'am');
                $i -= 12 if $i >= 12;
                $i = 12 if $i == 0;
                foreach $j (@mins)
                {
                    push (@times, sprintf('%d:%02d %s', $i, $j, $ampm));
                }
            }
            print wrap('', "    ", "  At ".list2str(@times)." each day\n");
        }

        # Hour given but minute not given.
        else
        {
            print wrap('', "    ", "  Every minute during the ".
                       convertnum($hour)." hour each day\n")
                if $hour ne '*';
        }
    }
    
    # Hour is not given but minute is...
    elsif ($min ne '*')
    {
        print wrap('', "    ",
                   "  At the ".convertnum($min)." minute past each hour\n");
    }

    # Display day of month info if given.
    print wrap('', "    ", "  On the ".convertnum($mday)." day of the month\n")
        if $mday ne '*';

    # Display day of week info if given.  Converts the number into the
    # name of the day(s) given.
    if ($wday ne '*')
    {
        my $msg = convertnum($wday);
        $msg =~ s/(\d)(st|nd|rd|th)/$weekdays[$1]/g;
        print wrap('', "    ", "  On $msg\n");
    }

    # Display day of year info if given.
    print wrap('', "    ", "  On the ".convertnum($yday)." day of the year\n")
        if $yday ne '*';

    # Blank line after this entry.
    print "\n";
}

# Subroutine str2list takes a string which contains a single item or a
# comma-separated list of items, where 'item' is either a number or a
# hyphen-separated range of numbers.  It then returns an array of
# numbers represented by the list/ranges given.  Input is scalar;
# output is list.
sub str2list
{
    my ($nums) = @_;
    my (@numbers);
    my @list = split(/,/, $nums);
    foreach (@list)
    {
        if (/(\d+)-(\d+)/) { push (@numbers, $1 .. $2); }
        else               { push (@numbers, $_); }
    }
    return @numbers;
}

# Subroutine convertnum converts entry such as '1,12,23-25' into a
# string such as '1st, 12th, and 23rd through 25th'.  Input and output
# are both scalars.
sub convertnum
{
    my ($str) = @_;
    my @numbers = split(/,/, $str);
    my ($num, @retval);
    foreach $num (@numbers)
    {
        if ($num =~ /(\d+)-(\d+)/)
        {
            push (@retval, num2word($1).' through '.num2word($2));
        }
        else
        {
            push (@retval, num2word($num));
        }
    }
    return list2str(@retval);
}

# Subroutine num2word converts a number into a word by adding 'st',
# 'nd', 'rd', or 'th' as appropriate.  Input and output are both
# scalars.
sub num2word
{
    my ($num) = @_;
    $num += 0;                  # ensure value is numeric.
    my $lastdigit = substr($num, length($num)-1, 1);
    if    ($lastdigit == 1) { $num .= 'st'; }
    elsif ($lastdigit == 2) { $num .= 'nd'; }
    elsif ($lastdigit == 3) { $num .= 'rd'; }
    else                    { $num .= 'th'; }
    return $num;
}

# Converts an array into a string by adding commas and 'ands' where
# appropriate.  Uses the 'terminal comma' syntax, e.g. 'foo, bar, and
# baz' as opposed to 'foo, bar and baz'.  For two elements, no comma
# is used.  Input is array, output is scalar.
sub list2str
{
    my (@list) = @_;
    if (@list == 1)             # single element, just return it.
    {
        return $list[0];
    }
    elsif (@list == 2)          # two elements, return '1 and 2'
    {
        return join(' and ', @list);
    }
    else                        # three or more elements, return '1, 2, and 3'
    {
        my $last = pop @list;
        return join(', ', @list).", and $last";
    }
}
