%{
/* GNU Mailutils -- a suite of utilities for electronic mail
   Copyright (C) 2003-2020 Free Software Foundation, Inc.

   GNU Mailutils is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2, or (at your option)
   any later version.
   
   GNU Mailutils is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with GNU Mailutils.  If not, see <http://www.gnu.org/licenses/>. */
  
/* A heavily modified version of the well-known public domain getdate.y.
   It was originally written by Steven M. Bellovin <smb@research.att.com>
   while at the University of North Carolina at Chapel Hill.  Later tweaked
   by a couple of people on Usenet.  Completely overhauled by Rich $alz
   <rsalz@bbn.com> and Jim Berets <jberets@bbn.com> in August, 1990.
   Rewritten using a proper union by Sergey Poznyakoff <gray@gnu.org> */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <mailutils/cctype.h>
#include <mailutils/cstr.h>
#include <mailutils/datetime.h>

#define ISSPACE(c) mu_isspace (c)
#define ISALPHA(c) mu_isalpha (c)
#define ISUPPER(c) mu_isupper (c)

static inline int ISDIGIT (unsigned c)
{
  return mu_isdigit (c);
}

static int yyparse (void); 
static int yylex (void);
static int yyerror (char *s);

#define EPOCH		1970
#define HOUR(x)		((x) * 60)

#define MAX_BUFF_LEN    128   /* size of buffer to read the date into */

/*
**  An entry in the lexical lookup table.
*/
typedef struct _lex_tab {
  const char	*name;
  int		type;
  int		value;
} SYMBOL;


/*
**  Meridian:  am, pm, or 24-hour style.
*/
typedef enum meridian {
  MERam,
  MERpm,
  MER24
} MERIDIAN;
 
#define MASK_IS_SET(f,m) (((f)&(m))==(m))
#define MASK_TEST(f,m)   ((f)&(m)) 
struct pd_date
{
  int mask;
  int day;
  int hour;
  int minute;
  int month;
  int second;
  int year;
  int tz;
  char const *tzname;
  enum meridian meridian;
  int number;
  int ordinal;
};

#define DATE_INIT(date) memset(&(date), 0, sizeof(date))
#define DATE_SET(date, memb, m, val, lim, onerror)                        \
 do                                                                       \
   {                                                                      \
     int __x = val;                                                       \
     if (((m) != MU_PD_MASK_TZ && __x < 0) || (lim && __x > lim)) onerror;\
     date . memb = __x; date.mask |= m;                                   \
   }                                                                      \
 while (0)
   
#define __SET_SECOND(d,v,a)   DATE_SET(d,second,MU_PD_MASK_SECOND,v,59,a)
#define __SET_MINUTE(d,v,a)   DATE_SET(d,minute,MU_PD_MASK_MINUTE,v,59,a)  
#define __SET_HOUR(d,v,a)     DATE_SET(d,hour,MU_PD_MASK_HOUR,v,23,a)
#define __SET_DAY(d,v,a)      DATE_SET(d,day,MU_PD_MASK_DAY,v,31,a)   
#define __SET_MONTH(d,v,a)    DATE_SET(d,month,MU_PD_MASK_MONTH,v,12,a)
#define __SET_YEAR(d,v,a)     DATE_SET(d,year,MU_PD_MASK_YEAR,v,0,a)  
#define __SET_TZ(d,v,a)       DATE_SET(d,tz,MU_PD_MASK_TZ,v,0,a)
#define __SET_MERIDIAN(d,v,a) DATE_SET(d,meridian,MU_PD_MASK_MERIDIAN,v,MER24,a)
#define __SET_ORDINAL(d,v,a)  DATE_SET(d,ordinal,MU_PD_MASK_ORDINAL,v,0,a)
#define __SET_NUMBER(d,v,a)   DATE_SET(d,number,MU_PD_MASK_NUMBER,v,0,a) 
 
#define SET_SECOND(d,v)   __SET_SECOND(d,v,YYERROR)   
#define SET_MINUTE(d,v)   __SET_MINUTE(d,v,YYERROR)   
#define SET_HOUR(d,v)     __SET_HOUR(d,v,YYERROR)     
#define SET_DAY(d,v)      __SET_DAY(d,v,YYERROR)      
#define SET_MONTH(d,v)    __SET_MONTH(d,v,YYERROR)    
#define SET_YEAR(d,v)     __SET_YEAR(d,v,YYERROR)     
#define SET_TZ(d,v)       __SET_TZ(d,v,YYERROR)
/* Set timezone from a packed representation (HHMM)

   The proper way of doing so would be:
   
#define SET_TZ_PACK(d,v)				  \
  SET_TZ (d, ((v) < 0 ? -(-(v) % 100 + (-(v) / 100) * 60) \
	              : ((v) % 100 + ((v) / 100) * 60)))

   However, we need to invert the sign in order for mktime
   to work properly (see mu_parse_date_dtl below).  The proper
   sign is then restored upon return from the function.
   
   Once mu_mktime is in place, this can be changed.
*/
#define SET_TZ_PACK(d,v)				  \
  SET_TZ (d, ((v) < 0 ? (-(v) % 100 + (-(v) / 100) * 60)  \
	              : -((v) % 100 + ((v) / 100) * 60)))

#define SET_MERIDIAN(d,v) __SET_MERIDIAN(d,v,YYERROR) 
#define SET_ORDINAL(d,v)  __SET_ORDINAL(d,v,YYERROR)  
#define SET_NUMBER(d,v)   __SET_NUMBER(d,v,YYERROR)   

int
pd_date_union (struct pd_date *a, struct pd_date *b)
{
  int diff = (~a->mask) & b->mask;
  if (!diff)
    return 1;

  a->mask |= diff;
  
  if (diff & MU_PD_MASK_SECOND)
    a->second = b->second;
  
  if (diff & MU_PD_MASK_MINUTE)
    a->minute = b->minute;

  if (diff & MU_PD_MASK_HOUR)
    a->hour = b->hour;

  if (diff & MU_PD_MASK_DAY)
    a->day = b->day;

  if (diff & MU_PD_MASK_MONTH)
    a->month = b->month;

  if (diff & MU_PD_MASK_YEAR)
    a->year = b->year;

  if (diff & MU_PD_MASK_TZ)
    a->tz = b->tz;

  if (diff & MU_PD_MASK_MERIDIAN)
    a->meridian = b->meridian;

  if (diff & MU_PD_MASK_ORDINAL)
    a->ordinal = b->ordinal;

  if (diff & MU_PD_MASK_NUMBER)
    a->number = b->number;

  return 0;
}

struct pd_datespec
{
  struct pd_date date;
  struct pd_date rel;
};

static struct pd_datespec pd;
 
static const char	*yyinput;

%}

