]>
diplodocus.org Git - nmh/blob - sbr/datetime.c
295522632feacea11fed34b24351b4b78965d0f2
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.
11 #include "h/icalendar.h"
13 #include "h/fmt_scan.h"
19 * This doesn't try to support all of the myriad date-time formats
20 * allowed by RFC 5545. It is only used for viewing date-times,
21 * so that shouldn't be a problem: if a particular format can't
22 * be handled by this code, just present it to the user in its
25 * And, this assumes a valid iCalendar input file. E.g, it
26 * doesn't check that each BEGIN has a matching END and vice
27 * versa. That should be done in the parser, though it currently
31 typedef struct tzparams
{
32 /* Pointers to values in parse tree.
33 * TZOFFSETFROM is used to calculate the absolute time at which
34 * the transition to a given observance takes place.
35 * TZOFFSETTO is the timezone offset from UTC. Both are in HHmm
37 char *offsetfrom
, *offsetto
;
41 /* This is only used to make sure that timezone applies. And not
42 always, because if the timezone DTSTART is before the epoch, we
43 don't try to compare to it. */
44 time_t start_dt
; /* in seconds since epoch */
50 /* The following are translations of the pieces of RRULE and DTSTART
51 into seconds from beginning of year. */
52 tzparams standard_params
;
53 tzparams daylight_params
;
59 * Parse a datetime of the form YYYYMMDDThhmmss and a string
60 * representation of the timezone in units of [+-]hhmm and load the
64 parse_datetime (const char *datetime
, const char *zone
, bool dst
,
73 sscanf (datetime
, "%4d%2d%2dT%2d%2d%2d%c",
74 &tws
->tw_year
, &tws
->tw_mon
, &tws
->tw_mday
,
75 &tws
->tw_hour
, &tws
->tw_min
, &tws
->tw_sec
,
77 tws
->tw_flags
= TW_NULL
;
80 if (items_matched
== 7) {
81 /* The 'Z' must be capital according to RFC 5545 Sec. 3.3.5. */
82 if (utc_indicator
!= 'Z') {
83 inform("%s has invalid timezone-indicator byte: %#x",
84 datetime
, utc_indicator
);
87 } else if (zone
== NULL
) {
91 /* items_matched of 3 is for, e.g., 20151230. Assume that means
92 the entire day. The time fields of the tws struct were
93 initialized to 0 by the memset() above. */
94 if (items_matched
>= 6 || items_matched
== 3) {
95 /* struct tws defines tw_mon over [0, 11]. */
98 /* Fill out rest of tws, i.e., its tw_wday and tw_flags. */
100 /* set_dotw() sets TW_SIMP. Replace that with TW_SEXP so that
101 dasctime() outputs the dotw before the date instead of after. */
102 tws
->tw_flags
&= ~TW_SDAY
;
103 tws
->tw_flags
|= TW_SEXP
;
105 /* For the call to dmktime():
107 - tw_clock must be 0 on entry, and is set by dmktime()
108 - the only flag in tw_flags used is TW_DST
110 tws
->tw_yday
= tws
->tw_clock
= 0;
112 int offset
= atoi(zone
);
113 tws
->tw_zone
= 60 * (offset
/ 100) + offset
% 100;
117 tws
->tw_zone
-= 60; /* per dlocaltime() */
118 tws
->tw_flags
|= TW_DST
;
120 /* dmktime() just sets tws->tw_clock. */
121 (void) dmktime (tws
);
124 /* Set TW_SZEXP so that dasctime outputs timezone, except
125 with local time (Form #1). */
126 tws
->tw_flags
|= TW_SZEXP
;
128 /* Convert UTC time to time in local timezone. However,
129 don't try for years before 1970 because dlocatime()
130 doesn't handle them well. dlocaltime() will succeed if
131 tws->tw_clock is nonzero. */
132 if (tws
->tw_year
>= 1970 && tws
->tw_clock
> 0) {
133 const int was_dst
= tws
->tw_flags
& TW_DST
;
135 *tws
= *dlocaltime (&tws
->tw_clock
);
136 if (was_dst
&& ! (tws
->tw_flags
& TW_DST
)) {
137 /* dlocaltime() changed the DST flag from 1 to 0,
138 which means the time is in the hour (assumed to
139 be one hour) that is lost in the transition to
140 DST. So per RFC 5545 Sec. 3.3.5, "the
141 DATE-TIME value is interpreted using the UTC
142 offset before the gap in local times." In
143 other words, add an hour to it.
144 No adjustment is necessary for the transition
145 from DST to standard time, because dasctime()
146 shows the first occurrence of the time. */
147 tws
->tw_clock
+= 3600;
148 *tws
= *dlocaltime (&tws
->tw_clock
);
160 load_timezones (const contentline
*clines
)
162 tzdesc_t timezones
= NULL
, timezone
= NULL
;
163 bool in_vtimezone
, in_standard
, in_daylight
;
164 tzparams
*params
= NULL
;
165 const contentline
*node
;
167 /* Interpret each VTIMEZONE section. */
168 in_vtimezone
= in_standard
= in_daylight
= false;
169 for (node
= clines
; node
; node
= node
->next
) {
170 /* node->name will be NULL if the line was "deleted". */
171 if (! node
->name
) { continue; }
173 if (in_daylight
|| in_standard
) {
174 if (! strcasecmp ("END", node
->name
) &&
175 ((in_standard
&& ! strcasecmp ("STANDARD", node
->value
)) ||
176 (in_daylight
&& ! strcasecmp ("DAYLIGHT", node
->value
)))) {
179 if (in_standard
) { in_standard
= false; }
180 else if (in_daylight
) { in_daylight
= false; }
182 if (parse_datetime(params
->dtstart
, params
->offsetfrom
,
183 in_daylight
, &tws
) != OK
) {
184 inform("failed to parse start time %s for %s",
186 in_daylight
? "daylight" : "standard");
190 if (tws
.tw_year
>= 1970) {
191 /* dmktime() falls apart for, e.g., the year 1601. */
192 params
->start_dt
= tws
.tw_clock
;
195 } else if (! strcasecmp ("DTSTART", node
->name
)) {
196 /* Save DTSTART for use after getting TZOFFSETFROM. */
197 params
->dtstart
= node
->value
;
198 } else if (! strcasecmp ("TZOFFSETFROM", node
->name
)) {
199 params
->offsetfrom
= node
->value
;
200 } else if (! strcasecmp ("TZOFFSETTO", node
->name
)) {
201 params
->offsetto
= node
->value
;
202 } else if (! strcasecmp ("RRULE", node
->name
)) {
203 params
->rrule
= node
->value
;
205 } else if (in_vtimezone
) {
206 if (! strcasecmp ("END", node
->name
) &&
207 ! strcasecmp ("VTIMEZONE", node
->value
)) {
208 in_vtimezone
= false;
209 } else if (! strcasecmp ("BEGIN", node
->name
) &&
210 ! strcasecmp ("STANDARD", node
->value
)) {
212 params
= &timezone
->standard_params
;
213 } else if (! strcasecmp ("BEGIN", node
->name
) &&
214 ! strcasecmp ("DAYLIGHT", node
->value
)) {
216 params
= &timezone
->daylight_params
;
217 } else if (! strcasecmp ("TZID", node
->name
)) {
218 /* See comment below in format_datetime() about removing any enclosing quotes from a
219 timezone identifier. */
220 char *buf
= mh_xmalloc(strlen(node
->value
) + 1);
221 unquote_string(node
->value
, buf
);
222 timezone
->tzid
= buf
;
225 if (! strcasecmp ("BEGIN", node
->name
) &&
226 ! strcasecmp ("VTIMEZONE", node
->value
)) {
233 for (t
= timezones
; t
&& t
->next
; t
= t
->next
) { continue; }
234 /* The loop terminated at, not after, the last
238 timezones
= timezone
;
248 free_timezones (tzdesc_t timezone
)
252 for ( ; timezone
; timezone
= next
) {
253 free (timezone
->tzid
);
254 next
= timezone
->next
;
260 * Convert time to local timezone, accounting for daylight saving time:
261 * - Detect which type of datetime the node contains:
262 * Form #1: DATE WITH LOCAL TIME
263 * Form #2: DATE WITH UTC TIME
264 * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
265 * - Convert value to local time in seconds since epoch.
266 * - If there's a DST in the timezone, convert its start and end
267 * date-times to local time in seconds, also. Then determine
268 * if the value is between them, and therefore DST. Otherwise, it's
270 * - Format the time value.
274 * Given a recurrence rule and year, calculate its time in seconds
275 * from 01 January UTC of the year.
278 rrule_clock (const char *rrule
, const char *starttime
, const char *zone
,
283 if (nmh_strcasestr (rrule
, "FREQ=YEARLY;INTERVAL=1") ||
284 (nmh_strcasestr (rrule
, "FREQ=YEARLY") && nmh_strcasestr(rrule
, "INTERVAL") == NULL
)) {
287 int wday
= -1, month
= -1;
288 int specific_day
= 1; /* BYDAY integer (prefix) */
293 if ((cp
= nmh_strcasestr (rrule
, "BYDAY="))) {
295 /* BYDAY integers must be ASCII. */
296 if (*cp
== '+') { ++cp
; } /* +n specific day */
297 else if (*cp
== '-') { ++cp
; specific_day
= -1; }
299 if (isdigit ((unsigned char) *cp
)) { specific_day
*= *cp
++ - 0x30; }
301 if (! strncasecmp (cp
, "SU", 2)) { wday
= 0; }
302 else if (! strncasecmp (cp
, "MO", 2)) { wday
= 1; }
303 else if (! strncasecmp (cp
, "TU", 2)) { wday
= 2; }
304 else if (! strncasecmp (cp
, "WE", 2)) { wday
= 3; }
305 else if (! strncasecmp (cp
, "TH", 2)) { wday
= 4; }
306 else if (! strncasecmp (cp
, "FR", 2)) { wday
= 5; }
307 else if (! strncasecmp (cp
, "SA", 2)) { wday
= 6; }
309 if ((cp
= nmh_strcasestr (rrule
, "BYMONTH="))) {
310 month
= atoi (cp
+ 8);
313 end_of_week
= specific_day
> 0 ? 7 * specific_day
:
314 dmlastday(year
, month
) + 7 * (1 + specific_day
);
315 for (day
= end_of_week
- 6; day
<= end_of_week
; ++day
) {
316 /* E.g, 11-01-2014 02:00:00-0400 */
317 snprintf (buf
, sizeof buf
, "%02d-%02d-%04u %.2s:%.2s:%.2s%s",
318 month
, day
, year
, starttime
, starttime
+ 2,
319 starttime
+ 4, zone
? zone
: "0000");
320 if ((tws
= dparsetime (buf
))) {
321 if (! (tws
->tw_flags
& (TW_SEXP
|TW_SIMP
))) { set_dotw (tws
); }
323 if (tws
->tw_wday
== wday
) {
324 /* Found the day specified in the RRULE. */
330 if (day
<= end_of_week
) {
331 clock
= tws
->tw_clock
;
337 inform("Unsupported RRULE format: %s, assume local timezone, continuing...",
345 format_datetime (tzdesc_t timezones
, const contentline
*node
)
348 char *dt_timezone
= NULL
;
350 struct tws tws
[2]; /* [standard, daylight] */
352 char *tp_std
, *tp_dst
, *tp_dt
;
354 /* Extract the timezone, if specified (RFC 5545 Sec. 3.3.5 Form #3). */
355 for (p
= node
->params
; p
&& p
->param_name
; p
= p
->next
) {
356 if (! strcasecmp (p
->param_name
, "TZID") && p
->values
) {
357 /* Remove any enclosing quotes from the timezone identifier. I don't believe that it's
358 legal for it to be quoted, according to RFC 5545 ยง 3.2.19:
359 tzidparam = "TZID" "=" [tzidprefix] paramtext
361 where paramtext includes SAFE-CHAR, which specifically excludes DQUOTE. But we'll
362 be generous and strip quotes. */
363 char *buf
= mh_xmalloc(strlen(p
->values
->value
) + 1);
364 unquote_string(p
->values
->value
, buf
);
371 /* Form #1: DATE WITH LOCAL TIME, i.e., no time zone, or
372 Form #2: DATE WITH UTC TIME */
373 if (parse_datetime(node
->value
, NULL
, false, &tws
[0]) != OK
) {
374 inform("unable to parse datetime %s", node
->value
);
377 return strdup (dasctime (&tws
[0], 0));
382 * Form #3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
385 /* Find the corresponding tzdesc. */
386 for (tz
= timezones
; dt_timezone
&& tz
; tz
= tz
->next
) {
387 /* Property parameter values are case insensitive (RFC 5545
388 Sec. 2) and time zone identifiers are property parameters
389 (RFC 5545 Sec. 3.8.2.4), though it would seem odd to use
390 different case in the same file for identifiers that are
391 supposed to be the same. */
392 if (tz
->tzid
&& ! strcasecmp (dt_timezone
, tz
->tzid
)) { break; }
396 inform("did not find VTIMEZONE section for %s", dt_timezone
);
402 /* Determine if it's Daylight Saving. */
403 tp_std
= strchr (tz
->standard_params
.dtstart
, 'T');
404 tp_dt
= strchr (node
->value
, 'T');
406 if (tz
->daylight_params
.dtstart
) {
407 tp_dst
= strchr (tz
->daylight_params
.dtstart
, 'T');
409 /* No DAYLIGHT section. */
414 if (tp_std
&& tp_dt
) {
415 time_t transition
[2] = { 0, 0 }; /* [standard, daylight] */
416 time_t dt
[2]; /* [standard, daylight] */
420 /* Datetime is form YYYYMMDDThhmmss. Extract year. */
421 memcpy (buf
, node
->value
, sizeof buf
- 1);
422 buf
[sizeof buf
- 1] = '\0';
425 if (tz
->standard_params
.rrule
) {
426 /* +1 to skip the T before the time */
428 rrule_clock (tz
->standard_params
.rrule
, tp_std
+ 1,
429 tz
->standard_params
.offsetfrom
, year
);
431 if (tp_dst
&& tz
->daylight_params
.rrule
) {
432 /* +1 to skip the T before the time */
434 rrule_clock (tz
->daylight_params
.rrule
, tp_dst
+ 1,
435 tz
->daylight_params
.offsetfrom
, year
);
438 if (transition
[0] < transition
[1]) {
439 inform("format_datetime() requires that daylight "
440 "saving time transition precede standard time "
445 if (parse_datetime(node
->value
, tz
->standard_params
.offsetto
,
446 false, &tws
[0]) != OK
) {
447 inform("unable to parse datetime %s", node
->value
);
450 dt
[0] = tws
[0].tw_clock
;
453 if (dt
[0] < transition
[1]) {
456 if (parse_datetime(node
->value
,
457 tz
->daylight_params
.offsetto
, true, &tws
[1]) != OK
) {
458 inform("unable to parse datetime %s", node
->value
);
461 dt
[1] = tws
[1].tw_clock
;
462 dst
= dt
[1] <= transition
[0];
467 if (tz
->daylight_params
.start_dt
> 0 &&
468 dt
[dst
] < tz
->daylight_params
.start_dt
) {
469 inform("date-time of %s is before VTIMEZONE start "
470 "of %s", node
->value
,
471 tz
->daylight_params
.dtstart
);
475 if (tz
->standard_params
.start_dt
> 0 &&
476 dt
[dst
] < tz
->standard_params
.start_dt
) {
477 inform("date-time of %s is before VTIMEZONE start "
478 "of %s", node
->value
,
479 tz
->standard_params
.dtstart
);
485 inform("unsupported date-time format: %s",
486 tz
->standard_params
.dtstart
);
490 inform("unsupported date-time format: %s", node
->value
);
495 return strdup (dasctime (&tws
[dst
], 0));