3
\$\begingroup\$

Computing Easter for a given year is a classic computational problem.

It is also one of the few cases when code seems to just have to live with magic numbers.

Alternative to magic numbers: Decoding Gauss' Easter Algorithm

So I thought today, I'd do something positive and make an Easter bar graph.

C99 Review goal: General coding comments, style, etc.

// easter.h
// chux: April 15, 2019

#ifndef EASTER_H
#define EASTER_H

#define EASTER_EPOCH_YEAR 33
#define EASTER_JULIAN_YEAR EASTER_EPOCH_YEAR
#define EASTER_GREGORIAN_EPOCH_YEAR 1582 /* 15 October 1582 */
#define EASTER_JULIAN_PERIOD 532
#define EASTER_GREGORIAN_PERIOD 5700000

typedef struct ymd {
  int y, m, d;
} ymd;

ymd Easter_DateJulian(int year);
ymd Easter_DateGregorian(int year);
ymd Easter_Date(int year);

#endif

// easter.c

/*
 * Anonymous Gregorian algorithm: Meeus/Jones/Butcher
 *  * Dates of Easter
 * Astronomical Algorithms 1991
 * Jean Meeus
 * https://en.wikipedia.org/wiki/Computus#Anonymous_Gregorian_algorithm
 * 
 * Meeus's Julian algorithm
 * https://en.wikipedia.org/wiki/Computus#Meeus's_Julian_algorithm
 */
ymd Easter_DateJulian(int year) {
  if (year < EASTER_EPOCH_YEAR) {
    return (ymd) {0, 0, 0};
  }
  int a = year % 4;
  int b = year % 7;
  int c = year % 19;
  int d = (19 * c + 15) % 30;
  int e = (2 * a + 4 * b - d + 34) % 7;
  int f = (d + e + 114) / 31;
  int g = (d + e + 114) % 31;
  return (ymd) {year, f, g + 1};
}

ymd Easter_DateGregorian(int year) {
  if (year <= EASTER_GREGORIAN_EPOCH_YEAR) {
    return (ymd) {0, 0, 0};
  }
  int a = year%19;
  int b = year/100;
  int c = year%100;
  int d = b/4;
  int e = b%4;
  int f = (b+8)/25;
  int g = (b-f+1)/3;
  int h = (19*a + b - d - g + 15)%30;
  int i = c/4;
  int k = c%4;
  int l = (32 + 2*e + 2*i - h - k)%7;
  int m = (a+11*h + 22*l) / 451;
  int n = (h + l - 7 *m + 114)/31;
  int p = (h + l - 7 *m + 114)%31;
  return (ymd) {year, n, p+1};
}

ymd Easter_Date(int year) {
  return (year > EASTER_GREGORIAN_EPOCH_YEAR) ?
      Easter_DateGregorian(year) : Easter_DateJulian(year);
}

Test

// main.c

// **Alternate code used as a check**
// Find easter on any given year
// https://codereview.stackexchange.com/questions/193847/find-easter-on-any-given-year
// Decoding Gauss' Easter Algorithm
// https://math.stackexchange.com/q/896954/83175

static ymd Easter(int year) {
    int a = year%19;
    int b = year/100;
    int c = (b - (b/4) - ((8*b + 13)/25) + (19*a) + 15)%30;
    int d = c - (c/28)*(1 - (c/28)*(29/(c + 1))*((21 - a)/11));
    int e = d - ((year + (year/4) + d + 2 - b + (b/4))%7);
    int month = 3 + ((e + 40)/44);
    int day = e + 28 - (31*(month/4));
    return (ymd) {year, month , day};
}

#include <assert.h>
#include <stdio.h>

