dd83de43950b33b4d28597f3d00d9021c2ee6982
[AXE.git] / src / parsedate.c
1
2 /*
3  * 
4  * Purpose:
5  *
6  *     Manipulate character strings representing dates.
7  *
8  * Usage:
9  *
10  *     #include <parsedate.h>
11  *
12  *     char date;
13  *     struct parseddate *pd;
14  *
15  *     pd = parsedate (date);
16  *
17  *     compute_unixtime (pd);
18  *
19  *     break_down_unixtime (pd);
20  *
21  *     date = mail_date_string (pd);
22  *
23  *     date = uucp_date_string (pd);
24  *
25  * Notes:
26  *
27  *     The returned value from "parsedate", "mail_date_string", or
28  *     "uucp_date_string" points to static data whose contents are
29  *     overwritten by the next call to the same routine.
30  *
31  *     "compute_unixtime" is implicitly called by "parsedate".
32  *
33  * Global contents:
34  *
35  *     struct parseddate *parsedate (date) char *date;
36  *         Parse a character string representing a date and time into
37  *         individual values in a "struct parseddate" data structure.
38  *    
39  *     compute_unixtime (pd) struct parseddate *pd;
40  *         Given a mostly filled-in "struct parseddate", compute the day
41  *         of the week and the internal UNIX representation of the date.
42  *    
43  *     break_down_unixtime (pd) struct parseddate *pd;
44  *         Compute the date and time corresponding to the "unixtime" and
45  *         "zone" values in a "struct parseddate".
46  *    
47  *     char *mail_date_string (pd) struct parseddate *pd;
48  *         Generate a character string representing a date and time in
49  *         the RFC822 (ARPANET mail standard) format.
50  *    
51  *     char *uucp_date_string (pd) struct parseddate *pd;
52  *         Generate a character string representing a date and time in
53  *         the UUCP mail format.
54  *
55  * Local contents:
56  *
57  *     None.
58  */
59
60 #include <stdio.h>
61 #include <string.h>
62 #include "parsedate.h"
63
64 static char *RCSID = "$Id: parsedate.c,v 1.2 2002-09-28 06:58:45 arjen Exp $";
65
66 extern int yyparse();
67
68 void compute_unixtime (register struct parseddate *pd);
69
70 /* Number of seconds in various time intervals. */
71 #define SEC_PER_MIN  60
72 #define SEC_PER_HOUR (60*SEC_PER_MIN)
73 #define SEC_PER_DAY  (24*SEC_PER_HOUR)
74 #define SEC_PER_YEAR (365*SEC_PER_DAY)
75
76 /* Number of days in each month. */
77 static int monthsize[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
78
79 /* Three-letter abbreviations of month and day names. */
80 static char monthname[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
81 static char dayname[]   = "SunMonTueWedThuFriSat";
82
83 /* struct parseddate *parsedate (date) char *date;
84  *     Analyze a character string representing a date and time.  The
85  *     returned value points to a data structure with the desired
86  *     information.  (NOTE:  The returned value points to static data
87  *     whose contents are overwritten by each call.)
88  */
89 struct parseddate * parsedate (char *date)
90 {   register char *c;
91     register int year_save;
92     extern struct parseddate yyans;
93     extern char *yyinbuf;
94
95     /* Initialize the returned-value structure. */
96     yyans.unixtime  = -1;
97     yyans.year      = -1;
98     yyans.month     = -1;
99     yyans.day       = -1;
100     yyans.hour      = -1;
101     yyans.minute    = -1;
102     yyans.second    = -1;
103     yyans.zone      = -1;
104     yyans.dst       = -1;
105     yyans.weekday   = -1;
106     yyans.c_weekday = -1;
107     yyans.error     =  NULL;
108
109     /* Parse the argument string. */
110     yyinbuf = date;
111
112     if (yyparse () != 0 && yyans.error == NULL)
113       yyans.error = yyinbuf;
114
115     /* Validate the day of the month, compute/validate the day of the
116      * week, and compute the internal UNIX form of the time.  See if
117      * "compute_unixtime" found fault with the year or the day of the
118      * month.  (Note that we have to remember the original "year" value
119      * because it might legitimately have been -1 to begin with.)
120      */
121     year_save = yyans.year; compute_unixtime (&yyans);
122     if (yyans.error == NULL
123         && (yyans.year != year_save
124             || (yyans.month > 0 && yyans.day < 0)
125             || (yyans.month < 0 && yyans.day > 0)))
126         yyans.error = yyinbuf;
127
128     return &yyans;
129 }
130
131 /* compute_unixtime (pd) struct parseddate *pd;
132  *     Given a mostly filled-in "struct parseddate", compute the day of
133  *     the week and the internal UNIX representation of the date.
134  *
135  *     A year before 1600 will be rejected and replaced with -1.  A
136  *     date from 1600 on which falls outside the range representable in
137  *     internal UNIX form will still have the correct day of the week
138  *     computed.
139  *
140  *     The day of the week is always computed on the assumption that the
141  *     Gregorian calendar is in use.  Days of the week for dates in the
142  *     far future may turn out to be incorrect if any changes are made
143  *     to the calendar between now and then.
144  */
145 void compute_unixtime (register struct parseddate *pd)
146 {
147     register int weekday, n, l, a;
148
149     /* Validate the year. */
150     if (pd->year >= 0 && pd->year < 1600) pd->year = -1;
151
152     /* Validate the day of the month.  Also calculate the number of days
153      * in February (even if this is not February, we will need the num-
154      * ber of days in February later on when computing the UNIX time).
155      */
156     if (pd->month > 0)
157     {   if      (pd->year < 0)        monthsize[2] = 29;
158         else if (pd->year %   4 != 0) monthsize[2] = 28;
159         else if (pd->year % 100 != 0) monthsize[2] = 29;
160         else if (pd->year % 400 != 0) monthsize[2] = 28;
161         else                          monthsize[2] = 29;
162         if (pd->day <= 0 || pd->day > monthsize[pd->month])
163             pd->day = -1;
164     }
165
166     /* Compute the day of the week.  The next several lines constitute a
167      * perpetual-calendar formula.  Note, of course, that the "claimed"
168      * day of the week (pd->c_weekday) is ignored here.
169      */
170     if (pd->year > 0 && pd->month > 0 && pd->day > 0)
171     {   if (pd->month >= 3) n = pd->year / 100,
172                             l = pd->year % 100;
173         else                n = (pd->year-1) / 100,
174                             l = (pd->year-1) % 100;
175         a = (26 * ((pd->month+9)%12 + 1) - 2) / 10;
176         weekday = (a+(l>>2)+(n>>2)+l-(n+n)+pd->day);
177         while (weekday < 0) weekday += 7;
178         pd->weekday = weekday % 7;
179     }
180
181     /* Figure out the internal UNIX form of the date. */
182     if (pd->year >= 1969 && pd->year <= 2038
183         && pd->month > 0 && pd->day > 0
184         && pd->hour >= 0 && pd->minute >= 0
185         && pd->zone != -1 && pd->zone > -1440 && pd->zone < 1440)
186     {   pd->unixtime =
187               SEC_PER_YEAR * (pd->year - 1970)
188             + SEC_PER_DAY  * ((pd->year - 1969) / 4)
189             /* month is taken care of later */
190             + SEC_PER_DAY  * (pd->day - 1)
191             + SEC_PER_HOUR * pd->hour
192             + SEC_PER_MIN  * pd->minute
193             /* seconds are taken care of later */
194             - SEC_PER_MIN  * pd->zone;
195         if (pd->second >= 0)
196             pd->unixtime += pd->second;
197         for (n = pd->month - 1; n > 0; n--)
198             pd->unixtime += SEC_PER_DAY * monthsize[n];
199         if (pd->unixtime < 0) pd->unixtime = -1;
200     }
201     else pd->unixtime = -1;
202 }
203
204 /* break_down_unixtime (pd) struct parseddate *pd;
205  *     Given the "unixtime" and "zone" fields of a "struct parseddate",
206  *     compute the values of the "year", "month", "day", "hour", "min-
207  *     ute", "second", and "weekday" fields.  The "dst" and "error"
208  *     fields of the structure are not used or modified.
209  */
210 void break_down_unixtime (pd)
211     register struct parseddate *pd;
212 {   register unsigned long timevalue;
213     register int m, n;
214
215     /* Validate the "unixtime" and "zone" fields. */
216     if (pd->unixtime < 0
217         || pd->zone == -1 || pd->zone <= -1440 || pd->zone >= 1440)
218     {   /* Sorry, can't do it. */
219         pd->year = -1; pd->month = -1; pd->day = -1;
220         pd->hour = -1; pd->minute = -1; pd->second = -1;
221         pd->weekday = -1;
222         return;
223     }
224
225     /* Even though "pd->unixtime" must be non-negative, and thus cannot
226      * indicate a time earlier than 1970, a negative "pd->zone" could
227      * cause the local date to be Wednesday, 31 December 1969.  Such a
228      * date requires special handling.
229      *
230      * A local date earlier than 31 December 1969 is impossible because
231      * "pd->zone" must represent a time-zone shift of less than a day.
232      */
233     if (pd->zone < 0 && pd->unixtime + SEC_PER_MIN * pd->zone < 0)
234     {   pd->year = 1969; pd->month = 12; pd->day = 31;
235         pd->weekday = 3;    /* Wednesday */
236         timevalue = pd->unixtime + SEC_PER_MIN * pd->zone + SEC_PER_DAY;
237         /* Note:  0 <= timevalue < SEC_PER_DAY */
238         pd->hour = timevalue / SEC_PER_HOUR;
239         pd->minute = (timevalue % SEC_PER_HOUR) / SEC_PER_MIN;
240         pd->second = timevalue % SEC_PER_MIN;
241         return;
242     }
243
244     /* Handle the general case (local time is 1970 or later). */
245     timevalue = pd->unixtime + SEC_PER_MIN * pd->zone;
246
247     /* day of the week (1 January 1970 was a Thursday) . . . */
248     pd->weekday = (timevalue/SEC_PER_DAY + 4 /* Thursday */) % 7;
249
250     /* year (note that the only possible century year here is 2000,
251      * a leap year -- hence no special tests for century years are
252      * needed) . . .
253      */
254     for (m = 1970; ; m++)
255     {   n = (m%4==0) ? SEC_PER_YEAR+SEC_PER_DAY : SEC_PER_YEAR;
256         if (n > timevalue) break;
257         timevalue -= n;
258     }
259     pd->year = m;
260     monthsize[2] = (m%4==0) ? 29 : 28;
261
262     /* month . . . */
263     for (m = 1; ; m++)
264     {   n = SEC_PER_DAY * monthsize[m];
265         if (n > timevalue) break;
266         timevalue -= n;
267     }
268     pd->month = m;
269
270     /* day, hour, minute, and second . . . */
271     pd->day    = (timevalue / SEC_PER_DAY) + 1;
272     pd->hour   = (timevalue % SEC_PER_DAY) / SEC_PER_HOUR;
273     pd->minute = (timevalue % SEC_PER_HOUR) / SEC_PER_MIN;
274     pd->second = timevalue % SEC_PER_MIN;
275 }
276
277 /* char *mail_date_string (pd) struct parseddate *pd;
278  *     Generate a character string representing a date and time in the
279  *     RFC822 (ARPANET mail standard) format.  A value of NULL is re-
280  *     turned if "pd" does not contain all necessary data fields.
281  *     (NOTE:  The returned value points to static data whose contents
282  *     are overwritten by each call.)
283  */
284 char *
285 mail_date_string (pd)
286     register struct parseddate *pd;
287 {   register char *c;
288     static char answer[50];
289
290     /* Check the day of the month and compute the day of the week. */
291     compute_unixtime (pd);
292
293     /* Make sure all required fields are present. */
294     if (pd->year < 0 || pd->month < 0 || pd->day < 0
295         || pd->hour < 0 || pd->minute < 0
296         || pd->zone == -1 || pd->zone <= -1440 || pd->zone >= 1440)
297         return NULL;            /* impossible to generate string */
298
299     /* Generate the answer string. */
300     sprintf (answer,
301              "%.3s, %d %.3s %d %02d:%02d",
302              dayname + 3*pd->weekday,
303              pd->day, monthname + 3*(pd->month-1),
304              (pd->year >= 1960 && pd->year <= 1999)
305                  ? pd->year - 1900 : pd->year,
306              pd->hour, pd->minute);
307     c = answer + strlen (answer);
308     if (pd->second >= 0) sprintf (c, ":%02d", pd->second), c += 3;
309     *c++ = ' ';
310     switch (pd->zone)
311     {   /* NOTE:  Only zone abbreviations in RFC822 are used here. */
312         case    0: strcpy (c, pd->dst ? "+0000" : "GMT");   break;
313         case -240: strcpy (c, pd->dst ? "EDT"   : "-0400"); break;
314         case -300: strcpy (c, pd->dst ? "CDT"   : "EST");   break;
315         case -360: strcpy (c, pd->dst ? "MDT"   : "CST");   break;
316         case -420: strcpy (c, pd->dst ? "PDT"   : "MST");   break;
317         case -480: strcpy (c, pd->dst ? "-0800" : "PST");   break;
318         default:
319             if (pd->zone >= 0)
320                  sprintf (c, "+%02d%02d",  pd->zone/60,  pd->zone%60);
321             else sprintf (c, "-%02d%02d", -pd->zone/60, -pd->zone%60);
322     }
323
324     return answer;
325 }
326
327 /* char *uucp_date_string (pd) struct parseddate *pd;
328  *     Generate a character string representing a date and time in the
329  *     UUCP mail format.  A value of NULL is returned if "pd" does not
330  *     contain all necessary data fields.  (NOTE:  The returned value
331  *     points to static data whose contents are overwritten by each
332  *     call.)
333  */
334 char *
335 uucp_date_string (pd)
336     register struct parseddate *pd;
337 {   register char *c;
338     static char answer[50];
339
340     /* Check the day of the month and compute the day of the week. */
341     compute_unixtime (pd);
342
343     /* Make sure all required fields are present. */
344     if (pd->year < 0 || pd->month < 0 || pd->day < 0
345         || pd->hour < 0 || pd->minute < 0
346         || pd->zone == -1 || pd->zone <= -1440 || pd->zone >= 1440)
347         return NULL;            /* impossible to generate string */
348
349     /* Generate the answer string. */
350     sprintf (answer,
351              "%.3s %.3s %d %02d:%02d",
352              dayname + 3*pd->weekday,
353              monthname + 3*(pd->month-1), pd->day,
354              pd->hour, pd->minute);
355     c = answer + strlen (answer);
356     if (pd->second >= 0) sprintf (c, ":%02d", pd->second), c += 3;
357     switch (pd->zone)
358     {   /* NOTE:  Only zone abbreviations in RFC822 are used here. */
359         case    0: strcpy (c, pd->dst ? "+0000" : "-GMT");   break;
360         case -240: strcpy (c, pd->dst ? "-EDT"  : "-0400"); break;
361         case -300: strcpy (c, pd->dst ? "-CDT"  : "-EST");   break;
362         case -360: strcpy (c, pd->dst ? "-MDT"  : "-CST");   break;
363         case -420: strcpy (c, pd->dst ? "-PDT"  : "-MST");   break;
364         case -480: strcpy (c, pd->dst ? "-0800" : "-PST");   break;
365         default:
366             if (pd->zone >= 0)
367                  sprintf (c, "+%02d%02d",  pd->zone/60,  pd->zone%60);
368             else sprintf (c, "-%02d%02d", -pd->zone/60, -pd->zone%60);
369     }
370     c = answer + strlen (answer);
371     sprintf (c, " %d", pd->year);
372
373     return answer;
374 }