#!/usr/bin/env perl
########################################################################
# Given a date in the form Y:M:D on the command line, this program will
# output the number of days since a particular reference date.
# The reference date may also be supplied on the command line but will
# default to 1850:1:1.
#
# Larry Solheim Feb 2012
#
# $Id$
########################################################################

  require 5;

  # Declare global variables
  use vars qw(%epoch %curr $verbose);

  # Identify this script by name
  chomp($Runame = `basename $0`);

  # verbose determines the amount of debug output
  $verbose = 1;

  # quiet = 1 means provide a minimal amout of output (just the value of days)
  $quiet = 0;

  # epoch is the reference date
  $epoch{year} = 1850;
  $epoch{mon}  = 1;
  $epoch{day}  = 1;

  # The value that is returned will be the difference in days
  # between the current date and the reference date
  $curr{year} = 1850;
  $curr{mon}  = 1;
  $curr{day}  = 1;

  # require_dates = 0 means allow date fields to be empty and supply a default if they are
  # require_dates = 1 means do not allow date fields to be empty
  $require_dates = 0;

  # Number of days since Jan 1 0:0:0 to the start of each month (365 day calendar)
  # The [0] index is ignored below so that index [1] corresponds to month 1 etc
  @SOM = ( 0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 );

  # Number of days in each month (365 day calendar)
  # The [0] index is ignored below so that index [1] corresponds to month 1 etc
  @DPM = ( 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );

  # Define a usage function
  $Usage = sub {
    my ($msg)=@_;
    if ($msg) {print "${Runame}: $msg\n"};
    print <<EOR;
  Usage: $Runame [options] date
         date is of the form --> Y:M:D <--
Purpose: Determine the number of days since a reference date
Options:
  --reference=DATE ...set the reference date (default 1850:1:1)
  --strict         ...require all date fields to be present
                      Without this option, if any component of date is missing
                      then it will be set to the default value
  --verbose        ...increase verbosity (additive)
  --quiet          ...output only the value of days since the reference date
                      Quiet will override verbose if both options are present.
EOR
  die "\n";
};

  # Process command line arguments
  use Getopt::Long;
  $Getopt::Long::ignorecase = 0;
  $Getopt::Long::order = $PERMUTE;
  &GetOptions(
    "help"         => \&$Usage,
    "verbose"      => sub {$verbose++},
    "quiet!"       => \$quiet,
    "strict!"      => \$require_dates,
    "reference=s"  =>
      sub {
        @Date  = split /\s*:\s*/,$_[1];
        if ( scalar(@Date) > 3 ) {
          die "${Runame}: Invalid reference date --> $_[1] <--\n";
        }
        if ( $require_dates ) {
          # Ensure that all date fields are present
          if ( scalar(@Date) < 3 ) {
            die "${Runame}: A reference date field is missing when using strict. --> $_[1] <--\n";
          }
          foreach my $d (@Date) {
            unless (defined $d and $d =~ /.+/) {
              die "${Runame}: A reference date field is missing when using strict. --> $_[1] <--\n";
            }
          }
        }
        $epoch{year} = $Date[0] if (defined $Date[0] and $Date[0] =~ /.+/);
        $epoch{mon}  = $Date[1] if (defined $Date[1] and $Date[1] =~ /.+/);
        $epoch{day}  = $Date[2] if (defined $Date[2] and $Date[2] =~ /.+/);
      undef @Date;
      },
    "<>"           => sub {push @NonOpt,$_[0]})
      or die "${Runame}: Error on command line.\n";

  unless ( scalar(@NonOpt) ) {
     &$Usage("A date must be supplied on the command line.\n");
  }

  # Any non-option command line args should be a date (Y:M:D)
  foreach my $arg (@NonOpt) {
    next unless $arg;
    # Strip any enclosing single or double quotes
    $arg =~ s/^'(.*?)'$/$1/;
    $arg =~ s/^"(.*?)"$/$1/;

    @Date  = split /\s*:\s*/, $arg;
    if ( scalar(@Date) > 3 ) {
      die "${Runame}: Invalid date --> $arg <-- supplied on command line.\n";
    }
    if ( $require_dates ) {
      # Ensure that all date fields are present
      if ( scalar(@Date) < 3 ) {
        die "${Runame}: A date field is missing when using strict. --> $arg <--\n";
      }
      foreach (@Date) {
        unless (defined $_ and $_ =~ /.+/) {
          die "${Runame}: A date field is missing when using strict. --> $arg <--\n";
        }
      }
    }
    # Use any defined elements of Date to set the corresponding hash element
    # as long as they contain at least 1 character
    $curr{year} = $Date[0] if (defined $Date[0] and $Date[0] =~ /.+/);
    $curr{mon}  = $Date[1] if (defined $Date[1] and $Date[1] =~ /.+/);
    $curr{day}  = $Date[2] if (defined $Date[2] and $Date[2] =~ /.+/);
    undef @Date;

    # Ignore all but the first non option command line arg
    last;
  }

  # Reset verbose if the user has requested minimal output
  $verbose = 0 if $quiet;

  # Sanity checks for current date
  foreach my $k ( keys %curr ) {
    # Require integers only for the value of each hash element
    # except day, which may contain a period
    if ( $k =~ /^day$/ ) {
      # day can be a floating point real number
      $curr{$k} !~ /^\s*[0-9\.]+\s*$/ and
        die "${Runame}: Current $k invalid --> $curr{$k} <--\n";
    } else {
      $curr{$k} !~ /^\s*[0-9]+\s*$/ and
        die "${Runame}: Current $k invalid --> $curr{$k} <--\n";
    }
  }
  if ( $curr{year} < 0 ) {
    die "${Runame}: Current year = $curr{year} is less thatn zero.\n";
  }
  if ( $curr{mon} < 1 or $curr{mon} > 12 ) {
    die "${Runame}: Current month = $curr{mon} is out of range.\n";
  }
  if ( $curr{day} < 1 or $curr{day} > 1+$DPM[$curr{mon}] ) {
    die "${Runame}: Current day = $curr{day} is out of range.\n";
  }

  # Sanity checks for reference date
  foreach my $k ( keys %epoch ) {
    # Require integers only for the value of each hash element
    # except day, which may contain a period
    if ( $k =~ /^day$/ ) {
      # day can be a floating point real number
      $epoch{$k} !~ /^\s*[0-9\.]+\s*$/ and
        die "${Runame}: Reference $k invalid --> $epoch{$k} <--\n";
    } else {
      $epoch{$k} !~ /^\s*[0-9]+\s*$/ and
        die "${Runame}: Reference $k invalid --> $epoch{$k} <--\n";
    }
  }
  if ( $epoch{year} < 0 ) {
    die "${Runame}: Reference year = $epoch{year} is less thatn zero.\n";
  }
  if ( $epoch{mon} < 1 or $epoch{mon} > 12 ) {
    die "${Runame}: Reference month = $epoch{mon} is out of range.\n";
  }
  if ( $epoch{day} < 1 or $epoch{day} > 1+$DPM[$epoch{mon}] ) {
    die "${Runame}: Reference day = $epoch{day} is out of range.\n";
  }

  # Determine the number of days between the current date and the reference date
  # Days will be negative if the current date preceeds the reference date

  # Determine day of the year for current and reference dates
  my $doy_curr  = $SOM[$curr{mon}]  + $curr{day};
  my $doy_epoch = $SOM[$epoch{mon}] + $epoch{day};

  my $diff_years = $curr{year} - $epoch{year};

  if ( $curr{year} == $epoch{year} ) {
    $days = $doy_curr - $doy_epoch;
  } else {
    if ( $curr{year} < $epoch{year} ) {
      $days = (365-$doy_curr) + $doy_epoch + 365 * ($epoch{year} - $curr{year} - 1);
      $days = -$days;
    } else {
      # curr year > epoch year
      $days = (365-$doy_epoch) + $doy_curr + 365 * ($curr{year} - $epoch{year} - 1);
    }
  }

  if ($verbose > 0) {
    printf "       Reference date: %4d-%2.2d-%2.2d\n",$epoch{year},$epoch{mon},$epoch{day};
    printf "         Current date: %4d-%2.2d-%2.2d\n",$curr{year}, $curr{mon}, $curr{day};
    printf "days since %4d-%2.2d-%2.2d: %g\n",$epoch{year},$epoch{mon},$epoch{day},$days;
  } else {
    print $days;
  }

  exit 0;
########################################################
##################### End of main ######################
########################################################
