From 5d5bfe81c4596460934ebb6b9497ff70de9de5a9 Mon Sep 17 00:00:00 2001 From: Alvin Hermans Date: Thu, 18 Jun 2020 20:55:10 +0000 Subject: [PATCH 1/4] optional attendees integrated --- walkthroughs/week-5-tdd/project/pom.xml | 5 + .../java/com/google/sps/FindMeetingQuery.java | 72 +++++---- .../com/google/sps/FindMeetingQueryTest.java | 142 ++++++++++++++++++ 3 files changed, 193 insertions(+), 26 deletions(-) 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..85fa614 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; @@ -35,22 +36,62 @@ public Collection query(Collection events, MeetingRequest requ // Seperate relevant TimeRanges from events, put into ArrayList and sort by ascending meeting start time List attendedMeetings = new ArrayList<>(); + List allAttendedMeetings = new ArrayList<>(); 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()); + // Attendees is only required attendees, allAttendees includes optional attendees + Set requiredAttendees = new HashSet<>(request.getAttendees()); + Set optionalAttendees = new HashSet<>(request.getOptionalAttendees()); + 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); + + List requiredOpenings = new ArrayList<>(findOpenings(attendedMeetings, duration)); + List allOpenings = new ArrayList<>(findOpenings(allAttendedMeetings, duration)); + + 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(); + + // 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; + } + } - List validMeetings = new ArrayList<>(); + // 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 : attendedMeetings) { + for (TimeRange meeting : meetings) { int numMeetings = validMeetings.size(); @@ -85,25 +126,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); + } } From 1902dc87f1d3efe448fc75c1b09cb9d8175d43eb Mon Sep 17 00:00:00 2001 From: Alvin Hermans Date: Fri, 19 Jun 2020 22:13:40 +0000 Subject: [PATCH 2/4] style update --- .../main/java/com/google/sps/FindMeetingQuery.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 85fa614..a922520 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 @@ -35,6 +35,9 @@ 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<>(); for (Event event : events) { @@ -43,6 +46,8 @@ public Collection query(Collection events, MeetingRequest requ Set requiredAttendees = new HashSet<>(request.getAttendees()); Set optionalAttendees = new HashSet<>(request.getOptionalAttendees()); Set allAttendees = new HashSet<>(Sets.union(requiredAttendees, optionalAttendees)); + + // I couldn't think of a way to factor this, I'm sorry Zu :'( requiredAttendees.retainAll(event.getAttendees()); allAttendees.retainAll(event.getAttendees()); @@ -60,6 +65,8 @@ public Collection query(Collection events, MeetingRequest requ 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)); @@ -77,8 +84,8 @@ private TimeRange findNextTime(List meetings, int startMeetingIndex, : null; } - /* If next meeting's start time is farther away than duration, - * return a TimeRange from startTime -> the start of the next meeting. */ + // 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); } From f360fa4705c3b00378bd45dffd6634c7daf1516a Mon Sep 17 00:00:00 2001 From: Alvin Hermans Date: Mon, 22 Jun 2020 22:44:39 +0000 Subject: [PATCH 3/4] style improvements --- .../main/java/com/google/sps/FindMeetingQuery.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 a922520..c87bcc4 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 @@ -40,14 +40,15 @@ public Collection query(Collection events, MeetingRequest requ // 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 - // Attendees is only required attendees, allAttendees includes optional attendees - Set requiredAttendees = new HashSet<>(request.getAttendees()); - Set optionalAttendees = new HashSet<>(request.getOptionalAttendees()); Set allAttendees = new HashSet<>(Sets.union(requiredAttendees, optionalAttendees)); - // I couldn't think of a way to factor this, I'm sorry Zu :'( requiredAttendees.retainAll(event.getAttendees()); allAttendees.retainAll(event.getAttendees()); @@ -70,6 +71,8 @@ public Collection query(Collection events, MeetingRequest requ 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; } From 35fab9ce6fbb0f82d56c56d55d129ed2ccf236bc Mon Sep 17 00:00:00 2001 From: Alvin Hermans Date: Mon, 22 Jun 2020 22:48:21 +0000 Subject: [PATCH 4/4] top comment explains interface and return values --- .../project/src/main/java/com/google/sps/FindMeetingQuery.java | 3 +++ 1 file changed, 3 insertions(+) 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 c87bcc4..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 @@ -24,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.