Skip to content

Commit 957100c

Browse files
authored
feat: Add Zeller's congruence algorithm (TheAlgorithms#105)
1 parent f2c0c36 commit 957100c

File tree

2 files changed

+180
-0
lines changed

2 files changed

+180
-0
lines changed

maths/test/zellers_congruence.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { Calendar, GetWeekday } from "../zellers_congruence";
2+
3+
describe("Zeller's congruence", () => {
4+
test.each([
5+
{ year: 2000, month: 1, day: 1, expected: 6 },
6+
{ year: 2000, month: 2, day: 1, expected: 2 },
7+
{ year: 2000, month: 3, day: 1, expected: 3 },
8+
{ year: 2000, month: 4, day: 1, expected: 6 },
9+
{ year: 2000, month: 5, day: 1, expected: 1 },
10+
{ year: 2000, month: 6, day: 1, expected: 4 },
11+
{ year: 2000, month: 7, day: 1, expected: 6 },
12+
{ year: 2000, month: 8, day: 1, expected: 2 },
13+
{ year: 2000, month: 9, day: 1, expected: 5 },
14+
{ year: 2000, month: 10, day: 1, expected: 0 },
15+
{ year: 2000, month: 11, day: 1, expected: 3 },
16+
{ year: 2000, month: 12, day: 1, expected: 5 },
17+
{ year: 1, month: 1, day: 1, expected: 1 },
18+
{ year: 23, month: 2, day: 28, expected: 2 },
19+
{ year: 456, month: 3, day: 31, expected: 5 },
20+
{ year: 1850, month: 4, day: 1, expected: 1 },
21+
{ year: 2100, month: 12, day: 31, expected: 5 },
22+
{ year: 3000, month: 12, day: 31, expected: 3 },
23+
])(
24+
`The weekday of $year-$month-$day in the default calendar is $expected`,
25+
({ year, month, day, expected }) => {
26+
expect(GetWeekday(year, month, day)).toEqual(expected);
27+
}
28+
);
29+
30+
test.each([
31+
{ year: 1500, month: 1, day: 1, expected: 3 },
32+
{ year: 1500, month: 2, day: 1, expected: 6 },
33+
{ year: 1500, month: 3, day: 1, expected: 0 },
34+
{ year: 1500, month: 4, day: 1, expected: 3 },
35+
{ year: 1500, month: 5, day: 1, expected: 5 },
36+
{ year: 1500, month: 6, day: 1, expected: 1 },
37+
{ year: 1500, month: 7, day: 1, expected: 3 },
38+
{ year: 1500, month: 8, day: 1, expected: 6 },
39+
{ year: 1500, month: 9, day: 1, expected: 2 },
40+
{ year: 1500, month: 10, day: 1, expected: 4 },
41+
{ year: 1500, month: 11, day: 1, expected: 0 },
42+
{ year: 1500, month: 12, day: 1, expected: 2 },
43+
{ year: 1, month: 1, day: 1, expected: 6 },
44+
{ year: 23, month: 2, day: 28, expected: 0 },
45+
{ year: 456, month: 3, day: 31, expected: 6 },
46+
{ year: 1582, month: 2, day: 1, expected: 4 },
47+
])(
48+
`The weekday of $year-$month-$day in the Julian calendar is $expected`,
49+
({ year, month, day, expected }) => {
50+
expect(GetWeekday(year, month, day, Calendar.Julian)).toEqual(expected);
51+
}
52+
);
53+
54+
test(`The default calendar is Gregorian`, () => {
55+
expect(GetWeekday(1, 1, 1)).toEqual(1);
56+
});
57+
58+
test.each([
59+
{ year: 1, month: 1, day: 1, expected: 1 },
60+
{ year: 23, month: 2, day: 28, expected: 2 },
61+
{ year: 456, month: 3, day: 31, expected: 5 },
62+
{ year: 1850, month: 4, day: 1, expected: 1 },
63+
{ year: 2000, month: 1, day: 1, expected: 6 },
64+
{ year: 2100, month: 12, day: 31, expected: 5 },
65+
{ year: 3000, month: 12, day: 31, expected: 3 },
66+
])(
67+
`The weekday for $year-$month-$day in the default calendar matches getUTCDay`,
68+
({ year, month, day }) => {
69+
// Convert to a string to avoid Date constructor mapping 1 to year 1901
70+
const dateString = `${year.toString().padStart(4, "0")}-${month
71+
.toString()
72+
.padStart(2, "0")}-${day.toString().padStart(2, "0")}`;
73+
expect(GetWeekday(year, month, day)).toEqual(
74+
new Date(dateString).getUTCDay()
75+
);
76+
}
77+
);
78+
79+
test.each([
80+
{ year: 0, month: 1, day: 1 },
81+
{ year: -5, month: 1, day: 1 },
82+
{ year: 12.2, month: 1, day: 1 },
83+
])(`Should throw an error for invalid year $year`, ({ year, month, day }) => {
84+
expect(() => GetWeekday(year, month, day)).toThrow(
85+
"Year must be an integer greater than 0"
86+
);
87+
});
88+
89+
test.each([
90+
{ year: 2001, month: -5, day: 1 },
91+
{ year: 2001, month: 0, day: 1 },
92+
{ year: 2001, month: 13, day: 1 },
93+
{ year: 2001, month: 9.3, day: 1 },
94+
])(
95+
`Should throw an error for invalid month $month`,
96+
({ year, month, day }) => {
97+
expect(() => GetWeekday(year, month, day)).toThrow(
98+
"Month must be an integer between 1 and 12"
99+
);
100+
}
101+
);
102+
103+
test.each([
104+
{ year: 2001, month: 1, day: -5 },
105+
{ year: 2001, month: 1, day: 0 },
106+
{ year: 2001, month: 1, day: 32 },
107+
])(`Should throw an error for invalid day $day`, ({ year, month, day }) => {
108+
expect(() => GetWeekday(year, month, day)).toThrow(
109+
"Day must be an integer between 1 and 31"
110+
);
111+
});
112+
});

