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