Files
remind/src/trigger.c
2024-02-29 11:14:05 -05:00

671 lines
18 KiB
C

/***************************************************************/
/* */
/* TRIGGER.C */
/* */
/* Routines for figuring out the trigger date of a reminder */
/* */
/* This file is part of REMIND. */
/* Copyright (C) 1992-2024 by Dianne Skoll */
/* SPDX-License-Identifier: GPL-2.0-only */
/* */
/***************************************************************/
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include "types.h"
#include "expr.h"
#include "protos.h"
#include "globals.h"
#include "err.h"
#define GOT_DAY 1
#define GOT_MON 2
#define GOT_YR 4
#define GOT_WD 8
#define ADVANCE_TO_WD(x, wd) while (! ((wd) & (1 << ((x)%7)))) (x)++
static int DSEYear(int dse);
static int DSEMonth(int dse);
static int NextSimpleTrig(int startdate, Trigger *trig, int *err);
static int GetNextTriggerDate(Trigger *trig, int start, int *err, int *nextstart);
/***************************************************************/
/* */
/* NextSimpleTrig */
/* */
/* Compute the "simple" trigger date, taking into account */
/* ONLY the day of week, day, month and year components. */
/* Normally, returns -1 if the trigger has expired. As a */
/* special case, if D, M, Y [WD] are specified, returns the */
/* DSE date, regardless of whether it's expired. This is */
/* so that dates with a REP can be handled properly. */
/* */
/***************************************************************/
static int NextSimpleTrig(int startdate, Trigger *trig, int *err)
{
int typ = 0;
int d, m, y, j, d2, m2, y2;
*err = 0;
FromDSE(startdate, &y, &m, &d);
d2 = d;
m2 = m;
y2 = y;
if (trig->d != NO_DAY) typ |= GOT_DAY;
if (trig->m != NO_MON) typ |= GOT_MON;
if (trig->y != NO_YR) typ |= GOT_YR;
if (trig->wd != NO_WD) typ |= GOT_WD;
switch(typ) {
case 0:
return startdate;
case GOT_WD:
ADVANCE_TO_WD(startdate, trig->wd);
return startdate;
case GOT_DAY:
if (d > trig->d) {
m++;
if (m == 12) { m = 0; y++; }
}
while (trig->d > DaysInMonth(m, y)) {
m++;
if (m == 12) { m = 0; y++; }
}
j = DSE(y, m, trig->d);
return j;
case GOT_MON:
if (m == trig->m) return startdate;
else if (m > trig->m) return DSE(y+1, trig->m, 1);
else return DSE(y, trig->m, 1);
case GOT_YR:
if (y == trig->y) return startdate;
else if (y < trig->y) return DSE(trig->y, 0, 1);
else return -1;
case GOT_DAY+GOT_MON:
if (trig->d > MonthDays[trig->m]) {
*err = E_BAD_DATE;
return -1;
}
if (m > trig->m || (m == trig->m && d > trig->d)) y++;
/* Take care of Feb. 29 */
while (trig->d > DaysInMonth(trig->m, y)) y++;
return DSE(y, trig->m, trig->d);
case GOT_DAY+GOT_YR:
if (y < trig->y) return DSE(trig->y, 0, trig->d);
else if (y > trig->y) return -1;
if (d > trig->d) {
m++;
if (m == 12) return -1;
}
while (trig->d > DaysInMonth(m, trig->y)) m++;
return DSE(trig->y, m, trig->d);
case GOT_MON+GOT_YR:
if (y > trig->y || (y == trig->y && m > trig->m)) return -1;
if (y < trig->y) return DSE(trig->y, trig->m, 1);
if (m == trig->m) return startdate;
return DSE(trig->y, trig->m, 1);
case GOT_DAY+GOT_MON+GOT_YR:
if (trig->d > DaysInMonth(trig->m, trig->y)) {
*err = E_BAD_DATE;
return -1;
}
return DSE(trig->y, trig->m, trig->d);
case GOT_YR+GOT_WD:
if (y > trig->y) return -1;
if (y < trig->y) j = DSE(trig->y, 0, 1);
else j = startdate;
ADVANCE_TO_WD(j, trig->wd);
if (DSEYear(j) > trig->y) return -1;
return j;
case GOT_MON+GOT_WD:
if (m == trig->m) {
j = startdate;
ADVANCE_TO_WD(j, trig->wd);
if (DSEMonth(j) == trig->m) return j;
}
if (m >= trig->m) j = DSE(y+1, trig->m, 1);
else j = DSE(y, trig->m, 1);
ADVANCE_TO_WD(j, trig->wd);
return j; /* Guaranteed to be within the month */
case GOT_DAY+GOT_WD:
if (m !=0 || y > BASE) {
m2 = m-1;
if (m2 < 0) { y2 = y-1; m2 = 11; }
/* If there are fewer days in previous month, no match */
if (trig->d <= DaysInMonth(m2, y2)) {
j = DSE(y2, m2, trig->d);
ADVANCE_TO_WD(j, trig->wd);
if (j >= startdate) return j;
}
}
/* Try this month */
if (trig->d <= DaysInMonth(m, y)) {
j = DSE(y, m, trig->d);
ADVANCE_TO_WD(j, trig->wd);
if (j >= startdate) return j;
}
/* Argh! Try next avail. month */
m2 = m+1;
if (m2 > 11) { m2 = 0; y++; }
while (trig->d > DaysInMonth(m2, y)) m2++;
j = DSE(y, m2, trig->d);
ADVANCE_TO_WD(j, trig->wd);
return j;
case GOT_WD+GOT_YR+GOT_DAY:
if (y > trig->y+1 || (y > trig->y && m>0)) return -1;
if (y > trig->y) {
j = DSE(trig->y, 11, trig->d);
ADVANCE_TO_WD(j, trig->wd);
if (j >= startdate) return j;
} else if (y < trig->y) {
j = DSE(trig->y, 0, trig->d);
ADVANCE_TO_WD(j, trig->wd);
return j;
} else {
/* Try last month */
if (m > 0) {
m2 = m-1;
while (trig->d > DaysInMonth(m2, trig->y)) m2--;
j = DSE(trig->y, m2, trig->d);
ADVANCE_TO_WD(j, trig->wd);
if (DSEYear(j) > trig->y) return -1;
if (j >= startdate) return j;
}
}
/* Try this month */
if (trig->d <= DaysInMonth(m, trig->y)) {
j = DSE(trig->y, m, trig->d);
ADVANCE_TO_WD(j, trig->wd);
if (DSEYear(j) > trig->y) return -1;
if (j >= startdate) return j;
}
/* Must be next month */
if (m == 11) return -1;
m++;
while (trig->d > DaysInMonth(m, trig->d)) m++;
j = DSE(trig->y, m, trig->d);
ADVANCE_TO_WD(j, trig->wd);
if (DSEYear(j) > trig->y) return -1;
return j;
case GOT_DAY+GOT_MON+GOT_WD:
if (trig->d > MonthDays[trig->m]) {
*err = E_BAD_DATE;
return -1;
}
/* Back up a year in case we'll cross a year boundary*/
if (y > BASE) {
y--;
}
/* Move up to the first valid year */
while (trig->d > DaysInMonth(trig->m, y)) y++;
/* Try last year */
j = DSE(y, trig->m, trig->d);
ADVANCE_TO_WD(j, trig->wd);
if (j >= startdate) return j;
/* Try this year */
y++;
while (trig->d > DaysInMonth(trig->m, y)) y++;
j = DSE(y, trig->m, trig->d);
ADVANCE_TO_WD(j, trig->wd);
if (j >= startdate) return j;
/* Must be next year */
y++;
while (trig->d > DaysInMonth(trig->m, y)) y++;
j = DSE(y, trig->m, trig->d);
ADVANCE_TO_WD(j, trig->wd);
return j;
case GOT_WD+GOT_MON+GOT_YR:
if (y > trig->y || (y == trig->y && m > trig->m)) return -1;
if (trig->y > y || (trig->y == y && trig->m > m)) {
j = DSE(trig->y, trig->m, 1);
ADVANCE_TO_WD(j, trig->wd);
return j;
} else {
j = startdate;
ADVANCE_TO_WD(j, trig->wd);
FromDSE(j, &y2, &m2, &d2);
if (m2 == trig->m) return j; else return -1;
}
case GOT_WD+GOT_DAY+GOT_MON+GOT_YR:
if (trig->d > DaysInMonth(trig->m, trig->y)) {
*err = E_BAD_DATE;
return -1;
}
j = DSE(trig->y, trig->m, trig->d);
ADVANCE_TO_WD(j, trig->wd);
return j;
default:
Eprint("NextSimpleTrig %s %d", ErrMsg[E_SWERR], typ);
*err = E_SWERR;
return -1;
}
}
/***************************************************************/
/* */
/* DSEMonth - Given a DSE date, what's the month? */
/* */
/***************************************************************/
static int DSEMonth(int dse)
{
int y, m, d;
FromDSE(dse, &y, &m, &d);
return m;
}
/***************************************************************/
/* */
/* DSEYear - Given a DSE date, what's the year? */
/* */
/***************************************************************/
static int DSEYear(int dse)
{
int y, m, d;
FromDSE(dse, &y, &m, &d);
return y;
}
/***************************************************************/
/* */
/* GetNextTriggerDate */
/* */
/* Given a trigger, compute the next trigger date. */
/* */
/* Returns the DSE date of next trigger, -1 if */
/* expired, -2 if can't compute trigger date. */
/* */
/***************************************************************/
static int GetNextTriggerDate(Trigger *trig, int start, int *err, int *nextstart)
{
int simple, mod, omit;
/* First: Have we passed the UNTIL date? */
if (trig->until != NO_UNTIL &&
trig->until < start) {
trig->expired = 1;
return -1; /* expired */
}
/* Next: If it's an "AFTER"-type skip, back up
until we're at the start of a block of holidays */
if (trig->skip == AFTER_SKIP) {
int iter = 0;
while (iter++ <= MaxSatIter) {
*err = IsOmitted(start-1, trig->localomit, trig->omitfunc, &omit);
if (*err) return -2;
if (!omit) {
break;
}
start--;
if (start < 0) {
break;
}
}
if (start < 0 || iter > MaxSatIter) {
/* omitfunc must have returned "true" too often */
*err = E_CANT_TRIG;
return -2;
}
}
/* Find the next simple trigger */
simple = NextSimpleTrig(start, trig, err);
/* Problems? */
if (*err || (simple == -1)) return -1;
/* Suggested starting point for next attempt */
*nextstart = simple+1;
/* If there's a BACK, back up... */
if (trig->back != NO_BACK) {
mod = trig->back;
if (mod < 0) {
simple += mod;
}
else {
int iter = 0;
int max = MaxSatIter;
if (max < mod*2) {
max = mod*2;
}
while(iter++ <= max) {
if (!mod) {
break;
}
simple--;
*err = IsOmitted(simple, trig->localomit, trig->omitfunc, &omit);
if (*err) return -2;
if (!omit) mod--;
}
if (iter > max) {
*err = E_CANT_TRIG;
return -2;
}
}
}
/* If there's a REP, calculate the next occurrence */
if (trig->rep != NO_REP) {
if (simple < start) {
mod = (start - simple) / trig->rep;
simple = simple + mod * trig->rep;
if (simple < start) simple += trig->rep;
}
}
/* If it's a "BEFORE"-type skip, back up */
if (trig->skip == BEFORE_SKIP) {
int iter = 0;
while(iter++ <= MaxSatIter) {
*err = IsOmitted(simple, trig->localomit, trig->omitfunc, &omit);
if (*err) return -2;
if (!omit) {
break;
}
simple--;
if (simple < 0) {
*err = E_CANT_TRIG;
return -2;
}
}
if (iter > MaxSatIter) {
*err = E_CANT_TRIG;
return -2;
}
}
/* If it's an "AFTER"-type skip, jump ahead */
if (trig->skip == AFTER_SKIP) {
int iter = 0;
while (iter++ <= MaxSatIter) {
*err = IsOmitted(simple, trig->localomit, trig->omitfunc, &omit);
if (*err) return -2;
if (!omit) {
break;
}
simple++;
}
if (iter > MaxSatIter) {
*err = E_CANT_TRIG;
return -2;
}
}
/* Return the date */
return simple;
}
int
AdjustTriggerForDuration(int today, int r, Trigger *trig, TimeTrig *tim, int save_in_globals)
{
int y, m, d;
/* If we have an AT, save the original event start */
if (tim->ttime != NO_TIME) {
trig->eventstart = MINUTES_PER_DAY * r + tim->ttime;
if (tim->duration != NO_TIME) {
trig->eventduration = tim->duration;
}
}
/* Now potentially adjust */
if (r < today && r + trig->duration_days >= today) {
/* Adjust duration down */
tim->duration -= (today - r) * MINUTES_PER_DAY;
tim->duration += tim->ttime;
/* Start at midnight */
tim->ttime = 0;
/* Change trigger date to today */
r = today;
if (DebugFlag & DB_PRTTRIG) {
FromDSE(r, &y, &m, &d);
fprintf(ErrFp, "%s(%d): Trig(adj) = %s, %d %s, %d",
FileName, LineNo,
get_day_name(r % 7),
d,
get_month_name(m),
y);
if (tim->ttime != NO_TIME) {
fprintf(ErrFp, " AT %02d:%02d",
(tim->ttime / 60),
(tim->ttime % 60));
if (tim->duration != NO_TIME) {
fprintf(ErrFp, " DURATION %02d:%02d",
(tim->duration / 60),
(tim->duration % 60));
}
}
fprintf(ErrFp, "\n");
}
}
if (save_in_globals) {
SaveAllTriggerInfo(trig, tim, r, tim->ttime, 1);
}
return r;
}
/***************************************************************/
/* */
/* ComputeTrigger */
/* */
/* The main function. Compute the next trigger date given */
/* today's date. */
/* */
/***************************************************************/
int ComputeTrigger(int today, Trigger *trig, TimeTrig *tim,
int *err, int save_in_globals)
{
int r = ComputeTriggerNoAdjustDuration(today, trig, tim, err, save_in_globals, 0);
if (*err != OK) {
return r;
}
if (r == today) {
if (tim->ttime != NO_TIME) {
trig->eventstart = MINUTES_PER_DAY * r + tim->ttime;
if (tim->duration != NO_TIME) {
trig->eventduration = tim->duration;
}
}
if (save_in_globals) {
SaveAllTriggerInfo(trig, tim, r, tim->ttime, 1);
}
return r;
}
if (trig->duration_days) {
r = ComputeTriggerNoAdjustDuration(today, trig, tim, err, save_in_globals, trig->duration_days);
if (*err != OK) {
return r;
}
}
r = AdjustTriggerForDuration(today, r, trig, tim, save_in_globals);
return r;
}
/***************************************************************/
/* */
/* ComputeTriggerNoAdjustDuration */
/* */
/* Compute a trigger, but do NOT adjust the time trigger */
/* duration. */
/* */
/***************************************************************/
int ComputeTriggerNoAdjustDuration(int today, Trigger *trig, TimeTrig *tim,
int *err, int save_in_globals, int duration_days)
{
int nattempts = 0,
start = today - duration_days,
nextstart = 0,
y, m, d, omit,
result;
trig->expired = 0;
if (save_in_globals) {
LastTrigValid = 0;
}
/* Assume everything works */
*err = OK;
/* But check for obvious problems... */
if ((WeekdayOmits | trig->localomit) == 0x7F) {
*err = E_2MANY_LOCALOMIT;
return -1;
}
if (start < 0) {
*err = E_DATE_OVER;
return -1;
}
if (tim->duration != NO_TIME && tim->ttime == NO_TIME) {
*err = E_DURATION_NO_AT;
return -1;
}
if (trig->rep != NO_REP &&
(trig->d == NO_DAY ||
trig->m == NO_MON ||
trig->y == NO_YR)) {
Eprint("%s", ErrMsg[E_REP_FULSPEC]);
*err = E_REP_FULSPEC;
return -1;
}
/* Save the trigger */
if (save_in_globals) {
SaveLastTrigger(trig);
}
while (nattempts++ < TRIG_ATTEMPTS) {
result = GetNextTriggerDate(trig, start, err, &nextstart);
/* If there's an error, die immediately */
if (*err) return -1;
if (result == -1) {
trig->expired = 1;
if (DebugFlag & DB_PRTTRIG) {
fprintf(ErrFp, "%s(%d): %s\n",
FileName, LineNo, ErrMsg[E_EXPIRED]);
}
return -1;
}
/* If result is >= today, great! */
if (trig->skip == SKIP_SKIP) {
*err = IsOmitted(result, trig->localomit, trig->omitfunc, &omit);
if (*err) return -1;
} else {
omit = 0;
}
/** FIXME: Fix bad interaction with SATISFY... need to rethink!!! */
if (result+duration_days >= today &&
(trig->skip != SKIP_SKIP || !omit)) {
if (save_in_globals) {
LastTriggerDate = result; /* Save in global var */
LastTrigValid = 1;
}
if (DebugFlag & DB_PRTTRIG) {
FromDSE(result, &y, &m, &d);
fprintf(ErrFp, "%s(%d): Trig = %s, %d %s, %d",
FileName, LineNo,
get_day_name(result % 7),
d,
get_month_name(m),
y);
if (tim->ttime != NO_TIME) {
fprintf(ErrFp, " AT %02d:%02d",
(tim->ttime / 60),
(tim->ttime % 60));
if (tim->duration != NO_TIME) {
fprintf(ErrFp, " DURATION %02d:%02d",
(tim->duration / 60),
(tim->duration % 60));
}
}
fprintf(ErrFp, "\n");
}
return result;
}
/* If it's a simple trigger, no point in rescanning */
if (trig->back == NO_BACK &&
trig->skip == NO_SKIP &&
trig->rep == NO_REP) {
trig->expired = 1;
if (DebugFlag & DB_PRTTRIG) {
fprintf(ErrFp, "%s(%d): %s\n",
FileName, LineNo, ErrMsg[E_EXPIRED]);
}
if (save_in_globals) {
LastTriggerDate = result;
LastTrigValid = 1;
}
return -1;
}
if (trig->skip == SKIP_SKIP &&
omit &&
nextstart <= start &&
result >= start) {
nextstart = result + 1;
}
/* Keep scanning... unless there's no point in doing it.*/
if (nextstart <= start) {
if (save_in_globals) {
LastTriggerDate = result;
LastTrigValid = 1;
}
trig->expired = 1;
if (DebugFlag & DB_PRTTRIG) {
fprintf(ErrFp, "%s(%d): %s\n",
FileName, LineNo, ErrMsg[E_EXPIRED]);
}
return -1;
}
else start = nextstart;
}
/* We failed - too many attempts or trigger has expired*/
*err = E_CANT_TRIG;
return -1;
}