]>
diplodocus.org Git - nmh/blob - sbr/datetime.c
1 /* datetime.c -- functions for manipulating RFC 5545 date-time values
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.
9 #include "h/icalendar.h"
10 #include <h/fmt_scan.h>
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
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
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
33 char *offsetfrom
, *offsetto
;
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 */
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
;
55 * Parse a datetime of the form YYYYMMDDThhmmss and a string
56 * representation of the timezone in units of [+-]hhmm and load the
60 parse_datetime (const char *datetime
, const char *zone
, int dst
,
66 memset(tws
, 0, sizeof *tws
);
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
,
72 tws
->tw_flags
= TW_NULL
;
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
);
81 } else if (zone
== NULL
) {
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");
91 /* struct tws defines tw_mon over [0, 11]. */
94 /* Fill out rest of tws, i.e., its tw_wday and tw_flags. */
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
;
100 /* For the call to dmktime():
102 - tw_clock must be 0 on entry, and is set by dmktime()
103 - the only flag in tw_flags used is TW_DST
105 tws
->tw_yday
= tws
->tw_clock
= 0;
106 tws
->tw_zone
= 60 * (offset
/ 100) + offset
% 100;
108 tws
->tw_zone
-= 60; /* per dlocaltime() */
109 tws
->tw_flags
|= TW_DST
;
111 /* dmktime() just sets tws->tw_clock. */
112 (void) dmktime (tws
);
115 /* Set TW_SZEXP so that dasctime outputs timezone, except
116 with local time (Form #1). */
117 tws
->tw_flags
|= TW_SZEXP
;
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
;
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
);
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
;
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; }
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
)))) {
169 if (in_standard
) { in_standard
= 0; }
170 else if (in_daylight
) { in_daylight
= 0; }
171 if (parse_datetime (params
->dtstart
, params
->offsetfrom
,
174 if (tws
.tw_year
>= 1970) {
175 /* dmktime() falls apart for, e.g., the year 1601. */
176 params
->start_dt
= tws
.tw_clock
;
179 inform("failed to parse start time %s for %s",
181 in_standard
? "standard" : "daylight");
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
;
195 } else if (in_vtimezone
) {
196 if (! strcasecmp ("END", node
->name
) &&
197 ! strcasecmp ("VTIMEZONE", node
->value
)) {
199 } else if (! strcasecmp ("BEGIN", node
->name
) &&
200 ! strcasecmp ("STANDARD", node
->value
)) {
202 params
= &timezone
->standard_params
;
203 } else if (! strcasecmp ("BEGIN", node
->name
) &&
204 ! strcasecmp ("DAYLIGHT", node
->value
)) {
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
;
215 if (! strcasecmp ("BEGIN", node
->name
) &&
216 ! strcasecmp ("VTIMEZONE", node
->value
)) {
223 for (t
= timezones
; t
&& t
->next
; t
= t
->next
) { continue; }
224 /* The loop terminated at, not after, the last
228 timezones
= timezone
;
238 free_timezones (tzdesc_t timezone
) {
241 for ( ; timezone
; timezone
= next
) {
242 free (timezone
->tzid
);
243 next
= timezone
->next
;
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
259 * - Format the time value.
263 * Given a recurrence rule and year, calculate its time in seconds
264 * from 01 January UTC of the year.
267 rrule_clock (const char *rrule
, const char *starttime
, const char *zone
,
271 if (nmh_strcasestr (rrule
, "FREQ=YEARLY;INTERVAL=1") ||
272 (nmh_strcasestr (rrule
, "FREQ=YEARLY") && nmh_strcasestr(rrule
, "INTERVAL") == NULL
)) {
275 int wday
= -1, month
= -1;
276 int specific_day
= 1; /* BYDAY integer (prefix) */
280 if ((cp
= nmh_strcasestr (rrule
, "BYDAY="))) {
282 /* BYDAY integers must be ASCII. */
283 if (*cp
== '+') { ++cp
; } /* +n specific day; don't support '-' */
284 else if (*cp
== '-') { goto fail
; }
286 if (isdigit ((unsigned char) *cp
)) { specific_day
= *cp
++ - 0x30; }
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; }
296 if ((cp
= nmh_strcasestr (rrule
, "BYMONTH="))) {
297 month
= atoi (cp
+ 8);
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
); }
309 if (tws
->tw_wday
== wday
) {
310 /* Found the day specified in the RRULE. */
317 clock
= tws
->tw_clock
;
323 inform("Unsupported RRULE format: %s, assume local timezone, continuing...",
331 format_datetime (tzdesc_t timezones
, const contentline
*node
) {
333 char *dt_timezone
= NULL
;
335 struct tws tws
[2]; /* [standard, daylight] */
337 char *tp_std
, *tp_dst
, *tp_dt
;
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
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
);
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));
361 inform("unable to parse datetime %s", node
->value
);
367 * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
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; }
383 inform("did not find VTIMEZONE section for %s", dt_timezone
);
388 /* Determine if it's Daylight Saving. */
389 tp_std
= strchr (tz
->standard_params
.dtstart
, 'T');
390 tp_dt
= strchr (node
->value
, 'T');
392 if (tz
->daylight_params
.dtstart
) {
393 tp_dst
= strchr (tz
->daylight_params
.dtstart
, 'T');
395 /* No DAYLIGHT section. */
400 if (tp_std
&& tp_dt
) {
401 time_t transition
[2] = { 0, 0 }; /* [standard, daylight] */
402 time_t dt
[2]; /* [standard, daylight] */
406 /* Datetime is form YYYYMMDDThhmmss. Extract year. */
407 memcpy (buf
, node
->value
, sizeof buf
- 1);
408 buf
[sizeof buf
- 1] = '\0';
411 if (tz
->standard_params
.rrule
) {
412 /* +1 to skip the T before the time */
414 rrule_clock (tz
->standard_params
.rrule
, tp_std
+ 1,
415 tz
->standard_params
.offsetfrom
, year
);
417 if (tp_dst
&& tz
->daylight_params
.rrule
) {
418 /* +1 to skip the T before the time */
420 rrule_clock (tz
->daylight_params
.rrule
, tp_dst
+ 1,
421 tz
->daylight_params
.offsetfrom
, year
);
424 if (transition
[0] < transition
[1]) {
425 inform("format_datetime() requires that daylight "
426 "saving time transition precede standard time "
431 if (parse_datetime (node
->value
, tz
->standard_params
.offsetto
,
433 dt
[0] = tws
[0].tw_clock
;
435 inform("unable to parse datetime %s", node
->value
);
440 if (dt
[0] < transition
[1]) {
443 if (parse_datetime (node
->value
,
444 tz
->daylight_params
.offsetto
, 1,
446 dt
[1] = tws
[1].tw_clock
;
448 inform("unable to parse datetime %s",
453 dst
= dt
[1] > transition
[0] ? 0 : 1;
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
);
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
);
476 inform("unsupported date-time format: %s",
477 tz
->standard_params
.dtstart
);
481 inform("unsupported date-time format: %s", node
->value
);
486 return strdup (dasctime (&tws
[dst
], 0));