diff --git a/libs/A0146130W.md b/libs/A0146130W.md index 0051df58d474..73c5ea897901 100644 --- a/libs/A0146130W.md +++ b/libs/A0146130W.md @@ -1,4 +1,11 @@ # A0146130W +###### /src/main/java/seedu/gtd/commons/exceptions/DataConversionException.java +``` java + public DataConversionException(String cause) { + super(cause); + } +} +``` ###### /src/main/java/seedu/gtd/logic/commands/EditCommand.java ``` java /** @@ -46,8 +53,8 @@ return new CommandResult(ive.getMessage()); } - assert model != null; - try { + assert model != null; + try { model.editTask(targetIndex, taskToUpdate); } catch (TaskNotFoundException e) { assert false : "The target task cannot be missing"; @@ -95,40 +102,64 @@ ``` ###### /src/main/java/seedu/gtd/logic/parser/DateNaturalLanguageProcessor.java ``` java + +/** + * Uses natty API: http://natty.joestelmach.com to parse natural language into dates or string + */ public class DateNaturalLanguageProcessor implements NaturalLanguageProcessor { private static final com.joestelmach.natty.Parser parser = new com.joestelmach.natty.Parser(); + private static final String DATE_FORMAT = "dd/MM/yyyy HH:mm:ss"; @Override public String formatString(String naturalLanguageDate) { List dateGroups = parser.parse(naturalLanguageDate); - return refineDateGroupList(dateGroups); + Date parsedDate; + try { + parsedDate = refineDateGroupList(dateGroups); + } catch (NaturalLanguageException e) { + return ""; + } + return formatDateToString(parsedDate); } - /*Does not tolerate alternative dates*/ - private String refineDateGroupList(List groups) { - if(groups.size() == 1) { - return formatDateToString(groups.get(0).getDates().get(0)); - } - else return ""; + /** + * Chooses the first date from a list of dates that Natty has parsed from the natural language string + * @throws NaturalLanguageException + * */ + private Date refineDateGroupList(List groups) throws NaturalLanguageException { + if(groups.size() == 0) throw new NaturalLanguageException(); + return groups.get(0).getDates().get(0); } private String formatDateToString(Date date) { - Format formatter = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); + Format formatter = new SimpleDateFormat(DATE_FORMAT); return formatter.format(date); } } ``` ###### /src/main/java/seedu/gtd/logic/parser/NaturalLanguageProcessor.java ``` java + +package seedu.gtd.logic.parser; + +import seedu.gtd.commons.exceptions.DataConversionException; + public interface NaturalLanguageProcessor { - /*Takes in a string written in natural language and formats it.*/ + /** Takes in a string written in natural language and formats it.*/ String formatString(String s); + + public static class NaturalLanguageException extends DataConversionException { + protected NaturalLanguageException() { + super("Natural Language Processor was unable to convert input"); + } + } } ``` ###### /src/main/java/seedu/gtd/logic/parser/Parser.java ``` java + private String parseDueDate(String dueDateRaw) { NaturalLanguageProcessor nlp = new DateNaturalLanguageProcessor(); return nlp.formatString(dueDateRaw); @@ -153,14 +184,9 @@ public interface NaturalLanguageProcessor { Optional index = Optional.of(Integer.parseInt(matcher.group("targetIndex"))); String newDetail = matcher.group("newDetail"); - System.out.println(newDetail); - String detailType = extractDetailType(newDetail); - - if(detailType != "name") { - newDetail = newDetail.substring(2); - } - System.out.println(index.get() + " " + detailType + " " + newDetail); + String detailType = extractDetailType(newDetail); + newDetail = prepareNewDetail(detailType, newDetail); return new EditCommand( (index.get() - 1), @@ -170,7 +196,6 @@ public interface NaturalLanguageProcessor { } private String extractDetailType(String detailType) { - System.out.println(detailType.substring(0, 2)); switch(detailType.substring(0, 2)) { case "d/": return "dueDate"; case "a/": return "address"; @@ -179,6 +204,19 @@ public interface NaturalLanguageProcessor { } } + private String prepareNewDetail(String detailType, String newDetail) { + + if(detailType == "name") { + return newDetail; + } + + newDetail = newDetail.substring(2); + if(detailType == "dueDate") { + newDetail = parseDueDate(newDetail); + } + return newDetail; + } + ``` ###### /src/main/java/seedu/gtd/model/AddressBook.java ``` java @@ -226,3 +264,133 @@ public interface NaturalLanguageProcessor { } ``` +###### /src/test/java/guitests/EditCommandTest.java +``` java + +package guitests; + +import org.junit.Test; + +import seedu.gtd.testutil.TestTask; +import seedu.gtd.testutil.TestUtil; + +import static org.junit.Assert.assertTrue; +import static seedu.gtd.logic.commands.EditCommand.MESSAGE_EDIT_TASK_SUCCESS; + +public class EditCommandTest extends AddressBookGuiTest { + + @Test + public void edit() { + + //edit the priority of the first task + TestTask[] currentList = td.getTypicalTasks(); + int targetIndex = 1; + String change = "p/4"; + assertEditSuccess(targetIndex, change, currentList); + + //edit the dueDate of the last in the list + currentList = TestUtil.editTaskInList(currentList, targetIndex, change, currentList[targetIndex-1]); + targetIndex = currentList.length; + change = "d/2"; + assertEditSuccess(targetIndex, change, currentList); + + //edit the name task from the middle of the list + currentList = TestUtil.editTaskInList(currentList, targetIndex, change, currentList[targetIndex-1]); + targetIndex = currentList.length/2; + change = "Tutorial 4"; + assertEditSuccess(targetIndex, change, currentList); + + //edit the address task from the middle of the list + currentList = TestUtil.editTaskInList(currentList, targetIndex, change, currentList[targetIndex-1]); + change = "a/NTU"; + assertEditSuccess(targetIndex, change, currentList); + + //invalid index + commandBox.runCommand("edit " + currentList.length + 1 + " Invalid"); + assertResultMessage("The task index provided is invalid"); + + } + + /** + * Runs the delete command to delete the task at specified index and confirms the result is correct. + * @param targetIndexOneIndexed e.g. to delete the first task in the list, 1 should be given as the target index. + * @param currentList A copy of the current list of tasks (before deletion). + */ + private void assertEditSuccess(int targetIndexOneIndexed, String change, final TestTask[] currentList) { + TestTask taskToEdit = currentList[targetIndexOneIndexed-1]; //-1 because array uses zero indexing + TestTask[] expectedRemainder = TestUtil.editTaskInList(currentList, targetIndexOneIndexed, change, taskToEdit); + commandBox.runCommand("edit " + targetIndexOneIndexed + " " + change); + + //confirm the list now contains all previous tasks except the deleted task + assertTrue(taskListPanel.isListMatching(expectedRemainder)); + + //confirm the result message is correct + assertResultMessage(String.format(MESSAGE_EDIT_TASK_SUCCESS, expectedRemainder[targetIndexOneIndexed-1])); + } + +} +``` +###### /src/test/java/seedu/gtd/testutil/TestUtil.java +``` java + + /** + * Edits a task in the array of tasks. + * @param tasks A array of tasks. + * @param tasksToAdd The tasks that are to be appended behind the original array. + * @return The modified array of tasks. + * @throws IllegalValueException + */ + public static TestTask[] editTaskInList(final TestTask[] tasks, int index, String change, TestTask taskToEdit) { + List listOfTasks = asList(tasks); + TestTask taskEditted; + try { + taskEditted = TestUtilParser.editTask(taskToEdit, change); + } catch (IllegalValueException e) { + taskEditted = taskToEdit; + e.printStackTrace(); + } + listOfTasks.set(index-1, taskEditted); + return listOfTasks.toArray(new TestTask[listOfTasks.size()]); + } + +``` +###### /src/test/java/seedu/gtd/testutil/TestUtilParser.java +``` java + +package seedu.gtd.testutil; + +import seedu.gtd.commons.exceptions.IllegalValueException; +import seedu.gtd.logic.parser.DateNaturalLanguageProcessor; +import seedu.gtd.logic.parser.NaturalLanguageProcessor; +import seedu.gtd.model.task.Address; +import seedu.gtd.model.task.DueDate; +import seedu.gtd.model.task.Name; +import seedu.gtd.model.task.Priority; + +/** + * A utility class that parses tasks for test cases. + */ +public class TestUtilParser { + + public static TestTask editTask(TestTask task, String change) throws IllegalValueException { + + TestTask newTask = task; + String changeWithoutPrefix = change.substring(2); + String changePrefix = change.substring(0, 2); + System.out.println("From TestUtil Parser: " + changePrefix + " " + changeWithoutPrefix); + + switch(change.substring(0, 2)) { + case "d/": newTask = new TestTask(task.getName(), new DueDate(parseDueDate(changeWithoutPrefix)), task.getAddress(), task.getPriority(), task.getTags()); break; + case "a/": newTask = new TestTask(task.getName(), task.getDueDate(), new Address(changeWithoutPrefix), task.getPriority(), task.getTags()); break; + case "p/": newTask = new TestTask(task.getName(), task.getDueDate(), task.getAddress(), new Priority(changeWithoutPrefix), task.getTags()); break; + default: newTask = new TestTask(new Name(change), task.getDueDate(), task.getAddress(), task.getPriority(), task.getTags()); + } + return newTask; + } + + public static String parseDueDate(String dueDateRaw) { + NaturalLanguageProcessor nlp = new DateNaturalLanguageProcessor(); + return nlp.formatString(dueDateRaw); + } +} +``` diff --git a/libs/addressbooklevel4.md b/libs/addressbooklevel4.md index 0b5fbceecbd1..6743ab4432a1 100644 --- a/libs/addressbooklevel4.md +++ b/libs/addressbooklevel4.md @@ -11,6 +11,7 @@ ``` ###### /src/main/java/seedu/gtd/logic/parser/Parser.java ``` java + /** * Used for initial separation of command word and args. */ @@ -21,14 +22,29 @@ private static final Pattern KEYWORDS_ARGS_FORMAT = Pattern.compile("(?\\S+(?:\\s+\\S+)*)"); // one or more keywords separated by whitespace - private static final Pattern TASK_DATA_ARGS_FORMAT = // '/' forward slashes are reserved for delimiter prefixes - Pattern.compile("(?[^/]+)" - + " (?p?)d/(?[^/]+)" - + " (?p?)a/(?
[^/]+)" - + " (?p?)p/(?[^/]+)" - + "(?(?: t/[^/]+)*)"); // variable number of tags +// private static final Pattern TASK_DATA_ARGS_FORMAT = // '/' forward slashes are reserved for delimiter prefixes +// Pattern.compile("(?[^/]+)" +// + " d/(?[^/]+)" +// + " a/(?
[^/]+)" +// + " p/(?[^/]+)" +// + "(?(?: t/[^/]+)*)"); // variable number of tags + + private static final Pattern NAME_TASK_DATA_ARGS_FORMAT = + Pattern.compile("(?[^/]+) (t|p|a|d|e)/.*"); + + private static final Pattern PRIORITY_TASK_DATA_ARGS_FORMAT = + Pattern.compile(".* p/(?[^/]+) (t|a|d|e)/.*"); + + private static final Pattern ADDRESS_TASK_DATA_ARGS_FORMAT = + Pattern.compile(".* a/(?
[^/]+) (t|p|d|e)/.*"); + + private static final Pattern DUEDATE_TASK_DATA_ARGS_FORMAT = + Pattern.compile(".* d/(?[^/]+) (t|a|p|e)/.*"); + + private static final Pattern TAGS_TASK_DATA_ARGS_FORMAT = + Pattern.compile(".* t/(?[^/]+) (d|a|p|e)/.*"); - private static final Pattern EDIT_DATA_ARGS_FORMAT = // '/' forward slashes are reserved for delimiter prefixes + private static final Pattern EDIT_DATA_ARGS_FORMAT = Pattern.compile("(?\\S+)" + " (?.*)"); @@ -89,27 +105,61 @@ * @return the prepared command */ private Command prepareAdd(String args){ - final Matcher matcher = TASK_DATA_ARGS_FORMAT.matcher(args.trim()); + String preprocessedArg = appendEnd(args.trim()); + + final Matcher nameMatcher = NAME_TASK_DATA_ARGS_FORMAT.matcher(preprocessedArg); + final Matcher dueDateMatcher = DUEDATE_TASK_DATA_ARGS_FORMAT.matcher(preprocessedArg); + final Matcher addressMatcher = ADDRESS_TASK_DATA_ARGS_FORMAT.matcher(preprocessedArg); + final Matcher priorityMatcher = PRIORITY_TASK_DATA_ARGS_FORMAT.matcher(preprocessedArg); + final Matcher tagsMatcher = TAGS_TASK_DATA_ARGS_FORMAT.matcher(preprocessedArg); + + String nameToAdd = checkEmptyAndAddDefault(nameMatcher, "name", "none"); + String dueDateToAdd = checkEmptyAndAddDefault(dueDateMatcher, "dueDate", "none"); + String addressToAdd = checkEmptyAndAddDefault(addressMatcher, "address", "none"); + String priorityToAdd = checkEmptyAndAddDefault(priorityMatcher, "priority", "1"); +// String tagsToAdd = checkEmptyAndAddDefault(tagsMatcher, "tagsArgument", ""); + + // format date if due date is specified + if (dueDateMatcher.matches()) { + dueDateToAdd = parseDueDate(dueDateToAdd); + } + + Set tagsProcessed = Collections.emptySet(); + + if (tagsMatcher.matches()) { + tagsProcessed = getTagsFromArgs(tagsMatcher.group("tagArguments")); + } + // Validate arg string format - if (!matcher.matches()) { + if (!nameMatcher.matches()) { return new IncorrectCommand(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } - String dueDate = parseDueDate(matcher.group("dueDate")); - try { return new AddCommand( - matcher.group("name"), - dueDate, - matcher.group("address"), - matcher.group("priority"), - getTagsFromArgs(matcher.group("tagArguments")) + nameToAdd, + dueDateToAdd, + addressToAdd, + priorityToAdd, + tagsProcessed ); } catch (IllegalValueException ive) { return new IncorrectCommand(ive.getMessage()); } } + private String appendEnd(String args) { + return args + " e/"; + } + + private String checkEmptyAndAddDefault(Matcher matcher, String groupName, String defaultValue) { + if (matcher.matches()) { + return matcher.group(groupName); + } else { + return defaultValue; + } + } + ``` ###### /src/main/java/seedu/gtd/logic/parser/Parser.java ``` java @@ -117,13 +167,13 @@ * Extracts the new task's tags from the add command's tag arguments string. * Merges duplicate tag strings. */ - private static Set getTagsFromArgs(String tagArguments) throws IllegalValueException { + private static Set getTagsFromArgs(String tagArguments) { // no tags if (tagArguments.isEmpty()) { return Collections.emptySet(); } // replace first delimiter prefix, then split - final Collection tagStrings = Arrays.asList(tagArguments.replaceFirst(" t/", "").split(" t/")); + final Collection tagStrings = Arrays.asList(tagArguments.split(" ")); return new HashSet<>(tagStrings); } @@ -174,7 +224,6 @@ } String index = matcher.group("targetIndex"); - System.out.println(index); if(!StringUtil.isUnsignedInteger(index)){ return Optional.empty(); } @@ -904,20 +953,14 @@ public class MainApp extends Application { assertCommandBehavior("clear", ClearCommand.MESSAGE_SUCCESS, new AddressBook(), Collections.emptyList()); } - - + @Test public void execute_add_invalidArgsFormat() throws Exception { - String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE); - assertCommandBehavior( - "add wrong args wrong args", expectedMessage); - assertCommandBehavior( - "add Valid Name 12345 d/valid@dueDate.butNoDueDatePrefix a/valid, address", expectedMessage); - assertCommandBehavior( - "add Valid Name d/12345 valid@address.butNoPrefix p/valid, priority", expectedMessage); - assertCommandBehavior( - "add Valid Name d/12345 a/valid@email.butNoAddressPrefix valid, priority", expectedMessage); + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE); + assertCommandBehavior( + "add ", expectedMessage); } + @Test public void execute_add_invalidTaskData() throws Exception { @@ -948,6 +991,22 @@ public class MainApp extends Application { expectedAB.getTaskList()); } + + @Test + public void execute_add_optional_successful() throws Exception { + + // setup expectations + TestDataHelper helper = new TestDataHelper(); + Task intendedResult = helper.optionalAddressDateChanged(); + AddressBook expectedAB = new AddressBook(); + expectedAB.addTask(intendedResult); + String optionalAddressCmd = "add clean room d/noon p/3 t/tag1"; + + assertCommandBehavior(optionalAddressCmd, + String.format(AddCommand.MESSAGE_SUCCESS, intendedResult), + expectedAB, + expectedAB.getTaskList()); + } @Test public void execute_addDuplicate_notAllowed() throws Exception { @@ -1174,7 +1233,7 @@ public class MainApp extends Application { class TestDataHelper{ Task adam() throws Exception { - Name name = new Name("Adam Brown"); + Name name = new Name("Pick up laundry"); DueDate privateDueDate = new DueDate("noon"); Address address = new Address("111, alpha street"); Priority privatePriority = new Priority("1"); @@ -1185,7 +1244,7 @@ public class MainApp extends Application { Task adamChanged() throws Exception { NaturalLanguageProcessor nlpTest = new DateNaturalLanguageProcessor(); String formattedDate = nlpTest.formatString("noon"); - Name name = new Name("Adam Brown"); + Name name = new Name("Pick up laundry"); DueDate privateDueDate = new DueDate(formattedDate); Address address = new Address("111, alpha street"); Priority privatePriority = new Priority("1"); @@ -1193,6 +1252,19 @@ public class MainApp extends Application { UniqueTagList tags = new UniqueTagList(tag1); return new Task(name, privateDueDate, address, privatePriority, tags); } + + + Task optionalAddressDateChanged() throws Exception { + NaturalLanguageProcessor nlpTest = new DateNaturalLanguageProcessor(); + String formattedDate = nlpTest.formatString("noon"); + Name name = new Name("clean room"); + DueDate privateDueDate = new DueDate(formattedDate); + Address address = new Address("none"); + Priority privatePriority = new Priority("3"); + Tag tag1 = new Tag("tag1"); + UniqueTagList tags = new UniqueTagList(tag1); + return new Task(name, privateDueDate, address, privatePriority, tags); + } /** * Generates a valid task using the given seed. @@ -1322,3 +1394,325 @@ public class MainApp extends Application { } } ``` +###### /src/test/java/seedu/gtd/testutil/TestUtil.java +``` java + + public static String LS = System.lineSeparator(); + + public static void assertThrows(Class expected, Runnable executable) { + try { + executable.run(); + } + catch (Throwable actualException) { + if (!actualException.getClass().isAssignableFrom(expected)) { + String message = String.format("Expected thrown: %s, actual: %s", expected.getName(), + actualException.getClass().getName()); + throw new AssertionFailedError(message); + } else return; + } + throw new AssertionFailedError( + String.format("Expected %s to be thrown, but nothing was thrown.", expected.getName())); + } + + /** + * Folder used for temp files created during testing. Ignored by Git. + */ + public static String SANDBOX_FOLDER = FileUtil.getPath("./src/test/data/sandbox/"); + + public static final Task[] sampleTaskData = getSampleTaskData(); + + private static Task[] getSampleTaskData() { + try { + return new Task[]{ + new Task(new Name("Ali Muster"), new DueDate("9482424"), new Address("4th street"), new Priority("4"), new UniqueTagList()), + new Task(new Name("Boris Mueller"), new DueDate("87249245"), new Address("81th street"), new Priority("3"), new UniqueTagList()), + new Task(new Name("Carl Kurz"), new DueDate("95352563"), new Address("wall street"), new Priority("1"), new UniqueTagList()), + new Task(new Name("Daniel Meier"), new DueDate("87652533"), new Address("10th street"), new Priority("1"), new UniqueTagList()), + new Task(new Name("Elle Meyer"), new DueDate("9482224"), new Address("michegan ave"), new Priority("1"), new UniqueTagList()), + new Task(new Name("Fiona Kunz"), new DueDate("9482427"), new Address("little tokyo"), new Priority("1"), new UniqueTagList()), + new Task(new Name("George Best"), new DueDate("9482442"), new Address("4th street"), new Priority("4"), new UniqueTagList()), + new Task(new Name("Hoon Meier"), new DueDate("8482424"), new Address("little india"), new Priority("1"), new UniqueTagList()), + new Task(new Name("Ida Mueller"), new DueDate("8482131"), new Address("chicago ave"), new Priority("1"), new UniqueTagList()) + }; + } catch (IllegalValueException e) { + assert false; + //not possible + return null; + } + } + + public static final Tag[] sampleTagData = getSampleTagData(); + + private static Tag[] getSampleTagData() { + try { + return new Tag[]{ + new Tag("relatives"), + new Tag("friends") + }; + } catch (IllegalValueException e) { + assert false; + return null; + //not possible + } + } + + public static List generateSampleTaskData() { + return Arrays.asList(sampleTaskData); + } + + /** + * Appends the file name to the sandbox folder path. + * Creates the sandbox folder if it doesn't exist. + * @param fileName + * @return + */ + public static String getFilePathInSandboxFolder(String fileName) { + try { + FileUtil.createDirs(new File(SANDBOX_FOLDER)); + } catch (IOException e) { + throw new RuntimeException(e); + } + return SANDBOX_FOLDER + fileName; + } + + public static void createDataFileWithSampleData(String filePath) { + createDataFileWithData(generateSampleStorageAddressBook(), filePath); + } + + public static void createDataFileWithData(T data, String filePath) { + try { + File saveFileForTesting = new File(filePath); + FileUtil.createIfMissing(saveFileForTesting); + XmlUtil.saveDataToFile(saveFileForTesting, data); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void main(String... s) { + createDataFileWithSampleData(TestApp.SAVE_LOCATION_FOR_TESTING); + } + + public static AddressBook generateEmptyAddressBook() { + return new AddressBook(new UniqueTaskList(), new UniqueTagList()); + } + + public static XmlSerializableAddressBook generateSampleStorageAddressBook() { + return new XmlSerializableAddressBook(generateEmptyAddressBook()); + } + + /** + * Tweaks the {@code keyCodeCombination} to resolve the {@code KeyCode.SHORTCUT} to their + * respective platform-specific keycodes + */ + public static KeyCode[] scrub(KeyCodeCombination keyCodeCombination) { + List keys = new ArrayList<>(); + if (keyCodeCombination.getAlt() == KeyCombination.ModifierValue.DOWN) { + keys.add(KeyCode.ALT); + } + if (keyCodeCombination.getShift() == KeyCombination.ModifierValue.DOWN) { + keys.add(KeyCode.SHIFT); + } + if (keyCodeCombination.getMeta() == KeyCombination.ModifierValue.DOWN) { + keys.add(KeyCode.META); + } + if (keyCodeCombination.getControl() == KeyCombination.ModifierValue.DOWN) { + keys.add(KeyCode.CONTROL); + } + keys.add(keyCodeCombination.getCode()); + return keys.toArray(new KeyCode[]{}); + } + + public static boolean isHeadlessEnvironment() { + String headlessProperty = System.getProperty("testfx.headless"); + return headlessProperty != null && headlessProperty.equals("true"); + } + + public static void captureScreenShot(String fileName) { + File file = GuiTest.captureScreenshot(); + try { + Files.copy(file, new File(fileName + ".png")); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static String descOnFail(Object... comparedObjects) { + return "Comparison failed \n" + + Arrays.asList(comparedObjects).stream() + .map(Object::toString) + .collect(Collectors.joining("\n")); + } + + public static void setFinalStatic(Field field, Object newValue) throws NoSuchFieldException, IllegalAccessException{ + field.setAccessible(true); + // remove final modifier from field + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + // ~Modifier.FINAL is used to remove the final modifier from field so that its value is no longer + // final and can be changed + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.set(null, newValue); + } + + public static void initRuntime() throws TimeoutException { + FxToolkit.registerPrimaryStage(); + FxToolkit.hideStage(); + } + + public static void tearDownRuntime() throws Exception { + FxToolkit.cleanupStages(); + } + + /** + * Gets private method of a class + * Invoke the method using method.invoke(objectInstance, params...) + * + * Caveat: only find method declared in the current Class, not inherited from supertypes + */ + public static Method getPrivateMethod(Class objectClass, String methodName) throws NoSuchMethodException { + Method method = objectClass.getDeclaredMethod(methodName); + method.setAccessible(true); + return method; + } + + public static void renameFile(File file, String newFileName) { + try { + Files.copy(file, new File(newFileName)); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + + /** + * Gets mid point of a node relative to the screen. + * @param node + * @return + */ + public static Point2D getScreenMidPoint(Node node) { + double x = getScreenPos(node).getMinX() + node.getLayoutBounds().getWidth() / 2; + double y = getScreenPos(node).getMinY() + node.getLayoutBounds().getHeight() / 2; + return new Point2D(x,y); + } + + /** + * Gets mid point of a node relative to its scene. + * @param node + * @return + */ + public static Point2D getSceneMidPoint(Node node) { + double x = getScenePos(node).getMinX() + node.getLayoutBounds().getWidth() / 2; + double y = getScenePos(node).getMinY() + node.getLayoutBounds().getHeight() / 2; + return new Point2D(x,y); + } + + /** + * Gets the bound of the node relative to the parent scene. + * @param node + * @return + */ + public static Bounds getScenePos(Node node) { + return node.localToScene(node.getBoundsInLocal()); + } + + public static Bounds getScreenPos(Node node) { + return node.localToScreen(node.getBoundsInLocal()); + } + + public static double getSceneMaxX(Scene scene) { + return scene.getX() + scene.getWidth(); + } + + public static double getSceneMaxY(Scene scene) { + return scene.getX() + scene.getHeight(); + } + + public static Object getLastElement(List list) { + return list.get(list.size() - 1); + } + + /** + * Removes a subset from the list of tasks. + * @param tasks The list of tasks + * @param tasksToRemove The subset of tasks. + * @return The modified tasks after removal of the subset from tasks. + */ + public static TestTask[] removeTasksFromList(final TestTask[] tasks, TestTask... tasksToRemove) { + List listOfTasks = asList(tasks); + listOfTasks.removeAll(asList(tasksToRemove)); + return listOfTasks.toArray(new TestTask[listOfTasks.size()]); + } + + + /** + * Returns a copy of the list with the task at specified index removed. + * @param list original list to copy from + * @param targetIndexInOneIndexedFormat e.g. if the first element to be removed, 1 should be given as index. + */ + public static TestTask[] removeTaskFromList(final TestTask[] list, int targetIndexInOneIndexedFormat) { + return removeTasksFromList(list, list[targetIndexInOneIndexedFormat-1]); + } + + /** + * Replaces tasks[i] with a task. + * @param tasks The array of tasks. + * @param task The replacement task + * @param index The index of the task to be replaced. + * @return + */ + public static TestTask[] replaceTaskFromList(TestTask[] tasks, TestTask task, int index) { + tasks[index] = task; + return tasks; + } + + /** + * Appends tasks to the array of tasks. + * @param tasks A array of tasks. + * @param tasksToAdd The tasks that are to be appended behind the original array. + * @return The modified array of tasks. + */ + public static TestTask[] addTasksToList(final TestTask[] tasks, TestTask... tasksToAdd) { + List listOfTasks = asList(tasks); + listOfTasks.addAll(asList(tasksToAdd)); + return listOfTasks.toArray(new TestTask[listOfTasks.size()]); + } + +``` +###### /src/test/java/seedu/gtd/testutil/TestUtil.java +``` java + + private static List asList(T[] objs) { + List list = new ArrayList<>(); + for(T obj : objs) { + list.add(obj); + } + return list; + } + + public static boolean compareCardAndTask(TaskCardHandle card, ReadOnlyTask task) { + return card.isSameTask(task); + } + + public static Tag[] getTagList(String tags) { + + if (tags.equals("")) { + return new Tag[]{}; + } + + final String[] split = tags.split(", "); + + final List collect = Arrays.asList(split).stream().map(e -> { + try { + return new Tag(e.replaceFirst("Tag: ", "")); + } catch (IllegalValueException e1) { + //not possible + assert false; + return null; + } + }).collect(Collectors.toList()); + + return collect.toArray(new Tag[split.length]); + } + +} +``` diff --git a/src/main/java/seedu/gtd/commons/exceptions/DataConversionException.java b/src/main/java/seedu/gtd/commons/exceptions/DataConversionException.java index b1de151c6ea1..618900e8919b 100644 --- a/src/main/java/seedu/gtd/commons/exceptions/DataConversionException.java +++ b/src/main/java/seedu/gtd/commons/exceptions/DataConversionException.java @@ -7,4 +7,9 @@ public class DataConversionException extends Exception { public DataConversionException(Exception cause) { super(cause); } + + //@@author A0146130W + public DataConversionException(String cause) { + super(cause); + } } diff --git a/src/main/java/seedu/gtd/logic/commands/EditCommand.java b/src/main/java/seedu/gtd/logic/commands/EditCommand.java index cbad3d895d4d..769254157103 100644 --- a/src/main/java/seedu/gtd/logic/commands/EditCommand.java +++ b/src/main/java/seedu/gtd/logic/commands/EditCommand.java @@ -53,8 +53,8 @@ public CommandResult execute() { return new CommandResult(ive.getMessage()); } - assert model != null; - try { + assert model != null; + try { model.editTask(targetIndex, taskToUpdate); } catch (TaskNotFoundException e) { assert false : "The target task cannot be missing"; diff --git a/src/main/java/seedu/gtd/logic/parser/DateNaturalLanguageProcessor.java b/src/main/java/seedu/gtd/logic/parser/DateNaturalLanguageProcessor.java index 3f71141d4339..09967524130f 100644 --- a/src/main/java/seedu/gtd/logic/parser/DateNaturalLanguageProcessor.java +++ b/src/main/java/seedu/gtd/logic/parser/DateNaturalLanguageProcessor.java @@ -8,26 +8,38 @@ import com.joestelmach.natty.*; //@@author A0146130W + +/** + * Uses natty API: http://natty.joestelmach.com to parse natural language into dates or string + */ public class DateNaturalLanguageProcessor implements NaturalLanguageProcessor { private static final com.joestelmach.natty.Parser parser = new com.joestelmach.natty.Parser(); + private static final String DATE_FORMAT = "dd/MM/yyyy HH:mm:ss"; @Override public String formatString(String naturalLanguageDate) { List dateGroups = parser.parse(naturalLanguageDate); - return refineDateGroupList(dateGroups); + Date parsedDate; + try { + parsedDate = refineDateGroupList(dateGroups); + } catch (NaturalLanguageException e) { + return ""; + } + return formatDateToString(parsedDate); } - /** Does not tolerate alternative dates */ - private String refineDateGroupList(List groups) { - if(groups.size() == 1) { - return formatDateToString(groups.get(0).getDates().get(0)); - } - else return ""; + /** + * Chooses the first date from a list of dates that Natty has parsed from the natural language string + * @throws NaturalLanguageException + * */ + private Date refineDateGroupList(List groups) throws NaturalLanguageException { + if(groups.size() == 0) throw new NaturalLanguageException(); + return groups.get(0).getDates().get(0); } private String formatDateToString(Date date) { - Format formatter = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); + Format formatter = new SimpleDateFormat(DATE_FORMAT); return formatter.format(date); } } \ No newline at end of file diff --git a/src/main/java/seedu/gtd/logic/parser/NaturalLanguageProcessor.java b/src/main/java/seedu/gtd/logic/parser/NaturalLanguageProcessor.java index f99a71827173..1320fe8fbbb7 100644 --- a/src/main/java/seedu/gtd/logic/parser/NaturalLanguageProcessor.java +++ b/src/main/java/seedu/gtd/logic/parser/NaturalLanguageProcessor.java @@ -1,9 +1,17 @@ +//@@author A0146130W + package seedu.gtd.logic.parser; -//@@author A0146130W +import seedu.gtd.commons.exceptions.DataConversionException; public interface NaturalLanguageProcessor { /** Takes in a string written in natural language and formats it.*/ String formatString(String s); + + public static class NaturalLanguageException extends DataConversionException { + protected NaturalLanguageException() { + super("Natural Language Processor was unable to convert input"); + } + } } \ No newline at end of file diff --git a/src/main/java/seedu/gtd/logic/parser/Parser.java b/src/main/java/seedu/gtd/logic/parser/Parser.java index d2a2ab358af0..8dc20dff7aaa 100644 --- a/src/main/java/seedu/gtd/logic/parser/Parser.java +++ b/src/main/java/seedu/gtd/logic/parser/Parser.java @@ -16,6 +16,7 @@ */ public class Parser { //@@author addressbook-level4 + /** * Used for initial separation of command word and args. */ @@ -165,6 +166,7 @@ private String checkEmptyAndAddDefault(Matcher matcher, String groupName, String } //@@author A0146130W + private String parseDueDate(String dueDateRaw) { NaturalLanguageProcessor nlp = new DateNaturalLanguageProcessor(); return nlp.formatString(dueDateRaw); @@ -202,14 +204,9 @@ private Command prepareEdit(String args) { Optional index = Optional.of(Integer.parseInt(matcher.group("targetIndex"))); String newDetail = matcher.group("newDetail"); - System.out.println(newDetail); - String detailType = extractDetailType(newDetail); - if(detailType != "name") { - newDetail = newDetail.substring(2); - } - - System.out.println(index.get() + " " + detailType + " " + newDetail); + String detailType = extractDetailType(newDetail); + newDetail = prepareNewDetail(detailType, newDetail); return new EditCommand( (index.get() - 1), @@ -219,7 +216,6 @@ private Command prepareEdit(String args) { } private String extractDetailType(String detailType) { - System.out.println(detailType.substring(0, 2)); switch(detailType.substring(0, 2)) { case "d/": return "dueDate"; case "a/": return "address"; @@ -228,6 +224,19 @@ private String extractDetailType(String detailType) { } } + private String prepareNewDetail(String detailType, String newDetail) { + + if(detailType == "name") { + return newDetail; + } + + newDetail = newDetail.substring(2); + if(detailType == "dueDate") { + newDetail = parseDueDate(newDetail); + } + return newDetail; + } + //@@author addressbook-level4 /** * Parses arguments in the context of the delete task command. @@ -273,7 +282,6 @@ private Optional parseIndex(String command) { } String index = matcher.group("targetIndex"); - System.out.println(index); if(!StringUtil.isUnsignedInteger(index)){ return Optional.empty(); } diff --git a/src/main/java/seedu/gtd/model/task/Task.java b/src/main/java/seedu/gtd/model/task/Task.java index ff6bc907e614..97b0121d0c64 100644 --- a/src/main/java/seedu/gtd/model/task/Task.java +++ b/src/main/java/seedu/gtd/model/task/Task.java @@ -6,8 +6,6 @@ import seedu.gtd.commons.util.CollectionUtil; import seedu.gtd.model.task.ReadOnlyTask; import seedu.gtd.model.tag.UniqueTagList; -import seedu.gtd.logic.parser.*; - /** * Represents a Task in the task list. @@ -83,10 +81,9 @@ public void setPriority(Priority priority) { } public void edit(String detailType, String newDetail) throws IllegalValueException { - NaturalLanguageProcessor nlp = new DateNaturalLanguageProcessor(); switch(detailType) { - case "dueDate": setDueDate(new DueDate(nlp.formatString(newDetail))); break; + case "dueDate": setDueDate(new DueDate(newDetail)); break; case "address": setAddress(new Address(newDetail)); break; case "priority": setPriority(new Priority(newDetail)); break; default: setName(new Name(newDetail)); @@ -118,4 +115,4 @@ public String toString() { return getAsText(); } -} \ No newline at end of file +} diff --git a/src/test/java/guitests/EditCommandTest.java b/src/test/java/guitests/EditCommandTest.java new file mode 100644 index 000000000000..f2eb71b15f29 --- /dev/null +++ b/src/test/java/guitests/EditCommandTest.java @@ -0,0 +1,64 @@ +//@@author A0146130W + +package guitests; + +import org.junit.Test; + +import seedu.gtd.testutil.TestTask; +import seedu.gtd.testutil.TestUtil; + +import static org.junit.Assert.assertTrue; +import static seedu.gtd.logic.commands.EditCommand.MESSAGE_EDIT_TASK_SUCCESS; + +public class EditCommandTest extends AddressBookGuiTest { + + @Test + public void edit() { + + //edit the priority of the first task + TestTask[] currentList = td.getTypicalTasks(); + int targetIndex = 1; + String change = "p/4"; + assertEditSuccess(targetIndex, change, currentList); + + //edit the dueDate of the last in the list + currentList = TestUtil.editTaskInList(currentList, targetIndex, change, currentList[targetIndex-1]); + targetIndex = currentList.length; + change = "d/2"; + assertEditSuccess(targetIndex, change, currentList); + + //edit the name task from the middle of the list + currentList = TestUtil.editTaskInList(currentList, targetIndex, change, currentList[targetIndex-1]); + targetIndex = currentList.length/2; + change = "Tutorial 4"; + assertEditSuccess(targetIndex, change, currentList); + + //edit the address task from the middle of the list + currentList = TestUtil.editTaskInList(currentList, targetIndex, change, currentList[targetIndex-1]); + change = "a/NTU"; + assertEditSuccess(targetIndex, change, currentList); + + //invalid index + commandBox.runCommand("edit " + currentList.length + 1 + " Invalid"); + assertResultMessage("The task index provided is invalid"); + + } + + /** + * Runs the delete command to delete the task at specified index and confirms the result is correct. + * @param targetIndexOneIndexed e.g. to delete the first task in the list, 1 should be given as the target index. + * @param currentList A copy of the current list of tasks (before deletion). + */ + private void assertEditSuccess(int targetIndexOneIndexed, String change, final TestTask[] currentList) { + TestTask taskToEdit = currentList[targetIndexOneIndexed-1]; //-1 because array uses zero indexing + TestTask[] expectedRemainder = TestUtil.editTaskInList(currentList, targetIndexOneIndexed, change, taskToEdit); + commandBox.runCommand("edit " + targetIndexOneIndexed + " " + change); + + //confirm the list now contains all previous tasks except the deleted task + assertTrue(taskListPanel.isListMatching(expectedRemainder)); + + //confirm the result message is correct + assertResultMessage(String.format(MESSAGE_EDIT_TASK_SUCCESS, expectedRemainder[targetIndexOneIndexed-1])); + } + +} diff --git a/src/test/java/seedu/gtd/testutil/TestTask.java b/src/test/java/seedu/gtd/testutil/TestTask.java index b08f14c4b6fb..a983ef564eff 100644 --- a/src/test/java/seedu/gtd/testutil/TestTask.java +++ b/src/test/java/seedu/gtd/testutil/TestTask.java @@ -18,7 +18,15 @@ public TestTask() { tags = new UniqueTagList(); } - public void setName(Name name) { + public TestTask(Name name, DueDate dueDate, Address address, Priority priority, UniqueTagList tags) { + this.name = name; + this.dueDate = dueDate; + this.address = address; + this.priority = priority; + this.tags = new UniqueTagList(tags); // protect internal tags from changes in the arg list + } + + public void setName(Name name) { this.name = name; } diff --git a/src/test/java/seedu/gtd/testutil/TestUtil.java b/src/test/java/seedu/gtd/testutil/TestUtil.java index 714a88e6cc48..6dbc5be4e448 100644 --- a/src/test/java/seedu/gtd/testutil/TestUtil.java +++ b/src/test/java/seedu/gtd/testutil/TestUtil.java @@ -1,6 +1,21 @@ package seedu.gtd.testutil; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import org.loadui.testfx.GuiTest; +import org.testfx.api.FxToolkit; + import com.google.common.io.Files; + import guitests.guihandles.TaskCardHandle; import javafx.geometry.Bounds; import javafx.geometry.Point2D; @@ -10,35 +25,29 @@ import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import junit.framework.AssertionFailedError; -import org.loadui.testfx.GuiTest; -import org.testfx.api.FxToolkit; - import seedu.gtd.TestApp; import seedu.gtd.commons.exceptions.IllegalValueException; import seedu.gtd.commons.util.FileUtil; import seedu.gtd.commons.util.XmlUtil; import seedu.gtd.model.AddressBook; -import seedu.gtd.model.task.*; import seedu.gtd.model.tag.Tag; import seedu.gtd.model.tag.UniqueTagList; +import seedu.gtd.model.task.Address; +import seedu.gtd.model.task.DueDate; +import seedu.gtd.model.task.Name; +import seedu.gtd.model.task.Priority; +import seedu.gtd.model.task.ReadOnlyTask; +import seedu.gtd.model.task.Task; +import seedu.gtd.model.task.UniqueTaskList; import seedu.gtd.storage.XmlSerializableAddressBook; -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeoutException; -import java.util.stream.Collectors; - /** * A utility class for test cases. */ public class TestUtil { - + + //@@author addressbook-level4 + public static String LS = System.lineSeparator(); public static void assertThrows(Class expected, Runnable executable) { @@ -318,7 +327,31 @@ public static TestTask[] addTasksToList(final TestTask[] tasks, TestTask... task listOfTasks.addAll(asList(tasksToAdd)); return listOfTasks.toArray(new TestTask[listOfTasks.size()]); } - + + //@@author A0146130W + + /** + * Edits a task in the array of tasks. + * @param tasks A array of tasks. + * @param tasksToAdd The tasks that are to be appended behind the original array. + * @return The modified array of tasks. + * @throws IllegalValueException + */ + public static TestTask[] editTaskInList(final TestTask[] tasks, int index, String change, TestTask taskToEdit) { + List listOfTasks = asList(tasks); + TestTask taskEditted; + try { + taskEditted = TestUtilParser.editTask(taskToEdit, change); + } catch (IllegalValueException e) { + taskEditted = taskToEdit; + e.printStackTrace(); + } + listOfTasks.set(index-1, taskEditted); + return listOfTasks.toArray(new TestTask[listOfTasks.size()]); + } + + //@@author addressbook-level4 + private static List asList(T[] objs) { List list = new ArrayList<>(); for(T obj : objs) { diff --git a/src/test/java/seedu/gtd/testutil/TestUtilParser.java b/src/test/java/seedu/gtd/testutil/TestUtilParser.java new file mode 100644 index 000000000000..56b83ab9c5bd --- /dev/null +++ b/src/test/java/seedu/gtd/testutil/TestUtilParser.java @@ -0,0 +1,38 @@ +//@@author A0146130W + +package seedu.gtd.testutil; + +import seedu.gtd.commons.exceptions.IllegalValueException; +import seedu.gtd.logic.parser.DateNaturalLanguageProcessor; +import seedu.gtd.logic.parser.NaturalLanguageProcessor; +import seedu.gtd.model.task.Address; +import seedu.gtd.model.task.DueDate; +import seedu.gtd.model.task.Name; +import seedu.gtd.model.task.Priority; + +/** + * A utility class that parses tasks for test cases. + */ +public class TestUtilParser { + + public static TestTask editTask(TestTask task, String change) throws IllegalValueException { + + TestTask newTask = task; + String changeWithoutPrefix = change.substring(2); + String changePrefix = change.substring(0, 2); + System.out.println("From TestUtil Parser: " + changePrefix + " " + changeWithoutPrefix); + + switch(change.substring(0, 2)) { + case "d/": newTask = new TestTask(task.getName(), new DueDate(parseDueDate(changeWithoutPrefix)), task.getAddress(), task.getPriority(), task.getTags()); break; + case "a/": newTask = new TestTask(task.getName(), task.getDueDate(), new Address(changeWithoutPrefix), task.getPriority(), task.getTags()); break; + case "p/": newTask = new TestTask(task.getName(), task.getDueDate(), task.getAddress(), new Priority(changeWithoutPrefix), task.getTags()); break; + default: newTask = new TestTask(new Name(change), task.getDueDate(), task.getAddress(), task.getPriority(), task.getTags()); + } + return newTask; + } + + public static String parseDueDate(String dueDateRaw) { + NaturalLanguageProcessor nlp = new DateNaturalLanguageProcessor(); + return nlp.formatString(dueDateRaw); + } +}