3
\$\begingroup\$

This is a simple implementation of the calendar(1) utility included in some UNIX systems (all BSDs have it, GNU has not). I do not have much experience with <sys/time.h> and <time.h>

Manual:

CALENDAR(1)                 General Commands Manual                CALENDAR(1)

NAME
       calendar - print upcoming events

SYNOPSIS
       calendar [-l] [-A num] [-B num] [-t [[yyyy]mm]dd] [file...]

DESCRIPTION
       calendar reads files for events, one event per line; and writes to
       standard output those events beginning with either today's date or
       tomorrow's.  On Fridays and Saturdays, events through Monday are
       printed.  If a hyphen (-) is provided as argument or the argument is
       absent, calendar reads from the standard input.

       The options are as follows:

       -A num Print lines from today and next num days (forward, future).

       -B num Print lines from today and previous num days (backward, past).

       -l     Rather than print the date on the same line of each event, print
              the date alone in a line and followed by each event indented
              with a tab.

       -t[[yyyy]mm]dd
              Act like the specified value is “today” instead of using the
              current date.

       Each event should begin with a date pattern in the format
       [[YYYY-[MM]]-DDWW[+N|-N].  The hyphen (-) that separates the values can
       be replaced by a slash (/) or a period (.).  Several date patterns can
       be supplied separated by a comma (,).

       YYYY should be any year number.  MM should be a month number or a month
       name (either complete or abbreviate, such as "April" or "Apr").  DD
       should be the number of a day in the month.  WW should be the name of a
       day in the week (either complete or abbreviate).  Either DD or WW (or
       both) must be supplied.

       The date pattern can be followed by +N or -N to specify the week on the
       month (for example Sun+2 is the second Sunday in the month, Mon-3 is
       the third from last Monday in the month).

EXAMPLES
       Consider the following input.

              # holidays
              01/01      New Year's day
              05/01      Labor Day
              07/25      Generic holiday
              12/25      Christmas
              May/Sun+2  Mother's day
              13Fri      Friday the 13th

              # classes
              Mon,Wed Java Class
              Tue,Thu Algebra Class
              Tue,Thu Operating Systems Class
              Tue,Thu Computer Network Class

       If today were 09 March 2021, then running calendar with the options -l
       and -A7 on this input would print the following:

              Sunday     09 May 2021
                      Mother's day
              Monday     10 May 2021
                      Java Class
              Tuesday    11 May 2021
                      Algebra Class
                      Computer Network Class
                      Operating Systems Class
              Wednesday  12 May 2021
                      Java Class
              Thursday   13 May 2021
                      Algebra Class
                      Computer Network Class
                      Operating Systems Class
              Friday     14 May 2021
              Saturday   15 May 2021

SEE ALSO
       at(1), cal(1), cron(1), todo(1)

STANDARDS
       The calendar program previously selected lines which had the correct
       date anywhere in the line.  This is no longer true: the date is only
       recognized when it occurs at the beginning of a line.

       The calendar program previously could interpret only one date for each
       event.  This is no longer true: each event can occur in more than one
       date (see the examples for the classes above).

       The calendar program previously could not read events from the standard
       input.  This is no longer true: this version of calendar is an actual
       filter, which can read from the standard input or from named files.

       The calendar program previously had an option to send a mail to all
       users.  This is no longer true: to have your calendar mailed every day,
       use cron(8).

HISTORY
       A calendar command appeared in Version 7 AT&T UNIX.

                                                                   CALENDAR(1)

calendar.c:

#include <sys/time.h>

#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include "calendar.h"

#define SECS_PER_DAY  (24 * 60 * 60)
#define DAYS_PER_WEEK 7
#define MIDDAY        12
#define EPOCH         1970
#define isleap(y)     ((!((y) % 4) && ((y) % 100)) || !((y) % 400))

static const int days_in_month[2][13] = {
    {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
    {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
};

/* show usage and exit */
static void
usage(void)
{
    (void)fprintf(stderr, "usage: calendar [-d] [-A num] [-B num] -t [yyyymmdd] [file ...]\n");
    exit(1);
}

/* call calloc checking for error */
static void *
ecalloc(size_t nmemb, size_t size)
{
    void *p;

    if ((p = calloc(nmemb, size)) == NULL)
        err(1, "malloc");
    return p;
}

/* call strdup checking for error */
static char *
estrdup(const char *s)
{
    char *t;

    if ((t = strdup(s)) == NULL)
        err(1, "strdup");
    return t;
}

/* convert string value to int */
static int
strtonum(const char *s)
{
    long n;
    char *ep;

    errno = 0;
    n = strtol(s, &ep, 10);
    if (s[0] == '\0' || *ep != '\0')
        goto error;
    if ((errno == ERANGE && (n == LONG_MAX || n == LONG_MIN)) || (n > INT_MAX || n < 0))
        goto error;
    return (int)n;
error:
    errno = EINVAL;
    err(1, "%s", s);
    return -1;
}

/* convert YYYYMMDD to time */
static time_t
strtotime(const char *s)
{
    struct tm *tmorig;
    struct tm tm;
    size_t len;
    time_t t;
    char *ep;

    t = time(NULL);
    tmorig = localtime(&t);
    tm = *tmorig;
    len = strlen(s);
    if (len == 2 || len == 1)
        ep = strptime(s, "%d", &tm);
    else if (len == 4)
        ep = strptime(s, "%m%d", &tm);
    else
        ep = strptime(s, "%Y%m%d", &tm);
    if (s[0] == '\0' || ep == NULL || ep[0] != '\0')
        goto error;
    tm.tm_hour = MIDDAY;
    tm.tm_min = 0;
    tm.tm_sec = 0;
    t = mktime(&tm);
    if (t == -1)
        goto error;
    return t;
error:
    errno = EINVAL;
    err(1, "%s", s);
    return (time_t)-1;
}

/* set today time for 12:00; also set number of days after today */
static void
settoday(time_t *today, int *after)
{

    struct tm *tmorig;
    struct tm tm;
    time_t t;

    t = time(NULL);
    tmorig = localtime(&t);
    tm = *tmorig;
    tm.tm_hour = MIDDAY;
    tm.tm_min = 0;
    tm.tm_sec = 0;
    *today = mktime(&tm);
    switch (tm.tm_wday) {
    case 5:
        *after = 3;
        break;
    case 6:
        *after = 2;
        break;
    default:
        *after = 1;
        break;
    }
}

/* check if c is separator */
static int
isseparator(int c)
{
    return c == '-' || c == '.' || c == '/';
}

/* get patterns for event s; also return its name */
static struct Day *
getpatterns(char *s, char **name)
{
    struct tm tm;
    struct Day *patt, *oldpatt;
    struct Day d;
    size_t len;
    int n;
    char *t, *end;

    patt = NULL;
    for (;;) {
        memset(&d, 0, sizeof(d));
        while (isspace(*s)) {
            s++;
        }
        n = strtol(s, &end, 10);
        if (n > 0 && isseparator(*end)) {
            /* got numeric year or month */
            d.month = n;
            s = end + 1;
            n = strtol(s, &end, 10);
            if (n > 0 && isseparator(*end)) {
                /* got numeric month after year */
                d.year = d.month;
                d.month = n;
                s = end + 1;
            } else if ((t = strptime(s, "%b", &tm)) != NULL && isseparator(*t)){
                /* got month name after year */
                d.year = d.month;
                d.month = tm.tm_mon + 1;
                s = t + 1;
            }
        } else if ((t = strptime(s, "%b", &tm)) != NULL && isseparator(*t)) {
            /* got month name */
            d.month = tm.tm_mon + 1;
            s = t + 1;
        }
        n = strtol(s, &end, 10);
        if (n > 0 && *end != '\0') {
            /* got month day */
            d.monthday = n;
            s = end;
        }
        if ((t = strptime(s, "%a", &tm)) != NULL) {
            /* got week day */
            d.weekday = tm.tm_wday + 1;
            s = t;
        }
        if (d.monthday == 0 && d.weekday == 0)
            break;
        n = strtol(s, &end, 10);
        if (n >= -5 && n <= 5 && *end != '\0') {
            d.monthweek = n;
            s = end;
        }
        oldpatt = patt;
        patt = ecalloc(1, sizeof(*patt));
        *patt = d;
        patt->next = oldpatt;
        while (isspace(*s)) {
            s++;
        }
        if (*s == ',') {
            s++;
            while (isspace(*s)) {
                s++;
            }
        } else {
            break;
        }
    }
    *name = s;
    len = strlen(*name);
    if ((*name)[len - 1] == '\n')
        (*name)[len - 1] = '\0';
    return patt;
}

/* get events for file fp */
static struct Event *
getevents(FILE *fp, struct Event *evs)
{
    struct Event *p;
    struct Day *patt;
    char buf[BUFSIZ];
    char *name;

    while (fgets(buf, BUFSIZ, fp) != NULL) {
        if ((patt = getpatterns(buf, &name)) != NULL) {
            p = ecalloc(1, sizeof(*p));
            p->next = evs;
            p->days = patt;
            p->name = estrdup(name);
            evs = p;
        }
    }
    return evs;
}

/* get week of year */
static int
getwofy(int yday, int wday)
{
    return (yday + DAYS_PER_WEEK - (wday ? (wday - 1) : (DAYS_PER_WEEK - 1))) / DAYS_PER_WEEK;
}

/* check if event occurs today */
static int
occurstoday(struct Event *ev, struct tm *tm, int thiswofm, int lastwofm)
{
    struct Day *d;

    for (d = ev->days; d != NULL; d = d->next) {
        if ((d->year == 0 || d->year == tm->tm_year + EPOCH) &&
            (d->month == 0 || d->month == tm->tm_mon + 1) &&
                (d->monthday == 0 || d->monthday == tm->tm_mday) &&
                (d->weekday == 0 || d->weekday == tm->tm_wday + 1) &&
                (d->monthweek == 0 ||
                 (d->monthweek < 0 && d->monthweek == -1 * (lastwofm - thiswofm - 1)) || 
                 (d->monthweek == thiswofm))) {
                    return 1;
            }
    }
    return 0;
}

/* print events for today and after days */
static void
printevents(struct Event *evs, time_t today, int after, int lflag)
{
    struct tm *tmorig;
    struct tm tm;
    struct Event *ev;
    int wofy;       /* week of year of first day of month */
    int thiswofm;   /* this week of current month */
    int lastwofm;   /* last week of current month */
    int n, a, b;
    char buf1[BUFSIZ];
    char buf2[BUFSIZ];

    buf1[0] = buf2[0] = '\0';
    while (after-- > 0) {
        tmorig = localtime(&today);
        tm = *tmorig;
        n = days_in_month[isleap(tm.tm_year + EPOCH)][tm.tm_mon + 1];
        a = (tm.tm_wday - tm.tm_mday + 1) % DAYS_PER_WEEK;
        if (a < 0)
            a += DAYS_PER_WEEK;
        b = (tm.tm_wday + n - tm.tm_mday + 1) % DAYS_PER_WEEK;
        if (b < 0)
            b += DAYS_PER_WEEK;
        wofy = getwofy(tm.tm_yday - tm.tm_mday + 1, a);
        thiswofm = getwofy(tm.tm_yday, tm.tm_wday) - wofy + 1;
        lastwofm = getwofy(tm.tm_yday + n - tm.tm_mday + 1, b) - wofy + 1;
        if (lflag) {
            strftime(buf1, sizeof(buf1), "%A", &tm);
            strftime(buf2, sizeof(buf2), "%d %B %Y", &tm);
            printf("%-10s %s\n", buf1, buf2);
        } else {
            strftime(buf1, sizeof(buf1), "%b %d", &tm);
        }
        for (ev = evs; ev != NULL; ev = ev->next) {
            if (occurstoday(ev, &tm, thiswofm, lastwofm)) {
                if (lflag) {
                    printf("\t");
                } else {
                    printf("%-8s ", buf1);
                }
                printf("%s\n", ev->name);
            }
        }
        today += SECS_PER_DAY;
    }
}

/* calendar: print upcoming events */
int
main(int argc, char *argv[])
{
    static struct Event *evs;
    FILE *fp;
    time_t today;           /* seconds of 12:00:00 of today since epoch*/
    int before;             /* number of days before today */
    int after;              /* number of days after today */
    int lflag;              /* whether to print in long format */
    int exitval;
    int ch;

    before = lflag = 0;
    settoday(&today, &after);
    while ((ch = getopt(argc, argv, "A:B:lt:")) != -1) {
        switch (ch) {
        case 'A':
            after = strtonum(optarg);
            break;
        case 'B':
            before = strtonum(optarg);
            break;
        case 'l':
            lflag = 1;
            break;
        case 't':
            today = strtotime(optarg);
            break;
        default:
            usage();
            break;
        }
    }
    argc -= optind;
    argv += optind;
    today -= before * SECS_PER_DAY;
    evs = NULL;
    exitval = 0;
    if (argc == 0) {
        evs = getevents(stdin, evs);
    } else {
        for (; *argv != NULL; argv++) {
            if (strcmp(*argv, "-") == 0) {
                evs = getevents(stdin, evs);
                continue;
            }
            if ((fp = fopen(*argv, "r")) == NULL) {
                warn("%s", *argv);
                exitval = 1;
                continue;
            }
            evs = getevents(fp, evs);
            fclose(fp);
        }
    }
    printevents(evs, today, after, lflag);
    return exitval;
}

calendar.h:

/* day or day pattern */
struct Day {
    /*
     * This structure express both a day and a day pattern.  For
     * convenience, let's express a Day entry as YYYY-MM-DD-m-w,
     * where:
     * - year is YYYY (1 to INT_MAX)
     * - month is MM (1 to 12)
     * - monthday is DD (1 to 31)
     * - monthweek is m (-5 to 5)
     * - weekday is w (1-Monday to 7-Sunday)
     *
     * A day is expressed with all values nonzero.  For example,
     * 2020-03-11-2-3 represents 11 March 2020, which was a
     * Wednesday (3) on the second week of March.
     *
     * A day pattern can have any value as zero.  A zero value
     * matches anything.  For example:
     * - 0000-12-25-0-0 matches 25 December of every year.
     * - 0000-05-00-2-7 matches the second Sunday of May.
     * - 2020-03-11-2-3 matches 11 March 2020.
     */
    struct Day *next;
    int year;
    int month;
    int monthday;
    int monthweek;
    int weekday;
};

/* event */
struct Event {
    struct Event *next;
    struct Day *days;       /* list of day patterns */
    char *name;             /* event name */
};
\$\endgroup\$
3
\$\begingroup\$

Missing error checks

t = time(NULL);
if (t == -1) handle_error_gracefully();
tmorig = localtime(&t);
if (tmorig == NULL) handle_error_gracefully();

Simplify

Value check not needed.

// if ((errno == ERANGE && (n == LONG_MAX || n == LONG_MIN)) || (n > INT_MAX || n < 0))
if ((errno == ERANGE) || (n > INT_MAX || n < 0))
    goto error;

Better description

As strtonum() fails on negative numbers, consider a different comment or function name.

Overflow detection

As strtonum() results are used in subsequent calculations that lack overflow protection, perhaps form strtonum(const char *s, int min, int max) and pass in limiting range values.

Week of the year

OP’s getwofy(int yday, int wday) does not appear to follow the week-of-the-year per ISO8601 as that depends on the day of the week of Jan 1.

calendar.h does not discuss next member

calendar.h does not discuss a negative monthweek

Unclear how negative values relate, given a value of zero is special.

day vs. struct Day

// A day pattern can have any value as zero. 
A struct Day pattern can have any value as zero. 

calendar.h naming

Consider code re-use. calendar.h introduces struct Day and struct Event. These common names can easily conflict with other code and are surprising to find in a file called calendar.h.

calendar.h missing public functions

I’d expect the calendar functions in calendar.c meant for general use to be non-static and declared in calendar.h.

Else as a stand-alone program, might as well put all of calendar.h in calendar.c as nothing is for general use.

Pedantic: Avoid UB of negative char in is...()

    // while (isspace(*s)) {
    while (isspace(* (unsigned char *)s)) {
        s++;
    }

Various strtol() lack error checks

RAII

// struct Day d;
…

for (;;) {
    // memset(&d, 0, sizeof(d));
    struct Day d = { 0 };

Avoid hacker exploit

Consider what happens when len == 0.

len = strlen(*name);
if ((*name)[len - 1] == '\n')
    (*name)[len - 1] = '\0';

Instead:

(*name)[strcspn(*name, "\n")] = 0;

Minor: Alternative to a < 0

    // a = (tm.tm_wday - tm.tm_mday + 1) % DAYS_PER_WEEK;
    // if (a < 0)
    //     a += DAYS_PER_WEEK;
    a = (tm.tm_wday - tm.tm_mday + 1 + 35 /* some large multiple of 7) % DAYS_PER_WEEK;

Easter

Reference should you want to include Easter.

Minor: Overflow with 16-bit int

Common in embedded processors, 24 * 60 * 60 overflows 16-bit int. Posix, I believe, requires at least 32-bit int.

// #define SECS_PER_DAY  (24 * 60 * 60)
#define SECS_PER_DAY  ((time_t) 24 * 60 * 60)
// or 
#define SECS_PER_DAY  86400

Minor: Failure to account DST

localtime() will populate .tm_isdst, yet noon of that day may have a different .tm_isdst value. Usually best to let mktime() determine best setting.

tmorig = localtime(&t);
tm = *tmorig;
tm.tm_hour = MIDDAY;
tm.tm_min = 0;
tm.tm_sec = 0;
tm.tm_isdst = -1; // Add
*today = mktime(&tm);

Also in strtotime().

Unclear if this affects overall code, yet something to consider.

\$\endgroup\$
5
  • 1
    \$\begingroup\$ Funnily enough I had the exact 16-bit int overflow occur when I was doing an arduino project for a clock. \$\endgroup\$
    – qwr
    Aug 12 at 2:53
  • \$\begingroup\$ @qwr I too had many stubbed toes. See also Why (not) write 1,000,000,000 as 1000*1000*1000. \$\endgroup\$ Aug 12 at 3:36
  • \$\begingroup\$ is there any difference between isspace(*(unsigned char *)s) and isspace((unsigned char)*s)? Here's the preliminary of calendar.c: 0x0.st/-yzU.c I merged the header into the .c file. I'm gonna implement easter later. \$\endgroup\$
    – phillbush
    Aug 15 at 14:59
  • \$\begingroup\$ getwofy should not follow ISO8601. Instead, it follows strftime(3)'s %w format. This is necessary to calculate the number of the week in a month (for a monthweek equals to 2 or -4 in a struct Day to match the second or fourth-to-last week of the month). \$\endgroup\$
    – phillbush
    Aug 15 at 15:04
  • \$\begingroup\$ @phillbush isspace(*(unsigned char *)s) and isspace((unsigned char)*s) differs when char is like signed char and encoded using non-2's complement. isspace(*(unsigned char *)s) is the more formally correct approach. \$\endgroup\$ Aug 15 at 17:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.