%union {
  int number;
  enum meridian meridian;
  struct pd_date date;
  struct pd_datespec datespec;
  struct { char const *name; int delta; } tz;
}

/*FIXME: do we need T_ID? */
%token  T_AGO T_DST  T_ID

%token <number> T_DAY T_DAY_UNIT T_HOUR_UNIT T_MINUTE_UNIT
%token <number> T_MONTH T_MONTH_UNIT
%token <number>	T_SEC_UNIT T_SNUMBER T_UNUMBER T_YEAR_UNIT
%token <tz>  T_ZONE T_DAYZONE
%token <meridian> T_MERIDIAN

%type <meridian> o_merid
%type <date> item time zone date day rel relspec relunit
%type <datespec> spec

%%

input   : spec
          {
	    pd = $1;
	  }
        ;

spec	: /* NULL */
          {
	    DATE_INIT ($$.date);
	    DATE_INIT ($$.rel);
	  }
	| spec item
          {
	    if (pd_date_union (&$1.date, &$2))
	      YYERROR;
	    $$ = $1;
	  }
        | spec rel
          {
	    if (pd_date_union (&$1.rel, &$2))
	      YYERROR;
	    $$ = $1;
	  }
        | spec T_UNUMBER
          {
	    if (MASK_IS_SET ($1.date.mask, (MU_PD_MASK_TIME|MU_PD_MASK_DATE))
		&& !$1.rel.mask)
	      {
		if (MASK_IS_SET ($1.date.mask, MU_PD_MASK_YEAR))
		  {
		    if (!MASK_IS_SET ($1.date.mask, MU_PD_MASK_TZ))
		      SET_TZ_PACK ($1.date, $2);
		    else
		      YYERROR;
		  }
		else
		  {
		    SET_YEAR ($1.date, $2);
		  }
	      }
	    else
	      {
		if ($2 > 10000)
		  {
		    SET_DAY ($1.date, $2 % 100);
		    SET_MONTH ($1.date, ($2 / 100) %100);
		    SET_YEAR ($1.date, $2 / 10000);
		  }
		else
		  {
		    if ($2 < 100)
		      {
			SET_YEAR ($1.date, $2);
		      }
		    else
		      {
		    	SET_HOUR ($1.date, $2 / 100);
		    	SET_MINUTE ($1.date, $2 % 100);
		      }
		    SET_MERIDIAN ($1.date, MER24);
		  }
	      }
	    $$ = $1;
	  }
	;

item	: time
        | zone
	| date
	| day
	;

time	: T_UNUMBER T_MERIDIAN
          {
	    DATE_INIT ($$);
	    SET_HOUR ($$, $1);
	    SET_MERIDIAN ($$, $2);
	  }
	| T_UNUMBER ':' T_UNUMBER o_merid
          {
	    DATE_INIT ($$);
	    SET_HOUR ($$, $1);
	    SET_MINUTE ($$, $3);
	    SET_MERIDIAN ($$, $4);
	  }
	| T_UNUMBER ':' T_UNUMBER T_SNUMBER
          {
	    DATE_INIT ($$);
	    SET_HOUR ($$, $1);
	    SET_MINUTE ($$, $3);
	    SET_MERIDIAN ($$, MER24);
	    SET_TZ_PACK ($$, $4);
	  }
	| T_UNUMBER ':' T_UNUMBER ':' T_UNUMBER o_merid
          {
	    DATE_INIT ($$);
	    SET_HOUR ($$, $1);
	    SET_MINUTE ($$, $3);
	    SET_SECOND ($$, $5);
	    SET_MERIDIAN ($$, $6);
	  }
	| T_UNUMBER ':' T_UNUMBER ':' T_UNUMBER T_SNUMBER
          {
	    DATE_INIT ($$);
	    SET_HOUR ($$, $1);
	    SET_MINUTE ($$, $3);
	    SET_SECOND ($$, $5);
	    SET_MERIDIAN ($$, MER24);
	    SET_TZ_PACK ($$, $6);
	  }
	;

