Skip to content

Commit

Permalink
Merge pull request #145 from AY2122S1-CS2103T-T15-4/trace
Browse files Browse the repository at this point in the history
[Feat] Contact Tracing
  • Loading branch information
chongjunwei authored Oct 28, 2021
2 parents 87f1f37 + e234d4c commit fd45a12
Show file tree
Hide file tree
Showing 30 changed files with 492 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public CommandResult execute(Model model) throws CommandException {
}

ArrayList<Person> toRemove = model.toPersonList(residentList);
ArrayList<Person> currentResidents = model.getCurrentEventResidents(event.getResidents());
ArrayList<Person> currentResidents = model.getCurrentEventResidents(event.getResidentList());

checkAllExists(toRemove, currentResidents);

Expand Down
4 changes: 4 additions & 0 deletions src/main/java/safeforhall/logic/commands/ImportCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ public class ImportCommand extends Command {
public static final String PARAMETERS = "CSV_NAME";
public static final String MESSAGE_USAGE = COMMAND_WORD + ": Imports resident information from the specified csv "
+ "file located within the /data folder\n"
+ "Parameters: "
+ "NAME_OF_CSV\n"
+ "Example: " + COMMAND_WORD + " "
+ "safeforhall\n"
+ "Note: \n"
+ " 1. 8 comma separated values for each row in order; \n"
+ " name, room, phone, email, vaccStatus, faculty, lastFetDate, lastCollectionDate\n"
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/safeforhall/logic/commands/IncludeCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,15 @@ public CommandResult execute(Model model) throws CommandException {
+ "' could be found");
}
ArrayList<Person> toAdd = model.toPersonList(residentList);
ArrayList<Person> currentResidents = model.getCurrentEventResidents(event.getResidents());
ArrayList<Person> currentResidents = model.getCurrentEventResidents(event.getResidentList());

checkForDuplicates(toAdd, currentResidents);

String combinedDisplayString = event.getCombinedDisplayString(toAdd);
String combinedStorageString = event.getCombinedStorageString(toAdd);

if (new ResidentList(combinedDisplayString,
combinedStorageString).getResidentList().size() > event.getCapacity().capacity) {
combinedStorageString).getResidents().size() > event.getCapacity().capacity) {
throw new CommandException(MESSAGE_EXCEED_CAPACITY);
}

Expand Down
141 changes: 141 additions & 0 deletions src/main/java/safeforhall/logic/commands/TraceCommand.java
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public CommandResult execute(Model model) throws CommandException {
throw new CommandException(String.format(MESSAGE_INVALID_RESIDENT, invalidResident));
}

ArrayList<Person> personList = model.toPersonList(toAdd.getResidents());
ArrayList<Person> personList = model.toPersonList(toAdd.getResidentList());
if (personList.size() > toAdd.getCapacity().capacity) {
throw new CommandException(MESSAGE_EXCEED_CAPACITY);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ private static Event createEditedEvent(Event eventToEdit, EditEventDescriptor ed
Venue updatedVenue = editEventDescriptor.getVenue().orElse(eventToEdit.getVenue());
Capacity updatedCapacity = editEventDescriptor.getCapacity().orElse(eventToEdit.getCapacity());
ResidentList updatedResidentList = editEventDescriptor.getResidentList()
.orElse(eventToEdit.getResidents());
.orElse(eventToEdit.getResidentList());

return new Event(updatedName, updatedDate, updatedTime, updatedVenue, updatedCapacity, updatedResidentList);
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/safeforhall/logic/parser/AddressBookParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import safeforhall.logic.commands.ImportCommand;
import safeforhall.logic.commands.IncludeCommand;
import safeforhall.logic.commands.SwitchCommand;
import safeforhall.logic.commands.TraceCommand;
import safeforhall.logic.commands.add.AddEventCommand;
import safeforhall.logic.commands.add.AddPersonCommand;
import safeforhall.logic.commands.delete.DeleteEventCommand;
Expand Down Expand Up @@ -125,6 +126,9 @@ private Command parseResidentCommand(String commandWord, String arguments) throw
case ExportCommand.COMMAND_WORD:
return new ExportCommandParser().parse(arguments);

case TraceCommand.COMMAND_WORD:
return new TraceCommandParser().parse(arguments);

default:
throw new ParseException(MESSAGE_UNKNOWN_COMMAND);
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/safeforhall/logic/parser/CliSyntax.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@ public class CliSyntax {
public static final Prefix PREFIX_VENUE = new Prefix("v/");
public static final Prefix PREFIX_CAPACITY = new Prefix("c/");
public static final Prefix PREFIX_RESIDENTS = new Prefix("r/");
public static final Prefix PREFIX_RESIDENT = new Prefix("r/");
public static final Prefix PREFIX_DEPTH = new Prefix("d/");
public static final Prefix PREFIX_TIME = new Prefix("t/");
public static final Prefix PREFIX_DURATION = new Prefix("t/");
}
69 changes: 69 additions & 0 deletions src/main/java/safeforhall/logic/parser/TraceCommandParser.java
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());
}
}
8 changes: 8 additions & 0 deletions src/main/java/safeforhall/model/Model.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,12 @@ public interface Model {
* @throws NullPointerException if {@code predicate} is null.
*/
void updateFilteredEventList(Predicate<Event> predicate);

/**
* Returns an array list of events the specified person is in.
* @param person The person to search for in events
* @param predicate A predicate to filter given events by
* @return The array list of events
*/
ArrayList<Event> getPersonEvents(Person person, Predicate<Event> predicate);
}
15 changes: 14 additions & 1 deletion src/main/java/safeforhall/model/ModelManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public ArrayList<Person> toPersonList(ResidentList residentList) throws CommandE
personFound = addressBook.findPerson(information);

if (personFound.isEmpty()) {
throw new CommandException("No person with this information '" + information + "' could be found");
throw new CommandException("No resident with this information '" + information + "' could be found");
} else {
personList.add(personFound.get());
}
Expand Down Expand Up @@ -289,6 +289,19 @@ public void updateFilteredEventList(Predicate<Event> predicate) {
filteredEvents.setPredicate(predicate);
}

@Override
public ArrayList<Event> getPersonEvents(Person person, Predicate<Event> predicate) {
ArrayList<Event> events = new ArrayList<>();
for (Event e: filteredEvents.filtered(predicate)) {
if (e.getResidentList().getResidents().contains(person)) {
if (!events.contains(e)) {
events.add(e);
}
}
}
return events;
}

@Override
public boolean equals(Object obj) {
// short circuit if same object
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/safeforhall/model/event/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public Capacity getCapacity() {
return capacity;
}

public ResidentList getResidents() {
public ResidentList getResidentList() {
return residents;
}

Expand Down Expand Up @@ -180,7 +180,7 @@ public String toString() {
.append("; Capacity: ")
.append(getCapacity())
.append("; Residents: ")
.append(getResidents());
.append(getResidentList());
return builder.toString();
}
}
2 changes: 1 addition & 1 deletion src/main/java/safeforhall/model/event/ResidentList.java
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ public String getResidentsDisplay() {
return this.residentsDisplay;
}

public ArrayList<Person> getResidentList() {
public ArrayList<Person> getResidents() {
return this.residentList;
}
}
Loading

0 comments on commit fd45a12

Please sign in to comment.