Support import of time zones (RFC 5545)

The property parameter time zone identifier (TZID) is recognized in DTSTART,
DTEND and EXDATE and the DATE-TIME value converted to a local time. The time
zone identifier is logged in the note file.

Signed-off-by: Lars Henriksen <LarsHenriksen@get2net.dk>
Signed-off-by: Lukas Fleischer <lfleischer@calcurse.org>
This commit is contained in:
Lars Henriksen 2020-05-25 20:52:22 +02:00 committed by Lukas Fleischer
parent 32783496e6
commit d8c1c48e78
6 changed files with 112 additions and 48 deletions

View File

@ -1219,7 +1219,7 @@ int get_item_min(time_t);
struct tm date2tm(struct date, unsigned, unsigned); struct tm date2tm(struct date, unsigned, unsigned);
time_t date2sec(struct date, unsigned, unsigned); time_t date2sec(struct date, unsigned, unsigned);
struct date sec2date(time_t); struct date sec2date(time_t);
time_t utcdate2sec(struct date, unsigned, unsigned); time_t tzdate2sec(struct date, unsigned, unsigned, char *);
int date_cmp(struct date *, struct date *); int date_cmp(struct date *, struct date *);
int date_cmp_day(time_t, time_t); int date_cmp_day(time_t, time_t);
char *date_sec2date_str(time_t, const char *); char *date_sec2date_str(time_t, const char *);

View File

