# Date: Sat, 28 Aug 2004 15:20:05 -0500 (CDT)
# From: Dave Cash
# Subject: [SPOILER] Solution to Perl Quiz-of-the-Week #22
# 
# Hi.
# 
# For my solution, I decided to work with only core Perl modules (I
# only ended up using one: POSIX).  I also decided to add a few
# features, which I'll discuss shortly.
# 
# I didn't try to add the "2nd tuesday" feature, nor does my code
# currently recurse below the .upcoming directory.  That said, my code
# runs pretty fast.
# 
# My solution takes up to three command-line options and also lets you
# specify a list of files you'd like processed, in case you don't want
# to see them all, so you could, for instance, do:
# 
#   $ ./upcoming birthdays
# 
# or
# 
#   $ ./upcoming birthdays events
# 
# or just
# 
#   $ ./upcoming
# 
# to do all the files in the .upcoming directory.
# 
# The options are -n, -f and -d.  -n works as required.  -d lets you
# specify a directory (if other than ~/.upcoming) and -f lets you tell
# the script what date formate you use (e.g. 'MM/DD/YYYY',
# 'DD-MM-YYYY', 'YYYY:MM-DD', etc.).  So to use a European date
# format, just do:
# 
#   $ ./upcoming -f 'DD/MM/YYYY'
# 
# and as long as the dates in your event files use that format, you
# should be good to go.
# 
# The fun thing about my solution is that the regular expression to
# read lines from the event files is constructed on the fly, based on
# the format string.  Also, if your preferred format has day before
# month, then your output preserves that ordering.
# 
# My algorithm for working through dates is basically this:
# 
#    1) skip if the year is specified and before now
#   2a) if month, day and year are all specified, make a calendar
#       entry for this date
#   2b) if one of month or year are not specified (day must always be
#       specified) make as many calendar entries as are necessary to
#       accomodate the number of years implied by the value of the
#       days-ahead variable and make an entry for each month if month
#       is unspecified
# 
# There are definitely limits to my solution, besides those mentioned
# above.  There are few places that could use better exception
# handling.
# 
# My solution is below my signature.
# 
# BTW, this was fun!
# 
# Thanks,
# 
# Dave
# 
# /L\_/E\_/A\_/R\_/N\_/T\_/E\_/A\_/C\_/H\_/L\_/E\_/A\_/R\_/N\
# Dave Cash                              Power to the People!
# Frolicking in Fields of Garlic               Right On-Line!
# dave@gnofn.org                                  Dig it all.
# 
# 
# 
#!/usr/bin/perl

#
# upcoming
#
# by Dave Cash
#
# solution to Perl Quiz-of-the-Week #22
# http://perl.plover.com/qotw/r/022
#

use strict;
use warnings;
use POSIX;

#
# set defaults for run-time configurtion and read in overrides
#

my %config = (
	n => 7,
	f => 'MM/DD/YYYY',
	d => ( getpwuid( $< ) )[ 5 ] . '/.upcoming',
);
my @files = ();

my @args = @ARGV;
while ( my $arg = shift @args ) {
	die <<EOM if $arg =~ /^-([a-z])/ and not exists $config{ ( $arg =~ /^-([a-z])/ )[ 0 ] };
unrecognized option: $1
usage: $0 [-n days] [-f format] [-d dir] [file file file ...]

	days:   show events this many days from now or less
	format: date format (e.g. 'MM/DD/YYYY', 'DD/MM/YYYY', etc.)
	dir:    directory where event files are stored
EOM

	$arg =~ /^-([a-z])$/ ? $config{ $1 } = shift @args : push @files, $arg;
}

#
# set up nearness indicators map
#

my %indicators = (
	0 =>  '   ===>',
	1 =>  '   -->',
	2 =>  '  -->',
	3 =>  ' -->',
	4 =>  '-->',
	5 =>  '->',
	6 =>  '>',
	7 =>  ' '
);

