Update import of basic recurrence rules

Conversion of COUNT to UNTIL was a simple calculation which assumed one
repetiton per period (day, week, month or year); it does not take exception
days and invalid dates into account. Solved by a new function which returns the
n'th occurrence of a recurrence rule.

In calcurse UNTIL is interpreted as a day (DATE), in RFC 5545 as a time of day
(DATE-TIME). This has implications when a recurrence rule has an occurrence on
the UNTIL day, see comment in ical.c

An "Import:" note is added when a multi-day event is imported and turned into a
calcurse all-day event.

Icalendar quotes in comments have been updated to RFC 5545.

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-27 21:49:36 +02:00 committed by Lukas Fleischer
parent d8c1c48e78
commit d2791b046a
8 changed files with 336 additions and 135 deletions

View File

@ -1106,6 +1106,7 @@ void recur_apoint_switch_notify(struct recur_apoint *);
void recur_event_paste_item(struct recur_event *, time_t); void recur_event_paste_item(struct recur_event *, time_t);
void recur_apoint_paste_item(struct recur_apoint *, time_t); void recur_apoint_paste_item(struct recur_apoint *, time_t);
int recur_next_occurrence(time_t, long, struct rpt *, llist_t *, time_t, time_t *); int recur_next_occurrence(time_t, long, struct rpt *, llist_t *, time_t, time_t *);
int recur_nth_occurrence(time_t, long, struct rpt *, llist_t *, int, time_t *);
/* sigs.c */ /* sigs.c */

View File