zone	: T_ZONE
          {
	    DATE_INIT ($$);
	    SET_TZ ($$, $1.delta);
	    $$.tzname = $1.name;
	  }
	| T_DAYZONE
          {
	    DATE_INIT ($$);
	    SET_TZ ($$, $1.delta - 60);
	    $$.tzname = $1.name;
	  }
	| T_ZONE T_DST
          {
	    DATE_INIT ($$);
	    SET_TZ ($$, $1.delta - 60);
	    $$.tzname = $1.name;
	  }
	;

day	: T_DAY
          {
	    DATE_INIT ($$);
	    SET_ORDINAL ($$, 1);
	    SET_NUMBER ($$, $1);
	  }
	| T_DAY ','
          {
	    DATE_INIT ($$);
	    SET_ORDINAL ($$, 1);
	    SET_NUMBER ($$, $1);
	  }
	| T_UNUMBER T_DAY
          {
	    DATE_INIT ($$);
	    SET_ORDINAL ($$, $1);
	    SET_NUMBER ($$, $2);
	  }
	;

date	: T_UNUMBER '/' T_UNUMBER
          {
	    DATE_INIT ($$);
	    SET_MONTH ($$, $1);
	    SET_DAY ($$, $3);
	  }
	| T_UNUMBER '/' T_UNUMBER '/' T_UNUMBER
          {
	    DATE_INIT ($$);
	    /* Interpret as YYYY/MM/DD if $1 >= 1000, otherwise as MM/DD/YY.
	       The goal in recognizing YYYY/MM/DD is solely to support legacy
	       machine-generated dates like those in an RCS log listing.  If
	       you want portability, use the ISO 8601 format.  */
	    if ($1 >= 1000)
	      {
		SET_YEAR ($$, $1);
		SET_MONTH ($$, $3);
		SET_DAY ($$, $5);
	      }
	    else
	      {
		SET_MONTH ($$, $1);
		SET_DAY ($$, $3);
		SET_YEAR ($$, $5);
	      }
	  }
	| T_UNUMBER T_SNUMBER T_SNUMBER
          {
	    DATE_INIT ($$);
	    /* ISO 8601 format.  yyyy-mm-dd.  */
	    SET_YEAR ($$, $1);
	    SET_MONTH ($$, -$2);
	    SET_DAY ($$, -$3);
	  }
	| T_UNUMBER T_MONTH T_SNUMBER
          {
	    /* either 17-JUN-1992 or 1992-JUN-17 */
	    DATE_INIT ($$);
	    if ($1 < 32)
	      {
		SET_DAY ($$, $1);
		SET_MONTH ($$, $2);
		SET_YEAR ($$, -$3);
	      }
	    else
	      {
		SET_DAY ($$, -$3);
		SET_MONTH ($$, $2);
		SET_YEAR ($$, $1);
	      }
	  }
	| T_MONTH T_UNUMBER
          {
	    DATE_INIT ($$);
	    SET_MONTH ($$, $1);
	    SET_DAY ($$, $2);
	  }
	| T_MONTH T_UNUMBER ',' T_UNUMBER
          {
	    DATE_INIT ($$);
	    SET_MONTH ($$, $1);
	    SET_DAY ($$, $2);
	    SET_YEAR ($$, $4);
	  }
	| T_UNUMBER T_MONTH
          {
	    DATE_INIT ($$);
	    SET_MONTH ($$, $2);
	    SET_DAY ($$, $1);
	  }
	| T_UNUMBER T_MONTH T_UNUMBER
          {
	    DATE_INIT ($$);
	    SET_MONTH ($$, $2);
	    SET_DAY ($$, $1);
	    SET_YEAR ($$, $3);
	  }
        | T_DAY T_MONTH T_UNUMBER time T_UNUMBER
          {
	    DATE_INIT ($$);

	    SET_ORDINAL ($$, 1);
	    SET_NUMBER ($$, $1);

	    SET_MONTH ($$, $2);
	    SET_DAY ($$, $3);
	    SET_YEAR ($$, $5);
	    if (pd_date_union (&$$, &$4))
	      YYERROR;
	  }
	;

rel	: relspec T_AGO
          {
	    $1.second = - $1.second;
	    $1.minute = - $1.minute;
	    $1.hour = - $1.hour;
	    $1.day = - $1.day;
	    $1.month = - $1.month;
	    $1.year = - $1.year;
	    $$ = $1;
	  }
	| relspec
	;


relspec : relunit
          {
	    DATE_INIT ($$);
	    if (pd_date_union (&$$, &$1))
	      YYERROR;
	  }
        | relspec relunit
          {
	    if (pd_date_union (&$1, &$2))
	      YYERROR;
	    $$ = $1;
	  }
        ;

