package Date::Manip;

# Copyright (c) 1995,1996 Sullivan Beck. All rights reserved.
# This program is free software; you can redistribute it and/or modify it
# under the same terms as Perl itself.

###########################################################################
# CUSTOMIZATION
###########################################################################
#
# See the section of the POD documentation section CUSTOMIZING DATE::MANIP
# below for a complete description of each of these variables.

# Location of a the global config file.  Tilde (~) expansions are allowed.
$Date::Manip::GlobalCnf="";
$Date::Manip::IgnoreGlobalCnf="";

### Date::Manip variables set in the global config file

# Name of a personal config file and the path to search for it.  Tilde (~)
# expansions are allowed.
$Date::Manip::PersonalCnf=".DateManip.cnf";
$Date::Manip::PersonalCnfPath=".:~";

### Date::Manip variables set in the global or personal config file

# Which language to use when parsing dates.
$Date::Manip::Language="English";

# 12/10 = Dec 10 (US) or Oct 12 (anything else)
$Date::Manip::DateFormat="US";

# Local timezone
$Date::Manip::TZ="";

# Timezone to work in (""=local, "IGNORE"=ignore timezones, or a timezone)
$Date::Manip::ConvTZ="";

# Date::Manip internal format (0=YYYYMMDDHH:MN:SS, 1=YYYYHHMMDDHHMNSS)
$Date::Manip::Internal=0;

# First day of the week (0=sunday, 6=saturday)
$Date::Manip::FirstDay=0;

# First and last day of the work week  (0=sunday, 6=saturday)
$Date::Manip::WorkWeekBeg=1;
$Date::Manip::WorkWeekEnd=5;

# If non-nil, a work day is treated as 24 hours long (WorkDayBeg/WorkDayEnd
# ignored)
$Date::Manip::WorkDay24Hr=0;

# Start and end time of the work day (any time format allowed, seconds ignored)
$Date::Manip::WorkDayBeg="08:00";
$Date::Manip::WorkDayEnd="17:00";

# Erase the old holidays
$Date::Manip::EraseHolidays="";

# Set this to non-zero to be produce completely backwards compatible deltas
$Date::Manip::DeltaSigns=0;

###########################################################################

require 5.000;
require Exporter;
@ISA = qw(Exporter);
@EXPORT = qw(
   DateManipVersion
   ParseDate
   UnixDate
   DateCalc
   ParseDateDelta

   Date_SetTime
   Date_GetPrev
   Date_GetNext
   Date_DayOfWeek
   Date_SecsSince1970
   Date_SecsSince1970GMT
   Date_DaysSince999
   Date_DayOfYear
   Date_DaysInYear
   Date_WeekOfYear
   Date_LeapYear
   Date_DaySuffix
   Date_TimeZone
   Date_ConvTZ
   Date_Init
   Date_IsWorkDay
   Date_NextWorkDay
   Date_PrevWorkDay
);
use strict;
#use POSIX qw(tzname);

########################################################################
# HISTORY
########################################################################

# Written by:
#    Sullivan Beck (beck@qtp.ufl.edu)
# Any suggestions, bug reports, or donations :-) should be sent to me.

# Version 1.0  01/20/95
#    Combined all routines into one library
#
# Version 1.1  02/08/95
#    Added leap year checking
#    Both "Feb" and "February" formats available
#
# Version 1.2  03/31/95
#    Made months case insensitive
#    Added a few date formats
#
# Version 2.0  04/17/95
#    Included ideas from packages
#       Time::ParseDate (David Muir Sharnoff <muir@idiom.com>)
#       date.pl 3.2     (Terry McGonigal <tmcgonigal@gvc.com>)
#    Made error checking much nicer
#    Added seconds to ParseDate
#
# Version 3.0  05/03/95
#    Added %DATE_ global variable to clean some stuff up
#    Simplified several routines
#    Added today/now/tomorrows/etc. formats
#    Added UnixDate
#    Added ParseDateDelta
#
# Version 4.0  08/13/95
#    Switched to perl 5
#    Cleaned up ParseDate, ParseDateDelta
#    Added time first formats to ParseDate
#    First public release
#
# Version 4.1  10/18/95
#    Changed %DATE_ to %DateManip::Date
#    Rewrote ParseDateDelta
#    Added DateCalc
#
# Version 4.2  10/23/95
#    UnixDate will now return a scalar or list depending on context
#    ParseDate/ParseDateDelta will now take a scalar, a reference to a
#       scalar, or a eference to an array
#    Added copyright notice (requested by Tim Bunce <Tim.Bunce@ig.co.uk>)
#    Simple timezone handling
#    Added Date_SetTime, Date_GetPrev, Date_GetNext
#
# Version 4.3  10/26/95
#    Added "which dofw in mmm" formats to ParseDate
#    Added a bugfix of Adam Nevins where "12:xx pm" used to be parsed
#        "24:xx:00".
#
# Version 5.00  06/21/96
#    Switched to a package (patch supplied by Peter Bray
#       <pbray@ind.tansu.com.au>)
#       o  renamed to Date::Manip
#       o  changed version number to 2 decimal places
#       o  added POD documentation
#       Thanks to Peter Bray, Randal Schwartz, Andreas Koenig for suggestions
#    Fixed a bug pointed out by Peter Bray where it was complaining of
#       an uninitialized variable.
#
# Version 5.01  06/24/96
#    Fixes suggested by Rob Perelman <robp@electriciti.com>
#       o  Fixed a typo (Friday misspelled Fridat)
#       o  Documentation problem for \$err in DateCalc
#       o  Added %F formtat to UnixDate
#    Reworked a number of the ParseDate regular expressions to make
#       them more flexible
#    Added "Date at Time" types
#    Weekdays can be entered and checked
#    Two digit years fall in the range CurrYear-89 to CurrYear+10
#
# Version 5.02  07/15/96
#    Fixed a bug where repeated calls to ParseDate("today") was not reset
#    Replaced the %Date::Manip::Date variable with a large number of
#       other, more flexible variables
#    Added some internationalization (most of the routines had to be
#       modified at least slightly)
#    Rewrote the Init routine
#
# Version 5.03  07/17/96
#    Fixed a couple of bugs in UnixDate.
#    Declared package variables to avoid warning "Identifier XXX used
#       only once".  Thanks to Peter Bray for the suggestion.
#
# Version 5.04  08/01/96
#    Added support for fractional seconds (as generated by Sybase).  They
#       are parsed and ignored.  Added by Kurt Stephens
#       <stephens@il.us.swissbank.com>.
#    Fixed bugs reported by J.B. Nicholson-Owens
#       <jbn@mystery-train.cu-online.com>
#       o  "Tue Jun 25 1996" wasn't parsed correctly (regexp was case
#          sensitive)
#       o  full day names not parsed correctly
#       o  the default day in ErrorCheck should be 1, NOT currd since when
#          currd>28, it may not be a valid date for the month
#
# Version 5.05  10/11/96
#    Added Swedish translation (thanks to Andreas Johansson
#       <Andreas.XS.Johansson@trab.se>
#    Fixed bad mistake in documentation (use Date::Manip instead of
#       use DateManip) pointed out by tuc@valhalla.stormking.com
#    Fixed bug introduced in 5.04 when default day set to 1.  When no
#       date given, have day default to today rather than 1.  It only
#       defaults to one if a partial date is given.
#    Changed deltas to be all positive or all negative when produced by
#       DateCalc.  Suggested by Steve Braun <braun@gandalf.sp.trw.com>
#    Fixed bug where Date_DaysSince999 returned the wrong value (the
#       error did not affect any other functions in Date::Manip due to
#       the way it was called and the nature of the error).  Pointed out
#       by Jason Baker <bm11455@themis.ag.gov.bc.ca>.
#    Minor improvements to documentation.
#    Documented the 'sort within a sort' bug.
#    Added DateManipVersion routine.
#    Dates with commas in them are now read properly.
#    Now supports timezones.
#    Parses RFC 822 dates (thanks to J.B. Nicholson-Owens
#       <jbn@mystery-train.cu-online.com> for suggestion).
#    Parses ctime() date formats (suggested by Matthew R. Sheahan
#       <chaos@crystal.palace.net>).
#    Added Date_ConvTZ routine for timezone support.
#    Fixed two places where a variable was declared twice using my (thanks
#       to Ric Steinberger <ric@isl.sri.com>).
#    Hopefully fixed installation problems.
#    Now supports times like "noon" and "midnight".
#    Got rid of the last (I think) couple of US specific strings.
#    The time separators are now language specific so the French can
#       write "10h30" and the Swedes can write "10.30".  Suggested by
#       Andreas Johansson <Andreas.XS.Johansson@trab.se>.
#    Fixed type in documentation/README pointed out by James K. Bence
#       <jbence@math.ucla.edu>.
#    Fixed bug in Date_SetTime (didn't work with $hr,$min,$sec < 10).
#    Added ModuloAddition routine and simplified DateCalc.
#    Date_TimeZone will now also check `date '+%Z'` suggested by
#       Aharon Schkolnik <aharon@healdb.matat.health.gov.il>.
#
# Version 5.06  10/25/96
#    Fixed another two places where a variable was declared twice using my
#       (thanks to Ric Steinberger <ric@isl.sri.com>).
#    Fixed a bug where fractional seconds weren't parsed correctly.
#    Fixed a bug where "noon" and other special times were not parsed
#       in the "which day of month" formats.
#    Added "today at time" formats.
#    Fixed a minor bug where a few matches were case sensitive.
#    ParseDateDelta now normalizes the delta as well as DateCalc.
#    Added %Q format "YYYYMMDD" to UnixDate.  Requested by Rob Perelman
#       <robp@electriciti.com>.
#    The command "date +%Z" doesn't work on SunOS machines (and perhaps
#        others) so 5.05 is effectively broken.  5.06 released to fix this.
#        Reported by Rob Perelman <robp@electriciti.com>.
#
# Version 5.07  12/10/96
#    Huge number of code changes to clean things up.
#    Added %q format "YYYYMMDDHHMMSS" to UnixDate.  Requested by Rob Perelman
#       <robp@electriciti.com>.  Also added %P format "YYYYMMDDHH:MM:SS".
#    Added a new config variable to allow you to work with multiple internal
#       formats (with and without colons).  Requested by Rob Perelman
#       <robp@electriciti.com>.  See Date_Init documentation.
#    Added the following formats suggested by Andreas Johansson
#       <Andreas.XS.Johansson@trab.se>:
#          sunday week 22 [in 1996] [at 12:00]
#          22nd sunday [in 1996] [at 12:00]
#          sunday 22nd week [in 1996] [at 12:00]
#    Added weeks to ParseDateDelta.  Suggested by Mike Bassman
#       <mbassman@fia21-43.fiadev21.lehman.com>.  Note that since
#       this is a late addition, I did not change the internal format
#       of a delta.  Instead, it is added to the days field.
#    Added a new config variable to allow you to define the first day of
#       the week.  See Date_Init documentation.
#    Added the following formats to ParseDate for conveniance (some were
#       suggested by Mike Bassman <mbassman@fia21-43.fiadev21.lehman.com>):
#          next/last friday [at time]
#          next/last week [at time]
#          in 2 weeks [at time]
#          2 weeks ago [at time]
#          Friday in 2 weeks
#          in 2 weeks on friday
#          Friday 2 weeks ago
#          2 weeks ago friday
#    Added Date_SecsSince1970GMT, moved the %s format to %o (secs since 1/1/70)
#       and added %s format (secs since 1/1/70 GMT).  Based on suggestions by
#       Mark Osbourne <marko@lexis-nexis.com>.  Note this introduces a minot
#       backward incompatibility!
#    Date_SetTime now works with international time separators.
#    Changed how Date_Init arguments work.
#    Fixed bug in Date_TimeZone where it didn't recognize +HHMN type time
#       zones.  Thanks to Are Bryne <are.bryne@communique.no>.
#    Added the %g format (%a, %d %b %Y %H:%M:%S %z) for an RFC 1123 date.
#       Suggested by Are Bryne <are.bryne@communique.no>.
#    Added WindowsNT check to Date_TimeZone to get around NT's weird date
#       command.  Thanks to Are Bryne <are.bryne@communique.no>.
#    Subroutines now check to see if 4 digit years are entered.  Suggested
#       by Are Bryne <are.bryne@communique.no>.
#    Fixed typo (CSD instead of CST).
#    Now reads a config file.
#    Added options to delete existing holidays and ignore global config file.
#    The d:h:mn:s of ALL deltas are normalized.
#    Added local($_) to all routines which use $_.  Suggested by Rob
#       Perelman <robp@electriciti.com>.
#    Date_GetNext and Date_GetPrev now return the next/prev occurence of a
#       time as well as a day.  Suggested by Are Bryne
#       <are.bryne@communique.no>.
#    Complete rewrite of DateCalc.
#    In approximate mode, deltas now come out completely normalized (only 1
#       sign).  Suggested by Rob Perelman <robp@electriciti.com>.
#    Added business mode.  See documentation.  Suggested by Mike Bassman
#       <mbassman@fia21-43.fiadev21.lehman.com>.
#    Modified how deltas are normalized and added the DeltaSigns config
#       variable.
#    Added test suite!
#    Fixed sign in military timezones making Date::Manip RFC 1123 compliant
#       (except that timezone information is not stored in any format)
#    Added Date::Manip::InitDone so initialization isn't duplicated.
#    Added a 3rd internal format to store YYYY-MM-DD HH:MN:SS (iso 8601).
#    Fixed a bug where UnixDate %E format didn't work with single digit
#       dates.  Patch supplied by J\yrgen N\yrgaard <jnp@www.ifs.dk>.
#    Added a config variable to allow you to work with 24 hour business
#       days.  Suggested by Mike Bassman
#       <mbassman@fia21-43.fiadev21.lehman.com>.
#    ParseDateDelta now returns "" rather than "+0:0:0:0:0:0" when there is
#       an error.
#    Fixed a bug where "today" was not converted to the correct timezone.
#
# Version 5.07p2  01/03/97
#    Added lots of timezone abbreviations.
#    Can now understand PST8PDT type zones (but only in Date_TimeZone).
#    Fixed some tests (good for another year).
#    Fixed a bug where a delta component of "-0" would mess things up.
#       Reported by Nigel Chapman <nigel@macavon.demon.co.uk>.
#    Released two patches for 5.07.
#
# Version 5.08  01/24/97
#    Fixed serious bug in ConvTZ pointed out by David Hall
#       <dhall@sportsline.com>.
#    Modified Date_ConvTZ (and documented it).
#    Released 5.08 to get this and the other two patches into circulation.

# Incompatibilities or changes from older version
#
# In 5.07
#   %s format changed
#   By default, the signs of stored in a different format (only minimum
#     number of signs stored).  Backwards compatible if you set DeltaSigns=1.
#   Date_Init arguments changed (old method supported but depreciated)

$Date::Manip::Version="5.08";

########################################################################
# TODO
########################################################################

################ NEXT VERSION

### SPEEDUPS

# use integer;   whenever possible

# in ParseDate/ParseDateDelta check for the internal format right away

# &Date_Init  if (! $Date::Manip::Initiailized)   to all calls to Date_Init

# UpdateHolidays, don't use ParseDate to parse dates of form DD/MM or MM/DD.

### ISO

# Make sure "-" is not a date separator ever. (reserved by ISO 8601).

# In order to conform to ISO 8601:
#
#   date=YYYY-MM-DD
#        YY-MM-DD
#        YYYY-MM
#        YY-MM
#        YYYYMMDD
#        YYMMDD
#        YYYY
#        YY
#
# By default, the week starts with Monday.
# The 1st week of the year is the first full week which contains at least
#    4 days in the year (so Jan 1 may actually be in one of the weeks of
#    the previous year or week 1 may contain Dec 31 of the previous year!).
#
# The first week of the year 1997 lasts from 1996-12-30 to 1997-01-05 can
# be specified:
#        1995-W01
#        1995W01
#        95W01
#
# The day 1996-12-31, which is the Tuesday (day 2) of the first week of 1997,
# can also be written as
#        1997-W01-2
#        1997W012
#        97W012
# Weeks are 1-53.
#
# Weekdays are numbered 1 (monday) to 7 (sunday).
#
# Formats with the day-of-year (001 to 366)
#        YYYY-DOY
#        YYYYDOY
#
# Times are given by:
#        HH:MN:SS
#        HH:MN
#        HHMNSS
#        HHMN
#        HH
#        HH:MN:SS.FFFFFFF
#        HHMNSSFFFFFFF
#
# Note:  24:00:00 is NOT supported (though ISO allows it).
#
# All times may have a timezone attached to the end (space between time
# and zone is optional) of the forms listed above as well as:
#        +HH:MN
#        +HHMN
#        +HH

# Add a UnixDate format to print out a date in the format:
#   YYYY-Wwwd

# check "Sunday 22nd week", "22nd Sunday" vs. ISO weeks

### TESTS

# Make sure test suite runs in an alternate timezone

# Add tests for all the new ParseDate formats to the test suite.

### MISC

# Add to ParseDate (Rob Perelman)
#    dofw           (Friday == &Date_GetNext("today","friday",0,"00:00:00")

# Document how you need to use the stock .DateManip.cnf file when running
# the tests.  Make sure that TZ=EST is set in the sample one.

# Add a bunch of dayofyear formats (dayofyear is a 3 digit field that
# replaces day/mon or mon/day.

# Fix Date::Manip:: Future,Past,Next,Prev

# Clean up ParseDate

# Clean up ParseDateDelta

# Combine GetNext and GetPrev (?)

# Make err an optional argument to DateCalc (by checking to see if the
# 3rd argument exists).  If it does and is a reference, it is err, otherwise,
# it is mode.

# Add a "SPECIAL HOLIDAY" section to fully specify holidays so weird ones
# can be defined for each year.

# Add
#   Spanish
#   German
#   Italian

# Fill in some of the language variables ($past, $future, $zones).

# Check Swedish/French special characters.

# Change EXPORT to EXPORT_OK (message 9 by Peter Bray)

# Use POSIX::tzname

################ MAYBE (undecided whether it should be added)

# Mike Bassman (mess 49)
#    "friday before last"

# $Date problems with RCS (mess 35 by Tim Freeman)

# Add equivalent of UnixDate to print out Deltas in various formats
# (mess 37 by Alan Burlison)

# Add full timezone and daylight saving time handling.

# Add "delta FROM date", "IN delta ON date", "delta AGO ON date"

########################################################################
########################################################################
#
# Declare variables so we don't get any warnings about variables only
# being used once.  In Date_Init, I often define a whole batch of related
# variables knowing that I only have immediate use for some of them but
# I may need others in the future.  To avoid the "Identifier XXX used only
# once: possibly typo" warnings, all are declared here.
#
# Pacakge Variables
#

$Date::Manip::Am = undef;
$Date::Manip::AmExp = undef;
$Date::Manip::AmPmExp = undef;
$Date::Manip::Approx = undef;
$Date::Manip::At = undef;
$Date::Manip::Business = undef;
$Date::Manip::Curr = undef;
$Date::Manip::CurrAmPm = undef;
$Date::Manip::CurrD = undef;
$Date::Manip::CurrH = undef;
$Date::Manip::CurrHolidayYear = 0;
$Date::Manip::CurrM = undef;
$Date::Manip::CurrMn = undef;
$Date::Manip::CurrS = undef;
$Date::Manip::CurrY = undef;
$Date::Manip::DExp = undef;
$Date::Manip::DayExp = undef;
$Date::Manip::Exact = undef;
$Date::Manip::Future = undef;
$Date::Manip::HExp = undef;
$Date::Manip::In = undef;
$Date::Manip::Init = 0;
$Date::Manip::InitDone = 0;
$Date::Manip::InitFilesRead = 0;
$Date::Manip::MExp = undef;
$Date::Manip::MnExp = undef;
$Date::Manip::Mode = undef;
$Date::Manip::MonExp = undef;
$Date::Manip::Next = undef;
$Date::Manip::Now = undef;
$Date::Manip::Offset = undef;
$Date::Manip::On = undef;
$Date::Manip::Past = undef;
$Date::Manip::Pm = undef;
$Date::Manip::PmExp = undef;
$Date::Manip::Prev = undef;
$Date::Manip::ResetWorkDay = 1;
$Date::Manip::SepHM = undef;
$Date::Manip::SepMS = undef;
$Date::Manip::SepSS = undef;
$Date::Manip::SExp = undef;
$Date::Manip::TimesExp = undef;
$Date::Manip::UpdateHolidays = 0;
$Date::Manip::WDBh = undef;
$Date::Manip::WDBm = undef;
$Date::Manip::WDEh = undef;
$Date::Manip::WDEm = undef;
$Date::Manip::WDlen = undef;
$Date::Manip::WExp = undef;
$Date::Manip::WhichExp = undef;
$Date::Manip::WkExp = undef;
$Date::Manip::YExp = undef;
$Date::Manip::ZoneExp = undef;

@Date::Manip::Day = ();
@Date::Manip::Mon = ();
@Date::Manip::Month = ();
@Date::Manip::W = ();
@Date::Manip::Week = ();
@Date::Manip::Wk = ();

%Date::Manip::AmPm = ();
%Date::Manip::CurrHolidays = ();
%Date::Manip::Day = ();
%Date::Manip::Holidays = ();
%Date::Manip::Month = ();
%Date::Manip::Offset = ();
%Date::Manip::Times = ();
%Date::Manip::Replace = ();
%Date::Manip::Week = ();
%Date::Manip::Which = ();
%Date::Manip::Zone = ();

########################################################################
########################################################################

sub DateManipVersion {
  return $Date::Manip::Version;
}

sub Date_SetTime {
  my($date,$h,$mn,$s)=@_;
  &Date_Init()  if (! $Date::Manip::InitDone);
  my($y,$m,$d)=();

  if (! &CheckDate($date)) {
    $date=&ParseDate($date);
    return ""  if (! $date);
  }

  ($y,$m,$d)=( &CheckDate($date) )[0..2];
  ($h,$mn,$s)=&ParseTime($h,$mn,$s);

  my($ampm,$wk);
  return ""  if (&Date_ErrorCheck(\$y,\$m,\$d,\$h,\$mn,\$s,\$ampm,\$wk));
  &FormDate($y,$m,$d,$h,$mn,$s);
}

sub Date_GetPrev {
  my($date,$day,$today,$hr,$min,$sec)=@_;
  &Date_Init()  if (! $Date::Manip::InitDone);
  my($y,$m,$d,$h,$mn,$s,$err)=();

  if (! &CheckDate($date)) {
    $date=&ParseDate($date);
    return ""  if (! $date);
  }
  ($y,$m,$d)=( &CheckDate($date) )[0..2];

  if (defined $day and $day ne "") {
    my($day_w)=();
    my($date_w)=&Date_DayOfWeek($m,$d,$y);
    my(%days)=%Date::Manip::Week;
    if (&IsInt($day)) {
      $day_w=$day;
    } else {
      return ""  if (! exists $days{lc($day)});
      $day_w=$days{lc($day)};
    }
    if ($day_w == $date_w) {
      $date=&DateCalc_DateDelta($date,"-0:0:7:0:0:0",\$err,0)  if (! $today);
    } else {
      $day_w -= 7  if ($day_w>$date_w); # make sure previous day is less
      $day = $date_w - $day_w;
      $date=&DateCalc_DateDelta($date,"-0:0:$day:0:0:0",\$err,0);
    }
    $date=&Date_SetTime($date,$hr,$min,$sec)  if (defined $hr);

  } else {
    ($h,$mn,$s)=( &CheckDate($date) )[3..5];
    my($delta,$d,$err)=();
    my($th,$tm,$ts)=&ParseTime($hr,$min,$sec);
    if (defined $hr and $hr ne "") {
      ($hr,$min,$sec)=($th,$tm,$ts);
      $delta="-0:0:1:0:0:0";
    } elsif (defined $min and $min ne "") {
      ($hr,$min,$sec)=($h,$tm,$ts);
      $delta="-0:0:0:1:0:0";
    } elsif (defined $sec and $sec ne "") {
      ($hr,$min,$sec)=($h,$mn,$ts);
      $delta="-0:0:0:0:1:0";
    } else {
      die "ERROR: invalid arguments in Date_GetPrev.\n";
    }

    $d=&Date_SetTime($date,$hr,$min,$sec);
    if ($today) {
      $d=&DateCalc_DateDelta($d,$delta,\$err,0)  if ($d gt $date);
    } else {
      $d=&DateCalc_DateDelta($d,$delta,\$err,0)  if ($d ge $date);
    }
    $date=$d;
  }
  return $date;
}