maths/zellers_congruence.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
export enum Calendar {
2+
Gregorian,
3+
Julian,
4+
}
5+
6+
/**
7+
* @function GetWeekday
8+
* @description Calculate the day of the week for any Julian or Gregorian calendar date.
9+
* @param {number} year - Year with century.
10+
* @param {number} month - Month of the year (1-12).
11+
* @param {number} day - Day of the month (1-31).
12+
* @return {number} Day of the week, where 0 represents Sunday.
13+
* @see https://en.wikipedia.org/wiki/Zeller's_congruence
14+
* @example GetWeekday(2000, 1, 1) = 6
15+
* @example GetWeekday(1500, 1, 1, Calendar.Julian) = 3
16+
*/
17+
export const GetWeekday = (
18+
year: number,
19+
month: number,
20+
day: number,
21+
calendar: Calendar = Calendar.Gregorian
22+
): number => {
23+
// Input validation
24+
if (!Number.isInteger(year) || year < 1) {
25+
throw new Error("Year must be an integer greater than 0");
26+
}
27+
28+
if (!Number.isInteger(month) || month < 1 || month > 12) {
29+
throw new Error("Month must be an integer between 1 and 12");
30+
}
31+
32+
if (!Number.isInteger(day) || day < 1 || day > 31) {
33+
throw new Error("Day must be an integer between 1 and 31");
34+
}
35+
36+
// Move January and February to the end of the previous year
37+
if (month < 3) {
38+
month += 12;
39+
year--;
40+
}
41+
42+
const century = Math.floor(year / 100);
43+
year %= 100;
44+
45+
let weekday: number | undefined = undefined;
46+
if (calendar === Calendar.Gregorian) {
47+
weekday =
48+
(day +
49+
Math.floor(2.6 * (month + 1)) +
50+
year +
51+
Math.floor(year / 4) +
52+
Math.floor(century / 4) +
53+
5 * century) %
54+
7;
55+
} else {
56+
weekday =
57+
(day +
58+
Math.floor(2.6 * (month + 1)) +
59+
year +
60+
Math.floor(year / 4) +
61+
5 +
62+
6 * century) %
63+
7;
64+
}
65+
66+
// Convert to Sunday being 0
67+
return (weekday + 6) % 7;
68+
};

0 commit comments

Comments
 (0)