relunit	: T_UNUMBER T_YEAR_UNIT
          {
	    DATE_INIT ($$);
	    SET_YEAR ($$, $1 * $2);
	  }
	| T_SNUMBER T_YEAR_UNIT
          {
	    DATE_INIT ($$);
	    SET_YEAR ($$, $1 * $2);
	  }
	| T_YEAR_UNIT
          {
	    DATE_INIT ($$);
	    SET_YEAR ($$, $1);
	  }
	| T_UNUMBER T_MONTH_UNIT
          {
	    DATE_INIT ($$);
	    SET_MONTH ($$, $1 * $2);
	  }
	| T_SNUMBER T_MONTH_UNIT
          {
	    DATE_INIT ($$);
	    SET_MONTH ($$, $1 * $2);
	  }
	| T_MONTH_UNIT
          {
	    DATE_INIT ($$);
	    SET_MONTH ($$, $1);
	  }
	| T_UNUMBER T_DAY_UNIT
          {
	    DATE_INIT ($$);
	    SET_DAY ($$, $1 * $2);
	  }
	| T_SNUMBER T_DAY_UNIT
          {
	    DATE_INIT ($$);
	    SET_DAY ($$, $1 * $2);
	  }
	| T_DAY_UNIT
          {
	    DATE_INIT ($$);
	    SET_DAY ($$, $1);
	  }
	| T_UNUMBER T_HOUR_UNIT
          {
	    DATE_INIT ($$);
	    SET_HOUR ($$, $1 * $2);
	  }
	| T_SNUMBER T_HOUR_UNIT
          {
	    DATE_INIT ($$);
	    SET_HOUR ($$, $1 * $2);
	  }
	| T_HOUR_UNIT
          {
	    DATE_INIT ($$);
	    SET_HOUR ($$, $1);
	  }
	| T_UNUMBER T_MINUTE_UNIT
          {
	    DATE_INIT ($$);
	    SET_MINUTE ($$, $1 * $2);
	  }
	| T_SNUMBER T_MINUTE_UNIT
          {
	    DATE_INIT ($$);
	    SET_MINUTE ($$, $1 * $2);
	  }
	| T_MINUTE_UNIT
          {
	    DATE_INIT ($$);
	    SET_MINUTE ($$, $1);
	  }
	| T_UNUMBER T_SEC_UNIT
          {
	    DATE_INIT ($$);
	    SET_SECOND ($$, $1 * $2);
	  }
	| T_SNUMBER T_SEC_UNIT
          {
	    DATE_INIT ($$);
	    SET_SECOND ($$, $1 * $2);
	  }
	| T_SEC_UNIT
          {
	    DATE_INIT ($$);
	    SET_SECOND ($$, $1);
	  }
	;

o_merid	: /* empty */
          {
	    $$ = MER24;
	  }
	| T_MERIDIAN
	  {
	    $$ = $1;
	  }
	;

%%

#include <mailutils/types.h>

#if TIME_WITH_SYS_TIME
# include <sys/time.h>
# include <time.h>
#else
# if HAVE_SYS_TIME_H
#  include <sys/time.h>
# else
#  include <time.h>
# endif
#endif

#include <mailutils/util.h>

/* Month and day table. */
static SYMBOL const month_day_tab[] = {
  { "january",	T_MONTH,  1 },
  { "february",	T_MONTH,  2 },
  { "march",	T_MONTH,  3 },
  { "april",	T_MONTH,  4 },
  { "may",	T_MONTH,  5 },
  { "june",	T_MONTH,  6 },
  { "july",	T_MONTH,  7 },
  { "august",	T_MONTH,  8 },
  { "september",T_MONTH,  9 },
  { "sept",	T_MONTH,  9 },
  { "october",	T_MONTH, 10 },
  { "november",	T_MONTH, 11 },
  { "december",	T_MONTH, 12 },
  { "sunday",	T_DAY,   0 },
  { "monday",	T_DAY,   1 },
  { "tuesday",	T_DAY,   2 },
  { "tues",	T_DAY,   2 },
  { "wednesday",T_DAY,   3 },
  { "wednes",	T_DAY,   3 },
  { "thursday",	T_DAY,   4 },
  { "thur",	T_DAY,   4 },
  { "thurs",	T_DAY,   4 },
  { "friday",	T_DAY,   5 },
  { "saturday",	T_DAY,   6 },
  { NULL, 0, 0 }
};

/* Time units table. */
static SYMBOL const units_tab[] = {
  { "year",	T_YEAR_UNIT,	1 },
  { "month",	T_MONTH_UNIT,	1 },
  { "fortnight",T_DAY_UNIT,	14 },
  { "week",	T_DAY_UNIT,	7 },
  { "day",	T_DAY_UNIT,	1 },
  { "hour",	T_HOUR_UNIT,	1 },
  { "minute",	T_MINUTE_UNIT,	1 },
  { "min",	T_MINUTE_UNIT,	1 },
  { "second",	T_SEC_UNIT,	1 },
  { "sec",	T_SEC_UNIT,	1 },
  { NULL, 0, 0 }
};

/* Assorted relative-time words. */
static SYMBOL const other_tab[] = {
  { "tomorrow",	T_MINUTE_UNIT,	1 * 24 * 60 },
  { "yesterday",T_MINUTE_UNIT,	-1 * 24 * 60 },
  { "today",	T_MINUTE_UNIT,	0 },
  { "now",	T_MINUTE_UNIT,	0 },
  { "last",	T_UNUMBER,	-1 },
  { "this",	T_MINUTE_UNIT,	0 },
  { "next",	T_UNUMBER,	1 },
  { "first",	T_UNUMBER,	1 },
/*  { "second",	T_UNUMBER,	2 }, */
  { "third",	T_UNUMBER,	3 },
  { "fourth",	T_UNUMBER,	4 },
  { "fifth",	T_UNUMBER,	5 },
  { "sixth",	T_UNUMBER,	6 },
  { "seventh",	T_UNUMBER,	7 },
  { "eighth",	T_UNUMBER,	8 },
  { "ninth",	T_UNUMBER,	9 },
  { "tenth",	T_UNUMBER,	10 },
  { "eleventh",	T_UNUMBER,	11 },
  { "twelfth",	T_UNUMBER,	12 },
  { "ago",	T_AGO,	        1 },
  { NULL, 0, 0 }
};

