import java.time.DayOfWeek; import java.time.LocalDate; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; import java.util.stream.Collectors; /** * Represents an event that occurs on a specific date or a range of dates. *

* The event has a time interval and a description. *

* The event can be recurring on specific days of the week. * * @author Yuri Tatishchev * @version 0.1 2025-02-08 */ public class Event implements Comparable { private final LocalDate startDate; private final LocalDate endDate; private final TimeInterval timeInterval; private final String name; private final SortedSet recurs; private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("M/d/yyyy"); private static final DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("H:mm"); private static final Map charToDayOfWeek = Map.of( 'M', DayOfWeek.MONDAY, 'T', DayOfWeek.TUESDAY, 'W', DayOfWeek.WEDNESDAY, 'R', DayOfWeek.THURSDAY, 'F', DayOfWeek.FRIDAY, 'S', DayOfWeek.SATURDAY, 'U', DayOfWeek.SUNDAY ); private static final Map dayOfWeekToChar = charToDayOfWeek.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); /** * Constructs a one-time event with the specified name, date, and time interval. */ public Event(String name, LocalDate date, TimeInterval timeInterval) { this.name = name; this.startDate = date; this.endDate = date; this.timeInterval = timeInterval; this.recurs = null; } /** * Constructs a recurring event with the specified name, start date, time interval, and recurring days. * * @throws IllegalArgumentException if the start date is after the end date */ public Event(String name, LocalDate startDate, LocalDate endDate, TimeInterval timeInterval, SortedSet recurs) { if (startDate.isAfter(endDate)) { throw new IllegalArgumentException("Start date must be before or equal to end date"); } this.name = name; this.startDate = startDate; this.endDate = endDate; this.timeInterval = timeInterval; this.recurs = recurs; } public LocalDate getStartDate() { return startDate; } public LocalDate getEndDate() { return endDate; } public TimeInterval getTimeInterval() { return timeInterval; } public String getName() { return name; } public boolean isRecurring() { return recurs != null; } public SortedSet getRecurs() { return recurs; } /** * Checks if the event occurs on a specific date. * * @param date the date to check * @return true if the event occurs on the date, false otherwise */ public boolean occursOnDate(LocalDate date) { if (date.isBefore(startDate) || date.isAfter(endDate)) { return false; } if (recurs == null) { return true; } for (DayOfWeek day : recurs) { if (date.getDayOfWeek() == day) { return true; } } return false; } /** * Checks if this event overlaps with another event. *

* Two events overlap if their dates overlap and their time intervals overlap. * * @param other the other event * @return true if the events overlap, false otherwise */ public boolean overlapsWith(Event other) { // Dates do not overlap if (this.endDate.isBefore(other.startDate) || this.startDate.isAfter(other.endDate)) { return false; // Dates overlap, but time intervals do not } else if (!this.timeInterval.overlapsWith(other.timeInterval)) { return false; // Dates and time intervals overlap, but possibly not on the same day } else if (this.isRecurring()) { // this is recurring, but other is not if (!other.isRecurring()) { return this.recurs.contains(other.getStartDate().getDayOfWeek()); } // Both are recurring, check if any of the recurring days overlap // TODO: check if this is correct. // I suspect there are edge cases where the days that overlap are not in the overlapping date range. return this.recurs.stream().anyMatch(other.recurs::contains); // other is recurring, but this is not } else if (other.isRecurring()) { return other.recurs.contains(this.startDate.getDayOfWeek()); } return true; } public int compareTo(Event other) { if (this.startDate.isBefore(other.startDate)) { return -1; } else if (this.startDate.isAfter(other.startDate)) { return 1; } else { return this.timeInterval.getStart().compareTo(other.timeInterval.getStart()); } } /** * Returns a string representation of the event. * * @return a string representation of the event *

* Example output:
*
One event name *
1/23/2025 9:00 10:15 *
*
Recurring event name *
TR 10:30 11:45 1/27/2025 5/12/2025 */ public String toString() { return String.format("%s\n%s\n", name, scheduleString()); } /** * Returns a string representation of the event's schedule. * * @return a string representation of the event's schedule *

* Example output:
*
1/23/2025 9:00 10:15 *
TR 10:30 11:45 1/27/2025 5/12/2025 */ public String scheduleString() { if (recurs == null) return String.format("%s %s", dateFormat.format(startDate), timeInterval); String recursString = recurs.stream().map(dayOfWeekToChar::get).map(String::valueOf).collect(Collectors.joining()); return String.format("%s %s %s %s", recursString, timeInterval, dateFormat.format(startDate), dateFormat.format(endDate)); } /** * Parses a schedule string and returns an event. * * @param name the name of the event * @param schedule the schedule string * @return an event *

* Example schedule strings: *
TR 9:00 10:15 1/23/2025 5/8/2025 *
MW 10:30 11:45 1/27/2025 5/12/2025 *
4/18/2025 9:30 11:30 *
6/3/2025 16:15 17:00 *
F 18:30 20:30 1/24/2025 5/9/2025 */ public static Event fromScheduleString(String name, String schedule) { String[] parts = schedule.split("\\s+"); if (parts.length == 3) { LocalDate date = LocalDate.parse(parts[0], dateFormat); LocalTime start = LocalTime.parse(parts[1], timeFormat); LocalTime end = LocalTime.parse(parts[2], timeFormat); TimeInterval timeInterval = new TimeInterval(start, end); return new Event(name, date, timeInterval); } else { LocalTime start = LocalTime.parse(parts[1], timeFormat); LocalTime end = LocalTime.parse(parts[2], timeFormat); LocalDate startDate = LocalDate.parse(parts[3], dateFormat); LocalDate endDate = LocalDate.parse(parts[4], dateFormat); TimeInterval timeInterval = new TimeInterval(start, end); SortedSet recurs = parts[0].chars().mapToObj(c -> charToDayOfWeek.get((char) c)).collect(Collectors.toCollection(TreeSet::new)); return new Event(name, startDate, endDate, timeInterval, recurs); } } }