sub Date_GetNext {
  my($date,$day,$today,$hr,$min,$sec)=@_;
  &Date_Init()  if (! $Date::Manip::InitDone);
  my($y,$m,$d,$h,$mn,$s,$err)=();

  if (! &CheckDate($date)) {
    $date=&ParseDate($date);
    return ""  if (! $date);
  }
  ($y,$m,$d)=( &CheckDate($date) )[0..2];

  if (defined $day and $day ne "") {
    my($day_w)=();
    my($date_w)=&Date_DayOfWeek($m,$d,$y);
    my(%days)=%Date::Manip::Week;
    if (&IsInt($day)) {
      $day_w=$day;
    } else {
      return ""  if (! exists $days{lc($day)});
      $day_w=$days{lc($day)};
    }
    if ($day_w == $date_w) {
      $date=&DateCalc_DateDelta($date,"+0:0:7:0:0:0",\$err,0)  if (! $today);
    } else {
      $date_w -= 7  if ($date_w>$day_w); # make sure next date is greater
      $day = $day_w - $date_w;
      $date=&DateCalc_DateDelta($date,"+0:0:$day:0:0:0",\$err,0);
    }
    $date=&Date_SetTime($date,$hr,$min,$sec)  if (defined $hr);

  } else {
    ($h,$mn,$s)=( &CheckDate($date) )[3..5];
    my($delta,$d,$err)=();
    my($th,$tm,$ts)=&ParseTime($hr,$min,$sec);
    if (defined $hr and $hr ne "") {
      ($hr,$min,$sec)=($th,$tm,$ts);
      $delta="+0:0:1:0:0:0";
    } elsif (defined $min and $min ne "") {
      ($hr,$min,$sec)=($h,$tm,$ts);
      $delta="+0:0:0:1:0:0";
    } elsif (defined $sec and $sec ne "") {
      ($hr,$min,$sec)=($h,$mn,$ts);
      $delta="+0:0:0:0:1:0";
    } else {
      die "ERROR: invalid arguments in Date_GetNext.\n";
    }

    $d=&Date_SetTime($date,$hr,$min,$sec);
    if ($today) {
      $d=&DateCalc_DateDelta($d,$delta,\$err,0)  if ($d lt $date);
    } else {
      $d=&DateCalc_DateDelta($d,$delta,\$err,0)  if ($d le $date);
    }
    $date=$d;
  }

  return $date;
}

sub DateCalc {
  my($D1,$D2,$errref,$mode)=@_;
  my(@date,@delta,$ret,$tmp)=();

  if (defined $mode  and  $mode>=0  and  $mode<=2) {
    $Date::Manip::Mode=$mode;
  } else {
    $Date::Manip::Mode=0;
  }

  if ($tmp=&ParseDate($D1)) {
    push(@date,$tmp);
  } elsif ($tmp=&ParseDateDelta($D1)) {
    push(@delta,$tmp);
  } else {
    $$errref=1;
    return;
  }

  if ($tmp=&ParseDate($D2)) {
    push(@date,$tmp);
  } elsif ($tmp=&ParseDateDelta($D2)) {
    push(@delta,$tmp);
  } else {
    $$errref=2;
    return;
  }
  $mode=$Date::Manip::Mode;

  if ($#date==1) {
    $ret=&DateCalc_DateDate(@date,$mode);
  } elsif ($#date==0) {
    $ret=&DateCalc_DateDelta(@date,@delta,$errref,$mode);
  } else {
    $ret=&DateCalc_DeltaDelta(@delta,$mode);
  }
  $ret;
}