#
# figure out the current month and year
#

my $now       = time;
my $now_month = POSIX::strftime( '%m',     localtime $now );
my $now_year  = POSIX::strftime( '%Y',     localtime $now );
my $now_ymd   = POSIX::strftime( '%Y%m%d', localtime $now );
my $now_jday  = POSIX::strftime( '%j',     localtime $now );

#
# interpret the format and configure pattern mapping accordingly
#

my %positions;
my $regex = '^';
my $pos = 0;
foreach my $piece ( $config{ t } =~ /^([MDY]+)([^MDY]*)([MDY]+)([^MDY]*)([MDY]+)/ ) {
	if ( $piece =~ /^([MDY])/ ) {
		$positions{ $1 } = $pos++;
		$regex .= $piece =~ /^D/ ? '(\d+)' : '(\d+|\s*?)';
	}
	else {
		$regex .= '(?:' . $piece . '|\s)';
	}
}
$regex .= '\s*(.*)';

#
# build the list of files we need to read (unless they were supplied)
#

unless ( @files ) {
	opendir DIR, $config{ d } or die "can't opendir $config{ d }: $!";
	@files = grep { !/^\.{1,2}/ && -f "$config{ d }/$_" } readdir DIR;
	closedir DIR;
}

#
# work through files, displaying near-enough events from each
#

foreach my $file ( @files ) {
	my %events;

	open FILE, "< $config{ d }/$file" or die "$file: $!";
	while ( my $line = <FILE> ) {
		next if $line =~ /^(#.*|)$/;

		chomp $line;
		my @fields = $line =~ m#$regex#o;

		my $month = $fields[ $positions{ M } ];
		my $day   = $fields[ $positions{ D } ];
		my $year  = $fields[ $positions{ Y } ];

		next if $year =~ /^\d+$/ && $year < $now_year;

		#
		# if have been given an exact month, day and year, store the event and move on
		#

		if ( $month =~ /^\d+$/ && $day =~ /^\d+$/ && $year =~ /^\d+$/ ) {
			my $event_epoch = POSIX::strftime( '%s', 0, 0, 0, $day, $month - 1, $year - 1900 );
			push @{ $events{ $event_epoch } }, $fields[ 3 ];
		}

		#
		# otherwise, build as many events as necessary to accomodate the wildcards and $config{ n }
		#

		else {
			my @years = $year =~ /^\d+$/ ?
			            ( $year ) :
			            map { $now_year + $_ } 0 .. int( ( $config{ n } + $now_jday ) / 365 );
			my @months = $month =~ /^\d+$/ ? ( $month ) : 1 .. 12;
			foreach my $y ( @years ) {
				foreach my $m ( @months ) {
					my $event_epoch = POSIX::strftime( '%s', 0, 0, 0, $day, $m - 1, $y - 1900 );
					push @{ $events{ $event_epoch } }, $fields[ 3 ];
				}
			}
		}
	}
	close FILE;

	#
	# generate a report for the file, showing only date withing $config{ n } days from now
	#

	my $file_report = '';
	foreach my $event_epoch ( sort { $a <=> $b } keys %events ) {
		my ( $first, $sep, $second ) = $config{ t } =~ m/([MD])([^MD]+)([MD])/;
		my $date = POSIX::strftime( '%' . lc $first . $sep . '%' . lc $second, localtime $event_epoch );
		foreach my $event ( @{ $events{ $event_epoch } } ) {
			my $days_away = int( ( ( $event_epoch + 84600 ) - $now ) / 86400 );
			next if $days_away < 0 or $days_away > $config{ n };
			my $indicator = $indicators{ $days_away } || "($days_away)";
			$file_report .= sprintf "  %-7s %s   %s\n", $indicator, $date, $event;
		}
	}

	#
	# show the report for the file if we have one
	#

	print "$file:\n$file_report\n" if $file_report;
}