int main(void) {
  int count[5][32] = { 0 };
  for (int year = EASTER_GREGORIAN_EPOCH_YEAR + 1;
      year <= EASTER_GREGORIAN_EPOCH_YEAR + EASTER_GREGORIAN_PERIOD;
      year++) {
    ymd e1 = Easter_Date(year);
    ymd e2 = Easter(year);
    if (e1.d != e2.d) {
      printf("%5d-%02d-%02d ", e1.y, e1.m, e1.d);
      printf("%5d-%02d-%02d ", e2.y, e2.m, e2.d);
      puts("");
    }
    assert(e1.m >= 3 && e1.m <=4);
    assert(e1.d >= 1 && e1.d <=31);
    count[e1.m][e1.d]++;
  }
  for (int m = 3; m <= 4; m++) {
    for (int d = 1; d <= 31; d++) {
      if (count[m][d]) {
        double permill =  round(1000.0*count[m][d]/EASTER_GREGORIAN_PERIOD);
        printf("%d, %2d, %3.1f%%, %0*d\n", m, d, permill/10, (int) permill, 0);
      }
    }
  }
  return 0;
}

Output: Month, Day, Percentage, Graph

3, 22, 0.5%, 00000
3, 23, 1.0%, 0000000000
3, 24, 1.4%, 00000000000000
3, 25, 1.9%, 0000000000000000000
3, 26, 2.3%, 00000000000000000000000
3, 27, 2.9%, 00000000000000000000000000000
3, 28, 3.3%, 000000000000000000000000000000000
3, 29, 3.4%, 0000000000000000000000000000000000
3, 30, 3.3%, 000000000000000000000000000000000
3, 31, 3.3%, 000000000000000000000000000000000
4,  1, 3.4%, 0000000000000000000000000000000000
4,  2, 3.3%, 000000000000000000000000000000000
4,  3, 3.4%, 0000000000000000000000000000000000
4,  4, 3.3%, 000000000000000000000000000000000
4,  5, 3.4%, 0000000000000000000000000000000000
4,  6, 3.3%, 000000000000000000000000000000000
4,  7, 3.3%, 000000000000000000000000000000000
4,  8, 3.4%, 0000000000000000000000000000000000
4,  9, 3.3%, 000000000000000000000000000000000
4, 10, 3.4%, 0000000000000000000000000000000000
4, 11, 3.3%, 000000000000000000000000000000000
4, 12, 3.4%, 0000000000000000000000000000000000
4, 13, 3.3%, 000000000000000000000000000000000
4, 14, 3.3%, 000000000000000000000000000000000
4, 15, 3.4%, 0000000000000000000000000000000000
4, 16, 3.3%, 000000000000000000000000000000000
4, 17, 3.4%, 0000000000000000000000000000000000
4, 18, 3.5%, 00000000000000000000000000000000000
4, 19, 3.9%, 000000000000000000000000000000000000000
4, 20, 3.3%, 000000000000000000000000000000000  <-- 2019
4, 21, 2.9%, 00000000000000000000000000000
4, 22, 2.4%, 000000000000000000000000
4, 23, 1.9%, 0000000000000000000
4, 24, 1.5%, 000000000000000
4, 25, 0.7%, 0000000
\$\endgroup\$
  • \$\begingroup\$ Can you clarify what percentage means here? Percentage of what? \$\endgroup\$ – user1118321 Apr 16 at 5:11
  • \$\begingroup\$ @user1118321 True " percentage" lacks specific context. It is the percentage of occurrence. The values sum to 100.0% \$\endgroup\$ – chux Apr 16 at 12:12
1
\$\begingroup\$

Nice work! Here are my comments, mostly style related:

easter.h:

easter.c:

  • #include "easter.h" is missing.
  • In Easter_DateGregorian, variables n and p could be renamed month and day, respectively. While this knowledge could be implied from the return statement, it's better to give them a meaningful name. Then, the + 1 should be added to day, instead of when returning.
  • Similarly, in Easter_DateJulian, f and g could be renamed month and day, respectively.

main.c:

  • #include "easter.h" is missing.
  • The function Easter appears before the #includes block: my recommendation is always placing #includes at the very beginning of the source file. It is useful to know that includes are always found in the same place, and not hiding in the source code. Also, since Easter is a reference function, consider a more expressive name such as Easter_Ref.
  • #include <math.h> is missing for round.h.
  • Since count is a two-dimentional array, it should be initialized with double braces: int count[5][32] = { { 0 } };.
  • Consider using named constants for months 3 and 4, such as MARCH and APRIL. That way, the meaning of m is clear immediately.
  • From the same reasoning, consider renaming m and d to month and day respectively.
\$\endgroup\$

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.