#!/usr/bin/perl
# Filename:	dateCalc
# Author:	David Ljung Madison <DaveSource.com>
# See License:	http://MarginalHacks.com/License/
# Description:	Date math

use strict;
use Time::Local;

##################################################
# Setup the variables
##################################################
my $PROGNAME = $0; $PROGNAME =~ s|.*/||;
my ($BASENAME,$PROGNAME) = ($0 =~ m|(.*)/(.+)|) ? ($1?$1:'/',$2) : ('.',$0);


##################################################
# Usage
##################################################
sub fatal {
	foreach my $msg (@_) { print STDERR "[$PROGNAME] ERROR:  $msg\n"; }
	exit(-1);
}

sub usage {
	foreach my $msg (@_) { print STDERR "ERROR:  $msg\n"; }
	print STDERR <<USAGE;

Usage:\t$PROGNAME [-d] <date>[<op><date>..]
  Performs date math
  -d     Set debug mode
  -date  Show result as date '12/3/71' (default if answer > 1 year)
  -time  Show result as time '6d3h40s' (default if answer < 1 year)
  -sec   Show result as number of seconds
  -s     Show result as number of seconds, no trailing 's' or newline
  -days  Show result as number of days (floating point)
  -weeks Show result as number of weeks (floating point)
  -wds   Show week/days
  -all   Show result as date, time, number of days/seconds

<date>   A date, such as 1/2/2010 or a time period, such as 10d3h
<op>     Operations, currently + or -

Dates can be in m/d/y or y/m/d format.

Examples:

% dateCalc today-6d3h2s
% dateCalc 1/2/2010-12/3/1971

Note that differences may include daylight savings time, for example:
% dateCalc 11/8/2010 - 11/7/2010

USAGE
	exit -1;
}

sub parseArgs {
	my $opt = {};
	while (my $arg=shift(@ARGV)) {
		if ($arg =~ /^-h$/) { usage(); }
		if ($arg =~ /^-d$/) { $MAIN::DEBUG=1; next; }
		if ($arg =~ /^-date$/) { $opt->{date}=1; next; }
		if ($arg =~ /^-time$/) { $opt->{time}=1; next; }
		if ($arg =~ /^-sec$/) { $opt->{sec}=1; next; }
		if ($arg =~ /^-s$/) { $opt->{s}=1; next; }
		if ($arg =~ /^-days?$/) { $opt->{days}=1; next; }
		if ($arg =~ /^-weeks?$/) { $opt->{weeks}=1; next; }
		if ($arg =~ /^-wds?$/) { $opt->{wd}=1; next; }
		if ($arg =~ /^-all$/) { $opt->{date}=1; $opt->{time}=1; $opt->{sec}=1; $opt->{days}=1; $opt->{weeks}=1; $opt->{wd}=1; next; }
		#if ($arg =~ /^-./) { usage("Unknown option: $arg"); }
		$opt->{calc}.=$arg;
	}
	usage("No dates/ops defined") unless $opt->{calc};
	
	$opt;
}

sub debug {
	return unless $MAIN::DEBUG;
	foreach my $msg (@_) { print STDERR "[$PROGNAME] $msg\n"; }
}