@ -65,8 +65,8 @@ typedef enum {
typedef struct { typedef struct {
enum recur_type type; enum recur_type type;
int freq; unsigned freq;
long until; time_t until;
unsigned count; unsigned count;
} ical_rpt_t; } ical_rpt_t;
@ -373,8 +373,12 @@ static void ical_store_todo(int priority, int completed, char *mesg,
erase_note(&note); erase_note(&note);
} }
/*
* Calcurse limitation: events are one-day (all-day), and all multi-day events
* are turned into one-day events; a note has been added by ical_read_event().
*/
static void static void
ical_store_event(char *mesg, char *note, long day, long end, ical_store_event(char *mesg, char *note, time_t day, time_t end,
ical_rpt_t *irpt, llist_t *exc, const char *fmt_ev, ical_rpt_t *irpt, llist_t *exc, const char *fmt_ev,
const char *fmt_rev) const char *fmt_rev)
{ {
@ -382,6 +386,10 @@ ical_store_event(char *mesg, char *note, long day, long end,
struct event *ev; struct event *ev;
struct recur_event *rev; struct recur_event *rev;
/*
* Repeating event. The end day is ignored, and the event becomes
* one-day even if multi-day.
*/
if (irpt) { if (irpt) {
struct rpt rpt; struct rpt rpt;
rpt.type = irpt->type; rpt.type = irpt->type;
@ -398,7 +406,8 @@ ical_store_event(char *mesg, char *note, long day, long end,
goto cleanup; goto cleanup;
} }
if (end == 0 || end - day <= DAYINSEC) { /* Ordinary one-day event. */
if (end - day <= DAYINSEC) {
ev = event_new(mesg, note, day, EVENTID); ev = event_new(mesg, note, day, EVENTID);
if (fmt_ev) if (fmt_ev)
print_event(fmt_ev, day, ev); print_event(fmt_ev, day, ev);
@ -406,17 +415,14 @@ ical_store_event(char *mesg, char *note, long day, long end,
} }
/* /*
* Here we have an event that spans over several days. * Ordinary multi-day event. The event is turned into a daily repeating
* * event until the day before the end. In iCal, the end day is
* In iCal, the end specifies when the event is supposed to end, in * exclusive, the until day inclusive.
* calcurse, the end specifies the time that the last occurrence of the
* event starts, so we need to do some conversion here.
*/ */
end = day + ((end - day - 1) / DAYINSEC) * DAYINSEC;
struct rpt rpt; struct rpt rpt;
rpt.type = RECUR_DAILY; rpt.type = RECUR_DAILY;
rpt.freq = 1; rpt.freq = 1;
rpt.until = end; rpt.until = day + ((end - day - 1) / DAYINSEC) * DAYINSEC;
LLIST_INIT(&rpt.bymonth); LLIST_INIT(&rpt.bymonth);
LLIST_INIT(&rpt.bywday); LLIST_INIT(&rpt.bywday);
LLIST_INIT(&rpt.bymonthday); LLIST_INIT(&rpt.bymonthday);
@ -431,13 +437,14 @@ cleanup:
} }
static void static void
ical_store_apoint(char *mesg, char *note, long start, long dur, ical_store_apoint(char *mesg, char *note, time_t start, long dur,
ical_rpt_t * irpt, llist_t * exc, int has_alarm, ical_rpt_t * irpt, llist_t * exc, int has_alarm,
const char *fmt_apt, const char *fmt_rapt) const char *fmt_apt, const char *fmt_rapt)
{ {
char state = 0L; char state = 0L;
struct apoint *apt; struct apoint *apt;
struct recur_apoint *rapt; struct recur_apoint *rapt;
time_t day;
if (has_alarm) if (has_alarm)
state |= APOINT_NOTIFY; state |= APOINT_NOTIFY;
@ -450,8 +457,25 @@ ical_store_apoint(char *mesg, char *note, long start, long dur,
LLIST_INIT(&rpt.bywday); LLIST_INIT(&rpt.bywday);
LLIST_INIT(&rpt.bymonthday); LLIST_INIT(&rpt.bymonthday);
rpt.exc = *exc; rpt.exc = *exc;
rapt = recur_apoint_new(mesg, note, start, dur, state, &rpt); /*
* In calcurse, "until" is interpreted as a day (DATE) - hours,
* minutes and seconds are ignored - whereas in iCal the full
* DATE-TIME value of "until" is taken into account. It follows
* that if the event in calcurse has an occurrence on the until
* day, and the start time is after the until value, the
* calcurse until day must be changed to the day before.
*/
if (rpt.until) {
day = update_time_in_date(rpt.until, 0, 0);
if (recur_item_find_occurrence(start, dur, &rpt, NULL,
day, NULL) &&
get_item_time(rpt.until) < get_item_time(start))
rpt.until = date_sec_change(day, 0, -1);
else
rpt.until = day;
}
mem_free(irpt); mem_free(irpt);
rapt = recur_apoint_new(mesg, note, start, dur, state, &rpt);
if (fmt_rapt) if (fmt_rapt)
print_recur_apoint(fmt_rapt, start, rapt->start, rapt); print_recur_apoint(fmt_rapt, start, rapt->start, rapt);
} else { } else {
@ -715,6 +739,7 @@ static long ical_durtime2long(char *timestr)
* dur-second = 1*DIGIT "S" * dur-second = 1*DIGIT "S"
* dur-day = 1*DIGIT "D" * dur-day = 1*DIGIT "D"
* *
* For events, duration must be days or weeks.
* Example: A duration of 15 days, 5 hours and 20 seconds would be: * Example: A duration of 15 days, 5 hours and 20 seconds would be:
* P15DT5H0M20S * P15DT5H0M20S
* A duration of 7 weeks would be: * A duration of 7 weeks would be:
@ -743,39 +768,32 @@ static long ical_dur2long(char *durstr, ical_vevent_e type)
else if (sscanf(p, "%u%c%n", &day, &c, &bytes_read) == 2 && c == 'D') { else if (sscanf(p, "%u%c%n", &day, &c, &bytes_read) == 2 && c == 'D') {
/* dur-date */ /* dur-date */
p += bytes_read; p += bytes_read;
if (*p == 'T' && type == APPOINTMENT) return day * DAYINSEC + (*p == 'T' && type == APPOINTMENT ?
return day * DAYINSEC + ical_durtime2long(p); ical_durtime2long(p) :
else if (*p != 'T' && type == EVENT) 0);
return day * DAYINSEC;
} }
return 0; return 0;
} }
/* /*
* Compute the vevent repetition end date from the repetition count. * Set repetition until date from repetition count
* * for an ical recurrence rule (s, d, i, e).
* Extract from RFC2445:
* The COUNT rule part defines the number of occurrences at which to
* range-bound the recurrence. The "DTSTART" property value, if specified,
* counts as the first occurrence.
*/ */
static long ical_compute_rpt_until(long start, ical_rpt_t * rpt) static void ical_count2until(time_t s, long d, ical_rpt_t *i, llist_t *e,
ical_vevent_e type)
{ {
switch (rpt->type) { struct rpt rpt;
case RECUR_DAILY:
return date_sec_change(start, 0, rpt->freq * (rpt->count - 1)); if (type == EVENT)
case RECUR_WEEKLY: d = -1;
return date_sec_change(start, 0, rpt.type = i->type;
rpt->freq * WEEKINDAYS * (rpt->count - 1)); rpt.freq = i->freq;
case RECUR_MONTHLY: rpt.until = 0;
return date_sec_change(start, rpt->freq * (rpt->count - 1), 0); LLIST_INIT(&rpt.bymonth);
case RECUR_YEARLY: LLIST_INIT(&rpt.bywday);
return date_sec_change(start, LLIST_INIT(&rpt.bymonthday);
rpt->freq * 12 * (rpt->count - 1), 0); recur_nth_occurrence(s, d, &rpt, e, i->count, &i->until);
default:
return 0;
}
} }
/* /*
@ -803,53 +821,52 @@ static char *ical_get_value(char *p)
* Purpose: This value type is used to identify properties that contain * Purpose: This value type is used to identify properties that contain
* a recurrence rule specification. * a recurrence rule specification.
* *
* Formal Definition: The value type is defined by the following * Format Definition: The value type is defined by the following
* notation: * notation:
* *
* recur = "FREQ"=freq *( * recur = recur-rule-part *( ";" recur-rule-part )
* ;
* ; The rule parts are not ordered in any particular sequence.
* ;
* ; The FREQ rule part is REQUIRED,
* ; but MUST NOT occur more than once.
* ;
* ; The UNTIL or COUNT rule parts are OPTIONAL,
* ; but they MUST NOT occur in the same 'recur'.
* ;
* ; The other rule parts are OPTIONAL,
* ; but MUST NOT occur more than once.
* *
* ; either UNTIL or COUNT may appear in a 'recur', * recur-rule-part = ( "FREQ"=freq )
* ; but UNTIL and COUNT MUST NOT occur in the same 'recur' * / ( "UNTIL" "=" enddate )
* * / ( "COUNT" "=" 1*DIGIT )
* ( ";" "UNTIL" "=" enddate ) / * / ( "INTERVAL" "=" 1*DIGIT )
* ( ";" "COUNT" "=" 1*DIGIT ) / * / ( "BYSECOND" "=" byseclist )
* * / ( "BYMINUTE" "=" byminlist )
* ; the rest of these keywords are optional, * / ( "BYHOUR" "=" byhrlist )
* ; but MUST NOT occur more than * / ( "BYDAY" "=" bywdaylist )
* ; once * / ( "BYMONTHDAY" "=" bymodaylist )
* * / ( "BYYEARDAY" "=" byyrdaylist )
* ( ";" "INTERVAL" "=" 1*DIGIT ) / * / ( "BYWEEKNO" "=" bywknolist )
* ( ";" "BYSECOND" "=" byseclist ) / * / ( "BYMONTH" "=" bymolist )
* ( ";" "BYMINUTE" "=" byminlist ) / * / ( "BYSETPOS" "=" bysplist )
* ( ";" "BYHOUR" "=" byhrlist ) / * / ( "WKST" "=" weekday )
* ( ";" "BYDAY" "=" bywdaylist ) / */
* ( ";" "BYMONTHDAY" "=" bymodaylist ) /
* ( ";" "BYYEARDAY" "=" byyrdaylist ) /
* ( ";" "BYWEEKNO" "=" bywknolist ) /
* ( ";" "BYMONTH" "=" bymolist ) /
* ( ";" "BYSETPOS" "=" bysplist ) /
* ( ";" "WKST" "=" weekday ) /
* ( ";" x-name "=" text )
* )
*/
static ical_rpt_t *ical_read_rrule(FILE *log, char *rrulestr, static ical_rpt_t *ical_read_rrule(FILE *log, char *rrulestr,
unsigned *noskipped, unsigned *noskipped,
const int itemline, const int itemline,
ical_vevent_e type) ical_vevent_e type)
{ {
const char count[] = "COUNT="; char freqstr[8];
const char interv[] = "INTERVAL=";
char freqstr[BUFSIZ];
unsigned interval;
ical_rpt_t *rpt; ical_rpt_t *rpt;
char *p; char *p, *q;
/* 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."));
return NULL; return NULL;
} }
p = ical_get_value(rrulestr); p = ical_get_value(rrulestr);
if (!p) { if (!p) {
ical_log(log, ICAL_VEVENT, itemline, ical_log(log, ICAL_VEVENT, itemline,
@ -857,69 +874,90 @@ static ical_rpt_t *ical_read_rrule(FILE *log, char *rrulestr,
(*noskipped)++; (*noskipped)++;
return NULL; return NULL;
} }
/* Prepare for scanf(): replace semicolons by spaces. */
for (q = p; (q = strchr(q, ';')); *q = ' ', q++)
;
rpt = mem_malloc(sizeof(ical_rpt_t)); rpt = mem_malloc(sizeof(ical_rpt_t));
memset(rpt, 0, sizeof(ical_rpt_t)); memset(rpt, 0, sizeof(ical_rpt_t));
if (sscanf(p, "FREQ=%s", freqstr) != 1) {
ical_log(log, ICAL_VEVENT, itemline,
_("recurrence frequency not found."));
(*noskipped)++;
mem_free(rpt);
return NULL;
}
if (starts_with(freqstr, "DAILY")) { /* FREQ rule part */
rpt->type = RECUR_DAILY; if ((p = strstr(rrulestr, "FREQ="))) {
} else if (starts_with(freqstr, "WEEKLY")) { if (sscanf(p, "FREQ=%s", freqstr) != 1) {
rpt->type = RECUR_WEEKLY; ical_log(log, ICAL_VEVENT, itemline,
} else if (starts_with(freqstr, "MONTHLY")) { _("frequency not set in rrule."));
rpt->type = RECUR_MONTHLY; (*noskipped)++;
} else if (starts_with(freqstr, "YEARLY")) { mem_free(rpt);
rpt->type = RECUR_YEARLY; return NULL;
}
} else { } else {
ical_log(log, ICAL_VEVENT, itemline, ical_log(log, ICAL_VEVENT, itemline,
_("recurrence frequency not recognized.")); _("frequency absent in rrule."));
(*noskipped)++; (*noskipped)++;
mem_free(rpt); mem_free(rpt);
return NULL; return NULL;
} }
/* if (!strcmp(freqstr, "DAILY"))
* The UNTIL rule part defines a date-time value which bounds the rpt->type = RECUR_DAILY;
* recurrence rule in an inclusive manner. If not present, and the else if (!strcmp(freqstr, "WEEKLY"))
* COUNT rule part is also not present, the RRULE is considered to rpt->type = RECUR_WEEKLY;
* repeat forever. else if (!strcmp(freqstr, "MONTHLY"))
rpt->type = RECUR_MONTHLY;
else if (!strcmp(freqstr, "YEARLY"))
rpt->type = RECUR_YEARLY;
else {
ical_log(log, ICAL_VEVENT, itemline,
_("rrule frequency not supported."));
(*noskipped)++;
mem_free(rpt);
return NULL;
}
* The COUNT rule part defines the number of occurrences at which to /* INTERVAL rule part */
* range-bound the recurrence. The "DTSTART" property value, if rpt->freq = 1;
* specified, counts as the first occurrence. if ((p = strstr(rrulestr, "INTERVAL="))) {
*/ if (sscanf(p, "INTERVAL=%u", &rpt->freq) != 1) {
if ((p = strstr(rrulestr, "UNTIL")) != NULL) { ical_log(log, ICAL_VEVENT, itemline, _("invalid interval."));
(*noskipped)++;
mem_free(rpt);
return NULL;
}
}
/* UNTIL and COUNT rule parts */
if (strstr(rrulestr, "UNTIL=") && strstr(rrulestr, "COUNT=")) {
ical_log(log, ICAL_VEVENT, itemline,
_("either until or count."));
(*noskipped)++;
mem_free(rpt);
return NULL;
}
if ((p = strstr(rrulestr, "UNTIL="))) {
rpt->until = ical_datetime2time_t(strchr(p, '=') + 1, NULL, 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."));
(*noskipped)++; (*noskipped)++;
mem_free(rpt); mem_free(rpt);
return NULL; return NULL;
} }
} else {
unsigned cnt;
char *countstr;
rpt->until = 0;
if ((countstr = strstr(rrulestr, count))) {
countstr += sizeof(count) - 1;
if (sscanf(countstr, "%u", &cnt) == 1)
rpt->count = cnt;
}
} }
rpt->freq = 1; /*
if ((p = strstr(rrulestr, interv))) { * COUNT is converted to UNTIL in ical_read_event() once all recurrence
p += sizeof(interv) - 1; * parameters are known.
if (sscanf(p, "%u", &interval) == 1) */
rpt->freq = interval; if ((p = strstr(rrulestr, "COUNT="))) {
p = strchr(p, '=') + 1;
if (!(sscanf(p, "%u", &rpt->count) == 1 && rpt->count)) {
ical_log(log, ICAL_VEVENT, itemline,
_("invalid count value."));
(*noskipped)++;
mem_free(rpt);
return NULL;
}
} }
return rpt; return rpt;
@ -1083,7 +1121,8 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
llist_t exc; llist_t exc;
ical_rpt_t *rpt; ical_rpt_t *rpt;
char *mesg, *desc, *loc, *comm, *stat, *imp, *note; char *mesg, *desc, *loc, *comm, *stat, *imp, *note;
long start, end, dur; time_t start, end;
long dur;
int has_alarm; int has_alarm;
} vevent; } vevent;
int skip_alarm, has_note, separator; int skip_alarm, has_note, separator;
@ -1115,22 +1154,35 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
_("item start date is not defined.")); _("item start date is not defined."));
goto skip; goto skip;
} }
if (vevent_type == APPOINTMENT && vevent.dur == 0) { /* An APPOINTMENT must always have a duration. */
if (vevent.end != 0) { if (vevent_type == APPOINTMENT && !vevent.dur) {
vevent.dur = vevent.end - vevent.start; vevent.dur = vevent.end ?
} vevent.end - vevent.start :
0;
if (vevent.dur < 0) { }
ical_log(log, ICAL_VEVENT, ITEMLINE, /* An EVENT must always have an end. */
_("item has a negative duration.")); if (vevent_type == EVENT) {
goto skip; if (!vevent.end)
vevent.end = vevent.start + vevent.dur;
vevent.dur = vevent.end - vevent.start;
if (vevent.dur > DAYINSEC) {
/* Add note on multi-day events. */
char *md = _("multi-day event changed "
"to one-day event");
if (vevent.imp) {
asprintf(&tmp, "%s, %s",
vevent.imp, md);
mem_free(vevent.imp);
vevent.imp = tmp;
} else
asprintf(&vevent.imp, "%s", md);
has_note = separator = 1;
} }
} }
if (vevent.rpt && vevent.rpt->count) { if (vevent.rpt && vevent.rpt->count)
vevent.rpt->until = ical_count2until(vevent.start, vevent.dur,
ical_compute_rpt_until(vevent.start, vevent.rpt, &vevent.exc,
vevent.rpt); vevent_type);
}
if (has_note) { if (has_note) {
/* Construct string with note file contents. */ /* Construct string with note file contents. */
string_init(&s); string_init(&s);
@ -1227,7 +1279,11 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
goto skip; goto skip;
} }
} else if (starts_with_ci(buf, "DTEND")) { } else if (starts_with_ci(buf, "DTEND")) {
/* See DTSTART. */ if (vevent.dur) {
ical_log(log, ICAL_VEVENT, ITEMLINE,
_("either end or duration."));
goto skip;
}
if (vevent_type == UNDEFINED) { if (vevent_type == UNDEFINED) {
ical_log(log, ICAL_VEVENT, ITEMLINE, ical_log(log, ICAL_VEVENT, ITEMLINE,
_("need DTSTART to determine " _("need DTSTART to determine "
@ -1255,8 +1311,17 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents,
_("malformed event end time.")); _("malformed event end time."));
goto skip; goto skip;
} }
if (vevent.end <= vevent.start) {
ical_log(log, ICAL_VEVENT, ITEMLINE,
_("end must be later than start."));
goto skip;
}
} else if (starts_with_ci(buf, "DURATION")) { } else if (starts_with_ci(buf, "DURATION")) {
/* See DTSTART. */ if (vevent.end) {
ical_log(log, ICAL_VEVENT, ITEMLINE,
_("either end or duration."));
goto skip;
}
if (vevent_type == UNDEFINED) { if (vevent_type == UNDEFINED) {
ical_log(log, ICAL_VEVENT, ITEMLINE, ical_log(log, ICAL_VEVENT, ITEMLINE,
_("need DTSTART to determine " _("need DTSTART to determine "

View File

@ -1826,3 +1826,23 @@ int recur_next_occurrence(time_t s, long d, struct rpt *r, llist_t *e,
} }
return ret; return ret;
} }
/*
* Finds the nth occurrence of a recurrence rule (s, d, r, e) (incl. the start)
* and returns it in the provided buffer.
*/
int recur_nth_occurrence(time_t s, long d, struct rpt *r, llist_t *e, int n,
time_t *nth)
{
time_t day;
if (n <= 0)
return 0;
for (n--, *nth = s; n > 0; n--) {
day = update_time_in_date(*nth, 0, 0);
if (!recur_next_occurrence(s, d, r, e, day, nth))
break;
}
return !n;
}

View File

@ -22,4 +22,31 @@ EXDATE:20000215T000000
EXDATE:20000223T000000 EXDATE:20000223T000000
SUMMARY:Recurring appointment SUMMARY:Recurring appointment
END:VEVENT END:VEVENT
BEGIN:VEVENT
DTSTART:20200526T120000
DURATION:PT1H17M0S
RRULE:FREQ=DAILY;UNTIL=20200529T130000
SUMMARY: until May 29 2020\, 13:00
END:VEVENT
BEGIN:VEVENT
DTSTART:20200526T120000
DURATION:PT1H17M0S
RRULE:FREQ=DAILY;UNTIL=20200529T110000
SUMMARY: until May 29 2020\, 11:00
END:VEVENT
BEGIN:VEVENT
DTSTART:20200531T214500
DURATION:PT15M0S
RRULE:FREQ=MONTHLY;COUNT=10
EXDATE:20200731T214500,20210131T214500
SUMMARY:monthly on 31th\, count 10\, exceptions 31/7/2020 and 31/1/2021
END:VEVENT
BEGIN:VEVENT
DTSTAMP:
UID:
DTSTART;VALUE=DATE:20200502
DTEND;VALUE=DATE:20200504
DESCRIPTION:The first weekend in May is a two-day event.
SUMMARY:First weekend in May
END:VEVENT
END:VCALENDAR END:VCALENDAR

View File

@ -111,6 +111,17 @@ RRULE:FREQ=MONTHLY;UNTIL=20201030
EXDATE;VALUE=DATE:20200606T120000Z EXDATE;VALUE=DATE:20200606T120000Z
SUMMARY:Invalid EXDATE value SUMMARY:Invalid EXDATE value
END:VEVENT END:VEVENT
BEGIN:VEVENT
DTSTART:20200527T163000
DTEND:20200528T163000
DURATION:P1D
SUMMARY:Both end time and duration
END:VEVENT
BEGIN:VEVENT
DTSTART:20200527T163000
DTEND:20200526T163000
SUMMARY:End time before start
END:VEVENT
BEGIN:VTODO BEGIN:VTODO
SUMMARY:finally\, missing end of item SUMMARY:finally\, missing end of item
END:VCALENDAR END:VCALENDAR

View File

@ -1,4 +1,5 @@
#!/bin/sh #!/bin/sh
# Recurrence rules.
. "${TEST_INIT:-./test-init.sh}" . "${TEST_INIT:-./test-init.sh}"
@ -7,11 +8,13 @@ if [ "$1" = 'actual' ]; then
cp "$DATA_DIR/conf" .calcurse || exit 1 cp "$DATA_DIR/conf" .calcurse || exit 1
"$CALCURSE" -D "$PWD/.calcurse" -i "$DATA_DIR/ical-003.ical" "$CALCURSE" -D "$PWD/.calcurse" -i "$DATA_DIR/ical-003.ical"
"$CALCURSE" -D "$PWD/.calcurse" -s01/01/2000 -r365 "$CALCURSE" -D "$PWD/.calcurse" -s01/01/2000 -r365
"$CALCURSE" -D "$PWD/.calcurse" -s05/01/2020 --to 01/01/2022
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: 0025 lines read Import process report: 0052 lines read
3 apps / 0 events / 0 todos / 0 skipped 6 apps / 1 event / 0 todos / 0 skipped
01/01/00: 01/01/00:
- 00:00 -> 01:30 - 00:00 -> 01:30
Recurring appointment Recurring appointment
@ -131,6 +134,76 @@ Import process report: 0025 lines read
02/29/00: 02/29/00:
- 00:00 -> 01:30 - 00:00 -> 01:30
Recurring appointment Recurring appointment
05/02/20:
* First weekend in May
05/03/20:
* First weekend in May
05/26/20:
- 12:00 -> 13:17
until May 29 2020, 11:00
- 12:00 -> 13:17
until May 29 2020, 13:00
05/27/20:
- 12:00 -> 13:17
until May 29 2020, 11:00
- 12:00 -> 13:17
until May 29 2020, 13:00
05/28/20:
- 12:00 -> 13:17
until May 29 2020, 11:00
- 12:00 -> 13:17
until May 29 2020, 13:00
05/29/20:
- 12:00 -> 13:17
until May 29 2020, 13:00
05/31/20:
- 21:45 -> 22:00
monthly on 31th, count 10, exceptions 31/7/2020 and 31/1/2021
08/31/20:
- 21:45 -> 22:00
monthly on 31th, count 10, exceptions 31/7/2020 and 31/1/2021
10/31/20:
- 21:45 -> 22:00
monthly on 31th, count 10, exceptions 31/7/2020 and 31/1/2021
12/31/20:
- 21:45 -> 22:00
monthly on 31th, count 10, exceptions 31/7/2020 and 31/1/2021
03/31/21:
- 21:45 -> 22:00
monthly on 31th, count 10, exceptions 31/7/2020 and 31/1/2021
05/31/21:
- 21:45 -> 22:00
monthly on 31th, count 10, exceptions 31/7/2020 and 31/1/2021
07/31/21:
- 21:45 -> 22:00
monthly on 31th, count 10, exceptions 31/7/2020 and 31/1/2021
08/31/21:
- 21:45 -> 22:00
monthly on 31th, count 10, exceptions 31/7/2020 and 31/1/2021
10/31/21:
- 21:45 -> 22:00
monthly on 31th, count 10, exceptions 31/7/2020 and 31/1/2021
12/31/21:
- 21:45 -> 22:00
monthly on 31th, count 10, exceptions 31/7/2020 and 31/1/2021
The first weekend in May is a two-day event.
--
Import: multi-day event changed to one-day event
EOD EOD
else else
./run-test "$0" ./run-test "$0"

View File

@ -7,6 +7,7 @@ if [ "$1" = 'actual' ]; then
cp "$DATA_DIR/conf" .calcurse || exit 1 cp "$DATA_DIR/conf" .calcurse || exit 1
"$CALCURSE" -D "$PWD/.calcurse" -i "$DATA_DIR/ical-005.ical" "$CALCURSE" -D "$PWD/.calcurse" -i "$DATA_DIR/ical-005.ical"
"$CALCURSE" -D "$PWD/.calcurse" -s10/03/2013 -r3 "$CALCURSE" -D "$PWD/.calcurse" -s10/03/2013 -r3
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
@ -20,6 +21,7 @@ Import process report: 0023 lines read
10/04/13: 10/04/13:
* Two days * Two days
Import: multi-day event changed to one-day event
EOD EOD
else else
./run-test "$0" ./run-test "$0"

View File

@ -17,10 +17,10 @@ if [ "$1" = 'actual' ]; then
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: 0116 lines read Import process report: 0127 lines read
2 apps / 0 events / 1 todo / 18 skipped 2 apps / 0 events / 1 todo / 20 skipped
VEVENT [12]: invalid or malformed event start time. VEVENT [12]: invalid or malformed event start time.
VEVENT [17]: recurrence frequency not recognized. VEVENT [17]: rrule frequency not supported.
VEVENT [23]: malformed summary line. VEVENT [23]: malformed summary line.
VTODO [28]: item priority is invalid (must be between 0 and 9). VTODO [28]: item priority is invalid (must be between 0 and 9).
VEVENT [32]: malformed exceptions line. VEVENT [32]: malformed exceptions line.
@ -36,7 +36,9 @@ VEVENT [89]: invalid end time value type.
VEVENT [94]: invalid until format. VEVENT [94]: invalid until format.
VEVENT [100]: invalid exception date value type. VEVENT [100]: invalid exception date value type.
VEVENT [107]: invalid exception. VEVENT [107]: invalid exception.
VTODO [114]: The ical file seems to be malformed. The end of item was not found. VEVENT [114]: either end or duration.
VEVENT [120]: end must be later than start.
VTODO [125]: The ical file seems to be malformed. The end of item was not found.
101 101
EOD EOD
else else