Skip to content

Commit

Permalink
Merge pull request #219 from softwareconstruction240/172-holidays-and…
Browse files Browse the repository at this point in the history
…-weekends-dont-extend-late-calculation

172 holidays and weekends dont extend late calculation
  • Loading branch information
pawlh authored Mar 13, 2024
2 parents 68b05a7 + edb79e5 commit e1c4754
Show file tree
Hide file tree
Showing 9 changed files with 405 additions and 17 deletions.
17 changes: 16 additions & 1 deletion src/main/java/edu/byu/cs/autograder/Grader.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ public abstract class Grader implements Runnable {

protected Observer observer;

private DateTimeUtils dateTimeUtils;

/**
* Creates a new grader
*
Expand All @@ -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() {
Expand Down Expand Up @@ -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) {
Expand Down
178 changes: 166 additions & 12 deletions src/main/java/edu/byu/cs/util/DateTimeUtils.java
Original file line number Diff line number Diff line change
@@ -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<LocalDate> publicHolidays;

/**
* Generates a String representation of a timestamp
Expand All @@ -16,21 +28,31 @@ 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);
return zonedDateTime.format(formatter);
}

/**
* 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)) {
Expand All @@ -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<LocalDate> 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<LocalDate> 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;
}

/**
* <h1>Public Holiday Decoding</h1>
* Interprets a string of encoded public holidays and returns a set of objects that
* can be used to efficiently query against the dataset.
* <br>
*
* <h2>Encoding Approaches</h2>
* 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.
* <br>
*
* <h3>Single Line Encoding</h3>
* Dates can be stored on a single line, separated by any combination of the following:
* <ul>
* <li>spaces " ", or</li>
* <li>commas ",", or </li>
* <li>semicolon ";" </li>
* </ul>
* <br>
*
* Each word will be interpreted individually, and any that do not parse into a date properly
* will be excluded individually.
* <br>
*
* <h3>Multi-line Encoding</h3>
* 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.
* <br>
*
* <h2>Encoding Date Formats</h2>
* 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.
* <br>
*
* @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<Date>} that will be used to efficiently comparing against this dataset
*/
private Set<LocalDate> 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<String> 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<LocalDate> parsePublicHolidayStrings(@NonNull String dateFormat, String[] holidayDateStrings) {
Set<LocalDate> 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<Date> 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);
}
}
Loading

0 comments on commit e1c4754

Please sign in to comment.