]>
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.
10 #include "h/icalendar.h"
11 #include "h/fmt_scan.h"
17 * This doesn't try to support all of the myriad date-time formats
18 * allowed by RFC 5545. It is only used for viewing date-times,
19 * so that shouldn't be a problem: if a particular format can't
20 * be handled by this code, just present it to the user in its
23 * And, this assumes a valid iCalendar input file. E.g, it
24 * doesn't check that each BEGIN has a matching END and vice
25 * versa. That should be done in the parser, though it currently
29 typedef struct tzparams
{
30 /* Pointers to values in parse tree.
31 * TZOFFSETFROM is used to calculate the absolute time at which
32 * the transition to a given observance takes place.
33 * TZOFFSETTO is the timezone offset from UTC. Both are in HHmm
35 char *offsetfrom
, *offsetto
;
39 /* This is only used to make sure that timezone applies. And not
40 always, because if the timezone DTSTART is before the epoch, we
41 don't try to compare to it. */
42 time_t start_dt
; /* in seconds since epoch */
48 /* The following are translations of the pieces of RRULE and DTSTART
49 into seconds from beginning of year. */
50 tzparams standard_params
;
51 tzparams daylight_params
;
57 * Parse a datetime of the form YYYYMMDDThhmmss and a string
58 * representation of the timezone in units of [+-]hhmm and load the
62 parse_datetime (const char *datetime
, const char *zone
, bool dst
,
71 sscanf (datetime
, "%4d%2d%2dT%2d%2d%2d%c",
72 &tws
->tw_year
, &tws
->tw_mon
, &tws
->tw_mday
,
73 &tws
->tw_hour
, &tws
->tw_min
, &tws
->tw_sec
,
75 tws
->tw_flags
= TW_NULL
;
78 if (items_matched
== 7) {
79 /* The 'Z' must be capital according to RFC 5545 Sec. 3.3.5. */
80 if (utc_indicator
!= 'Z') {
81 inform("%s has invalid timezone-indicator byte: %#x",
82 datetime
, utc_indicator
);
85 } else if (zone
== NULL
) {
89 /* items_matched of 3 is for, e.g., 20151230. Assume that means
90 the entire day. The time fields of the tws struct were
91 initialized to 0 by the memset() above. */
92 if (items_matched
>= 6 || items_matched
== 3) {
93 /* struct tws defines tw_mon over [0, 11]. */
96 /* Fill out rest of tws, i.e., its tw_wday and tw_flags. */
98 /* set_dotw() sets TW_SIMP. Replace that with TW_SEXP so that
99 dasctime() outputs the dotw before the date instead of after. */
100 tws
->tw_flags
&= ~TW_SDAY
;
101 tws
->tw_flags
|= TW_SEXP
;
103 /* For the call to dmktime():
105 - tw_clock must be 0 on entry, and is set by dmktime()
106 - the only flag in tw_flags used is TW_DST
108 tws
->tw_yday
= tws
->tw_clock
= 0;
110 int offset
= atoi(zone
);
111 tws
->tw_zone
= 60 * (offset
/ 100) + offset
% 100;
115 tws
->tw_zone
-= 60; /* per dlocaltime() */
116 tws
->tw_flags
|= TW_DST
;
118 /* dmktime() just sets tws->tw_clock. */
119 (void) dmktime (tws
);
122 /* Set TW_SZEXP so that dasctime outputs timezone, except
123 with local time (Form #1). */
124 tws
->tw_flags
|= TW_SZEXP
;
126 /* Convert UTC time to time in local timezone. However,
127 don't try for years before 1970 because dlocatime()
128 doesn't handle them well. dlocaltime() will succeed if
129 tws->tw_clock is nonzero. */
130 if (tws
->tw_year
>= 1970 && tws
->tw_clock
> 0) {
131 const int was_dst
= tws
->tw_flags
& TW_DST
;
133 *tws
= *dlocaltime (&tws
->tw_clock
);
134 if (was_dst
&& ! (tws
->tw_flags
& TW_DST
)) {
135 /* dlocaltime() changed the DST flag from 1 to 0,
136 which means the time is in the hour (assumed to
137 be one hour) that is lost in the transition to
138 DST. So per RFC 5545 Sec. 3.3.5, "the
139 DATE-TIME value is interpreted using the UTC
140 offset before the gap in local times." In
141 other words, add an hour to it.
142 No adjustment is necessary for the transition
143 from DST to standard time, because dasctime()
144 shows the first occurrence of the time. */
145 tws
->tw_clock
+= 3600;
146 *tws
= *dlocaltime (&tws
->tw_clock
);
158 load_timezones (const contentline
*clines
)
160 tzdesc_t timezones
= NULL
, timezone
= NULL
;
161 bool in_vtimezone
, in_standard
, in_daylight
;
162 tzparams
*params
= NULL
;
163 const contentline
*node
;
165 /* Interpret each VTIMEZONE section. */
166 in_vtimezone
= in_standard
= in_daylight
= false;
167 for (node
= clines
; node
; node
= node
->next
) {
168 /* node->name will be NULL if the line was "deleted". */
169 if (! node
->name
) { continue; }
171 if (in_daylight
|| in_standard
) {
172 if (! strcasecmp ("END", node
->name
) &&
173 ((in_standard
&& ! strcasecmp ("STANDARD", node
->value
)) ||
174 (in_daylight
&& ! strcasecmp ("DAYLIGHT", node
->value
)))) {
177 if (in_standard
) { in_standard
= false; }
178 else if (in_daylight
) { in_daylight
= false; }
180 if (parse_datetime(params
->dtstart
, params
->offsetfrom
,
181 in_daylight
, &tws
) != OK
) {
182 inform("failed to parse start time %s for %s",
184 in_daylight
? "daylight" : "standard");
188 if (tws
.tw_year
>= 1970) {
189 /* dmktime() falls apart for, e.g., the year 1601. */
190 params
->start_dt
= tws
.tw_clock
;
193 } else if (! strcasecmp ("DTSTART", node
->name
)) {
194 /* Save DTSTART for use after getting TZOFFSETFROM. */
195 params
->dtstart
= node
->value
;
196 } else if (! strcasecmp ("TZOFFSETFROM", node
->name
)) {
197 params
->offsetfrom
= node
->value
;
198 } else if (! strcasecmp ("TZOFFSETTO", node
->name
)) {
199 params
->offsetto
= node
->value
;
200 } else if (! strcasecmp ("RRULE", node
->name
)) {
201 params
->rrule
= node
->value
;
203 } else if (in_vtimezone
) {
204 if (! strcasecmp ("END", node
->name
) &&
205 ! strcasecmp ("VTIMEZONE", node
->value
)) {
206 in_vtimezone
= false;
207 } else if (! strcasecmp ("BEGIN", node
->name
) &&
208 ! strcasecmp ("STANDARD", node
->value
)) {
210 params
= &timezone
->standard_params
;
211 } else if (! strcasecmp ("BEGIN", node
->name
) &&
212 ! strcasecmp ("DAYLIGHT", node
->value
)) {
214 params
= &timezone
->daylight_params
;
215 } else if (! strcasecmp ("TZID", node
->name
)) {
216 /* See comment below in format_datetime() about removing any enclosing quotes from a
217 timezone identifier. */
218 char *buf
= mh_xmalloc(strlen(node
->value
) + 1);
219 unquote_string(node
->value
, buf
);
220 timezone
->tzid
= buf
;
223 if (! strcasecmp ("BEGIN", node
->name
) &&
224 ! strcasecmp ("VTIMEZONE", node
->value
)) {
231 for (t
= timezones
; t
&& t
->next
; t
= t
->next
) { continue; }
232 /* The loop terminated at, not after, the last
236 timezones
= timezone
;
246 free_timezones (tzdesc_t timezone
)
250 for ( ; timezone
; timezone
= next
) {
251 free (timezone
->tzid
);
252 next
= timezone
->next
;
258 * Convert time to local timezone, accounting for daylight saving time:
259 * - Detect which type of datetime the node contains:
260 * Form #1: DATE WITH LOCAL TIME
261 * Form #2: DATE WITH UTC TIME
262 * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
263 * - Convert value to local time in seconds since epoch.
264 * - If there's a DST in the timezone, convert its start and end
265 * date-times to local time in seconds, also. Then determine
266 * if the value is between them, and therefore DST. Otherwise, it's
268 * - Format the time value.
272 * Given a recurrence rule and year, calculate its time in seconds
273 * from 01 January UTC of the year.
276 rrule_clock (const char *rrule
, const char *starttime
, const char *zone
,
281 if (nmh_strcasestr (rrule
, "FREQ=YEARLY;INTERVAL=1") ||
282 (nmh_strcasestr (rrule
, "FREQ=YEARLY") && nmh_strcasestr(rrule
, "INTERVAL") == NULL
)) {
285 int wday
= -1, month
= -1;
286 int specific_day
= 1; /* BYDAY integer (prefix) */
290 if ((cp
= nmh_strcasestr (rrule
, "BYDAY="))) {
292 /* BYDAY integers must be ASCII. */
293 if (*cp
== '+') { ++cp
; } /* +n specific day; don't support '-' */
294 else if (*cp
== '-') { goto fail
; }
296 if (isdigit ((unsigned char) *cp
)) { specific_day
= *cp
++ - 0x30; }
298 if (! strncasecmp (cp
, "SU", 2)) { wday
= 0; }
299 else if (! strncasecmp (cp
, "MO", 2)) { wday
= 1; }
300 else if (! strncasecmp (cp
, "TU", 2)) { wday
= 2; }
301 else if (! strncasecmp (cp
, "WE", 2)) { wday
= 3; }
302 else if (! strncasecmp (cp
, "TH", 2)) { wday
= 4; }
303 else if (! strncasecmp (cp
, "FR", 2)) { wday
= 5; }
304 else if (! strncasecmp (cp
, "SA", 2)) { wday
= 6; }
306 if ((cp
= nmh_strcasestr (rrule
, "BYMONTH="))) {
307 month
= atoi (cp
+ 8);
310 for (day
= 1; day
<= 7; ++day
) {
311 /* E.g, 11-01-2014 02:00:00-0400 */
312 snprintf (buf
, sizeof buf
, "%02d-%02d-%04u %.2s:%.2s:%.2s%s",
313 month
, day
+ 7 * (specific_day
-1), year
,
314 starttime
, starttime
+ 2, starttime
+ 4,
315 zone
? zone
: "0000");
316 if ((tws
= dparsetime (buf
))) {
317 if (! (tws
->tw_flags
& (TW_SEXP
|TW_SIMP
))) { set_dotw (tws
); }
319 if (tws
->tw_wday
== wday
) {
320 /* Found the day specified in the RRULE. */
327 clock
= tws
->tw_clock
;
333 inform("Unsupported RRULE format: %s, assume local timezone, continuing...",
341 format_datetime (tzdesc_t timezones
, const contentline
*node
)
344 char *dt_timezone
= NULL
;
346 struct tws tws
[2]; /* [standard, daylight] */
348 char *tp_std
, *tp_dst
, *tp_dt
;
350 /* Extract the timezone, if specified (RFC 5545 Sec. 3.3.5 Form #3). */
351 for (p
= node
->params
; p
&& p
->param_name
; p
= p
->next
) {
352 if (! strcasecmp (p
->param_name
, "TZID") && p
->values
) {
353 /* Remove any enclosing quotes from the timezone identifier. I don't believe that it's
354 legal for it to be quoted, according to RFC 5545 ยง 3.2.19:
355 tzidparam = "TZID" "=" [tzidprefix] paramtext
357 where paramtext includes SAFE-CHAR, which specifically excludes DQUOTE. But we'll
358 be generous and strip quotes. */
359 char *buf
= mh_xmalloc(strlen(p
->values
->value
) + 1);
360 unquote_string(p
->values
->value
, buf
);
367 /* Form #1: DATE WITH LOCAL TIME, i.e., no time zone, or
368 Form #2: DATE WITH UTC TIME */
369 if (parse_datetime(node
->value
, NULL
, false, &tws
[0]) != OK
) {
370 inform("unable to parse datetime %s", node
->value
);
373 return strdup (dasctime (&tws
[0], 0));
378 * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
381 /* Find the corresponding tzdesc. */
382 for (tz
= timezones
; dt_timezone
&& tz
; tz
= tz
->next
) {
383 /* Property parameter values are case insensitive (RFC 5545
384 Sec. 2) and time zone identifiers are property parameters
385 (RFC 5545 Sec. 3.8.2.4), though it would seem odd to use
386 different case in the same file for identifiers that are
387 supposed to be the same. */
388 if (tz
->tzid
&& ! strcasecmp (dt_timezone
, tz
->tzid
)) { break; }
392 inform("did not find VTIMEZONE section for %s", dt_timezone
);
398 /* Determine if it's Daylight Saving. */
399 tp_std
= strchr (tz
->standard_params
.dtstart
, 'T');
400 tp_dt
= strchr (node
->value
, 'T');
402 if (tz
->daylight_params
.dtstart
) {
403 tp_dst
= strchr (tz
->daylight_params
.dtstart
, 'T');
405 /* No DAYLIGHT section. */
410 if (tp_std
&& tp_dt
) {
411 time_t transition
[2] = { 0, 0 }; /* [standard, daylight] */
412 time_t dt
[2]; /* [standard, daylight] */
416 /* Datetime is form YYYYMMDDThhmmss. Extract year. */
417 memcpy (buf
, node
->value
, sizeof buf
- 1);
418 buf
[sizeof buf
- 1] = '\0';
421 if (tz
->standard_params
.rrule
) {
422 /* +1 to skip the T before the time */
424 rrule_clock (tz
->standard_params
.rrule
, tp_std
+ 1,
425 tz
->standard_params
.offsetfrom
, year
);
427 if (tp_dst
&& tz
->daylight_params
.rrule
) {
428 /* +1 to skip the T before the time */
430 rrule_clock (tz
->daylight_params
.rrule
, tp_dst
+ 1,
431 tz
->daylight_params
.offsetfrom
, year
);
434 if (transition
[0] < transition
[1]) {
435 inform("format_datetime() requires that daylight "
436 "saving time transition precede standard time "
441 if (parse_datetime(node
->value
, tz
->standard_params
.offsetto
,
442 false, &tws
[0]) != OK
) {
443 inform("unable to parse datetime %s", node
->value
);
446 dt
[0] = tws
[0].tw_clock
;
449 if (dt
[0] < transition
[1]) {
452 if (parse_datetime(node
->value
,
453 tz
->daylight_params
.offsetto
, true, &tws
[1]) != OK
) {
454 inform("unable to parse datetime %s", node
->value
);
457 dt
[1] = tws
[1].tw_clock
;
458 dst
= dt
[1] <= transition
[0];
463 if (tz
->daylight_params
.start_dt
> 0 &&
464 dt
[dst
] < tz
->daylight_params
.start_dt
) {
465 inform("date-time of %s is before VTIMEZONE start "
466 "of %s", node
->value
,
467 tz
->daylight_params
.dtstart
);
471 if (tz
->standard_params
.start_dt
> 0 &&
472 dt
[dst
] < tz
->standard_params
.start_dt
) {
473 inform("date-time of %s is before VTIMEZONE start "
474 "of %s", node
->value
,
475 tz
->standard_params
.dtstart
);
481 inform("unsupported date-time format: %s",
482 tz
->standard_params
.dtstart
);
486 inform("unsupported date-time format: %s", node
->value
);
491 return strdup (dasctime (&tws
[dst
], 0));