]>
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>
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
, bool dst
,
70 sscanf (datetime
, "%4d%2d%2dT%2d%2d%2d%c",
71 &tws
->tw_year
, &tws
->tw_mon
, &tws
->tw_mday
,
72 &tws
->tw_hour
, &tws
->tw_min
, &tws
->tw_sec
,
74 tws
->tw_flags
= TW_NULL
;
77 if (items_matched
== 7) {
78 /* The 'Z' must be capital according to RFC 5545 Sec. 3.3.5. */
79 if (utc_indicator
!= 'Z') {
80 inform("%s has invalid timezone-indicator byte: %#x",
81 datetime
, utc_indicator
);
84 } else if (zone
== NULL
) {
88 /* items_matched of 3 is for, e.g., 20151230. Assume that means
89 the entire day. The time fields of the tws struct were
90 initialized to 0 by the memset() above. */
91 if (items_matched
>= 6 || items_matched
== 3) {
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
;
100 tws
->tw_flags
|= TW_SEXP
;
102 /* For the call to dmktime():
104 - tw_clock must be 0 on entry, and is set by dmktime()
105 - the only flag in tw_flags used is TW_DST
107 tws
->tw_yday
= tws
->tw_clock
= 0;
109 int offset
= atoi(zone
);
110 tws
->tw_zone
= 60 * (offset
/ 100) + offset
% 100;
114 tws
->tw_zone
-= 60; /* per dlocaltime() */
115 tws
->tw_flags
|= TW_DST
;
117 /* dmktime() just sets tws->tw_clock. */
118 (void) dmktime (tws
);
121 /* Set TW_SZEXP so that dasctime outputs timezone, except
122 with local time (Form #1). */
123 tws
->tw_flags
|= TW_SZEXP
;
125 /* Convert UTC time to time in local timezone. However,
126 don't try for years before 1970 because dlocatime()
127 doesn't handle them well. dlocaltime() will succeed if
128 tws->tw_clock is nonzero. */
129 if (tws
->tw_year
>= 1970 && tws
->tw_clock
> 0) {
130 const int was_dst
= tws
->tw_flags
& TW_DST
;
132 *tws
= *dlocaltime (&tws
->tw_clock
);
133 if (was_dst
&& ! (tws
->tw_flags
& TW_DST
)) {
134 /* dlocaltime() changed the DST flag from 1 to 0,
135 which means the time is in the hour (assumed to
136 be one hour) that is lost in the transition to
137 DST. So per RFC 5545 Sec. 3.3.5, "the
138 DATE-TIME value is interpreted using the UTC
139 offset before the gap in local times." In
140 other words, add an hour to it.
141 No adjustment is necessary for the transition
142 from DST to standard time, because dasctime()
143 shows the first occurrence of the time. */
144 tws
->tw_clock
+= 3600;
145 *tws
= *dlocaltime (&tws
->tw_clock
);
157 load_timezones (const contentline
*clines
)
159 tzdesc_t timezones
= NULL
, timezone
= NULL
;
160 bool in_vtimezone
, in_standard
, in_daylight
;
161 tzparams
*params
= NULL
;
162 const contentline
*node
;
164 /* Interpret each VTIMEZONE section. */
165 in_vtimezone
= in_standard
= in_daylight
= false;
166 for (node
= clines
; node
; node
= node
->next
) {
167 /* node->name will be NULL if the line was "deleted". */
168 if (! node
->name
) { continue; }
170 if (in_daylight
|| in_standard
) {
171 if (! strcasecmp ("END", node
->name
) &&
172 ((in_standard
&& ! strcasecmp ("STANDARD", node
->value
)) ||
173 (in_daylight
&& ! strcasecmp ("DAYLIGHT", node
->value
)))) {
176 if (in_standard
) { in_standard
= false; }
177 else if (in_daylight
) { in_daylight
= false; }
179 if (parse_datetime(params
->dtstart
, params
->offsetfrom
,
180 in_daylight
, &tws
) != OK
) {
181 inform("failed to parse start time %s for %s",
183 in_daylight
? "daylight" : "standard");
187 if (tws
.tw_year
>= 1970) {
188 /* dmktime() falls apart for, e.g., the year 1601. */
189 params
->start_dt
= tws
.tw_clock
;
192 } else if (! strcasecmp ("DTSTART", node
->name
)) {
193 /* Save DTSTART for use after getting TZOFFSETFROM. */
194 params
->dtstart
= node
->value
;
195 } else if (! strcasecmp ("TZOFFSETFROM", node
->name
)) {
196 params
->offsetfrom
= node
->value
;
197 } else if (! strcasecmp ("TZOFFSETTO", node
->name
)) {
198 params
->offsetto
= node
->value
;
199 } else if (! strcasecmp ("RRULE", node
->name
)) {
200 params
->rrule
= node
->value
;
202 } else if (in_vtimezone
) {
203 if (! strcasecmp ("END", node
->name
) &&
204 ! strcasecmp ("VTIMEZONE", node
->value
)) {
205 in_vtimezone
= false;
206 } else if (! strcasecmp ("BEGIN", node
->name
) &&
207 ! strcasecmp ("STANDARD", node
->value
)) {
209 params
= &timezone
->standard_params
;
210 } else if (! strcasecmp ("BEGIN", node
->name
) &&
211 ! strcasecmp ("DAYLIGHT", node
->value
)) {
213 params
= &timezone
->daylight_params
;
214 } else if (! strcasecmp ("TZID", node
->name
)) {
215 /* See comment below in format_datetime() about removing any enclosing quotes from a
216 timezone identifier. */
217 char *buf
= mh_xmalloc(strlen(node
->value
) + 1);
218 unquote_string(node
->value
, buf
);
219 timezone
->tzid
= buf
;
222 if (! strcasecmp ("BEGIN", node
->name
) &&
223 ! strcasecmp ("VTIMEZONE", node
->value
)) {
230 for (t
= timezones
; t
&& t
->next
; t
= t
->next
) { continue; }
231 /* The loop terminated at, not after, the last
235 timezones
= timezone
;
245 free_timezones (tzdesc_t timezone
)
249 for ( ; timezone
; timezone
= next
) {
250 free (timezone
->tzid
);
251 next
= timezone
->next
;
257 * Convert time to local timezone, accounting for daylight saving time:
258 * - Detect which type of datetime the node contains:
259 * Form #1: DATE WITH LOCAL TIME
260 * Form #2: DATE WITH UTC TIME
261 * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
262 * - Convert value to local time in seconds since epoch.
263 * - If there's a DST in the timezone, convert its start and end
264 * date-times to local time in seconds, also. Then determine
265 * if the value is between them, and therefore DST. Otherwise, it's
267 * - Format the time value.
271 * Given a recurrence rule and year, calculate its time in seconds
272 * from 01 January UTC of the year.
275 rrule_clock (const char *rrule
, const char *starttime
, const char *zone
,
280 if (nmh_strcasestr (rrule
, "FREQ=YEARLY;INTERVAL=1") ||
281 (nmh_strcasestr (rrule
, "FREQ=YEARLY") && nmh_strcasestr(rrule
, "INTERVAL") == NULL
)) {
284 int wday
= -1, month
= -1;
285 int specific_day
= 1; /* BYDAY integer (prefix) */
289 if ((cp
= nmh_strcasestr (rrule
, "BYDAY="))) {
291 /* BYDAY integers must be ASCII. */
292 if (*cp
== '+') { ++cp
; } /* +n specific day; don't support '-' */
293 else if (*cp
== '-') { goto fail
; }
295 if (isdigit ((unsigned char) *cp
)) { specific_day
= *cp
++ - 0x30; }
297 if (! strncasecmp (cp
, "SU", 2)) { wday
= 0; }
298 else if (! strncasecmp (cp
, "MO", 2)) { wday
= 1; }
299 else if (! strncasecmp (cp
, "TU", 2)) { wday
= 2; }
300 else if (! strncasecmp (cp
, "WE", 2)) { wday
= 3; }
301 else if (! strncasecmp (cp
, "TH", 2)) { wday
= 4; }
302 else if (! strncasecmp (cp
, "FR", 2)) { wday
= 5; }
303 else if (! strncasecmp (cp
, "SA", 2)) { wday
= 6; }
305 if ((cp
= nmh_strcasestr (rrule
, "BYMONTH="))) {
306 month
= atoi (cp
+ 8);
309 for (day
= 1; day
<= 7; ++day
) {
310 /* E.g, 11-01-2014 02:00:00-0400 */
311 snprintf (buf
, sizeof buf
, "%02d-%02d-%04u %.2s:%.2s:%.2s%s",
312 month
, day
+ 7 * (specific_day
-1), year
,
313 starttime
, starttime
+ 2, starttime
+ 4,
314 zone
? zone
: "0000");
315 if ((tws
= dparsetime (buf
))) {
316 if (! (tws
->tw_flags
& (TW_SEXP
|TW_SIMP
))) { set_dotw (tws
); }
318 if (tws
->tw_wday
== wday
) {
319 /* Found the day specified in the RRULE. */
326 clock
= tws
->tw_clock
;
332 inform("Unsupported RRULE format: %s, assume local timezone, continuing...",
340 format_datetime (tzdesc_t timezones
, const contentline
*node
)
343 char *dt_timezone
= NULL
;
345 struct tws tws
[2]; /* [standard, daylight] */
347 char *tp_std
, *tp_dst
, *tp_dt
;
349 /* Extract the timezone, if specified (RFC 5545 Sec. 3.3.5 Form #3). */
350 for (p
= node
->params
; p
&& p
->param_name
; p
= p
->next
) {
351 if (! strcasecmp (p
->param_name
, "TZID") && p
->values
) {
352 /* Remove any enclosing quotes from the timezone identifier. I don't believe that it's
353 legal for it to be quoted, according to RFC 5545 ยง 3.2.19:
354 tzidparam = "TZID" "=" [tzidprefix] paramtext
356 where paramtext includes SAFE-CHAR, which specifically excludes DQUOTE. But we'll
357 be generous and strip quotes. */
358 char *buf
= mh_xmalloc(strlen(p
->values
->value
) + 1);
359 unquote_string(p
->values
->value
, buf
);
366 /* Form #1: DATE WITH LOCAL TIME, i.e., no time zone, or
367 Form #2: DATE WITH UTC TIME */
368 if (parse_datetime(node
->value
, NULL
, false, &tws
[0]) != OK
) {
369 inform("unable to parse datetime %s", node
->value
);
372 return strdup (dasctime (&tws
[0], 0));
377 * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
380 /* Find the corresponding tzdesc. */
381 for (tz
= timezones
; dt_timezone
&& tz
; tz
= tz
->next
) {
382 /* Property parameter values are case insensitive (RFC 5545
383 Sec. 2) and time zone identifiers are property parameters
384 (RFC 5545 Sec. 3.8.2.4), though it would seem odd to use
385 different case in the same file for identifiers that are
386 supposed to be the same. */
387 if (tz
->tzid
&& ! strcasecmp (dt_timezone
, tz
->tzid
)) { break; }
391 inform("did not find VTIMEZONE section for %s", dt_timezone
);
397 /* Determine if it's Daylight Saving. */
398 tp_std
= strchr (tz
->standard_params
.dtstart
, 'T');
399 tp_dt
= strchr (node
->value
, 'T');
401 if (tz
->daylight_params
.dtstart
) {
402 tp_dst
= strchr (tz
->daylight_params
.dtstart
, 'T');
404 /* No DAYLIGHT section. */
409 if (tp_std
&& tp_dt
) {
410 time_t transition
[2] = { 0, 0 }; /* [standard, daylight] */
411 time_t dt
[2]; /* [standard, daylight] */
415 /* Datetime is form YYYYMMDDThhmmss. Extract year. */
416 memcpy (buf
, node
->value
, sizeof buf
- 1);
417 buf
[sizeof buf
- 1] = '\0';
420 if (tz
->standard_params
.rrule
) {
421 /* +1 to skip the T before the time */
423 rrule_clock (tz
->standard_params
.rrule
, tp_std
+ 1,
424 tz
->standard_params
.offsetfrom
, year
);
426 if (tp_dst
&& tz
->daylight_params
.rrule
) {
427 /* +1 to skip the T before the time */
429 rrule_clock (tz
->daylight_params
.rrule
, tp_dst
+ 1,
430 tz
->daylight_params
.offsetfrom
, year
);
433 if (transition
[0] < transition
[1]) {
434 inform("format_datetime() requires that daylight "
435 "saving time transition precede standard time "
440 if (parse_datetime(node
->value
, tz
->standard_params
.offsetto
,
441 false, &tws
[0]) != OK
) {
442 inform("unable to parse datetime %s", node
->value
);
445 dt
[0] = tws
[0].tw_clock
;
448 if (dt
[0] < transition
[1]) {
451 if (parse_datetime(node
->value
,
452 tz
->daylight_params
.offsetto
, true, &tws
[1]) != OK
) {
453 inform("unable to parse datetime %s", node
->value
);
456 dt
[1] = tws
[1].tw_clock
;
457 dst
= dt
[1] <= transition
[0];
462 if (tz
->daylight_params
.start_dt
> 0 &&
463 dt
[dst
] < tz
->daylight_params
.start_dt
) {
464 inform("date-time of %s is before VTIMEZONE start "
465 "of %s", node
->value
,
466 tz
->daylight_params
.dtstart
);
470 if (tz
->standard_params
.start_dt
> 0 &&
471 dt
[dst
] < tz
->standard_params
.start_dt
) {
472 inform("date-time of %s is before VTIMEZONE start "
473 "of %s", node
->value
,
474 tz
->standard_params
.dtstart
);
480 inform("unsupported date-time format: %s",
481 tz
->standard_params
.dtstart
);
485 inform("unsupported date-time format: %s", node
->value
);
490 return strdup (dasctime (&tws
[dst
], 0));