2 * ical-dates: library to handle ical dates/times and recurrences.
4 * Given a list of strings, typically from an ical file, we produce
5 * a data structure which encodes the relevant date info.
6 * Given such a structure, a start date, end date, and max count, we
7 * produce a list of dates.
15 #include "ical-dates.h"
17 static int month_days[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
18 static char *days[7] = { "SU","MO","TU","WE","TH","FR","SA"};
19 static char *freqs[] = { "SECONDLY","MINUTELY","HOURLY",
20 "DAILY","WEEKLY","MONTHLY","YEARLY"};
22 static int daysin(int mon, int yr)
25 return month_days[mon-1];
33 /* ical_times can be modified by adding/subtracting fields, but
34 * must then be normalised
36 void ical_set_wday(struct ical_time *t)
41 for (m = 1; m < t->mon; m++)
42 yday += daysin(m, t->yr);
44 wday = yr + yr/4 - yr/400 + yday + 4;
49 void ical_norm_mon(struct ical_time *t)
62 void ical_norm_day(struct ical_time *t)
64 if (t->day >= 1 && t->day <= daysin(t->mon, t->yr)) {
72 t->day += daysin(t->mon, t->yr);
74 while (t->day > daysin(t->mon, t->yr)) {
75 t->day -= daysin(t->mon, t->yr);
82 void ical_norm_hr(struct ical_time *t)
84 if (t->hr >= 0 && t->hr < 24)
94 t->wday = -1; // force wday/yday update
98 void ical_norm_min(struct ical_time *t)
100 if (t->min >= 0 && t->min < 60)
102 while (t->min >= 60) {
113 void ical_norm_sec(struct ical_time *t)
115 if (t->sec >= 0 && t->sec < 60)
117 while (t->sec >= 60) {
128 int ical_ordered(const struct ical_time *a, const struct ical_time *b)
131 return a->yr - b->yr;
132 if (a->yday != b->yday)
133 return a->yday - b->yday;
134 return (a->hr*3600 + a->min*60 + a->sec) -
135 (b->hr*3600 + b->min*60 + b->sec);
138 static int ical_orderedv(const void *av, const void *bv)
140 const struct ical_time *a = av;
141 const struct ical_time *b = bv;
142 return ical_ordered(a,b);
145 void ical_next(struct ical_iter *it)
149 it->now.sec += it->step;
150 ical_norm_sec(&it->now);
153 it->now.min += it->step;
154 ical_norm_min(&it->now);
157 it->now.hr += it->step;
158 ical_norm_hr(&it->now);
161 it->now.day += it->step;
162 ical_norm_day(&it->now);
165 it->now.day += it->step * 7;
166 ical_norm_day(&it->now);
167 ical_set_wday(&it->now);
170 it->now.mon += it->step;
171 ical_norm_mon(&it->now);
172 ical_set_wday(&it->now);
175 it->now.yr += it->step;
176 ical_set_wday(&it->now);
183 static void list_add(struct ical_list *l, int v)
185 if (l->cnt >= l->size) {
186 l->size = l->cnt + 8;
187 l->v = realloc(l->v, l->size * sizeof(v));
192 void ical_list_free(struct ical_list *l)
197 static void timelist_add(struct ical_timelist *tl, struct ical_time *tm)
199 if (tl->cnt >= tl->size) {
200 tl->size = tl->cnt + 8;
201 tl->v = realloc(tl->v, tl->size * sizeof(*tm));
203 tl->v[tl->cnt++] = *tm;
206 void ical_timelist_free(struct ical_timelist *tl)
211 /* When parsing we destroy the argument string */
212 static int parse_bysimple(struct ical_list *list, char *arg, int min, int max, int neg)
216 int l = strcspn(arg, ",");
222 v = strtol(w, &e, 10);
225 if ((v < 0 && !neg) ||
234 static int match_day(char *d)
237 for (n = 0; n < 7; n++)
238 if (strcmp(d, days[n]) == 0)
243 static int parse_byday(struct ical_list lists[7], char *arg)
248 int l = strspn(arg, "0123456789+-SUMOTWEHFRA");
256 d = match_day(w+l-2);
263 v = strtol(w, &e, 10);
267 list_add(&lists[d], v);
272 static enum ical_interval parse_freq(char *arg)
274 enum ical_interval rv;
275 for (rv = Secondly; rv < BadInterval; rv++)
276 if (strcmp(arg, freqs[rv]) == 0)
281 int ical_parse_rrule(struct ical_rrule *rr, char *arg, char *tz)
283 /* ';' separated list of X=Y assignments where Y is a
284 * ',' separated list of values
286 memset(rr, 0, sizeof(*rr));
287 rr->wkst = 1; // MO by default
288 rr->unit = BadInterval;
292 int l = strcspn(arg, ";");
306 if (strcmp(w, "FREQ") == 0) {
307 rr->unit = parse_freq(b);
308 } else if (strcmp(w, "INTERVAL") == 0) {
310 rr->step = strtol(b, &e, 10);
311 if (rr->step < 1 || *b == 0 || *e != 0)
313 } else if (strcmp(w, "COUNT") == 0) {
315 rr->count = strtol(b, &e, 10);
316 if (rr->count < 1 || *b == 0 || *e != 0)
318 } else if (strcmp(w, "UNTIL") == 0) {
319 if (ical_parse_time(&rr->until, b, tz) == 0)
321 } else if (strcmp(w, "BYMONTH") == 0) {
322 if (parse_bysimple(&rr->bymonth, b, 1, 31, 0) == 0)
324 } else if (strcmp(w, "BYWEEKNO") == 0) {
325 if (parse_bysimple(&rr->byweekno, b, 0, 53, 1) == 0)
327 } else if (strcmp(w, "BYYEARDAY") == 0) {
328 if (parse_bysimple(&rr->byyday, b, 1, 366, 1) == 0)
330 } else if (strcmp(w, "BYMONTHDAY") == 0) {
331 if (parse_bysimple(&rr->bymday, b, 1, 31, 1) == 0)
333 } else if (strcmp(w, "BYDAY") == 0) {
334 if (parse_byday(rr->bydays, b) == 0)
337 } else if (strcmp(w, "BYHOUR") == 0) {
338 if (parse_bysimple(&rr->byhr, b, 0, 23, 0) == 0)
340 } else if (strcmp(w, "BYMINUTE") == 0) {
341 if (parse_bysimple(&rr->bymin, b, 0, 59, 0) == 0)
343 } else if (strcmp(w, "BYSECOND") == 0) {
344 if (parse_bysimple(&rr->bysec, b, 0, 59, 0) == 0)
346 } else if (strcmp(w, "BYSETPOS") == 0) {
347 if (parse_bysimple(&rr->setpos, b, 1, 366, 1) == 0)
349 } else if (strcmp(w, "WKST") == 0) {
350 rr->wkst = match_day(b);
356 /* FREQ is required. Everything else is optional */
357 if (rr->unit == BadInterval)
362 void ical_rrule_free(struct ical_rrule *rr)
365 ical_list_free(&rr->bysec);
366 ical_list_free(&rr->bymin);
367 ical_list_free(&rr->byhr);
368 ical_list_free(&rr->bymonth);
369 ical_list_free(&rr->bymday);
370 ical_list_free(&rr->byyday);
371 ical_list_free(&rr->byweekno);
372 ical_list_free(&rr->setpos);
374 ical_list_free(&rr->bydays[i]);
377 static int getnum(char *arg, int len)
381 n = n*10 + arg[0] - '0';
388 int ical_parse_time(struct ical_time *tm, char *arg, char *tz)
390 /* 'arg' can by a date or datetime possibly followed by
391 * another date-or-datetime or a period.
392 * For now we just parse the start date[time]
396 memset(tm, 0, sizeof(*tm));
397 l = strspn(arg, "0123456789");
400 tm->yr = getnum(arg, 4);
401 tm->mon = getnum(arg+4, 2);
402 tm->day = getnum(arg+6, 2);
403 if (tm->mon < 1 || tm->mon > 12 ||
404 tm->day < 1 || tm->day > daysin(tm->mon, tm->yr))
411 /* Need a time now! */
414 l = strspn(arg, "0123456789");
417 tm->hr = getnum(arg, 2);
418 tm->min = getnum(arg+2, 2);
419 tm->sec = getnum(arg+4, 2);
422 /* Leap-sec. Cannot cope, sorry */
424 if (tm->hr >= 24 || tm->min >= 60 || tm->sec >= 60)
436 /* Ignore duration */
440 int ical_parse_time_list(struct ical_timelist *tl, char *arg, char *tz)
444 int l = strcspn(arg, ",");
450 if (ical_parse_time(&tm, w, tz) == 0)
452 timelist_add(tl, &tm);
457 int ical_parse_dates_line(struct ical_dates *id, char *arg)
459 int l = strcspn(arg, ":");
469 /* Look at params, but only care about TZID= */
470 while ((param = strchr(param, ';')) != NULL) {
472 if (strncmp(param, "TZID=", 5) == 0)
476 if (strcmp(arg, "DTSTART") == 0)
477 return ical_parse_time(&id->start, body, tz);
478 if (strcmp(arg, "RRULE") == 0)
479 return ical_parse_rrule(&id->rr, body, tz);
480 if (strcmp(arg, "RDATE") == 0)
481 return ical_parse_time_list(&id->rdate, body, tz);
482 if (strcmp(arg, "EXDATE") == 0)
483 return ical_parse_time_list(&id->exdate, body, tz);
485 /*put it back like we found it */
495 void ical_dates_free(struct ical_dates *d)
497 ical_rrule_free(&d->rr);
498 ical_timelist_free(&d->rdate);
499 ical_timelist_free(&d->exdate);
502 static int inlist(struct ical_list *l, int v)
505 for (i = 0; i < l->cnt; i++)
511 static int set_mon(struct ical_time *tm, int v, int wkst)
513 if (tm->day > daysin(v, tm->yr))
519 static int get_mon(struct ical_time *tm)
524 static int set_mday(struct ical_time *tm, int v, int wkst)
526 int d = daysin(tm->mon, tm->yr);
535 static int test_mday(struct ical_time *tm, int v)
539 v = daysin(tm->mon, tm->yr) + 1 + v;
543 static int set_yday(struct ical_time *tm, int v, int wkst)
545 int d = daysin(2, tm->yr) - 28 + 365;
552 for (m=1; m<=12; m++) {
553 int md = daysin(m, tm->yr);
563 static int test_yday(struct ical_time *tm, int v)
566 int d = daysin(2, tm->yr) - 28 + 365;
569 return tm->yday == v;
572 static int set_weekno(struct ical_time *tm, int v, int wkst)
574 struct ical_time st = *tm;
581 while (st.wday != wkst) {
591 while (st.wday != wkst) {
599 /* Now at the start of the week */
600 for (i=0; i<7; i++) {
601 if (st.yr == tm->yr &&
602 st.wday == tm->wday) {
613 static int set_hr(struct ical_time *tm, int v, int wkst)
619 static int get_hr(struct ical_time *tm)
624 static int set_min(struct ical_time *tm, int v, int wkst)
630 static int get_min(struct ical_time *tm)
635 static int set_sec(struct ical_time *tm, int v, int wkst)
641 static int get_sec(struct ical_time *tm)
646 static void expand(struct ical_timelist *tl,
647 int (*set)(struct ical_time *tm, int v, int wkst),
648 struct ical_list *l, int wkst)
650 struct ical_timelist newl = {0};
652 for (i = 0; i < tl->cnt; i++) {
653 for (j = 0; j < l->cnt; j++) {
654 struct ical_time t = tl->v[i];
655 if (set(&t, l->v[j], wkst)) {
657 timelist_add(&newl, &t);
661 ical_timelist_free(tl);
665 static void expand_byday(struct ical_timelist *tl,
666 struct ical_list *dl, int wkst, enum ical_interval xtype)
668 struct ical_timelist newl = {0};
670 for (i = 0; i < tl->cnt; i++) {
674 if (xtype == Yearly) {
675 /* Every relevant day in this year */
678 days = daysin(2, st.yr) - 28 + 365;
679 } else if (xtype == Monthly) {
680 /* Every relevant day in this month */
682 days = daysin(st.mon, st.yr);
684 /* The relevant day in this week */
685 while (st.wday != wkst) {
693 for (d = 0 ; d < days; d++) {
694 struct ical_list *l = dl + st.wday;
695 if (inlist(l, 0) || inlist(l, d/7+1) || inlist(l, -((days-d)/7+1)))
696 timelist_add(&newl, &st);
702 ical_timelist_free(tl);
706 static void filter(struct ical_timelist *tl, int(*get)(struct ical_time *tm),
709 struct ical_timelist newl = {0};
711 for (i = 0; i < tl->cnt; i++)
712 if (inlist(l, get(&tl->v[i])))
713 timelist_add(&newl, &tl->v[i]);
715 ical_timelist_free(tl);
719 static void filter_byday(struct ical_timelist *tl,
722 struct ical_timelist newl = {0};
724 for (i = 0; i < tl->cnt; i++)
725 if (inlist(l+tl->v[i].wday, 0))
726 timelist_add(&newl, &tl->v[i]);
728 ical_timelist_free(tl);
732 static void filter_test(struct ical_timelist *tl,
733 int(*test)(struct ical_time *tm, int v),
736 struct ical_timelist newl = {0};
738 for (i = 0; i < tl->cnt; i++)
739 for (j = 0; j < l->cnt; j++)
740 if (test(&tl->v[i], l->v[j])) {
741 timelist_add(&newl, &tl->v[i]);
744 ical_timelist_free(tl);
748 static void filter_setpos(struct ical_timelist *tl, struct ical_list *l)
750 struct ical_timelist newl = {0};
752 for (i = 0; i < l->cnt; i++) {
760 if (j >= 0 && j < tl->cnt)
761 timelist_add(&newl, &tl->v[i]);
763 ical_timelist_free(tl);
767 static void dedup(struct ical_timelist *tl)
770 for (i = 1; i < tl->cnt; i++) {
771 if (ical_ordered(&tl->v[i-1], &tl->v[i]) == 0) {
773 memmove(&tl->v[i], &tl->v[i+1], (tl->cnt - i)*sizeof(tl->v[0]));
779 static void trim(struct ical_timelist *tl, struct ical_time *tm)
782 for (i = 0; i < tl->cnt; i++)
783 if (ical_ordered(&tl->v[i], tm) >= 0)
788 memmove(tl->v, tl->v+i, tl->cnt * sizeof(tl->v[0]));
791 static void timelist_join(struct ical_timelist *a, struct ical_timelist *b)
793 if (a->size < a->cnt + b->cnt) {
794 a->size = a->cnt + b->cnt;
795 a->v = realloc(a->v, a->size * sizeof(a->v[0]));
797 memcpy(&a->v[a->cnt], b->v, b->cnt * sizeof(b->v[0]));
799 ical_timelist_free(b);
802 void ical_rr_dates(struct ical_time *start, struct ical_rrule *rr,
803 struct ical_time *from, int max,
804 struct ical_timelist *rv)
806 /* generate dates from the given info an return some in 'rv'
807 * We only report dates at or after 'from' and at most 'max'
810 struct ical_time last;
813 if (start->mon == 0 || rr->unit == BadInterval)
821 if (rr->count > 0 && rr->count < max)
826 (rr->until.mon == 0 || ical_ordered(&last, &rr->until) < 0));
829 struct ical_timelist nl = {0};
830 timelist_add(&nl, &it.now);
833 if (rr->bymonth.cnt) {
834 if (rr->unit > Monthly)
835 expand(&nl, set_mon, &rr->bymonth, rr->wkst);
837 filter(&nl, get_mon, &rr->bymonth);
840 if (rr->byweekno.cnt) {
841 if (rr->unit == Yearly)
842 expand(&nl, set_weekno, &rr->byweekno, rr->wkst);
845 if (rr->byyday.cnt) {
846 if (rr->unit == Yearly)
847 expand(&nl, set_yday, &rr->byyday, rr->wkst);
848 else if (rr->unit < Daily)
849 filter_test(&nl, test_yday, &rr->byyday);
852 if (rr->bymday.cnt) {
853 if (rr->unit > Weekly)
854 expand(&nl, set_mday, &rr->bymday, rr->wkst);
855 else if (rr->unit < Weekly)
856 filter_test(&nl, test_mday, &rr->bymday);
859 if (rr->any_bydays) {
860 enum ical_interval xtype = BadInterval;
863 xtype = Weekly; break;
865 if (rr->bymday.cnt == 0)
869 if (rr->byyday.cnt || rr->bymday.cnt)
870 /* select, not expand */;
871 else if (rr->byweekno.cnt)
873 else if (rr->bymonth.cnt)
879 if (xtype == BadInterval)
880 filter_byday(&nl, rr->bydays);
882 expand_byday(&nl, rr->bydays, rr->wkst, xtype);
886 if (rr->unit > Hourly)
887 expand(&nl, set_hr, &rr->byhr, rr->wkst);
889 filter(&nl, get_hr, &rr->byhr);
893 if (rr->unit > Minutely)
894 expand(&nl, set_min, &rr->bymin, rr->wkst);
896 filter(&nl, get_min, &rr->bymin);
900 if (rr->unit > Secondly)
901 expand(&nl, set_sec, &rr->bysec, rr->wkst);
903 filter(&nl, get_sec, &rr->bysec);
906 qsort(nl.v, nl.cnt, sizeof(nl.v[0]), ical_orderedv);
909 filter_setpos(&nl, &rr->setpos);
913 timelist_join(rv, &nl);
915 last = rv->v[rv->cnt-1];
922 int ical_strftime(char *buf, int max, const char *fmt, struct ical_time *itm)
925 tm.tm_sec = itm->sec;
926 tm.tm_min = itm->min;
927 tm.tm_hour = itm->hr;
928 tm.tm_mday = itm->day;
929 tm.tm_mon = itm->mon - 1;
930 tm.tm_year = itm->yr - 1900;
931 tm.tm_wday = itm->wday;
932 tm.tm_yday = itm->yday - 1;
934 return strftime(buf, max, fmt, &tm);
937 void ical_localtime(struct ical_time *itm, const time_t *timep)
940 localtime_r(timep, &tm);
943 itm->sec = tm.tm_sec;
944 itm->min = tm.tm_min;
945 itm->hr = tm.tm_hour;
946 itm->day = tm.tm_mday;
947 itm->mon = tm.tm_mon + 1;
948 itm->yr = tm.tm_year + 1900;
949 itm->wday = tm.tm_wday;
950 itm->yday = tm.tm_yday + 1;
951 itm->zone = tzname[tm.tm_isdst];
954 time_t ical_mktime(struct ical_time *itm)
957 tm.tm_sec = itm->sec;
958 tm.tm_min = itm->min;
959 tm.tm_hour = itm->hr;
960 tm.tm_mday = itm->day;
961 tm.tm_mon = itm->mon - 1;
962 tm.tm_year = itm->yr - 1900;
963 tm.tm_wday = itm->wday;
964 tm.tm_yday = itm->yday - 1;
969 int ical_fmt_time(char *buf, int size, struct ical_time *tm)
975 rv += snprintf(buf+rv, size-rv, "%04d%02d%02d", tm->yr, tm->mon, tm->day);
976 if (tm->time_set == 0)
978 rv += snprintf(buf+rv, size-rv, "T%02d%02d%02d%s", tm->hr, tm->min, tm->sec,
979 (tm->zone && strcmp(tm->zone, "UTC")==0) ? "Z":"");
983 int fmt_list(char *buf, int size, char *str, struct ical_list *l)
987 rv += snprintf(buf+rv, size-rv, "%s", str);
988 for (i = 0; i < l->cnt; i++) {
991 rv += snprintf(buf+rv, size-rv, "%d", l->v[i]);
996 int fmt_days(char *buf, int size, char *str, struct ical_list *l)
1001 rv += snprintf(buf+rv, size-rv, "%s", str);
1002 for (d = 0; d < 7; d++)
1003 for (i = 0; i < l[d].cnt; i++) {
1004 if (!first && rv < size)
1008 rv += snprintf(buf+rv, size-rv, "%d%s", l[d].v[i], days[d]);
1010 rv += snprintf(buf+rv, size-rv, "%s", days[d]);
1015 int ical_fmt_rr(char *buf, int size, struct ical_rrule *rr)
1018 if (rr->unit == BadInterval)
1021 rv += snprintf(buf+rv, size-rv, "FREQ=%s", freqs[rr->unit]);
1023 rv += snprintf(buf+rv, size-rv, ";INTERVAL=%d", rr->step);
1025 rv += snprintf(buf+rv, size-rv, ";COUNT=%d", rr->count);
1026 if (rr->until.mon) {
1027 rv += snprintf(buf+rv, size-rv, ";UNTIL=");
1028 rv += ical_fmt_time(buf+rv, size-rv, &rr->until);
1031 rv += snprintf(buf+rv, size-rv, ";WKST=%s", days[rr->wkst]);
1032 if (rr->bymonth.cnt)
1033 rv += fmt_list(buf+rv, size-rv, ";BYMONTH=", &rr->bymonth);
1034 if (rr->byweekno.cnt)
1035 rv += fmt_list(buf+rv, size-rv, ";BYWEEKNO=", &rr->byweekno);
1037 rv += fmt_list(buf+rv, size-rv, ";BYYEARDAY=", &rr->byyday);
1039 rv += fmt_list(buf+rv, size-rv, ";BYMONTHDAY=", &rr->bymday);
1041 rv += fmt_days(buf+rv, size-rv, ";BYDAY=", rr->bydays);
1043 rv += fmt_list(buf+rv, size-rv, ";BYHOUR=", &rr->byhr);
1045 rv += fmt_list(buf+rv, size-rv, ";BYMINUTE=", &rr->bymin);
1047 rv += fmt_list(buf+rv, size-rv, ";BYSECOND=", &rr->bysec);