]>
diplodocus.org Git - nmh/blob - sbr/datetime.c
2 * datetime.c -- functions for manipulating RFC 5545 date-time values
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.
10 #include "h/icalendar.h"
11 #include <h/fmt_scan.h>
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
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
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
34 char *offsetfrom
, *offsetto
;
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 */
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
;
56 * Parse a datetime of the form YYYYMMDDThhmmss and a string
57 * representation of the timezone in units of [+-]hhmm and load the
61 parse_datetime (const char *datetime
, const char *zone
, int dst
,
67 memset(tws
, 0, sizeof *tws
);
69 sscanf (datetime
, "%4d%2d%2dT%2d%2d%2d%c",
70 &tws
->tw_year
, &tws
->tw_mon
, &tws
->tw_mday
,
71 &tws
->tw_hour
, &tws
->tw_min
, &tws
->tw_sec
,
73 tws
->tw_flags
= TW_NULL
;
75 if (items_matched
== 7) {
76 /* The 'Z' must be capital according to RFC 5545 Sec. 3.3.5. */
77 if (utc_indicator
!= 'Z') {
78 advise (NULL
, "%s has invalid timezone indicator of 0x%x",
79 datetime
, utc_indicator
);
82 } else if (zone
== NULL
) {
86 /* items_matched of 3 is for, e.g., 20151230. Assume that means
87 the entire day. The time fields of the tws struct were
88 initialized to 0 by the memset() above. */
89 if (items_matched
>= 6 || items_matched
== 3) {
90 int offset
= atoi (zone
? zone
: "0");
92 /* struct tws defines tw_mon over [0, 11]. */
95 /* Fill out rest of tws, i.e., its tw_wday and tw_flags. */
97 /* set_dotw() sets TW_SIMP. Replace that with TW_SEXP so that
98 dasctime() outputs the dotw before the date instead of after. */
99 tws
->tw_flags
&= ~TW_SDAY
, tws
->tw_flags
|= TW_SEXP
;
101 /* For the call to dmktime():
103 - tw_clock must be 0 on entry, and is set by dmktime()
104 - the only flag in tw_flags used is TW_DST
106 tws
->tw_yday
= tws
->tw_clock
= 0;
107 tws
->tw_zone
= 60 * (offset
/ 100) + offset
% 100;
109 tws
->tw_zone
-= 60; /* per dlocaltime() */
110 tws
->tw_flags
|= TW_DST
;
112 /* dmktime() just sets tws->tw_clock. */
113 (void) dmktime (tws
);
116 /* Set TW_SZEXP so that dasctime outputs timezone, except
117 with local time (Form #1). */
118 tws
->tw_flags
|= TW_SZEXP
;
120 /* Convert UTC time to time in local timezone. However,
121 don't try for years before 1970 because dlocatime()
122 doesn't handle them well. dlocaltime() will succeed if
123 tws->tw_clock is nonzero. */
124 if (tws
->tw_year
>= 1970 && tws
->tw_clock
> 0) {
125 const int was_dst
= tws
->tw_flags
& TW_DST
;
127 *tws
= *dlocaltime (&tws
->tw_clock
);
128 if (was_dst
&& ! (tws
->tw_flags
& TW_DST
)) {
129 /* dlocaltime() changed the DST flag from 1 to 0,
130 which means the time is in the hour (assumed to
131 be one hour) that is lost in the transition to
132 DST. So per RFC 5545 Sec. 3.3.5, "the
133 DATE-TIME value is interpreted using the UTC
134 offset before the gap in local times." In
135 other words, add an hour to it.
136 No adjustment is necessary for the transition
137 from DST to standard time, because dasctime()
138 shows the first occurrence of the time. */
139 tws
->tw_clock
+= 3600;
140 *tws
= *dlocaltime (&tws
->tw_clock
);
152 load_timezones (const contentline
*clines
) {
153 tzdesc_t timezones
= NULL
, timezone
= NULL
;
154 int in_vtimezone
, in_standard
, in_daylight
;
155 tzparams
*params
= NULL
;
156 const contentline
*node
;
158 /* Interpret each VTIMEZONE section. */
159 in_vtimezone
= in_standard
= in_daylight
= 0;
160 for (node
= clines
; node
; node
= node
->next
) {
161 /* node->name will be NULL if the line was "deleted". */
162 if (! node
->name
) { continue; }
164 if (in_daylight
|| in_standard
) {
165 if (! strcasecmp ("END", node
->name
) &&
166 ((in_standard
&& ! strcasecmp ("STANDARD", node
->value
)) ||
167 (in_daylight
&& ! strcasecmp ("DAYLIGHT", node
->value
)))) {
170 if (in_standard
) { in_standard
= 0; }
171 else if (in_daylight
) { in_daylight
= 0; }
172 if (parse_datetime (params
->dtstart
, params
->offsetfrom
,
175 if (tws
.tw_year
>= 1970) {
176 /* dmktime() falls apart for, e.g., the year 1601. */
177 params
->start_dt
= tws
.tw_clock
;
180 advise (NULL
, "failed to parse start time %s for %s",
182 in_standard
? "standard" : "daylight");
186 } else if (! strcasecmp ("DTSTART", node
->name
)) {
187 /* Save DTSTART for use after getting TZOFFSETFROM. */
188 params
->dtstart
= node
->value
;
189 } else if (! strcasecmp ("TZOFFSETFROM", node
->name
)) {
190 params
->offsetfrom
= node
->value
;
191 } else if (! strcasecmp ("TZOFFSETTO", node
->name
)) {
192 params
->offsetto
= node
->value
;
193 } else if (! strcasecmp ("RRULE", node
->name
)) {
194 params
->rrule
= node
->value
;
196 } else if (in_vtimezone
) {
197 if (! strcasecmp ("END", node
->name
) &&
198 ! strcasecmp ("VTIMEZONE", node
->value
)) {
200 } else if (! strcasecmp ("BEGIN", node
->name
) &&
201 ! strcasecmp ("STANDARD", node
->value
)) {
203 params
= &timezone
->standard_params
;
204 } else if (! strcasecmp ("BEGIN", node
->name
) &&
205 ! strcasecmp ("DAYLIGHT", node
->value
)) {
207 params
= &timezone
->daylight_params
;
208 } else if (! strcasecmp ("TZID", node
->name
)) {
209 /* See comment below in format_datetime() about removing any enclosing quotes from a
210 timezone identifier. */
211 char *buf
= mh_xmalloc(strlen(node
->value
) + 1);
212 unquote_string(node
->value
, buf
);
213 timezone
->tzid
= buf
;
216 if (! strcasecmp ("BEGIN", node
->name
) &&
217 ! strcasecmp ("VTIMEZONE", node
->value
)) {
224 for (t
= timezones
; t
&& t
->next
; t
= t
->next
) { continue; }
225 /* The loop terminated at, not after, the last
229 timezones
= timezone
;
239 free_timezones (tzdesc_t timezone
) {
242 for ( ; timezone
; timezone
= next
) {
243 free (timezone
->tzid
);
244 next
= timezone
->next
;
250 * Convert time to local timezone, accounting for daylight saving time:
251 * - Detect which type of datetime the node contains:
252 * Form #1: DATE WITH LOCAL TIME
253 * Form #2: DATE WITH UTC TIME
254 * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
255 * - Convert value to local time in seconds since epoch.
256 * - If there's a DST in the timezone, convert its start and end
257 * date-times to local time in seconds, also. Then determine
258 * if the value is between them, and therefore DST. Otherwise, it's
260 * - Format the time value.
264 * Given a recurrence rule and year, calculate its time in seconds
265 * from 01 January UTC of the year.
268 rrule_clock (const char *rrule
, const char *starttime
, const char *zone
,
272 if (nmh_strcasestr (rrule
, "FREQ=YEARLY;INTERVAL=1") ||
273 (nmh_strcasestr (rrule
, "FREQ=YEARLY") && nmh_strcasestr(rrule
, "INTERVAL") == NULL
)) {
276 int wday
= -1, month
= -1;
277 int specific_day
= 1; /* BYDAY integer (prefix) */
281 if ((cp
= nmh_strcasestr (rrule
, "BYDAY="))) {
283 /* BYDAY integers must be ASCII. */
284 if (*cp
== '+') { ++cp
; } /* +n specific day; don't support '-' */
285 else if (*cp
== '-') { goto fail
; }
287 if (isdigit ((unsigned char) *cp
)) { specific_day
= *cp
++ - 0x30; }
289 if (! strncasecmp (cp
, "SU", 2)) { wday
= 0; }
290 else if (! strncasecmp (cp
, "MO", 2)) { wday
= 1; }
291 else if (! strncasecmp (cp
, "TU", 2)) { wday
= 2; }
292 else if (! strncasecmp (cp
, "WE", 2)) { wday
= 3; }
293 else if (! strncasecmp (cp
, "TH", 2)) { wday
= 4; }
294 else if (! strncasecmp (cp
, "FR", 2)) { wday
= 5; }
295 else if (! strncasecmp (cp
, "SA", 2)) { wday
= 6; }
297 if ((cp
= nmh_strcasestr (rrule
, "BYMONTH="))) {
298 month
= atoi (cp
+ 8);
301 for (day
= 1; day
<= 7; ++day
) {
302 /* E.g, 11-01-2014 02:00:00-0400 */
303 snprintf (buf
, sizeof buf
, "%02d-%02d-%04u %.2s:%.2s:%.2s%s",
304 month
, day
+ 7 * (specific_day
-1), year
,
305 starttime
, starttime
+ 2, starttime
+ 4,
306 zone
? zone
: "0000");
307 if ((tws
= dparsetime (buf
))) {
308 if (! (tws
->tw_flags
& (TW_SEXP
|TW_SIMP
))) { set_dotw (tws
); }
310 if (tws
->tw_wday
== wday
) {
311 /* Found the day specified in the RRULE. */
318 clock
= tws
->tw_clock
;
325 "Unsupported RRULE format: %s, assume local timezone",
333 format_datetime (tzdesc_t timezones
, const contentline
*node
) {
335 char *dt_timezone
= NULL
;
337 struct tws tws
[2]; /* [standard, daylight] */
339 char *tp_std
, *tp_dst
, *tp_dt
;
341 /* Extract the timezone, if specified (RFC 5545 Sec. 3.3.5 Form #3). */
342 for (p
= node
->params
; p
&& p
->param_name
; p
= p
->next
) {
343 if (! strcasecmp (p
->param_name
, "TZID") && p
->values
) {
344 /* Remove any enclosing quotes from the timezone identifier. I don't believe that it's
345 legal for it to be quoted, according to RFC 5545 ยง 3.2.19:
346 tzidparam = "TZID" "=" [tzidprefix] paramtext
348 where paramtext includes SAFE-CHAR, which specifically excludes DQUOTE. But we'll
349 be generous and strip quotes. */
350 char *buf
= mh_xmalloc(strlen(p
->values
->value
) + 1);
351 unquote_string(p
->values
->value
, buf
);
358 /* Form #1: DATE WITH LOCAL TIME, i.e., no time zone, or
359 Form #2: DATE WITH UTC TIME */
360 if (parse_datetime (node
->value
, NULL
, 0, &tws
[0]) == OK
) {
361 return strdup (dasctime (&tws
[0], 0));
363 advise (NULL
, "unable to parse datetime %s", node
->value
);
369 * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
372 /* Find the corresponding tzdesc. */
373 for (tz
= timezones
; dt_timezone
&& tz
; tz
= tz
->next
) {
374 /* Property parameter values are case insenstive (RFC 5545
375 Sec. 2) and time zone identifiers are property parameters
376 (RFC 5545 Sec. 3.8.2.4), though it would seem odd to use
377 different case in the same file for identifiers that are
378 supposed to be the same. */
379 if (tz
->tzid
&& ! strcasecmp (dt_timezone
, tz
->tzid
)) { break; }
385 advise (NULL
, "did not find VTIMEZONE section for %s", dt_timezone
);
390 /* Determine if it's Daylight Saving. */
391 tp_std
= strchr (tz
->standard_params
.dtstart
, 'T');
392 tp_dt
= strchr (node
->value
, 'T');
394 if (tz
->daylight_params
.dtstart
) {
395 tp_dst
= strchr (tz
->daylight_params
.dtstart
, 'T');
397 /* No DAYLIGHT section. */
402 if (tp_std
&& tp_dt
) {
403 time_t transition
[2] = { 0, 0 }; /* [standard, daylight] */
404 time_t dt
[2]; /* [standard, daylight] */
408 /* Datetime is form YYYYMMDDThhmmss. Extract year. */
409 memcpy (buf
, node
->value
, sizeof buf
- 1);
410 buf
[sizeof buf
- 1] = '\0';
413 if (tz
->standard_params
.rrule
) {
414 /* +1 to skip the T before the time */
416 rrule_clock (tz
->standard_params
.rrule
, tp_std
+ 1,
417 tz
->standard_params
.offsetfrom
, year
);
419 if (tp_dst
&& tz
->daylight_params
.rrule
) {
420 /* +1 to skip the T before the time */
422 rrule_clock (tz
->daylight_params
.rrule
, tp_dst
+ 1,
423 tz
->daylight_params
.offsetfrom
, year
);
426 if (transition
[0] < transition
[1]) {
427 advise (NULL
, "format_datetime() requires that daylight "
428 "saving time transition precede standard time "
433 if (parse_datetime (node
->value
, tz
->standard_params
.offsetto
,
435 dt
[0] = tws
[0].tw_clock
;
437 advise (NULL
, "unable to parse datetime %s", node
->value
);
442 if (dt
[0] < transition
[1]) {
445 if (parse_datetime (node
->value
,
446 tz
->daylight_params
.offsetto
, 1,
448 dt
[1] = tws
[1].tw_clock
;
450 advise (NULL
, "unable to parse datetime %s",
455 dst
= dt
[1] > transition
[0] ? 0 : 1;
460 if (tz
->daylight_params
.start_dt
> 0 &&
461 dt
[dst
] < tz
->daylight_params
.start_dt
) {
462 advise (NULL
, "date-time of %s is before VTIMEZONE start "
463 "of %s", node
->value
,
464 tz
->daylight_params
.dtstart
);
468 if (tz
->standard_params
.start_dt
> 0 &&
469 dt
[dst
] < tz
->standard_params
.start_dt
) {
470 advise (NULL
, "date-time of %s is before VTIMEZONE start "
471 "of %s", node
->value
,
472 tz
->standard_params
.dtstart
);
478 advise (NULL
, "unsupported date-time format: %s",
479 tz
->standard_params
.dtstart
);
483 advise (NULL
, "unsupported date-time format: %s", node
->value
);
488 return strdup (dasctime (&tws
[dst
], 0));