-
Notifications
You must be signed in to change notification settings - Fork 0
Optimized scheduling service to maximize optional attendees #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
702eef1
462c5ef
faac3cc
137830a
07709ab
0173049
4ca8972
a46fdd8
ed72b38
951e93b
3b4e919
b9eccf9
88b11de
7a8941b
cce1015
857fd09
bd55190
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,15 +18,21 @@ | |
| import java.util.Arrays; | ||
| import java.util.Collection; | ||
| import java.util.Collections; | ||
| import java.util.stream.Collectors; | ||
| import java.util.Comparator; | ||
| import java.util.HashSet; | ||
| import java.util.HashMap; | ||
| import java.util.Iterator; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
| import java.util.TreeMap; | ||
|
|
||
| /** Lists possible meeting times based on meeting information it takes in. */ | ||
| /** | ||
| * Supports a query function that lists optimal meeting times based on meeting information it takes | ||
| * in. | ||
| */ | ||
| public final class FindMeetingQuery { | ||
| private static final int END_OF_DAY = TimeRange.getTimeInMinutes(23, 59); | ||
|
|
||
| private static final Comparator<Event> ORDER_BY_START_ASC = | ||
| new Comparator<Event>() { | ||
| @Override | ||
|
|
@@ -35,106 +41,208 @@ public int compare(Event a, Event b) { | |
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Returns a list of time periods in which the meeting, specified by request, could happen. If one | ||
| * or more time slots exists so that both mandatory and optional attendees can attend, it returns | ||
| * those time slots. Otherwise, it returns the time slots that fit just the mandatory attendees. | ||
| * | ||
| * @param eventsCollection the events we know about | ||
| * @param request information about the meeting, including attendees, optional attendees, and how | ||
| * long it needs to be | ||
| */ | ||
| public Collection<TimeRange> query(Collection<Event> eventsCollection, MeetingRequest request) { | ||
| Collection<TimeRange> withOptionalAttendees = getMeetingTimes(eventsCollection, request, true); | ||
|
|
||
| // Special case: if no mandatory attendees and optional attendees' schedules cannot fit in a | ||
| // meeting, no meeting times are possible. | ||
| return !withOptionalAttendees.isEmpty() || request.getAttendees().isEmpty() | ||
| ? withOptionalAttendees | ||
| : getMeetingTimes(eventsCollection, request, false); | ||
| } | ||
| // All logic needed to find optimal meeting times with one instance of meeting information and | ||
| // events to be considered. | ||
| private static class SingleMeetingResolver { | ||
| // events are all the events to be considered. | ||
| private final Collection<Event> events; | ||
|
|
||
| private Collection<TimeRange> getMeetingTimes( | ||
| Collection<Event> eventsCollection, | ||
| MeetingRequest request, | ||
| boolean includeOptionalAttendees) { | ||
| HashSet<String> attendees = new HashSet<String>(request.getAttendees()); | ||
| if (includeOptionalAttendees) { | ||
| attendees.addAll(request.getOptionalAttendees()); | ||
| /* request contains information about the meeting, including attendees, optional attendees, and how long it needs to be. | ||
| */ private final MeetingRequest request; | ||
| private ArrayList<TimeRange> optimalMeetingTimes = new ArrayList<TimeRange>(); | ||
| private long minMeetingDuration; | ||
|
|
||
| SingleMeetingResolver(Collection<Event> events, MeetingRequest request) { | ||
| this.events = events; | ||
| this.request = request; | ||
| this.minMeetingDuration = request.getDuration(); | ||
| } | ||
| ArrayList<Event> events = getRelevantEvents(attendees, new ArrayList<Event>(eventsCollection)); | ||
| List<TimeRange> possibleMeetingTimes = new ArrayList<TimeRange>(); | ||
|
|
||
| // Need to check this so we don't access out of bounds when we add first gap. | ||
| if (events.isEmpty()) { | ||
| addIfLongEnough( | ||
| TimeRange.fromStartEnd(0, END_OF_DAY, true), possibleMeetingTimes, request.getDuration()); | ||
| return possibleMeetingTimes; | ||
| /** | ||
| * Returns a list of time periods in which the meeting, specified by request, could happen. If | ||
| * no time exists for all optional and mandatory attendees, find the time slot(s) that allow | ||
| * mandatory attendees and the greatest possible number of optional attendees to attend. | ||
| */ | ||
| Collection<TimeRange> resolveBestTime() { | ||
| ArrayList<TimeRange> mandatoryAttendeesMeetingTimes = | ||
| getMandatoryAttendeesMeetingTimes(new HashSet<String>(request.getAttendees())); | ||
| getOptimalMeetingTimes( | ||
| mandatoryAttendeesMeetingTimes, | ||
| getChangesInOptionalAttendeesAttendance( | ||
| getOptionalAttendeesFreeTimes(new HashSet<String>(request.getOptionalAttendees())))); | ||
|
|
||
| // If there are no meeting times with at least one optional attendee, just return | ||
| // mandatoryAttendeesMeetingTimes. | ||
| return optimalMeetingTimes.isEmpty() ? mandatoryAttendeesMeetingTimes : optimalMeetingTimes; | ||
| } | ||
| Collections.sort(events, ORDER_BY_START_ASC); | ||
|
|
||
| // Add first gap. | ||
| addIfLongEnough( | ||
| TimeRange.fromStartEnd(0, events.get(0).getWhen().start(), false), | ||
| possibleMeetingTimes, | ||
| request.getDuration()); | ||
| int end = events.get(0).getWhen().end(); | ||
| for (Event event : events) { | ||
| // event can be merged with current time range | ||
| if (event.getWhen().start() <= end) { | ||
| end = Math.max(end, event.getWhen().end()); | ||
| continue; | ||
|
|
||
| /** | ||
| * Returns all meeting times that allow the most optional attendees to attend. These times must | ||
| * also fall in a time satisfying all mandatory attendees and must be at least | ||
| * minMeetingDuration long. Utilizes two pointers: one to mandatoryAttendeesMeetingTimes, one to | ||
| * changeLog. | ||
| * | ||
| * @param mandatoryAttendeesMeetingTimes all meeting times satisfying mandatory attendees | ||
| * @param changes a change log of the number of available optional attendees over time | ||
| */ | ||
| private void getOptimalMeetingTimes( | ||
| ArrayList<TimeRange> mandatoryAttendeesMeetingTimes, TreeMap<Integer, Integer> changes) { | ||
| int mandatoryAttendeesMeetingTimesIndex = 0, | ||
| prevTime = 0, | ||
| bestAttendance = 0, | ||
| currAttendance = 0; | ||
| for (Map.Entry changeEntry : changes.entrySet()) { | ||
| // First need to back up mandatoryAttendeesMeetingTimesIndex in case we missed a time range | ||
| // in mandatoryAttendeesMeetingTimes. | ||
| mandatoryAttendeesMeetingTimesIndex = Math.max(0, mandatoryAttendeesMeetingTimesIndex - 1); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not the clearest way to do this. From what I can tell, you're intending to iterate over mandatoryAttendeesMeetingTimes only once, and you iterate over it over the course of iterating through changes. This "backtracking" is necessary because of the way you're iterating on the index variable. The pattern I'd use for this is to (1) retain an iterator for mandatoryAttendeesMeetingTimes and (2) retain the "next" meeting time to be processed. Upon successful processing, get the A skeleton of this: Iterator<TimeRange> mandatoryMeetingTimesIter = mandatoryAttendeesMeetingTimes.iterator();
Optional<TimeRange> nextMandatoryMeetingTimeOpt =
mandatoryMeetingTimesIter.hasNext() ? Optional.of(mandatoryMeetingTimesIter.next()) : Optional.empty();
for (Map.Entry<String,String> changeEntry : changes.entrySet()) {
while (nextMandatoryMeetingTimeOpt.isPresent()) {
TimeRange thisMandatoryMeetingTime = nextMandatoryMeetingTImeOpt.get();
// TODO: Explain what this check is doing
if (thisMandatoryMeetingTime.start() < changeEntry.getKey()) {
break;
}
updateOptimalTimes(
TimeRange.fromStartEnd(
Math.max(thisMandatoryMeetingTime.start(), prevTime),
Math.min(thisMandatoryMeetingTime.end(),
(Integer) changeEntry.getKey()),
false));
// Queue up the next meeting time.
nextMandatoryMeetingTimeOpt = mandatoryMeetingTimesIter.hasNext() ?
Optional.of(mandatoryMeetingTimesIter.next()) : Optional.empty();
}
prevTime = (Integer) changeEntry.getKey();
currAttendance += (Integer) changeEntry.getValue();
}
// TODO: What does it mean if nextMandatoryMeetingTimeOpt.isPresent() (still)? Handle this
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
l could accomplish this decrement by keeping a
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Acknowledged, though I don't see this check in the new code, so I'm really not sure what's going on.
This could be interpreted as "why doesn't your code do this" or "my code doesn't actually do this". I'll try to answer both, but I just wanted to give you this caveat that i didn't really actually understand your comment. The solution I described doesn't require mandatoryMeetingTimesIter to "go back one", because it is not being incremented unless the value is consumed. This is the purpose behind storing "the next value", and incrementing that immediately after the call to updateOptimalTimes. This more closely matches the semantics you want, rather than the for loop construction, which always increments the value when the value (that it is an index to) doesn't always get consumed. A
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry for the confusion.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nvm, iterators are annoyingly verbose. l'll take you up on the |
||
|
|
||
| // Then compare time range from previous time in changeLog to current time in changeLog | ||
| // with mandatoryAttendeesMeetingTimes that overlap with this time range. | ||
| while (mandatoryAttendeesMeetingTimesIndex < mandatoryAttendeesMeetingTimes.size() | ||
| && mandatoryAttendeesMeetingTimes.get(mandatoryAttendeesMeetingTimesIndex).start() | ||
| < (int) changeEntry.getKey()) { | ||
| TimeRange meetingRange = | ||
| TimeRange.fromStartEnd( | ||
| Math.max( | ||
| mandatoryAttendeesMeetingTimes | ||
| .get(mandatoryAttendeesMeetingTimesIndex) | ||
| .start(), | ||
| prevTime), | ||
| Math.min( | ||
| mandatoryAttendeesMeetingTimes.get(mandatoryAttendeesMeetingTimesIndex).end(), | ||
| (int) changeEntry.getKey()), | ||
| false); | ||
| mandatoryAttendeesMeetingTimesIndex++; | ||
| if (meetingRange.duration() < minMeetingDuration) { | ||
| continue; | ||
| } | ||
|
|
||
| // Clear out all former optimal meeting times. They aren't the most optimal anymore. | ||
| if (currAttendance > bestAttendance) { | ||
| bestAttendance = currAttendance; | ||
| optimalMeetingTimes.clear(); | ||
| } | ||
| if (currAttendance == bestAttendance) { | ||
| optimalMeetingTimes.add(meetingRange); | ||
| } | ||
| } | ||
| prevTime = (int) changeEntry.getKey(); | ||
| currAttendance += (int) changeEntry.getValue(); | ||
| } | ||
| // Add the time range we were tracking, start a new one from event. | ||
| addIfLongEnough( | ||
| TimeRange.fromStartEnd(end, event.getWhen().start(), false), | ||
| possibleMeetingTimes, | ||
| request.getDuration()); | ||
| end = event.getWhen().end(); | ||
| } | ||
|
|
||
| // Add the last one we were tracking. | ||
| addIfLongEnough( | ||
| TimeRange.fromStartEnd(end, END_OF_DAY, true), possibleMeetingTimes, request.getDuration()); | ||
| return possibleMeetingTimes; | ||
| } | ||
| /** | ||
| * Returns a mapping of optional attendees to their free times. | ||
| * | ||
| * @param optionalAttendees everyone who needs to attend this meeting | ||
| */ | ||
| private HashMap<String, ArrayList<TimeRange>> getOptionalAttendeesFreeTimes( | ||
| HashSet<String> optionalAttendees) { | ||
| HashMap<String, ArrayList<TimeRange>> times = new HashMap<String, ArrayList<TimeRange>>(); | ||
| for (String attendee : optionalAttendees) { | ||
| HashSet<String> attendeeSet = new HashSet<String>(); | ||
| attendeeSet.add(attendee); | ||
|
|
||
| /** | ||
| * Adds range to ranges if it is long enough to fit in a meeting. | ||
| * | ||
| * @param range the range being considered | ||
| * @param ranges the list of ranges >= meetingDuration | ||
| * @param meetingDuration the duration of meeting to be scheduled | ||
| */ | ||
| private static void addIfLongEnough( | ||
| TimeRange range, List<TimeRange> ranges, long meetingDuration) { | ||
| if (range.duration() >= meetingDuration) { | ||
| ranges.add(range); | ||
| // Find all possible meeting times for just this one attendee. Must do this to deal with | ||
| // double bookings. | ||
| times.put(attendee, getMandatoryAttendeesMeetingTimes(attendeeSet)); | ||
| } | ||
| return times; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns only those events that are attended by at least one attendee that is attending the | ||
| * meeting we are trying to schedule. More intuitively, an event is "relevant" if it is attended | ||
| * by at least one "relevant" attendee. | ||
| * | ||
| * @param requestAttendees the set of attendees attending the meeting ("relevant" people) | ||
| */ | ||
| private static ArrayList<Event> getRelevantEvents( | ||
| HashSet<String> relevantAttendees, ArrayList<Event> events) { | ||
| ArrayList<Event> relevantEvents = new ArrayList<Event>(); | ||
| for (Event event : events) { | ||
| boolean isRelevant = false; | ||
| for (String person : event.getAttendees()) { | ||
| if (relevantAttendees.contains(person)) { | ||
| isRelevant = true; | ||
| break; | ||
| /** | ||
| * Returns a change log of the number of available optional attendees over time. First part of a | ||
| * sweep-line algorithm. | ||
| * | ||
| * @param optionalAttendeesFreeTimes mapping of all attendees to their free times | ||
| */ | ||
| private TreeMap<Integer, Integer> getChangesInOptionalAttendeesAttendance( | ||
| HashMap<String, ArrayList<TimeRange>> optionalAttendeesFreeTimes) { | ||
| TreeMap<Integer, Integer> changes = new TreeMap<Integer, Integer>(); | ||
| for (Map.Entry e : optionalAttendeesFreeTimes.entrySet()) { | ||
| for (TimeRange time : (ArrayList<TimeRange>) e.getValue()) { | ||
| changes.put(time.start(), changes.getOrDefault(time.start(), 0) + 1); | ||
| changes.put(time.end(), changes.getOrDefault(time.end(), 0) - 1); | ||
| } | ||
| } | ||
| if (isRelevant) { | ||
| relevantEvents.add(event); | ||
| return changes; | ||
| } | ||
|
|
||
| /** | ||
| * Returns all meeting times that satisfy all mandatory attendees of request. Sorted in | ||
| * ascending order. | ||
| * | ||
| * @param mandatoryAttendees everyone who needs to attend this meeting | ||
| */ | ||
| private ArrayList<TimeRange> getMandatoryAttendeesMeetingTimes( | ||
| HashSet<String> mandatoryAttendees) { | ||
| ArrayList<Event> relevantEvents = getRelevantEvents(mandatoryAttendees); | ||
| ArrayList<TimeRange> possibleMeetingTimes = new ArrayList<TimeRange>(); | ||
|
|
||
| // Need to check this so we don't access out of bounds when we add first gap. | ||
| if (relevantEvents.isEmpty()) { | ||
| addIfLongEnough( | ||
| TimeRange.fromStartEnd(0, TimeRange.END_OF_DAY, true), possibleMeetingTimes); | ||
| return possibleMeetingTimes; | ||
| } | ||
| Collections.sort(relevantEvents, ORDER_BY_START_ASC); | ||
|
|
||
| // Add first gap. | ||
| addIfLongEnough( | ||
| TimeRange.fromStartEnd(0, relevantEvents.get(0).getWhen().start(), false), | ||
| possibleMeetingTimes); | ||
| int end = relevantEvents.get(0).getWhen().end(); | ||
| for (Event event : relevantEvents) { | ||
| // event can be merged with current time range | ||
| if (event.getWhen().start() <= end) { | ||
| end = Math.max(end, event.getWhen().end()); | ||
| continue; | ||
| } | ||
| // Add the time range we were tracking, start a new one from event. | ||
| addIfLongEnough( | ||
| TimeRange.fromStartEnd(end, event.getWhen().start(), false), possibleMeetingTimes); | ||
| end = event.getWhen().end(); | ||
| } | ||
|
|
||
| // Add the last one we were tracking. | ||
| addIfLongEnough( | ||
| TimeRange.fromStartEnd(end, TimeRange.END_OF_DAY, true), possibleMeetingTimes); | ||
| return possibleMeetingTimes; | ||
| } | ||
| return relevantEvents; | ||
|
|
||
| /** | ||
| * Adds range to ranges if it is long enough to fit in a meeting. | ||
| * | ||
| * @param range the range being considered | ||
| * @param ranges the list of ranges >= meetingDuration | ||
| */ | ||
| private void addIfLongEnough(TimeRange range, List<TimeRange> ranges) { | ||
| if (range.duration() >= minMeetingDuration) { | ||
| ranges.add(range); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns only those events that are attended by at least one attendee that is attending the | ||
| * meeting we are trying to schedule. More intuitively, an event is "relevant" if it is attended | ||
| * by at least one "relevant" attendee. | ||
| * | ||
| * @param relevantAttendees the set of attendees attending the meeting ("relevant" people) | ||
| */ | ||
| private ArrayList<Event> getRelevantEvents(HashSet<String> relevantAttendees) { | ||
| return new ArrayList<Event>( | ||
| events.stream() | ||
| .filter( | ||
| e -> | ||
| e.getAttendees().stream() | ||
| .filter(relevantAttendees::contains) | ||
| .findAny() | ||
| .isPresent()) | ||
| .collect(Collectors.toList())); | ||
| } | ||
| } | ||
|
|
||
| public Collection<TimeRange> query(Collection<Event> events, MeetingRequest request) { | ||
| return new SingleMeetingResolver(events, request).resolveBestTime(); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.