/* The timezone table. */
static SYMBOL const tz_tab[] = {
  { "gmt",	T_ZONE,     HOUR ( 0) },	/* Greenwich Mean */
  { "ut",	T_ZONE,     HOUR ( 0) },	/* Universal (Coordinated) */
  { "utc",	T_ZONE,     HOUR ( 0) },
  { "wet",	T_ZONE,     HOUR ( 0) },	/* Western European */
  { "bst",	T_DAYZONE,  HOUR ( 0) },	/* British Summer */
  { "wat",	T_ZONE,     HOUR ( 1) },	/* West Africa */
  { "at",	T_ZONE,     HOUR ( 2) },	/* Azores */
#if	0
  /* For completeness.  BST is also British Summer, and GST is
   * also Guam Standard. */
  { "bst",	T_ZONE,     HOUR ( 3) },	/* Brazil Standard */
  { "gst",	T_ZONE,     HOUR ( 3) },	/* Greenland Standard */
#endif
#if 0
  { "nft",	T_ZONE,     HOUR (3.5) },	/* Newfoundland */
  { "nst",	T_ZONE,     HOUR (3.5) },	/* Newfoundland Standard */
  { "ndt",	T_DAYZONE,  HOUR (3.5) },	/* Newfoundland Daylight */
#endif
  { "ast",	T_ZONE,     HOUR ( 4) },	/* Atlantic Standard */
  { "adt",	T_DAYZONE,  HOUR ( 4) },	/* Atlantic Daylight */
  { "est",	T_ZONE,     HOUR ( 5) },	/* Eastern Standard */
  { "edt",	T_DAYZONE,  HOUR ( 5) },	/* Eastern Daylight */
  { "cst",	T_ZONE,     HOUR ( 6) },	/* Central Standard */
  { "cdt",	T_DAYZONE,  HOUR ( 6) },	/* Central Daylight */
  { "mst",	T_ZONE,     HOUR ( 7) },	/* Mountain Standard */
  { "mdt",	T_DAYZONE,  HOUR ( 7) },	/* Mountain Daylight */
  { "pst",	T_ZONE,     HOUR ( 8) },	/* Pacific Standard */
  { "pdt",	T_DAYZONE,  HOUR ( 8) },	/* Pacific Daylight */
  { "yst",	T_ZONE,     HOUR ( 9) },	/* Yukon Standard */
  { "ydt",	T_DAYZONE,  HOUR ( 9) },	/* Yukon Daylight */
  { "hst",	T_ZONE,     HOUR (10) },	/* Hawaii Standard */
  { "hdt",	T_DAYZONE,  HOUR (10) },	/* Hawaii Daylight */
  { "cat",	T_ZONE,     HOUR (10) },	/* Central Alaska */
  { "ahst",	T_ZONE,     HOUR (10) },	/* Alaska-Hawaii Standard */
  { "nt",	T_ZONE,     HOUR (11) },	/* Nome */
  { "idlw",	T_ZONE,     HOUR (12) },	/* International Date Line West */
  { "cet",	T_ZONE,     -HOUR (1) },	/* Central European */
  { "met",	T_ZONE,     -HOUR (1) },	/* Middle European */
  { "mewt",	T_ZONE,     -HOUR (1) },	/* Middle European Winter */
  { "mest",	T_DAYZONE,  -HOUR (1) },	/* Middle European Summer */
  { "mesz",	T_DAYZONE,  -HOUR (1) },	/* Middle European Summer */
  { "swt",	T_ZONE,     -HOUR (1) },	/* Swedish Winter */
  { "sst",	T_DAYZONE,  -HOUR (1) },	/* Swedish Summer */
  { "fwt",	T_ZONE,     -HOUR (1) },	/* French Winter */
  { "fst",	T_DAYZONE,  -HOUR (1) },	/* French Summer */
  { "eet",	T_ZONE,     -HOUR (2) },	/* Eastern Europe, USSR Zone 1 */
  { "bt",	T_ZONE,     -HOUR (3) },	/* Baghdad, USSR Zone 2 */
#if 0
  { "it",	T_ZONE,     -HOUR (3.5) },/* Iran */
#endif
  { "zp4",	T_ZONE,     -HOUR (4) },	/* USSR Zone 3 */
  { "zp5",	T_ZONE,     -HOUR (5) },	/* USSR Zone 4 */
#if 0
  { "ist",	T_ZONE,     -HOUR (5.5) },/* Indian Standard */
#endif
  { "zp6",	T_ZONE,     -HOUR (6) },	/* USSR Zone 5 */
#if	0
  /* For completeness.  NST is also Newfoundland Standard, and SST is
   * also Swedish Summer. */
  { "nst",	T_ZONE,     -HOUR (6.5) },/* North Sumatra */
  { "sst",	T_ZONE,     -HOUR (7) },	/* South Sumatra, USSR Zone 6 */
#endif	/* 0 */
  { "wast",	T_ZONE,     -HOUR (7) },	/* West Australian Standard */
  { "wadt",	T_DAYZONE,  -HOUR (7) },	/* West Australian Daylight */
#if 0
  { "jt",	T_ZONE,     -HOUR (7.5) },/* Java (3pm in Cronusland!) */
#endif
  { "cct",	T_ZONE,     -HOUR (8) },	/* China Coast, USSR Zone 7 */
  { "jst",	T_ZONE,     -HOUR (9) },	/* Japan Standard, USSR Zone 8 */
#if 0
  { "cast",	T_ZONE,     -HOUR (9.5) },/* Central Australian Standard */
  { "cadt",	T_DAYZONE,  -HOUR (9.5) },/* Central Australian Daylight */
#endif
  { "east",	T_ZONE,     -HOUR (10) },	/* Eastern Australian Standard */
  { "eadt",	T_DAYZONE,  -HOUR (10) },	/* Eastern Australian Daylight */
  { "gst",	T_ZONE,     -HOUR (10) },	/* Guam Standard, USSR Zone 9 */
  { "nzt",	T_ZONE,     -HOUR (12) },	/* New Zealand */
  { "nzst",	T_ZONE,     -HOUR (12) },	/* New Zealand Standard */
  { "nzdt",	T_DAYZONE,  -HOUR (12) },	/* New Zealand Daylight */
  { "idle",	T_ZONE,     -HOUR (12) },	/* International Date Line
						   East */
  {  NULL, 0, 0  }
};

