diff --git a/src/main/java/edu/byu/cs/autograder/Grader.java b/src/main/java/edu/byu/cs/autograder/Grader.java index f8f3decdb..3691e0c31 100644 --- a/src/main/java/edu/byu/cs/autograder/Grader.java +++ b/src/main/java/edu/byu/cs/autograder/Grader.java @@ -111,6 +111,8 @@ public abstract class Grader implements Runnable { protected Observer observer; + private DateTimeUtils dateTimeUtils; + /** * Creates a new grader * @@ -136,6 +138,19 @@ public Grader(String repoUrl, String netId, Observer observer, Phase phase) thro this.requiredCommits = 10; this.observer = observer; + + this.initializeDateUtils(); + } + + private void initializeDateUtils() { + this.dateTimeUtils = new DateTimeUtils(); + dateTimeUtils.initializePublicHolidays(getEncodedPublicHolidays()); + } + private String getEncodedPublicHolidays() { + // FIXME: Return from some dynamic location like a configuration file or a configurable table + return "1/1/2024;1/15/2024;2/19/2024;3/15/2024;4/25/2024;5/27/2024;6/19/2024;" + + "7/4/2024;7/24/2024;9/2/2024;11/27/2024;11/28/2024;11/29/2024;12/24/2024;12/25/2024;12/31/2024;" + + "1/1/2025;"; } public void run() { @@ -342,7 +357,7 @@ private int calculateLateDays(Rubric rubric) { } ZonedDateTime handInDate = DaoService.getQueueDao().get(netId).timeAdded().atZone(ZoneId.of("America/Denver")); - return Math.min(DateTimeUtils.getNumDaysLate(handInDate, dueDate), MAX_LATE_DAYS_TO_PENALIZE); + return Math.min(dateTimeUtils.getNumDaysLate(handInDate, dueDate), MAX_LATE_DAYS_TO_PENALIZE); } private float calculateScoreWithLatePenalty(Rubric rubric, int numDaysLate) { diff --git a/src/main/java/edu/byu/cs/util/DateTimeUtils.java b/src/main/java/edu/byu/cs/util/DateTimeUtils.java index 66bfcf355..d09562af6 100644 --- a/src/main/java/edu/byu/cs/util/DateTimeUtils.java +++ b/src/main/java/edu/byu/cs/util/DateTimeUtils.java @@ -1,13 +1,25 @@ package edu.byu.cs.util; +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; + import java.time.Instant; +import java.time.LocalDate; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.Date; -import java.util.Set; +import java.time.format.DateTimeParseException; +import java.util.*; +/** + * Has helper methods for handling dates. + * Note that some methods are available statically. + * but some methods require configuration ahead of time and are available + * only on instances of the class. + * TODO: Design a more intentional DateTimeUtils API for consistently referencing methods. + */ public class DateTimeUtils { + private Set publicHolidays; /** * Generates a String representation of a timestamp @@ -16,7 +28,7 @@ public class DateTimeUtils { * @param includeTime whether the time is included * @return a string formatted like "yyyy-MM-dd HH:mm:ss" */ - public static String getDateString(long timestamp, boolean includeTime) { + public static String getDateString(@NonNull long timestamp, boolean includeTime) { ZonedDateTime zonedDateTime = Instant.ofEpochSecond(timestamp).atZone(ZoneId.of("America/Denver")); String pattern = includeTime ? "yyyy-MM-dd HH:mm:ss" : "yyyy-MM-dd"; DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); @@ -24,13 +36,23 @@ public static String getDateString(long timestamp, boolean includeTime) { } /** - * Gets the number of days late the submission is. This excludes weekends and public holidays + * Gets the number of days late the submission is. This excludes weekends and public holidays. + * + * In the event that the due date also happens to be a holiday, the assignment not receive + * a late penalty until AFTER the holiday and weekends following it. While this behavior + * may be surprising, it can be controlled by professors being careful to never assign a + * due date on a holiday. This behavior is illustrated in the provided test cases. * * @param handInDate the date the submission was handed in * @param dueDate the due date of the phase * @return the number of days late or 0 if the submission is not late */ - public static int getNumDaysLate(ZonedDateTime handInDate, ZonedDateTime dueDate) { + public int getNumDaysLate(@NonNull ZonedDateTime handInDate, @NonNull ZonedDateTime dueDate) { + if (publicHolidays == null) { + throw new RuntimeException("Public Holidays have not yet been initialized. " + + "Call `dateTimeUtils.initializePublicHolidays()` before attempting to count the days late."); + } + int daysLate = 0; while (handInDate.isAfter(dueDate)) { @@ -43,20 +65,152 @@ public static int getNumDaysLate(ZonedDateTime handInDate, ZonedDateTime dueDate return daysLate; } + /** + * Initializes the public holidays to an empty value. + * Useful for testing when it's recognized that no holidays exist. + */ + public void initializePublicHolidays() { + publicHolidays = new HashSet<>(); + } + /** + * Initializes our public holidays with a common formatting string + * that will accept strings matching this example: "9/16/2024" + * + * @see #initializePublicHolidays(String, String) + * + * @param encodedPublicHolidays A string representing the encoded data. + */ + public Set initializePublicHolidays(@NonNull String encodedPublicHolidays) { + return initializePublicHolidays(encodedPublicHolidays, "M/d/yyyy"); + } + + /** + * Initializes our internal public holidays data set with the given string and a given date format. + * This method also returns the constructed set of dates. + * + * This must be called before calls to {@code getNumDaysLate()} will properly account for the holidays. + * + * @see #interpretPublicHolidays(String, String) + * + * @param encodedPublicHolidays @non-nullable A string representing the configured data + * @param dateEncodingFormat A string representing the intended date format within the data + */ + public Set initializePublicHolidays(@NonNull String encodedPublicHolidays, @NonNull String dateEncodingFormat) { + if (encodedPublicHolidays == null || dateEncodingFormat == null) { + throw new RuntimeException("Error initializing public holidays. Received null as a parameter. " + + "If some data isn't available, explicitly call the no argument initializer instead."); + } + + publicHolidays = interpretPublicHolidays(encodedPublicHolidays, dateEncodingFormat); + // TODO: Validate that some holidays are configured for the current calendar year and throw an error otherwise + return publicHolidays; + } + + /** + *

Public Holiday Decoding

+ * Interprets a string of encoded public holidays and returns a set of objects that + * can be used to efficiently query against the dataset. + *
+ * + *

Encoding Approaches

+ * The input should be only one of the two approaches. It should either be a single-line entry with + * zero or more dates, or it should be a multi-line string which can include comments following the date string. + *
+ * + *

Single Line Encoding

+ * Dates can be stored on a single line, separated by any combination of the following: + *
    + *
  • spaces " ", or
  • + *
  • commas ",", or
  • + *
  • semicolon ";"
  • + *
+ *
+ * + * Each word will be interpreted individually, and any that do not parse into a date properly + * will be excluded individually. + *
+ * + *

Multi-line Encoding

+ * Additionally, the dates can each appear on a separate line. + * Only the first word of the line will be interpreted as a date, + * and the rest of the line will be ignored as a comment. + * Any line that does not contain a date format at the beginning + * (i.e. an empty line) will be ignored and not considered for interpretation. + * Lines beginning with {@code #} are always ignored as a comment; however, a comment + * need not begin with a special symbol. Lines where a date cannot be interpreted + * from the first word will simply be skipped. + *
+ * + *

Encoding Date Formats

+ * Since both approaches use whitespace to delimit words, no acceptable date format can ever contain + * a whitespace character. This is the only restraint for the multi-line approach; however, + * however, in the single line approach, the format cannot contain any of the delimiting characters. + *
+ * + * @param encodedPublicHolidays A string representing the public holidays. + * This could be read in from a file or from a cell in a table + * depending on the most convenient form of maintaining this information. + * @param dateEncodingFormat A string representing the date format. + * This will be interpreted by {@link DateTimeFormatter}. + * @return A {@code Set} that will be used to efficiently comparing against this dataset + */ + private Set interpretPublicHolidays(@Nullable String encodedPublicHolidays, @NonNull String dateEncodingFormat) { + String[] dateStrings; + if (encodedPublicHolidays == null) { + dateStrings = new String[]{}; + } else if (encodedPublicHolidays.contains("\n")) { + dateStrings = extractPublicHolidaysMultiline(encodedPublicHolidays); + } else { + dateStrings = extractPublicHolidaysSingleline(encodedPublicHolidays); + } + + return parsePublicHolidayStrings(dateEncodingFormat, dateStrings); + } + private String[] extractPublicHolidaysSingleline(String singleLineEncodedHolidays) { + String DELIMITERS = " ,;"; + return singleLineEncodedHolidays.split("["+DELIMITERS+"]"); + } + private String[] extractPublicHolidaysMultiline(String multilineEncodedHolidays) { + ArrayList holidays = new ArrayList<>(); + Scanner scanner = new Scanner(multilineEncodedHolidays); + while (scanner.hasNext()) { + holidays.add(scanner.next()); // The first word in interpreted as a date + scanner.nextLine(); // Skip the rest of the line + } + return holidays.toArray(new String[0]); + } + private Set parsePublicHolidayStrings(@NonNull String dateFormat, String[] holidayDateStrings) { + Set publicHolidays = new HashSet<>(); + var parser = DateTimeFormatter.ofPattern(dateFormat); + for (var holidayDateString : holidayDateStrings) { + if (holidayDateString.isEmpty()) { + continue; // Empty line + } + if (holidayDateString.charAt(0) == '#') { + continue; // Explicitly marked as a comment + } + try { + publicHolidays.add(parser.parse(holidayDateString, LocalDate::from)); + } catch (DateTimeParseException e) { + System.out.println("Skipping unrecognized date string: " + holidayDateString); + } + } + return publicHolidays; + } + /** * Checks if the given date is a public holiday * * @param zonedDateTime the date to check * @return true if the date is a public holiday, false otherwise */ - private static boolean isPublicHoliday(ZonedDateTime zonedDateTime) { - Date date = Date.from(zonedDateTime.toInstant()); - // TODO: use non-hardcoded list of public holidays - Set publicHolidays = Set.of( - Date.from(ZonedDateTime.parse("2023-02-19T00:00:00.000Z").toInstant()), - Date.from(ZonedDateTime.parse("2023-03-15T00:00:00.000Z").toInstant()) - ); + private boolean isPublicHoliday(@NonNull ZonedDateTime zonedDateTime) { + if (publicHolidays == null) { +// return false; // Holidays have not been initialized + throw new RuntimeException("Holiday settings have not been initialized properly"); + } + LocalDate date = LocalDate.of(zonedDateTime.getYear(), zonedDateTime.getMonthValue(), zonedDateTime.getDayOfMonth()); return publicHolidays.contains(date); } } diff --git a/src/test/java/edu/byu/cs/util/DateTimeUtilsTest.java b/src/test/java/edu/byu/cs/util/DateTimeUtilsTest.java index 42905eacb..70b2c7d51 100644 --- a/src/test/java/edu/byu/cs/util/DateTimeUtilsTest.java +++ b/src/test/java/edu/byu/cs/util/DateTimeUtilsTest.java @@ -3,19 +3,27 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import java.time.LocalDate; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.Set; import static org.junit.jupiter.api.Assertions.*; class DateTimeUtilsTest { @Test - void getNumDaysLate() { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss a z"); - ZonedDateTime dueDate = ZonedDateTime.parse("2024-03-07 11:59:00 PM -07:00", formatter); + void getNumDaysLateWithoutHolidays() { + // Initialize without holidays + DateTimeUtils dateTimeUtils = new DateTimeUtils(); + dateTimeUtils.initializePublicHolidays(); + + // See images: days-late-without-holidays (1&2) + String dueDateStr = "2024-03-07 11:59:00 PM -07:00"; ExpectedDaysLate[] expectedDaysLate = { // On time submissions + // Notice the timezone testing and edge case testing new ExpectedDaysLate("2024-02-03 02:00:00 PM -07:00", 0), new ExpectedDaysLate("2024-03-03 02:00:00 PM -17:00", 0), new ExpectedDaysLate("2024-03-06 11:59:00 AM -07:00", 0), @@ -58,11 +66,222 @@ void getNumDaysLate() { new ExpectedDaysLate("2024-04-15 02:15:00 PM -07:00", 27), }; + // Validate + validateExpectedDaysLate(dueDateStr, expectedDaysLate, dateTimeUtils); + } + @Test + void getNumDaysLateWithHolidays() { + // Initialize with holidays + DateTimeUtils standardDateTimeUtils = new DateTimeUtils(); + standardDateTimeUtils.initializePublicHolidays(getMultilinePublicHolidaysConfiguration()); + + // See image: days-late-with-holidays-common + String commonDueDate = "2024-03-07 11:59:00 PM -07:00"; + ExpectedDaysLate[] commonExpectedDaysLate = { + // Early & on-time + new ExpectedDaysLate("2024-03-03 02:15:00 PM -07:00", 0), + new ExpectedDaysLate("2024-03-04 02:15:00 PM -07:00", 0), + new ExpectedDaysLate("2024-03-05 02:15:00 PM -07:00", 0), + new ExpectedDaysLate("2024-03-06 02:15:00 PM -07:00", 0), + new ExpectedDaysLate("2024-03-07 02:15:00 PM -07:00", 0), // Due date + + // Late + new ExpectedDaysLate("2024-03-08 02:15:00 PM -07:00", 1), + new ExpectedDaysLate("2024-03-09 02:15:00 PM -07:00", 2), + new ExpectedDaysLate("2024-03-10 02:15:00 PM -07:00", 2), + new ExpectedDaysLate("2024-03-11 02:15:00 PM -07:00", 2), + new ExpectedDaysLate("2024-03-12 02:15:00 PM -07:00", 3), + new ExpectedDaysLate("2024-03-13 02:15:00 PM -07:00", 4), + new ExpectedDaysLate("2024-03-14 02:15:00 PM -07:00", 5), + new ExpectedDaysLate("2024-03-15 02:15:00 PM -07:00", 6), // Holiday + new ExpectedDaysLate("2024-03-16 02:15:00 PM -07:00", 6), + new ExpectedDaysLate("2024-03-17 02:15:00 PM -07:00", 6), + new ExpectedDaysLate("2024-03-18 02:15:00 PM -07:00", 6), + new ExpectedDaysLate("2024-03-19 02:15:00 PM -07:00", 7), + new ExpectedDaysLate("2024-03-20 02:15:00 PM -07:00", 8), + new ExpectedDaysLate("2024-03-21 02:15:00 PM -07:00", 9), + new ExpectedDaysLate("2024-03-22 02:15:00 PM -07:00", 10), + new ExpectedDaysLate("2024-03-23 02:15:00 PM -07:00", 11), + new ExpectedDaysLate("2024-03-24 02:15:00 PM -07:00", 11), + new ExpectedDaysLate("2024-03-25 02:15:00 PM -07:00", 11), + }; + validateExpectedDaysLate(commonDueDate, commonExpectedDaysLate, standardDateTimeUtils); + + // See image: days-late-with-holidays-due-on-holiday + // This edge case is professor approved + String holidayDueDate = "2024-06-19 11:59:00 PM -07:00"; + ExpectedDaysLate[] holidayExpectedDaysLate = { + // Early & on-time + new ExpectedDaysLate("2024-06-16 02:15:00 PM -07:00", 0), + new ExpectedDaysLate("2024-06-17 02:15:00 PM -07:00", 0), + new ExpectedDaysLate("2024-06-18 02:15:00 PM -07:00", 0), + new ExpectedDaysLate("2024-06-19 02:15:00 PM -07:00", 0), // Due date, holiday + + // Edge Case + // If the due date is a holiday, + // then the assignment will be counted on time the following day + new ExpectedDaysLate("2024-06-20 02:15:00 PM -07:00", 0), + + // Late + new ExpectedDaysLate("2024-06-21 02:15:00 PM -07:00", 1), + new ExpectedDaysLate("2024-06-22 02:15:00 PM -07:00", 2), // Weekend + new ExpectedDaysLate("2024-06-23 02:15:00 PM -07:00", 2), // Weekend + new ExpectedDaysLate("2024-06-24 02:15:00 PM -07:00", 2), + new ExpectedDaysLate("2024-06-25 02:15:00 PM -07:00", 3), + new ExpectedDaysLate("2024-06-26 02:15:00 PM -07:00", 4), + new ExpectedDaysLate("2024-06-27 02:15:00 PM -07:00", 5), + new ExpectedDaysLate("2024-06-28 02:15:00 PM -07:00", 6), + new ExpectedDaysLate("2024-06-29 02:15:00 PM -07:00", 7), // Weekend + new ExpectedDaysLate("2024-06-30 02:15:00 PM -07:00", 7), // Weekend + new ExpectedDaysLate("2024-07-01 02:15:00 PM -07:00", 7), + new ExpectedDaysLate("2024-07-02 02:15:00 PM -07:00", 8), + new ExpectedDaysLate("2024-07-03 02:15:00 PM -07:00", 9), + new ExpectedDaysLate("2024-07-04 02:15:00 PM -07:00", 10), // Holiday + new ExpectedDaysLate("2024-07-05 02:15:00 PM -07:00", 10), + new ExpectedDaysLate("2024-07-06 02:15:00 PM -07:00", 11), // Weekend + }; + validateExpectedDaysLate(holidayDueDate, holidayExpectedDaysLate, standardDateTimeUtils); + + + // See image: days-late-with-holidays-friday-holiday-and-consecutive-holidays + DateTimeUtils customDateTimeUtils = new DateTimeUtils(); + customDateTimeUtils.initializePublicHolidays("12/20/2024;12/24/2024;12/25/2024;12/31/2024;1/1/2025"); + String fridayHolidayDueDate = "2024-12-20 11:59:00 PM -07:00"; + ExpectedDaysLate[] fridayHolidayAndConsecutiveHolidays = { + // On Time + new ExpectedDaysLate("2024-12-15 02:15:00 PM -07:00", 0), + new ExpectedDaysLate("2024-12-16 02:15:00 PM -07:00", 0), + new ExpectedDaysLate("2024-12-17 02:15:00 PM -07:00", 0), + new ExpectedDaysLate("2024-12-18 02:15:00 PM -07:00", 0), + new ExpectedDaysLate("2024-12-19 02:15:00 PM -07:00", 0), + new ExpectedDaysLate("2024-12-20 02:15:00 PM -07:00", 0), // Due date, holiday + + // Approved edge case (same as above) + new ExpectedDaysLate("2024-12-21 02:15:00 PM -07:00", 0), // Weekend + new ExpectedDaysLate("2024-12-22 02:15:00 PM -07:00", 0), // Weekend + new ExpectedDaysLate("2024-12-23 02:15:00 PM -07:00", 0), + + // Late + new ExpectedDaysLate("2024-12-24 02:15:00 PM -07:00", 1), // Holiday + new ExpectedDaysLate("2024-12-25 02:15:00 PM -07:00", 1), // Holiday + new ExpectedDaysLate("2024-12-26 02:15:00 PM -07:00", 1), + new ExpectedDaysLate("2024-12-27 02:15:00 PM -07:00", 2), + new ExpectedDaysLate("2024-12-28 02:15:00 PM -07:00", 3), // Weekend + new ExpectedDaysLate("2024-12-29 02:15:00 PM -07:00", 3), // Weekend + new ExpectedDaysLate("2024-12-30 02:15:00 PM -07:00", 3), + new ExpectedDaysLate("2024-12-31 02:15:00 PM -07:00", 4), // Holiday + new ExpectedDaysLate("2025-01-01 02:15:00 PM -07:00", 4), // Holiday + new ExpectedDaysLate("2025-01-02 02:15:00 PM -07:00", 4), + new ExpectedDaysLate("2025-01-03 02:15:00 PM -07:00", 5), + new ExpectedDaysLate("2025-01-04 02:15:00 PM -07:00", 6), // Weekend + new ExpectedDaysLate("2025-01-05 02:15:00 PM -07:00", 6), // Weekend + new ExpectedDaysLate("2025-01-06 02:15:00 PM -07:00", 6), + new ExpectedDaysLate("2025-01-07 02:15:00 PM -07:00", 7), + new ExpectedDaysLate("2025-01-08 02:15:00 PM -07:00", 8), + new ExpectedDaysLate("2025-01-09 02:15:00 PM -07:00", 9), + new ExpectedDaysLate("2025-01-10 02:15:00 PM -07:00", 10), + new ExpectedDaysLate("2025-01-11 02:15:00 PM -07:00", 11), // Weekend + }; + validateExpectedDaysLate(fridayHolidayDueDate, fridayHolidayAndConsecutiveHolidays, customDateTimeUtils); + + + // See image: days-late-with-holidays-holidays-on-weekends + DateTimeUtils customDateTimeUtils2 = new DateTimeUtils(); + customDateTimeUtils2.initializePublicHolidays("09/16/2028;09/17/2028;09/18/2028;"); + String holidaysOnWeekendsDueDate = "2028-09-14 02:15:00 PM -07:00"; + ExpectedDaysLate[] holidaysOnWeekends = { + new ExpectedDaysLate("2028-09-14 02:15:00 PM -07:00", 0), // Due date + new ExpectedDaysLate("2028-09-15 02:15:00 PM -07:00", 1), + new ExpectedDaysLate("2028-09-16 02:15:00 PM -07:00", 2), // Weekend, holiday + new ExpectedDaysLate("2028-09-17 02:15:00 PM -07:00", 2), // Weekend, holiday + new ExpectedDaysLate("2028-09-18 02:15:00 PM -07:00", 2), // Holiday + new ExpectedDaysLate("2028-09-19 02:15:00 PM -07:00", 2), + new ExpectedDaysLate("2028-09-20 02:15:00 PM -07:00", 3), + new ExpectedDaysLate("2028-09-21 02:15:00 PM -07:00", 4), + }; + validateExpectedDaysLate(holidaysOnWeekendsDueDate, holidaysOnWeekends, customDateTimeUtils2); + } + private void validateExpectedDaysLate(String dueDateStr, ExpectedDaysLate[] expectedDaysLate, DateTimeUtils dateTimeUtils) { + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss a z"); + ZonedDateTime dueDate = ZonedDateTime.parse(dueDateStr, formatter); + // Evaluate all the test cases above ZonedDateTime handInTime; for (var expectedResult : expectedDaysLate) { handInTime = ZonedDateTime.parse(expectedResult.handInDate, formatter); - Assertions.assertEquals(expectedResult.daysLate, DateTimeUtils.getNumDaysLate(handInTime, dueDate)); + Assertions.assertEquals(expectedResult.daysLate, dateTimeUtils.getNumDaysLate(handInTime, dueDate)); + } + } + + @Test + void initializePublicHolidaysSingleLine() { + String encodedPublicHolidays = getSinglelinePublicHolidaysConfiguration(); + validateExpectedHolidays(encodedPublicHolidays); + } + private String getSinglelinePublicHolidaysConfiguration() { + return " " // Leading whitespace + + "1/1/2024;1/15/2024;2/19/2024;3/15/2024;4/25/2024;5/27/2024;6/19/2024;" // Delimited with ";" + + "This isn't a date, you silly goose! " // Invalid date should be skipped + + "7/4/2024,7/24/2024,9/2/2024,11/27/2024,11/28/2024,11/29/2024," // Delimited with "," + + "16 sep 2024," // This date shouldn't be accepted since it's in the wrong format + + ";, " // Multiple consecutive delimiters + + "12/24/2024 12/25/2024 12/31/2024 " // Delimited with " " + + "1/1/2025;"; // Has trailing delimiter + } + @Test + void initializePublicHolidaysMultiLine() { + String encodedPublicHolidays = getMultilinePublicHolidaysConfiguration(); + validateExpectedHolidays(encodedPublicHolidays); + } + private String getMultilinePublicHolidaysConfiguration() { + return + """ + 1/1/2024 New Years + 1/15/2024 MLK Jr's day + 2/19/2024 President's Day + 3/15/2024 Spring day + 4/25/2024 Commencement Extra indentation shouldn't mess anything up + 5/27/2024 Memorial day + 6/19/2024 Juneteenth + + This is just a comment. + The following date should not be accepted: 7/1/2024 + 7/4/2024 July 4th + 7/24/2024 Pioneer day + # Comments beginning with '#' should be specifically ignored + + The following dates have comments following a tab character "\t" + 9/2/2024 Labor day + # 9/16/2024 My birthday doesn't count as a holiday + 11/27/2024 No classes + 11/28/2024 Thanksgiving + 11/29/2024 Thanksgiving holiday + 12/24/2024 Christmas Eve + 12/25/2024 Christmas Day + 12/31/2024 New Years Holiday + 1/1/2025 New Years Holiday + """; + } + private void validateExpectedHolidays(String encodedPublicHolidays) { + DateTimeUtils dateTimeUtils = new DateTimeUtils(); + var initializedPublicHolidays = dateTimeUtils.initializePublicHolidays(encodedPublicHolidays); + + Assertions.assertEquals(17, initializedPublicHolidays.size(), + "Set does not have the right number of public holidays"); + + LocalDate[] sampleExpectedHolidays = { + LocalDate.of(2024, 1, 1), + LocalDate.of(2024, 1, 15), + LocalDate.of(2024, 4, 25), + LocalDate.of(2024, 6, 19), + LocalDate.of(2024, 9, 2), + LocalDate.of(2024, 12, 25), + LocalDate.of(2025, 1, 1), + }; + for (var expectedHoliday : sampleExpectedHolidays) { + Assertions.assertTrue(initializedPublicHolidays.contains(expectedHoliday), + "Expected holiday not found in resulting set: " + expectedHoliday.toString()); } } diff --git a/src/test/java/edu/byu/cs/util/days-late-with-holidays-common.jpg b/src/test/java/edu/byu/cs/util/days-late-with-holidays-common.jpg new file mode 100644 index 000000000..c32b04bfd Binary files /dev/null and b/src/test/java/edu/byu/cs/util/days-late-with-holidays-common.jpg differ diff --git a/src/test/java/edu/byu/cs/util/days-late-with-holidays-due-on-holiday.jpg b/src/test/java/edu/byu/cs/util/days-late-with-holidays-due-on-holiday.jpg new file mode 100644 index 000000000..97c4a6ce7 Binary files /dev/null and b/src/test/java/edu/byu/cs/util/days-late-with-holidays-due-on-holiday.jpg differ diff --git a/src/test/java/edu/byu/cs/util/days-late-with-holidays-friday-holiday-and-consecutive-holidays.jpg b/src/test/java/edu/byu/cs/util/days-late-with-holidays-friday-holiday-and-consecutive-holidays.jpg new file mode 100644 index 000000000..3d2531d64 Binary files /dev/null and b/src/test/java/edu/byu/cs/util/days-late-with-holidays-friday-holiday-and-consecutive-holidays.jpg differ diff --git a/src/test/java/edu/byu/cs/util/days-late-with-holidays-holidays-on-weekends.jpg b/src/test/java/edu/byu/cs/util/days-late-with-holidays-holidays-on-weekends.jpg new file mode 100644 index 000000000..40fbd7fc8 Binary files /dev/null and b/src/test/java/edu/byu/cs/util/days-late-with-holidays-holidays-on-weekends.jpg differ diff --git a/src/test/java/edu/byu/cs/util/days-late-without-holidays-1.jpg b/src/test/java/edu/byu/cs/util/days-late-without-holidays-1.jpg new file mode 100644 index 000000000..e453a746d Binary files /dev/null and b/src/test/java/edu/byu/cs/util/days-late-without-holidays-1.jpg differ diff --git a/src/test/java/edu/byu/cs/util/days-late-without-holidays-2.jpg b/src/test/java/edu/byu/cs/util/days-late-without-holidays-2.jpg new file mode 100644 index 000000000..7e2bcfb6a Binary files /dev/null and b/src/test/java/edu/byu/cs/util/days-late-without-holidays-2.jpg differ