@ -578,6 +578,32 @@ ical_chk_header(FILE * fd, char *buf, char *lstore, unsigned *lineno,
return 1; return 1;
} }
/*
* Return the TZID property parameter value from a DTSTART/DTEND/EXDATE property
* in an allocated string. The value may be any text string not containing the
* characters '"', ';', ':' and ',' (RFC 5545, sections 3.2.19 and 3.1).
*/
static char *ical_get_tzid(char *p)
{
const char param[] = ";TZID=";
char *q;
int s;
if (!(p = strstr(p, param)))
return NULL;
p += sizeof(param) - 1;
if (*p == '"')
return NULL;
q = strpbrk(p, ":;");
s = q - p + 1;
q = mem_malloc(s);
strncpy(q, p, s);
q[s - 1] = '\0';
return q;
}
/* /*
* Return event type from a DTSTART/DTEND/EXDATE property. * Return event type from a DTSTART/DTEND/EXDATE property.
*/ */
@ -601,23 +627,23 @@ static ical_vevent_e ical_get_type(char *c_line)
* where DATE is 'YYYYMMDD' and TIME is 'HHMMSS'. * where DATE is 'YYYYMMDD' and TIME is 'HHMMSS'.
* The time and 'T' separator are optional (in the case of an day-long event). * The time and 'T' separator are optional (in the case of an day-long event).
* *
* The type argument is either APPOINTMENT or EVENT and the time format must * The type argument is either APPOINTMENT or EVENT, and the time format must
* agree. * match (either DATE-TIME or DATE). The time zone identifier is ignored in an
* * EVENT or in an APPOINTMENT with UTC time.
* The timezone is not yet handled by calcurse.
*/ */
static time_t ical_datetime2time_t(char *datestr, ical_vevent_e type) static time_t ical_datetime2time_t(char *datestr, char *tzid, ical_vevent_e type)
{ {
const int INVALID = 0, DATE = 3, DATETIME = 6, DATETIMEZ = 7; const int INVALID = 0, DATE = 3, DATETIME = 6, DATETIMEZ = 7;
struct date date; struct date date;
unsigned hour, min, sec; unsigned hour, min, sec;
char c; char c, UTC[] = "";
int format; int format;
EXIT_IF(type == UNDEFINED, "event type not set"); EXIT_IF(type == UNDEFINED, "event type not set");
format = sscanf(datestr, "%04u%02u%02uT%02u%02u%02u%c", format = sscanf(datestr, "%04u%02u%02uT%02u%02u%02u%c",
&date.yyyy, &date.mm, &date.dd, &hour, &min, &sec, &c); &date.yyyy, &date.mm, &date.dd, &hour, &min, &sec, &c);
if (format == DATE && strlen(datestr) > 8) if (format == DATE && strlen(datestr) > 8)
format = INVALID; format = INVALID;
if (format == DATETIMEZ && c != 'Z') if (format == DATETIMEZ && c != 'Z')
@ -626,9 +652,9 @@ static time_t ical_datetime2time_t(char *datestr, ical_vevent_e type)
if (format == DATE && type == EVENT) if (format == DATE && type == EVENT)
return date2sec(date, 0, 0); return date2sec(date, 0, 0);
else if (format == DATETIME && type == APPOINTMENT) else if (format == DATETIME && type == APPOINTMENT)
return date2sec(date, hour, min); return tzdate2sec(date, hour, min, tzid);
else if (format == DATETIMEZ && type == APPOINTMENT) else if (format == DATETIMEZ && type == APPOINTMENT)
return utcdate2sec(date, hour, min); return tzdate2sec(date, hour, min, UTC);
return 0; return 0;
} }
@ -869,7 +895,7 @@ static ical_rpt_t *ical_read_rrule(FILE *log, char *rrulestr,
* specified, counts as the first occurrence. * specified, counts as the first occurrence.
*/ */
if ((p = strstr(rrulestr, "UNTIL")) != NULL) { if ((p = strstr(rrulestr, "UNTIL")) != NULL) {
rpt->until = ical_datetime2time_t(strchr(p, '=') + 1, type); rpt->until = ical_datetime2time_t(strchr(p, '=') + 1, NULL, type);
if (!(rpt->until)) { if (!(rpt->until)) {
ical_log(log, ICAL_VEVENT, itemline, ical_log(log, ICAL_VEVENT, itemline,
_("invalid until format.")); _("invalid until format."));
@ -915,47 +941,47 @@ static int
ical_read_exdate(llist_t * exc, FILE * log, char *exstr, unsigned *noskipped, ical_read_exdate(llist_t * exc, FILE * log, char *exstr, unsigned *noskipped,
const int itemline, ical_vevent_e type) const int itemline, ical_vevent_e type)
{ {
char *p, *q; char *p, *q, *tzid = NULL;
time_t t; time_t t;
int n; int n;
/* See DTSTART. */
if (type == UNDEFINED) { if (type == UNDEFINED) {
ical_log(log, ICAL_VEVENT, itemline, ical_log(log, ICAL_VEVENT, itemline,
_("need DTSTART to determine event type.")); _("need DTSTART to determine event type."));
(*noskipped)++; goto cleanup;
return 0;
} }
if (type != ical_get_type(exstr)) { if (type != ical_get_type(exstr)) {
ical_log(log, ICAL_VEVENT, itemline, ical_log(log, ICAL_VEVENT, itemline,
_("invalid exception date value type.")); _("invalid exception date value type."));
(*noskipped)++; goto cleanup;
return 0;
} }
p = ical_get_value(exstr); p = ical_get_value(exstr);
if (!p) { if (!p) {
ical_log(log, ICAL_VEVENT, itemline, ical_log(log, ICAL_VEVENT, itemline,
_("malformed exceptions line.")); _("malformed exceptions line."));
(*noskipped)++; goto cleanup;
return 0;
} }
tzid = ical_get_tzid(exstr);
/* Count the exceptions and replace commas by zeroes */ /* Count the exceptions and replace commas by zeroes */
for (q = p, n = 1; (q = strchr(q, ',')); *q = '\0', q++, n++) for (q = p, n = 1; (q = strchr(q, ',')); *q = '\0', q++, n++)
; ;
while (n) { while (n) {
if (!(t = ical_datetime2time_t(p, type))) { if (!(t = ical_datetime2time_t(p, tzid, type))) {
ical_log(log, ICAL_VEVENT, itemline, ical_log(log, ICAL_VEVENT, itemline,
_("invalid exception.")); _("invalid exception."));
(*noskipped)++; goto cleanup;
return 0;
} }
ical_add_exc(exc, t); ical_add_exc(exc, t);
p = strchr(p, '\0') + 1; p = strchr(p, '\0') + 1;
n--; n--;
} }
return 1; return 1;
cleanup:
(*noskipped)++;
if (tzid)
mem_free(tzid);
return 0;
} }
/* /*
@ -1050,13 +1076,13 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
const int ITEMLINE = *lineno - !feof(fdi); const int ITEMLINE = *lineno - !feof(fdi);
ical_vevent_e vevent_type; ical_vevent_e vevent_type;
ical_property_e property; ical_property_e property;
char *p, *note = NULL, *comment; char *p, *note = NULL, *tmp, *tzid;
const char *SEPARATOR = "-- \n"; const char *SEPARATOR = "-- \n";
struct string s; struct string s;
struct { struct {
llist_t exc; llist_t exc;
ical_rpt_t *rpt; ical_rpt_t *rpt;
char *mesg, *desc, *loc, *comm, *stat, *note; char *mesg, *desc, *loc, *comm, *stat, *imp, *note;
long start, end, dur; long start, end, dur;
int has_alarm; int has_alarm;
} vevent; } vevent;
@ -1129,6 +1155,11 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
vevent.stat); vevent.stat);
mem_free(vevent.stat); mem_free(vevent.stat);
} }
if (vevent.imp) {
string_catf(&s, ("Import: %s\n"),
vevent.imp);
mem_free(vevent.imp);
}
vevent.note = generate_note(string_buf(&s)); vevent.note = generate_note(string_buf(&s));
mem_free(s.buf); mem_free(s.buf);
} }
@ -1167,6 +1198,18 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
* event. * event.
*/ */
vevent_type = ical_get_type(buf); vevent_type = ical_get_type(buf);
if ((tzid = ical_get_tzid(buf)) &&
vevent_type == APPOINTMENT) {
/* Add note on TZID. */
if (vevent.imp) {
asprintf(&tmp, "%s, TZID=%s",
vevent.imp, tzid);
mem_free(vevent.imp);
vevent.imp = tmp;
} else
asprintf(&vevent.imp, "TZID=%s", tzid);
has_note = separator = 1;
}
p = ical_get_value(buf); p = ical_get_value(buf);
if (!p) { if (!p) {
ical_log(log, ICAL_VEVENT, ITEMLINE, ical_log(log, ICAL_VEVENT, ITEMLINE,
@ -1174,7 +1217,9 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
goto skip; goto skip;
} }
vevent.start = ical_datetime2time_t(p, vevent_type); vevent.start = ical_datetime2time_t(p, tzid, vevent_type);
if (tzid)
mem_free(tzid);
if (!vevent.start) { if (!vevent.start) {
ical_log(log, ICAL_VEVENT, ITEMLINE, ical_log(log, ICAL_VEVENT, ITEMLINE,
_("invalid or malformed event " _("invalid or malformed event "
@ -1194,6 +1239,7 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
_("invalid end time value type.")); _("invalid end time value type."));
goto skip; goto skip;
} }
tzid = ical_get_tzid(buf);
p = ical_get_value(buf); p = ical_get_value(buf);
if (!p) { if (!p) {
ical_log(log, ICAL_VEVENT, ITEMLINE, ical_log(log, ICAL_VEVENT, ITEMLINE,
@ -1201,7 +1247,9 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
goto skip; goto skip;
} }
vevent.end = ical_datetime2time_t(p, vevent_type); vevent.end = ical_datetime2time_t(p, tzid, vevent_type);
if (tzid)
mem_free(tzid);
if (!vevent.end) { if (!vevent.end) {
ical_log(log, ICAL_VEVENT, ITEMLINE, ical_log(log, ICAL_VEVENT, ITEMLINE,
_("malformed event end time.")); _("malformed event end time."));
@ -1257,6 +1305,7 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
ICAL_VEVENT, ITEMLINE, log); ICAL_VEVENT, ITEMLINE, log);
if (!note) if (!note)
goto cleanup; goto cleanup;
if (!separator)
separator = (property != DESCRIPTION); separator = (property != DESCRIPTION);
has_note = 1; has_note = 1;
} }
@ -1280,10 +1329,10 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
case COMMENT: case COMMENT:
/* There may be more than one. */ /* There may be more than one. */
if (vevent.comm) { if (vevent.comm) {
asprintf(&comment, "%sComment: %s", asprintf(&tmp, "%sComment: %s",
vevent.comm, note); vevent.comm, note);
mem_free(vevent.comm); mem_free(vevent.comm);
vevent.comm = comment; vevent.comm = tmp;
} else } else
vevent.comm = note; vevent.comm = note;
break; break;
@ -1322,6 +1371,8 @@ cleanup:
mem_free(vevent.comm); mem_free(vevent.comm);
if (vevent.stat) if (vevent.stat)
mem_free(vevent.stat); mem_free(vevent.stat);
if (vevent.imp)
mem_free(vevent.imp);
if (vevent.mesg) if (vevent.mesg)
mem_free(vevent.mesg); mem_free(vevent.mesg);
if (vevent.rpt) if (vevent.rpt)
@ -1335,7 +1386,7 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped,
{ {
const int ITEMLINE = *lineno - !feof(fdi); const int ITEMLINE = *lineno - !feof(fdi);
ical_property_e property; ical_property_e property;
char *note = NULL, *comment; char *note = NULL, *tmp;
const char *SEPARATOR = "-- \n"; const char *SEPARATOR = "-- \n";
struct string s; struct string s;
struct { struct {
@ -1430,6 +1481,7 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped,
ICAL_VTODO, ITEMLINE, log); ICAL_VTODO, ITEMLINE, log);
if (!note) if (!note)
goto cleanup; goto cleanup;
if (!separator)
separator = (property != DESCRIPTION); separator = (property != DESCRIPTION);
has_note = 1; has_note = 1;
} }
@ -1453,10 +1505,10 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped,
case COMMENT: case COMMENT:
/* There may be more than one. */ /* There may be more than one. */
if (vtodo.comm) { if (vtodo.comm) {
asprintf(&comment, "%sComment: %s", asprintf(&tmp, "%sComment: %s",
vtodo.comm, note); vtodo.comm, note);
mem_free(vtodo.comm); mem_free(vtodo.comm);
vtodo.comm = comment; vtodo.comm = tmp;
} else } else
vtodo.comm = note; vtodo.comm = note;
break; break;

View File

@ -423,22 +423,25 @@ struct date sec2date(time_t t)
return d; return d;
} }
time_t utcdate2sec(struct date day, unsigned hour, unsigned min) time_t tzdate2sec(struct date day, unsigned hour, unsigned min, char *tznew)
{ {
char *tz; char *tzold;
time_t t; time_t t;
tz = getenv("TZ"); if (!tznew)
if (tz) return date2sec(day, hour, min);
tz = mem_strdup(tz);
setenv("TZ", "", 1);
tzset();
tzold = getenv("TZ");
if (tzold)
tzold = mem_strdup(tzold);
setenv("TZ", tznew, 1);
tzset();
t = date2sec(day, hour, min); t = date2sec(day, hour, min);
if (tz) { if (tzold) {
setenv("TZ", tz, 1); setenv("TZ", tzold, 1);
mem_free(tz); mem_free(tzold);
} else { } else {
unsetenv("TZ"); unsetenv("TZ");
} }

View File

@ -10,4 +10,9 @@ SUMMARY:UTC
DTSTART:20150223T110000Z DTSTART:20150223T110000Z
DURATION:PT1H DURATION:PT1H
END:VEVENT END:VEVENT
BEGIN:VEVENT
SUMMARY:CET
DTSTART;TZID=CET:20150223T110000
DURATION:PT1H
END:VEVENT
END:VCALENDAR END:VCALENDAR

View File

@ -1,8 +1,8 @@
BEGIN:VCALENDAR BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
BEGIN:VEVENT BEGIN:VEVENT
DTSTART;TZID="(UTC+01:00) Amsterdam, Berlin, Bern, Rom, Stockholm, Wien":19800101T000100 DTSTART:19800101T000100
DURATION:P1DT9H17M0S DURATION;TESTPARAM="Quoted string with colon(:), semicolon(;) and comma(,)":P1DT9H17M0S
SUMMARY:Calibrator's SUMMARY:Calibrator's
END:VEVENT END:VEVENT
BEGIN:VTODO BEGIN:VTODO

View File

@ -8,16 +8,20 @@ if [ "$1" = 'actual' ]; then
TZ="America/New_York" "$CALCURSE" -D "$PWD/.calcurse" \ TZ="America/New_York" "$CALCURSE" -D "$PWD/.calcurse" \
-i "$DATA_DIR/ical-007.ical" -i "$DATA_DIR/ical-007.ical"
"$CALCURSE" -D "$PWD/.calcurse" -s02/23/2015 "$CALCURSE" -D "$PWD/.calcurse" -s02/23/2015
cat "$PWD/.calcurse/notes/"*
rm -rf .calcurse || exit 1 rm -rf .calcurse || exit 1
elif [ "$1" = 'expected' ]; then elif [ "$1" = 'expected' ]; then
cat <<EOD cat <<EOD
Import process report: 0013 lines read Import process report: 0018 lines read
2 apps / 0 events / 0 todos / 0 skipped 3 apps / 0 events / 0 todos / 0 skipped
02/23/15: 02/23/15:
- 05:00 -> 06:00
CET
- 06:00 -> 07:00 - 06:00 -> 07:00
UTC UTC
- 11:00 -> 12:00 - 11:00 -> 12:00
Local time Local time
Import: TZID=CET
EOD EOD
else else
./run-test "$0" ./run-test "$0"