my $YEAR = 365*24*60*60;
sub date {
	my ($date) = @_;
	$date =~ s/^\s*//; $date =~ s/\s*$//;

	# Date
	if ($date =~ m|^(\d+)/(\d+)(?:/(\d+))?(\s+(\d+):(\d+):(\d+))?$|) {
		my ($m,$d,$y) = ($1,$2,$3);
		my ($hour,$min,$sec) = ($5,$6,$7);
		$y ||= 1900+(localtime(time))[5];
		# Dates can be in m/d/y or y/m/d format.
		($y,$m,$d) = ($m,$d,$y) if $m>$y;
		$m--;
		return timelocal($sec,$min,$hour,$d,$m,$y);
	}

	# 6d2h3s...
	if ($date =~ m|(\d+)[ywdmhs]|) {
		my $sec = 0;
		while ($date =~ s|^(\d+\.?\d*)([ywdmhs])||) {
			my ($num,$unit) = ($1,$2);
			$sec += $num if $unit eq 's';
			$sec += 60*$num if $unit eq 'm';
			$sec += 60*60*$num if $unit eq 'h';
			$sec += 24*60*60*$num if $unit eq 'd';
			$sec += 7*24*60*60*$num if $unit eq 'w';
			$sec += $YEAR*$num if $unit eq 'y';	# Hacky.
		}
		usage("Couldn't understand rest of time period [$date]") if $date;
		return $sec;
	}

	# "today"
	return time if $date =~ /^(today|now)$/i;

	# Number of seconds since epoch
	return $date if $date !~ /\D/;

	usage("Unknown date format  [$date]");
}

sub splitNum {
	my ($num,$base) = @_;
	my $top = int($num/$base);
	$num -= $top*$base;
	($top,int($num))

	## Great for integers
	#my $rem = $num % $base;
	#$num -= $rem;
	#($num / $base, $rem);
}

sub unsec_date {
	my ($sec) = @_;
	my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($sec);
	$mon++; $year += 1900;
	my $date = sprintf("%0.4d/%0.2d/%0.2d", $year,$mon,$mday);
	$date .= " ${hour}h" if $hour;
	$date .= " ${min}m" if $min;
	$date .= " ${sec}s" if $sec;
	$date;
}

sub unsec_time {
	my ($sec) = @_;

	my ($year,$mon,$week,$day,$hour,$min);
	# Pull years out first, then months, since months are actually fractional days (365d/12)
	($year,$sec) = splitNum($sec,365*24*60*60);
	($mon,$sec) = splitNum($sec,365*24*60*60/12);
	($min,$sec) = splitNum($sec,60);
	($hour,$min) = splitNum($min,60);
	($day,$hour) = splitNum($hour,24);
	($week,$day) = splitNum($day,7);	# Break up >7 days into weeks

	my @date;
	push(@date, "${year}y") if $year;
	push(@date, "${mon}m")  if $mon;
	push(@date, "${week}w") if $week;
	push(@date, "${day}d")  if $day;
	push(@date, "${hour}h") if $hour;
	push(@date, "${min}m")  if $min;
	push(@date, "${sec}s")  if $sec;

	return "0s" unless @date;
	join(' ',@date);
}

sub unsec {
	my ($opt,$sec) = @_;

	unless ($opt->{time} || $opt->{date} || $opt->{sec} || $opt->{s} || $opt->{week} || $opt->{wd}) {
		# Heuristic.  It's a date if it's more than 1 year
		($sec>$YEAR) ? ($opt->{date}=1) : ($opt->{time}=1);
	}

	return print $sec if $opt->{s};
	print unsec_date($sec),"\n" if $opt->{date};
	print unsec_time($sec),"\n" if $opt->{time};
	my $d = $sec/(24*60*60);
	my $w = $d/7;
	my $wdw = int($w);
	my $wdd = int($d)-7*$wdw;
	printf "%0.4fd\n",$d if $opt->{days};
	printf "%0.4fw\n",$w if $opt->{weeks};
	print "${wdw}w ${wdd}d\n" if $opt->{wd};
	print "${sec}s\n" if $opt->{sec};
}


sub main {
	my $opt = parseArgs();
	
	my @c = split(/\s*([+-])\s*/,$opt->{calc});
	my $amt = date(shift(@c));
	#print "START: $amt\n";
	while (@c) {
		my ($op,$what) = (shift(@c),date(shift(@c)));
		usage("Op without value?") unless defined $what;
		$amt += $what if $op eq '+';
		$amt -= $what if $op eq '-';
	}

	unsec($opt,$amt);
}
main();