/* Military timezone table. */
static SYMBOL const mil_tz_tab[] = {
  { "a",	T_ZONE,	HOUR (  1) },
  { "b",	T_ZONE,	HOUR (  2) },
  { "c",	T_ZONE,	HOUR (  3) },
  { "d",	T_ZONE,	HOUR (  4) },
  { "e",	T_ZONE,	HOUR (  5) },
  { "f",	T_ZONE,	HOUR (  6) },
  { "g",	T_ZONE,	HOUR (  7) },
  { "h",	T_ZONE,	HOUR (  8) },
  { "i",	T_ZONE,	HOUR (  9) },
  { "k",	T_ZONE,	HOUR ( 10) },
  { "l",	T_ZONE,	HOUR ( 11) },
  { "m",	T_ZONE,	HOUR ( 12) },
  { "n",	T_ZONE,	HOUR (- 1) },
  { "o",	T_ZONE,	HOUR (- 2) },
  { "p",	T_ZONE,	HOUR (- 3) },
  { "q",	T_ZONE,	HOUR (- 4) },
  { "r",	T_ZONE,	HOUR (- 5) },
  { "s",	T_ZONE,	HOUR (- 6) },
  { "t",	T_ZONE,	HOUR (- 7) },
  { "u",	T_ZONE,	HOUR (- 8) },
  { "v",	T_ZONE,	HOUR (- 9) },
  { "w",	T_ZONE,	HOUR (-10) },
  { "x",	T_ZONE,	HOUR (-11) },
  { "y",	T_ZONE,	HOUR (-12) },
  { "z",	T_ZONE,	HOUR (  0) },
  { NULL, 0, 0 }
};




/* ARGSUSED */
static int
yyerror (char *s MU_ARG_UNUSED)
{
  return 0;
}

static int
norm_hour (int hours, MERIDIAN meridian)
{
  switch (meridian)
    {
    case MER24:
      if (hours < 0 || hours > 23)
	return -1;
      return hours;
      
    case MERam:
      if (hours < 1 || hours > 12)
	return -1;
      if (hours == 12)
	hours = 0;
      return hours;
      
    case MERpm:
      if (hours < 1 || hours > 12)
	return -1;
      if (hours == 12)
	hours = 0;
      return hours + 12;
      
    default:
      abort ();
    }
  /* NOTREACHED */
}

static int
norm_year (int year)
{
  if (year < 0)
    year = -year;
  
  /* XPG4 suggests that years 00-68 map to 2000-2068, and
     years 69-99 map to 1969-1999.  */
  if (year < 69)
    year += 2000;
  else if (year < 100)
    year += 1900;

  return year;
}

static int
sym_lookup (char *buff)
{
  register char *p;
  register char *q;
  register const SYMBOL *tp;
  int i;
  int abbrev;
  
  /* Make it lowercase. */
  mu_strlower (buff);
  
  if (strcmp (buff, "am") == 0 || strcmp (buff, "a.m.") == 0)
    {
      yylval.meridian = MERam;
      return T_MERIDIAN;
    }
  if (strcmp (buff, "pm") == 0 || strcmp (buff, "p.m.") == 0)
    {
      yylval.meridian = MERpm;
      return T_MERIDIAN;
    }
  
  /* See if we have an abbreviation for a month. */
  if (strlen (buff) == 3)
    abbrev = 1;
  else if (strlen (buff) == 4 && buff[3] == '.')
    {
      abbrev = 1;
      buff[3] = '\0';
    }
  else
    abbrev = 0;

  for (tp = month_day_tab; tp->name; tp++)
    {
      if (abbrev)
	{
	  if (strncmp (buff, tp->name, 3) == 0)
	    {
	      yylval.number = tp->value;
	      return tp->type;
	    }
	}
      else if (strcmp (buff, tp->name) == 0)
	{
	  yylval.number = tp->value;
	  return tp->type;
	}
    }

  for (tp = tz_tab; tp->name; tp++)
    if (strcmp (buff, tp->name) == 0)
      {
        yylval.tz.name = tp->name;
	yylval.tz.delta = tp->value;
	return tp->type;
      }

  if (strcmp (buff, "dst") == 0)
    return T_DST;

  for (tp = units_tab; tp->name; tp++)
    if (strcmp (buff, tp->name) == 0)
      {
	yylval.number = tp->value;
	return tp->type;
      }

  /* Strip off any plural and try the units table again. */
  i = strlen (buff) - 1;
  if (buff[i] == 's')
    {
      buff[i] = '\0';
      for (tp = units_tab; tp->name; tp++)
	if (strcmp (buff, tp->name) == 0)
	  {
	    yylval.number = tp->value;
	    return tp->type;
	  }
      buff[i] = 's';		/* Put back for "this" in other_tab. */
    }

  for (tp = other_tab; tp->name; tp++)
    if (strcmp (buff, tp->name) == 0)
      {
	yylval.number = tp->value;
	return tp->type;
      }

  /* Military timezones. */
  if (buff[1] == '\0' && ISALPHA ((unsigned char) *buff))
    {
      for (tp = mil_tz_tab; tp->name; tp++)
	if (strcmp (buff, tp->name) == 0)
	  {
            yylval.tz.name = tp->name;
	    yylval.tz.delta = tp->value;
	    return tp->type;
	  }
    }

  /* Drop out any periods and try the timezone table again. */
  for (i = 0, p = q = buff; *q; q++)
    if (*q != '.')
      *p++ = *q;
    else
      i++;
  *p = '\0';
  if (i)
    for (tp = tz_tab; tp->name; tp++)
      if (strcmp (buff, tp->name) == 0)
	{
	  yylval.number = tp->value;
	  return tp->type;
	}

  return T_ID;
}