sub ParseDateDelta {
  my($args,@args,@a,$ref,$date)=();
  local($_)=();
  @a=@_;

  # @a : is the list of args to ParseDateDelta.  Currently, only one argument
  #      is allowed and it must be a scalar (or a reference to a scalar)
  #      or a reference to an array.

  if ($#a!=0) {
    print "ERROR:  Invalid number of arguments to ParseDateDelta.\n";
    return "";
  }
  $args=$a[0];
  $ref=ref $args;
  if (! $ref) {
    @args=($args);
  } elsif ($ref eq "ARRAY") {
    @args=@$args;
  } elsif ($ref eq "SCALAR") {
    @args=($$args);
  } else {
    print "ERROR:  Invalid arguments to ParseDateDelta.\n";
    return "";
  }
  @a=@args;

  # @args : a list containing all the arguments (dereferenced if appropriate)
  # @a    : a list containing all the arguments currently being examined
  # $ref  : nil, "SCALAR", or "ARRAY" depending on whether a scalar, a
  #         reference to a scalar, or a reference to an array was passed in
  # $args : the scalar or refererence passed in

  my($y,$m,$w,$d,$h,$mn,$s,$ys,$ms,$ws,$ds,$hs,$mns,$ss,$dir)=();
  my($def,@delta1,@delta2,$colon,$sign,$delta,$i,$sign)=();
  my($from,$to)=();
  my($workweek)=$Date::Manip::WorkWeekEnd-$Date::Manip::WorkWeekBeg+1;

  &Date_Init();
  my($signexp)='([+-]?)';
  my($numexp)='(\d+)';
  my($exp1)="(?: \\s* $signexp \\s* $numexp \\s*)";
  my($yexp,$mexp,$wexp,$dexp,$hexp,$mnexp,$sexp)=();
  $yexp =$mexp=$wexp=$dexp=$hexp=$mnexp=$sexp="()()";
  $yexp ="(?: $exp1 $Date::Manip::YExp)?"  if ($Date::Manip::YExp);
  $mexp ="(?: $exp1 $Date::Manip::MExp)?"  if ($Date::Manip::MExp);
  $wexp ="(?: $exp1 $Date::Manip::WExp)?"  if ($Date::Manip::WExp);
  $dexp ="(?: $exp1 $Date::Manip::DExp)?"  if ($Date::Manip::DExp);
  $hexp ="(?: $exp1 $Date::Manip::HExp)?"  if ($Date::Manip::HExp);
  $mnexp="(?: $exp1 $Date::Manip::MnExp)?" if ($Date::Manip::MnExp);
  $sexp ="(?: $exp1 $Date::Manip::SExp)?"  if ($Date::Manip::SExp);
  my($future)=$Date::Manip::Future;
  my($past)=$Date::Manip::Past;

  $delta="";
  PARSE: while (@a) {
    $_ = join(" ",@a);
    s/\s*$//;

    # Mode is set in DateCalc.  ParseDateDelta only overrides it if the
    # string contains a mode.
    if      ($Date::Manip::Exact and s/$Date::Manip::Exact//) {
      $Date::Manip::Mode=0;
    } elsif ($Date::Manip::Approx and s/$Date::Manip::Approx//) {
      $Date::Manip::Mode=1;
    } elsif ($Date::Manip::Business and s/$Date::Manip::Business//) {
      $Date::Manip::Mode=2;
    } elsif (! defined $Date::Manip::Mode) {
      $Date::Manip::Mode=0;
    }
    $workweek=7  if ($Date::Manip::Mode != 2);

    foreach $from (keys %Date::Manip::Replace) {
      $to=$Date::Manip::Replace{$from};
      s/(^|[^a-z])$from($|[^a-z])/$1$to$2/i;
    }

    # in or ago
    s/^\s* $future \s*//ix  if ($future ne "(?:)");
    $dir=1;
    $dir=-1  if ($past  and  s/\s*$past\s*//i);

    # the colon part of the delta
    $colon="";
    if (s/$signexp?$numexp?(:($signexp?$numexp)?)+$//) {
      $colon=$&;
      s/\s*$//;
    }

    # the non-colon part of the delta
    $sign="+";
    s/^$yexp $mexp $wexp $dexp $hexp $mnexp $sexp$//xi;
    ($ys,$y,$ms,$m,$ws,$w,$ds,$d,$hs,$h,$mns,$mn,$ss,$s)=
      ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14);
    (defined($ys)  and $ys)  ? ($sign=$ys)  : ($ys=$sign);
    (defined($ms)  and $ms)  ? ($sign=$ms)  : ($ms=$ys);
    (defined($ws)  and $ws)  ? ($sign=$ws)  : ($ws=$ms);
    (defined($ds)  and $ds)  ? ($sign=$ds)  : ($ds=$ws);
    (defined($hs)  and $hs)  ? ($sign=$hs)  : ($hs=$ds);
    (defined($mns) and $mns) ? ($sign=$mns) : ($mns=$hs);
    (defined($ss)  and $ss)  ? ($sign=$ss)  : ($ss=$ms);

    # keep track of the last defined element (from -1 to 5)
    $def=-1;
    (defined($y))  ? ($def=0) : ($y=0);
    (defined($m))  ? ($def=1) : ($m=0);
    (defined($d))  ? ($def=2) : ($d=0);
    (defined($h))  ? ($def=3) : ($h=0);
    (defined($mn)) ? ($def=4) : ($mn=0);
    (defined($s))  ? ($def=5) : ($s=0);

    # add weeks to the days
    if (defined $w  and  $w) {
      $d= "$ds$d" + $workweek*"$ws$w";
      if ($d<0) {
        $ds="-";
        $d=-$d;
      }
    }

    @delta1=("$ys$y","$ms$m","$ds$d","$hs$h","$mns$mn","$ss$s");

    # Split the colon part and set the sign
    @delta2=&CheckDelta($colon,$sign);

    # check to see that too many fields have not been entered and that
    # the entire argument list has been used
    if ($_ or ($def+$#delta2)>4) {
      pop(@a);
      next PARSE;
    }

    # add the colon and non-colon part together (and take care of dir)
    # (force it to carry a sign)
    unshift (@delta2,"+0")  while ($#delta2<5);
    for ($i=0; $i<=5; $i++) {
      $delta1[$i] += $delta2[$i];
      $delta1[$i] *= $dir  if ($delta1[$i] != 0);
      $delta1[$i] = "+".$delta1[$i]  if ($delta1[$i]>0);
    }

    # form the delta and shift off the valid part
    $delta=join(":",@delta1);
    splice(@args,0,$#a+1);
    @$args=@args  if (defined $ref  and  $ref eq "ARRAY");
    last PARSE;
  }

  $delta=&NormalizeDelta($delta,$Date::Manip::Mode);
  return $delta;
}

sub UnixDate {
  my($date,@format)=@_;
  local($_)=();
  my($format,%f,$out,@out,$c,$date1,$date2)=();
  my($scalar)=();
  $date=&ParseDate($date);
  return  if (! $date);

  ($f{"Y"},$f{"m"},$f{"d"},$f{"H"},$f{"M"},$f{"S"})=&CheckDate($date);
  $f{"y"}=substr $f{"Y"},2;
  my($m,$d,$y)=($f{"m"},$f{"d"},$f{"Y"});
  &Date_Init();

  if (! wantarray) {
    $format=join(" ",@format);
    @format=($format);
    $scalar=1;
  }

  # month, week
  $_=$m;
  s/^0//;
  $f{"b"}=$f{"h"}=$Date::Manip::Mon[$_-1];
  $f{"B"}=$Date::Manip::Month[$_-1];
  $_=$m;
  s/^0/ /;
  $f{"f"}=$_;
  $f{"U"}=&Date_WeekOfYear($m,$d,$y,0);
  $f{"W"}=&Date_WeekOfYear($m,$d,$y,1);
  $f{"U"}="0".$f{"U"}  if (length $f{"U"} < 2);
  $f{"W"}="0".$f{"W"}  if (length $f{"W"} < 2);

  # day
  $f{"j"}=&Date_DayOfYear($m,$d,$y);
  $_=$d;
  s/^0/ /;
  $f{"e"}=$_;
  $f{"w"}=&Date_DayOfWeek($m,$d,$y);
  $f{"v"}=$Date::Manip::W[$f{"w"}];
  $f{"v"}=" ".$f{"v"}  if (length $f{"v"} < 2);
  $f{"a"}=$Date::Manip::Wk[$f{"w"}];
  $f{"A"}=$Date::Manip::Week[$f{"w"}];
  $f{"E"}=&Date_DaySuffix($f{"e"});

  # hour
  $_=$f{"H"};
  s/^0/ /;
  $f{"k"}=$_;
  $f{"i"}=$f{"k"}+1;
  $f{"i"}=$f{"k"};
  $f{"i"}=12          if ($f{"k"}==0);
  $f{"i"}=$f{"k"}-12  if ($f{"k"}>12);
  $f{"i"}=$f{"i"}-12  if ($f{"i"}>12);
  $f{"i"}=" ".$f{"i"} if (length($f{"i"})<2);
  $f{"I"}=$f{"i"};
  $f{"I"}=~ s/^ /0/;
  $f{"p"}=$Date::Manip::Am;
  $f{"p"}=$Date::Manip::Pm  if ($f{"k"}>11);

  # minute, second, timezone
  $f{"o"}=&Date_SecsSince1970($m,$d,$y,$f{"H"},$f{"M"},$f{"S"});
  $f{"s"}=&Date_SecsSince1970GMT($m,$d,$y,$f{"H"},$f{"M"},$f{"S"});
  $f{"z"}=$f{"Z"}=
    ($Date::Manip::ConvTZ eq "IGNORE" or $Date::Manip::ConvTZ eq "" ?
     $Date::Manip::TZ : $Date::Manip::ConvTZ);

  # date, time
  $f{"c"}=qq|$f{"a"} $f{"b"} $f{"e"} $f{"H"}:$f{"M"}:$f{"S"} $y|;
  $f{"C"}=$f{"u"}=
    qq|$f{"a"} $f{"b"} $f{"e"} $f{"H"}:$f{"M"}:$f{"S"} $f{"z"} $y|;
  $f{"g"}=qq|$f{"a"},$d $f{"b"} $y $f{"H"}:$f{"M"}:$f{"S"} $f{"z"}|;
  $f{"D"}=$f{"x"}=qq|$m/$d/$f{"y"}|;
  $f{"r"}=qq|$f{"I"}:$f{"M"}:$f{"S"} $f{"p"}|;
  $f{"R"}=qq|$f{"H"}:$f{"M"}|;
  $f{"T"}=$f{"X"}=qq|$f{"H"}:$f{"M"}:$f{"S"}|;
  $f{"V"}=qq|$m$d$f{"H"}$f{"M"}$f{"y"}|;
  $f{"Q"}="$y$m$d";
  $f{"q"}=qq|$y$m$d$f{"H"}$f{"M"}$f{"S"}|;
  $f{"P"}=qq|$y$m$d$f{"H"}:$f{"M"}:$f{"S"}|;
  $f{"F"}=qq|$f{"A"}, $f{"B"} $f{"e"}, $f{"Y"}|;
  # %l is a special case.  Since it requires the use of the calculator
  # which requires this routine, an infinite recursion results.  To get
  # around this, %l is NOT determined every time this is called so the
  # recursion breaks.

  # other formats
  $f{"n"}="\n";
  $f{"t"}="\t";
  $f{"%"}="%";
  $f{"+"}="+";

  foreach $format (@format) {
    $format=reverse($format);
    $out="";
    while ($format) {
      $c=chop($format);
      if ($c eq "%") {
        $c=chop($format);
        if ($c eq "l") {
          $date1=&DateCalc_DateDelta($Date::Manip::Curr,"-0:6:0:0:0:0");
          $date2=&DateCalc_DateDelta($Date::Manip::Curr,"+0:6:0:0:0:0");
          if ($date gt $date1  and  $date lt $date2) {
            $f{"l"}=qq|$f{"b"} $f{"e"} $f{"H"}:$f{"M"}|;
          } else {
            $f{"l"}=qq|$f{"b"} $f{"e"}  $f{"Y"}|;
          }
          $out .= $f{"$c"};
        } elsif (exists $f{"$c"}) {
          $out .= $f{"$c"};
        } else {
          $out .= $c;
        }
      } else {
        $out .= $c;
      }
    }
    push(@out,$out);
  }
  if ($scalar) {
    return $out[0];
  } else {
    return (@out);
  }
}

sub ParseDate {
  my($args,@args,@a,$ref,$date)=();
  local($_)=();
  @a=@_;
  my($y,$m,$d,$h,$mn,$s,$i,$which,$dofw,$wk,$tmp,$z,$num,$err)=();

  # @a : is the list of args to ParseDate.  Currently, only one argument
  #      is allowed and it must be a scalar (or a reference to a scalar)
  #      or a reference to an array.

  if ($#a!=0) {
    print "ERROR:  Invalid number of arguments to ParseDate.\n";
    return "";
  }
  $args=$a[0];
  $ref=ref $args;
  if (! $ref) {
    return $args  if (&CheckDate($args));
    @args=($args);
  } elsif ($ref eq "ARRAY") {
    @args=@$args;
  } elsif ($ref eq "SCALAR") {
    return $$args  if (&CheckDate($$args));
    @args=($$args);
  } else {
    print "ERROR:  Invalid arguments to ParseDate.\n";
    return "";
  }
  @a=@args;

  # @args : a list containing all the arguments (dereferenced if appropriate)
  # @a    : a list containing all the arguments currently being examined
  # $ref  : nil, "SCALAR", or "ARRAY" depending on whether a scalar, a
  #         reference to a scalar, or a reference to an array was passed in
  # $args : the scalar or refererence passed in

  &Date_Init();
  my($type)=$Date::Manip::DateFormat;
  my($mmm)=$Date::Manip::MonExp;
  my($now)=$Date::Manip::Now;
  my($offset)=$Date::Manip::Offset;
  my($wkexp)=$Date::Manip::WkExp;
  my($timeexp)=$Date::Manip::TimesExp;
  my(%dofw)=%Date::Manip::Week;
  my($whichexp)=$Date::Manip::WhichExp;
  my(%which)=%Date::Manip::Which;
  my($daysexp)=$Date::Manip::DayExp;
  my(%dayshash)=%Date::Manip::Day;
  my($ampm)=$Date::Manip::AmPmExp;
  my($weeks)=$Date::Manip::WExp;
  my($next)=$Date::Manip::Next;
  my($prev)=$Date::Manip::Prev;
  my($ago)=$Date::Manip::Past;
  my($on)=$Date::Manip::On;

  # Regular expressions for part of the date
  my($hm)=$Date::Manip::SepHM;
  my($ms)=$Date::Manip::SepMS;
  my($ss)=$Date::Manip::SepSS;
  my($YY) ='(\d{2}|\d{4})'; # 2 or 4 digits (year)
  my($DD)='(\d{2})';        # 2 digits      (month/day/hour/minute/second)
  my($D) ='(\d{1,2})';      # 1 or 2 digit  (month/day/hour)
  my($FD)="(?:$ss\\d+)?";   # fractional secs
  # There are two forms of the time.  Time/time are used when the time is
  # not the last element of the string.  TimeL/timeL are used when the time
  # is the last element.
  my($zone)=$Date::Manip::ZoneExp;
  # time in HH:MM:SS [Zone]
  my($Time) ="(?:$DD$hm$DD(?:$ms$DD$FD)?(?:\\s*$ampm)?$zone)";
  # time in hh:MM:SS [Zone]
  my($time) ="(?:$D$hm$DD(?:$ms$DD$FD)?(?:\\s*$ampm)?$zone)";
  my($sep)='([\/ .-])';
  my($at)=$Date::Manip::At;
  my($in)=$Date::Manip::In;
  my($com)=',?';

  $ampm="";

  $date="";
  PARSE: while($#a>=0) {
    $_=join(" ",@a);

    # Mode is set in DateCalc.  ParseDate only overrides it if the string
    # contains a mode.
    if      ($Date::Manip::Exact and s/$Date::Manip::Exact//) {
      $Date::Manip::Mode=0;
    } elsif ($Date::Manip::Approx and s/$Date::Manip::Approx//) {
      $Date::Manip::Mode=1;
    } elsif ($Date::Manip::Business and s/$Date::Manip::Business//) {
      $Date::Manip::Mode=2;
    } elsif (! defined $Date::Manip::Mode) {
      $Date::Manip::Mode=0;
    }

    # Substitute all special time expressions.
    if ($timeexp ne "()"  and  /$timeexp/i) {
      $tmp=$1;
      $tmp=$Date::Manip::Times{$tmp};
      s/$timeexp/ $tmp /;
    }

    $tmp=0;
    if (/^\s*$whichexp\s*$wkexp$in$mmm\s*$YY?(?:$at$time)?\s*$/i) {
      # last friday in October 95
      ($which,$dofw,$m,$y,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6,$7,$8,$9);
      # fix $m, $y
      &Date_ErrorCheck(\$y,\$m,\$d,\$h,\$mn,\$s,\$ampm,\$wk);
      $dofw=$dofw{lc($dofw)};
      $which=$which{lc($which)};
      # Get the first day of the month
      if ($Date::Manip::DateFormat eq "US") {
        $date=&ParseDate("$m 1 $y $h:$mn:$s");
      } else {
        $date=&ParseDate("1 $m $y $h:$mn:$s");
      }
      if ($which==-1) {
        $date=&DateCalc_DateDelta($date,"+0:1:0:0:0:0",\$err,0);
        $date=&Date_GetPrev($date,$dofw,0);
      } else {
        for ($i=0; $i<$which; $i++) {
          if ($i==0) {
            $date=&Date_GetNext($date,$dofw,1);
          } else {
            $date=&Date_GetNext($date,$dofw,0);
          }
          $date="err", last PARSE  if (! $date);
        }
      }
      last PARSE;

    } elsif (/^\s*$next\s*$wkexp\s*(?:$at$time)?\s*$/i) {
      # next friday
      ($dofw,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6);
      $date=&Date_GetNext($Date::Manip::Curr,$dofw,0,$h,$mn,$s);
      last PARSE;

    } elsif (/^\s*$prev\s*$wkexp\s*(?:$at$time)?\s*$/i) {
      # last friday
      ($dofw,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6);
      $date=&Date_GetPrev($Date::Manip::Curr,$dofw,0,$h,$mn,$s);
      last PARSE;

    } elsif (/^\s*$next\s*$weeks\s*(?:$at$time)?\s*$/i) {
      # next week
      ($h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6);
      $date=&DateCalc_DateDelta($Date::Manip::Curr,"+0:0:7:0:0:0",\$err,0);
      $date=&Date_SetTime($date,$h,$mn,$s)  if (defined $h);
      last PARSE;
    } elsif (/^\s*$prev\s*$weeks\s*(?:$at$time)?\s*$/i) {
      # last week
      ($h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6);
      $date=&DateCalc_DateDelta($Date::Manip::Curr,"-0:0:7:0:0:0",\$err,0);
      $date=&Date_SetTime($date,$h,$mn,$s)  if (defined $h);
      last PARSE;

    } elsif (/^\s*$in\s*(\d+)\s*$weeks\s*(?:$at$time)?\s*$/i) {
      # in 2 weeks
      ($num,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6);
      $date=&DateCalc_DateDelta($Date::Manip::Curr,"+0:0:" .7*$num. ":0:0:0",
                                \$err,0);
      $date=&Date_SetTime($date,$h,$mn,$s)  if (defined $h);
      last PARSE;
    } elsif (/^\s*(\d+)\s*$weeks\s*$ago\s*(?:$at$time)?\s*$/i) {
      # 2 weeks ago
      ($num,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6);
      $date=&DateCalc_DateDelta($Date::Manip::Curr,"-0:0:" .7*$num. ":0:0:0",
                               \$err,0);
      $date=&Date_SetTime($date,$h,$mn,$s)  if (defined $h);
      last PARSE;

    } elsif (/^\s*$wkexp\s*$in\s*(\d+)\s*$weeks(?:$at$time)?\s*$/i) {
      # friday in 2 weeks
      ($dofw,$num,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6,$7);
      $tmp="+";
    } elsif (/^\s*$wkexp\s*(\d+)\s*$weeks\s*$ago(?:$at$time)?\s*$/i) {
      # friday 2 weeks ago
      ($dofw,$num,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6,$7);
      $tmp="-";
    } elsif (/^\s*$in\s*(\d+)\s*$weeks$on$wkexp(?:$at$time)?\s*$/i) {
      # in 2 weeks on friday
      ($num,$dofw,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6,$7);
      $tmp="+"
    } elsif (/^\s*(\d+)\s*$weeks\s*$ago$on$wkexp(?:$at$time)?\s*$/i) {
      # 2 weeks ago on friday
      ($num,$dofw,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6,$7);
      $tmp="-";
    }
    if ($tmp) {
      $date=&DateCalc_DateDelta($Date::Manip::Curr,
                                $tmp . "0:0:" .7*$num. ":0:0:0",\$err,0);
      $date=&Date_GetPrev($date,$Date::Manip::FirstDay,1);
      $date=&Date_GetNext($date,$dofw,1,$h,$mn,$s);
      last PARSE;
    }

    # Change 2nd, second to 2
    if (/$daysexp/i) {
      $tmp=lc($1);
      $tmp=$dayshash{"$tmp"};
      s/\s*$daysexp\s*/ $tmp /;
      s/^\s+//;
      s/\s+$//;
    }

    $tmp=0;
    if (/^\s*$D\s*$wkexp\s*(?:$in?\s*$YY)?(?:\s*$at?\s*$time)?\s*$/i) {
      # 22nd sunday [in 1996] [at 12:00]
      ($which,$dofw,$y,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6,$7,$8);
      $tmp=1;
    } elsif (/^\s*$wkexp\s*$weeks\s*$D(?:$in?\s*$YY)?(?:\s*$at?\s*$time)?\s*$/i) {
      # sunday week 22 [in 1996] [at 12:00]
      ($dofw,$which,$y,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6,$7,$8);
      $tmp=1;
    } elsif (/^\s*$wkexp\s*$D\s*$weeks(?:$in?\s*$YY)?(?:\s*$at?\s*$time)?\s*$/i) {
      # sunday 22nd week [in 1996] [at 12:00]
      ($dofw,$which,$y,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6,$7,$8);
      $tmp=1;
    }
    if ($tmp) {
      $y=""  if (! defined $y);
      $date=&ParseDate("1 1 $y");
      $date=&Date_GetNext($date,$dofw,1);
      $date=&DateCalc_DateDelta($date,"+0:0:". ($which-1)*7 . ":0:0:0",\$err,0)
        if ($which>1);
      $date=&Date_SetTime($date,$h,$mn,$s)  if (defined $h);
      last PARSE;
    }

    if ($wkexp ne "()" and /$wkexp/i) {
      $wk=$1;
      s/$wkexp$com/ /i;
    }
    s/\s+/ /g;                  # all whitespace are now a single space
    s/^\s+//;
    s/\s+$//;

    if (/^$YY$DD$DD$at?$Time?$/i) {
      # DateTime
      #    Date=YYMMDD
      ($y,$m,$d,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6,$7,$8);

    } elsif (/^(\d{4})$DD$DD$DD$DD$DD$/i) {
      # YYYYMMDDHHMNSS
      ($y,$m,$d,$h,$mn,$s)=($1,$2,$3,$4,$5,$6);

    } elsif (/^$D$sep$D(?:\2$YY)?(?:(?:$com\s+|$com$at|\2)$time)?$/i) {
      # Date Time
      # Date%Time
      #   Date=mm%dd, mm%dd%YY
      ($m,$d,$y,$h,$mn,$s,$ampm,$z)=($1,$3,$4,$5,$6,$7,$8,$9);
      ($m,$d)=($d,$m)  if ($type ne "US");

    } elsif (/^$mmm$sep$D(?:(?:$com\s*|\2)$YY)?$com(?:(?:\s+|$at)$time)?$/i) {
      # Date Time
      #   Date=mmm%dd mmm%dd%YY
      ($m,$d,$y,$h,$mn,$s,$ampm,$z)=($1,$3,$4,$5,$6,$7,$8,$9);

    } elsif (/^$mmm$sep$D(?:\2$YY)?(?:\2$time)?$/i) {
      # Date%Time
      #   Date=mmm%dd mmm%dd%YY
      ($m,$d,$y,$h,$mn,$s,$ampm,$z)=($1,$3,$4,$5,$6,$7,$8,$9);

    } elsif (/^$D$sep*$mmm(?:(?:$com\s*|\2)$YY)?$com(?:(?:\s+|$at)$time)?$/i) {
      # Date Time
      #   Date=dd%mmm, dd%mmm%YY
      ($d,$m,$y,$h,$mn,$s,$ampm,$z)=($1,$3,$4,$5,$6,$7,$8,$9);

    } elsif (/^$D$sep*$mmm(?:\2$YY)?(?:\2$time)?$/i) {
      # Date%Time
      #   Date=dd%mmm, dd%mmm%YY
      ($d,$m,$y,$h,$mn,$s,$ampm,$z)=($1,$3,$4,$5,$6,$7,$8,$9);

    } elsif (/^$D\s*$mmm(?:$com\s*$YY)?$com(?:(?:\s+|$at)$time)?$/i) {
      # Date Time
      #   Date=ddmmm, ddmmmYY, ddmmm YY, dd mmmYY
      ($d,$m,$y,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6,$7,$8);

    } elsif (/^$mmm$D(?:$com\s*$YY)?$com(?:(?:\s+|$at)$time)?$/i) {
      # Date Time
      #   Date=mmmdd, mmmdd YY
      ($m,$d,$y,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6,$7,$8);

    } elsif (/^$mmm\s*$DD(?:$com$YY)?$com(?:(?:\s+|$at)$time)?$/i) {
      # Date Time
      #   Date=mmm DDYY, mmmDDYY
      ($m,$d,$y,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6,$7,$8);


    } elsif (/^$time$/) {
      ($h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5);

    } elsif (/^$time\s*$D$sep$D(?:\7$YY)?$/i) {
      # TimeDate
      # Time Date
      #   Date=mm%dd, mm%dd%YY
      ($h,$mn,$s,$ampm,$z,$m,$d,$y)=($1,$2,$3,$4,$5,$6,$8,$9);
      ($m,$d)=($d,$m)  if ($type ne "US");

    } elsif (/^$time$sep$D\6$D(?:\6$YY)?$/i) {
      # Time%Date
      #   Date=mm%dd mm%dd%YY
      ($h,$mn,$s,$ampm,$z,$m,$d,$y)=($1,$2,$3,$4,$5,$7,$8,$9);
      ($m,$d)=($d,$m)  if ($type ne "US");

    } elsif (/^$time\s*$mmm$sep$D(?:(?:$com\s*|\7)$YY)?$/i) {
      # TimeDate
      # Time Date
      #   Date=mmm%dd mmm%dd%YY
      ($h,$mn,$s,$ampm,$z,$m,$d,$y)=($1,$2,$3,$4,$5,$6,$8,$9);

    } elsif (/^$time$sep$mmm\6$D(?:\6$YY)?$/i) {
      # Time%Date
      #   Date=mmm%dd mmm%dd%YY
      ($h,$mn,$s,$ampm,$z,$m,$d,$y)=($1,$2,$3,$4,$5,$7,$8,$9);

    } elsif (/^$time\s*$D$sep$mmm(?:(?:$com\s*|\7)$YY)?$/i) {
      # TimeDate
      # Time Date
      #   Date=dd%mmm dd%mmm%YY
      ($h,$mn,$s,$ampm,$z,$d,$m,$y)=($1,$2,$3,$4,$5,$6,$8,$9);

    } elsif (/^$time$sep$D\5$mmm(?:\6$YY)?$/i) {
      # Time%Date
      #   Date=dd%mmm dd%mmm%YY
      ($h,$mn,$s,$ampm,$z,$d,$m,$y)=($1,$2,$3,$4,$5,$7,$8,$9);

    } elsif (/^$time\s*$mmm\s*$D(?:$com\s+$YY)?$/i) {
      # TimeDate
      # Time Date
      #   Date=mmmdd, mmmdd YY
      ($h,$mn,$s,$ampm,$z,$m,$d,$y)=($1,$2,$3,$4,$5,$6,$7,$8);

    } elsif (/^$time\s*$mmm\s*$DD$com$YY$/i) {
      # TimeDate
      # Time Date
      #   Date=mmmDDYY
      ($h,$mn,$s,$ampm,$z,$m,$d,$y)=($1,$2,$3,$4,$5,$6,$7,$8);

    } elsif (/^$time\s*$D\s*$mmm(?:$com\s*$YY)?$/i) {
      # TimeDate
      # Time Date
      #   Date=ddmmm, ddmmm YY, ddmmmYY
      ($h,$mn,$s,$ampm,$z,$d,$m,$y)=($1,$2,$3,$4,$5,$6,$7,$8);

    } elsif (/^$now$at?$time?$/i) {
      # now, today
      ($h,$mn,$s,$ampm,$z)=($2,$3,$4,$5,$6);
      $date=$Date::Manip::Curr;
      if (defined $h) {
        if (&Date_ErrorCheck(\$y,\$m,\$d,\$h,\$mn,\$s,\$ampm,\$wk)) {
          pop(@a);
          next PARSE;
        }
        $date=&Date_SetTime($date,$h,$mn,$s);
      }
      $date=&Date_ConvTZ($date,$z);
      return $date;

    } elsif (/^$mmm\s*$D\s+$time\s*$YY$/i) {
      # mmmdd time YY   (ctime format)
      ($m,$d,$h,$mn,$s,$ampm,$z,$y)=($1,$2,$3,$4,$5,$6,$7,$8);

    } elsif (/^$offset$at?$time?$/i) {
      # yesterday, tomorrow
      ($offset,$h,$mn,$s,$ampm,$z)=($1,$2,$3,$4,$5,$6);
      $offset=$Date::Manip::Offset{lc($offset)};
      $date=&DateCalc_DateDelta($Date::Manip::Curr,$offset,\$err,0);
      if (defined $h) {
        if (&Date_ErrorCheck(\$y,\$m,\$d,\$h,\$mn,\$s,\$ampm,\$wk)) {
          pop(@a);
          next PARSE;
        }
        $date=&Date_SetTime($date,$h,$mn,$s);
        $date=&Date_ConvTZ($date,$z);
      }
      return $date;

    } else {
      pop(@a);
      next PARSE;
    }

    if (&Date_ErrorCheck(\$y,\$m,\$d,\$h,\$mn,\$s,\$ampm,\$wk)) {
      pop(@a);
    } else {
      last PARSE;
    }
  }

  if ($date ne "err") {
    if (@a) {
      splice(@args,0,$#a+1);
      @$args=@args  if (defined $ref  and  $ref eq "ARRAY");
      $date=&FormDate($y,$m,$d,$h,$mn,$s)  if (! $date);
      $date=&Date_ConvTZ($date,$z);
      return $date;
    }
  }
  return "";
}

########################################################################
# OTHER SUBROUTINES
########################################################################

sub Date_DayOfWeek {
  my($m,$d,$y)=@_;
  die "ERROR: Date_DayOfWeek requires a 4 digit year.\n"  if ($y!~/^\d{4}$/);
  my($dayofweek,$dec31)=();

  $dec31=2;                     # Dec 31, 0999 was Tuesday
  $dayofweek=(&Date_DaysSince999($m,$d,$y)+$dec31) % 7;
  return $dayofweek;
}

sub Date_SecsSince1970 {
  my($m,$d,$y,$h,$mn,$s)=@_;
  die "ERROR: Date_SecsSince1970 requires a 4 digit year.\n"
    if ($y!~/^\d{4}$/);
  my($sec_now,$sec_70)=();
  $sec_now=(&Date_DaysSince999($m,$d,$y)-1)*24*3600 + $h*3600 + $mn*60 + $s;
# $sec_70 =(&Date_DaysSince999(1,1,1970)-1)*24*3600;
  $sec_70 =30610224000;
  return ($sec_now-$sec_70);
}

sub Date_SecsSince1970GMT {
  my($m,$d,$y,$h,$mn,$s)=@_;
  die "ERROR: Date_SecsSince1970GMT requires a 4 digit year.\n"
    if ($y!~/^\d{4}$/);

  my($sec)=&Date_SecsSince1970($m,$d,$y,$h,$mn,$s);
  return $sec   if ($Date::Manip::ConvTZ eq "IGNORE");

  my($tz)=$Date::Manip::ConvTZ;
  $tz=$Date::Manip::TZ  if (! $tz);
  $tz=$Date::Manip::Zone{lc($tz)}  if ($tz !~ /^[+-]\d{4}$/);

  my($tzs)=1;
  $tzs=-1 if ($tz<0);
  $tz=~/.(..)(..)/;
  my($tzh,$tzm)=($1,$2);
  $sec - $tzs*($tzh*3600+$tzm*60);
}

sub Date_DaysSince999 {
  my($m,$d,$y)=@_;
  die "ERROR: Date_DaysSince999 requires a 4 digit year.\n"
    if ($y!~/^\d{4}$/);
  my($Ny,$N4,$N100,$N400,$dayofyear,$days)=();
  my($cc,$yy)=();

  $y=~ /(\d{2})(\d{2})/;
  ($cc,$yy)=($1,$2);

  # Number of full years since Dec 31, 0999
  $Ny=$y-1000;

  # Number of full 4th years (incl. 1000) since Dec 31, 0999
  $N4=int(($Ny-1)/4)+1;
  $N4=0         if ($y==1000);

  # Number of full 100th years (incl. 1000)
  $N100=$cc-9;
  $N100--       if ($yy==0);

  # Number of full 400th years
  $N400=int(($N100+1)/4);

  $dayofyear=&Date_DayOfYear($m,$d,$y);
  $days= $Ny*365 + $N4 - $N100 + $N400 + $dayofyear;

  return $days;
}

sub Date_DayOfYear {
  my($m,$d,$y)=@_;
  die "ERROR: Date_DayOfYear requires a 4 digit year.\n"  if ($y!~/^\d{4}$/);
  my(@daysinmonth)=(0,31,28,31,30,31,30,31,31,30,31,30,31);
  my($daynum,$i)=();
  $daysinmonth[2]=29  if (&Date_LeapYear($y));
  $daynum=0;
  for ($i=1; $i<$m; $i++) {
    $daynum += $daysinmonth[$i];
  }
  $daynum += $d;
  $daynum="0$daynum"   if ($daynum<10);
  $daynum="0$daynum"   if ($daynum<100);
  return $daynum;
}

sub Date_DaysInYear {
  my($y)=@_;
  die "ERROR: Date_DaysInYear requires a 4 digit year.\n"  if ($y!~/^\d{4}$/);
  return 366  if (&Date_LeapYear($y));
  return 365;
}

sub Date_WeekOfYear {
  my($m,$d,$y,$f)=@_;
  die "ERROR: Date_WeekOfWeek requires a 4 digit year.\n"  if ($y!~/^\d{4}$/);
  my($jan1)=&Date_DayOfWeek(1,1,$y); # Jan 1 is what day of week
  my($dofy)=&Date_DayOfYear($m,$d,$y);

  # Renumber the days (still 0 to 6) so that the first day of
  # the week is always 0.
  $jan1=$jan1-$f;
  $jan1+=7 if ($jan1<0);

  # Add days to the beginning of the year so that the first day
  # of this "extended" year falls on the first day of the week and
  # is numbered day 0 (rather than day 1).
  $dofy+=$jan1-1;

  return (int($dofy/7)+1);
}

sub Date_LeapYear {
  my($y)=@_;
  die "ERROR: Date_LeapYear requires a 4 digit year.\n"  if ($y!~/^\d{4}$/);
  return 0 unless $y % 4 == 0;
  return 1 unless $y % 100 == 0;
  return 0 unless $y % 400 == 0;
  return 1;
}

sub Date_DaySuffix {
  my($d)=@_;
  return $Date::Manip::Day[$d-1];
}

sub Date_ConvTZ {
  my($date,$from,$to)=@_;
  my($gmt)=();

  if (! defined $to  or  ! $to) {
    # 2 argument form, convert $from to $Date::Manip::ConvTZ
    return $date
      if (! defined $from  or  ! $from  or  $Date::Manip::ConvTZ eq "IGNORE");

    $to=$Date::Manip::ConvTZ  if (! defined $to  or  ! $to);
    $to=$Date::Manip::TZ  if (! $to);
  }
  if (! defined $from  or  ! $from) {
    $from=$Date::Manip::ConvTZ;
    $from=$Date::Manip::TZ  if (! $from);
  }

  $to=$Date::Manip::Zone{lc($to)}
    if (exists $Date::Manip::Zone{lc($to)});
  $from=$Date::Manip::Zone{lc($from)} 
    if (exists $Date::Manip::Zone{lc($from)});
  $gmt=$Date::Manip::Zone{gmt};

  return $date  if ($from !~ /^[+-]\d{4}$/ or $to !~ /^[+-]\d{4}$/);
  return $date  if ($from eq $to);

  my($h,$m,$err)=();
  # Convert $date from $from to GMT
  if ($from ne $gmt) {
    $from=~/([+-]\d{2})(\d{2})/;
    ($h,$m)=($1,$2);
    $h=-$h;
    $date=&DateCalc_DateDelta($date,"+0:0:0:$h:$m:00",\$err,0);
  }
  # Convert $date from GMT to $to
  if ($to ne $gmt) {
    $to=~/([+-]\d{2})(\d{2})/;
    ($h,$m)=($1,$2);
    $date=&DateCalc_DateDelta($date,"+0:0:0:$h:$m:00",\$err,0);
  }
  return $date;
}

sub Date_TimeZone {
  my($null,$tz,@tz,$std,$dst,$time,$isdst)=();

  # Get timezones from all of the relevant places

  push(@tz,$Date::Manip::TZ)  if (defined $Date::Manip::TZ);  # TZ config var
  push(@tz,$ENV{"TZ"})        if (exists $ENV{"TZ"});         # TZ environ var
  push(@tz,$main::TZ)         if (defined $main::TZ);         # $main::TZ
  if (-s "/etc/TIMEZONE") {                                   # /etc/TIMEZONE
    ($null,$tz) = split (/\=/,`grep ^TZ /etc/TIMEZONE`);
    chomp($tz);
    $tz=~ s/\s+//g;
    push(@tz,$tz);
  }
  unless (defined $ENV{OS}  and  $ENV{OS} ne 'Windows_NT') {  # date command
    $tz = `date`;                                             #  not on NT!
    chomp($tz);
    $tz=(split(/\s+/,$tz))[4];
    push(@tz,$tz);
  }

  # Now parse each one to find the first valid one.
  foreach $tz (@tz) {
    return $tz  if (defined $Date::Manip::Zone{lc($tz)} or $tz=~/^[+-]\d{4}/);

    # Handle STD#DST# format
    if ($tz =~ /^([a-z]+)\d([a-z]+)\d?$/i) {
      ($std,$dst)=($1,$2);
      next  if (! defined $Date::Manip::Zone{lc($std)} or
                ! defined $Date::Manip::Zone{lc($dst)});
      $time = time();
      ($null,$null,$null,$null,$null,$null,$null,$null,$isdst) =
        localtime($time);
      return $dst  if ($isdst);
      return $std;
    }
  }

  die "ERROR: Date::Manip unable to determine TimeZone.\n";
}

sub Date_Init {
  my($language,$format,$tz,$convtz,@args)=@_;
  $Date::Manip::InitDone=1;
  local($_)=();
  my($internal,$firstday)=();
  my($var,$val,$file)=();

  #### Backwards compatibility junk
  if (defined $language  and  $language) {
    if ($language=~ /=/) {
      push(@args,$language);
    } else {
      push(@args,"Language=$language");
    }
  }
  if (defined $format  and  $format) {
    if ($format=~ /=/) {
      push(@args,$format);
    } else {
      push(@args,"DateFormat=$format");
    }
  }
  if (defined $tz  and  $tz) {
    if ($tz=~ /=/) {
      push(@args,$tz);
    } else {
      push(@args,"TZ=$tz");
    }
  }
  if (defined $convtz  and  $convtz) {
    if ($convtz=~ /=/) {
      push(@args,$convtz);
    } else {
      push(@args,"ConvTZ=$convtz");
    }
  }
  #### End backwards compatibility junk

  $Date::Manip::EraseHolidays=0;
  foreach (@args) {
    s/\s*$//;
    s/^\s*//;
    /^(\S+) \s* = \s* (.+)$/x;
    ($var,$val)=($1,$2);
    $Date::Manip::InitFilesRead--,
    $Date::Manip::PersonalCnf=$val,      next  if ($var eq "PersonalCnf");
    $Date::Manip::PersonalCnfPath=$val,  next  if ($var eq "PersonalCnfPath");
  }

  $Date::Manip::InitFilesRead=1  if ($Date::Manip::IgnoreGlobalCnf);
  if ($Date::Manip::InitFilesRead<1) {
    $Date::Manip::InitFilesRead=1;
    # Read Global Init file
    if ($Date::Manip::GlobalCnf) {
      $file=&ExpandTilde($Date::Manip::GlobalCnf);
    }
    &ReadInitFile($file)  if (defined $file  and  -r "$file");
  }
  if ($Date::Manip::InitFilesRead<2) {
    $Date::Manip::InitFilesRead=2;
    # Read Personal Init file
    if ($Date::Manip::PersonalCnf  and  $Date::Manip::PersonalCnfPath) {
      $file=&SearchPath($Date::Manip::PersonalCnf,
                        $Date::Manip::PersonalCnfPath,"r");
    }
    &ReadInitFile($file)  if (defined $file  and  -r $file);
  }

  foreach (@args) {
    s/\s*$//;
    s/^\s*//;
    /^(\S+) \s* = \s* (.+)$/x;
    ($var,$val)=($1,$2);

    &Date_SetConfigVariable($var,$val);
  }

  die "ERROR: Unknown FirstDay in Date::Manip.\n"
    if (! &IsInt($Date::Manip::FirstDay,0,6));
  die "ERROR: Unknown WorkWeekBeg in Date::Manip.\n"
    if (! &IsInt($Date::Manip::WorkWeekBeg,0,6));
  die "ERROR: Unknown WorkWeekEnd in Date::Manip.\n"
    if (! &IsInt($Date::Manip::WorkWeekEnd,0,6));
  die "ERROR: Invalid WorkWeek in Date::Manip.\n"
    if ($Date::Manip::WorkWeekEnd <= $Date::Manip::WorkWeekBeg);

  my($i,$j,@tmp,@tmp2,@tmp3,$a,$b,$now,$offset,$last,$in,$at,$on,$tmp,%tmp,
     $mon,$month,@mon,@month,
     $w,$wk,$week,@w,@wk,@week,$weeks,
     $days,@days,$am,$pm,
     $zones,$zonesrfc,@zones,$times,$future,$past,$sephm,$sepms,$sepss,
     $years,$months,$days,$hours,$minutes,$seconds,$replace,$next,$prev,
     $approx,$exact,$business)=();
  my($lang)=$Date::Manip::Language;

  if (! $Date::Manip::Init) {
    $Date::Manip::Init=1;

    # Set the following variables based on the language.  They should all
    # be capitalized correctly, and any spaces appearing in the string
    # should be replaced with an underscore (_) (they will be correctly
    # parsed as spaces).

    #  $month   : space separated string containing months spelled out
    #  $mon     : space separated string containing months abbreviated
    #  $week    : space separated string containing weekdays spelled out
    #  $wk      : space separated string containing weekdays abbreviated
    #  $w       : space separated string containing weekdays very abbreviated
    #  $am,$pm  : different ways of expressing AM (PM), the first one in each
    #             list is the one that will be used when printing out an AM
    #             or PM string
    #  @days    : different ways that numbers can appear as days (first, 1st,
    #             etc.  Each element of @days has a space separated string
    #             with up to 31 values).  The first one should contain the
    #             nubers in the 1st, 2nd, etc. format.
    #  $last    : strings containing synonyms for last
    #  $years   : string containing abbreviations for the word year
    #  $months  : string containing abbreviations for the word month
    #  $weeks   : string containing abbreviations for the work week
    #  $days    : string containing abbreviations for the word day
    #  $hours   : string containing abbreviations for the word hour
    #  $minutes : string containing abbreviations for the word minute
    #  $seconds : string containing abbreviations for the word second
    #  $now     : string containing words referring to now
    #  $in      : strings fitting "1st sunday in June"
    #  $at      : strings fitting "at 12:00"
    #  $on      : strings fitting "on June 1st"
    #  $future  : strings to indicate the future
    #  $past    : strings to indicate the past
    #  $next    : strings to indicate the "next" of something
    #  $prev    : strings to indicate the "previous" of something
    #  $times   : different strings which stand for specific times and
    #             the time they translate to (ex. "noon 12:00:00")
    #  $zones   : a space separated string containing additional timezone
    #             strings (beyond the RFC 822 zones) along with their
    #             translatrion.  So, the string "EST -0500 EDT -0400"
    #             contain two time zones, EST and EDT, which have offsets
    #             of -0500 and -0400 respectively from Universal Time.
    #  $sephm   : the separator used between the hours and minutes of a time
    #  $sepms   : the separator used between the minutes and seconds of a time
    #  $sepss   : the separator used between seconds and fractional seconds
    #             NOTE:  all three of the separators can be any format suitable
    #             for a regular expression PROVIDED it does not create a
    #             back-reference.  For example, in french, the hour/minute
    #             separator might be a colon or the letter h.  This would be
    #             defined as (?::|h) or [:h] but NOT as (:|h) since the latter
    #             produces a back-reference.  Also, the dot "." should be
    #             defined as '\.' since it is in a regular expression.
    #  $approx  : strings which force approximate mode in DateCalc.
    #  $exact   : exact mode
    #  $business: business mode
    #
    # One important variable is $replace.  In English (and probably
    # other languages), one of the abbreviations for the word month that
    # would be nice is "m".  The problem is that "m" matches the "m" in
    # "minute" which causes the string to be improperly matched in some
    # cases.  Hence, the list of abbreviations for month is given as:
    #   "mon month months"
    # In order to allow you to enter "m", replacements can be done.
    # $replace is a list of pairs of words which are matched and replaced
    # AS ENTIRE WORDS".  Having $replace equal to "m month" means that
    # the entire word "m" will be replaced with "month".  This allows the
    # desired abbreviation to be used.  Make sure that $replace contains
    # an even number of words (i.e. all must be pairs).
    #
    # One other variable to set is $offset.  This contains a space separated
    # set of dates which are defined as offsets from the current time.
    #
    # If a string contains spaces, replace the space(s) with underscores.

    if ($lang eq "English") {
      $month="January February March April May June ".
        "July August September October November December";
      $mon="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec";

      $week="Sunday Monday Tuesday Wednesday Thursday Friday Saturday";
      $wk="Sun Mon Tue Wed Thu Fri Sat";
      $w="S M T W Th F Sa";

      $days[0]="1st 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th ".
        "15th 16th 17th 18th 19th 20th 21st 22nd 23rd 24th 25th 26th 27th ".
        "28th 29th 30th 31st";
      $days[1]="first second third fourth fifth sixth seventh eighth ninth ".
        "tenth eleventh twelfth thirteenth fourteenth fifteenth sixteenth ".
        "seventeenth eighteenth nineteenth twentieth twenty-first ".
        "twenty-second twenty-third twenty-fourth twenty-fifth twenty-sixth ".
        "twenty-seventh twenty-eighth twenty-ninth thirtieth thirty-first";

      $last="last";
      $in="in of";
      $at="at";
      $on="on";
      $future="in";
      $past="ago";
      $next="next";
      $prev="previous last";

      $am="AM";
      $pm="PM";

      $years  ="y yr year yrs years";
      $months ="mon month months";
      $weeks  ="w wk wks week weeks";
      $days   ="d day days";
      $hours  ="h hr hrs hour hours";
      $minutes="mn min minute minutes";
      $seconds="s sec second seconds";
      $replace="m month";

      $now="today now";
      $offset="yesterday -0:0:1:0:0:0 tomorrow +0:0:1:0:0:0";
      $times="noon 12:00:00 midnight 00:00:00";

      $sephm=':';
      $sepms=':';
      $sepss='[.:]';
      $zones="";

      $exact="exactly";
      $approx="approximately";
      $business="business";

    } elsif ($lang eq "Swedish") {
      $month="Januari Februari Mars April Maj Juni ".
        "Juli Augusti September Oktober November December";
      $mon="Jan Feb Mar Apr Maj Jun Jul Aug Sep Okt Nov Dec";

      $week="Sundag Mondag Tisdag Onsdag Torsdag Fredag Lurdag";
      $wk="Sun Mon Tis Ons Tor Fre Lur";
      $w="S M Ti O To F Lu";

      $days[0]="1:a 2:a 3:e 4:e 5:e 6:e 7:e 8:e 9:e 10:e 11:e 12:e 13:e 14:e ".
        "15:e 16:e 17:e 18:e 19:e 20:e 21:a 22:a 23:e 24:e 25:e 26:e 27:e ".
        "28:e 29:e 30:e 31:a";
      $days[1]="fursta andra tredje fj=E4rde femte sj=E4tte sjunde ottonde ".
        "nionde tionde elte tolfte trettonde fjortonde femtonde sextonde ".
        "sjuttonde artonde nittonde tjugonde tjugofursta ".
        "tjugoandra tjugotredje tjugofj=E4rde tjugofemte tjugosj=E4tte ".
        "tjugosjunde tjugoottonde tjugonionde trettionde trettiofursta";

      $last="furra senaste";
      $in="om";
      $on="";
      $at="kl kl. klockan";
      $future="";
      $past="";
      $next="";
      $prev="";

      $am="FM";
      $pm="EM";

      $years  ="o or";
      $months ="mon monad monader";
      $weeks  ="";
      $days   ="d dag dagar";
      $hours  ="t tim timme timmar";
      $minutes="mn min minut minuter";
      $seconds="s sek sekund sekunder";
      $replace="m monad";

      $now="idag nu";
      $offset="igor -0:0:1:0:0:0 imorgon +0:0:1:0:0:0";
      $times="";
      $sephm='[:.]';
      $sepms=':';
      $sepss='[.:]';
      $zones="";

      $exact="";
      $approx="";
      $business="";

    } elsif ($lang eq "French") {
      $month="janvier fevrier mars avril mai juin juillet aout ".
        "septembre octobre novembre decembre";
      # NOTE: I am not sure what the abbreviation for juin and juillet are.
      $mon="jan fev mar avr mai juin juil aou sep oct nov dec";

      $week="dimanche lundi mardi mercredi jeudi vendredi samedi";
      $wk="dim lun mar mer jeu ven sam";
      $w="d l ma me j v s";

      @tmp=map { ($_."e"); } (1...31);
      $tmp[0] = "1er";
      $days[0]=join " ",@tmp;   # 1er 2e 3e ...
      $days[1]="1re";           # 1re
      $days[2]="premier deux trois quatre cinq six sept huit neuf dix onze ".
        "douze treize quatorze quinze size dix-sept dix-huit dix-neuf vingt ".
        "vingt_et_un vingt-deux vingt-trois vingt-quatre vingt-cinq ".
        "vingt-six vingt-sept vingt-huit vingt-neuf trente trente_et_un";

      $last="dernier";
      $in="en de";
      $on="";
      $at="a";
      $future="en";
      $past="il_y_a";
      $next="";
      $prev="";

      $am="du_matin";
      $pm="du_soir";

      $years  ="an annee ans annees";
      $months ="mois";
      $weeks  ="";
      $days   ="j jour jours";
      $hours  ="h heure heures";
      $minutes="mn min minute minutes";
      $seconds="s sec seconde secondes";
      $replace="m mois";

      $now="aujourd'hui maintenant";
      $offset="hier -0:0:1:0:0:0 demain +0:0:1:0:0:0";
      $times="";
      $sephm='[:h]';
      $sepms=':';
      $sepss='[.:,]';

      $zones="";

      $exact="";
      $approx="";
      $business="";

      # } elsif ($lang eq "Spanish") {
      #   $month="enero febrero marzo abril mayo junio julio agosto ".
      #     "septiembre octubre noviembre diciembre";
      #   $mon="ene feb mar abr may jun jul ago sep oct nov dic";

      #   $week="domingo lunes martes miercoles jueves viernes sabado";
      #   $wk="dom lun mar mier jue vie sab";
      #   $w="d l ma mi j v s";

      # } elsif ($lang eq "Italian") {
      # } elsif ($lang eq "Portugese") {
      # } elsif ($lang eq "German") {
      # } elsif ($lang eq "Russian") {

    } else {
      die "ERROR: Unknown language in Date::Manip.\n";
    }

    # Date::Manip:: variables for months
    #   $MonExp   : "(jan|january|feb|february ... )"
    #   @Mon      : ("Jan","Feb",...)
    #   @Month    : ("January","February", ...)
    #   %Month    : ("january",1,"jan",1, ...)
    $Date::Manip::MonExp=&Date_Regexp("$mon $month","lc,under,sort,back");
    ($tmp,@Date::Manip::Mon)  =&Date_Regexp($mon,"under",1);
    ($tmp,@Date::Manip::Month)=&Date_Regexp($month,"under",1);
    ($tmp,@tmp2)  =&Date_Regexp($mon,"","val1");
    ($tmp,@tmp3)=&Date_Regexp($month,"","val1");
    @tmp=reverse(@tmp2,@tmp3);
    ($tmp,%Date::Manip::Month)=&Date_Regexp(\@tmp,"lc,under","1");

    # Date::Manip:: variables for day of week
    #   $WkExp  : "(sun|sunday|mon|monday ... )"
    #   @W      : ("S","M",...)
    #   @Wk     : ("Sun","Mon",...)
    #   @Week   : ("Sunday","Monday",...)
    #   %Week   : ("sunday",0,"sun",0,"s",0,...)
    $Date::Manip::WkExp=&Date_Regexp("$wk $week","lc,under,sort,back");
    ($tmp,@Date::Manip::W)   =&Date_Regexp($w,"",1);
    ($tmp,@Date::Manip::Wk)  =&Date_Regexp($wk,"",1);
    ($tmp,@Date::Manip::Week)=&Date_Regexp($week,"",1);
    ($tmp,@tmp )=&Date_Regexp($w,"","val0");
    ($tmp,@tmp2)=&Date_Regexp($wk,"","val0");
    ($tmp,@tmp3)=&Date_Regexp($week,"","val0");
    @tmp=reverse(@tmp,@tmp2,@tmp3);
    ($tmp,%Date::Manip::Week)=&Date_Regexp(\@tmp,"lc,under","1");

    # Date::Manip:: variables for day of week
    #   $DayExp   : "(1st|first|2nd|second ... )"
    #   %Day      : ("1st",1,"first",1, ... )"
    #   @Day      : ("1st","2nd",...);
    # Date::Manip:: variables for week of month
    #   $WhichExp : "(1st|first|2nd|second ... fifth|last)"
    #   %Which    : ("1st",1,"first",1, ... "fifth",5,"last",-1)"
    $Date::Manip::DayExp=&Date_Regexp(join(" ",@days),"back,sort,lc,under");
    ($tmp,@Date::Manip::Day)=&Date_Regexp($days[0],"lc,under",1);
    %Date::Manip::Day  =();
    %Date::Manip::Which=();
    @tmp2=@tmp3=();
    foreach $days (@days) {
      ($tmp,%tmp) =&Date_Regexp($days,"lc,under","val1");
      @tmp=();
      foreach (1,2,3,4,5) {
        push(@tmp,($_,$tmp{$_}))  if (exists $tmp{$_});
      }
      push(@tmp3,reverse (@tmp));

      @tmp=%tmp;
      push(@tmp2,reverse(@tmp));
    }
    %Date::Manip::Day=@tmp2;
    ($tmp,@tmp) =&Date_Regexp($last,"lc,under",1);
    push(@tmp3, map { $_,-1 } @tmp);
    ($Date::Manip::WhichExp,@tmp)= &Date_Regexp(\@tmp3,"sort,back","keys");
    %Date::Manip::Which=@tmp3;

    # Date::Manip:: variables for AM or PM
    #   $AmExp   : "(am)"
    #   $PmExp   : "(pm)"
    #   $AmPmExp : "(am|pm)"
    #   %AmPm    : (am,1,pm,2)
    #   $Am      : "AM"
    #   $Pm      : "PM"
    $Date::Manip::AmPmExp=&Date_Regexp("$am $pm","lc,back,under");
    ($Date::Manip::AmExp,@tmp2)=&Date_Regexp("$am","lc,back,under",1);
    ($Date::Manip::PmExp,@tmp3)=&Date_Regexp("$pm","lc,back,under",1);
    @tmp=map { $_,1 } @tmp2;
    push(@tmp,map { $_,2 } @tmp3);
    %Date::Manip::AmPm=@tmp;
    ($tmp,@tmp2)=&Date_Regexp("$am","under",1);
    ($tmp,@tmp3)=&Date_Regexp("$pm","under",1);
    $Date::Manip::Am=shift(@tmp2);
    $Date::Manip::Pm=shift(@tmp3);

    # Date::Manip:: variables for expressions used in parsing deltas
    #    $YExp   : "(?:y|yr|year|years)"
    #    $MExp   : similar for months
    #    $WExp   : similar for weeks
    #    $DExp   : similar for days
    #    $HExp   : similar for hours
    #    $MnExp  : similar for minutes
    #    $SExp   : similar for seconds
    #    %Replace: a list of replacements
    $Date::Manip::YExp   =&Date_Regexp($years,"lc,sort,under");
    $Date::Manip::MExp   =&Date_Regexp($months,"lc,sort,under");
    $Date::Manip::WExp   =&Date_Regexp($weeks,"lc,sort,under");
    $Date::Manip::DExp   =&Date_Regexp($days,"lc,sort,under");
    $Date::Manip::HExp   =&Date_Regexp($hours,"lc,sort,under");
    $Date::Manip::MnExp  =&Date_Regexp($minutes,"lc,sort,under");
    $Date::Manip::SExp   =&Date_Regexp($seconds,"lc,sort,under,opt");
    %Date::Manip::Replace=split(/\s+/,lc($replace));

    # Date::Manip:: variables for special dates that are offsets from now
    #    $Now      : "(now|today)"
    #    $Offset   : "(yesterday|tomorrow)"
    #    %Offset   : ("yesterday","-1:0:0:0",...)
    #    $TimesExp : "(noon|midnight)"
    #    %Times    : ("noon","12:00:00","midnight","00:00:00")
    $Date::Manip::Now=   &Date_Regexp($now,"lc,back,under");
    ($Date::Manip::Offset,%Date::Manip::Offset)=
      &Date_Regexp($offset,"lc,under,back","keys");
    ($Date::Manip::TimesExp,%Date::Manip::Times)=
      &Date_Regexp($times,"lc,under,back","keys");
    $Date::Manip::SepHM=$sephm;
    $Date::Manip::SepMS=$sepms;
    $Date::Manip::SepSS=$sepss;

    # Date::Manip:: variables for time zones
    #    $ZoneExp: regular expression
    #    %Zone   : all zones with their translation
    #    $Zone   : the current time zone
    $zonesrfc=
      "US/Pacific  -0800 ".
      "US/Mountain -0700 ".
      "US/Central  -0600 ".
      "US/Eastern  -0500 ".
      "idlw -1200 ".  # International Date Line West
      "nt   -1100 ".  # Nome
      "hst  -1000 ".  # Hawaii Standard
      "cat  -1000 ".  # Central Alaska
      "ahst -1000 ".  # Alaska-Hawaii Standard
      "yst  -0900 ".  # Yukon Standard
      "hdt  -0900 ".  # Hawaii Daylight
      "ydt  -0800 ".  # Yukon Daylight
      "pst  -0800 ".  # Pacific Standard
      "pdt  -0700 ".  # Pacific Daylight
      "mst  -0700 ".  # Mountain Standard
      "mdt  -0600 ".  # Mountain Daylight
      "cst  -0600 ".  # Central Standard
      "cdt  -0500 ".  # Central Daylight
      "est  -0500 ".  # Eastern Standard
      "edt  -0400 ".  # Eastern Daylight
      "ast  -0400 ".  # Atlantic Standard
      #"nst -0330 ".  # Newfoundland Standard       nst=North Sumatra    +0630
      "nft  -0330 ".  # Newfoundland
      #"gst -0300 ".  # Greenland Standard          gst=Guam Standard    +1000
      "bst  -0300 ".  # Brazil Standard             bst=British Summer   +0100
      "adt  -0300 ".  # Atlantic Daylight
      "ndt  -0230 ".  # Newfoundland Daylight
      "at   -0200 ".  # Azores
      "wat  -0100 ".  # West Africa
      "gmt  +0000 ".  # Greenwich Mean
      "ut   +0000 ".  # Universal (Coordinated)
      "utc  +0000 ".  # Universal (Coordinated)
      "wet  +0000 ".  # Western European
      "cet  +0100 ".  # Central European
      "fwt  +0100 ".  # French Winter
      "met  +0100 ".  # Middle European
      "mewt +0100 ".  # Middle European Winter
      "swt  +0100 ".  # Swedish Winter
      #"bst +0100 ".  # British Summer              bst=Brazil standard  -0300
      "eet  +0200 ".  # Eastern Europe, USSR Zone 1
      "fst  +0200 ".  # French Summer
      "mest +0200 ".  # Middle European Summer
      "sst  +0200 ".  # Swedish Summer              sst=South Sumatra    +0700
      "bt   +0300 ".  # Baghdad, USSR Zone 2
      "it   +0330 ".  # Iran
      "zp4  +0400 ".  # USSR Zone 3
      "zp5  +0500 ".  # USSR Zone 4
      "ist  +0530 ".  # Indian Standard
      "zp6  +0600 ".  # USSR Zone 5
      "nst  +0630 ".  # North Sumatra               nst=Newfoundland Std -0330
      "wast +0700 ".  # West Australian Standard
      #"sst +0700 ".  # South Sumatra, USSR Zone 6  sst=Swedish Summer   +0200
      "jt   +0730 ".  # Java (3pm in Cronusland!)
      "cct  +0800 ".  # China Coast, USSR Zone 7
      "wadt +0800 ".  # West Australian Daylight
      "jst  +0900 ".  # Japan Standard, USSR Zone 8
      "cast +0930 ".  # Central Australian Standard
      "east +1000 ".  # Eastern Australian Standard
      "gst  +1000 ".  # Guam Standard, USSR Zone 9  gst=Greenland Std    -0300
      "cadt +1030 ".  # Central Australian Daylight
      "eadt +1100 ".  # Eastern Australian Daylight
      "idle +1200 ".  # International Date Line East
      "nzst +1200 ".  # New Zealand Standard
      "nzt  +1200 ".  # New Zealand
      "nzdt +1300 ".  # New Zealand Daylight
      "z +0000 ".
      "a -0100 b -0200 c -0300 d -0400 e -0500 f -0600 g -0700 h -0800 ".
      "i -0900 k -1000 l -1100 m -1200 ".
      "n +0100 o +0200 p +0300 q +0400 r +0500 s +0600 t +0700 u +0800 ".
      "v +0900 w +1000 x +1100 y +1200 ".
      '[+-]\d{4} 0000';
    ($Date::Manip::ZoneExp,%Date::Manip::Zone)=
      &Date_Regexp("$zonesrfc $zones","sort,lc,under,back,opt,PRE,POST",
                   "keys");
    # In order to handle "US/Eastern", quote "/" characters.
    $Date::Manip::ZoneExp =~ s|/|\/|g;
    $Date::Manip::TZ=&Date_TimeZone;

    # Date::Manip:: misc. variables
    #    $At     : "(?:at)"
    #    $In     : "(?:in|of)"
    #    $On     : "(?:on)"
    #    $Future : "(?:in)"
    #    $Past   : "(?:ago)"
    #    $Next   : "(?:next)"
    #    $Prev   : "(?:last|previous)"
    $Date::Manip::At    =&Date_Regexp($at,"lc,under,pre,post");
    $Date::Manip::In    =&Date_Regexp($in,"lc,under,pre,post");
    $Date::Manip::On    =&Date_Regexp($on,"lc,under,pre,post");
    $Date::Manip::Future=&Date_Regexp($future,"lc,under");
    $Date::Manip::Past  =&Date_Regexp($past,"lc,under");
    $Date::Manip::Next  =&Date_Regexp($next,"lc,under");
    $Date::Manip::Prev  =&Date_Regexp($prev,"lc,under");

    # Date::Manip:: calc mode variables
    #    $Approx  : "(?:approximately)"
    #    $Exact   : "(?:exactly)"
    #    $Business: "(?:business)"
    $Date::Manip::Exact   =&Date_Regexp($exact,"lc,under");
    $Date::Manip::Approx  =&Date_Regexp($approx,"lc,under");
    $Date::Manip::Business=&Date_Regexp($business,"lc,under");

    ############### END OF LANGUAGE INITIALIZATION
  }

  if ($Date::Manip::ResetWorkDay) {
    my($h1,$m1,$h2,$m2)=();
    if ($Date::Manip::WorkDay24Hr) {
      ($Date::Manip::WDBh,$Date::Manip::WDBm)=(0,0);
      ($Date::Manip::WDEh,$Date::Manip::WDEm)=(24,0);
      $Date::Manip::WDlen=24*60;
      $Date::Manip::WorkDayBeg="00:00";
      $Date::Manip::WorkDayEnd="23:59";

    } else {
      die "ERROR: Invalid WorkDayBeg in Date::Manip.\n"
        if (! (($h1,$m1)=&CheckTime($Date::Manip::WorkDayBeg)));
      die "ERROR: Invalid WorkDayEnd in Date::Manip.\n"
        if (! (($h2,$m2)=&CheckTime($Date::Manip::WorkDayEnd)));

      ($Date::Manip::WDBh,$Date::Manip::WDBm)=($h1,$m1);
      ($Date::Manip::WDEh,$Date::Manip::WDEm)=($h2,$m2);

      # Work day length = h1:m1  or  0:len (len minutes)
      $h1=$h2-$h1;
      $m1=$m2-$m1;
      if ($m1<0) {
        $h1--;
        $m1+=60;
      }
      $Date::Manip::WDlen=$h1*60+$m1;
    }
    $Date::Manip::ResetWorkDay=0;
  }

  # current time
  my($s,$mn,$h,$d,$m,$y,$wday,$yday,$isdst)=localtime(time);
  $y+=1900;
  my($ampm)=();
  $wk="";
  $m++;
  &Date_ErrorCheck(\$y,\$m,\$d,\$h,\$mn,\$s,\$ampm,\$wk);
  $Date::Manip::CurrY=$y;
  $Date::Manip::CurrM=$m;
  $Date::Manip::CurrD=$d;
  $Date::Manip::CurrH=$h;
  $Date::Manip::CurrMn=$mn;
  $Date::Manip::CurrS=$s;
  $Date::Manip::CurrAmPm=$ampm;
  $Date::Manip::Curr=&FormDate($y,$m,$d,$h,$mn,$s);
}

# Returns 1 if $date is a work day.  If $time is non-zero, the time is
# also checked to see if it falls within work hours.
sub Date_IsWorkDay {
  my($date,$time)=@_;
  &Date_Init()  if (! $Date::Manip::InitDone);
  my($d)=$date;
  $d=&Date_SetTime($date,$Date::Manip::WorkDayBeg)
    if (! defined $time  or  ! $time);

  my($y,$dofw,$h,$m)=&UnixDate($d,"%Y","%w","%H","%M");
  return 0  if ($dofw<$Date::Manip::WorkWeekBeg or
                $dofw>$Date::Manip::WorkWeekEnd or
                "$h:$m" lt $Date::Manip::WorkDayBeg or
                "$h:$m" gt $Date::Manip::WorkDayEnd);
  if ($y!=$Date::Manip::CurrHolidayYear) {
    $Date::Manip::CurrHolidayYear=$y;
    &Date_UpdateHolidays;
  }
  $d=&Date_SetTime($date,"00:00:00");
  return 0  if (exists $Date::Manip::CurrHolidays{$d});
  1;
}

# Finds the day $off work days from now.  If $time is passed in, we must
# also take into account the time of day.
#
# If $time is not passed in, day 0 is today (if today is a workday) or the
# next work day if it isn't.  In any case, the time of day is unaffected.
#
# If $time is passed in, day 0 is now (if now is part of a workday) or the
# start of the very next work day.
sub Date_NextWorkDay {
  my($date,$off,$time)=@_;
  &Date_Init()  if (! $Date::Manip::InitDone);
  my($err)=();

  if (! &Date_IsWorkDay($date,$time)) {
    if (defined $time and $time) {
      while (1) {
        $date=&Date_GetNext($date,undef,0,$Date::Manip::WorkDayBeg);
        last  if (&Date_IsWorkDay($date,$time));
      }
    } else {
      while (1) {
        $date=&DateCalc_DateDelta($date,"+0:0:1:0:0:0",\$err,0);
        last  if (&Date_IsWorkDay($date,$time));
      }
    }
  }

  while ($off>0) {
    while (1) {
      $date=&DateCalc_DateDelta($date,"+0:0:1:0:0:0",\$err,0);
      last  if (&Date_IsWorkDay($date,$time));
    }
    $off--;
  }

  return $date;
}

# Finds the day $off work days before now.  If $time is passed in, we must
# also take into account the time of day.
#
# If $time is not passed in, day 0 is today (if today is a workday) or the
# previous work day if it isn't.  In any case, the time of day is unaffected.
#
# If $time is passed in, day 0 is now (if now is part of a workday) or the
# end of the previous work period.  Note that since the end of a work day
# will automatically be turned into the start of the next one, this time
# may actually be treated as AFTER the current time.
sub Date_PrevWorkDay {
  my($date,$off,$time)=@_;
  &Date_Init()  if (! $Date::Manip::InitDone);
  my($err)=();

  if (! &Date_IsWorkDay($date,$time)) {
    if (defined $time and $time) {
      while (1) {
        $date=&Date_GetPrev($date,undef,0,$Date::Manip::WorkDayEnd);
        last  if (&Date_IsWorkDay($date,$time));
      }
      while (1) {
        $date=&Date_GetNext($date,undef,0,$Date::Manip::WorkDayBeg);
        last  if (&Date_IsWorkDay($date,$time));
      }
    } else {
      while (1) {
        $date=&DateCalc_DateDelta($date,"-0:0:1:0:0:0",\$err,0);
        last  if (&Date_IsWorkDay($date,$time));
      }
    }
  }

  while ($off>0) {
    while (1) {
      $date=&DateCalc_DateDelta($date,"-0:0:1:0:0:0",\$err,0);
      last  if (&Date_IsWorkDay($date,$time));
    }
    $off--;
  }

  return $date;
}

########################################################################
# NOT FOR EXPORT
########################################################################

# This is used in Date_Init to prepare regular expressions.  A list of
# items is passed in (either as a space separated string, or a reference to
# a list) and a regular expression which matches any one of the items is
# prepared.  The regular expression will be of one of the forms:
#   "(a|b)"       @list not empty, back option included
#   "(?:a|b)"     @list not empty
#   "()"          @list empty,     back option included
#   ""            @list empty
# $options is a string which contains any of the following strings:
#   back     : the regular expression has a backreference
#   opt      : the regular expression is optional and a "?" is appended in
#              the first two forms
#   sort     : the items in the list are sorted by length (longest first)
#   lc       : the string is lowercased
#   under    : any underscores are converted to spaces
#   pre      : it may be preceded by whitespace
#   Pre      : it must be preceded by whitespace
#   PRE      : it must be preceded by whitespace or the start
#   post     : it may be followed by whitespace
#   Post     : it must be followed by whitespace
#   POST     : it must be followed by whitespace or the end
# Spaces due to pre/post options will not be included in the back reference.
#
# If $array is included, then the elements will also be returned as a list.
# $array is a string which may contain any of the following:
#   keys     : treat the list as a hash and only the keys go into the regexp
#   key0     : treat the list as the values of a hash with keys 0 .. N-1
#   key1     : treat the list as the values of a hash with keys 1 .. N
#   val0     : treat the list as the keys of a hash with values 0 .. N-1
#   val1     : treat the list as the keys of a hash with values 1 .. N
sub Date_Regexp {
  my($list,$options,$array)=@_;
  my(@list,$ret,%hash,$i)=();
  $options=""  if (! defined $options);
  $array=""    if (! defined $array);

  my($sort,$lc,$under)=(0,0,0);
  $sort =1  if ($options =~ /sort/i);
  $lc   =1  if ($options =~ /lc/i);
  $under=1  if ($options =~ /under/i);
  my($back,$opt,$pre,$post)=("?:","","","");
  $back =""          if ($options =~ /back/i);
  $opt  ="?"         if ($options =~ /opt/i);
  $pre  ='\s*'       if ($options =~ /pre/);
  $pre  ='\s+'       if ($options =~ /Pre/);
  $pre  ='(?:\s+|^)' if ($options =~ /PRE/);
  $post ='\s*'       if ($options =~ /post/);
  $post ='\s+'       if ($options =~ /Post/);
  $post ='(?:$|\s+)' if ($options =~ /POST/);

  my($hash,$keys,$key0,$key1,$val0,$val1)=(0,0,0,0,0,0);
  $keys =1     if ($array =~ /keys/i);
  $key0 =1     if ($array =~ /key0/i);
  $key1 =1     if ($array =~ /key1/i);
  $val0 =1     if ($array =~ /val0/i);
  $val1 =1     if ($array =~ /val1/i);
  $hash =1     if ($keys or $key0 or $key1 or $val0 or $val1);

  my($ref)=ref $list;
  if (! $ref) {
    $list =~ s/\s*$//;
    $list =~ s/^\s*//;
    $list =~ s/\s+/&&&/g;
  } elsif ($ref eq "ARRAY") {
    $list = join("&&&",@$list);
  } else {
    die "ERROR: Date_Regexp.\n";
  }

  if (! $list) {
    if ($back eq "") {
      return "()";
    } else {
      return "";
    }
  }

  $list=lc($list)  if ($lc);
  $list=~ s/_/ /g  if ($under);
  @list=split(/&&&/,$list);
  if ($keys) {
    %hash=@list;
    @list=keys %hash;
  } elsif ($key0 or $key1 or $val0 or $val1) {
    $i=0;
    $i=1  if ($key1 or $val1);
    if ($key0 or $key1) {
      %hash= map { $_,$i++ } @list;
    } else {
      %hash= map { $i++,$_ } @list;
    }
  }
  @list=sort sortByLength(@list)  if ($sort);

  $ret="($back" . join("|",@list) . ")";
  $ret="(?:$pre$ret$post)"  if ($pre or $post);
  $ret.=$opt;

  if ($array and $hash) {
    return ($ret,%hash);
  } elsif ($array) {
    return ($ret,@list);
  } else {
    return $ret;
  }
}

# This will produce a delta with the correct number of signs.  At most two
# signs will be in it normally (one before the year, and one in front of
# the day), but if appropriate, signs will be in front of all elements.
# Also, as many of the signs will be equivalent as possible.
sub NormalizeDelta {
  my($delta,$mode)=@_;
  return "" if (! defined $delta  or  ! $delta);
  return "+0:+0:+0:+0:+0:+0"
    if ($delta =~ /^([+-]?0+:){5}[+-]?0+/ and $Date::Manip::DeltaSigns);
  return "+0:0:0:0:0:0" if ($delta =~ /^([+-]?0+:){5}[+-]?0+/);

  my($tmp,$sign1,$sign2,$len)=();

  # Calculate the length of the day in minutes
  $len=24*60;
  $len=$Date::Manip::WDlen  if ($mode==2);

  # We have to get the sign of every component explicitely so that a "-0"
  # or "+0" doesn't get lost by treating it numerically (i.e. "-0:0:2" must
  # be a negative delta).

  my($y,$mon,$d,$h,$m,$s)=&CheckDelta($delta);

  # We need to make sure that the signs of all parts of a delta are the
  # same.  The easiest way to do this is to convert all of the large
  # components to the smallest ones, then convert the smaller components
  # back to the larger ones.

  # Do the year/month part

  $mon += $y*12;                         # convert y to m
  $sign1="+";
  if ($mon<0) {
    $mon *= -1;
    $sign1="-";
  }

  $y    = ($tmp=int($mon/12));           # convert m to y
  $mon -= $tmp*12;

  $y=0    if ($y eq "-0");               # get around silly -0 problem
  $mon=0  if ($mon eq "-0");

  # Do the day/hour/min/sec part

  $s += $d*$len*60 + $h*3600 + $m*60;    # convert d/h/m to s
  $sign2="+";
  if ($s<0) {
    $s*=-1;
    $sign2="-";
  }

  $m  = ($tmp=int($s/60));               # convert s to m
  $s -= $tmp*60;
  $d  = ($tmp=int($m/$len));             # convert m to d
  $m -= $tmp*$len;
  $h  = ($tmp=int($m/60));               # convert m to h
  $m -= $tmp*60;

  $d=0    if ($d eq "-0");               # get around silly -0 problem
  $h=0    if ($h eq "-0");
  $m=0    if ($m eq "-0");
  $s=0    if ($s eq "-0");

  # Only include two signs if necessary
  $sign1=$sign2  if ($y==0 and $mon==0);
  $sign2=$sign1  if ($d==0 and $h==0 and $m==0 and $s==0);
  $sign2=""  if ($sign1 eq $sign2  and  ! $Date::Manip::DeltaSigns);

  if ($Date::Manip::DeltaSigns) {
    return "$sign1$y:$sign1$mon:$sign2$d:$sign2$h:$sign2$m:$sign2$s";
  } else {
    return "$sign1$y:$mon:$sign2$d:$h:$m:$s";
  }
}

# This checks a delta to make sure it is valid.  If it is, it splits
# it and returns the elements with a sign on each.  The 2nd argument
# specifies the default sign.  Blank elements are set to 0.  If the
# third element is non-nil, exactly 6 elements must be included.
sub CheckDelta {
  my($delta,$sign,$exact)=@_;
  my(@delta)=split(/:/,$delta);
  return ()  if (defined $exact  and  $exact  and $#delta != 5);
  my($i)=();
  $sign="+"  if (! defined $sign);
  for ($i=0; $i<=$#delta; $i++) {
    $delta[$i]="0"  if (! $delta[$i]);
    return ()  if ($delta[$i] !~ /^[+-]?\d+$/);
    $sign = ($delta[$i] =~ s/^([+-])// ? $1 : $sign);
    $delta[$i] = $sign.$delta[$i];
  }
  @delta;
}

# Reads up to 3 arguments.  $h may contain the time in any international
# fomrat.  Any empty elements are set to 0.
sub ParseTime {
  my($h,$m,$s)=@_;
  my($t)=&CheckTime("one");

  if (defined $h  and  $h =~ /$t/) {
    $h=$1;
    $m=$2;
    $s=$3   if (defined $3);
  }
  $h="00"  if (! defined $h);
  $m="00"  if (! defined $m);
  $s="00"  if (! defined $s);

  ($h,$m,$s);
}

# Forms a date with the 6 elements passed in (all of which must be defined).
# No check as to validity is made.
sub FormDate {
  my($y,$m,$d,$h,$mn,$s)=@_;
  my($ym,$md,$dh,$hmn,$mns)=();

  if      ($Date::Manip::Internal == 0) {
    $ym=$md=$dh="";
    $hmn=$mns=":";

  } elsif ($Date::Manip::Internal == 1) {
    $ym=$md=$dh=$hmn=$mns="";

  } elsif ($Date::Manip::Internal == 2) {
    $ym=$md="-";
    $dh=" ";
    $hmn=$mns=":";

  } else {
    die "ERROR: Invalid internal format in Date_FormDate.\n";
  }
  $m="0$m"    if (length($m)==1);
  $d="0$d"    if (length($d)==1);
  $h="0$h"    if (length($h)==1);
  $mn="0$mn"  if (length($mn)==1);
  $s="0$s"    if (length($s)==1);
  "$y$ym$m$md$d$dh$h$hmn$mn$mns$s";
}

# This checks a time.  If it is valid, it splits it and returns 3 elements.
# If "one" or "two" is passed in, a regexp with 1/2 or 2 digit hours is
# returned.
sub CheckTime {
  my($time)=@_;
  my($h)='(?:0?[0-9]|1[0-9]|2[0-3])';
  my($h2)='(?:0[0-9]|1[0-9]|2[0-3])';
  my($m)='[0-5][0-9]';
  my($s)=$m;
  my($hm)="(?:$Date::Manip::SepHM|:)";
  my($ms)="(?:$Date::Manip::SepMS|:)";
  my($ss)=$Date::Manip::SepSS;
  my($t)="^($h)$hm($m)(?:$ms($s)(?:$ss\d+)?)?\$";
  if ($time eq "one") {
    return $t;
  } elsif ($time eq "two") {
    $t="^($h2)$hm($m)(?:$ms($s)(?:$ss\d+)?)?\$";
    return $t;
  }

  if ($time =~ /$t/i) {
    ($h,$m,$s)=($1,$2,$3);
    $h="0$h" if (length($h)<2);
    $m="0$m" if (length($m)<2);
    $s="00"  if (! defined $s);
    return ($h,$m,$s);
  } else {
    return ();
  }
}

# This checks a date.  If it is valid, it splits it and returns the elements.
# If no date is passed in, it returns a regular expression for the date.
sub CheckDate {
  my($date)=@_;
  my($ym,$md,$dh,$hmn,$mns)=();
  my($y)='(\d{4})';
  my($m)='(0[1-9]|1[0-2])';
  my($d)='(0[1-9]|[1-2][0-9]|3[0-1])';
  my($h)='([0-1][0-9]|2[0-3])';
  my($mn)='([0-5][0-9])';
  my($s)=$mn;

  if      ($Date::Manip::Internal == 0) {
    $ym=$md=$dh="";
    $hmn=$mns=":";

  } elsif ($Date::Manip::Internal == 1) {
    $ym=$md=$dh=$hmn=$mns="";

  } elsif ($Date::Manip::Internal == 2) {
    $ym=$md="-";
    $dh=" ";
    $hmn=$mns=":";

  } else {
    die "ERROR: Invalid internal format in Date_CheckDate.\n";
  }

  my($t)="^$y$ym$m$md$d$dh$h$hmn$mn$mns$s\$";
  return $t  if (! $date);

  return ($1,$2,$3,$4,$5,$6)  if ($date =~ /$t/);
  return ();
}

sub DateCalc_DateDate {
  my($D1,$D2,$mode)=@_;
  my(@d_in_m)=(0,31,28,31,30,31,30,31,31,30,31,30,31);

  # Exact mode
  if ($mode==0) {
    my($y1,$m1,$d1,$h1,$mn1,$s1)=&CheckDate($D1);
    my($y2,$m2,$d2,$h2,$mn2,$s2)=&CheckDate($D2);
    my($i,@delta,$d,$delta,$y)=();

    # form the delta for hour/min/sec
    $delta[3]=$h2-$h1;
    $delta[4]=$mn2-$mn1;
    $delta[5]=$s2-$s1;

    # form the delta for yr/mon/day
    $delta[0]=$delta[1]=0;
    $d=0;
    if ($y2>$y1) {
      $d=&Date_DaysInYear($y1) - &Date_DayOfYear($m1,$d1,$y1);
      $d+=&Date_DayOfYear($m2,$d2,$y2);
      for ($y=$y1+1; $y<$y2; $y++) {
        $d+= &Date_DaysInYear($y);
      }
    } elsif ($y2<$y1) {
      $d=&Date_DaysInYear($y2) - &Date_DayOfYear($m2,$d2,$y2);
      $d+=&Date_DayOfYear($m1,$d1,$y1);
      for ($y=$y2+1; $y<$y1; $y++) {
        $d+= &Date_DaysInYear($y);
      }
      $d *= -1;
    } else {
      $d=&Date_DayOfYear($m2,$d2,$y2) - &Date_DayOfYear($m1,$d1,$y1);
    }
    $delta[2]=$d;

    for ($i=0; $i<6; $i++) {
      $delta[$i]="+".$delta[$i]  if ($delta[$i]>=0);
    }

    $delta=join(":",@delta);
    $delta=&NormalizeDelta($delta,0);
    return $delta;
  }

  my($date1,$date2)=($D1,$D2);
  my($tmp,$sign,$err,@tmp)=();

  # make sure both are work days
  if ($mode==2) {
    $date1=&Date_NextWorkDay($date1,0,1);
    $date2=&Date_NextWorkDay($date2,0,1);
  }

  # make sure date1 comes before date2
  if ($date1 gt $date2) {
    $sign="-";
    $tmp=$date1;
    $date1=$date2;
    $date2=$tmp;
  } else {
    $sign="+";
  }
  if ($date1 eq $date2) {
    return "+0:+0:+0:+0:+0:+0"  if ($Date::Manip::DeltaSigns);
    return "+0:0:0:0:0:0";
  }

  my($y1,$m1,$d1,$h1,$mn1,$s1)=&CheckDate($date1);
  my($y2,$m2,$d2,$h2,$mn2,$s2)=&CheckDate($date2);
  my($dy,$dm,$dd,$dh,$dmn,$ds,$ddd)=();

  # Do years
  $dy=$y2-$y1;
  $dm=0;
  if ($dy>0) {
    $tmp=&DateCalc_DateDelta($date1,"+$dy:0:0:0:0:0",\$err,0);
    if ($tmp gt $date2) {
      $dy--;
      $tmp=$date1;
      $tmp=&DateCalc_DateDelta($date1,"+$dy:0:0:0:0:0",\$err,0)  if ($dy>0);
      $dm=12;
    }
    $date1=$tmp;
  }

  # Do months
  $dm+=$m2-$m1;
  if ($dm>0) {
    $tmp=&DateCalc_DateDelta($date1,"+0:$dm:0:0:0:0",\$err,0);
    if ($tmp gt $date2) {
      $dm--;
      $tmp=$date1;
      $tmp=&DateCalc_DateDelta($date1,"+0:$dm:0:0:0:0",\$err,0)  if ($dm>0);
    }
    $date1=$tmp;
  }

  # Do days
  if ($mode==2) {
    $dd=0;
    while (1) {
      $tmp=&Date_NextWorkDay($date1,1,1);
      if ($tmp le $date2) {
        $dd++;
        $date1=$tmp;
      } else {
        last;
      }
    }

  } else {
    $d1=( &CheckDate($date1) )[2];
    $dd=0;
    $ddd=0;
    # If we're jumping across months, set $d1 to the first of the next month
    # (or possibly the 0th of next month which is equivalent to the last day
    # of this month)
    if ($d1>$d2) {
      ($y1,$m1,$d1)=( &CheckDate($date1) )[0..2];
      $d_in_m[2]=29  if (&Date_LeapYear($y1));
      $dd=$d_in_m[$m1]-$d1+1;
      $d1=1;
      $tmp=&DateCalc_DateDelta($date1,"+0:0:$dd:0:0:0",\$err,0);
      if ($tmp gt $date2) {
        $dd--;
        $d1--;
        $tmp=&DateCalc_DateDelta($date1,"+0:0:$dd:0:0:0",\$err,0);
      }
      $date1=$tmp;
    }

    if ($d1<$d2) {
      $ddd=$d2-$d1;
      $tmp=&DateCalc_DateDelta($date1,"+0:0:$ddd:0:0:0",\$err,0);
      if ($tmp gt $date2) {
        $ddd--;
        $tmp=&DateCalc_DateDelta($date1,"+0:0:$ddd:0:0:0",\$err,0);
      }
      $date1=$tmp;
    }
    $dd+=$ddd;
  }

  # in business mode, make sure h1 comes before h2 (if not find delta between
  # now and end of day and move to start of next business day)
  $d1=( &CheckDate($date1) )[2];
  $dh=$dmn=$ds=0;
  if ($mode==2  and  $d1<$d2) {
    $tmp=&Date_SetTime($date1,$Date::Manip::WorkDayEnd);
    $tmp=&DateCalc_DateDelta($tmp,"+0:0:0:0:1:0")
      if ($Date::Manip::WorkDay24Hr);
    $tmp=&DateCalc_DateDate($date1,$tmp,0);
    ($tmp,$tmp,$tmp,$dh,$dmn,$ds)=&CheckDelta($tmp);
    $date1=&Date_NextWorkDay($date1,1,0);
    $date1=&Date_SetTime($date1,$Date::Manip::WorkDayBeg);
    $d1=( &CheckDate($date1) )[2];
    die "ERROR: DateCalc DateDate Business.\n"  if ($d1 != $d2);
  }

  # Hours, minutes, seconds
  $tmp=&DateCalc_DateDate($date1,$date2,0);
  @tmp=&CheckDelta($tmp);
  $dh  += $tmp[3];
  $dmn += $tmp[4];
  $ds  += $tmp[5];

  if ($Date::Manip::DeltaSigns) {
    return "$sign$dy:$sign$dm:$sign$dd:$sign$dh:$sign$dmn:$sign$ds";
  } else {
    return "$sign$dy:$dm:$dd:$dh:$dmn:$ds";
  }
}

sub DateCalc_DeltaDelta {
  my($D1,$D2,$mode)=@_;
  my(@delta1,@delta2,$i,$delta,@delta)=();

  @delta1=&CheckDelta($D1);
  @delta2=&CheckDelta($D2);
  for ($i=0; $i<6; $i++) {
    $delta[$i]=$delta1[$i]+$delta2[$i];
    $delta[$i]="+".$delta[$i]  if ($delta[$i]>=0);
  }

  $delta=join(":",@delta);
  $delta=&NormalizeDelta($delta,$mode);
  return $delta;
}

sub DateCalc_DateDelta {
  my($D1,$D2,$errref,$mode)=@_;
  my($date)=();
  my(@d_in_m)=(0,31,28,31,30,31,30,31,31,30,31,30,31);
  my($h1,$m1,$h2,$m2,$len,$hh,$mm)=();

  if ($mode==2) {
    $h1=$Date::Manip::WDBh;
    $m1=$Date::Manip::WDBm;
    $h2=$Date::Manip::WDEh;
    $m2=$Date::Manip::WDEm;
    $hh=$h2-$h1;
    $mm=$m2-$m1;
    if ($mm<0) {
      $hh--;
      $mm+=60;
    }
  }

  # Date, delta
  my($y,$m,$d,$h,$mn,$s)=&CheckDate($D1);
  my($dy,$dm,$dd,$dh,$dmn,$ds)=&CheckDelta($D2);

  # do the month/year part
  $y+=$dy;
  &ModuloAddition(-12,$dm,\$m,\$y);   # -12 means 1-12 instead of 0-11

  # in business mode, set the day to a work day at this point so the h/mn/s
  # stuff will work out
  if ($mode==2) {
    $d_in_m[2]=29  if (&Date_LeapYear($y));
    $d=$d_in_m[$m] if ($d>$d_in_m[$m]);
    $date=&Date_NextWorkDay(&FormDate($y,$m,$d,$h,$mn,$s),0,1);
    ($y,$m,$d,$h,$mn,$s)=&CheckDate($date);
  }

  # seconds, minutes, hours
  &ModuloAddition(60,$ds,\$s,\$mn);
  if ($mode==2) {
    while (1) {
      &ModuloAddition(60,$dmn,\$mn,\$h);
      $h+= $dh;

      if ($h>$h2  or  $h==$h2 && $mn>$m2) {
        $dh=$h-$h2;
        $dmn=$mn-$m2;
        $h=$h1;
        $mn=$m1;
        $dd++;

      } elsif ($h<$h1  or  $h==$h1 && $mn<$m1) {
        $dh=$h1-$h;
        $dmn=$m1-$mn;
        $h=$h2;
        $mn=$m2;
        $dd--;

      } elsif ($h==$h2  &&  $mn==$m2) {
        $dd++;
        $dh=-$hh;
        $dmn=-$mm;

      } else {
        last;
      }
    }

  } else {
    &ModuloAddition(60,$dmn,\$mn,\$h);
    &ModuloAddition(24,$dh,\$h,\$d);
  }

  # days
  if ($mode==2) {
    if ($dd>=0) {
      $date=&Date_NextWorkDay(&FormDate($y,$m,$d,$h,$mn,$s),$dd,1);
    } else {
      $date=&Date_PrevWorkDay(&FormDate($y,$m,$d,$h,$mn,$s),-$dd,1);
    }
    ($y,$m,$d,$h,$mn,$s)=&CheckDate($date);

  } else {
    $d_in_m[2]=29  if (&Date_LeapYear($y));
    $d=$d_in_m[$m]  if ($d>$d_in_m[$m]);
    $d += $dd;
    while ($d<1) {
      $m--;
      if ($m==0) {
        $m=12;
        $y--;
        if (&Date_LeapYear($y)) {
          $d_in_m[2]=29;
        } else {
          $d_in_m[2]=28;
        }
      }
      $d += $d_in_m[$m];
    }
    while ($d>$d_in_m[$m]) {
      $d -= $d_in_m[$m];
      $m++;
      if ($m==13) {
        $m=1;
        $y++;
        if (&Date_LeapYear($y)) {
          $d_in_m[2]=29;
        } else {
          $d_in_m[2]=28;
        }
      }
    }
  }

  if ($y<1000 or $y>9999) {
    $$errref=3;
    return;
  }
  &FormDate($y,$m,$d,$h,$mn,$s);
}

sub Date_UpdateHolidays {
  my($date,$delta,$err)=();
  local($_)=();
  foreach (keys %Date::Manip::Holidays) {
    if (/^(.*)([+-].*)$/) {
      # Date +/- Delta
      ($date,$delta)=($1,$2);
      $Date::Manip::UpdateHolidays=1;
      $date=&ParseDate($date);
      $Date::Manip::UpdateHolidays=0;
      $date=&DateCalc($date,$delta,\$err,0);

    } else {
      # Date
      $Date::Manip::UpdateHolidays=1;
      $date=&ParseDate($_);
      $Date::Manip::UpdateHolidays=0;
    }
    $Date::Manip::CurrHolidays{$date}=1;
  }
}

# This sets a Date::Manip config variable.
sub Date_SetConfigVariable {
  my($var,$val)=@_;

  return  if ($var =~ /^PersonalCnf$/i);
  return  if ($var =~ /^PersonalCnfPath$/i);

  $Date::Manip::InitFilesRead=1,   return  if ($var =~ /^IgnoreGlobalCnf$/i);
  %Date::Manip::Holidays=(),       return  if ($var =~ /^EraseHolidays$/i);
  $Date::Manip::Init=0,
  $Date::Manip::Language=$val,     return  if ($var =~ /^Language$/i);
  $Date::Manip::DateFormat=$val,   return  if ($var =~ /^DateFormat$/i);
  $Date::Manip::TZ=$val,           return  if ($var =~ /^TZ$/i);
  $Date::Manip::ConvTZ=$val,       return  if ($var =~ /^ConvTZ$/i);
  $Date::Manip::Internal=$val,     return  if ($var =~ /^Internal$/i);
  $Date::Manip::FirstDay=$val,     return  if ($var =~ /^FirstDay$/i);
  $Date::Manip::WorkWeekBeg=$val,  return  if ($var =~ /^WorkWeekBeg$/i);
  $Date::Manip::WorkWeekEnd=$val,  return  if ($var =~ /^WorkWeekEnd$/i);
  $Date::Manip::WorkDayBeg=$val,
  $Date::Manip::ResetWorkDay=1,    return  if ($var =~ /^WorkDayBeg$/i);
  $Date::Manip::WorkDayEnd=$val,
  $Date::Manip::ResetWorkDay=1,    return  if ($var =~ /^WorkDayEnd$/i);
  $Date::Manip::WorkDay24Hr=$val,
  $Date::Manip::ResetWorkDay=1,    return  if ($var =~ /^WorkDay24Hr$/i);
  $Date::Manip::DeltaSigns=$val,   return  if ($var =~ /^DeltaSigns$/i);

  die "ERROR: Unknown configuration variable $var in Date::Manip.\n";
}

# This reads an init file.
sub ReadInitFile {
  my($file)=@_;
  local($_)=();
  my($section)="vars";
  my($var,$val,$date,$name)=();

  open(IN,$file);
  while($_=<IN>) {
    chomp;
    s/^\s+//;
    s/\s+$//;
    next  if (! $_  or  /^\#/);
    if (s/^\*\s*//) {
      $section=$_;
      next;
    }

    if ($section =~ /var/) {
      die "ERROR: invalid Date::Manip config file line.\n  $_\n"
        if (! /(.*\S)\s*=\s*(.*)$/);
      ($var,$val)=($1,$2);
      &Date_SetConfigVariable($var,$val);

    } elsif ($section =~ /holiday/i) {
      die "ERROR: invalid Date::Manip config file line.\n  $_\n"
        if (! /(.*\S)\s*=\s*(.*)$/);
      ($date,$name)=($1,$2);
      $name=""  if (! defined $name);
      $Date::Manip::Holidays{$date}=$name;

    } else {
      # A section not currently used by Date::Manip (but may be
      # used by some extension to it).
      next;
    }
  }
  close(IN);
}

# Get rid of a problem on old versions of perl
no strict "vars";
# This sorts from longest to shortest element
sub sortByLength {
  return (length $b <=> length $a);
}
use strict "vars";

# $flag=&Date_ErrorCheck(\$y,\$m,\$d,\$h,\$mn,\$s,\$ampm,\$wk);
#   Returns 1 if any of the fields are bad.  All fields are optional, and
#   all possible checks are done on the data.  If a field is not passed in,
#   it is set to default values.  If data is missing, appropriate defaults
#   are supplied.
#
#   If the flag Date::Manip::UpdateHolidays is set, the year is set to
#   Date::Manip::CurrHolidayYear.
sub Date_ErrorCheck {
  my($y,$m,$d,$h,$mn,$s,$ampm,$wk)=@_;
  my($tmp1,$tmp2,$tmp3)=();

  my(@d_in_m)=(0,31,28,31,30,31,30,31,31,30,31,30,31);
  my($curr_y)=$Date::Manip::CurrY;
  my($curr_m)=$Date::Manip::CurrM;
  my($curr_d)=$Date::Manip::CurrD;
  $$y=""     if (! defined $$y);
  $$m=""     if (! defined $$m);
  $$d=""     if (! defined $$d);
  $$h=""     if (! defined $$h);
  $$mn=""    if (! defined $$mn);
  $$s=""     if (! defined $$s);
  $$ampm=""  if (! defined $$ampm);
  $$ampm=uc($$ampm)  if ($$ampm);
  $$wk=""    if (! defined $$wk);
  $$d=$curr_d  if ($$y eq "" and $$m eq "" and $$d eq "");

  # Check year.
  $$y=$Date::Manip::CurrHolidayYear  if ($Date::Manip::UpdateHolidays);
  $$y=$curr_y    if ($$y eq "");
  if (length($$y)==2) {
    $tmp1=$curr_y-89;
    $$y="19$$y";
    while ($$y<$tmp1) {
      $$y+=100;
    }
  }
  return 1       if (! &IsInt($$y,1,9999));
  $d_in_m[2]=29  if (&Date_LeapYear($$y));

  # Check month
  $$m=$curr_m     if ($$m eq "");
  $$m=$Date::Manip::Month{lc($$m)}  if (exists $Date::Manip::Month{lc($$m)});
  $$m="0$$m"      if (length($$m)==1);
  return 1        if (! &IsInt($$m,1,12));

  # Check day
  $$d="01"        if ($$d eq "");
  $$d="0$$d"      if (length($$d)==1);
  return 1        if (! &IsInt($$d,1,$d_in_m[$$m]));
  if ($$wk) {
    $tmp1=&Date_DayOfWeek($$m,$$d,$$y);
    $tmp2=$Date::Manip::Week{lc($$wk)}
      if (exists $Date::Manip::Week{lc($$wk)});
    return 1      if ($tmp1 != $tmp2);
  }

  # Check hour
  $tmp1=$Date::Manip::AmPmExp;
  $tmp2="";
  if ($$ampm =~ /^$tmp1$/i) {
    $tmp3=$Date::Manip::AmExp;
    $tmp2="AM"  if ($$ampm =~ /^$tmp3$/i);
    $tmp3=$Date::Manip::PmExp;
    $tmp2="PM"  if ($$ampm =~ /^$tmp3$/i);
  } elsif ($$ampm) {
    return 1;
  }
  if ($tmp2 eq "AM" || $tmp2 eq "PM") {
    $$h="0$$h"    if (length($$h)==1);
    return 1      if ($$h<1 || $$h>12);
    $$h="00"      if ($tmp2 eq "AM"  and  $$h==12);
    $$h += 12     if ($tmp2 eq "PM"  and  $$h!=12);
  } else {
    $$h="00"      if ($$h eq "");
    $$h="0$$h"    if (length($$h)==1);
    return 1      if (! &IsInt($$h,0,23));
    $tmp2="AM"    if ($$h<12);
    $tmp2="PM"    if ($$h>=12);
  }
  $$ampm=$Date::Manip::Am;
  $$ampm=$Date::Manip::Pm  if ($tmp2 eq "PM");

  # Check minutes
  $$mn="00"       if ($$mn eq "");
  $$mn="0$$mn"    if (length($$mn)==1);
  return 1        if (! &IsInt($$mn,0,59));

  # Check seconds
  $$s="00"        if ($$s eq "");
  $$s="0$$s"      if (length($$s)==1);
  return 1        if (! &IsInt($$s,0,59));

  return 0;
}

########################################################################
# FROM MY PERSONAL LIBRARIES
########################################################################

# This takes 4 numbers ($N,$add,\$val,\$rem), adds $add to $val, and forces
# $val to be in a certain range.  This is useful for adding numbers for
# which only a certain range is allowed (for example, minutes can be
# between 0 and 59 or months can be between 1 and 12).  The absolute value
# of $N determines the range and the sign of $N determines whether the
# range is 0 to N-1 (if N>0) or 1 to N (N<0).  The remainder (as modulo N)
# is added to $rem.
sub ModuloAddition {
  my($N,$add,$val,$rem)=@_;
  return  if ($N==0);
  $$val+=$add;
  if ($N<0) {
    # 1 to N
    $N = -$N;
    if ($$val>$N) {
      $$rem+= int(($$val-1)/$N);
      $$val = ($$val-1)%$N +1;
    } elsif ($$val<1) {
      $$rem-= int(-$$val/$N)+1;
      $$val = $N-(-$$val % $N);
    }

  } else {
    # 0 to N-1
    if ($$val>($N-1)) {
      $$rem+= int($$val/$N);
      $$val = $$val%$N;
    } elsif ($$val<0) {
      $$rem-= int(-($$val+1)/$N)+1;
      $$val = ($N-1)-(-($$val+1)%$N);
    }
  }
}

# Returns 1 if $String is a valid integer, 0 otherwise.  If $low
# and $high are entered, the integer must be in that range.
sub IsInt {
  my($N,$low,$high)=@_;
  return 0 if ($N eq "");
  my($sign)='^\s* [-+]? \s*';
  my($int) ='\d+ \s* $ ';
  if ($N =~ /$sign $int/x) {
    if (defined $low  and  defined $high) {
      return 1  if ($N>=$low  and  $N<=$high);
      return 0;
    }
    return 1;
  }
  return 0;
}

# $Pos=&SinLindex(\@List,$Str [,$Offset [,$CaseInsensitive]]);
#    Searches for a list element exactly equal to $Str.
#
#    This returns the location of first element (starting at $Offset) in
#    @List containing $Str.  $Offset defaults to 0.  A negative offset
#    refers to the number of elements before the end of the list (-1 is the
#    last element).  The regular expression is case sensitive by default.
#
#    -1 is returned if it is not found and -2 is returned if an error is
#    encountered.
sub SinLindex {
  my($listref,$Str,$Offset,$Insensitive)=@_;
  my($i,$len,$tmp)=();
  $len=$#$listref;
  return -2  if ($len<0 or ! $Str);
  return -1  if (&Index_First(\$Offset,$len));
  $Str=uc($Str)  if ($Insensitive);
  for ($i=$Offset; $i<=$len; $i++) {
    $tmp=$$listref[$i];
    $tmp=uc($tmp)  if ($Insensitive);
    return $i  if ($tmp eq $Str);
  }
  return -1;
}
sub Index_First {
  my($Offsetref,$max)=@_;
  $$Offsetref=0  if (! $$Offsetref);
  if ($$Offsetref < 0) {
    $$Offsetref += $max + 1;
    $$Offsetref=0  if ($$Offsetref < 0);
  }
  return -1 if ($$Offsetref > $max);
  return 0;
}

# $File=&ExpandTilde($file);
#   This checks to see if a "~" appears as the first character in a path.
#   If it does, the "~" expansion is interpreted (if possible) and the full
#   path is returned.  If a "~" expansion is used but cannot be
#   interpreted, an empty string is returned.
sub ExpandTilde {
  my($file)=shift;
  my($user)=();
  my($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell)=();
  # ~USER/a =~      ~ USER      /a
  if ($file =~ m% ^~ ([^\/]*) (\/.*)? %x) {
    return ""  if ($^O =~ /MacOS/);
    ($user,$file)=($1,$2);
    $user=""  if (! defined $user);
    $file=""  if (! defined $file);
    if ($user) {
      ($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell)=
        getpwnam($user);
    } else {
      ($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell)=
        getpwuid($<);
    }
    if (! $dir) {
      return "";
    }
    # For root's sake...
    $file="$dir$file";
    $file=~ s#//#/#g;
  }
  return $file;
}

# $File=&SearchPath($file,$path [,$mode [,@suffixes]]);
#   Searches through directories in $path for a file named $file.  The
#   full path is returned if one is found, or an empty string otherwise.
#   The file may exist with one of the @suffixes.  The mode is checked
#   similar to &CheckFilePath.
#
#   The first full path that matches the name and mode is returned.  If none
#   is found, an empty string is returned.
sub SearchPath {
  my($file,$path,$mode,@suff)=@_;
  my($f,$s,$d,@dir,$fs)=();
  $path=&FixPath($path,1,"e");
  @dir=split(/:/,$path);
  foreach $d (@dir) {
    $f="$d/$file";
    $f=~ s|//|/|g;
    return $f if (&CheckFilePath($f,$mode));
    foreach $s (@suff) {
      $fs="$f.$s";
      return $fs if (&CheckFilePath($fs,$mode));
    }
  }
  return "";
}

# $Path=&FixPath($path [,$relative] [,$mode]);
#   Makes sure that every directory in $path appears as a full path or
#   relative path.  All "~" expansions are removed.  All trailing slashes
#   are removed also.  If $relative is non-nil, relative paths are expanded
#   to full paths as well.  If $mode is given, additional checking is done
#   to each directory and directories that do not pass the check are
#   removed from the path.  Valid modes are:
#      x   : directories do not have to exist (though "~" expansions that
#            cannot be exmpanded are removed) and no mode checking is done.
#      e   : each directory must exist or it is removed from the list
#      E   : each directory must exist or an error occurs
#      r   : each directory must exist with the r, and x permssion or it is
#            removed from the list
#      R   : each directory must exist with the r, and x permssion or an error
#            occurs
#      eR  : each directory that exists must have the r, and x permssion or
#            an error occurs (nonexistant directories are removed from the
#            list)
#      w   : each directory must exist with the r, w, and x permissions set
#            or an error occurs
#      W   : each directory must exist with the r, w, and x permissions set
#            or an error occurs
#      eW  : each directory that exists must have the r, w, and x permssion
#            or an error occurs (nonexistant directories are removed from the
#            list)
#   If no mode is given, it defaults to "x".
#
#   $path is a colon separated list of directories.
#
#   The empty string is returned if an error occurs.  The path is returned
#   otherwise.
sub FixPath {
  my($path,$rel,$mode)=@_;
  local($_)=();
  my(@dir)=split(/:/,$path);
  $mode="x" if (! $mode);
  if ($mode !~ /x|e|E|r|R|eR|w|W|eW/) {
    die "ERROR: unknonw mode $mode in FixPath.\n";
  }
  $path="";
  foreach (@dir) {

    # Expand path
    if ($rel) {
      $_=&FullFilePath($_);
    } else {
      $_=&ExpandTilde($_);
    }

    # x mode
    if ($mode eq "x") {
      $path .= ":$_" if ($_);
      next;
    }

    # If directory doesn't exist, either remove it or return an error
    if (! &CheckFilePath($_,"d")) {
      next if ($mode=~/e/ || $mode eq "r" || $mode eq "w");
      return "";
    }

    # Check mode
    if ($mode =~ /^e$/i or
        $mode =~ /r/i && &CheckFilePath($_,"rx") or
        $mode =~ /w/i && &CheckFilePath($_,"rwx")) {
      $path .= ":$_";
    } else {
      if ($mode =~ /RW/) {
        return "";
      } else {
        next;
      }
    }
  }
  $path =~ s/^://;
  return $path;
}

# $Flag=&CheckFilePath($file [,$mode]);
#   Checks to see if $file exists, to see what type it is, and whether
#   the script can access it.
#
#   If $mode contains any of the characters "fdlSpbc", it checks to
#   see if it is type file, directory, soft link, socket, named pipe,
#   block special, or character special.  If none are present, the file
#   type is ignored.
#
#   Soft links to a certain type of file will pass the same tests that
#   that tests on that type of file would pass (in addition to passing
#   the test that they are a link).
#
#   If $mode contains any of the characters "rwxugk", it tests to to see
#   if the file is readable, writable, or executable by the script or has
#   the setuid, setgid, or sticky bit set.
#
#   If $mode contains any of the characters "zs", it checks to see if the
#   file is of zero/non-zero length.
#
#   An empty string is returned if the file doesn't exist, is the wrong
#   type (a directory when a file was expected or vice versa), or has the
#   wrong size.  0 is returned if the file exists but does not have the
#   mode given.
sub CheckFilePath {
  my($file,$mode)=@_;
  $file=&FullFilePath($file);

  # File doesn't exist
  return "" if (! $file or
                ! -e $file );

  # File the wrong type
  return "" if ($mode =~ /f/ && ! -f $file or
                $mode =~ /d/ && ! -d $file or
                $mode =~ /l/ && ! -l $file or
                $mode =~ /S/ && ! -S $file or
                $mode =~ /p/ && ! -p $file or
                $mode =~ /b/ && ! -b $file or
                $mode =~ /c/ && ! -c $file);
  $mode =~ s/[fdlSpbc]//g;

  # File the wrong size
  return "" if ($mode =~ /z/ && ! -z $file or
                $mode =~ /s/ && ! -s $file);
  $mode =~ s/[zs]//g;

  # File the wrong mode
  return 0 if ($mode =~ /r/ && ! -r $file or
               $mode =~ /w/ && ! -w $file or
               $mode =~ /x/ && ! -x $file or
               $mode =~ /u/ && ! -u $file or
               $mode =~ /g/ && ! -g $file or
               $mode =~ /k/ && ! -k $file);
  $mode =~ s/[rwxugk]//g;

  if ($mode) {
    die "ERROR: unknown mode $mode in CheckFilePath.\n";
  }
  return 1;
}

# $File=&FullFilePath($file);
#   Returns the full path to $file.  Returns an empty string if a "~"
#   expansion cannot be interpreted.  The path does not need to exist.
sub FullFilePath {
  my($file)=shift;
  my($curr)=();
  $file=&ExpandTilde($file);
  if (! $file) {
    return "";
  }
  if ($file !~ m|^/|) {         # $file = "a/b/c"
    $curr=&Pwd;
    $file="$curr/$file";
    $file =~ s|//|/|g;
  }
  return $file;
}

# $Dire=&Pwd;
#   Returns the current directory.  Taken from an early news post of
#   Randal Schwartz.
sub Pwd {
  my($pwd)=();
  chomp($pwd = `pwd`);
  return $pwd;
}

1;

########################################################################
########################################################################
# POD
########################################################################
########################################################################

=head1 NAME

Date::Manip - date manipulation routines

=head1 SYNOPSIS

 use Date::Manip;

 $date=&ParseDate(\@args)
 $date=&ParseDate($string)
 $date=&ParseDate(\$string)

 @date=&UnixDate($date,@format)
 $date=&UnixDate($date,@format)

 $delta=&ParseDateDelta(\@args)
 $delta=&ParseDateDelta($string)
 $delta=&ParseDateDelta(\$string)

 $d=&DateCalc($d1,$d2,$errref,$del)

 $date=&Date_SetTime($date,$hr,$min,$sec)
 $date=&Date_SetTime($date,$time)

 $date=&Date_GetPrev($date,$dow,$today,$hr,$min,$sec)
 $date=&Date_GetPrev($date,$dow,$today,$time)

 $date=&Date_GetNext($date,$dow,$today,$hr,$min,$sec)
 $date=&Date_GetNext($date,$dow,$today,$time)

 &Date_Init()
 &Date_Init("VAR=VAL",...)

 $version=&DateManipVersion

 $flag=&Date_IsWorkDay($date [,$time]);

 $date=&Date_NextWorkDay($date,$off [,$time]);
 $date=&Date_PrevWorkDay($date,$off [,$time]);

The following routines are used by the above routines (though they can
also be called directly).  Make sure that $y is entered as the full 4
digit year (it will die if a 2 digit years is entered).  Most (if not
all) of the information below can be gotten from UnixDate which is really
the way I intended it to be gotten.

 $day=&Date_DayOfWeek($m,$d,$y)
 $secs=&Date_SecsSince1970($m,$d,$y,$h,$mn,$s)
 $secs=&Date_SecsSince1970GMT($m,$d,$y,$h,$mn,$s)
 $days=&Date_DaysSince999($m,$d,$y)
 $day=&Date_DayOfYear($m,$d,$y)
 $days=&Date_DaysInYear($y)
 $wkno=&Date_WeekOfYear($m,$d,$y,$first)
 $flag=&Date_LeapYear($y)
 $day=&Date_DaySuffix($d)
 $tz=&Date_TimeZone()

=head1 DESCRIPTION

This is a set of routines designed to make any common date/time
manipulation easy to do.  Operations such as comparing two times,
calculating a time a given amount of time from another, or parsing
international times are all easily done.

Date::Manip deals only with the Gregorian calendar (the one currently in
use).  The Julian calendar defined leap years as every 4th year.  The
Gregorian calendar improved this by making every 100th year NOT a leap
year, unless it was also the 400th year.  The Gregorian calendar has been
extrapolated back to the year 1000 AD and forward to the year 9999 AD.
Note that in historical context, the Julian calendar was in use until 1582
when the Gregorian calendar was adopted by the Catholic church.  Protestant
countries did not accept it until later; Germany and Netherlands in 1698,
British Empire in 1752, Russia in 1918.  Note that the Gregorian calendar
is itself imperfect.  Each year is on average 26 seconds too long, which
means that every 3,323 years, a day should be removed from the calendar.
No attempt is made to correct for that.

Date::Manip is therefore not equipped to truly deal with historacle dates,
but should be able to perform (virtually) any operation dealing with a
modern time and date.

Among other things, Date::Manip allow you to:

1.  Enter a date and be able to choose any format conveniant

2.  Compare two dates, entered in widely different formats to determine
    which is earlier

3.  Extract any information you want from ANY date using a format string
    similar to the Unix date command

4.  Determine the amount of time between two dates

5.  Add a time offset to a date to get a second date (i.e. determine the
    date 132 days ago or 2 years and 3 months after Jan 2, 1992)

6.  Work with dates with dates using international formats (foreign month
    names, 12-10-95 referring to October rather than December, etc.).

Each of these tasks is trivial (one or two lines at most) with this package.

Although the word date is used extensively here, it is actually somewhat
misleading.  Date::Manip works with the full date AND time (year, month,
day, hour, minute, second).

In the documentation below, US formats are used, but in most (if not all)
cases, a non-English equivalent will work equally well.

=head1 EXAMPLES

1.  Parsing a date from any conveniant format

  $date=&ParseDate("today");
  $date=&ParseDate("1st thursday in June 1992");
  $date=&ParseDate("05-10-93");
  $date=&ParseDate("12:30 Dec 12th 1880");
  $date=&ParseDate("8:00pm december tenth");
  if (! $date) {
    # Error in the date
  }

2.  Compare two dates

  $date1=&ParseDate($string1);
  $date2=&ParseDate($string2);
  if ($date1 lt $date2) {
    # date1 is earlier
  } else {
    # date2 is earlier (or the two dates are identical)
  }

3.  Extract information from a date.

  print &UnixDate("today","The time is now %T on %b %e, %Y.");
  =>  "The time is now 13:24:08 on Feb  3, 1996."

4.  The amount of time between two dates.

  $date1=&ParseDate($string1);
  $date2=&ParseDate($string2);
  $delta=&DateCalc($date1,$date2,\$err);
  => 0:0:DD:HH:MM:SS   the days, hours, minutes, and seconds between the two
  $delta=&DateCalc($date1,$date2,\$err,1);
  => YY:MM:DD:HH:MM:SS  the years, months, etc. between the two

  Read the documentation below for an explanation of the difference.

5.  To determine a date a given offset from another.

  $date=&DateCalc("today","+ 3hours 12minutes 6 seconds",\$err);
  $date=&DateCalc("12 hours ago","12:30 6Jan90",\$err);

  It even works with business days:

  $date=&DateCalc("today","+ 3 business days",\$err);

6.  To work with dates in another language.

  &Date_Init("Language=French","DateFormat=non-US");
  $date=&ParseDate("1er decembre 1990");

NOTE: Some date forms do not work as well in languages other than English,
but this is not because DateManip is incapable of doing so (almost nothing
in this module is language dependent).  It is simply that I do not have the
correct translation available for some words.  If there is a date form that
works in English but does not work in a language you need, let me know and
if you can provide me the translation, I will fix DateManip.

=head1 ROUTINES

=over 4
=item ParseDate

 $date=&ParseDate(\@args)
 $date=&ParseDate($string)
 $date=&ParseDate(\$string)

This takes an array or a string containing a date and parses it.  When the
date is included as an array (for example, the arguments to a program) the
array should contain a valid date in the first one or more elements
(elements after a valid date are ignored).  Elements containing a valid
date are shifted from the array.  The largest possible number of elements
which can be correctly interpreted as a valid date are always used.  If a
string is entered rather than an array, that string is tested for a valid
date.  The string is unmodified, even if passed in by reference.

A date actually includes 2 parts: date and time.  A time must include
hours and minutes and can optionally include seconds, fractional seconds,
an am/pm type string, and a timezone.  For example:

     HH:MN  [Zone]
     HH:MN:SS  [Zone]
     HH:MN am  [Zone]
     HH:MN:SS am  [Zone]
     HH:MN:SS:SSSS  [Zone]
     HH:MN:SS.SSSS am [Zone]

Hours can be written using 1 or 2 digits when the time follows the date and
is separated from the date with spaces or some other separator.  Any time a
digit (that is part of the date) immediately precedes the hour, 2 digits
MUST be included for the hours.

Fractional seconds are also supported in parsing but the fractional part is
discarded.

Timezones always appear after the time and must be separated from all other
parts of the time/date by spaces.  For now, only rudimentary timezone
handling is done.  At the time the date is parsed, it is converted to a
specific time zone (which defaults to whatever time zone you are in, but
this can be overridden using the Date_Init routine described below).  After
that, the time zone is never used.  Once converted, information about the
time zone is no longer stored or used.

See the section below on TIMEZONEs for a list of all defined timezone
names.

Spaces in the date are almost always optional when there is absolutely
no ambiguity if they are not present.  Years can be entered as 2 or 4 digits,
days and months as 1 or 2 digits.  Both days and months must include 2 digits
whenver they are immediately adjacent to another part of the date or time
Valid formats for a full date and time (and examples of how Dec 10, 1965 at
9:00 pm might appear) are:
  DateTime
     Date=YYMMDD             1965121021:00:00
                             65121021:00

  Date Time
  Date%Time
    Date=mm%dd, mm%dd%YY     12/10/65 21:00
                             12 10 1965 9:00pm
    Date=mmm%dd, mmm%dd%YY   December-10-65-9:00:00pm
    Date=dd%mmm, dd%mmm%YY   10/December/65 9:00:00pm

  Date Time
    Date=mmmdd, mmmdd YY,    Dec10 65 9:00:00 pm
         mmmDDYY, mmm DDYY   December 10 1965 9:00pm

    Date=ddmmm, ddmmm YY, ddmmmYY, dd mmmYY
                             10Dec65 9:00:00 pm     10 December 1965 9:00pm

  TimeDate
  Time Date
  Time%Date
    Date=mm%dd, mm%dd%YY     9:00pm 12.10.65      21:00 12/10/1965
    Date=mmm%dd, mmm%dd%YY   9:00pm December/10/65
    Date=dd%mmm, dd%mmm%YY   9:00pm 10-December-65  21:00/10/Dec/65

  TimeDate
  Time Date
    Date=mmmdd, mmmdd YY, mmmDDYY
                             21:00:00DeCeMbEr10
    Date=ddmmm, ddmmm YY, ddmmmYY, dd mmmYY
                             21:00 10Dec95

Miscellaneous other allowed formats are:
  which dofw in mmm [at time]
  which dofw in mmm YY [at time]    "first sunday in june 1996 at 14:00"

  dofw week num [in YY] [at time]   "sunday week 22 in 1995"
  which dofw [in YY] [at time]      "22nd sunday in 1996 at noon"
  dofw which week [in YY] [at time] "sunday 22nd week in 1996"
  next/last dofw [at time]          "next friday at noon"
  in num weeks [at time]            "in 3 weeks at 12:00"
  num weeks ago [at time]           "3 weeks ago"
  dofw in num week [at time]        "Friday in 2 weeks"
  in num weeks on dofw [at time]    "in 2 weeks on friday"
  dofw num week ago [at time]       "Friday 2 weeks ago"
  num week ago dofw [at time]       "2 weeks ago friday"

In addition, the following strings are recognized:
  today
  now       (synonym for today)
  yesterday (exactly 24 hours before now)
  tomorrow  (exactly 24 hours from now)
  noon      (12:00:00)
  midnignt  (00:00:00)

 %       One of the valid date separators: - . / or whitespace (the same
         character must be used for all occurences of a single date)
         example: mm%dd%YY works for 1-1-95, 1 1 95, or 1/1/95
 YY      year in 2 or 4 digit format
 MM      two digit month (01 to 12)
 mm      one or two digit month (1 to 12 or 01 to 12)
 mmm     month name or 3 character abbreviation
 DD      two digit day (01 to 31)
 dd      one or two digit day (1 to 31 or 01 to 31)
 HH      one or two digit hour in 12 or 24 hour mode (0 to 23 or 00 to 23)
 MN      two digit minutes (00 to 59)
 SS      two digit seconds (00 to 59)
 which   one of the strings (first-fifth, 1st-5th, or last)
 dofw    either the 3 character abbreviation or full name of a day of
         the week

Some things to note:

All strings are case insensitive.  "December" and "DEceMBer" both work.

When a part of the date is not given, defaults are used: year defaults
to current year; hours, minutes, seconds to 00.

In the above, the mm%dd formats can be switched to dd%mm by calling
Date_Init and telling it to use a non-US date format.

All "Date Time" and "DateTime" type formats allow the word "at" in them
(i.e.  Jan 12 at 12:00) (and at can replace the space).  So the following
are both acceptable: "Jan 12at12:00" and "Jan 12 at 12:00".

A time is usually entered in 24 hour mode.  It can be followed by "am" or
"pm" to force it to be read in in 12 hour mode.

The year may be entered as 2 or 4 digits.  If entered as 2 digits, it is
taken to be the year in the range CurrYear-89 to CurrYear+10.  So, if the
current year is 1996, the range is [1907 to 2006] so entering the year 00
refers to 2000, 05 to 2005, but 07 refers to 1907.

Any number of spaces or tabs can be used anyhere whitespace is appropriate.

Dates are always checked to make sure they are valid.

In all of the formats, the day of week ("Friday") can be entered anywhere
in the date and it will be checked for accuracy.  In other words,
  "Tue Jul 16 1996 13:17:00"
will work but
  "Jul 16 1996 Wednesday 13:17:00"
will not (because Jul 16, 1996 is Tuesday, not Wednesday).  Note that
depending on where the weekday comes, it may give unexpected results when
used in array context.  For example, the date ("Jun","25","Sun","1990")
would return June 25 of the current year since Jun 25, 1990 is not Sunday.

The times "12:00 am", "12:00 pm", and "midnight" are not well defined.  For
good or bad, I use the following convention in Date::Manip:
  midnight = 12:00am = 00:00:00
  noon     = 12:00pm = 12:00:00
and the day goes from 00:00:00 to 23:59:59.  In otherwords, midnight is the
beginning of a day rather than the end of one.  At midnight on July 5, July
5 has just begun.  The time 24:00:00 is NOT allowed.

The format of the date returned is YYYYMMDDHH:MM:SS.  The advantage of this
time format is that two times can be compared using simple string comparisons
to find out which is later.  Also, it is readily understood by a human.
Alternate forms can be used if that is more conveniant.  See Date_Init below
and the config variable Internal.

=item UnixDate

 @date=&UnixDate($date,@format)
 $date=&UnixDate($date,@format)

This takes a date and a list of strings containing formats roughly
identical to the format strings used by the UNIX date(1) command.  Each
format is parsed and an array of strings corresponding to each format is
returned.

$date must be of the form produced by &ParseDate.

The format options are:

 Year
     %y     year                     - 00 to 99
     %Y     year                     - 0001 to 9999
 Month, Week
     %m     month of year            - 01 to 12
     %f     month of year            - " 1" to "12"
     %b,%h  month abbreviation       - Jan to Dec
     %B     month name               - January to December
     %U     week of year, Sunday
            as first day of week     - 00 to 53
     %W     week of year, Monday
            as first day of week     - 00 to 53
 Day
     %j     day of the year          - 001 to 366
     %d     day of month             - 01 to 31

     %e     day of month             - " 1" to "31"
     %v     weekday abbreviation     - " S"," M"," T"," W","Th"," F","Sa"
     %a     weekday abbreviation     - Sun to Sat
     %A     weekday name             - Sunday to Saturday
     %w     day of week              - 0 (Sunday) to 6
     %E     day of month with suffix - 1st, 2nd, 3rd...
 Hour
     %H     hour                     - 00 to 23
     %k     hour                     - " 0" to "23"
     %i     hour                     - " 1" to "12"
     %I     hour                     - 01 to 12
     %p     AM or PM
 Minute, Second, Timezone
     %M     minute                   - 00 to 59
     %S     second                   - 00 to 59
     %s     seconds from Jan 1, 1970 GMT
                                     - negative if before 1/1/1970
     %o     seconds from Jan 1, 1970 in the current time zone
     %z,%Z  timezone (3 characters)  - "EDT"
 Date, Time
     %c     %a %b %e %H:%M:%S %Y     - Fri Apr 28 17:23:15 1995
     %C,%u  %a %b %e %H:%M:%S %z %Y  - Fri Apr 28 17:25:57 EDT 1995
     %g     %a, %d %b %Y %H:%M:%S %z - Fri, 28 Apr 1995 17:23:15 EDT
     %D,%x  %m/%d/%y                 - 04/28/95
     %l     date in ls(1) format
              %b %e $H:$M            - Apr 28 17:23  (if within 6 months)
              %b %e  %Y              - Apr 28  1993  (otherwise)
     %r     %I:%M:%S %p              - 05:39:55 PM
     %R     %H:%M                    - 17:40
     %T,%X  %H:%M:%S                 - 17:40:58
     %V     %m%d%H%M%y               - 0428174095
     %Q     %Y%m%d                   - 19961025
     %q     %Y%m%d%H%M%S             - 19961025174058
     %P     %Y%m%d%H%M%S             - 1996102517:40:58
     %F     %A, %B %e, %Y            - Sunday, January  1, 1996
 Other formats
     %n     insert a newline character
     %t     insert a tab character
     %%     insert a `%' character
     %+     insert a `+' character
 The following formats are currently unused but may be used in the future:
     GJKLNO 1234567890 !@#$^&*()_|-=\`[];',./~{}:<>?
 They currently insert the character following the %, but may (and probably
 will) change in the future as new formats are requested.

If a lone percent is the final character in a format, it is ignored.

Note that the ls format applies to date within the past OR future 6 months!

Note that the %s format was introduced in version 5.07.  Prior to that,
%s referred to the seconds since 1/1/70.  This was moved to %o in 5.07.

This routine is loosely based on date.pl (version 3.2) by Terry McGonigal.
No code was used, but most of his formats were.

=item ParseDateDelta

 $delta=&ParseDateDelta(\@args)
 $delta=&ParseDateDelta($string)
 $delta=&ParseDateDelta(\$string)

This takes an array and shifts a valid delta date (an amount of time)
from the array.  Recognized deltas are of the form:
  +Yy +Mm +Ww +Dd +Hh +MNmn +Ss
      examples:
         +4 hours +3mn -2second
         + 4 hr 3 minutes -2
         4 hour + 3 min -2 s
  +Y:+M:+D:+H:+MN:+S
      examples:
         0:0:0:4:3:-2
         +4:3:-2
  mixed format
      examples:
         4 hour 3:-2

A field in the format +Yy is a sign, a number, and a string specifying
the type of field.  The sign is "+", "-", or absent (defaults to the
next larger element).  The valid strings specifying the field type
are:
   y:  y, yr, year, years
   m:  m, mon, month, months
   w:  w, wk, ws, wks, week, weeks
   d:  d, day, days
   h:  h, hr, hour, hours
   mn: mn, min, minute, minutes
   s:  s, sec, second, seconds

Also, the "s" string may be omitted.  The sign, number, and string may
all be separated from each other by any number of whitespaces.

In the date, all fields must be given in the order: y m d h mn s.  Any
number of them may be omitted provided the rest remain in the correct
order.  In the 2nd (colon) format, from 2 to 6 of the fields may be given.
For example +D:+H:+MN:+S may be given to specify only four of the fields.
In any case, both the MN and S field may be present.  No spaces may be
present in the colon format.

Deltas may also be given as a combination of the two formats.  For example,
the following is valid: +Yy +D:+H:+MN:+S.  Again, all fields must be given
in the correct order.

The word "in" may be prepended to the delta ("in 5 years") and the word
"ago" may be appended ("6 months ago").  The "in" is completely ignored.
The "ago" has the affect of reversing all signs that appear in front of the
components of the delta.  I.e. "-12 yr 6 mon ago" is identical to "+12yr
+6mon" (don't forget that there is an impled minus sign in front of the 6
because when no sign is explicitely given, it carries the previously
entered sign).

The "week" field does not occur in the colon separated delta.  The reason
for this is to maintain backward compatibility with previous versions of
Date::Manip.  Parsing of weeks was only added in version 5.07.  At this
point, rather than change the internal format of the delta to
"Y:M:W:D:H:MN:S", I simply added the weeks to the days (1 week = 7 days) in
order to be compatible with previous versions.  So, they are not parsed in
the colon format, only in the first format.  Hopefully, this will not
result in too much confusion.

One thing is worth noting.  The year/month and day/hour/min/sec parts are
returned in a "normalized" form.  That is, the signs are adjusted so as to
be all positive or all negative.  For example, "+ 2 day - 2hour" does not
return "0:0:2:-2:0:0".  It returns "+0:0:1:22:0:0" (1 day 22 hours which is
equivalent).  I find (and I think most others agree) that this is a more
useful form.

Since the year/month and day/hour/min/sec parts must be normalized
separately there is the possibility that the sign of the two parts will be
different.  So, the delta "+ 2years -10 months - 2 days + 2 hours" produces
the delta "+1:2:-1:22:0:0".

For backwards compatibility, it is possible to include a sign for all
elements that is output.  See the configuration variable DeltaSigns below.

=item DateCalc

 $d=&DateCalc($d1,$d2,\$err [,$mode])

This takes two dates, deltas, or one of each and performs the appropriate
calculation with them.  Dates must be in the format given by &ParseDate and
or must be a string which can be parsed as a date.  Deltas must be in the
format returned by &ParseDateDelta or must be a string that can be parsed
as a delta.  Two deltas add together to form a third delta.  A date and a
delta returns a 2nd date.  Two dates return a delta (the difference between
the two dates).

Note that in many cases, it is somewhat ambiguous what the delta actually
refers to.  Although it is ALWAYS known how many months in a year, hours in
a day, etc., it is NOT known how many days form a month.  As a result, the
part of the delta containing month/year and the part with sec/min/hr/day
must be treated separately.  For example, "Mar 31, 12:00:00" plus a delta
of 1month 2days would yield "May 2 12:00:00".  The year/month is first
handled while keeping the same date.  Mar 31 plus one month is Apr 31 (but
since Apr only has 30 days, it becomes Apr 30).  Apr 30 + 2 days is May 2.
As a result, in the case where two dates are entered, the resulting delta
can take on two different forms.  By default ($mode=0), an absolutely
correct delta (ignoring daylight savings time) is returned in days, hours,
minutes, and seconds.

If $mode is 1, the math is done using an approximate mode where a delta is
returned using years and months as well.  The year and month part is
calculated first followed by the rest.  For example, the two dates "Mar 12
1995" and "Apr 13 1995" would have an exact delta of "31 days" but in the
approximate mode, it would be returned as "1 month 1 day".  Also, "Mar 31"
and "Apr 30" would have deltas of "30 days" or "1 month" (since Apr 31
doesn't exist, it drops down to Apr 30).  Approximate mode is a more human
way of looking at things (you'd say 1 month and 2 days more often then 33
days), but it is less meaningful in terms of absolute time.  In approximate
mode $d1 and $d2 must be dates.  If either or both is a delta, the
calculation is done in exact mode.

If $mode is 2, a business mode is used.  That is, the calculation is done
using business days, ignoring holidays, weekends, etc.  In order to
correctly use this mode, a config file must exist which contains the
section defining holidays (see documentation on the config file below).
The config file can also define the work week and the hours of the work
day, so it is possible to have different config files for different
businesses.

For example, if a config file defines the workday as 08:00 to 18:00, a
workweek consisting of Mon-Sat, and the standard (American) holidays, then
from Tuesday at 12:00 to the following Monday at 14:00 is 5 days and 2
hours.  If the "end" of the day is reached in a calculation, it
autmoatically switches to the next day.  So, Tuesday at 12:00 plus 6 hours
is Wednesday at 08:00 (provided Wed is not a holiday).  Also, a date that
is not during a workday automatically becomes the start of the next
workday.  So, Sunday 12:00 and Monday at 03:00 both automatically becomes
Monday at 08:00 (provided Monday is not a holiday).  In business mode, any
combination of date and delta may be entered, but a delta should not
contain a year or month field (weeks are fine though).

See below for some additional comments about business mode calculations.

Any other non-nil value of $mode is treated as $mode=1 (approximate mode).

The mode can be automatically set in the dates/deltas passed by including a
key word somewhere in it.  For example, in English, if the word
"approximately" is found in either of the date/delta arguments, approximate
mode is forced.  Likewise, if the word "business" or "exactly" appears,
business/exact mode is forced (and $mode is ignored).  So, the two
following are equivalent:

   $date=&DateCalc("today","+ 2 business days",\$err);
   $date=&DateCalc("today","+ 2 days",\$err,2);

Note that if the keyword method is used instead of passing in $mode, it is
important that the keyword actually appear in the argument passed in to
DateCalc.  The following will NOT work:

   $delta=&ParseDateDelta("+ 2 business days");
   $today=&ParseDate("today");
   $date=&DateCalc($today,$delta,\$err);

because the mode keyword is removed from a date/delta by the parse routines,
and the mode is reset each time a parse routine is called.  Since DateCalc
parses both of its arguments, whatever mode was previously set is ignored.

$err is set to:
   1 is returned if $d1 is not a delta or date
   2 is returned if $d2 is not a delta or date
   3 is returned if the date is outside the years 1000 to 9999

Nothing is returned if an error occurs.

When a delta is returned, the signs such that it is strictly positive or
strictly negative ("1 day - 2 hours" would never be returned for example).
The only time when this cannot be enforced is when two deltas with a
year/month component are entered.  In this case, only the signs on the
day/hour/min/sec part are standardized.

=item Date_SetTime

 $date=&Date_SetTime($date,$hr,$min,$sec)
 $date=&Date_SetTime($date,$time)

This takes a date sets the time in that date.  For example, to get
the time for 7:30 tomorrow, use the lines:

   $date=&ParseDate("tomorrow")
   $date=&Date_SetTime($date,"7:30")

=item Date_GetPrev

 $date=&Date_GetPrev($date,$dow, $curr [,$hr,$min,$sec])
 $date=&Date_GetPrev($date,$dow, $curr [,$time])
 $date=&Date_GetPrev($date,undef,$curr,$hr,$min,$sec)
 $date=&Date_GetPrev($date,undef,$curr,$time)

If $dow is defined, it is a day of week (a string such as "Fri" or a number
from 0 to 6).  The date of the previous $dow is returned.  If $date falls
on this day of week, the date returned will be $date (if $curr is non-zero)
or a week earlier (if $curr is 0).  If a time is passed in (either as
separate hours, minutes, seconds or as a time in HH:MM:SS or HH:MM format),
the time on this date is set to it.  The following examples should
illustrate the use of Date_GetPrev:

    date                   dow    curr  time            returns
    Fri Nov 22 18:15:00    Thu    0     12:30           Thu Nov 21 12:30:00
    Fri Nov 22 18:15:00    Fri    0     12:30           Fri Nov 15 12:30:00
    Fri Nov 22 18:15:00    Fri    1     12:30           Fri Nov 22 12:30:00

If $dow is undefined, then a time must be entered, and the date returned is
the previous occurence of this time.  If $curr is non-zero, the current
time is returned if it matches the criteria passed in.  In other words, the
time returned is the last time that a digital clock (in 24 hour mode) would
have displayed the time you pass in.  If you define hours, minutes and
seconds default to 0 and you might jump back as much as an entire day.  If
hours are undefined, you are looking for the last time the minutes/seconds
appeared on the digital clock, so at most, the time will jump back one hour.

    date               curr  hr     min    sec      returns
    Nov 22 18:15:00    0/1   18     undef  undef    Nov 22 18:00:00
    Nov 22 18:15:00    0/1   18     30     0        Nov 21 18:30:00
    Nov 22 18:15:00    0     18     15     undef    Nov 21 18:15:00
    Nov 22 18:15:00    1     18     15     undef    Nov 22 18:15:00
    Nov 22 18:15:00    0     undef  15     undef    Nov 22 17:15:00
    Nov 22 18:15:00    1     undef  15     undef    Nov 22 18:15:00


=item Date_GetNext

 $date=&Date_GetNext($date,$dow, $curr [,$hr,$min,$sec])
 $date=&Date_GetNext($date,$dow, $curr [,$time])
 $date=&Date_GetNext($date,undef,$curr,$hr,$min,$sec)
 $date=&Date_GetNext($date,undef,$curr,$time)

Similar to Date_GetPrev.

=item Date_DayOfWeek

 $day=&Date_DayOfWeek($m,$d,$y);

Returns the day of the week (0 for Sunday, 6 for Saturday).  Dec 31, 0999
was Tuesday.

=item Date_SecsSince1970

 $secs=&Date_SecsSince1970($m,$d,$y,$h,$mn,$s)

Returns the number of seconds since Jan 1, 1970 00:00 (negative if date is
earlier).

=item Date_SecsSince1970GMT

 $secs=&Date_SecsSince1970GMT($m,$d,$y,$h,$mn,$s)

Returns the number of seconds since Jan 1, 1970 00:00 GMT (negative if date
is earlier).  If CurrTZ is "IGNORE", the number will be identical to
Date_SecsSince1970 (i.e. the date given will be treated as being in GMT).

=item Date_DaysSince999

 $days=&Date_DaysSince999($m,$d,$y)

Returns the number of days since Dec 31, 0999.

=item Date_DayOfYear

 $day=&Date_DayOfYear($m,$d,$y);

Returns the day of the year (001 to 366)

=item Date_DaysInYear

 $days=&Date_DaysInYear($y);

Returns the number of days in the year (365 or 366)

=item Date_WeekOfYear

 $wkno=&Date_WeekOfYear($m,$d,$y,$first);

Figure out week number.  $first is the first day of the week which is
usually 0 (Sunday) or 1 (Monday), but could be any number between 0 and 6
in practice.

=item Date_LeapYear

 $flag=&Date_LeapYear($y);

Returns 1 if the argument is a leap year
Written by David Muir Sharnoff <muir@idiom.com>

=item Date_DaySuffix

 $day=&Date_DaySuffix($d);

Add `st', `nd', `rd', `th' to a date (ie 1st, 22nd, 29th).  Works for
international dates.

=item Date_TimeZone

 $tz=&Date_TimeZone

This returns a timezone.  It looks in the following places for a timezone
in the following order:

   $ENV{TZ}
   $main::TZ
   /etc/TIMEZONE
   date '+%Z'

If it's not found in any of those places, an error occurs:

   ERROR: Date::Manip unable to determine TimeZone.

Date_TimeZone is able to read zones of the format PST8PDT (see TIMEZONES
documentation below).

=item Date_ConvTZ

 $date=&Date_ConvTZ($date,$from)
 $date=&Date_ConvTZ($date,$from,$to)

This converts a date (which MUST be in the format returned by ParseDate)
from one timezone to another.  The behavior of Date_ConvTZ depends on
whether it is called with 2 or 3 arguments.

If it is called with 2 arguments, $date is assumed to be in timezone given
in $from and it is converted to the timzone specified by the config
variable ConvTZ.  If ConvTZ is set to "IGNORE", no conversion is done and
$date is returned unmodified (see documentation on ConvTZ below).  This
form is most often used internally by the Date::Manip module.  The 3
argument form is of more use to most users.

If Date_ConvTZ is called with 3 arguments, the config variable ConvTZ is
ignored and $date is given in the timezone $from and is converted to the
timzone $to.  If $from is not given, it defaults to the working timezone.
NOTE: As in all other cases, the $date returned from Date_ConvTZ has no
timezone information included as part of it, so calling UnixDate with the
"%z" format will return the timezone that Date::Manip is working in
(usually the local timezone).

Example:  To convert 2/2/96 noon PST to CST (regardless of what timezone
you are in, do the following:

 $date=&ParseDate("2/2/96 noon");
 $date=&Date_ConvTZ($date,"PST","CST");

Both timezones MUST be in one of the formst listed below in the section
TIMEZONES.

=item Date_Init

 $flag=&Date_Init();
 $flag=&Date_Init("VAR=VAL","VAR=VAL",...);

Normally, it is not necessary to explicitely call Date_Init.  The first
time any of the other routines are called, Date_Init will be called to set
everything up.  If for some reason you want to change the configuration of
Date::Manip, you can pass the appropriate string or strings into Date_Init
to reinitizize things.

The strings to pass in are of the form "VAR=VAL".  Any number may be
included and they can come in any order.  VAR may be any configuration
variable.  A list of all configuaration variables is given in the section
CUSTOMIZING DATE::MANIP below.  VAL is any allowed value for that variable.
For example, to switch from English to French and use non-US format (so
that 12/10 is Oct 12), do the following:

  &Date_Init("Language=French","DateFormat=nonUS");

Note that the usage of Date_Init changed with version 5.07.  The old
calling convention is allowed but is depreciated.

If you change timezones in the middle of using Date::Manip, comparing dates
from before the switch to dates from after the switch will produce incorrect
results.

=item Date_IsWorkDay

  $flag=&Date_IsWorkDay($date [,$time]);

This returns 1 if $date is a work day.  If $time is non-zero, the time is
checked to see if it falls within work hours.

=item Date_NextWorkDay

  $date=&Date_NextWorkDay($date,$off [,$time]);

Finds the day $off work days from now.  If $time is passed in, we must also
take into account the time of day.

If $time is not passed in, day 0 is today (if today is a workday) or the
next work day if it isn't.  In any case, the time of day is unaffected.

If $time is passed in, day 0 is now (if now is part of a workday) or the
start of the very next work day.

=item Date_PrevWorkDay

  $date=&Date_PrevWorkDay($date,$off [,$time]);

Similar to Date_NextWorkDay.

=item DateManipVersion

  $version=&DateManipVersion

Returns the version of Date::Manip.

=back
=head1 TIMEZONES

The following timezone names are currently understood (and can be used in
parsing dates).  These are zones defined in RFC 822.

    Universal:  GMT, UT
    US zones :  EST, EDT, CST, CDT, MST, MDT, PST, PDT
    Military :  A to Z (except J)
    Other    :  +HHMM or -HHMM

In addition, the following timezone abbreviations are also accepted.  In a
few cases, the same abbreviation is used for two different timezones (for
example, NST stands for Newfoundland Standare -0330 and North Sumatra +0630).
In these cases, only 1 of the two is available.  The one preceded by a "#"
sign is NOT available but is documented here for completeness.  This list of
zones comes from the Time::Zone module by Graham Barr, David Muir Sharnoff,
and Paul Foley.

      IDLW    -1200    International Date Line West
      NT      -1100    Nome
      HST     -1000    Hawaii Standard
      CAT     -1000    Central Alaska
      AHST    -1000    Alaska-Hawaii Standard
      YST     -0900    Yukon Standard
      HDT     -0900    Hawaii Daylight
      YDT     -0800    Yukon Daylight
      PST     -0800    Pacific Standard
      PDT     -0700    Pacific Daylight
      MST     -0700    Mountain Standard
      MDT     -0600    Mountain Daylight
      CST     -0600    Central Standard
      CDT     -0500    Central Daylight
      EST     -0500    Eastern Standard
      EDT     -0400    Eastern Daylight
      AST     -0400    Atlantic Standard
     #NST     -0330    Newfoundland Standard       nst=North Sumatra    +0630
      NFT     -0330    Newfoundland
     #GST     -0300    Greenland Standard          gst=Guam Standard    +1000
      BST     -0300    Brazil Standard             bst=British Summer   +0100
      ADT     -0300    Atlantic Daylight
      NDT     -0230    Newfoundland Daylight
      AT      -0200    Azores
      WAT     -0100    West Africa
      GMT     +0000    Greenwich Mean
      UT      +0000    Universal (Coordinated)
      UTC     +0000    Universal (Coordinated)
      WET     +0000    Western European
      CET     +0100    Central European
      FWT     +0100    French Winter
      MET     +0100    Middle European
      MEWT    +0100    Middle European Winter
      SWT     +0100    Swedish Winter
     #BST     +0100    British Summer              bst=Brazil standard  -0300
      EET     +0200    Eastern Europe, USSR Zone 1
      FST     +0200    French Summer
      MEST    +0200    Middle European Summer
      SST     +0200    Swedish Summer              sst=South Sumatra    +0700
      BT      +0300    Baghdad, USSR Zone 2
      IT      +0330    Iran
      ZP4     +0400    USSR Zone 3
      ZP5     +0500    USSR Zone 4
      IST     +0530    Indian Standard
      ZP6     +0600    USSR Zone 5
      NST     +0630    North Sumatra               nst=Newfoundland Std -0330
      WAST    +0700    West Australian Standard
     #SST     +0700    South Sumatra, USSR Zone 6  sst=Swedish Summer   +0200
      JT      +0730    Java (3pm in Cronusland!)
      CCT     +0800    China Coast, USSR Zone 7
      WADT    +0800    West Australian Daylight
      JST     +0900    Japan Standard, USSR Zone 8
      CAST    +0930    Central Australian Standard
      EAST    +1000    Eastern Australian Standard
      GST     +1000    Guam Standard, USSR Zone 9  gst=Greenland Std    -0300
      CADT    +1030    Central Australian Daylight
      EADT    +1100    Eastern Australian Daylight
      IDLE    +1200    International Date Line East
      NZST    +1200    New Zealand Standard
      NZT     +1200    New Zealand
      NZDT    +1300    New Zealand Daylight

      US/Pacific  -0800
      US/Mountain -0700
      US/Central  -0600
      US/Eastern  -0500

Others can be added in the future upon request.

DateManip needs to be able to determine the local timezone.  It can do this
by certain things such as the TZ environment variable (see Date_TimeZone
documentation above) or useing the TZ config variable (described below).
In either case, the timezone can be of the form STD#DST (for example
EST5EDT).  Both the standard and daylight savings time abbreviations must
be in the table above in order for this to work.  Also, this form may NOT
be used when parsing a date as there is no way to determine whether the
date is in daylight saving time or not.

=head1 BUSINESS MODE

Anyone using business mode is going to notice a few quirks about it which
should be explained.  When I designed business mode, I had in mind what UPS
tells me when they say 2 day delivery, or what the local business which
promises 1 business day turnaround really means.

If you do a business day calculation (with the workday set to 9:00-5:00),
you will get the following:

   Saturday at noon + 1 business day = Tuesday at 9:00
   Saturday at noon - 1 business day = Friday at 9:00

What does this mean?

We have a business that works 9-5 and they have a drop box so I can drop
things off over the weekend and they promise 1 business day turnaround.  If
I drop something off Friday night, Saturday, or Sunday, it doesn't matter.
They're going to get started on it Monday morning.  It'll be 1 business day
to finish the job, so the earliest I can expect it to be done is around
17:00 Monday or 9:00 Tuesday morning.  Unfortunately, there is some
ambiguity as to what day 17:00 really falls on, similar to the ambiguity
that occurs when you ask what day midnight falls on.  Although it's not the
only answer, Date::Manip treats midnight as the beginning of a day rather
than the end of one.  In the same way, 17:00 is equivalent to 9:00 the next
day and any time the date calculations encounter 17:00, it automatically
switch to 9:00 the next day.  Although this introduces some quirks, I think
this is justified.  You just have to treat 9:00 as being ambiguous (in the
same way you treat midnight as being ambiguous).

Equivalently, if I want a job to be finished on Saturday (despite the fact
that I cannot pick it up since the business is closed), I have to drop it
off no later than Friday at 9:00.  That gives them a full business day to
finish it off.  Of course, I could just as easily drop it off at 17:00
Thursday, or any time between then and 9:00 Friday.  Again, it's a matter
of treating 9:00 as ambiguous.

So, in case the business date calculations ever produce results that you
find confusing, I believe the solution is to write a wrapper which,
whenever it sees a date with the time of exactly 9:00, it treats it
specially (depending on what you want.

So Saturday + 1 business day = Tuesday at 9:00 (which means anything
from Monday 17:00 to Tuesday 9:00), but Monday at 9:01 + 1 business
day = Tuesday at 9:01 which is exact.

If this is not exactly what you have in mind, don't use the DateCalc
routine.  You can probably get whatever behavior you want using the
routines Date_IsWorkDay, Date_NextWorkDay, and Date_PrevWorkDay described
above.

=head1 CUSTOMIZING DATE::MANIP

There are a number of variables which can be used to customize the way
Date::Manip behaves.  There are also several ways to set these variables.

At the top of the Manip.pm file, there is a section which contains all
customization variables.  These provide the default values.

These can be overridden in a global config file if one is present (this
file is optional).  If the GlobalCnf variable is set in the Manip.pm file,
it contains the full path to a config file.  If the file exists, it's
values will override those set in the Manip.pm file.  A sample config file
is included with the Date::Manip distribution.  Modify it as appropriate
and copy it to some appropriate directory and set the GlobalCnf variable
in the Manip.pm file.

Each user can have a personal config file which is of the same form as
the global config file.  The variables PersonalCnf and PersonalCnfPath
set the name and search path for the personal config file.

Finally, any variables passed in through Date_Init override all other
values.

A config file can be composed of several sections (though only 2 of them
are currently used).  The first section sets configuration varibles.  Lines
in this section are of the form:

   VARIABLE = VALUE

For example, to make the default language French, include the line:

   Language = French

Only variables described below may be used.  Blank lines and lines beginning
with a pound sign (#) are ignored.  All spaces are optional and strings are
case insensitive.

A line which starts with an asterix (*) designates a new section.  The only
section currently used is the Holiday section.  All lines are of the form:

   DATE = HOLIDAY

HOLIDAY is the name of the holiday (or it can be blank in which case the
day will still be treated as a holiday... for example the day after
Thanksgiving or Christmas is often a work holiday though neither are
named).

DATE is a string which can be parsed to give a valid date in any year.  It
can be of the form

   Date
   Date + Delta
   Date - Delta

A valid holiday section would be:

   *Holiday

   1/1                             = New Year's Day
   third Monday in Feb             = Presidents' Day
   fourth Thu in Nov               = Thanksgiving

   # The Friday after Thanksgiving is an unnamed holiday most places
   fourth Thu in Nov + 1 day       =

In a Date + Delta or Date - Delta string, you can use business mode by
including the appropriate string (see documentation on DateCalc) in the
Date or Delta.  So (in English), the first workday before Christmas could
be defined as:

   12/25 - 1 business day          =

All Date::Manip variables which can be used are described in the following
section.

=over 4
=item IgnoreGlobalCnf

If this variable is used (any value is ignored), the global config file
is not read.  It must be present in the initial call to Date_Init or the
global config file will be read.

=item EraseHolidays

If this variable is used (any value is ignored), the current list of
defined holidays is erased.  A new set will be set the next time a
config file is read in.

=item PersonalCnf

This variable can be passed into Date_Init to read a different personal
configuration file.  It can also be included in the global config file
to define where personal config files live.

=item PersonalCnfPath

Used in the same way as the PersonalCnf option.  You can use tilde (~)
expansions when defining the path.

=item Language

Date::Manip can be used to parse dates in many different languages.
Currently, it is configured to read English, Swedish, and French dates,
but others can be added easily.  Language is set to the language used to
parse dates.

=item DateFormat

Different countries look at the date 12/10/96 as Dec 10 or Oct 12.  In the
United States, the first is most common, but this certainly doesn't hold
true for other countries.  Setting DateFormat to "US" forces the first
behavior (Dec 10).  Setting DateFormat to anything else forces the second
behavior (Oct 12).

=item TZ

Date::Manip is able to understand some timezones (and others will be added
in the future).  At the very least, all zones defined in RFC 822 are
supported.  Currently supported zones are listed in the TIMEZONES section
above and all timezones should be entered as one of them.

Date::Manip must be able to determine the timezone the user is in.  It does
this by looking in the following places:

   the environment variable TZ
   the variable $main::TZ
   the file /etc/TIMEZONE
   the 5th element of the unix "date" command (not available on NT machines)

At least one of these should contain a timezone in one of the supported
forms.  If it doesn't, the TZ variable must be set to contain the local
timezone in the appropriate form.

The TZ variable will override the other methods of determining the
timezone, so it should probably be left blank if any of the other methods
will work.  Otherwise, you will have to modify the variable every time you
switch to/from daylight savings time.

=item ConvTZ

All date comparisons and calculations must be done in a single time zone in
order for them to work correctly.  So, when a date is parsed, it should be
converted to a specific timezone.  This allows dates to easily be compared
and manipulated as if they are all in a single timezone.

The ConvTZ variable determines which timezone should be used to store dates
in.  If it is left blank, all dates are converted to the local timezone
(see the TZ variable above).  If it is set to one of the timezones listed
above, all dates are converted to this timezone.  Finally, if it is set to
the string "IGNORE", all timezone information is ignored as the dates are
read in (in this case, the two dates "1/1/96 12:00 GMT" and "1/1/96 12:00
EST" would be treated as identical).

=item Internal

When a date is parsed using ParseDate, that date is stored in an internal
format which is understood by the Date::Manip routines UnixDate and
DateCalc.  Originally, the format used to store the date internally was:

   YYYYMMDDHH:MN:SS

It has been suggested that I remove the colons (:) to shorten this to:

   YYYYMMDDHHMNSS

The main advantage of this is that some databases are colon delimited which
makes storing date from Date::Manip tedious.

In order to maintain backwards compatibility, the Internal varialbe was
introduced.  Set it to 0 (to use the old format) or 1 (to use the new
format).

=item FirstDay

It is sometimes necessary to know what day of week is regarded as first.
By default, this is set to sunday, but many countries and people will
prefer monday (and in a few cases, a different day may be desired).  Set
the FirstDay variable to be the first day of the week (0=sunday to
6=saturday).  Incidentally, monday should be chosen as the default to be in
complete accordance with ISO 8601.

=item WorkWeekBeg, WorkWeekEnd

The first and last days of the work week.  By default, monday and friday.
WorkWeekBeg must come before WorkWeekEnd (i.e. there is no way to handle
an odd work week of Thu to Mon for example).

=item WorkDay24Hr

If this is non-nil, a work day is treated as being 24 hours long.  The
WorkDayBeg and WorkDayEnd variables are ignored in this case.

=item WorkDayBeg, WorkDayEnd

The times when the work day starts and ends.  WorkDayBeg must come before
WorkDayEnd (i.e. there is no way to handle the night shift where the work
day starts one day and ends another).  Also, the workday MUST be more than
one hour long (of course, if this isn't the case, let me know... I want a
job there!).

The time in both can be in any valid time format (including international
formats), but seconds will be ignored.

=item DeltaSigns

Prior to Date::Manip version 5.07, a negative delta would put negative
signs in front of every component (i.e. "0:0:-1:-3:0:-4").  By default,
5.07 changes this behavior to print only 1 or two signs in front of the
year and day elements (even if these elements might be zero) and the sign
for year/month and day/hour/minute/second are the same.  Setting this
variable to non-zero forces deltas to be stored with a sign in front of
every element (including elements equal to 0).

=back
=head1 KNOWN PROBLEMS

If you use Date::Manip to sort a number of dates, you must call Date_Init
either explicitely, or by way of some other Date::Manip routine before it
is used in the sort.  For example, the following code fails:

   use Date::Manip;
   # &Date_Init;
   sub sortDate {
       my($date1, $date2);
       $date1 = &ParseDate($a);
       $date2 = &ParseDate($b);
       return ($date1 cmp $date2);
   }
   @date = ("Fri 16 Aug 96",
            "Mon 19 Aug 96",
            "Thu 15 Aug 96");
   @i=sort sortDate @dates;

but if you uncomment the Date_Init line, it works.  The reason for this is
that the first time you call Date_Init, it initializes a number of items
used by Date::Manip.  Some of these are sorted.  It turns out that perl
(5.003 and earlier, fixed in 5.003_07) does not like a sort within a sort.
The solution is to either upgrade to perl 5.004 (when it comes out) or to
do the initialization sorting ahead of time by calling Date_Init
explicitely.

Date::Manip has other sorting problems on an NT machine.  Again, 5.004 may
fix this, but I can't be sure since I don't have access to an NT machine.
If someone will confirm this one way or the other, I'd appreciate it.

If you are running a script which uses Date::Manip over a period of time
which starts in one time zone and ends in another (i.e. it switches form
Daylight Savings Time to Standard Time or vice versa), many things may be
wrong (especially elapsed time).  Since the most likely place for
Date::Manip probably gets the current time zone is from an environment
variable, you will have to reset this variable (by logging out or other
means) and restart the script under the new environment before it is back
to normal.

If you try to put Date::Manip under RCS control, you are going to have
problems.  Apparently, RCS replaces strings of the form "$Date...$" with
the current date.  This form occurs all over in Date::Manip.  Since very
few people will ever have a desire to do this (and I don't use RCS), I have
not worried about it.

=head1 AUTHOR

Sullivan Beck (beck@qtp.ufl.edu)

=cut
