This is really the whole Eastertide program, in a single section.

§1. This utility, or perhaps futility, is a minimal example of the use of the Foundation library, which comes supplied with inweb for the benefit of C programs wanting to use it. We will do everything in an unnecessarily fancy way, just to give the tires a kick.

Any program using Foundation must define this constant:

    define PROGRAM_NAME "eastertide"

§2. The main routine must start and end Foundation, and is not allowed to do much after that; but it is allowed to ask if error messages were generated, so that it can return conventional Unix return values 0 (okay) or 1 (not okay).

    int main(int argc, char *argv[]) {
        Foundation::start();

        <Read the command line 2.3>;
        <Write the report to the console 2.1>;

        Foundation::end();
        if (Errors::have_occurred()) return 1;
        return 0;
    }

§2.1. The general scheme here is that we read the command line to work out which the user is interested in, and store those in memory; but eventually we will actually report on them, thus. STDOUT is the "text stream" for standard output, i.e., writing to it will print to the Terminal or similar console.

<Write the report to the console 2.1> =

        Main::report_on_years(STDOUT);

This code is used in §2.

§2.2. All this program does is to print the date of Easter on any years requested, but we'll give it some command-line options anyway. Foundation will add also -help and a few others to the mix.

    enum CALENDAR_FILE_CLSW
    enum QUALIFYING_CLSG
    enum VERBOSE_CLSW
    enum AMERICAN_CLSW

§2.3. <Read the command line 2.3> =

        CommandLine::declare_heading(
            L"eastertide: an Easter date calculator\n\n"
            L"usage: eastertide [OPTIONS] year1 year2 ...\n");

        CommandLine::declare_switch(CALENDAR_FILE_CLSW, L"calendar-file", 2,
            L"specify file X as a list of year requests, one per line");

        CommandLine::begin_group(QUALIFYING_CLSG, I"for qualifying the output");
        CommandLine::declare_boolean_switch(VERBOSE_CLSW, L"verbose", 1,
            L"print output verbosely", FALSE);
        CommandLine::declare_boolean_switch(AMERICAN_CLSW, L"american", 1,
            L"print dates in American MM/DD format", FALSE);
        CommandLine::end_group();
        CommandLine::read(argc, argv, NULL, &Main::switch, &Main::bareword);

This code is used in §2.

§3. That results in dialogue like the following:

        $ eastertide 2020
        12/4/2020
        $ eastertide -help
        eastertide: an Easter date calculator

        usage: eastertide [OPTIONS] year1 year2 ...

        -calendar-file X    specify file X as a list of year requests, one per line

        for qualifying the output:
          -american         print dates in American MM/DD format (default is -no-american)
          -verbose          print output verbosely (default is -no-verbose)

        -at X               specify that this tool is installed at X
        -crash              intentionally crash on internal errors, for backtracing
        -fixtime            pretend the time is 11 a.m. on 28 March 2016 for testing
        -help               print this help information
        -log X              write the debugging log to include diagnostics on X
        -version            print out version number
        $ eastertide -verbose 2021 2022
        Easter in 2021 falls on 4/4.
        Easter in 2022 falls on 17/4.
        $ eastertide 1496
        eastertide: Gregorian calendar only valid from 1582
        $ eastertide 1685 1750
        22/4/1685
        29/3/1750
        $ eastertide -calendar-file cal.txt
        eastertide: cal.txt, line 2: not a year: '1791b'
        15/4/1770
        15/4/1827

§4. So let's get back to how this is done. The Foundation function CommandLine::read calls our function Main::switch when any of our three switches is used (we don't need to handle the ones Foundation added, only our own); and Main::bareword for any other words given on the command line. For example,

        $ eastertide -american -calendar-file cal.txt 1982 2007

...results in two calls to Main::switch, then two to Main::bareword.

    int verbose_mode = FALSE;
    int american_mode = FALSE;

    void Main::bareword(int id, text_stream *arg, void *state) {
        Main::request(arg, NULL);
    }

    void Main::switch(int id, int val, text_stream *arg, void *state) {
        switch (id) {
            case VERBOSE_CLSW: verbose_mode = val; break;
            case AMERICAN_CLSW: american_mode = val; break;
            case CALENDAR_FILE_CLSW: <Process calendar file 4.1>; break;
        }
    }

§4.1. We will read in the calendar file as soon as it is mentioned:

<Process calendar file 4.1> =

        filename *F = Filenames::from_text(arg);
        TextFiles::read(F, FALSE, "can't open calendar file",
            TRUE, Main::calendar_line, NULL, NULL);

This code is used in §4.

§5. To make this a little more gratuitous, we'll give calendar files some syntax. The following function is called on each line in turn; we're going to trim white space, ignore blank lines, and also ignore any line beginning withn a # as being a comment.

    void Main::calendar_line(text_stream *line, text_file_position *tfp, void *state) {
        Str::trim_white_space(line);
        if (Str::len(line) == 0) return;
        if (Str::get_first_char(line) == '#') return;
        Main::request(line, tfp);
    }

§6. And with that done, we can process a request for a year, which comes from either the command lihe (in which case tfp here is null), or from the calendar file (in which case it remembers the filename and line number).

    void Main::request(text_stream *year, text_file_position *tfp) {
        int bad_digit = FALSE;
        LOOP_THROUGH_TEXT(pos, year)
            if (Characters::isdigit(Str::get(pos)) == FALSE)
                bad_digit = TRUE;
        if (bad_digit) {
            TEMPORARY_TEXT(err);
            WRITE_TO(err, "not a year: '%S'", year);
            Errors::in_text_file_S(err, tfp);
            return;
        }
        int Y = Str::atoi(year, 0);
        if (Y < 1582) {
            Errors::in_text_file("Gregorian calendar only valid from 1582", tfp);
            return;
        }
        Main::new_year(Y);
    }

§7. Now it's time to actually store a request. There are many simpler ways to do this, but we want to demonstrate Foundation's objects system in action, so we'll wrap each year supplied in an object. First, we have to define an ID constant for this new class of object, and use a macro which causes Foundation to generate the necessary handling functions:

    enum year_request_MT
    ALLOCATE_INDIVIDUALLY(year_request)

§8. Now we should define the rather unnecessary structure itself, and then a sort of constructor function. We won't need a destructor: we will never destroy the years.

    typedef struct year_request {
        int year;
        MEMORY_MANAGEMENT
    } year_request;

    year_request *Main::new_year(int Y) {
        year_request *YR = CREATE(year_request);
        YR->year = Y;
        return YR;
    }

The structure year_request is private to this section.

§9. We can't spin this out much longer, though... This is the actually functional part of the program, and even so, it only calls a routine in Foundation. (See Time::easter.)

    void Main::report_on_years(text_stream *OUT) {
        year_request *YR;
        LOOP_OVER(YR, year_request) {
            int d, m;
            Time::easter(YR->year, &d, &m);
            if (american_mode) { int x = d; d = m; m = x; }
            if (verbose_mode) WRITE("Easter in %d falls on %d/%d.\n", YR->year, d, m);
            else WRITE("%d/%d/%d\n", d, m, YR->year);
        }
    }