static int
yylex (void)
{
  register unsigned char c;
  register char *p;
  char buff[20];
  int count;
  int sign;

  for (;;)
    {
      while (ISSPACE ((unsigned char) *yyinput))
	yyinput++;

      if (ISDIGIT (c = *yyinput) || c == '-' || c == '+')
	{
	  if (c == '-' || c == '+')
	    {
	      sign = c == '-' ? -1 : 1;
	      if (!ISDIGIT (*++yyinput))
		/* skip the '-' sign */
		continue;
	    }
	  else
	    sign = 0;
	  for (yylval.number = 0; ISDIGIT (c = *yyinput++);)
	    yylval.number = 10 * yylval.number + c - '0';
	  yyinput--;
	  if (sign < 0)
	    yylval.number = -yylval.number;
	  return sign ? T_SNUMBER : T_UNUMBER;
	}
      if (ISALPHA (c))
	{
	  for (p = buff; (c = *yyinput++, ISALPHA (c)) || c == '.';)
	    if (p < &buff[sizeof buff - 1])
	      *p++ = c;
	  *p = '\0';
	  yyinput--;
	  return sym_lookup (buff);
	}
      if (c != '(')
	return *yyinput++;
      count = 0;
      do
	{
	  c = *yyinput++;
	  if (c == '\0')
	    return c;
	  if (c == '(')
	    count++;
	  else if (c == ')')
	    count--;
	}
      while (count > 0);
    }
}

#define TM_YEAR_ORIGIN 1900

/* Yield A - B, measured in seconds.  */
static long
difftm (struct tm *a, struct tm *b)
{
  int ay = a->tm_year + (TM_YEAR_ORIGIN - 1);
  int by = b->tm_year + (TM_YEAR_ORIGIN - 1);
  long days = (
  /* difference in day of year */
		a->tm_yday - b->tm_yday
  /* + intervening leap days */
		+ ((ay >> 2) - (by >> 2))
		- (ay / 100 - by / 100)
		+ ((ay / 100 >> 2) - (by / 100 >> 2))
  /* + difference in years * 365 */
		+ (long) (ay - by) * 365
  );
  return (60 * (60 * (24 * days + (a->tm_hour - b->tm_hour))
		+ (a->tm_min - b->tm_min))
	  + (a->tm_sec - b->tm_sec));
}

