]> diplodocus.org Git - nmh/blob - sbr/datetime.c
Add support for setting the environment variable MH_TEST_NOCLEANUP to
[nmh] / sbr / datetime.c
1 /*
2 * datetime.c -- functions for manipulating RFC 5545 date-time values
3 *
4 * This code is Copyright (c) 2014, by the authors of nmh.
5 * See the COPYRIGHT file in the root directory of the nmh
6 * distribution for complete copyright information.
7 */
8
9 #include "h/mh.h"
10 #include "h/icalendar.h"
11 #include <h/fmt_scan.h>
12 #include "h/tws.h"
13 #include "h/utils.h"
14
15 /*
16 * This doesn't try to support all of the myriad date-time formats
17 * allowed by RFC 5545. It is only used for viewing date-times,
18 * so that shouldn't be a problem: if a particular format can't
19 * be handled by this code, just present it to the user in its
20 * original form.
21 *
22 * And, this assumes a valid iCalendar input file. E.g, it
23 * doesn't check that each BEGIN has a matching END and vice
24 * versa. That should be done in the parser, though it currently
25 * isn't.
26 */
27
28 typedef struct tzparams {
29 /* Pointers to values in parse tree.
30 * TZOFFSETFROM is used to calculate the absolute time at which
31 * the transition to a given observance takes place.
32 * TZOFFSETTO is the timezone offset from UTC. Both are in HHmm
33 * format. */
34 char *offsetfrom, *offsetto;
35 const char *dtstart;
36 const char *rrule;
37
38 /* This is only used to make sure that timezone applies. And not
39 always, because if the timezone DTSTART is before the epoch, we
40 don't try to compare to it. */
41 time_t start_dt; /* in seconds since epoch */
42 } tzparams;
43
44 struct tzdesc {
45 char *tzid;
46
47 /* The following are translations of the pieces of RRULE and DTSTART
48 into seconds from beginning of year. */
49 tzparams standard_params;
50 tzparams daylight_params;
51
52 struct tzdesc *next;
53 };
54
55 /*
56 * Parse a datetime of the form YYYYMMDDThhmmss and a string
57 * representation of the timezone in units of [+-]hhmm and load the
58 * struct tws.
59 */
60 static int
61 parse_datetime (const char *datetime, const char *zone, int dst,
62 struct tws *tws) {
63 char utc_indicator;
64 int form_1 = 0;
65 int items_matched =
66 sscanf (datetime, "%4d%2d%2dT%2d%2d%2d%c",
67 &tws->tw_year, &tws->tw_mon, &tws->tw_mday,
68 &tws->tw_hour, &tws->tw_min, &tws->tw_sec,
69 &utc_indicator);
70 tws->tw_flags = TW_NULL;
71
72 if (items_matched == 7) {
73 /* The 'Z' must be capital according to RFC 5545 Sec. 3.3.5. */
74 if (utc_indicator != 'Z') {
75 advise (NULL, "%s has invalid timezone indicator of 0x%x",
76 datetime, utc_indicator);
77 return NOTOK;
78 }
79 } else if (zone == NULL) {
80 form_1 = 1;
81 }
82
83 if (items_matched >= 6) {
84 int offset = atoi (zone ? zone : "0");
85
86 /* struct tws defines tw_mon over [0, 11]. */
87 --tws->tw_mon;
88
89 set_dotw (tws);
90 /* set_dotw() sets TW_SIMP. Replace that with TW_SEXP so that
91 dasctime() outputs the dotw before the date instead of after. */
92 tws->tw_flags &= ~TW_SDAY, tws->tw_flags |= TW_SEXP;
93
94 /* For the call to dmktime():
95 - don't need tw_yday
96 - tw_clock must be 0 on entry, and is set by dmktime()
97 - the only flag in tw_flags used is TW_DST
98 */
99 tws->tw_yday = tws->tw_clock = 0;
100 tws->tw_zone = 60 * (offset / 100) + offset % 100;
101 if (dst) {
102 tws->tw_zone -= 60; /* per dlocaltime() */
103 tws->tw_flags |= TW_DST;
104 }
105 /* dmktime() just sets tws->tw_clock. */
106 (void) dmktime (tws);
107
108 if (! form_1) {
109 /* Set TW_SZEXP so that dasctime outputs timezone, except
110 with local time (Form #1). */
111 tws->tw_flags |= TW_SZEXP;
112
113 /* Convert UTC time to time in local timezone. However,
114 don't try for years before 1970 because dlocatime()
115 doesn't handle them well. dlocaltime() will succeed if
116 tws->tw_clock is nonzero. */
117 if (tws->tw_year >= 1970 && tws->tw_clock > 0) {
118 const int was_dst = tws->tw_flags & TW_DST;
119
120 *tws = *dlocaltime (&tws->tw_clock);
121 if (was_dst && ! (tws->tw_flags & TW_DST)) {
122 /* dlocaltime() changed the DST flag from 1 to 0,
123 which means the time is in the hour (assumed to
124 be one hour) that is lost in the transition to
125 DST. So per RFC 5545 Sec. 3.3.5, "the
126 DATE-TIME value is interpreted using the UTC
127 offset before the gap in local times." In
128 other words, add an hour to it.
129 No adjustment is necessary for the transition
130 from DST to standard time, because dasctime()
131 shows the first occurrence of the time. */
132 tws->tw_clock += 3600;
133 *tws = *dlocaltime (&tws->tw_clock);
134 }
135 }
136 }
137
138 return OK;
139 } else {
140 return NOTOK;
141 }
142 }
143
144 tzdesc_t
145 load_timezones (const contentline *clines) {
146 tzdesc_t timezones = NULL, timezone = NULL;
147 int in_vtimezone, in_standard, in_daylight;
148 tzparams *params = NULL;
149 const contentline *node;
150
151 /* Interpret each VTIMEZONE section. */
152 in_vtimezone = in_standard = in_daylight = 0;
153 for (node = clines; node; node = node->next) {
154 /* node->name will be NULL if the line was "deleted". */
155 if (! node->name) { continue; }
156
157 if (in_daylight || in_standard) {
158 if (! strcasecmp ("END", node->name) &&
159 ((in_standard && ! strcasecmp ("STANDARD", node->value)) ||
160 (in_daylight && ! strcasecmp ("DAYLIGHT", node->value)))) {
161 struct tws tws;
162
163 if (in_standard) { in_standard = 0; }
164 else if (in_daylight) { in_daylight = 0; }
165 if (parse_datetime (params->dtstart, params->offsetfrom,
166 in_daylight ? 1 : 0,
167 &tws) == OK) {
168 if (tws.tw_year >= 1970) {
169 /* dmktime() falls apart for, e.g., the year 1601. */
170 params->start_dt = tws.tw_clock;
171 }
172 } else {
173 advise (NULL, "failed to parse start time %s for %s",
174 params->dtstart,
175 in_standard ? "standard" : "daylight");
176 return NULL;
177 }
178 params = NULL;
179 } else if (! strcasecmp ("DTSTART", node->name)) {
180 /* Save DTSTART for use after getting TZOFFSETFROM. */
181 params->dtstart = node->value;
182 } else if (! strcasecmp ("TZOFFSETFROM", node->name)) {
183 params->offsetfrom = node->value;
184 } else if (! strcasecmp ("TZOFFSETTO", node->name)) {
185 params->offsetto = node->value;
186 } else if (! strcasecmp ("RRULE", node->name)) {
187 params->rrule = node->value;
188 }
189 } else if (in_vtimezone) {
190 if (! strcasecmp ("END", node->name) &&
191 ! strcasecmp ("VTIMEZONE", node->value)) {
192 in_vtimezone = 0;
193 } else if (! strcasecmp ("BEGIN", node->name) &&
194 ! strcasecmp ("STANDARD", node->value)) {
195 in_standard = 1;
196 params = &timezone->standard_params;
197 } else if (! strcasecmp ("BEGIN", node->name) &&
198 ! strcasecmp ("DAYLIGHT", node->value)) {
199 in_daylight = 1;
200 params = &timezone->daylight_params;
201 } else if (! strcasecmp ("TZID", node->name)) {
202 timezone->tzid = strdup (node->value);
203 }
204 } else {
205 if (! strcasecmp ("BEGIN", node->name) &&
206 ! strcasecmp ("VTIMEZONE", node->value)) {
207
208 in_vtimezone = 1;
209 timezone = mh_xcalloc (1, sizeof (struct tzdesc));
210 if (timezones) {
211 tzdesc_t t;
212
213 for (t = timezones; t && t->next; t = t->next) { continue; }
214 /* The loop terminated at, not after, the last
215 timezones node. */
216 t->next = timezone;
217 } else {
218 timezones = timezone;
219 }
220 }
221 }
222 }
223
224 return timezones;
225 }
226
227 void
228 free_timezones (tzdesc_t timezone) {
229 tzdesc_t next;
230
231 for ( ; timezone; timezone = next) {
232 free (timezone->tzid);
233 next = timezone->next;
234 free (timezone);
235 }
236 }
237
238 /*
239 * Convert time to local timezone, accounting for daylight saving time:
240 * - Detect which type of datetime the node contains:
241 * Form #1: DATE WITH LOCAL TIME
242 * Form #2: DATE WITH UTC TIME
243 * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
244 * - Convert value to local time in seconds since epoch.
245 * - If there's a DST in the timezone, convert its start and end
246 * date-times to local time in seconds, also. Then determine
247 * if the value is between them, and therefore DST. Otherwise, it's
248 * not.
249 * - Format the time value.
250 */
251
252 /*
253 * Given a recurrence rule and year, calculate its time in seconds
254 * from 01 January UTC of the year.
255 */
256 time_t
257 rrule_clock (const char *rrule, const char *starttime, const char *zone,
258 unsigned int year) {
259 time_t clock = 0;
260
261 if (nmh_strcasestr (rrule, "FREQ=YEARLY;INTERVAL=1")) {
262 struct tws *tws;
263 const char *cp;
264 int wday = -1, month = -1;
265 int specific_day = 1; /* BYDAY integer (prefix) */
266 char buf[32];
267 int day;
268
269 if ((cp = nmh_strcasestr (rrule, "BYDAY="))) {
270 cp += 6;
271 /* BYDAY integers must be ASCII. */
272 if (*cp == '+') { ++cp; } /* +n specific day; don't support '-' */
273 else if (*cp == '-') { goto fail; }
274
275 if (isdigit ((unsigned char) *cp)) { specific_day = *cp++ - 0x30; }
276
277 if (! strncasecmp (cp, "SU", 2)) { wday = 0; }
278 else if (! strncasecmp (cp, "MO", 2)) { wday = 1; }
279 else if (! strncasecmp (cp, "TU", 2)) { wday = 2; }
280 else if (! strncasecmp (cp, "WE", 2)) { wday = 3; }
281 else if (! strncasecmp (cp, "TH", 2)) { wday = 4; }
282 else if (! strncasecmp (cp, "FR", 2)) { wday = 5; }
283 else if (! strncasecmp (cp, "SA", 2)) { wday = 6; }
284 }
285 if ((cp = nmh_strcasestr (rrule, "BYMONTH="))) {
286 month = atoi (cp + 8);
287 }
288
289 for (day = 1; day <= 7; ++day) {
290 /* E.g, 11-01-2014 02:00:00-0400 */
291 snprintf (buf, sizeof buf, "%02d-%02d-%04u %.2s:%.2s:%.2s%s",
292 month, day + 7 * (specific_day-1), year,
293 starttime, starttime + 2, starttime + 4,
294 zone ? zone : "0000");
295 if ((tws = dparsetime (buf))) {
296 if (! (tws->tw_flags & (TW_SEXP|TW_SIMP))) { set_dotw (tws); }
297
298 if (tws->tw_wday == wday) {
299 /* Found the day specified in the RRULE. */
300 break;
301 }
302 }
303 }
304
305 if (day <= 7) {
306 clock = tws->tw_clock;
307 }
308 }
309
310 fail:
311 if (clock == 0) {
312 admonish (NULL,
313 "Unsupported RRULE format: %s, assume local timezone",
314 rrule);
315 }
316
317 return clock;
318 }
319
320 char *
321 format_datetime (tzdesc_t timezones, const contentline *node) {
322 param_list *p;
323 char *dt_timezone = NULL;
324 int dst = 0;
325 struct tws tws[2]; /* [standard, daylight] */
326 tzdesc_t tz;
327 char *tp_std, *tp_dst, *tp_dt;
328
329 /* Extract the timezone, if specified (RFC 5545 Sec. 3.3.5 Form #3). */
330 for (p = node->params; p && p->param_name; p = p->next) {
331 if (! strcasecmp (p->param_name, "TZID") && p->values) {
332 dt_timezone = p->values->value;
333 break;
334 }
335 }
336
337 if (! dt_timezone) {
338 /* Form #1: DATE WITH LOCAL TIME, i.e., no time zone, or
339 Form #2: DATE WITH UTC TIME */
340 if (parse_datetime (node->value, NULL, 0, &tws[0]) == OK) {
341 return strdup (dasctime (&tws[0], 0));
342 } else {
343 advise (NULL, "unable to parse datetime %s", node->value);
344 return NULL;
345 }
346 }
347
348 /*
349 * must be
350 * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
351 */
352
353 /* Find the corresponding tzdesc. */
354 for (tz = timezones; dt_timezone && tz; tz = tz->next) {
355 /* Property parameter values are case insenstive (RFC 5545
356 Sec. 2) and time zone identifiers are property parameters
357 (RFC 5545 Sec. 3.8.2.4), though it would seem odd to use
358 different case in the same file for identifiers that are
359 supposed to be the same. */
360 if (tz->tzid && ! strcasecmp (dt_timezone, tz->tzid)) { break; }
361 }
362
363 if (! tz) {
364 advise (NULL, "did not find VTIMEZONE section for %s", dt_timezone);
365 return NULL;
366 }
367
368 /* Determine if it's Daylight Saving. */
369 tp_std = strchr (tz->standard_params.dtstart, 'T');
370 tp_dt = strchr (node->value, 'T');
371
372 if (tz->daylight_params.dtstart) {
373 tp_dst = strchr (tz->daylight_params.dtstart, 'T');
374 } else {
375 /* No DAYLIGHT section. */
376 tp_dst = NULL;
377 dst = 0;
378 }
379
380 if (tp_std && tp_dt) {
381 time_t transition[2] = { 0, 0 }; /* [standard, daylight] */
382 time_t dt[2]; /* [standard, daylight] */
383 unsigned int year;
384 char buf[5];
385
386 /* Datetime is form YYYYMMDDThhmmss. Extract year. */
387 memcpy (buf, node->value, sizeof buf - 1);
388 buf[sizeof buf - 1] = '\0';
389 year = atoi (buf);
390
391 if (tz->standard_params.rrule) {
392 /* +1 to skip the T before the time */
393 transition[0] =
394 rrule_clock (tz->standard_params.rrule, tp_std + 1,
395 tz->standard_params.offsetfrom, year);
396 }
397 if (tp_dst && tz->daylight_params.rrule) {
398 /* +1 to skip the T before the time */
399 transition[1] =
400 rrule_clock (tz->daylight_params.rrule, tp_dst + 1,
401 tz->daylight_params.offsetfrom, year);
402 }
403
404 if (transition[0] < transition[1]) {
405 advise (NULL, "format_datetime() requires that daylight "
406 "saving time transition precede standard time "
407 "transition");
408 return NULL;
409 }
410
411 if (parse_datetime (node->value, tz->standard_params.offsetto,
412 0, &tws[0]) == OK) {
413 dt[0] = tws[0].tw_clock;
414 } else {
415 advise (NULL, "unable to parse datetime %s", node->value);
416 return NULL;
417 }
418
419 if (tp_dst) {
420 if (dt[0] < transition[1]) {
421 dst = 0;
422 } else {
423 if (parse_datetime (node->value,
424 tz->daylight_params.offsetto, 1,
425 &tws[1]) == OK) {
426 dt[1] = tws[1].tw_clock;
427 } else {
428 advise (NULL, "unable to parse datetime %s",
429 node->value);
430 return NULL;
431 }
432
433 dst = dt[1] > transition[0] ? 0 : 1;
434 }
435 }
436
437 if (dst) {
438 if (tz->daylight_params.start_dt > 0 &&
439 dt[dst] < tz->daylight_params.start_dt) {
440 advise (NULL, "date-time of %s is before VTIMEZONE start "
441 "of %s", node->value,
442 tz->daylight_params.dtstart);
443 return NULL;
444 }
445 } else {
446 if (tz->standard_params.start_dt > 0 &&
447 dt[dst] < tz->standard_params.start_dt) {
448 advise (NULL, "date-time of %s is before VTIMEZONE start "
449 "of %s", node->value,
450 tz->standard_params.dtstart);
451 return NULL;
452 }
453 }
454 } else {
455 if (! tp_std) {
456 advise (NULL, "unsupported date-time format: %s",
457 tz->standard_params.dtstart);
458 return NULL;
459 }
460 if (! tp_dt) {
461 advise (NULL, "unsupported date-time format: %s", node->value);
462 return NULL;
463 }
464 }
465
466 return strdup (dasctime (&tws[dst], 0));
467 }