forked from nus-cs2103-AY2122S1/tp
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #145 from AY2122S1-CS2103T-T15-4/trace
[Feat] Contact Tracing
- Loading branch information
Showing
30 changed files
with
492 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
141 changes: 141 additions & 0 deletions
141
src/main/java/safeforhall/logic/commands/TraceCommand.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package safeforhall.logic.commands; | ||
|
||
import static java.util.Objects.requireNonNull; | ||
|
||
import java.time.LocalDate; | ||
import java.time.temporal.ChronoUnit; | ||
import java.util.ArrayList; | ||
import java.util.Optional; | ||
import java.util.function.Predicate; | ||
|
||
import safeforhall.logic.commands.exceptions.CommandException; | ||
import safeforhall.logic.parser.CliSyntax; | ||
import safeforhall.model.AddressBook; | ||
import safeforhall.model.Model; | ||
import safeforhall.model.event.Event; | ||
import safeforhall.model.person.Person; | ||
|
||
public class TraceCommand extends Command { | ||
|
||
public static final String COMMAND_WORD = "trace"; | ||
public static final String PARAMETERS = "r/RESIDENT [d/DEPTH] [t/DURATION] "; | ||
public static final String MESSAGE_USAGE = COMMAND_WORD + ": Traces a resident's close contacts based on the " | ||
+ "events they're involved in. \n" | ||
+ "Parameters: " | ||
+ CliSyntax.PREFIX_RESIDENT + "RESIDENT " | ||
+ "[" + CliSyntax.PREFIX_DEPTH + "DEPTH] " | ||
+ "[" + CliSyntax.PREFIX_DURATION + "DURATION] \n" | ||
+ "Example: " + COMMAND_WORD + " " | ||
+ CliSyntax.PREFIX_RESIDENT + "A210 " | ||
+ CliSyntax.PREFIX_DEPTH + "2 " | ||
+ CliSyntax.PREFIX_DURATION + "4 \n" | ||
+ "Note: \n" | ||
+ " 1. A resident can be identified either by full name or room \n" | ||
+ " 2. Depth refers to the number of maximum links to reach resident in question \n" | ||
+ " 3. Depth should be an integer >= 1 and will default to 1 \n" | ||
+ " 4. Duration is in days and will default to 7\n"; | ||
|
||
public static final String MESSAGE_FOUND_CONTACTS = "Found %1d close contacts at this depth: "; | ||
|
||
public static final Integer DEFAULT_DEPTH = 1; | ||
public static final Integer DEFAULT_DURATION = 7; | ||
|
||
private final String personInput; | ||
private final Integer depth; | ||
private final Integer duration; | ||
private Optional<Person> person; | ||
|
||
/** | ||
* Creates a TraceCommand to trace the depth-level contacts of the specified {@code Person} | ||
* | ||
* @param person The resident to trace (either name or room validated) | ||
*/ | ||
public TraceCommand(String person) { | ||
this.personInput = person; | ||
this.depth = DEFAULT_DEPTH; | ||
this.duration = DEFAULT_DURATION; | ||
} | ||
|
||
/** | ||
* Creates a TraceCommand to trace the depth-level contacts of the specified {@code Person} | ||
* | ||
* @param person The resident to trace (either name or room validated) | ||
* @param depth The depth of tracing | ||
*/ | ||
public TraceCommand(String person, Integer depth) { | ||
this.personInput = person; | ||
this.depth = depth; | ||
this.duration = DEFAULT_DURATION; | ||
} | ||
|
||
/** | ||
* Creates a TraceCommand to trace the depth-level contacts of the specified {@code Person} | ||
* | ||
* @param person The resident to trace (either name or room validated) | ||
* @param depth The depth of tracing | ||
* @param duration The number of days to trace back to (for events) | ||
*/ | ||
public TraceCommand(String person, Integer depth, Integer duration) { | ||
this.personInput = person; | ||
this.depth = depth; | ||
this.duration = duration; | ||
} | ||
|
||
@Override | ||
public CommandResult execute(Model model) throws CommandException { | ||
requireNonNull(model); | ||
AddressBook addressBook = (AddressBook) model.getAddressBook(); | ||
this.person = addressBook.findPerson(this.personInput); | ||
|
||
if (this.person.isEmpty()) { | ||
throw new CommandException("No resident with this information '" + this.personInput + "' could be found"); | ||
} | ||
|
||
ArrayList<Person> contacts = findCloseContacts(model, this.person.get()); | ||
contacts.remove(this.person.get()); | ||
|
||
model.updateFilteredPersonList(contacts::contains); | ||
return new CommandResult( | ||
String.format(MESSAGE_FOUND_CONTACTS, model.getFilteredPersonList().size())); | ||
} | ||
|
||
private ArrayList<Person> findCloseContacts(Model model, Person person) { | ||
Predicate<Event> predicate = event -> { | ||
LocalDate eventDate = event.getEventDate().toLocalDate(); | ||
LocalDate today = LocalDate.now(); | ||
long days = ChronoUnit.DAYS.between(eventDate, today); | ||
return days >= 0 && days <= this.duration; | ||
}; | ||
ArrayList<Person> contacts = new ArrayList<>(); | ||
contacts.add(person); | ||
for (int i = 0; i < this.depth; i++) { | ||
ArrayList<Person> copyOfContacts = new ArrayList<>(contacts); | ||
for (Person contact: contacts) { | ||
ArrayList<Event> relevantEvents = model.getPersonEvents(contact, predicate); | ||
addToContacts(copyOfContacts, relevantEvents); | ||
} | ||
contacts = copyOfContacts; | ||
} | ||
return contacts; | ||
} | ||
|
||
private void addToContacts(ArrayList<Person> contacts, ArrayList<Event> relevantEvents) { | ||
for (Event e: relevantEvents) { | ||
ArrayList<Person> attendees = e.getResidentList().getResidents(); | ||
for (Person attendee: attendees) { | ||
if (!contacts.contains(attendee)) { | ||
contacts.add(attendee); | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
public boolean equals(Object other) { | ||
return other == this // short circuit if same object | ||
|| (other instanceof TraceCommand // instanceof handles nulls | ||
&& this.personInput.equals(((TraceCommand) other).personInput) | ||
&& this.depth.equals(((TraceCommand) other).depth) | ||
&& this.duration.equals(((TraceCommand) other).duration)); // state check | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
src/main/java/safeforhall/logic/parser/TraceCommandParser.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package safeforhall.logic.parser; | ||
|
||
import static java.util.Objects.requireNonNull; | ||
import static safeforhall.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; | ||
|
||
import java.util.stream.Stream; | ||
|
||
import safeforhall.logic.commands.TraceCommand; | ||
import safeforhall.logic.parser.exceptions.ParseException; | ||
|
||
public class TraceCommandParser implements Parser<TraceCommand> { | ||
|
||
/** | ||
* Parses the given {@code String} of arguments in the context of the TraceCommand | ||
* and returns a TraceCommand object for execution. | ||
* @throws ParseException if the user input does not conform the expected format | ||
*/ | ||
public TraceCommand parse(String args) throws ParseException { | ||
requireNonNull(args); | ||
String trimmedArgs = args.trim(); | ||
if (trimmedArgs.isEmpty()) { | ||
throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, TraceCommand.MESSAGE_USAGE)); | ||
} | ||
|
||
ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, CliSyntax.PREFIX_RESIDENT, | ||
CliSyntax.PREFIX_DEPTH, CliSyntax.PREFIX_DURATION); | ||
|
||
if (!arePrefixesPresent(argMultimap, CliSyntax.PREFIX_RESIDENT) | ||
|| !argMultimap.getPreamble().isEmpty()) { | ||
throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, TraceCommand.MESSAGE_USAGE)); | ||
} | ||
|
||
// Required fields | ||
// Either name or room need to be valid | ||
String inputForResident = argMultimap.getValue(CliSyntax.PREFIX_RESIDENT).get(); | ||
try { | ||
ParserUtil.parseName(inputForResident); | ||
} catch (ParseException e) { | ||
try { | ||
ParserUtil.parseRoom(inputForResident); | ||
} catch (ParseException pe) { | ||
throw new ParseException("Information is neither a room or name\n" | ||
+ TraceCommand.MESSAGE_USAGE); | ||
} | ||
} | ||
|
||
// Optional fields | ||
Integer depth; | ||
Integer duration; | ||
try { | ||
depth = Integer.parseInt(argMultimap.getValue(CliSyntax.PREFIX_DEPTH) | ||
.orElse(TraceCommand.DEFAULT_DEPTH.toString())); | ||
duration = Integer.parseInt(argMultimap.getValue(CliSyntax.PREFIX_DURATION) | ||
.orElse(TraceCommand.DEFAULT_DURATION.toString())); | ||
} catch (NumberFormatException e) { | ||
throw new ParseException("Depth and duration must be integers\n" + TraceCommand.MESSAGE_USAGE); | ||
} | ||
|
||
return new TraceCommand(inputForResident, depth, duration); | ||
} | ||
|
||
/** | ||
* Returns true if none of the prefixes contains empty {@code Optional} values in the given | ||
* {@code ArgumentMultimap}. | ||
*/ | ||
private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { | ||
return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.