Skip to content

Commit 61fbb18

Browse files
Dev: Add Zeller's Congruence utility class to calculate the day of the week (TheAlgorithms#6614)
* Add Zeller's Congruence utility class and unit tests * fixed identified bugs * fixed pmd failure
1 parent 3c071c4 commit 61fbb18

File tree

2 files changed

+143
-0
lines changed

2 files changed

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