int
mu_parse_date_dtl (const char *p, const time_t *now, 
		   time_t *rettime,
		   struct tm *rettm,
		   struct mu_timezone *rettz,
		   int *retflags)
{
  struct tm tm, tm0, *tmp;
  time_t start;

  yyinput = p;
  start = now ? *now : time ((time_t *) NULL);
  tmp = localtime (&start);
  if (!tmp)
    return -1;

  memset (&tm, 0, sizeof tm);
  tm.tm_isdst = tmp->tm_isdst;

  if (yyparse ())
    return -1;
  
  if (!MASK_IS_SET (pd.date.mask, MU_PD_MASK_YEAR))
    __SET_YEAR (pd.date, tmp->tm_year + TM_YEAR_ORIGIN, return -1);
  if (!MASK_IS_SET (pd.date.mask, MU_PD_MASK_MONTH))
    __SET_MONTH (pd.date, tmp->tm_mon + 1, return -1);
  if (!MASK_IS_SET (pd.date.mask, MU_PD_MASK_DAY))
    __SET_DAY (pd.date, tmp->tm_mday, return -1);
  if (!MASK_IS_SET (pd.date.mask, MU_PD_MASK_HOUR))
    __SET_HOUR (pd.date, tmp->tm_hour, return -1);
  if (!MASK_IS_SET (pd.date.mask, MU_PD_MASK_MERIDIAN))
    __SET_MERIDIAN (pd.date, MER24, return -1);
  if (!MASK_IS_SET (pd.date.mask, MU_PD_MASK_MINUTE))
    __SET_MINUTE (pd.date, tmp->tm_min, return -1);
  if (!MASK_IS_SET (pd.date.mask, MU_PD_MASK_SECOND))
    __SET_SECOND (pd.date, tmp->tm_sec, return -1);
  
  tm.tm_year = norm_year (pd.date.year) - TM_YEAR_ORIGIN + pd.rel.year;
  tm.tm_mon = pd.date.month - 1 + pd.rel.month;
  tm.tm_mday = pd.date.day + pd.rel.day;
  if (MASK_TEST (pd.date.mask, MU_PD_MASK_TIME)
      || (pd.rel.mask && !MASK_TEST (pd.date.mask, MU_PD_MASK_DATE)
	  && !MASK_TEST (pd.date.mask, MU_PD_MASK_DOW)))
    {
      tm.tm_hour = norm_hour (pd.date.hour, pd.date.meridian);
      if (tm.tm_hour < 0)
	return -1;
      tm.tm_min = pd.date.minute;
      tm.tm_sec = pd.date.second;
    }
  else
    {
      tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
    }
  tm.tm_hour += pd.rel.hour;
  tm.tm_min += pd.rel.minute;
  tm.tm_sec += pd.rel.second;

  /* Let mktime deduce tm_isdst if we have an absolute timestamp,
     or if the relative timestamp mentions days, months, or years.  */
  if (MASK_TEST (pd.date.mask, MU_PD_MASK_DATE | MU_PD_MASK_DOW | MU_PD_MASK_TIME)
      || MASK_TEST (pd.rel.mask, MU_PD_MASK_DOW | MU_PD_MASK_MONTH | MU_PD_MASK_YEAR))
    tm.tm_isdst = -1;

  tm0 = tm;

  start = mktime (&tm);

  if (start == (time_t) -1)
    {

      /* Guard against falsely reporting errors near the time_t boundaries
         when parsing times in other time zones.  For example, if the min
         time_t value is 1970-01-01 00:00:00 UTC and we are 8 hours ahead
         of UTC, then the min localtime value is 1970-01-01 08:00:00; if
         we apply mktime to 1970-01-01 00:00:00 we will get an error, so
         we apply mktime to 1970-01-02 08:00:00 instead and adjust the time
         zone by 24 hours to compensate.  This algorithm assumes that
         there is no DST transition within a day of the time_t boundaries.  */
      if (MASK_TEST (pd.date.mask, MU_PD_MASK_TZ))
	{
	  tm = tm0;
	  if (tm.tm_year <= EPOCH - TM_YEAR_ORIGIN)
	    {
	      tm.tm_mday++;
	      pd.date.tz -= 24 * 60;
	    }
	  else
	    {
	      tm.tm_mday--;
	      pd.date.tz += 24 * 60;
	    }
	  start = mktime (&tm);
	}

      if (start == (time_t) -1)
	return -1;
    }

  if (MASK_TEST (pd.date.mask, MU_PD_MASK_DOW)
      && !MASK_TEST (pd.date.mask, MU_PD_MASK_DATE))
    {
      tm.tm_mday += ((pd.date.number - tm.tm_wday + 7) % 7
		     + 7 * (pd.date.ordinal - (0 < pd.date.ordinal)));
      start = mktime (&tm);
      if (start == (time_t) -1)
	return -1;
    }
  
  if (MASK_TEST (pd.date.mask, MU_PD_MASK_TZ))
    {
      long delta;
      struct tm *gmt = gmtime (&start);
      if (gmt)
	{
	  delta = pd.date.tz * 60L + difftm (&tm, gmt);
	  if ((start + delta < start) != (delta < 0))
	    return -1;		/* time_t overflow */
	  start += delta;
	}
    }

  if (MASK_TEST (pd.date.mask, MU_PD_MASK_TZ))
    {
      pd.date.tz = - pd.date.tz * 60L;
      if (!pd.date.tzname)
	pd.date.tzname = tm.tm_isdst != -1 ? tzname[tm.tm_isdst] : NULL;
#if HAVE_STRUCT_TM_TM_GMTOFF
      tm.tm_gmtoff = pd.date.tz;
#endif
#if HAVE_STRUCT_TM_TM_ZONE 	
      tm.tm_zone = pd.date.tzname;
#endif
    }
  if (rettime)
    *rettime = start;
  if (rettm)
    *rettm = tm;
  if (rettz)
    {
      if (MASK_TEST (pd.date.mask, MU_PD_MASK_TZ))
	{
	  rettz->utc_offset = pd.date.tz;
	  rettz->tz_name = pd.date.tzname;
	}
      else
	{
	  mu_datetime_tz_local (rettz);
	}
    }
  if (retflags)
    *retflags = pd.date.mask;
  return 0;
}

int
mu_parse_date (const char *p, time_t *rettime, const time_t *now)
{
  return mu_parse_date_dtl (p, now, rettime, NULL, NULL, NULL);
}

#ifdef STANDALONE
int
main (int argc, char *argv[])
{
  char buff[MAX_BUFF_LEN + 1];
  time_t d;

  if (argc > 1 && strcmp (argv[1], "-d") == 0)
    yydebug++;
  printf ("Enter date, or blank line to exit.\n\t> ");
  fflush (stdout);

  buff[MAX_BUFF_LEN] = 0;
  while (fgets (buff, MAX_BUFF_LEN, stdin) && buff[0])
    {
      if (mu_parse_date (buff, &d, NULL))
	printf ("Bad format - couldn't convert.\n");
      else
	printf ("%s", ctime (&d));
      printf ("\t> ");
      fflush (stdout);
    }
  exit (0);
  /* NOTREACHED */
}

#endif
