Skip to content

Commit 7bec682

Browse files
Add Zeller's Congruence utility class and unit tests
1 parent 7e29be3 commit 7bec682

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package com.thealgorithms.maths;
2+
3+
import java.time.DateTimeException;
4+
import java.time.LocalDate;
5+
6+
/**
7+
* A utility class for calculating the day of the week for a given date using Zeller's Congruence.
8+
*
9+
* <p>Zeller's Congruence is an algorithm devised by Christian Zeller in the 19th century to calculate
10+
* the day of the week for any Julian or Gregorian calendar date. The input date must be in the format
11+
* "MM-DD-YYYY" or "MM/DD/YYYY".
12+
*
13+
* <p>This class is final and cannot be instantiated.
14+
*
15+
* @see <a href="https://en.wikipedia.org/wiki/Zeller%27s_congruence">Wikipedia: Zeller's Congruence</a>
16+
*/
17+
public final class ZellersCongruence {
18+
19+
private static final String[] DAYS = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
20+
21+
// Private constructor to prevent instantiation
22+
private ZellersCongruence() {
23+
}
24+
25+
/**
26+
* Calculates the day of the week for a given date using Zeller's Congruence.
27+
*
28+
* <p>The algorithm works for both Julian and Gregorian calendar dates. The input date must be
29+
* in the format "MM-DD-YYYY" or "MM/DD/YYYY".
30+
*
31+
* @param input the date in the format "MM-DD-YYYY" or "MM/DD/YYYY"
32+
* @return a string indicating the day of the week for the given date
33+
* @throws IllegalArgumentException if the input format is invalid, the date is invalid,
34+
* or the year is out of range
35+
*/
36+
public static String calculateDay(String input) {
37+
if (input == null || input.length() != 10) {
38+
throw new IllegalArgumentException("Input date must be 10 characters long in the format MM-DD-YYYY or MM/DD/YYYY.");
39+
}
40+
41+
int month = parsePart(input.substring(0, 2), 1, 12, "Month must be between 1 and 12.");
42+
char sep1 = input.charAt(2);
43+
validateSeparator(sep1);
44+
45+
int day = parsePart(input.substring(3, 5), 1, 31, "Day must be between 1 and 31.");
46+
char sep2 = input.charAt(5);
47+
validateSeparator(sep2);
48+
49+
int year = parsePart(input.substring(6, 10), 46, 8499, "Year must be between 46 and 8499.");
50+
51+
try {
52+
LocalDate.of(year, month, day);
53+
} catch (DateTimeException e) {
54+
throw new IllegalArgumentException("Invalid date.");
55+
}
56+
if (month <= 2) {
57+
year -= 1;
58+
month += 12;
59+
}
60+
61+
int century = year / 100;
62+
int yearOfCentury = year % 100;
63+
int t = (int) (2.6 * month - 5.39);
64+
int u = century / 4;
65+
int v = yearOfCentury / 4;
66+
int f = Math.round((day + yearOfCentury + t + u + v - 2 * century) % 7);
67+
68+
int correctedDay = (f + 7) % 7;
69+
70+
return "The date " + input + " falls on a " + DAYS[correctedDay] + ".";
71+
}
72+
73+
/**
74+
* Parses a part of the date string and validates its range.
75+
*
76+
* @param part the substring to parse
77+
* @param min the minimum valid value
78+
* @param max the maximum valid value
79+
* @param error the error message to throw if validation fails
80+
* @return the parsed integer value
81+
* @throws IllegalArgumentException if the part is not a valid number or is out of range
82+
*/
83+
private static int parsePart(String part, int min, int max, String error) {
84+
try {
85+
int value = Integer.parseInt(part);
86+
if (value < min || value > max) {
87+
throw new IllegalArgumentException(error);
88+
}
89+
return value;
90+
} catch (NumberFormatException e) {
91+
throw new IllegalArgumentException("Invalid numeric part: " + part);
92+
}
93+
}
94+
95+
/**
96+
* Validates the separator character in the date string.
97+
*
98+
* @param sep the separator character
99+
* @throws IllegalArgumentException if the separator is not '-' or '/'
100+
*/
101+
private static void validateSeparator(char sep) {
102+
if (sep != '-' && sep != '/') {
103+
throw new IllegalArgumentException("Date separator must be '-' or '/'.");
104+
}
105+
}
106+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.thealgorithms.maths;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
6+
import java.util.stream.Stream;
7+
import org.junit.jupiter.params.ParameterizedTest;
8+
import org.junit.jupiter.params.provider.Arguments;
9+
import org.junit.jupiter.params.provider.MethodSource;
10+
11+
class ZellersCongruenceTest {
12+
13+
static Stream<Arguments> validDates() {
14+
return Stream.of(Arguments.of("01-01-2000", "Saturday"), Arguments.of("12-25-2021", "Saturday"), Arguments.of("07-04-1776", "Thursday"), Arguments.of("02-29-2020", "Saturday"), Arguments.of("03-01-1900", "Thursday"), Arguments.of("03/01/1900", "Thursday"));
15+
}
16+
17+
static Stream<Arguments> invalidDates() {
18+
return Stream.of(Arguments.of("13-01-2000", "Month must be between 1 and 12."), Arguments.of("02-30-2020", "Invalid date."), Arguments.of("00-15-2020", "Month must be between 1 and 12."), Arguments.of("01-01-0000", "Year must be between 46 and 8499."),
19+
Arguments.of("01/01/200", "Input date must be 10 characters long in the format MM-DD-YYYY or MM/DD/YYYY."), Arguments.of("01@01>2000", "Date separator must be '-' or '/'."), Arguments.of("aa-01-1900", "Invalid numeric part: aa"),
20+
Arguments.of(null, "Input date must be 10 characters long in the format MM-DD-YYYY or MM/DD/YYYY."));
21+
}
22+
23+
@ParameterizedTest
24+
@MethodSource("validDates")
25+
void testValidDates(String inputDate, String expectedDay) {
26+
String result = ZellersCongruence.calculateDay(inputDate);
27+
assertEquals("The date " + inputDate + " falls on a " + expectedDay + ".", result);
28+
}
29+
30+
@ParameterizedTest
31+
@MethodSource("invalidDates")
32+
void testInvalidDates(String inputDate, String expectedErrorMessage) {
33+
Exception exception = assertThrows(IllegalArgumentException.class, () -> ZellersCongruence.calculateDay(inputDate));
34+
assertEquals(expectedErrorMessage, exception.getMessage());
35+
}
36+
}

0 commit comments

Comments
 (0)