Merge Jean Laroche's 'fix-recurrence' into maint

This commit is contained in:
John Ralls 2020-03-22 13:14:50 -07:00
commit 2bbf5b2ce0
2 changed files with 127 additions and 45 deletions

View File

@ -174,6 +174,28 @@ nth_weekday_compare(const GDate *start, const GDate *next, PeriodType pt)
} }
static void adjust_for_weekend(PeriodType pt, WeekendAdjust wadj, GDate *date)
{
if (pt == PERIOD_YEAR || pt == PERIOD_MONTH || pt == PERIOD_END_OF_MONTH)
{
if (g_date_get_weekday(date) == G_DATE_SATURDAY || g_date_get_weekday(date) == G_DATE_SUNDAY)
{
switch (wadj)
{
case WEEKEND_ADJ_BACK:
g_date_subtract_days(date, g_date_get_weekday(date) == G_DATE_SATURDAY ? 1 : 2);
break;
case WEEKEND_ADJ_FORWARD:
g_date_add_days(date, g_date_get_weekday(date) == G_DATE_SATURDAY ? 2 : 1);
break;
case WEEKEND_ADJ_NONE:
default:
break;
}
}
}
}
/* This is the only real algorithm related to recurrences. It goes: /* This is the only real algorithm related to recurrences. It goes:
Step 1) Go forward one period from the reference date. Step 1) Go forward one period from the reference date.
Step 2) Back up to align to the phase of the start date. Step 2) Back up to align to the phase of the start date.
@ -183,6 +205,7 @@ recurrenceNextInstance(const Recurrence *r, const GDate *ref, GDate *next)
{ {
PeriodType pt; PeriodType pt;
const GDate *start; const GDate *start;
GDate adjusted_start;
guint mult; guint mult;
WeekendAdjust wadj; WeekendAdjust wadj;
@ -191,20 +214,23 @@ recurrenceNextInstance(const Recurrence *r, const GDate *ref, GDate *next)
g_return_if_fail(g_date_valid(&r->start)); g_return_if_fail(g_date_valid(&r->start));
g_return_if_fail(g_date_valid(ref)); g_return_if_fail(g_date_valid(ref));
/* If the ref date comes before the start date then the next
occurrence is always the start date, and we're done. */
start = &r->start; start = &r->start;
if (g_date_compare(ref, start) < 0) mult = r->mult;
pt = r->ptype;
wadj = r->wadj;
/* If the ref date comes before the start date then the next
occurrence is always the start date, and we're done. */
// However, it's possible for the start date to fall on an exception (a weekend), in that case, it needs to be corrected.
adjusted_start = *start;
adjust_for_weekend(pt,wadj,&adjusted_start);
if (g_date_compare(ref, &adjusted_start) < 0)
{ {
g_date_set_julian(next, g_date_get_julian(start)); g_date_set_julian(next, g_date_get_julian(&adjusted_start));
return; return;
} }
g_date_set_julian(next, g_date_get_julian(ref)); /* start at refDate */ g_date_set_julian(next, g_date_get_julian(ref)); /* start at refDate */
/* Step 1: move FORWARD one period, passing exactly one occurrence. */ /* Step 1: move FORWARD one period, passing exactly one occurrence. */
mult = r->mult;
pt = r->ptype;
wadj = r->wadj;
switch (pt) switch (pt)
{ {
case PERIOD_YEAR: case PERIOD_YEAR:
@ -342,25 +368,7 @@ recurrenceNextInstance(const Recurrence *r, const GDate *ref, GDate *next)
g_date_set_day(next, g_date_get_day(start)); /*same day as start*/ g_date_set_day(next, g_date_get_day(start)); /*same day as start*/
/* Adjust for dates on the weekend. */ /* Adjust for dates on the weekend. */
if (pt == PERIOD_YEAR || pt == PERIOD_MONTH || pt == PERIOD_END_OF_MONTH) adjust_for_weekend(pt,wadj,next);
{
if (g_date_get_weekday(next) == G_DATE_SATURDAY || g_date_get_weekday(next) == G_DATE_SUNDAY)
{
switch (wadj)
{
case WEEKEND_ADJ_BACK:
g_date_subtract_days(next, g_date_get_weekday(next) == G_DATE_SATURDAY ? 1 : 2);
break;
case WEEKEND_ADJ_FORWARD:
g_date_add_days(next, g_date_get_weekday(next) == G_DATE_SATURDAY ? 2 : 1);
break;
case WEEKEND_ADJ_NONE:
default:
break;
}
}
}
} }
break; break;
case PERIOD_WEEK: case PERIOD_WEEK:

View File

