diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/calcurse.h | 15 | ||||
-rw-r--r-- | src/ical.c | 9 | ||||
-rw-r--r-- | src/io.c | 26 | ||||
-rw-r--r-- | src/recur.c | 761 | ||||
-rw-r--r-- | src/ui-day.c | 3 | ||||
-rw-r--r-- | src/utils.c | 58 |
6 files changed, 847 insertions, 25 deletions
diff --git a/src/calcurse.h b/src/calcurse.h index a2fea09..6b3905b 100644 --- a/src/calcurse.h +++ b/src/calcurse.h @@ -148,6 +148,12 @@ /* Calendar window. */ #define CALHEIGHT 8 +/* + * Week day numbering (0, 1,..., 6) which depends on the first day of the week. + * The argument (d) is the "Sunday"-numbering of member tm_wday in struct tm. + */ +#define WDAY(d) \ + (ui_calendar_week_begins_on_monday() ? ((d ? d : WEEKINDAYS) - 1) : d) /* Key definitions. */ #define CTRLVAL 0x1F @@ -395,6 +401,9 @@ struct rpt { enum recur_type type; /* FREQ */ int freq; /* INTERVAL */ time_t until; /* UNTIL */ + llist_t bymonth; /* BYMONTH list */ + llist_t bywday; /* BY(WEEK)DAY list */ + llist_t bymonthday; /* BYMONTHDAY list */ llist_t exc; /* EXDATE's */ }; @@ -1076,6 +1085,9 @@ void recur_event_add_exc(struct recur_event *, time_t); void recur_apoint_add_exc(struct recur_apoint *, time_t); void recur_event_erase(struct recur_event *); void recur_apoint_erase(struct recur_apoint *); +void recur_bymonth(llist_t *, FILE *); +void recur_bywday(enum recur_type, llist_t *, FILE *); +void recur_bymonthday(llist_t *, FILE *); void recur_exc_scan(llist_t *, FILE *); void recur_apoint_check_next(struct notify_app *, time_t, time_t); void recur_apoint_switch_notify(struct recur_apoint *); @@ -1240,6 +1252,9 @@ int hash_matches(const char *, const char *); int show_dialogs(void); long overflow_add(long, long, long *); long overflow_mul(long, long, long *); +time_t next_wday(time_t, int); +int wday_per_year(int, int); +int wday_per_month(int, int, int); /* vars.c */ extern int col, row; @@ -387,6 +387,9 @@ ical_store_event(char *mesg, char *note, long day, long end, rpt.type = irpt->type; rpt.freq = irpt->freq; rpt.until = irpt->until; + LLIST_INIT(&rpt.bymonth); + LLIST_INIT(&rpt.bywday); + LLIST_INIT(&rpt.bymonthday); rpt.exc = *exc; rev = recur_event_new(mesg, note, day, EVENTID, &rpt); mem_free(irpt); @@ -414,6 +417,9 @@ ical_store_event(char *mesg, char *note, long day, long end, rpt.type = RECUR_DAILY; rpt.freq = 1; rpt.until = end; + LLIST_INIT(&rpt.bymonth); + LLIST_INIT(&rpt.bywday); + LLIST_INIT(&rpt.bymonthday); rpt.exc = *exc; rev = recur_event_new(mesg, note, day, EVENTID, &rpt); if (fmt_rev) @@ -440,6 +446,9 @@ ical_store_apoint(char *mesg, char *note, long start, long dur, rpt.type = irpt->type; rpt.freq = irpt->freq; rpt.until = irpt->until; + LLIST_INIT(&rpt.bymonth); + LLIST_INIT(&rpt.bywday); + LLIST_INIT(&rpt.bymonthday); rpt.exc = *exc; rapt = recur_apoint_new(mesg, note, start, dur, state, &rpt); mem_free(irpt); @@ -644,7 +644,7 @@ void io_load_app(struct item_filter *filter) &until.tm_mon, &until.tm_mday, &until.tm_year) != 3) io_load_error(path_apts, line, - _("syntax error in item repetition")); + _("syntax error in until date")); if (!check_date(until.tm_year, until.tm_mon, until.tm_mday)) io_load_error(path_apts, line, @@ -659,6 +659,30 @@ void io_load_app(struct item_filter *filter) c = getc(data_file); } else rpt.until = 0; + /* Optional bymonthday list */ + if (c == 'd') { + if (rpt.type == RECUR_WEEKLY) + io_load_error(path_apts, line, + _("BYMONTHDAY illegal with WEEKLY")); + ungetc(c, data_file); + recur_bymonthday(&rpt.bymonthday, data_file); + c = getc(data_file); + } else + LLIST_INIT(&rpt.bymonthday); + /* Optional bywday list */ + if (c == 'w') { + ungetc(c, data_file); + recur_bywday(rpt.type, &rpt.bywday, data_file); + c = getc(data_file); + } else + LLIST_INIT(&rpt.bywday); + /* Optional bymonth list */ + if (c == 'm') { + ungetc(c, data_file); + recur_bymonth(&rpt.bymonth, data_file); + c = getc(data_file); + } else + LLIST_INIT(&rpt.bymonth); /* Optional exception dates */ if (c == '!') { ungetc(c, data_file); diff --git a/src/recur.c b/src/recur.c index 168ba17..53c4f53 100644 --- a/src/recur.c +++ b/src/recur.c @@ -46,6 +46,39 @@ llist_ts_t recur_alist_p; llist_t recur_elist; +static void free_int(int *i) +{ + mem_free(i); +} + +static void free_int_list(llist_t *ilist) +{ + LLIST_FREE_INNER(ilist, free_int); + LLIST_FREE(ilist); +} + +static void int_list_dup(llist_t *l, llist_t *ilist) +{ + llist_item_t *i; + int *o, *p; + + LLIST_INIT(l); + + if (ilist->head) { + LLIST_FOREACH(ilist, i) { + p = LLIST_GET_DATA(i); + o = mem_malloc(sizeof(int)); + *o = *p; + LLIST_ADD(l, o); + } + } +} + +static int int_cmp(int *list, int *i) +{ + return *list == *i; +} + static void free_exc(struct excp *exc) { mem_free(exc); @@ -62,6 +95,11 @@ static int exc_cmp_day(struct excp *a, struct excp *b) return a->st < b->st ? -1 : (a->st == b->st ? 0 : 1); } +static int exc_inday(struct excp *exc, time_t *day_start) +{ + return (date_cmp_day(exc->st, *day_start) == 0); +} + static void recur_add_exc(llist_t * exc, time_t day) { struct excp *o = mem_malloc(sizeof(struct excp)); @@ -273,6 +311,12 @@ struct recur_apoint *recur_apoint_new(char *mesg, char *note, time_t start, rapt->state = state; rapt->rpt = mem_malloc(sizeof(struct rpt)); *rapt->rpt = *rpt; + int_list_dup(&rapt->rpt->bymonth, &rpt->bymonth); + free_int_list(&rpt->bymonth); + int_list_dup(&rapt->rpt->bywday, &rpt->bywday); + free_int_list(&rpt->bywday); + int_list_dup(&rapt->rpt->bymonthday, &rpt->bymonthday); + free_int_list(&rpt->bymonthday); /* * Note. The exception dates are in the list rapt->exc. * The (empty) list rapt->rpt->exc is not used. @@ -300,6 +344,12 @@ struct recur_event *recur_event_new(char *mesg, char *note, time_t day, rev->id = id; rev->rpt = mem_malloc(sizeof(struct rpt)); *rev->rpt = *rpt; + int_list_dup(&rev->rpt->bymonth, &rpt->bymonth); + free_int_list(&rpt->bymonth); + int_list_dup(&rev->rpt->bywday, &rpt->bywday); + free_int_list(&rpt->bywday); + int_list_dup(&rev->rpt->bymonthday, &rpt->bymonthday); + free_int_list(&rpt->bymonthday); /* Similarly as for recurrent appointment. */ recur_exc_dup(&rev->exc, &rpt->exc); recur_free_exc_list(&rpt->exc); @@ -367,6 +417,39 @@ int recur_char2def(char type) return recur_def; } +/* Write the bymonthday list. */ +static void bymonthday_append(struct string *s, llist_t *l) +{ + llist_item_t *i; + + LLIST_FOREACH(l, i) { + int *day = LLIST_GET_DATA(i); + string_catf(s, " d%d", *day); + } +} + +/* Write the bywday list. */ +static void bywday_append(struct string *s, llist_t *l) +{ + llist_item_t *i; + + LLIST_FOREACH(l, i) { + int *wday = LLIST_GET_DATA(i); + string_catf(s, " w%d", *wday); + } +} + +/* Write the bymonth list. */ +static void bymonth_append(struct string *s, llist_t *l) +{ + llist_item_t *i; + + LLIST_FOREACH(l, i) { + int *mon = LLIST_GET_DATA(i); + string_catf(s, " m%d", *mon); + } +} + /* Write days for which recurrent items should not be repeated. */ static void recur_exc_append(struct string *s, llist_t *lexc) { @@ -546,6 +629,9 @@ char *recur_apoint_tostr(struct recur_apoint *o) recur_def2char(o->rpt->type), lt.tm_mon + 1, lt.tm_mday, 1900 + lt.tm_year); } + bymonthday_append(&s, &o->rpt->bymonthday); + bywday_append(&s, &o->rpt->bywday); + bymonth_append(&s, &o->rpt->bymonth); recur_exc_append(&s, &o->exc); string_catf(&s, "} "); if (o->note) @@ -607,6 +693,9 @@ char *recur_event_tostr(struct recur_event *o) recur_def2char(o->rpt->type), end_mon, end_day, end_year); } + bymonthday_append(&s, &o->rpt->bymonthday); + bywday_append(&s, &o->rpt->bywday); + bymonth_append(&s, &o->rpt->bymonth); recur_exc_append(&s, &o->exc); string_catf(&s, "} "); if (o->note) @@ -652,6 +741,20 @@ void recur_save_data(FILE * f) } /* + * Return the month day counted from the opposite end of the month. + */ +static int opp_mday(int year, int month, int day) +{ + EXIT_IF(day == 0, _("month day is zero")); + + int m_days = days[month - 1] + (month == 2 && ISLEAP(year)); + if (day > 0) + return day - 1 - m_days; + else + return day + 1 + m_days; +} + +/* * The two following defines together with the diff_days, diff_months and * diff_years functions were provided by Lukas Fleischer to correct the wrong * calculation of recurrent dates after a turn of year. @@ -702,45 +805,54 @@ static long diff_years(struct tm lt_start, struct tm lt_end) return lt_end.tm_year - lt_start.tm_year; } -static int exc_inday(struct excp *exc, time_t *day_start) +/* + * Return true if 'mon' and 'mday' is month and day of t + * (after a call of mktime()). + */ +static int date_chk(time_t t, int mon, int mday) { - return (date_cmp_day(exc->st, *day_start) == 0); + struct tm tm; + + localtime_r(&t, &tm); + + return tm.tm_mon == mon && tm.tm_mday == mday; } /* - * Return true if the recurrent item has an occurrence on the given day - * and, if so, store the start date of that occurrence in a buffer. + * Return true if the rrule (start, dur, rpt, exc) has an occurrence on the + * given day. If so, save that occurrence in a (dynamic or static) buffer. */ -unsigned -recur_item_find_occurrence(time_t start, long dur, struct rpt *rpt, llist_t *exc, - time_t day_start, time_t *occurrence) +static int find_occurrence(time_t start, long dur, struct rpt *rpt, llist_t *exc, + time_t day, time_t *occurrence) { /* - * Duration fix. + * Duration-on-day-d fix. * An item cannot end on midnight or else it is counted towards the next day. * An event (dur == -1) has no explicit duration, but is considered to last for - * the entire day which depends on DST. + * the entire day (d) which depends on DST. */ #define DUR(d) (dur == -1 ? DAYLEN((d)) - 1 : dur - 1) long diff; - struct tm lt_day, lt_item, lt_occur; - time_t occ; + struct tm lt_day, lt_start, lt_occur; + time_t t; + int mday, order, pwday, nwday, mon; /* Is the given day before the day of the first occurence? */ - if (date_cmp_day(day_start, start) < 0) + if (date_cmp_day(day, start) < 0) return 0; + /* * - or after the day of the last occurrence (which may stretch beyond * the until date)? Extraneous days are eliminated later. */ if (rpt->until && - date_cmp_day(NEXTDAY(rpt->until) + DUR(rpt->until), day_start) < 0) + date_cmp_day(NEXTDAY(rpt->until) + DUR(rpt->until), day) < 0) return 0; - localtime_r(&day_start, <_day); /* Given day. */ - localtime_r(&start, <_item); /* Original item. */ - lt_occur = lt_item; /* First occurence. */ + localtime_r(&day, <_day); /* Given day. */ + localtime_r(&start, <_start); /* Original item. */ + lt_occur = lt_start; /* First occurence. */ /* * Update to the most recent occurrence before or on the selected day. @@ -782,39 +894,584 @@ recur_item_find_occurrence(time_t start, long dur, struct rpt *rpt, llist_t *exc /* Switch to calendar (Unix) time. */ lt_occur.tm_isdst = -1; - occ = mktime(<_occur); + t = mktime(<_occur); /* * Impossible dates must be ignored (according to RFC 5545). Changing * only the year or the month may lead to dates like 29 February in * non-leap years or 31 November. */ - if (rpt->type == RECUR_MONTHLY || rpt->type == RECUR_YEARLY) { - localtime_r(&occ, <_occur); - if (lt_occur.tm_mday != lt_item.tm_mday) + if ((rpt->type == RECUR_MONTHLY || rpt->type == RECUR_YEARLY) && + !date_chk(t, lt_occur.tm_mon, lt_start.tm_mday)) + return 0; + + /* + * BYMONTHDAY reduction + * A month day has two possible list forms. + */ + mday = opp_mday(lt_occur.tm_year + 1900, lt_occur.tm_mon + 1, + lt_occur.tm_mday); + if (rpt->bymonthday.head && + rpt->type == RECUR_DAILY && + !LLIST_FIND_FIRST(&rpt->bymonthday, <_occur.tm_mday, int_cmp) && + !LLIST_FIND_FIRST(&rpt->bymonthday, &mday, int_cmp)) + return 0; + + /* BYDAY reduction for DAILY */ + if (rpt->bywday.head && rpt->type == RECUR_DAILY && + !LLIST_FIND_FIRST(&rpt->bywday, <_occur.tm_wday, int_cmp)) + return 0; + + /* + * BYDAY reduction for MONTHLY + * A weekday has three possible list forms. + */ + if (rpt->bywday.head && + rpt->type == RECUR_MONTHLY && rpt->bymonthday.head) { + /* positive order */ + order = (lt_occur.tm_mday + 6) / WEEKINDAYS; + pwday = order * WEEKINDAYS + lt_occur.tm_wday; + /* negative order */ + order = order + - wday_per_month(lt_occur.tm_mon + 1, + lt_occur.tm_year + 1900, + lt_occur.tm_wday) + - 1; + nwday = order * WEEKINDAYS - lt_occur.tm_wday; + if (!LLIST_FIND_FIRST(&rpt->bywday, <_occur.tm_wday, int_cmp) && + !LLIST_FIND_FIRST(&rpt->bywday, &pwday, int_cmp) && + !LLIST_FIND_FIRST(&rpt->bywday, &nwday, int_cmp)) return 0; } + /* + * BYDAY reduction for YEARLY + * A weekday has three possible list forms. + */ + if (rpt->bywday.head && + rpt->type == RECUR_YEARLY && rpt->bymonthday.head) { + /* positive order */ + order = lt_occur.tm_yday / WEEKINDAYS; + pwday = order * WEEKINDAYS + lt_occur.tm_wday; + /* negative order */ + order = order + - wday_per_year(lt_occur.tm_year + 1900, + lt_occur.tm_wday) + - 1; + nwday = order * WEEKINDAYS - lt_occur.tm_wday; + if (!LLIST_FIND_FIRST(&rpt->bywday, <_occur.tm_wday, int_cmp) && + !LLIST_FIND_FIRST(&rpt->bywday, &pwday, int_cmp) && + !LLIST_FIND_FIRST(&rpt->bywday, &nwday, int_cmp)) + return 0; + } + + /* BYMONTH reduction */ + mon = lt_occur.tm_mon + 1; + if (rpt->bymonth.head && + rpt->type != RECUR_YEARLY && + !LLIST_FIND_FIRST(&rpt->bymonth, &mon, int_cmp)) + return 0; + /* Exception day? */ - if (LLIST_FIND_FIRST(exc, &occ, exc_inday)) + if (LLIST_FIND_FIRST(exc, &t, exc_inday)) return 0; /* Extraneous day? */ - if (rpt->until && occ >= NEXTDAY(rpt->until)) + if (rpt->until && t >= NEXTDAY(rpt->until)) return 0; /* Does it span the given day? */ - if (occ + DUR(occ) < day_start) + if (t + DUR(t) < day) return 0; if (occurrence) - *occurrence = occ; + *occurrence = t; return 1; #undef ITEM_DUR } #undef DUR +/* + * Return true if the rrule (s, d, r, e) has an occurrence, depending + * on the frequency, in the year, month or week of day. + */ +static int freq_chk(time_t day, time_t s, long d, struct rpt *r, llist_t *e) +{ + if (r->type == RECUR_DAILY) + EXIT(_("no daily frequency check")); + + struct tm tm_start, tm_day; + struct rpt fc_rpt; + time_t fc_day, fc_s; + + localtime_r(&s, &tm_start); + localtime_r(&day, &tm_day); + + if (r->type == RECUR_WEEKLY) { + /* Set day to the weekly occurrence. */ + fc_day = date_sec_change( + day, + 0, + WDAY(tm_start.tm_wday) - WDAY(tm_day.tm_wday) + ); + fc_s = s; + } else { + /* The start day may be invalid in some months. */ + tm_day.tm_mday = tm_start.tm_mday = 1; + if (r->type == RECUR_YEARLY) + tm_day.tm_mon = tm_start.tm_mon; + tm_day.tm_isdst = tm_start.tm_isdst = -1; + fc_day = mktime(&tm_day); + fc_s = mktime(&tm_start); + } + /* Turn all reductions off. */ + fc_rpt = *r; + fc_rpt.until = 0; + fc_rpt.bymonth.head = fc_rpt.bywday.head = fc_rpt.bymonthday.head = NULL; + + return find_occurrence(fc_s, d, &fc_rpt, e, fc_day, NULL); +} + +/* + * Return true if the rrule (s, d, r, e) has an occurrence on 'day' after + * 'first'; if so, return it in occurrence. + */ +static int test_occurrence(time_t s, long d, struct rpt *r, llist_t *e, + time_t first, time_t day, time_t *occurrence) +{ + time_t occ; + + if (find_occurrence(s, d, r, e, day, &occ)) { + if (occ < first) + return 0; + if (occurrence) + *occurrence = occ; + return 1; + } + return 0; +} + +#define NO_EXPANSION -1 +static int expand_weekly(time_t start, long dur, struct rpt *rpt, llist_t *exc, + time_t day, time_t *occurrence) +{ + struct tm tm_start; + llist_item_t *i; + int *w; + time_t w_start; + + localtime_r(&start, &tm_start); + + /* BYDAY expansion */ + if (rpt->bywday.head) { + LLIST_FOREACH(&rpt->bywday, i) { + w = LLIST_GET_DATA(i); + if (*w < 0 || *w > 6) + continue; + /* + * Modify rrule start with a new day in the same week as + * start - taking first day of the week into account. + */ + w_start = date_sec_change( + start, + 0, + WDAY(*w) - WDAY(tm_start.tm_wday) + ); + if (test_occurrence(w_start, dur, rpt, exc, + start, day, occurrence)) + return 1; + } + } else + return NO_EXPANSION; + + /* No occurrence */ + return 0; +} + +static int expand_monthly(time_t start, long dur, struct rpt *rpt, llist_t *exc, + time_t day, time_t *occurrence) +{ + struct tm tm_start, tm_day; + llist_item_t *i; + int *w, mday, mon, valid; + time_t nstart; + struct rpt r = *rpt; + + localtime_r(&day, &tm_day); + + /* + * The following three conditional alternatives are mutually exclusive + * and cover all four cases of two booleans. + */ + + /* BYMONTHDAY expansion */ + if (rpt->bymonthday.head) { + LLIST_FOREACH(&rpt->bymonthday, i) { + mday = *(int *)LLIST_GET_DATA(i); + + if (mday < 0) + mday = opp_mday(tm_day.tm_year + 1900, + tm_day.tm_mon + 1, mday); + /* + * Modify rrule start with a new monthday. + * If it is invalid (29, 30 or 31) in the start month, + * the month is changed to an earlier one matching the + * frequency. + */ + localtime_r(&start, &tm_start); + mon = tm_start.tm_mon; + + tm_start.tm_mday = mday; + tm_start.tm_isdst = -1; + nstart = mktime(&tm_start); + valid = date_chk(nstart, mon, mday); + /* Never valid? */ + if (!valid && !(rpt->freq % 12)) + return 0; + /* Note. The loop will terminate! */ + while (!valid) { + localtime_r(&start, &tm_start); + mon -= rpt->freq; + tm_start.tm_mon = mon; + tm_start.tm_mday = mday; + tm_start.tm_isdst = -1; + nstart = mktime(&tm_start); + valid = date_chk(nstart, (mon + 12) % 12, mday); + } + if (test_occurrence(nstart, dur, rpt, exc, + start, day, occurrence)) + return 1; + } + } + /* BYDAY special expansion for MONTHLY */ + else if (rpt->bywday.head) { + /* The frequency is modified later. */ + if (!freq_chk(day, start, dur, rpt, exc)) + return 0; + + LLIST_FOREACH(&rpt->bywday, i) { + w = LLIST_GET_DATA(i); + + int order, wday; + + localtime_r(&start, &tm_start); + /* + * Construct a weekly rrule; BYMONTH-reduction in + * find_occurrence() will reduce to the bymonth list. + */ + r.type = RECUR_WEEKLY; + if (*w > 6) { + /* + * A single occurrence counting forwards from + * the start of the month. + */ + order = *w / WEEKINDAYS; + wday = *w % WEEKINDAYS; + r.freq = order; + tm_start.tm_mday = 1; + tm_start.tm_mon = tm_day.tm_mon; + tm_start.tm_year = tm_day.tm_year; + tm_start.tm_isdst = -1; + /* Start in the week before the month. */ + nstart = date_sec_change( + next_wday(mktime(&tm_start), wday), + 0, + -WEEKINDAYS + ); + r.until = date_sec_change( + update_time_in_date(nstart, 0, 0), + 0, + r.freq * WEEKINDAYS + ); + if (rpt->until && r.until > rpt->until) + return 0; + } else if (*w > -1) { + /* Expansion to each week. */ + wday = *w % WEEKINDAYS; + r.freq = 1; + nstart = next_wday(start, wday); + } else if (*w < -6) { + /* + * A single ocurrence counting backwards from + * the end of the month. + */ + order = -(*w) / WEEKINDAYS; + wday = -(*w) % WEEKINDAYS; + r.freq = wday_per_month( + tm_day.tm_mon + 1, + tm_day.tm_year + 1900, + wday + ) - order + 1; + tm_start.tm_mday = 1; + tm_start.tm_mon = tm_day.tm_mon; + tm_start.tm_year = tm_day.tm_year; + tm_start.tm_isdst = -1; + nstart = date_sec_change( + next_wday(mktime(&tm_start), wday), + 0, + -WEEKINDAYS + ); + r.until = date_sec_change( + update_time_in_date(nstart, 0, 0), + 0, + r.freq * WEEKINDAYS + ); + if (rpt->until && r.until > rpt->until) + return 0; + } else + EXIT(_("illegal BYDAY value")); + + if (test_occurrence(nstart, dur, &r, exc, + start, day, occurrence)) + return 1; + } + } + else + return NO_EXPANSION; + + /* No occurrence */ + return 0; +} + +static int expand_yearly(time_t start, long dur, struct rpt *rpt, llist_t *exc, + time_t day, time_t *occurrence) +{ + struct tm tm_start, tm_day; + llist_item_t *i, *j; + int *m, *w, mday, wday, order; + time_t nstart; + struct rpt r; + + localtime_r(&day, &tm_day); + /* + * The following five conditional alternatives are mutually exclusive + * and cover all eight cases of three booleans. + */ + /* BYMONTH expansion */ + if (rpt->bymonth.head && !rpt->bymonthday.head && !rpt->bywday.head) { + LLIST_FOREACH(&rpt->bymonth, i) { + m = LLIST_GET_DATA(i); + + /* Modify rrule start with new month. */ + localtime_r(&start, &tm_start); + tm_start.tm_mon = *m - 1; + tm_start.tm_isdst = -1; + nstart = mktime(&tm_start); + if (!date_chk(nstart, *m - 1, tm_start.tm_mday)) + continue; + if (find_occurrence(nstart, dur, rpt, exc, day, + occurrence)) + return 1; + } + } else + /* BYDAY special expansion for MONTHLY or YEARLY */ + if (!rpt->bymonthday.head && rpt->bywday.head) { + /* Check needed because frequency is modified later. */ + if (!freq_chk(day, start, dur, rpt, exc)) + return 0; + + LLIST_FOREACH(&rpt->bywday, i) { + w = LLIST_GET_DATA(i); + + localtime_r(&start, &tm_start); + /* + * Construct a suitable weekly rrule. BYMONTH + * reduction in find_occurrence() will limit + * occurrences if needed. + */ + r = *rpt; + r.type = RECUR_WEEKLY; + if (*w > 6) { + /* + * Special expand: A single ocurrence counting + * forward from the start of the month/year. + * Start in the week before with a frequency + * that matches the ordered weekday and with + * until day that allows only one occurrence. + */ + order = *w / WEEKINDAYS; + wday = *w % WEEKINDAYS; + r.freq = order; + tm_start.tm_mday = 1; + if (rpt->bymonth.head) + tm_start.tm_mon = tm_day.tm_mon; + else + tm_start.tm_mon = 0; + tm_start.tm_year = tm_day.tm_year; + tm_start.tm_isdst = -1; + nstart = date_sec_change( + next_wday(mktime(&tm_start), wday), + 0, + -WEEKINDAYS + ); + r.until = date_sec_change( + update_time_in_date(nstart, 0, 0), + 0, + r.freq * WEEKINDAYS + ); + if (rpt->until && r.until > rpt->until) + return 0; + } else if (*w > -1) { + /* Expand to each week of the month/year. */ + wday = *w % WEEKINDAYS; + r.freq = 1; + nstart = next_wday(start, wday); + } else if (*w < -6) { + /* + * Special expand: A single ocurrence counting + * backward from the end of the month/year. + */ + order = -(*w) / WEEKINDAYS; + wday = -(*w) % WEEKINDAYS; + if (rpt->bymonth.head) { + r.freq = wday_per_month( + tm_day.tm_mon + 1, + tm_day.tm_year + 1900, + wday + ) - order + 1; + tm_start.tm_mon = tm_day.tm_mon; + } else { + r.freq = wday_per_year( + tm_day.tm_year + 1900, + wday + ) - order + 1; + tm_start.tm_mon = 0; + } + tm_start.tm_mday = 1; + tm_start.tm_year = tm_day.tm_year; + tm_start.tm_isdst = -1; + nstart = date_sec_change( + next_wday(mktime(&tm_start), wday), + 0, + -WEEKINDAYS + ); + r.until = date_sec_change( + update_time_in_date(nstart, 0, 0), + 0, + r.freq * WEEKINDAYS + ); + if (rpt->until && r.until > rpt->until) + return 0; + } else + EXIT(_("illegal BYDAY value")); + + if (test_occurrence(nstart, dur, &r, exc, + start, day, occurrence)) + return 1; + } + } else + /* BYMONTHDAY expansion */ + if (!rpt->bymonth.head && rpt->bymonthday.head) { + LLIST_FOREACH(&rpt->bymonthday, i) { + mday = *(int *)LLIST_GET_DATA(i); + if (mday < 0) + mday = opp_mday( + tm_day.tm_year + 1900, + tm_day.tm_mon + 1, mday + ); + /* Modify rrule start with new monthday. */ + localtime_r(&start, &tm_start); + tm_start.tm_mday = mday; + tm_start.tm_isdst = -1; + nstart = mktime(&tm_start); + if (!date_chk(nstart, tm_start.tm_mon, mday)) + continue; + if (find_occurrence(nstart, dur, rpt, exc, day, + occurrence)) + return 1; + } + } else + /* BYMONTH and BYMONTHDAY expansion */ + if (rpt->bymonth.head && rpt->bymonthday.head) { + LLIST_FOREACH(&rpt->bymonth, i) { + m = LLIST_GET_DATA(i); + + LLIST_FOREACH(&rpt->bymonthday, j) { + mday = *(int *)LLIST_GET_DATA(j); + if (mday < 0) + mday = opp_mday( + tm_day.tm_year + 1900, + tm_day.tm_mon + 1, mday + ); + /* Modify start with new monthday and month. */ + localtime_r(&start, &tm_start); + /* Number of days in February! */ + if (*m == 2 && mday == 29 && + !ISLEAP(tm_start.tm_year + 1900) && + rpt->freq % 4) { + if (!freq_chk(day, start, dur, rpt, exc)) + return 0; + tm_start.tm_year -= tm_start.tm_year % 4; + } + tm_start.tm_mday = mday; + tm_start.tm_mon = *m - 1; + tm_start.tm_isdst = -1; + nstart = mktime(&tm_start); + if (!date_chk(nstart, *m - 1, mday)) + continue; + if (find_occurrence(nstart, dur, rpt, exc, day, + occurrence)) + return 1; + } + } + } else + return NO_EXPANSION; + + /* No occurrence */ + return 0; +} + +/* + * Membership test for the recurrence set of the rrule (start, dur, rpt, exc). + * + * Return true if day belongs to the set. If so, the occurrence is saved in a + * buffer. A positive result is always the outcome of find_occurrence(), whereas + * a negative result may be arrived at in other ways. + * + * The basic (type, frequency)-check is in find_occurrence(). When recurrence + * set expansion and/or reduction (RFC 5545) is needed, expansion is done before + * call of find_occurrence(), while reduction takes place in find_occurrence(). + * + * Recurrence set expansion is accomplished by a combination of calls of + * find_occurrence(), possibly with change of type, frequency and start. + */ +unsigned +recur_item_find_occurrence(time_t start, long dur, struct rpt *rpt, llist_t *exc, + time_t day, time_t *occurrence) +{ + int res; + + /* To make it possible to set an earlier start without expanding the + * recurrence set. */ + if (date_cmp_day(day, start) < 0) + return 0; + + switch (rpt->type) { + case RECUR_DAILY: + res = NO_EXPANSION; + break; + case RECUR_WEEKLY: + res = expand_weekly(start, dur, rpt, exc, day, occurrence); + break; + case RECUR_MONTHLY: + res = expand_monthly(start, dur, rpt, exc, day, occurrence); + break; + case RECUR_YEARLY: + res = expand_yearly(start, dur, rpt, exc, day, occurrence); + break; + default: + res = 0; + } + + if (res == NO_EXPANSION) + return find_occurrence(start, dur, rpt, exc, day, occurrence); + + /* The result of find_occurrence() is passed on. */ + return res; +} +#undef NO_EXPANSION + unsigned recur_apoint_find_occurrence(struct recur_apoint *rapt, time_t day_start, time_t *occurrence) @@ -910,6 +1567,62 @@ void recur_apoint_erase(struct recur_apoint *rapt) LLIST_TS_UNLOCK(&recur_alist_p); } +/* Read monthday list. */ +void recur_bymonthday(llist_t *l, FILE *data_file) +{ + int c = 0, d; + + LLIST_INIT(l); + while ((c = getc(data_file)) == 'd') { + ungetc(c, data_file); + if (fscanf(data_file, "d%d ", &d) != 1) + EXIT(_("syntax error in bymonthday")); + int *i = mem_malloc(sizeof(int)); + *i = d; + LLIST_ADD(l, i); + } + ungetc(c, data_file); +} + +/* Read weekday list. */ +void recur_bywday(enum recur_type type, llist_t *l, FILE *data_file) +{ + int c = 0, w; + + type = !(type == RECUR_MONTHLY || type == RECUR_YEARLY); + + LLIST_INIT(l); + while ((c = getc(data_file)) == 'w') { + ungetc(c, data_file); + if (fscanf(data_file, "w%d ", &w) != 1) + EXIT(_("syntax error in bywday")); + if (type && (w < 0 || w > 6)) + EXIT(_("illegal BYDAY value")); + int *i = mem_malloc(sizeof(int)); + *i = w; + LLIST_ADD(l, i); + } + ungetc(c, data_file); +} + +/* Read month list. */ +void recur_bymonth(llist_t *l, FILE *data_file) +{ + int c = 0, m; + + LLIST_INIT(l); + while ((c = getc(data_file)) == 'm') { + ungetc(c, data_file); + if (fscanf(data_file, "m%d ", &m) != 1) + EXIT(_("syntax error in bymonth")); + EXIT_IF(m < 1 || m > 12, _("illegal bymonth value")); + int *i = mem_malloc(sizeof(int)); + *i = m; + LLIST_ADD(l, i); + } + ungetc(c, data_file); +} + /* * Read days for which recurrent items must not be repeated * (such days are called exceptions). diff --git a/src/ui-day.c b/src/ui-day.c index f2bcba5..3aa1e52 100644 --- a/src/ui-day.c +++ b/src/ui-day.c @@ -987,6 +987,9 @@ void ui_day_item_repeat(void) rpt.type = type; rpt.freq = freq; rpt.until = until; + LLIST_INIT(&rpt.bymonth); + LLIST_INIT(&rpt.bywday); + LLIST_INIT(&rpt.bymonthday); LLIST_INIT(&rpt.exc); if (p->type == EVNT) { struct event *ev = p->item.ev; diff --git a/src/utils.c b/src/utils.c index c19c800..5322db1 100644 --- a/src/utils.c +++ b/src/utils.c @@ -2009,3 +2009,61 @@ long overflow_mul(long x, long y, long *z) *z = x * y; return 0; } + +/* + * Return the upcoming weekday from day (possibly day itself). + */ +time_t next_wday(time_t day, int weekday) +{ + struct tm tm; + + localtime_r(&day, &tm); + return date_sec_change( + day, 0, (weekday - tm.tm_wday + WEEKINDAYS) % WEEKINDAYS + ); + +} + +/* + * Return the number of weekdays of the year. + */ +int wday_per_year(int year, int weekday) +{ + struct tm y_end; + struct date day; + int last_wday; + + /* Find weekday and yearday of the last day of the year. */ + day.dd = 31; + day.mm = 12; + day.yyyy = year; + y_end = date2tm(day, 0, 0); + mktime(&y_end); + + /* Find date of the last weekday of the year. */ + last_wday = (y_end.tm_yday + 1) - (y_end.tm_wday - weekday + 7) % 7; + + return last_wday / 7 + (last_wday % 7 > 0); +} + +/* + * Return the number of weekdays in month of year. + */ +int wday_per_month(int month, int year, int weekday) +{ + struct tm m_end; + struct date day; + int last_wday, m_days = days[month - 1] + (month == 2 && ISLEAP(year) ? 1 : 0); + + /* Find weekday of the last day of the month. */ + day.dd = m_days; + day.mm = month; + day.yyyy = year; + m_end = date2tm(day, 0, 0); + mktime(&m_end); + + /* Find date of the last weekday of the month. */ + last_wday = m_days - (m_end.tm_wday - weekday + 7) % 7; + + return last_wday / 7 + (last_wday % 7 > 0); +} |