diff --git a/walkthroughs/week-5-tdd/project/pom.xml b/walkthroughs/week-5-tdd/project/pom.xml index e4d5dce..8bb3582 100644 --- a/walkthroughs/week-5-tdd/project/pom.xml +++ b/walkthroughs/week-5-tdd/project/pom.xml @@ -30,6 +30,11 @@ 2.8.6 + + com.google.collections + google-collections + 1.0 + junit diff --git a/walkthroughs/week-5-tdd/project/src/main/java/com/google/sps/FindMeetingQuery.java b/walkthroughs/week-5-tdd/project/src/main/java/com/google/sps/FindMeetingQuery.java index 16ce36e..8b4d212 100644 --- a/walkthroughs/week-5-tdd/project/src/main/java/com/google/sps/FindMeetingQuery.java +++ b/walkthroughs/week-5-tdd/project/src/main/java/com/google/sps/FindMeetingQuery.java @@ -14,6 +14,7 @@ package com.google.sps; +import com.google.common.collect.Sets; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -23,6 +24,9 @@ import java.util.List; public final class FindMeetingQuery { + // Returns available meetings for day given events and request. query will return a list of TimeRanges including optional attendees + // if there is availability with optional attendees, and will otherwise return a list of TimeRanges for only required attendees. Returns + // an empty List object if there is no availability. public Collection query(Collection events, MeetingRequest request) { // In this case, long to int conversion is safe because duration can never exceed 2^32. Leaving duration as long // leads to compile errors. @@ -34,23 +38,73 @@ public Collection query(Collection events, MeetingRequest requ } // Seperate relevant TimeRanges from events, put into ArrayList and sort by ascending meeting start time + // Optional attendees are handled by running two sets of queries in parallel: a query if we are including optional attendees, + // and a query if we are only considering required attendees. If the optional attendee query is empty, then that means that + // there are no valid times if we include optional attendees, and optional attendees are ignored. List attendedMeetings = new ArrayList<>(); + List allAttendedMeetings = new ArrayList<>(); + + // Attendees is only required attendees, allAttendees includes optional attendees + Set requiredAttendees = new HashSet<>(request.getAttendees()); + Set optionalAttendees = new HashSet<>(request.getOptionalAttendees()); + for (Event event : events) { // First check if the event in question contains people from request, add those meetings to attendedMeetings - Set attendees = new HashSet<>(request.getAttendees()); - attendees.retainAll(event.getAttendees()); + Set allAttendees = new HashSet<>(Sets.union(requiredAttendees, optionalAttendees)); + + requiredAttendees.retainAll(event.getAttendees()); + allAttendees.retainAll(event.getAttendees()); - if (!attendees.isEmpty()){ + if (!requiredAttendees.isEmpty()){ attendedMeetings.add(event.getWhen()); } + + if (!allAttendees.isEmpty()) { + allAttendedMeetings.add(event.getWhen()); + } + } // Sort attendedMeetings so that we can filter out all nested meetings in next step Collections.sort(attendedMeetings, TimeRange.ORDER_BY_START); + Collections.sort(allAttendedMeetings, TimeRange.ORDER_BY_START); + + // All openings represents the availabilities WITH optional attendees, requiredOpenings represents only required attendees. + // if allOpenings is empty, that means that there is a conflict with optional attendees and optional attendees should be ignored. + List requiredOpenings = new ArrayList<>(findOpenings(attendedMeetings, duration)); + List allOpenings = new ArrayList<>(findOpenings(allAttendedMeetings, duration)); + + // As-is, the user will not be aware whether this function is returning allOpenings or requiredOpenings. As-is, the function + // simply returns whichever is available. + return allOpenings.size() == 0 ? requiredOpenings : allOpenings; + } + + // Takes in sorted list of meetings and returns next available TimeRange given, or null if none available with current startTime + private TimeRange findNextTime(List meetings, int startMeetingIndex, int duration) { + int startTime = meetings.get(startMeetingIndex).end(); - List validMeetings = new ArrayList<>(); + // If this is the last meeting in the list and there's a meeting time that fits, return a meeting time. + if (startMeetingIndex == meetings.size() - 1) { + return (startTime + duration <= TimeRange.END_OF_DAY) ? + TimeRange.fromStartEnd(startTime, TimeRange.END_OF_DAY, true) + : null; + } - for (TimeRange meeting : attendedMeetings) { + // If next meeting's start time is farther away than duration, + // return a TimeRange from startTime -> the start of the next meeting. + if (meetings.get(startMeetingIndex + 1).start() - startTime >= duration) { + return TimeRange.fromStartEnd(startTime, meetings.get(startMeetingIndex + 1).start(), false); + } + else { + return null; + } + } + + // Takes in sorted list of TimeRanges, filters out nested meetings, and then passes into findNextTime + private List findOpenings(List meetings, int duration) { + List validMeetings = new ArrayList<>(); + + for (TimeRange meeting : meetings) { int numMeetings = validMeetings.size(); @@ -85,25 +139,4 @@ public Collection query(Collection events, MeetingRequest requ return openings; } - - // Takes in sorted list of meetings and returns next available TimeRange given, or null if none available with current startTime - private TimeRange findNextTime(List meetings, int startMeetingIndex, int duration) { - int startTime = meetings.get(startMeetingIndex).end(); - - // If this is the last meeting in the list and there's a meeting time that fits, return a meeting time. - if (startMeetingIndex == meetings.size() - 1) { - return (startTime + duration <= TimeRange.END_OF_DAY) ? - TimeRange.fromStartEnd(startTime, TimeRange.END_OF_DAY, true) - : null; - } - - /* If next meeting's start time is farther away than duration, - * return a TimeRange from startTime -> the start of the next meeting. */ - if (meetings.get(startMeetingIndex + 1).start() - startTime >= duration) { - return TimeRange.fromStartEnd(startTime, meetings.get(startMeetingIndex + 1).start(), false); - } - else { - return null; - } - } } diff --git a/walkthroughs/week-5-tdd/project/src/test/java/com/google/sps/FindMeetingQueryTest.java b/walkthroughs/week-5-tdd/project/src/test/java/com/google/sps/FindMeetingQueryTest.java index bea1890..9668f5a 100644 --- a/walkthroughs/week-5-tdd/project/src/test/java/com/google/sps/FindMeetingQueryTest.java +++ b/walkthroughs/week-5-tdd/project/src/test/java/com/google/sps/FindMeetingQueryTest.java @@ -34,10 +34,12 @@ public final class FindMeetingQueryTest { // Some people that we can use in our tests. private static final String PERSON_A = "Person A"; private static final String PERSON_B = "Person B"; + private static final String PERSON_C = "Person C"; // All dates are the first day of the year 2020. private static final int TIME_0800AM = TimeRange.getTimeInMinutes(8, 0); private static final int TIME_0830AM = TimeRange.getTimeInMinutes(8, 30); + private static final int TIME_0845AM = TimeRange.getTimeInMinutes(8, 45); private static final int TIME_0900AM = TimeRange.getTimeInMinutes(9, 0); private static final int TIME_0930AM = TimeRange.getTimeInMinutes(9, 30); private static final int TIME_1000AM = TimeRange.getTimeInMinutes(10, 0); @@ -270,4 +272,144 @@ public void notEnoughRoom() { Assert.assertEquals(expected, actual); } + + @Test + public void optionalAttendeeNotAvailable() { + // Have each person have different events. We should see two options because each person has + // split the restricted times. Optional attendee C will not be considered because they are never + // available. + // + // Events : |--A--| |--B--| + // |-------------C---------------| + // Day : |-----------------------------| + // Options : |--1--| |--2--| |--3--| + + Collection events = Arrays.asList( + new Event("Event 1", TimeRange.fromStartDuration(TIME_0800AM, DURATION_30_MINUTES), + Arrays.asList(PERSON_A)), + new Event("Event 2", TimeRange.fromStartDuration(TIME_0900AM, DURATION_30_MINUTES), + Arrays.asList(PERSON_B)), + new Event("Event 3", TimeRange.fromStartEnd(TimeRange.START_OF_DAY, TimeRange.END_OF_DAY, true), + Arrays.asList(PERSON_C))); + + MeetingRequest request = + new MeetingRequest(Arrays.asList(PERSON_A, PERSON_B), DURATION_30_MINUTES); + + request.addOptionalAttendee(PERSON_C); + + Collection actual = query.query(events, request); + Collection expected = + Arrays.asList(TimeRange.fromStartEnd(TimeRange.START_OF_DAY, TIME_0800AM, false), + TimeRange.fromStartEnd(TIME_0830AM, TIME_0900AM, false), + TimeRange.fromStartEnd(TIME_0930AM, TimeRange.END_OF_DAY, true)); + + Assert.assertEquals(expected, actual); + } + + @Test + public void optionalAttendeeIncluded() { + // Have each person have different events. We should see two options because each person has + // split the restricted times. Optional attendee Person C is available for some timeslots, but not all. + // + // Events : |--A--| |--B--| + // |--C--| + // Day : |-----------------------------| + // Options : |--1--| |--2--| + + Collection events = Arrays.asList( + new Event("Event 1", TimeRange.fromStartDuration(TIME_0800AM, DURATION_30_MINUTES), + Arrays.asList(PERSON_A)), + new Event("Event 2", TimeRange.fromStartDuration(TIME_0900AM, DURATION_30_MINUTES), + Arrays.asList(PERSON_B)), + new Event("Event 3", TimeRange.fromStartDuration(TIME_0830AM, DURATION_30_MINUTES), + Arrays.asList(PERSON_C))); + + MeetingRequest request = + new MeetingRequest(Arrays.asList(PERSON_A, PERSON_B), DURATION_30_MINUTES); + request.addOptionalAttendee(PERSON_C); + + Collection actual = query.query(events, request); + Collection expected = + Arrays.asList(TimeRange.fromStartEnd(TimeRange.START_OF_DAY, TIME_0800AM, false), + TimeRange.fromStartEnd(TIME_0930AM, TimeRange.END_OF_DAY, true)); + + Assert.assertEquals(expected, actual); + } + + @Test + public void justEnoughRoomIgnoreOptional() { + // Have one person, but make it so that there is just enough room at one point in the day to + // have the meeting if you ignore optional attendee B. + // + // Events : |--A--| |----A----| + // |-B| + // Day : |---------------------| + // Options : |-----| + + Collection events = Arrays.asList( + new Event("Event 1", TimeRange.fromStartEnd(TimeRange.START_OF_DAY, TIME_0830AM, false), + Arrays.asList(PERSON_A)), + new Event("Event 2", TimeRange.fromStartEnd(TIME_0900AM, TimeRange.END_OF_DAY, true), + Arrays.asList(PERSON_A)), + new Event("Event 3", TimeRange.fromStartEnd(TIME_0830AM, TIME_0845AM, false), + Arrays.asList(PERSON_B))); + + MeetingRequest request = new MeetingRequest(Arrays.asList(PERSON_A), DURATION_30_MINUTES); + request.addOptionalAttendee(PERSON_B); + + Collection actual = query.query(events, request); + Collection expected = + Arrays.asList(TimeRange.fromStartDuration(TIME_0830AM, DURATION_30_MINUTES)); + + Assert.assertEquals(expected, actual); + } + + public void onlyOptionalWithTime() { + // Two optional attendees with gaps in their schedules. + // + // Events : |--A--| |--B--| + // Day : |-----------------------------| + // Options : |--1--| |--2--| |--3--| + + Collection events = Arrays.asList( + new Event("Event 1", TimeRange.fromStartEnd(TIME_0800AM, TIME_0900AM, false), + Arrays.asList(PERSON_A)), + new Event("Event 2", TimeRange.fromStartEnd(TIME_1000AM, TIME_1100AM, false), + Arrays.asList(PERSON_B))); + + MeetingRequest request = new MeetingRequest(Arrays.asList(), DURATION_30_MINUTES); + request.addOptionalAttendee(PERSON_A); + request.addOptionalAttendee(PERSON_B); + + Collection actual = query.query(events, request); + Collection expected = + Arrays.asList(TimeRange.fromStartEnd(TimeRange.START_OF_DAY, TIME_0800AM, false), + TimeRange.fromStartEnd(TIME_0900AM, TIME_1000AM, false), + TimeRange.fromStartEnd(TIME_1100AM, TimeRange.END_OF_DAY, true)); + + Assert.assertEquals(expected, actual); + } + + public void onlyOptionalWithNoTime() { + // Two optional attendees with no gaps in their schedules. Should return empty. + // + // Events : |-----A-----||-------B--------| + // Day : |-----------------------------| + // Options : + + Collection events = Arrays.asList( + new Event("Event 1", TimeRange.fromStartEnd(TimeRange.START_OF_DAY, TIME_1000AM, false), + Arrays.asList(PERSON_A)), + new Event("Event 2", TimeRange.fromStartEnd(TIME_1000AM, TimeRange.END_OF_DAY, true), + Arrays.asList(PERSON_B))); + + MeetingRequest request = new MeetingRequest(Arrays.asList(), DURATION_30_MINUTES); + request.addOptionalAttendee(PERSON_A); + request.addOptionalAttendee(PERSON_B); + + Collection actual = query.query(events, request); + Collection expected = Arrays.asList(); + + Assert.assertEquals(expected, actual); + } }