@ -30,13 +30,13 @@
static QofBook *book; static QofBook *book;
static void check_valid(GDate *next, GDate *ref, GDate *start, static gboolean check_valid(GDate *next, GDate *ref, GDate *start,
guint16 mult, PeriodType pt, WeekendAdjust wadj) guint16 mult, PeriodType pt, WeekendAdjust wadj)
{ {
gboolean valid; gboolean valid;
GDate adj_date;
gint startToNext; gint startToNext;
/* FIXME: The WeekendAdjust argument is completely ignored for gboolean ret_val = TRUE;
now. */
valid = g_date_valid(next); valid = g_date_valid(next);
if (pt == PERIOD_ONCE && g_date_compare(start, ref) <= 0) if (pt == PERIOD_ONCE && g_date_compare(start, ref) <= 0)
@ -44,7 +44,7 @@ static void check_valid(GDate *next, GDate *ref, GDate *start,
else else
do_test(valid, "incorrectly invalid"); do_test(valid, "incorrectly invalid");
if (!valid) return; if (!valid) return valid;
do_test(g_date_compare(ref, next) < 0, do_test(g_date_compare(ref, next) < 0,
"next date not strictly later than ref date"); "next date not strictly later than ref date");
@ -54,13 +54,41 @@ static void check_valid(GDate *next, GDate *ref, GDate *start,
switch (pt) switch (pt)
{ {
case PERIOD_YEAR: case PERIOD_YEAR:
do_test((g_date_get_year(next) - g_date_get_year(start)) % mult == 0, ret_val &= do_test((g_date_get_year(next) - g_date_get_year(start)) % mult == 0,
"year period phase wrong"); // redundant "year period phase wrong"); // redundant
mult *= 12; mult *= 12;
// fall through // fall through
case PERIOD_END_OF_MONTH: case PERIOD_END_OF_MONTH:
if (pt == PERIOD_END_OF_MONTH) if (pt == PERIOD_END_OF_MONTH)
do_test(g_date_is_last_of_month(next), "end of month phase wrong"); {
if(wadj == WEEKEND_ADJ_NONE)
ret_val &= do_test(g_date_is_last_of_month(next), "end of month phase wrong");
else
{
gboolean result;
if(!g_date_is_last_of_month(next))
{
adj_date = *next;
if(wadj == WEEKEND_ADJ_BACK)
{
// If adjusting back, one of the next two days to be end of month
g_date_add_days(&adj_date,1);
result = g_date_is_last_of_month(&adj_date);
g_date_add_days(&adj_date,1);
result |= g_date_is_last_of_month(&adj_date);
}
if(wadj == WEEKEND_ADJ_FORWARD)
{
// If adjusting forward, one of the two previous days has to be end of month
g_date_subtract_days(&adj_date,1);
result = g_date_is_last_of_month(&adj_date);
g_date_subtract_days(&adj_date,1);
result |= g_date_is_last_of_month(&adj_date);
}
ret_val &= do_test(result, "end of month phase wrong");
}
}
}
// fall through // fall through
case PERIOD_LAST_WEEKDAY: case PERIOD_LAST_WEEKDAY:
case PERIOD_NTH_WEEKDAY: case PERIOD_NTH_WEEKDAY:
@ -71,13 +99,14 @@ static void check_valid(GDate *next, GDate *ref, GDate *start,
monthdiff = (g_date_get_month(next) - g_date_get_month(start)) + monthdiff = (g_date_get_month(next) - g_date_get_month(start)) +
12 * (g_date_get_year(next) - g_date_get_year(start)); 12 * (g_date_get_year(next) - g_date_get_year(start));
do_test(monthdiff % mult == 0, "month or year phase wrong"); monthdiff %= mult;
ret_val &= do_test(monthdiff == 0 || (monthdiff == -1 && wadj == WEEKEND_ADJ_BACK) || (monthdiff == 1 && wadj == WEEKEND_ADJ_FORWARD), "month or year phase wrong");
if (pt == PERIOD_NTH_WEEKDAY || pt == PERIOD_LAST_WEEKDAY) if (pt == PERIOD_NTH_WEEKDAY || pt == PERIOD_LAST_WEEKDAY)
{ {
guint sweek, nweek; guint sweek, nweek;
do_test(g_date_get_weekday(next) == g_date_get_weekday(start), ret_val &= do_test(g_date_get_weekday(next) == g_date_get_weekday(start),
"weekday phase wrong"); "weekday phase wrong");
sweek = (g_date_get_day(start) - 1) / 7; sweek = (g_date_get_day(start) - 1) / 7;
nweek = (g_date_get_day(next) - 1) / 7; nweek = (g_date_get_day(next) - 1) / 7;
@ -87,7 +116,7 @@ static void check_valid(GDate *next, GDate *ref, GDate *start,
4th, OR 'start' didn't have 5 of the weekday that 4th, OR 'start' didn't have 5 of the weekday that
'next' does and we want the LAST weekday, so it's the 'next' does and we want the LAST weekday, so it's the
5th of that weekday */ 5th of that weekday */
do_test(sweek == nweek || ret_val &= do_test(sweek == nweek ||
(sweek == 4 && nweek == 3 && (g_date_get_day(next) + 7) > (sweek == 4 && nweek == 3 && (g_date_get_day(next) + 7) >
g_date_get_days_in_month( g_date_get_days_in_month(
g_date_get_month(next), g_date_get_year(next))) || g_date_get_month(next), g_date_get_year(next))) ||
@ -97,16 +126,61 @@ static void check_valid(GDate *next, GDate *ref, GDate *start,
} }
else else
{ {
GDateWeekday week_day;
GDateWeekday week_day_1;
GDateWeekday week_day_2;
day_start = g_date_get_day(start); day_start = g_date_get_day(start);
day_next = g_date_get_day(next); day_next = g_date_get_day(next);
if (day_start < 28) if (day_start < 28)
do_test(day_start == day_next, "dom don't match"); {
else if (pt != PERIOD_END_OF_MONTH) gboolean result;
week_day = g_date_get_weekday (next);
switch (wadj) {
case WEEKEND_ADJ_NONE:
ret_val &= do_test(day_start == day_next, "dom don't match");
break;
case WEEKEND_ADJ_BACK:
// Week_day cannot be a weekend.
result = (week_day != G_DATE_SATURDAY && week_day != G_DATE_SUNDAY);
if(day_start != day_next)
{
// If the dom don't match day must be a Friday
result &= (week_day == G_DATE_FRIDAY);
// Either day_next+1 or day_next+2 matches day_start
g_date_add_days(next,1);
week_day_1 = g_date_get_day(next);
g_date_add_days(next,1);
week_day_2 = g_date_get_day(next);
result &= week_day_1 == day_start || week_day_2 == day_start;
}
ret_val &= do_test(result, "dom don't match");
break;
case WEEKEND_ADJ_FORWARD:
// Week_day cannot be a weekend.
result = (week_day != G_DATE_SATURDAY && week_day != G_DATE_SUNDAY);
if(day_start != day_next)
{
// If the dom don't match day must be a Monday
result &= (week_day == G_DATE_MONDAY);
// Either day_next-1 or day_next-2 matches day_start
g_date_subtract_days(next,1);
week_day_1 = g_date_get_day(next);
g_date_subtract_days(next,1);
week_day_2 = g_date_get_day(next);
result &= week_day_1 == day_start || week_day_2 == day_start;
}
ret_val &= do_test(result, "dom don't match");
break;
default:
break;
}
}
else if (pt != PERIOD_END_OF_MONTH && wadj == WEEKEND_ADJ_NONE)
{ {
// the end of month case was already checked above. near // the end of month case was already checked above. near
// the end of the month, the days should still agree, // the end of the month, the days should still agree,
// unless they can't because of a short month. // unless they can't because of a short month.
do_test(day_start == day_next || g_date_is_last_of_month(next), ret_val &= do_test(day_start == day_next || g_date_is_last_of_month(next),
"dom don't match and next is not eom"); "dom don't match and next is not eom");
} }
} }
@ -116,16 +190,16 @@ static void check_valid(GDate *next, GDate *ref, GDate *start,
mult *= 7; mult *= 7;
// fall through // fall through
case PERIOD_DAY: case PERIOD_DAY:
do_test((startToNext % mult) == 0, "week or day period phase wrong"); ret_val &= do_test((startToNext % mult) == 0, "week or day period phase wrong");
break; break;
case PERIOD_ONCE: case PERIOD_ONCE:
do_test(startToNext == 0, "period once not on start date"); ret_val &= do_test(startToNext == 0, "period once not on start date");
break; break;
default: default:
do_test(FALSE, "invalid PeriodType"); ret_val &=do_test(FALSE, "invalid PeriodType");
break; break;
} }
return ret_val;
} }
#define NUM_DATES_TO_TEST 300 #define NUM_DATES_TO_TEST 300
@ -168,9 +242,9 @@ static void test_all()
wadj_reg = recurrenceGetWeekendAdjust(&r); wadj_reg = recurrenceGetWeekendAdjust(&r);
recurrenceNextInstance(&r, &d_ref, &d_next); recurrenceNextInstance(&r, &d_ref, &d_next);
check_valid(&d_next, &d_ref, &d_start_reg, if (!check_valid(&d_next, &d_ref, &d_start_reg,
mult_reg, pt_reg, wadj_reg); mult_reg, pt_reg, wadj_reg))
return;
} }
} }
} }