From 7b4408614ee035cd6a918252ed9c0f05fb8733c1 Mon Sep 17 00:00:00 2001 From: qhng Date: Sat, 29 Oct 2016 22:21:17 +0800 Subject: [PATCH 1/4] Add more data to UI for test verification --- .../savvytasker/model/task/ReadOnlyTask.java | 39 +++++++++++++++++++ .../java/seedu/savvytasker/ui/TaskCard.java | 30 +------------- src/main/resources/view/PersonListCard.fxml | 10 ++--- .../guitests/guihandles/TaskCardHandle.java | 10 ++++- 4 files changed, 54 insertions(+), 35 deletions(-) diff --git a/src/main/java/seedu/savvytasker/model/task/ReadOnlyTask.java b/src/main/java/seedu/savvytasker/model/task/ReadOnlyTask.java index f1a256e8aace..9b4df673008c 100644 --- a/src/main/java/seedu/savvytasker/model/task/ReadOnlyTask.java +++ b/src/main/java/seedu/savvytasker/model/task/ReadOnlyTask.java @@ -66,5 +66,44 @@ default String getAsText() { return builder.toString(); } + + /** + * Formats the task as text, showing all task details, formatted for the UI. + */ + default String getTextForUi() { + final StringBuilder builder = new StringBuilder(); + if (getStartDateTime() != null) { + builder.append(" Start: ") + .append(getStartDateTime()) + .append("\n"); + } + if (getEndDateTime() != null) { + builder.append(" End: ") + .append(getEndDateTime()) + .append("\n"); + } + if (getLocation() != null && !getLocation().isEmpty()) { + builder.append(" Location: ") + .append(getLocation()) + .append("\n"); + } + builder.append(" Priority: ") + .append(getPriority()) + .append("\n"); + if (getCategory() != null && !getCategory().isEmpty()) { + builder.append(" Category: ") + .append(getCategory()) + .append("\n"); + } + if (getDescription() != null && !getDescription().isEmpty()) { + builder.append(" Description: ") + .append(getDescription()) + .append("\n"); + } + builder.append(" Archived: ") + .append(isArchived()); + return builder.toString(); + } + } //@@author diff --git a/src/main/java/seedu/savvytasker/ui/TaskCard.java b/src/main/java/seedu/savvytasker/ui/TaskCard.java index b3cc0f963fce..45ac250c1c40 100644 --- a/src/main/java/seedu/savvytasker/ui/TaskCard.java +++ b/src/main/java/seedu/savvytasker/ui/TaskCard.java @@ -1,7 +1,5 @@ package seedu.savvytasker.ui; -import java.util.Date; - import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.Label; @@ -11,7 +9,6 @@ public class TaskCard extends UiPart{ private static final String FXML = "PersonListCard.fxml"; - private static final String EMPTY_FIELD = " - "; @FXML private HBox cardPane; @@ -20,13 +17,7 @@ public class TaskCard extends UiPart{ @FXML private Label id; @FXML - private Label startDate; - @FXML - private Label endDate; - @FXML - private Label description; - @FXML - private Label tags; + private Label details; private ReadOnlyTask task; private int displayedIndex; @@ -45,25 +36,8 @@ public static TaskCard load(ReadOnlyTask task, int displayedIndex){ @FXML public void initialize() { taskName.setText(task.getTaskName()); - Date startDate = task.getStartDateTime(); - if (startDate != null) { - this.startDate.setText(startDate.toString()); - } else { - this.startDate.setText(EMPTY_FIELD); - } - Date endDate = task.getEndDateTime(); - if (endDate != null) { - this.endDate.setText(endDate.toString()); - } else { - this.endDate.setText(EMPTY_FIELD); - } - String description = task.getDescription(); - if (description != null) { - this.description.setText(description); - } else { - this.description.setText(EMPTY_FIELD); - } id.setText(displayedIndex + ". "); + details.setText(task.getTextForUi()); } public HBox getLayout() { diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index 9953ce588beb..2df104257f29 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -12,7 +12,7 @@ - + @@ -26,14 +26,14 @@ - - diff --git a/src/test/java/guitests/guihandles/TaskCardHandle.java b/src/test/java/guitests/guihandles/TaskCardHandle.java index fe32021d1b5f..58771f3d2091 100644 --- a/src/test/java/guitests/guihandles/TaskCardHandle.java +++ b/src/test/java/guitests/guihandles/TaskCardHandle.java @@ -11,6 +11,7 @@ */ public class TaskCardHandle extends GuiHandle { private static final String TASKNAME_FIELD_ID = "#taskName"; + private static final String DETAILS_FIELD_ID = "#details"; private Node node; @@ -26,16 +27,21 @@ protected String getTextFromLabel(String fieldId) { public String getTaskName() { return getTextFromLabel(TASKNAME_FIELD_ID); } + + public String getDetails() { + return getTextFromLabel(DETAILS_FIELD_ID); + } public boolean isSameTask(ReadOnlyTask task) { - return getTaskName().equals(task.getTaskName()); + return getTaskName().equals(task.getTaskName()) && getDetails().equals(task.getTextForUi()); } @Override public boolean equals(Object obj) { if(obj instanceof TaskCardHandle) { TaskCardHandle handle = (TaskCardHandle) obj; - return getTaskName().equals(handle.getTaskName()); //TODO: compare the rest + return getTaskName().equals(handle.getTaskName()) && + getDetails().equals(handle.getDetails()); } return super.equals(obj); } From 6e22ca90b4acb05de04a87be74af1e0797b8218f Mon Sep 17 00:00:00 2001 From: qhng Date: Sat, 29 Oct 2016 22:21:27 +0800 Subject: [PATCH 2/4] Increase test coverage --- .../commons/util/SmartDefaultDates.java | 14 +- .../logic/commands/AddCommand.java | 3 - .../logic/commands/DeleteCommand.java | 2 +- .../logic/commands/ListCommand.java | 2 - .../logic/commands/MarkCommand.java | 3 - .../logic/commands/UnmarkCommand.java | 5 +- .../java/seedu/savvytasker/model/Model.java | 7 +- .../seedu/savvytasker/model/ModelManager.java | 23 +- .../seedu/savvytasker/model/SavvyTasker.java | 22 +- src/test/java/guitests/AddCommandTest.java | 14 +- src/test/java/guitests/FindCommandTest.java | 6 +- src/test/java/guitests/ListCommandTest.java | 7 +- src/test/java/guitests/ModifyCommandTest.java | 66 +++++ .../commons/util/SmartDefaultDatesTest.java | 245 ++++++++++++++++++ .../savvytasker/logic/LogicManagerTest.java | 1 - .../savvytasker/model/task/TaskListTest.java | 90 +++++++ .../seedu/savvytasker/testutil/TestTask.java | 25 ++ .../seedu/savvytasker/testutil/TestUtil.java | 9 +- .../testutil/TypicalTestTasks.java | 9 +- 19 files changed, 491 insertions(+), 62 deletions(-) create mode 100644 src/test/java/guitests/ModifyCommandTest.java create mode 100644 src/test/java/seedu/savvytasker/commons/util/SmartDefaultDatesTest.java create mode 100644 src/test/java/seedu/savvytasker/model/task/TaskListTest.java diff --git a/src/main/java/seedu/savvytasker/commons/util/SmartDefaultDates.java b/src/main/java/seedu/savvytasker/commons/util/SmartDefaultDates.java index cf6133f1c763..c7206ebdf7c7 100644 --- a/src/main/java/seedu/savvytasker/commons/util/SmartDefaultDates.java +++ b/src/main/java/seedu/savvytasker/commons/util/SmartDefaultDates.java @@ -66,6 +66,7 @@ public Date getEnd(InferredDate endDateTime) { calendar.set(Calendar.HOUR_OF_DAY, 23); calendar.set(Calendar.MINUTE, 59); calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MILLISECOND, 0); } return calendar.getTime(); } @@ -85,7 +86,13 @@ private void parseEnd(InferredDate endDateTime) { calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); this.startDateTime = calendar.getTime(); + + if (this.startDateTime.compareTo(this.endDateTime) > 0) { + // end date is before today, leave start date as null + this.startDateTime = null; + } } @@ -114,6 +121,7 @@ public Date getStart(InferredDate startDateTime) { calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); } return calendar.getTime(); } @@ -134,6 +142,7 @@ private void parseStart(InferredDate startDateTime) { calendar.set(Calendar.HOUR_OF_DAY, 23); calendar.set(Calendar.MINUTE, 59); calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MILLISECOND, 0); this.endDateTime = calendar.getTime(); } @@ -149,11 +158,6 @@ private void parseStartAndEnd(InferredDate startDateTime, InferredDate endDateTi Date end = getEnd(endDateTime); this.startDateTime = start; this.endDateTime = end; - if (this.startDateTime.compareTo(this.endDateTime) > 0) { - calendar.setTime(this.endDateTime); - calendar.add(Calendar.DATE, 7); - this.endDateTime = calendar.getTime(); - } } public Date getStartDate() { diff --git a/src/main/java/seedu/savvytasker/logic/commands/AddCommand.java b/src/main/java/seedu/savvytasker/logic/commands/AddCommand.java index 9d4fbdd1a2b6..83f6fc639954 100644 --- a/src/main/java/seedu/savvytasker/logic/commands/AddCommand.java +++ b/src/main/java/seedu/savvytasker/logic/commands/AddCommand.java @@ -11,7 +11,6 @@ import seedu.savvytasker.model.task.ReadOnlyTask; import seedu.savvytasker.model.task.RecurrenceType; import seedu.savvytasker.model.task.Task; -import seedu.savvytasker.model.task.TaskList.DuplicateTaskException; import seedu.savvytasker.model.task.TaskList.InvalidDateException; import seedu.savvytasker.model.task.TaskList.TaskNotFoundException; @@ -105,8 +104,6 @@ public CommandResult execute() { // GUI should never ever get here } return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); - } catch (DuplicateTaskException e) { - return new CommandResult(MESSAGE_DUPLICATE_TASK); } catch (InvalidDateException ex) { return new CommandResult(MESSAGE_INVALID_START_END); } diff --git a/src/main/java/seedu/savvytasker/logic/commands/DeleteCommand.java b/src/main/java/seedu/savvytasker/logic/commands/DeleteCommand.java index 418a864dfce2..40a64c594447 100644 --- a/src/main/java/seedu/savvytasker/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/savvytasker/logic/commands/DeleteCommand.java @@ -58,7 +58,7 @@ public CommandResult execute() { //tasksToUndo.add((Task)taskToDelete); resultSb.append(String.format(MESSAGE_DELETE_TASK_SUCCESS, taskToDelete)); } - } catch (TaskNotFoundException pnfe) { + } catch (TaskNotFoundException tnfe) { assert false : "The target task cannot be missing"; } diff --git a/src/main/java/seedu/savvytasker/logic/commands/ListCommand.java b/src/main/java/seedu/savvytasker/logic/commands/ListCommand.java index 9f297bb218f3..211efcac0488 100644 --- a/src/main/java/seedu/savvytasker/logic/commands/ListCommand.java +++ b/src/main/java/seedu/savvytasker/logic/commands/ListCommand.java @@ -46,8 +46,6 @@ public CommandResult execute() { case Archived: model.updateFilteredListToShowArchived(); break; - default: - assert false; // should not reach here } return new CommandResult(getMessageForTaskListShownSummary(model.getFilteredTaskList().size())); } diff --git a/src/main/java/seedu/savvytasker/logic/commands/MarkCommand.java b/src/main/java/seedu/savvytasker/logic/commands/MarkCommand.java index d20314e38d9f..f1d013b9ae9a 100644 --- a/src/main/java/seedu/savvytasker/logic/commands/MarkCommand.java +++ b/src/main/java/seedu/savvytasker/logic/commands/MarkCommand.java @@ -8,7 +8,6 @@ import seedu.savvytasker.model.SavvyTasker; import seedu.savvytasker.model.task.ReadOnlyTask; import seedu.savvytasker.model.task.Task; -import seedu.savvytasker.model.task.TaskList.DuplicateTaskException; import seedu.savvytasker.model.task.TaskList.InvalidDateException; import seedu.savvytasker.model.task.TaskList.TaskNotFoundException; @@ -63,8 +62,6 @@ public CommandResult execute() { } } catch (TaskNotFoundException pnfe) { assert false : "The target task cannot be missing"; - } catch (DuplicateTaskException e) { - e.printStackTrace(); } catch (InvalidDateException e) { assert false : "The target task should be valid, only the archived flag is set"; } diff --git a/src/main/java/seedu/savvytasker/logic/commands/UnmarkCommand.java b/src/main/java/seedu/savvytasker/logic/commands/UnmarkCommand.java index f3d360b01793..2b2e70f126f9 100644 --- a/src/main/java/seedu/savvytasker/logic/commands/UnmarkCommand.java +++ b/src/main/java/seedu/savvytasker/logic/commands/UnmarkCommand.java @@ -8,7 +8,6 @@ import seedu.savvytasker.model.SavvyTasker; import seedu.savvytasker.model.task.ReadOnlyTask; import seedu.savvytasker.model.task.Task; -import seedu.savvytasker.model.task.TaskList.DuplicateTaskException; import seedu.savvytasker.model.task.TaskList.InvalidDateException; import seedu.savvytasker.model.task.TaskList.TaskNotFoundException; @@ -64,9 +63,7 @@ public CommandResult execute() { } } catch (TaskNotFoundException pnfe) { assert false : "The target task cannot be missing"; - } catch (DuplicateTaskException e) { - e.printStackTrace(); - }catch (InvalidDateException e) { + } catch (InvalidDateException e) { assert false : "The target task should be valid, only the archived flag is set"; } return new CommandResult(resultSb.toString()); diff --git a/src/main/java/seedu/savvytasker/model/Model.java b/src/main/java/seedu/savvytasker/model/Model.java index 6628f3ff1e80..4702191da93c 100644 --- a/src/main/java/seedu/savvytasker/model/Model.java +++ b/src/main/java/seedu/savvytasker/model/Model.java @@ -44,20 +44,17 @@ public interface Model { * @throws {@link InvalidDateException} if the end date is earlier than the start date * @return Returns a Task if the add operation is successful, an exception is thrown otherwise. * */ - Task addTask(Task task) throws DuplicateTaskException, InvalidDateException; + Task addTask(Task task) throws InvalidDateException; /** Adds the given Task as a recurring task. The task's recurrence type must not be null. * @throws {@link DuplicateTaskException} if a duplicate is found * @throws {@link InvalidDateException} if the end date is earlier than the start date * @return Returns the list of Tasks added if the add operation is successful, an exception is thrown otherwise. * */ - LinkedList addRecurringTask(Task task) throws DuplicateTaskException, InvalidDateException; + LinkedList addRecurringTask(Task task) throws InvalidDateException; /** Returns the filtered task list as an {@code UnmodifiableObservableList} */ UnmodifiableObservableList getFilteredTaskList(); - - /** Returns the filtered task list as an {@code UnmodifiableObservableList} */ - UnmodifiableObservableList getFilteredTaskListTask(); /** Updates the filter of the filtered task list to show all active tasks sorted by due date */ void updateFilteredListToShowActiveSortedByDueDate(); diff --git a/src/main/java/seedu/savvytasker/model/ModelManager.java b/src/main/java/seedu/savvytasker/model/ModelManager.java index bd5edac68632..129c4c6a53c9 100644 --- a/src/main/java/seedu/savvytasker/model/ModelManager.java +++ b/src/main/java/seedu/savvytasker/model/ModelManager.java @@ -23,7 +23,6 @@ import seedu.savvytasker.model.task.FindType; import seedu.savvytasker.model.task.ReadOnlyTask; import seedu.savvytasker.model.task.Task; -import seedu.savvytasker.model.task.TaskList.DuplicateTaskException; import seedu.savvytasker.model.task.TaskList.InvalidDateException; import seedu.savvytasker.model.task.TaskList.TaskNotFoundException; @@ -110,7 +109,7 @@ public synchronized Task modifyTask(ReadOnlyTask target, Task replacement) throw } @Override - public synchronized Task addTask(Task t) throws DuplicateTaskException, InvalidDateException { + public synchronized Task addTask(Task t) throws InvalidDateException { Task taskAdded = savvyTasker.addTask(t); updateFilteredListToShowActive(); indicateSavvyTaskerChanged(); @@ -118,7 +117,7 @@ public synchronized Task addTask(Task t) throws DuplicateTaskException, InvalidD } @Override - public synchronized LinkedList addRecurringTask(Task recurringTask) throws DuplicateTaskException, InvalidDateException { + public synchronized LinkedList addRecurringTask(Task recurringTask) throws InvalidDateException { LinkedList recurringTasks = savvyTasker.addRecurringTasks(recurringTask); updateFilteredListToShowActive(); indicateSavvyTaskerChanged(); @@ -149,11 +148,6 @@ public synchronized void removeAliasSymbol(AliasSymbol symbol) throws SymbolKeyw public UnmodifiableObservableList getFilteredTaskList() { return new UnmodifiableObservableList(sortedAndFilteredTasks); } - - @Override - public UnmodifiableObservableList getFilteredTaskListTask() { - return new UnmodifiableObservableList(sortedAndFilteredTasks); - } @Override public void updateFilteredListToShowActiveSortedByDueDate() { @@ -475,18 +469,7 @@ public int compare(Task task1, Task task2) { else if (task1 == null) return 1; else if (task2 == null) return -1; else { - // Priority Level can be nulls - // Check for existence of priorityLevel before comparing - if (task1.getPriority() == null && - task2.getPriority() == null) { - return 0; - } else if (task1.getPriority() == null) { - return 1; - } else if (task2.getPriority() == null) { - return -1; - } else { - return task2.getPriority().compareTo(task1.getPriority()); - } + return task2.getPriority().compareTo(task1.getPriority()); } } diff --git a/src/main/java/seedu/savvytasker/model/SavvyTasker.java b/src/main/java/seedu/savvytasker/model/SavvyTasker.java index 9bfe2fbb39db..22d8d8774758 100644 --- a/src/main/java/seedu/savvytasker/model/SavvyTasker.java +++ b/src/main/java/seedu/savvytasker/model/SavvyTasker.java @@ -75,22 +75,26 @@ public void resetData(ReadOnlySavvyTasker newData) { /** * Adds a task to savvy tasker. - * @throws {@link DuplicateTaskException} if a duplicate is found * @throws {@link InvalidDateException} if the end date is earlier than the start date * @return Returns the task added if the operation succeeds, an exception is thrown otherwise. */ - public Task addTask(Task t) throws DuplicateTaskException, InvalidDateException { + public Task addTask(Task t) throws InvalidDateException { + // guarantees unique ID t.setId(tasks.getNextId()); - return tasks.add(t); + try { + return tasks.add(t); + } catch (DuplicateTaskException e) { + // should never get here. + return null; + } } /** * Adds a group of recurring tasks to savvy tasker. - * @throws {@link DuplicateTaskException} if a duplicate is found * @throws {@link InvalidDateException} if the end date is earlier than the start date * @return Returns the list of recurring tasks if the operation succeeds, an exception is thrown otherwise */ - public LinkedList addRecurringTasks(Task recurringTask) throws DuplicateTaskException, InvalidDateException { + public LinkedList addRecurringTasks(Task recurringTask) throws InvalidDateException { LinkedList tasksToAdd = createRecurringTasks(recurringTask, recurringTask.getRecurringType(), recurringTask.getNumberOfRecurrence()); @@ -102,7 +106,12 @@ public LinkedList addRecurringTasks(Task recurringTask) throws DuplicateTa // if the start/end dates are invalid, // the first task to be added will fail immediately, // subsequent tasks will not be added - tasks.add(itr.next()); + try { + tasks.add(itr.next()); + } catch (DuplicateTaskException e) { + // should never get here. + return null; + } } return tasksToAdd; } @@ -123,6 +132,7 @@ private LinkedList createRecurringTasks(Task recurringTask, RecurrenceType for (int i = 0; i < numberOfRecurrences; ++i) { Task t = recurringTask.clone(); + // guarantees uniqueness t.setId(tasks.getNextId()); listOfTasks.add(setDatesForRecurringType(t, recurringType, i)); } diff --git a/src/test/java/guitests/AddCommandTest.java b/src/test/java/guitests/AddCommandTest.java index 41984bea99f6..2eddf6c8044e 100644 --- a/src/test/java/guitests/AddCommandTest.java +++ b/src/test/java/guitests/AddCommandTest.java @@ -4,7 +4,7 @@ import org.junit.Test; -import seedu.savvytasker.commons.core.Messages; +import seedu.savvytasker.logic.commands.AddCommand; import seedu.savvytasker.logic.commands.HelpCommand; import seedu.savvytasker.testutil.TestTask; import seedu.savvytasker.testutil.TestUtil; @@ -35,6 +35,16 @@ public void add() { //invalid command commandBox.runCommand("adds Bad Command Task"); assertResultMessage(String.format(MESSAGE_UNKNOWN_COMMAND, HelpCommand.MESSAGE_USAGE)); + + //invalid start end date + commandBox.runCommand("add bad start-end pair s/31-12-2015 e/30-12-2015"); + assertResultMessage(String.format(AddCommand.MESSAGE_INVALID_START_END)); + + commandBox.runCommand("clear"); + //add recurring tasks + commandBox.runCommand("add recurring yall s/04-11-2016 e/05-11-2016 l/home r/daily p/high n/5 c/recurs d/AHAHA"); + assertResultMessage("New task added: Id: 0 Task Name: recurring yall Archived: false Start: Fri Nov 04 00:00:00 SGT 2016 End: Sat Nov 05 23:59:59 SGT 2016 Location: home Priority: High Category: recurs Description: AHAHA"); + } private void assertAddSuccess(TestTask taskToAdd, TestTask... currentList) { @@ -44,7 +54,7 @@ private void assertAddSuccess(TestTask taskToAdd, TestTask... currentList) { TaskCardHandle addedCard = taskListPanel.navigateToTask(taskToAdd.getTaskName()); assertMatching(taskToAdd, addedCard); - //confirm the list now contains all previous persons plus the new person + //confirm the list now contains all previous tasks plus the new task TestTask[] expectedList = TestUtil.addTasksToList(currentList, taskToAdd); assertTrue(taskListPanel.isListMatching(expectedList)); } diff --git a/src/test/java/guitests/FindCommandTest.java b/src/test/java/guitests/FindCommandTest.java index fc884bb9e436..2192ade95437 100644 --- a/src/test/java/guitests/FindCommandTest.java +++ b/src/test/java/guitests/FindCommandTest.java @@ -2,7 +2,6 @@ import org.junit.Test; -import seedu.savvytasker.commons.core.Messages; import seedu.savvytasker.logic.commands.HelpCommand; import seedu.savvytasker.testutil.TestTask; @@ -37,6 +36,11 @@ public void find_nonEmptyList_byFullMatch() { public void find_nonEmptyList_byExactMatch() { assertFindResult("find t/exact Nearer Due Task", td.nearerDue); // one matching result only } + + @Test + public void find_nonEmptyList_byCategory() { + assertFindResult("find t/category priority", td.highPriority, td.medPriority, td.lowPriority); // matching 3 results + } @Test public void find_emptyList(){ diff --git a/src/test/java/guitests/ListCommandTest.java b/src/test/java/guitests/ListCommandTest.java index 990f475711c9..5b3caf51bed8 100644 --- a/src/test/java/guitests/ListCommandTest.java +++ b/src/test/java/guitests/ListCommandTest.java @@ -2,7 +2,6 @@ import org.junit.Test; -import seedu.savvytasker.commons.core.Messages; import seedu.savvytasker.logic.commands.HelpCommand; import seedu.savvytasker.testutil.TestTask; @@ -23,6 +22,12 @@ public void list_nonEmptyList() { td.highPriority, td.medPriority, td.lowPriority); } + @Test + public void list_nonEmptyList_byInvalidSwitch() { + commandBox.runCommand("list t/badswitch"); + assertResultMessage("LIST_TYPE: Unknown type \'badswitch\'"); + } + @Test public void list_nonEmptyList_byDueDate() { // covered by list_nonEmptyList() diff --git a/src/test/java/guitests/ModifyCommandTest.java b/src/test/java/guitests/ModifyCommandTest.java new file mode 100644 index 000000000000..8cdbed8ce889 --- /dev/null +++ b/src/test/java/guitests/ModifyCommandTest.java @@ -0,0 +1,66 @@ +package guitests; + +import guitests.guihandles.TaskCardHandle; + +import org.junit.Test; + +import seedu.savvytasker.commons.core.Messages; +import seedu.savvytasker.testutil.TestTask; +import seedu.savvytasker.testutil.TestUtil; + +import static org.junit.Assert.assertTrue; + +import java.text.SimpleDateFormat; +import java.util.Date; + +//@@author A0139915W +public class ModifyCommandTest extends SavvyTaskerGuiTest { + + @Test + public void add() { + //modify task + TestTask[] currentList = td.getTypicalTasks(); + TestTask taskToModify = currentList[0]; + taskToModify.setStartDateTime(getDate("30/12/2016")); + taskToModify.setEndDateTime(getDate("31/12/2016")); + assertModifySuccess("modify 1 s/30-12-2016 e/31-12-2016", taskToModify, currentList); + currentList = TestUtil.replaceTaskFromList(currentList, taskToModify); + + taskToModify.setStartDateTime(null); + taskToModify.setEndDateTime(null); + assertModifySuccess("modify 1 s/ e/", taskToModify, currentList); + currentList = TestUtil.replaceTaskFromList(currentList, taskToModify); + + //modify invalid index + commandBox.runCommand("modify " + currentList.length + "1" + " s/sat"); + assertResultMessage(String.format(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX)); + + //modify with invalid end date + commandBox.runCommand("modify 1 s/31-12-2016 e/30-12-2016"); + assertResultMessage(String.format(Messages.MESSAGE_INVALID_START_END)); + } + + private void assertModifySuccess(String command, TestTask taskToModify, TestTask... currentList) { + commandBox.runCommand(command); + + //confirm the new card contains the right data + TaskCardHandle modifiedCard = taskListPanel.navigateToTask(taskToModify.getTaskName()); + assertMatching(taskToModify, modifiedCard); + + //confirm the list now contains all previous persons plus the new person + TestTask[] expectedList = TestUtil.replaceTaskFromList(currentList, taskToModify); + assertTrue(taskListPanel.isListMatching(expectedList)); + } + + private SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy"); + private Date getDate(String ddmmyyyy) { + try { + return format.parse(ddmmyyyy); + } catch (Exception e) { + assert false; //should not get an invalid date.... + } + return null; + } + +} +//@@author diff --git a/src/test/java/seedu/savvytasker/commons/util/SmartDefaultDatesTest.java b/src/test/java/seedu/savvytasker/commons/util/SmartDefaultDatesTest.java new file mode 100644 index 000000000000..eea2059370c2 --- /dev/null +++ b/src/test/java/seedu/savvytasker/commons/util/SmartDefaultDatesTest.java @@ -0,0 +1,245 @@ +package seedu.savvytasker.commons.util; + +import org.junit.Test; + +import seedu.savvytasker.logic.parser.DateParser; +import seedu.savvytasker.logic.parser.DateParser.InferredDate; +import seedu.savvytasker.logic.parser.ParseException; + +import static org.junit.Assert.assertEquals; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +public class SmartDefaultDatesTest { + + @Test + public void smartDefaultDates_parseStart() { + DateParser dateParser = new DateParser(); + InferredDate inferredStart = null; + try { + //use MM-dd-yyyy + inferredStart = dateParser.parseSingle("12/31/2016"); + } catch (ParseException e) { + assert false; //won't get here + } + InferredDate inferredEnd = null; + SmartDefaultDates sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // specifying only start date, assumed to on the given date at 12am + // and to end on the given date at 2359:59 + Date expectedStartTime = getDate("31/12/2016 000000"); + Date expectedEndTime = getDate("31/12/2016 235959"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + Date today = today(0, 0); + try { + //use MM-dd-yyyy + inferredStart = dateParser.parseSingle("3pm"); + } catch (ParseException e) { + assert false; //won't get here + } + sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // specifying only start time, assumed to start today at the given time + // and to end today 2359:59 + expectedStartTime = getDate(sdf.format(today) + " 150000"); + expectedEndTime = getDate(sdf.format(today) + " 235959"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + } + + @Test + public void smartDefaultDates_parseEnd() { + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + Date today = today(0, 0); + DateParser dateParser = new DateParser(); + InferredDate inferredEnd = null; + try { + //use MM-dd-yyyy + inferredEnd = dateParser.parseSingle("12/31/2016"); + } catch (ParseException e) { + assert false; //won't get here + } + InferredDate inferredStart = null; + SmartDefaultDates sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // specified only the end date, assumed to start today at 12am + // and to end on the given date at 2359:59 + Date expectedStartTime = getDate(sdf.format(today) + " 000000"); + Date expectedEndTime = getDate("31/12/2016 235959"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + + try { + //use MM-dd-yyyy + inferredEnd = dateParser.parseSingle("3pm"); + } catch (ParseException e) { + assert false; //won't get here + } + sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // specified only the end time, assumed to start today at 12am + // and to end at the given time today + expectedStartTime = getDate(sdf.format(today) + " 000000"); + expectedEndTime = getDate(sdf.format(today) + " 150000"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + + + try { + //use MM-dd-yyyy + inferredEnd = dateParser.parseSingle("12/31/2000"); + } catch (ParseException e) { + assert false; //won't get here + } + sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // specified only the end date in the past, start date will be null + // and to end on the given date at 2359:59 + expectedStartTime = null; + expectedEndTime = getDate("31/12/2000 235959"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + } + + @Test + public void smartDefaultDates_parseStartEnd() { + // START_TIME + // date not supplied -> today + // time not supplied -> 0000 + // END_TIME + // date not supplied -> today + // time not supplied -> 2359 + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + Date today = today(0, 0); + DateParser dateParser = new DateParser(); + InferredDate inferredStart = null; + InferredDate inferredEnd = null; + try { + //use MM-dd-yyyy + inferredStart = dateParser.parseSingle("12/31/2016"); + inferredEnd = dateParser.parseSingle("12/31/2016"); + } catch (ParseException e) { + assert false; //won't get here + } + SmartDefaultDates sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // no time supplied for start and end + // start defaults to 0000 + // end defaults to 2359:59 + Date expectedStartTime = getDate("31/12/2016 000000"); + Date expectedEndTime = getDate("31/12/2016 235959"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + + inferredStart = null; + inferredEnd = null; + try { + //use MM-dd-yyyy + inferredStart = dateParser.parseSingle("12/31/2016"); + inferredEnd = dateParser.parseSingle("12/30/2016"); + } catch (ParseException e) { + assert false; //won't get here + } + sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // no time supplied for start and end, end date earlier than start + // start defaults to 0000 + // end defaults to 2359:59 + // no restrictions imposed on end time earlier than start time + expectedStartTime = getDate("31/12/2016 000000"); + expectedEndTime = getDate("30/12/2016 235959"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + + inferredStart = null; + inferredEnd = null; + try { + //use MM-dd-yyyy + inferredStart = dateParser.parseSingle("10am"); + inferredEnd = dateParser.parseSingle("10pm"); + } catch (ParseException e) { + assert false; //won't get here + } + sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // no date supplied for start and end + // start and end defaults to the given time today + expectedStartTime = getDate(sdf.format(today) + " 100000"); + expectedEndTime = getDate(sdf.format(today) + " 220000"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + + inferredStart = null; + inferredEnd = null; + try { + //use MM-dd-yyyy + inferredStart = dateParser.parseSingle("10pm"); + inferredEnd = dateParser.parseSingle("10am"); + } catch (ParseException e) { + assert false; //won't get here + } + sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // no date supplied for start and end, end time ends before start time + // start and end defaults to the given time today + // no restrictions imposed on end time being earlier + expectedStartTime = getDate(sdf.format(today) + " 220000"); + expectedEndTime = getDate(sdf.format(today) + " 100000"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + } + + @Test + public void smartDefaultDates_defaultParse() { + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + Date today = today(0, 0); + DateParser dateParser = new DateParser(); + SmartDefaultDates sdd = new SmartDefaultDates(null, null); + Date actualStart = sdd.getStart(dateParser.new InferredDate(new Date(), true, true)); + Date actualEnd = sdd.getEnd(dateParser.new InferredDate(new Date(), true, true)); + Date expectedStart = null; + Date expectedEnd = null; + assertEquals(expectedStart, actualStart); + assertEquals(expectedEnd, actualEnd); + + try { + //use MM-dd-yyyy + actualStart = sdd.getStart(dateParser.parseSingle("10pm")); + actualEnd = sdd.getEnd(dateParser.parseSingle("10am")); + } catch (ParseException e) { + assert false; //won't get here + } + expectedStart = getDate(sdf.format(today) + " 220000"); + expectedEnd = getDate(sdf.format(today) + " 100000"); + assertEquals(expectedStart, actualStart); + assertEquals(expectedEnd, actualEnd); + + try { + //use MM-dd-yyyy + actualStart = sdd.getStart(dateParser.parseSingle("12/31/2016")); + actualEnd = sdd.getEnd(dateParser.parseSingle("12/31/2016")); + } catch (ParseException e) { + assert false; //won't get here + } + expectedStart = getDate("31/12/2016 000000"); + expectedEnd = getDate("31/12/2016 235959"); + assertEquals(expectedStart, actualStart); + assertEquals(expectedEnd, actualEnd); + } + + private SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy HHmmss"); + private Date getDate(String ddmmyyyy) { + try { + return format.parse(ddmmyyyy); + } catch (Exception e) { + assert false; //should not get an invalid date.... + } + return null; + } + + private Date today(int hours_24, int minute_60) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(new Date()); + calendar.set(Calendar.HOUR_OF_DAY, hours_24); + calendar.set(Calendar.MINUTE, minute_60); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } +} diff --git a/src/test/java/seedu/savvytasker/logic/LogicManagerTest.java b/src/test/java/seedu/savvytasker/logic/LogicManagerTest.java index eec2e4ca6fd3..735acd1cc75e 100644 --- a/src/test/java/seedu/savvytasker/logic/LogicManagerTest.java +++ b/src/test/java/seedu/savvytasker/logic/LogicManagerTest.java @@ -196,7 +196,6 @@ private void assertIncorrectIndexFormatBehaviorForCommand(String commandWord, St // the following commands outputs a different expected message dealing with // invalid indices. expectedMessage = String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.COMMAND_FORMAT) + ": " + IndexParser.INDEX_MUST_BE_POSITIVE; - //assertCommandBehavior(commandWord + " +1", expectedMessage); //index should be unsigned [NOT SUPPORTED] assertCommandBehavior(commandWord + " -1", expectedMessage); //index should be unsigned assertCommandBehavior(commandWord + " 0", expectedMessage); //index cannot be 0 assertCommandBehavior(commandWord + " not_a_number", expectedMessage); diff --git a/src/test/java/seedu/savvytasker/model/task/TaskListTest.java b/src/test/java/seedu/savvytasker/model/task/TaskListTest.java new file mode 100644 index 000000000000..9f3a5ab2bafa --- /dev/null +++ b/src/test/java/seedu/savvytasker/model/task/TaskListTest.java @@ -0,0 +1,90 @@ +package seedu.savvytasker.model.task; + +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import seedu.savvytasker.model.task.TaskList; +import seedu.savvytasker.model.task.TaskList.DuplicateTaskException; +import seedu.savvytasker.model.task.TaskList.InvalidDateException; +import seedu.savvytasker.model.task.TaskList.TaskNotFoundException; + +import static org.junit.Assert.assertEquals; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.junit.Rule; + + +public class TaskListTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void taskList_addDuplicate() throws DuplicateTaskException, InvalidDateException { + thrown.expect(DuplicateTaskException.class); + TaskList tasks = new TaskList(); + Task t = new Task("Test Task"); + t.setId(1); + tasks.add(t); // passes + assertEquals(1, tasks.getInternalList().size()); + tasks.add(t); // fails + } + + @Test + public void taskList_addInvalidDate() throws DuplicateTaskException, InvalidDateException { + thrown.expect(InvalidDateException.class); + TaskList tasks = new TaskList(); + Task t = new Task("Test Task"); + t.setId(1); + t.setStartDateTime(getDate("31/12/2016")); + t.setEndDateTime(getDate("31/12/2015")); + tasks.add(t); // fails, end date earlier than start date + } + + @Test + public void taskList_removeNonExistent() throws TaskNotFoundException { + thrown.expect(TaskNotFoundException.class); + TaskList tasks = new TaskList(); + Task t = new Task("Test Task"); + t.setId(1); + assertEquals(0, tasks.getInternalList().size()); + tasks.remove(t); // fails + } + + @Test + public void taskList_replaceNonExistent() throws TaskNotFoundException, InvalidDateException { + thrown.expect(TaskNotFoundException.class); + TaskList tasks = new TaskList(); + Task t = new Task("Test Task"); + t.setId(1); + assertEquals(0, tasks.getInternalList().size()); + tasks.replace(t, t); // fails + } + + @Test + public void taskList_replaceInvalidDate() throws TaskNotFoundException, InvalidDateException, DuplicateTaskException { + thrown.expect(InvalidDateException.class); + TaskList tasks = new TaskList(); + Task t = new Task("Test Task"); + t.setId(1); + t.setStartDateTime(getDate("30/12/2016")); + t.setEndDateTime(getDate("31/12/2016")); + tasks.add(t); + assertEquals(1, tasks.getInternalList().size()); + t.setStartDateTime(getDate("31/12/2016")); + t.setEndDateTime(getDate("31/12/2015")); + tasks.replace(t, t); // fails, end date earlier than start date + } + + private SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy"); + private Date getDate(String ddmmyyyy) { + try { + return format.parse(ddmmyyyy); + } catch (Exception e) { + assert false; //should not get an invalid date.... + } + return null; + } +} diff --git a/src/test/java/seedu/savvytasker/testutil/TestTask.java b/src/test/java/seedu/savvytasker/testutil/TestTask.java index baf28be569da..40b75a26f5da 100644 --- a/src/test/java/seedu/savvytasker/testutil/TestTask.java +++ b/src/test/java/seedu/savvytasker/testutil/TestTask.java @@ -1,5 +1,6 @@ package seedu.savvytasker.testutil; +import java.text.SimpleDateFormat; import java.util.Date; import seedu.savvytasker.model.task.PriorityLevel; @@ -151,8 +152,32 @@ public String toString() { } public String getAddCommand() { + SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy HHmm"); StringBuilder sb = new StringBuilder(); sb.append("add " + this.getTaskName()); + if (startDateTime != null) { + sb.append(" s/ ").append(sdf.format(startDateTime)); + } + if (endDateTime != null) { + sb.append(" e/ ").append(sdf.format(endDateTime)); + } + if (location != null && !location.isEmpty()) { + sb.append(" l/ ").append(location); + } + if (priority != null && priority != PriorityLevel.Medium) { + // p/ defaults to medium, if set to medium, take as non-existent + sb.append(" p/ ").append(priority.toString()); + } + if (recurringType != null && recurringType != RecurrenceType.None) { + // r/ defaults to none, if set to none, take as non-existent + sb.append(" r/ ").append(recurringType.toString()); + } + if (category != null && !category.isEmpty()) { + sb.append(" c/ ").append(category); + } + if (description != null && !description.isEmpty()) { + sb.append(" d/ ").append(description); + } return sb.toString(); } } diff --git a/src/test/java/seedu/savvytasker/testutil/TestUtil.java b/src/test/java/seedu/savvytasker/testutil/TestUtil.java index a6ca044f7c42..348a28c5e37b 100644 --- a/src/test/java/seedu/savvytasker/testutil/TestUtil.java +++ b/src/test/java/seedu/savvytasker/testutil/TestUtil.java @@ -280,8 +280,13 @@ public static TestTask[] removeTaskFromList(final TestTask[] list, int targetInd * @param index The index of the task to be replaced. * @return */ - public static TestTask[] replaceTaskFromList(TestTask[] tasks, TestTask task, int index) { - tasks[index] = task; + public static TestTask[] replaceTaskFromList(TestTask[] tasks, TestTask task) { + for (int i = 0; i < tasks.length; ++i) { + if (tasks[i].getId() == task.getId()) { + tasks[i] = task; + break; + } + } return tasks; } diff --git a/src/test/java/seedu/savvytasker/testutil/TypicalTestTasks.java b/src/test/java/seedu/savvytasker/testutil/TypicalTestTasks.java index 2b4569326b92..31f80cda0527 100644 --- a/src/test/java/seedu/savvytasker/testutil/TypicalTestTasks.java +++ b/src/test/java/seedu/savvytasker/testutil/TypicalTestTasks.java @@ -7,7 +7,6 @@ import seedu.savvytasker.model.SavvyTasker; import seedu.savvytasker.model.task.PriorityLevel; import seedu.savvytasker.model.task.Task; -import seedu.savvytasker.model.task.TaskList.DuplicateTaskException; import seedu.savvytasker.model.task.TaskList.InvalidDateException; //@@author A0139915W @@ -23,11 +22,11 @@ public class TypicalTestTasks { public TypicalTestTasks() { try { highPriority = new TaskBuilder().withId(1).withTaskName("High Priority Task") - .withPriority(PriorityLevel.High).build(); + .withPriority(PriorityLevel.High).withCategory("priority").build(); medPriority = new TaskBuilder().withId(2).withTaskName("Medium Priority Task") - .withPriority(PriorityLevel.Medium).build(); + .withPriority(PriorityLevel.Medium).withCategory("priority").build(); lowPriority = new TaskBuilder().withId(3).withTaskName("Low Priority Task") - .withPriority(PriorityLevel.Low).build(); + .withPriority(PriorityLevel.Low).withCategory("priority").build(); furthestDue = new TaskBuilder().withId(4).withTaskName("Furthest Due Task") .withEndDateTime(getDate("01/12/2016")).build(); nearerDue = new TaskBuilder().withId(5).withTaskName("Nearer Due Task") @@ -59,8 +58,6 @@ public static void loadSavvyTaskerWithSampleData(SavvyTasker st) { st.addTask(new Task(td.notSoNearerDue)); st.addTask(new Task(td.earliestDue)); st.addTask(new Task(td.longDue)); - } catch (DuplicateTaskException e) { - assert false : "not possible"; } catch (InvalidDateException e) { assert false : "not possible"; } From bb2137bedc068d296b74711d4bfdd60ca498b582 Mon Sep 17 00:00:00 2001 From: qhng Date: Sat, 29 Oct 2016 22:39:17 +0800 Subject: [PATCH 3/4] Include collate tool --- Collate-TUI.jar | Bin 0 -> 46217 bytes collate.bat | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 Collate-TUI.jar create mode 100644 collate.bat diff --git a/Collate-TUI.jar b/Collate-TUI.jar new file mode 100644 index 0000000000000000000000000000000000000000..50bdfe6902f8f5b19365c1cbbd9fb12eaf4dcc8c GIT binary patch literal 46217 zcmaI7Q*dtGx-A^rwr%5$ZQHi(WX3bLZQGi$ZF|PHlk=^;SN-+xI{U0qHLk|R=oe2z zZ`B?}8Bj1ZASfs(AiW+XDWLz~g9Zc!BqydSL@y;T&L}4&FC{LfqRJpA{yhN%^dL7i zDJw(IFb^+7Pct($+oa62#JYEMFE=%{=r$uo$Iz-g2HyD8h%$-tBrVPQl>Ol1j3T2f zBF*a6;n@MCC<6%vt}3`A@Xzo5^(y{b&c1#pmAG%R`i*pQ)V7cnwRLWs(d zk_SD#xzL81hsH0RjmH$Wg6Sf!!*N-E<5%wHlq@FtuZoOu!UciC#fXRPD#&|MeDQ_r z9hX_|7oOd>Ydyb*D~%s8N4PxeQ>5Q1@RI~xwz}*i7|{dhiwv=5exvLda`3=`HP)r3 zYf&amuxZmljB^6)e5_u?DOt1fGjg*EIn%O~E=?KTf^1KH+!tM`JzNa+hK`WvVQ`O;mGj9HXC8w#Qqh3}AZsCcP4_!916&u4t@v@kA(N)LMDm0l4M%i@9 zbes2-U;spfS!l3C`2@8WSX%>wv5ZRcV3O0pU$Iya;6aqQ{eF;~D}20|Bl4Fk^)iAQ zO{uIDN@r`q`GRY#v|%>!mKn}N-ESsnR z1mpClH~H;2Rs1b1K7z2m-?8*43JMdA)2=PuhMn9d1*Q|`0PrF@$XL zQsa~|sog2)H+j^;dcylojBrYL6e0+L)+BP9;EF{bcTu5cGA+W~X$vE1Km6Eab?gI7 zv)A^$uRF=k)Fm0gl zV)1)rC&n7ojTe+nG;J_$=$@nws`yp{%4Hn1TEiKFB28Hi_jXf3qCZj+7pavFDI*km ztD4dcTTUIYgIYH89G{hweG%bsydCp)LE~&l$_`DqCsr>8uJi`ihe5?Y$f;p6a%nBp zZn`7r_9SNvFVqSt#}vx37P!xM_R0H}_2XkBTKMRn(KwCM7vJx8Yxz3HcdoC>f!bom zbn7f9YWST)V>zR4gvV zTeU0NOHalT;G0!8oYf&$9PWJJdv&@*(s+02ETl#z#2!mlkfb~1il%6Gz)Id$ zy#HJ*;(SxpSNZ7_;|GZ6PSUR@o<}X%C$GQIPjiOwrgpDnUMvmJJIS@UfR=lwogwW( z&-w=S@>TqF@#lQ35_rj^95--&&Sf6bQe3ER2Wesc+2JBakkPn2nz{K~)fP2sh8cus zh37^TA|$#ovW@qkdi+2{VAb?;+a|Xj934@Z#v14BDs2Ytz9nM7ztIzP<SL=5NW9mCM=3Rnt0^*79V;~soLXZD` zZ%prxQ)QRpjcNjW!pmzbG)ZBcL4X%FVJag3YyPFcqI_FBTvUM0+xuswr}~t+B}CuD zhK^VTjzih>w8Aq%ZB$|fU1=7B+}Dt>?gN?P)6TOH-m@8;H!|5;JKVR5?LJ^6Z1S|| zrJhxwG05cvVL4uZgnvgreXWJgCvCFod%7W@`8z#Er+@N=FrKSioA6{KTyl&`;cl?n z5Kh8Tb^rIOhUGq&GzfVtQ`j9$#iikpe0O9XQyv*R*qs1gf-lU}VOch(Y7Hz(!)rEg zD3`uH3h@F7a(lS&H^YHm<&iT<79y2Wb)T#&^7C+rJLDJhl}s|&cj#e+VKpsAb$?1} zW#9dIfU(}sh-`IL*o6mELTXVwPRx!1n0T)^7l!1n(5xk$Bd(FY&Fk>#*Q4Gi#l|AV z0M7R<_ytx!Y&GN-gT`wDd(X_Q91AfvPv1rfukhzXLufB@cApimn7=#|Lm($ptd6I6 z182dSo_#}Gv;g(<-^ESO@b2ij6TR+H`CJOlfMeTuZ0|dMexgJ~o%h6V!g&MdYzfbl zfEE$y_EB~^>-K^_;H6Y5v{lIpj!9ZL_MeyuQahbp8b|%}&WSHs${&PP?tb_9|B^2G z*0UIU2q2(&*#9J5WdEyli8$EV8QGhXsd_p7Tfah+>=YIi(MEsL?CmsY_R#)HfeCFx zun65+CDl)5u(`*tG28hr#{i;qBsybE^ZFT!4#7o9?^*~aJg?&jj>K8B_TSPS%BzW#O%xd_KX zccdjT)a^4>?Mwc(+4u_^xGC}%_Y-Bp(qOrngf~{(r6rFD6Xn<)S`r^BTcduuy6vy2n`_+!5V`&)DyQ_^fSt%o}aLn);XdIe~ZdIDGpq^ zk>pt3&2ODu4jBJ6;QSFT<#8ZDKuci%X~6$?Z}#s2|8H*=s%GPWB80{ViUwm(@2B|Q z%IFrTy;Ql(Z%NxSQfw8&CU*&$1ia#Y(U`tnQu;HFn7PI_KSa0}$FhSB6>3&bxVE0* zGuvS?)68hKhq?p|8BZ=Wcx?E!m$fa=r=I~DMp(@Bjhk^ z(O>3}?xzXQZ()y$9F5R5nJ2>q2a;DdWR>^l7}vLB*k9iLm4a?UbaGpl%` z^?U&SdU}BADBOHpsTq`c8nKc^rB`XnwmY7GgGDDnf*9;^#B@)l2#88$_LDp`_rS2{ zG*1oSqg%M|w$mz0Kh{XU_bj%8^q$m!9+w#`VUFn+a#(&YATr2ERRpe$1j_Y|d|9^Yvc?PJ7fjE8+ZI^vq<%(_hh zuC9S6IVNjSg|m%8zTD$#8+Sz|=NNjK8Xoy^g*kM^JaVrvtQ@WrKja^qp@4-$Z*T&o zS>Puq{wF*iZ>u zc_H#K;dunrujtLsHR0-b38u;g=V0KRzl~jZP_KL?JCXQr!klqUB7NhuTvQO!GsgB%xwm{P7%LJ2E7So~`(hFY9KS4k_^yej@` z?3Spfq-mG_5cVQjyDl)2Nu*CR2oJeN3@Aw1=QHck;s;TDG4!g!c*kPOC8|l?w=Ne* z5!0=a*rwIjbwO3deP}mDa-F3muA~Il@27d@C%0GF?}Y6(ffyllNk&Z`gV&QYZ<>Y) zGXFh$u(r7*{1@POMx8CmK!Jepp#Br!@c&muk#(@JGWl<~)o8=|sve~XP-gs!{AAiSI(>t_>+2?yE#t{l0(5?9Es$fsms zLV|kX054=FonR~bmIY~fCa2A4*5pNjf5eqtWUaPmrZm$tr20dT62mN4sUZ_~(66Yb zN7PzF%=G)MJJm3N3yCl?Xd#nTqq>sNtc{!iKI}tdBlZG*2vm*z{8x&NU^S67nD;7V zQ9KRG_OzOYD?kH6(1yfH6f!r%At6^Yu?Wm`EWpnA==8>l&(JGa5 z3l$q?G^Tu>XLI#*d)WkTBsk&r@{gqIxDs<+$@Z$Y4y0aP)yoWO)D)`UY*QcGY|u{k z%62f2lzJJWv?e+n3R@{Wh=*mO*>Uq>Ur~T44dZcvuuUF`l!_T1zxH{k{o9$ z4bnB&1MF89rIrrs7K}}tS3!~D=Cc*N!6|vM7iex60?-x*k5R2ph`eHf5 zZd;+USuyyf@&rPVY_~LdIali9Up!i*rG7K?A85R7bLKJHz_D=#6)Lzbq+z0kJ^hYE z#ZyvxDW6KAxO|yK1GP_9M$zupE%kXHs)iauwF`tY&H3$e-m?|;; z03Ep=M1piqFv<6UU=gTMZ3P(Q3}G8-oVx?A5g_GIZX=f3MI`z#r9$zk-(^JDiZW!jyPJj z4Gd#wd3MQr%;g|qh>FQG6K*+5OA{Pzu+Li>diXJ6XoU6Xl=+;}-hO$a>GlQKdUN&K z+JdYnCQJlz6u9KczeHVAjtGu`nATkmymfVrX96^{+ zOr?jpy#xFPo13D&JceT&;h+VDhu0Qaf@06Ca&ikS67)^#yZ?a&kG%y28;rp2l9lM| zIO2%y>xK}do40;nSs-^tu+tRg4ifZ}v{TAv8P33Po7+3?yN)0!SdF$~#_v}ibq?a!7XW@4 zYI#8`xL_qE+mem(BwR;sW3=Z)KHA zXBO1AY~Tl*LxMlGi;SlNNZcPW^~j+YoVZXx{rF*YhM9THq!xl7QtymL*J(u!7P4nq z$j4Ie4)cd~35=IM)@|U0^nI1c*UWUf-*NIc^A7!H225KvE1g092AW%WR>NgHEU3Fu z-XaLE0y+K)H=sgiBtF~r+<_IJsMaVxZ}iNUh=pMSEcCQIlZAJKMI6e~+){++kH}xp ze+W;n#QM#}xFj+ZN?#VO@JnvpoQ2M=Qn-PFn_OowCILqT2{=>p+!}>u@(NTath$$` zX(L|LFG}~#_EF;Q(#0#>c|vVKY&u~OkjMrm5cjXDUAJe$iOe1Gl$N?|92=Lh!!f`) zl+Mg7I-=4+iEN5l`5j!37GMx$qz|S!GH8=`zbn+%g33;LQ?_YVL?J=e0+b-GJj{RV zW6Mr1dX5{R>WyF;sqiQN)rmf?<@KE!Yz zSjkIeaXsdc`{!_S7%||e>#kzOgxgcn1L?=v0d0H)uVIX{pL+?!x8BGv*vM&~8 zVBV|%Ya^2~Y~7@8r4zsiJR>Cs7R(51ct3a!wg{5LLWll9*5pA81zMs}?@SCyE+{P| z+ek@?t!tOG`5(u+hAJnjowYUujpdzm{5A>%ZO|`4_b`_PzY76+_F>bknJZsxB4n7M zgX0B$R=PJ+@2Hs^U+$S0c(>6dWM#`&mdbjfwXP}x)y!y1rXZ$4{{8{=9MCOz#!X^I z3S$(%%cG*?()vI3NwYm$Wzg-E7MZIZc}DmaObpm^pS7-VaEkHy(uNAim1ywmg=1z+#rwXLONWWt-MIW-RVkm}BuBYDJpTGd8Bc z%pf2@ZzqB)cqL1sppB;ajoJ@;1@JiY3IsXcE-B85#m5^GHYlj;oUcQPOBkl+;`0N&w z8yD-{)vDOaboPfS@=hCUBc0|egkoAKgT{d;ib`hZqacO64tt3;uK4$~h8=z$2#u;8 z5yGq$TS6qWfK-f}c<<6mUeKS-mAtTB?ACAD$JvG_i=u$q8Uf6&2TT9X0RDHg);I9e zw9R*PRNX@5W=a!VWcra8lt+e8*K%8{PD&|E&)9{=STa4&-|G2VF~^l6&F{ZR9GU+_ zjT1Ba-|2x9WA6VRZpI+(lnS!8#2<$})Yf)L2!Q7i^Rvsvv=s9S>V^*=*GR~J2V-aS zja-_=X$jBQFZ6mB|B>jc76NJ)=E&En-IJdu7H#`2c>RGJHyKaKChf^ ziruayPJ1e>9dm=DxGqS0hVuBSxekXAdbm!Zfib!eNo97V1PZ^fya%ZKJMs2g-EwKq z)!f3w7S>JM8UGi8C?;(|E0$|Z5$M2`^a6YMskf^`{Pjta>mqR{b`4|g$ZFab0oDFH zc8XLdY}Ykky!p3oXqh+CToz^`8+%Cmu zZ|NAnxm5|w0e@>6h35p{k(4)!lmh&sY&2h6n)Dr);-_#B%E#CdHKtuR*(hMCp#%m@ zqkmBNHWyeD17ZupZb74@q^?k-52q@Xx~F+W+9CG$qmrBWiYey`KJF}6nu)ya9Q5^{ zWu+Q?o!qP*UW;^&_LjtK^5t0UkMgA}MG!RaEd;YcBY;8}(TS`AIYdf+KpQ5&jYoOA z%zkP=F*|WfLV*w)eSz%-Nw^Q8{m#Dqj3E7u^)|!C@Rd0K(wqENxg>g~xi7~mkY^FZ zLmNb)AnKHm>7qVj#|rfe#W`DC)!BLoN7@sf9PRLfFhZVmX)Ww1Au~P6$`8XoZZV=6 z#6TkzQOlnn8s@XiPgN0t@aLL;V%&e^PFDL?cX*|S12Q`q!#)uuMC~GPKT?ZBh=N*3 z6B=Yoe&{djV>R?qHPhI6I@h>=tvg;x8z6RJ6aUvXV$x&LdG_T|hi;8i%L7%;_f`f+E2m)M~Xq2LfZ@!|1VT)Lo=qy!}bq2rgDZW%$4T zmN<4usOI}(^xnG!$=`CTqeYMeAZ^F0c@Q;iMeOK}5qW^>zYxJ}!SD7W#&;6#KLb|= z*JkA}Vt7LpI0(4S4Wa>E`Fke^d4}yzXe=H!C*|hjo=SCs``i?i0X3AJeJFk--hM*! zmC<#Kss?N%QlcYiWthe`no@61p4{u+UX}9kBA%QCb>;XHAXNm8a?PGR?4w0hWKDVk z2i+=Ln;|}T5=pc!R~Eg|Vbc>n%5((L(pw@bep}g&aTZi6DVvmi7PV;}hT>eUo|1`2 z@E3A><`!C&l|QoaQoKyzV;P=MJ1_4gAU2MDor$q{$Wsr!@%1=_D&6r0I-!H`2=g*- zNL&|nBb^??;w)*31*NqPj;sWs?;eE0qZe7(WL8oo#-A}6zanRfF%DeBl7Roo=00x7 zSfo@5T+qi*H`k({7b7s6euJv)JNkzTnY4X(?Cz{6XiO`@V5G#L^;^=iZ#)2gH~f3Eyb%=1>bqNp54-K;YFi46DoS6?hobAa2(d~Hj>Pj2QO-(0pUGBA>;W; zucXc*1*g3dy)9=-U^z7J8+&!GbkzDpwz`RXVaNe@TgbFUXZ%qzokz{Td@G_)61&h6zVnR;4`{3e*yNu+bDuHK|1uB#F{PET#Pc`1V(q* zO^;L^mwh3j&^z7U;wyuyRNL6GHPF{s$mS}Qtdk{qAYB3?8=u_OU$9w~=A{{#R$kFj z6nE&pB_)-*O3Q5GH{Y&1-vr{J=23gp{dyAZ;T1;d>S}oVM5uFW0m=GTlFma0TD;`= zq7c(W&Td_0-7MoywtxPiRMc)*+&ZHTkN3@@H!FxAiX$WoNRp3_P zIRw)77*$Dsy>BC^NN(KJQRSM$KqA{q-_}sS(L`EojP_;|Nc_mV^xJwtQIkKr6d5xr zIbHgJg&`8{5CYu_6c&C1?RdcG`a?A+%!`91P`snInCJmvJ1T%k*1tRuC|-fHe}G`P{z@tg z6Z_u^mXu5pdTVxCebN#&ARu8Xa$_!#Fcc1-YPc}#O(og#*2+D!rxGe70Afn1_%d-a zZ3jjQf}9pqXKhu2@FPe+7q--SB2zj^FCjcs#82(^KX%(T~Hb0`X zu%CNQ6KRme1p7*D9BO_D?dfu2zO^xXqEY#3!jyQ)`IY4?Wn`kBFD)}$%CxqFn%A2BuIZ`iD}vO~$ynxNE!cr=9A zur8uPl!w;8$h`TT#mF`0o%Kca^=08b^(SQFe=!0{(w@=i8wJ1pA}>gSvm7d6Af$Z* z>Z7l)%#hwKVlUSohOAkzD_Y8Dr6*O-66cMX;2gz^mz~~Bs5Wah{A8gt(*kAdr~Y{v zYFxcokt_Wo64-Uh#v{E7f17Ja)6L>-XFEtdv&5s2-~kA^Hmg#G#Ii1*iBNJbp9xWp zUb^zm&c^krqVDChzA|a%DZt)7&VkRZ7!3`#s79T5Z((QMPPL+UkV{)=VPH&9^~xV& zDvkO6fJ1Oq!TZHewjwohv0t0;NLOa(RUHlWbtc)Osz!Hw(Bjns664M zn=I<_Nerek-PNu|#VJYk_1Dr}?jASSmYvt9E0_9h95w~B zA78xz&^jb!fj?clhhB9zsCm8j=Vm~hRXp2N%HRA_SH_@C&Rc5}SOQ1&Y42J>$n8nP`<;2`|IV67XjuCKB7&b`53Jb7{8 zO9qmkGxI=zhmYwO5ZS9q5hx3wji5qo$~wQHJI#yB z=Cpg!uEGXRHf5+!^&mRui|XmPQDcgyL}1{(sLv`~X;AF0B*}!!I!VWqOXqF!5pUEr zo!GK*iX}0==rY7Ca8gk)S%g4Av5G_KSnwl?&dwa^#LzP9WGo{j>A8&e?C021OAfa0 zP|>K#fNCmcCp*Dku-Y+S<{e6f$ElsruQ>g+aM*8wgxQo{3tXwBwP>6hm{DbX27gmA z*&Biw8#9}iVcTyXK*HmREFEC71uq_J=>51Cn?M{m z>n1(tpe5KXAYx0d6@N7x7Fe1NQ0OI=x_vfexJs9y+dP+E)T>aH#D$d(RyYb1O~HcK zq|7|!CMvD~)6~Ues<6T)B&~u!Gql|MCmImQD%IxBMe|>bC@={fabJxn(0Ibqi1Y^~ z6g4Q){VK(m@#*#E;?)-W8M`juS zw$uF-2O9J-E`*CnA&VK=eDQf@b4l3J#+%;>Nl6fr5OiGA{+UKi@T}HLtfI`2w&d61 zvKM^mF5rqX;J7vN4Y?boBkDhP(4?st=;Jwd5m`wemVoD_&*;mD?xv(eC1ky&^;FmB zWk&60fAar9sYXNSK}~=OkHqbrsH{sPxmyuQu+%)b^8#fwjAh`(j*xQ|LlGrrNNMK_ zP^UgkS}pv-(?@&w9w{ttbu^Xa%HzC+9-lxTSa(n#fJT-HVxs2ppX>woP2?dNCuUX9 z&3db)Eh;hb`TonL9F%rz#mQe%UqEEqLJ|=OcWgy|F;P_Cz%=p>v@8&#A^N3(-n!Jg zZ4Z3N{n;A0N~S(}nvHKV?#Y$|^Af78B7ETi=PRDCn*mq$K3GtlVtV%i_xU|w%;d_O z-Bf-X4xvA4;YDit1RWc0oO31`WEJP*?FHFE>w7NBD}=X-UQR?huonmCOJI$2H^Rldxd5^-d)A)@%^ucxQDGFjBr5D4TCm>o(+bO>q)GY_qVSAYfg`i37U z8uZ&M5BRgUQ>uVO%h%=+=A-b*c%jb9?@VoO@9 zc^iL5%%&lmlZSp-Zg*b^7mhyY5~K+tic4YsRwJ)}_d^zub`sz)_W`CM>OGWYib;qz-Tfm#{$LAF(CN zG)7UPb7E3wIO9$fNmFT&@S?)UEJ}1bLwm@Fq6q2B4L004`V_qJIxlXMOqm>-XKJ=v zV|H3$KE`XkVkuWnDbj-9vQ&Z5kzv>?9))Ss-V%Gai)l;x}Fx)M0#ZI;LoE&6-qG#H0TJ?D(oqW&r>h63iF54PK(!Xn3rk* z?ph8Yf62&l9=9N$Fjj(u!>KdYMEZ6d$~WEmTl|URI}k+hmt4O46eZevWsW=Hjuyoe z44|R>m~!Q#!-u!P_AQ}U;>2Gf)U*n<9VPc5%ki#HDWS=~bwK-U*A=Mj+QGBsvUWOe_uEFif~9-oz8e1MFtH z)@WL5N4#9kq>|5L+|X>Y_|4O^9HGM519|p1q01+?s3&IGIi?S11WhJ+LG0wM-Zypi zEg(b&?S`?VeY1UbCVOWP2{L^G?Lf5+*8AK!pPTt+TOU>s5tkaS9x&2YO&x!2nr$9DiUXhvO$S8O_nn;E&sup~k1 z4e+$Dc7-o&iwfHH5H+4)Ja<&gUWCKmAU?Oi*WNH!uTam2*jup{&?67GR+1FZD9f!x z3Hr)|k5qi@p=?H81J`_JFS!jqvk!Iav?JVjqtLZq9bZ=F*2IofZ)z5>C575Ox$6 zY)MLm#`d3MxFoEG6UppjCyPkOTFmT^NV*AcinOw&8)WRO@YTfM2@Iq}(MtcxWI|~fH0>8We_U9E>8?g$5 zj#k`5`tk%((O&;5t#55az6$K7GQkRn?akN5wYuyzoh?`#!0b5xTLHKl)ly(rfT23B z4ez)>hOh4gQGYmj%+tF5o3#>lDYGG;CZ*G;A&2VreUDw8@@z8$`OL|oHhWmd*vvht zVX#L?uTH*l057xur2T4Dx{Reka3CFY#xtDkCrs8ZuswM`(ualL>x_-T63L{@pZ1c!IU3uvlP_PdYmvVUR!AlYTfb zYMURsE{MO_N>8%PENrgWAH$U|&3!_41R!69(1`7cJ6h|4t0gpwTJw^a*SYnS3`2Yd zr==Cq;WjK`Mx5@7JLc=9it;0&Ebz1i=1cvhH9`>Ol<;}oNEOvnaslKeg^{H(>0_(0 zVew3+TschF(yFByxT&j*u?+LB&uUkwjz8Yezv^ObjK2^3wHh21@=g9eUFI3^%H~cK zyvY;?isy0If>#dON&zf`IxokoWG})vQ4V|~C0 zJPX@E+U^s0HS%wS(YAJOvt#+mkwyEon^)UnLjVH0zQ)r`u6d=~4|Y4=3872*Im}5% z)vlYlozYH={>k)BlWX;CLFO00m$=M#c@t;3O4=@InM%#ZEfT3?Wxy$AKx;Yq2Bl3~ z!fN7K)7J?-_2#blpW7Sy05 z-+(lFcT5Vhr8U0=<8gQ`ztvJ`mDx{H+5Inlfzp&mB5I}%i}fmU##)Wz^SMX)gq;?-Y;o_n@;k)M)Go;f zCzj>zjFMqmO&^0ypfX!I$FjT-N51COvQeg*p++o-4LNvS*rarSC5C+q{?xwMy#q;V z*4DtGvttJID?dnXPj|Y~oB7O>dj`2e!sn)hKMLf1{Hwk`%AGJ7;32*}F)jp9+#9}( zA77Pf%$uwJ4Z1@J`md(T(xd%U9s&>$+&}HH|99=5=zrXJO^sZQ7)2aR%~b5I939PE z|K0mnvs1xU!{Xxv9;_FJGl!{ zZ)%3#*~FBy+bWIf<)FwUI}%!Zaot2?W13gHi(!gx;BOr_Gj0ow>daqbFuHWr0u#qK z4ktHuwkM5mJ|Od3R+{~8mlHePeW6FzCC3wyI>|?!=)#Oeq#%5a*Vh{Ofd#rzspNkB zV|!`t>nK=OZd9rl^5fe30^%|2EAr zcqcd&grX$ac_`)6X3!|dR4QSC)TN(_BR*)fjIHKfYR+_SDM=S~SWBfwEi^25Ib3Sp z8rf6Kjz5yWl|&-D8f^_hL7=~&qf#)GFIG{!JI?p11%4xFi7`bu9ldl$$h6}L!Kgqi zRI5n9QS-41YzT@LZ)^=+`8!MeH$LSLp}zRXEJpco_h| zFRZpJlj(Y4swfDEB=|R5@XjhfjmM9HXWyUS^#9%!1cqnNKI@bb&NFd-09%c{-B5kU8dvFSUjfMWaiK>mZc^=zFwGc;#LIHQ0>gip|r<%5%Lu6Qb(*sDTB;hZARRSePI67`{z+tGRUEmvc;Rhs;?hHUT)G#@a>DFT3+h+#A*Mv82c|(!(4DEA zJ0BoB?Wvuqic2lJ`X3a3LfrC_h{;5>JI1nm9RfZ!clpD6f4;sHfL3p1LlyeRYK_!U zH%D?4sIX!aG;O)eCe@J@9kpxKqphhM8awo+cwj)(Oxnr~nf$s_Tx^@PGganvn5%~0 zC)5{cZhtpc3s$YGPdDN(Ryq@x2+q1NYOve16slNUQ^*c7(?*DGa{Q6XatoI%$kwXF zksqX(S+=)0UL;re zD{2C$W}_560EFjMbrqSGLSw?9!iGAW@O$Ra;fgCnTN}Exnl)mkBhjWNa8uX^Ifl3N z;JD0^XhM31*>w77mb~Z3rTdo`<}JB!E-3s3;z~E45Jl6(a`=42OZu$>#+zon)7YJJ zSD1sT!7auZcgwM2i5L_jw_jXkNod41Ty=-pSH$y8B`^miqIvz$8n@<{qq1P+Gvwzv z@Fq+9@JG|OpE~rn2D)KVSv;LJMj49_xXTU@aG$JoEZbwc0EvYw`URyr zC1W22u|DkzDU;%Sv?_`@5|4Ez+~Q&cCW=`e=(?Ucpy-cBc%;2a%+~f*A8Wbcc4-BJ zsnk{dBF!aX=)TnlMx)C%*XOTb+=*0U>)K5frWxnm(F|y)sT-(~5VaStaJa6HRr6ON z0CqPc(KQo%E7PG<7knpKX#C!#JJnOF#OhWun4R@spNoopsWAJUCS%e2$!^JtrIdD6 zO7u`srN)S&)0+Ty#e(mJYFZuPJXK8o12O82H{)eyY0=)DZO@+%u1DNZ;l`ef!}-NJ1oYiL zI(QoN8?@&{Ao<>%@nU)!^ai{sh?3n+k`75UqHvy%K;oY6O0X*u*gwWGOx3eYovi;Ill*l=J-YFshHdBeg`y;shm(r21td{aZ2Wy-3LRcVeYw%)H>EnKySCXZ zRv6J1FXJJh(nbesk`__4k1^7 zt}6UweXLEwKHZimK71Q1Q=f8`-g)%m7su83=CCq}4ElXs`nx)9ZczU<31*JK{5`-x zK<58Q-T&Js6Z{XEvv9Lw{BM;LHZ!vSw@X(2uk9lLYb@I2fp`xkFSS!ZL1cHi2vYi~ zEGQW(ng3;ev`nk}nKo(Or%WbSs^tOVtpxV$6%4M4N@HmpnK#!(-ArR6ng z*EeoF>%@1xcD1|^n@6h&U%m!t>uBp|s%_;KI*6Ykni@Op!w2udGCWm84c|7zQl5)Q z4?%{gnSS$ZP_3`cQy#$@A>T5?vahJvBe-Uj^qSm+jpQt*i1xm-F{Q>HOh@vkh>+;% zFmU0j5fn}f(F*(9i^qYks_9$&&!PdG-`_z?TbLYvQWFU1f?6f7C0V)4h9LVf6pSC0 z*b6%(Z3Q&mVH6QYD||M##Nq`OjC#6a^$^ugik8xRNEi`auFZ!i?#VAh6zaiW`*<0l zB8x{P2m<0gD*B}ixK|X}=0eCx-d2%9@KOFz8Thv8>j0rb`u^!N{BngGJG`y*L#}1H zatk|s8Z82Rw?}7l-#E-i`c?K>c>QOpHlE3c*0VQ;WwR2u74HW#%RPC!V$snZnza#x z?XK#>E{PQeOtaWk*iNdx^ip-VIRRNTWG9tVdXi&iO)J%)ly^xRO0(y}xLl{mKZQK*SLQ8}=`R_emBQMH7`hZ%up z(f5s;*RP;_;^@%#RZZ^=ry5$rn3V)?aRQGkOD>3!1!Jz+9cx+dopl~~VVatKgZv8! zR}CtMJ^$u_fGfA490_9+VEn8w#$wE?rEiMABB|Z+$xfx6BN(joG zDO-}vu=BnuStq?`a>NSd)0f3nL{dDdVB9jcF5*%RT)I1hZ+TQz7uu{aBvn$b z%%yDewi%>_5(76PJBH@;mL+`r3Ull9^z_Vv6n+cY8M3M~VZX@&r~PD%kG?(OVSYYA z5!54j%L4r6GDGtc&+TP-l95)l`|+epwK?*dYjO9*Q0XvX4-QweIgW4|Yta#*+SSFt zQGD1127$xfe1OA~Vr8dO|5%qjl9nC z;!f3--J$k3YTZ71!l556+~5)R`0flqe8)CCNCyXUMXTXGD2#9JQ+L{GEI1bd&C?ry zkiAKMc;bOCFL`l)Y0%|x``!dNiU#H>lS392UW`u71l?3bzZGs4%8?x22b>G`|M2yW zQI>Ye)@Y?|yVAB@Y1_7K+qPY4+qP}1(zY{izHgt~=XCe@dXKgDetyTZR;-v2b4E~7 zSRS_1pfzjO>vCXfgD8}5A$X;#=gxPvpeag4mS}Fs$*sYv^i(i5yRD(R+_WTCC{JP@Ef4{pdjhCoEI|?YJ1R{;{SZ1 zA#FyDs~i+{!iRiggy9{g4Ro2OXo;||7y#KPgz4_9MrlZ)bTCRIifezSX*F2E23Cl2 zhze#~UAwzQWU068z3dVx`t_`I4F=QQYxD7p^jWdT1Eu=JYSt2g`ROm?ip1_bxaVh~ z$6P%daY2(FU2_{NhcERd)qizOU%eBFt=7j`d({CFL~&sh4l-^_2@6u>@?luw85LQMdN|fetv_@bdHmk^J6(i z!khFY0QVLCga*zl@Jq(^*q|WierB^k=C?_PcerD9h)=jk1o&8Y0m*)1T zdTx8$Cf-I5YuVfOXX1!7!xF$2>7;AON<2gGohklaKuZr-wA>#1SfoL0$}OGE_Fd@D zi8tmT^g7S(K!~Z&@|MY`^3Z)|&3-J;?kKctn#w3fxJbDtFA8~v6 z_kz+--s+_2;UxAvT+}#X_!I2D&eRQGJt_FBSio#hM9?z1gD&07e|X{Tn(oLp5&cOTkxchcb4=DQ+te1@Aja+j%+IoCSC^>?1z)Y3iEAuJS{3@Yt-Uq zlBU`6$OUrqcst@Lc=j=;lh^nYRk8FX8rfp(9l}C}O5Ja=| zas5vW2C_N&Wy`rz;pW7knO|U;K8EbcU#a*>pMf$(eemP$%z66o`o^&1522Gwdd!hS9 zW5oKUJbUQ`Zwf_$D3b2Y9tdPbo?o3ue-1!Qh`*WzOPpA16cJRa!8oLn&T0r=v<#rf zllJx#{SF0zX^WI4`~9b_Yok4155Yx@F(XkI{Y;!DT-go}4w*{s(r?oKp?I)SK!M4q z((GB>oNKOt>76>~wCUVVfvD%YF*u?{kfq;#K)78FMLSI)Z|v4i%37txWYnnvcD~rgY7xpq*Z%?r>q*~-is0w5YT3V$>xXQ~Sd~L`nJB&3$aX`?9 zV{?-=dA9zF4FtxW?JXb0-4N(fv zzmTRLpJN7fIU4CRe;rX#k594nun&-SzAT1Ps-)DDy)2dKaON~e`Oz6CBvL^g%W^fh$F$gdr$>Qwd(!KzPR`d?1p&W&HC&TTezbXV1|cGR8D_41k5loNr*b z#z!2_**q*9E9belEV6TZ5)p?J5m(@RF)Dr@?(qH<;A=T9;-P)# zJShKbLid{>`F|Qg{e^V@8Q}lBuc=fvQ$SWh=B^~Hl)^$rkdx~LNdv%UrkTYT779y% zCWDUFmlS1em|1g7xWo;#G-iG4O6M*eI1f`Ag*Q+dn|?{>jlFt-qd)4z&<&<8RV>lh ze)4$Qa=C2P_qsDns9>@@8N++i=;h$}naIyIJ@t>(i9snyNTQKSC@C4_!Kz;XJYx zUIJx7h}d}gels$K<%3J_ljxZm5H3#D(Vs5m2-*a2JR0|!%RtdpW}VY`?aM@1Je_(N z@{zoxIw!iN=MO?Zo+)Fn{%)dw!k2i%^(7A>-EXmzJYRUqnHWyLRU3C;Ldg*I4IYJl z&x*NP>~fTeYPe#>3OQMJW)6sDU2!wj5Oox0$YjYit~HM(NidEjg8=`a591kqES^>k zWjvY|HR~ZJ%RXZiC+NLILYxP1O0q(46CN(erRg1ytr7#!+5IdC^$Ott-|Z4zGp`#p zHe%`K<4}FdyI#_Uuv0xtsHmgN5ev&@yZRZ~(%F+Xzm@e;12pFZ#tcca5;A+5>PkI0 zeTv697dPY9+2`4QLNf@&1ICl09;gVaU9@4{riKeaq zw56_cd{SM=a|yw!2tvj>LzIx#_8wmISg57s733@IAN~W-P0^^OwIQ9stco2~x|5mv zI>Q;R5gT=IgAVl!cr~q)5z6^5NP!fp++gbzTZKK?0!sbxTgx{9JVZJ6_ABS~{qk?A z2+)!j^DmEN3p`6VVWP0~wkwZ<(nGV*u_w7<(UOIHwh&3lOY&#eh8$J)&+J`Y#-|3 zquNpLmXd6FG*5NC6{E>u&s*)RimeXMduzWazuBL>8ezz`M5;Ol2Lx6uo(G|-fWh%m8gCzeCH6gfvle+vdOM3Ia_y5v}yedDD?BzDw}ov%kL-gn!K zY@m_Q9;Uc`*mhdxIL~BAiB9?=rMBpVVv4*aIdjkmzXkaHFN`5@mXE}?Vpuc%1(VPGJfIUDbv z^zku!B{uIbJ5Ns|GWe73g#lX_J#6ZlRyaRUMR zaSi!e)gX~>i4p`_wMOm3kivk=QKI_)1SqnW0HqJEZH%2A32riLy0lm;dP}FtlVV*D z3&JY4ZfiALbKFj0+VtV?X6Ak}qavaz{_Kgo%y4OTnC`r~Irquw_@RsTc_9=P+-TN6 z1{{My8oA@R1qmyAnwIs@m%r<-#?67oWNf0KJZD>BNwQLZuOV=Ey`!s7u*S&47N2^| z%LTtUgQ-*`FKo<#?qW&FxLK)ZM3yniE$#V6_ zzkjs?NvYcfJ4>RN>DVAnb$e$;kRU!Z7%SJ}I=HShaW@sS>H;q0&E11PgoHm})>MXo z3jq%zsPD%0OSA!?oT-+q z4AfoU3r2O^kT4BNcr1M_aiVBm*w`FQAfvIeL2r5(#~^*SBCO5#KoGV}$@r>Xg4}FH zaV>#~u_(i-hSfqQ?J+hk?!&3{ydi0!hNn$2U5wCeV?q)n|2E8{$!TDiAZ(^LOi;## zX#*dN)6gKKy_QfBx{i>ZcYMS=T99c0uZAJnAtPAb=coi!hK$qbRW^c}qiR?fru?_^ zP~^dULQxhSi9!)FV+NCaey~;8IgNWJ24!&`w|Wv=;Q^QM)=2Xy>9G)NS@v(mn zLSdvQ3A+GFe|FL2q7qXm*lq)aR(m&Qmrw-l{Ehu+o|wGm?^w^Z!hKX=HQpB{MP<70 zQ#Y(VYmpcOsj0SEosR`JqG4b@22!_N!>qaA%|DlwuMNM#!pehhE&Ga}&aA3}XDw2V z#vQ3g-q|~9U_3XAz@HNwY&H*37@4WNb43NjNA3JJ6BD4_5znmtC?*2zVyepbq1T+m zWO<_k(AAPipS3e$n}}*7qVQQA_d=x62K8`ODd6|5BtYiNOf?hy)%?nDvT1o zEzULiST*EB!V7?=jpIy=ttjykQjh`wT-OW0cGbo8Vcxn|Zd7SS~j5A=Q%$K{}xC7kebhEt}c z`fD3Q2l}vRGc>YI+X_sk>v@JN4|UezUH#h0Ey-Enqe7&v&hdCXun4c_>E@!UOg~U+D@~h?t*pz8V*@+>ZGb59+?vYRUo~nBvp`bn}Bua7! zqF%vuc#Ly@gyx|mk{IC9?gY44PRtEo*vgb=F2~H(($tzrtw!#zs)SF3JrQ#r_Y|;Q zRF)QQFu3|=v7?Fm`Y~BL5|PqMa^`Q`JpF)bQUz2`cZ_SVPU}@s$&2Od#}FNJhyHfhO4l)Vrd1f(K@}0FL3Sl#|2Wo3lG~Q z;XKWeSadJnfHk7x?SfdlYA$^i>`#l^F(KmK?a|V7N0<%Q-FVs;Z8In+=&et!X=@H& zMwA071p}A_dn{eS6Nkr14mB)dSlS1^CkX6@_s$mw#!LcNC97Ns!~Wq|P`H&Z;{!6% zN$c>x9b;$>vv)|_;CFLM>j?JYnpv>Pu0NfOgJXIt5KnZvJiv*WmP+*Zyz7f->mgvt zQC@GpRjWo-$?sR%0p0M$QtDyty`tFGl;;VVA~wR??m=}WOTtGLOy^F#w1O+Jr*qo-R%;XijKQPVLYgQQqF#6+-ef z_uzfttuR}2))jP&*IS;X0jC|X%n%8IqBt4B^sTKuJg^Fnp@-w$l=sI6i5ct1JLZUJ z2b9^VT0#Ml9E-V2#W>=u4K)BMFe>zIPcfJuHwj%h&TpiPJEiR((&_tN#x@WcACkYq z^TDS2F?0sP>e*U5ViDr6qwDxjs|GP!(c7J2+c20UDvs!Rr_{T;d_9uaF?%Qm#(rDS z2rWmxy-57_$Teg>RJWp3_mYieQ|rk6WWDy(qZnvptK&mEQde{T3M`(JO1879_ zcv3cf5U97QYZ$`KKVJf3wyQ78>Z5u2kQaYM!gkzV4%j(MrW1|suxv=_uN8=8e~D%O z0wi@C%rSVsn|Q~)?23`H^-=pgk^gI%B{{)RH&9eWys+kan`AZHd1*NWU@y~vPqz-> zat4?T(e^o*pJQitUMF73?m&CYru$^e zW)}j^VAp2daf4mNnjLlVy|ZQA_IRe%4!z>3^9sFM8z(7CwB>jD1Nlk$)EA+7DC^3H zJH0QP1jhVXcK;dNP1$9^HSp=@v)T3O^H+Sen+pns_RVqVL-=nA#(&j4{uN&-IDL1| zw0HYY#BZU1q=N1Z3yj)V7V7_7sRIjMtx;JcPgSs{VV6J`-lI08rf1KXb#s&KR*~+QEcFc3iSJ=MC>w#XP4T0v{NjbKj7p{ix&V$ZN{u@yh4x8L7wYy&_LCmzjs)mUd{wNBfnW6gH0opVvOp1G&BS5A}5RNRcQ4@EhW-K%NB?N$3>RoB3q^% zAf=Gf_HUt-{T4rHs^uGy(_~GV$wJHgCv%@n&_)JFx}OTh%@!u|(LH`(1+nvan=`@U za3f3&#Ew%;es@$R91~vUR_sN*Y>OAtsP?Vzp?&x4e(YB6dYNktM@3pPd3DH#a!L-f zlQRU7bzXF{jG4!0811-Mn0Orw?pgp{3xOz&P?iLP@4qa<@f6&RV(6Z z4*X&=$82E==f2YmkVl%iT`O)?p&f%}I5K2VQKWoR56C_@@^RD|Fmqa~yX178@LGc$wq@nGC)ra3a_DH&ps zZFX@}n-JUg))afrZrKw<)V%O)nbRc$y}EoVrH#wEXw&8`1CUF55z#zc!!+uCC!;;Y z;xH0H!RcNvUaw&WzXW(!yVT1J=mN*+j{sp(ARf}G7U&3S96A}%2I;CwhT==w_0O(b z*yRGsTZ;5Fb-#ia&%WVs{DHl_eb%Fnf>US3K8Lp6n@It04`QE#T_q+U5(kAW#voA< zQ*4e09@mJIq(QK0yl^1NAfp}8BRsdcbct#`hk35W4zQ^PqvTwJl|iV()V;qYo07Xx z{IN;-4%s#PWqylaL*MmcynD(N;>jgpKEA5)cH6$(u+#>@+XB7*@m^@qs^A-Teu0%t z?Cy(t9S4g*b>A7+ju5LaVrOZ-a@ZDXC=HwyZUWl_C_^d4{P=dAlN~EL#!~$PM>ht9 zG1XohZ&z<))g|z_n2CSK9|AE!`)Y1kc83ewt+?!RnuQIbf@t06*Jn(ytUAnYwu#;l zQx1c}DlmZA88-IJFKIIUYC&~+u(PA!5MZegA zcgIr)?0G1Wt`m!Axt%}41DplNMX~41{j_9{?M;pmXh<>S-FFnSMN?#7%vEEuLFPP^ zjG-r+bjI%RHm%*Kzl3%4L>d$sC1tQ z9;2Hl40NbKIG?`EUSQ&f#CR%&HqfgjsC%+Ph95Lryen(G2{pTu zG`_|E z7@v_%ZlJ0sTu$;BRoJ)s-+nzm0`M3%xPSTt#)Qd5((~j=v3E}FvGDA#6AOP~>&V^` zTVa+Y)xU=)vsYG^KESxF!?#W^j4^}5bYj|A@T|8sViZ)H*qMJU#|hJH%IbxL9hU-*7$+n{Aexb=Fer zmKI3uIVU`s#;f63s0nO#2{dO-Oeb@dZzEmHg>PGRmmU^qS3cF*I{+~xy}T!#4p*&5 znI2PLnI2W|dss3$Ks7R%bF%oh{S9$rj+so-SU5rYWrT@R#Zmqkv_(uUAm)pLYa^1* z(2-e82_Sw_Uc0JPu1s8*CwQ>Jgq!(!eqCL?i+q04abWfx8FqL6j!NJ9Z!Hsx= zhIW%Bl|`XKEvjR;Ho=f_{Q0LB29Tx;+)|rGf)W%gcrVV+8x9j>3HGp#aa9vPX_Dzh zL*49=;q!9DHc$r68rgLF!TGa;>1(7%R)_8zHWz_ytga<%d00<1pRZ=wthRH?Y{87X z65iP6fK9SY*+n>5j6&tty3*lNuDcN-!oI713|MPXalxP&~Ptzx$wWzpyGw! zGm?@z8Th58XVYMa}RQ8qyh)1H)+@FT}ve}^($vcV)I5uJq zyzVADI0k(UjGfMLoEPG_oTA6idA914#FMA86RL@3vAmfUWk#Q|1p~YW%!fqr+lx!f zR>fr$T*AQWt2O9@xED;P$~9T!Ua?TGnYxt|re{eKcZ%k!oVJP#ccP+5H;GB8!P!g&W@D1Ba z6XO@>2FAl!12maLJ5YHzvm4X67p8Y&t|FDA#oC;`NOI|~7@yb|VX*C(oLE?IUyx~Q zIn%L`!HKtUV-hn8*6kR$lV|dGrhLoD`OnUB2azl#XK{S4`Z8&Xco0iwVfEx?y5GAd zz)S0{@xsCLh#|bi`}@zp^E!u{1Um$2Y5Ewp+FXGmf0m{c61L-nF3*^FO7|5QER!fK z?N?14J(ctD4YH|6#_(5hlV)K?sb1Z1x_xyF<vHsj&>``zYrRn%5-m5!jN7X^P90X52%C#G5ULu@(&)LZT5P^(5%VkO$c_VCP6383 zo?uL7V)u|5Rw?+Rc2cG^HgMG*Rw3?x6|2|9m^Z+bJnTTD-Lb&*_A-l;4|jS8^-gH# zq|WH@gYdg>22W8JG4Vihl^b5ZL4+bjNp`R95KO{3y%o#_W>GK?*`B#scMS^bHr%mIl)~D$tNdc{a3>@%h*KB`dtGh^wM*gHaXKa2U<;k_`pdq=Fs zUU|6rMrELWV&tH4b*(n|ss@!!{c6zS84JgUl0KjQ=f_Y3hs&4tFz*hY8;w*tM(J?m z-4E$ob{@7QM#IM!oDQ7U@mZAW-5jS58YIyn&E%-}9zoHp>+QjAv*%@ddWCUAG&-|G zBC;>7HX{8LGbxI^uhRYQ>us(pa>rUdmDbgZ!LPkH%&unOi_KL6EI{(-uRo=$R%n{( z3RPWG>k;tx!-;WMD%ypDEC}gxLoS_+vZD;j92^Jq4Qr(oF$QPxb!I4{VlWcvbjd%< z5zbsQRt%%VF}05MVXS`V@119krH%2F#_IPcOh~w_`t-vO-5E|81k|0_(En_rIbvi><|mm3C>+rcrU$Y9-|=3F9LzBE{>liXNnfRv3!q=NQLGwaE3vFj}rr!g)xMx#w_ zL{M0#rBEv>0)Le*W(g9e+AIXEMQHNP>Ih|opPr^0X;s;YPIph? zsIz$yn+z>jWL6`FG1Dws=9quH^6~9IX+%U%R!l}4Xm^?=hA9)zNE2Hd$y;|qKn_v6 zyc?|TSe#m-uB3|~jXX}?&{GP?R3&p8q6l7`A6hM$<2Qg*F6D|Jm%d9NSKQ3@eKjy4 zYBs1(P@$hC)rkM;fk2-d!DmN$P?nI|zCA>9FsZ(6P7`@TKod=mApkT>W>W|#Wn;7& zN)tV9Wc@-Jf!!j3Mw*Cyc7)IF97A)dWUyodZ-36+Mz_$nE9xqUWZ8~|x;dW{g_58k zB5&-@l{s$Yfa!k@hYBQ2OQnste!#JHMNnr2RT$;DHDN)}2)s6aUaZ~<)DiHRxP0Kw zl#8fTz$A}y9s9U;j5%jvd9Oa#$l}g@0>k1?c;b%L8Fp`swb|pNVR^4S$GUn3tE0M%F(D6 zah4Ox6R9V!GYBs(43TwtN7MGjX$-!|UBuBWpIk zJePcgCp!GgqFcEPa-3vxZO|J&&%ki3(%by8#C>u8L8mY$7`%B`Fcg2<+*a+E{79oi ziNrsLjE*R9#5DUR)jPY|Z4~A&Rn?O!0<2ns1{pRN*fhY0*C*afKjZ98b65a`|qBUen@#= zVH>aG(I%Z{p#09`+L(|jf3Qq(2pr#_>dww~VkGuFGda3B*H^_|`7StiVsv<L?A-K>KF9iIhv+S;Q_spp-yq9x$L(3Fn^F<%?6~{8ygh1mdu+#T& zv`)pi$r9?73CY`Q|3SyNgCKmv1z_)|>lS#IV~FvNJn4?!`6q0jv1vjq6mnNy%(K`w zLj2VVibF6d_UmF#?Yi<$S<~2raGC^93v)-=&13Nm316=Jfz#IQYY2763y?z}mQ@Cl z1+6L{3YPekbq3l)ayzwXt5w=3dTE5`8(<@^G!u_rK*DyoJ#kYQ=N{M;+0}-sCq0swU=L(gZ{9?^JS*#`jz$F1^K=}!}9AUvFAdo#Bm9s z{6Mx_?KA*EN-}>z!~+0y44BLvaJRTBSAV2ea`6FkcDxQgSGLiC8?4KYTQ-Z%;LjYw zKdmY639fp11hd|DzS1d|IL$luNU&iVgH{_?(&s2y6@b=QwnSg3P;6!3%Lr`xn}D`m zBCFbNAkBdH=pn?|a5KDMSa^fk)JL;B>qpAg;3d5`>~^A;E=w*E%;)BrNchaisuj`~ z6*)Z-4c`sp4v706jhtDxb<i(BO{-tmDY1yqM40Yb*iJ`JM0{FIyLten48l`(dh8Hv5E2E8nHmxB z^(l~ClE^8M2#YBuhYlnVa7b({%{gccpvzQUm>w|p@L2NZG|xoB=WL$KfWA0Z9(z+% zth~w1=ZD>#VWWDERny;dc+wk4Cb1&v%P%VTvUrMTc6iLo#C3}HBX<}F<6jpgT4}d(#Ah4F3Hgs$o!*-=tj}1h1cy8PEhl#1FigNd3>~(|C zfHTE9FSF^%q|>$Qr@BH8JP-(^Rq_awn&*s`ksFPd&q5QzJT{nl7oSFH(%d!PhKQRyJ$ZXjchQiC zxZip>NcvY^Si9XA8tz;5bol-KUlUteJ^zb||Cty5PZR%>6!s4b$MQ)G(7^{yUsQv$ zLy~A+*CZBz^m3zU*0?9FNLvs|iE~tK`q-pjwBe%OBuZ|jJQ)io-sz42ycc98IrcM% z_pnqgHl^IRZf#ndsTGuxJH-xWY!{ivD@hc}M-+S@%mUbyhmQi!hq~s9a$`_+hM>%D@FfDgpd( z(=+PAe}uI`6v@vTT7-b&0W|nnS$@HkU)FS2rLjU}jsq1yhK8En$k7I@CgQWjrxMqh znb`B@kkkR;Q1H4q)oCns!3;>I#P(IJy+bfP=jPbC3BglK<_6|3tE#%XSO9%n^zL7* z>iKV0CI5eK+<#j2Z!OJVdpAfy%VM1l*-QH87a#Z|Luz6)jCc*2sHkaPh0)#GaNg>2 zw6Yhv81+&kpgi)Bgt;8`1nv{v%)L0kb1)_`DpZuAUeLcpNA&q} zXp)Mhg1@M_+Wp##*s8jV{9;1V^H&jfMus z@qxi|ILsB82_?Mqur(~{4Fp#OIr6&kSi$O!yn6{rXm-UWNGo;yGlyoA1Y$CacafRA zrFt#2Z~f}+!5`4C0PUD6u&}|)9SnyMl(xzH-nDMY7-G|HR~!aMZtda!)D^k|dG6J^H0YzGe=K`Uvk*%ow}m)6%O>DD996{n zVe|uM;Ie!;hcs{V>U{d^`?!6VWvXNy=e{?|z`x9Wa;Y{G`KNpPe)umQPTt1md$7V^ z-tO=77ZfXF(N70GIDJ=q?TI}gtrsMJTMVu4yrE}cvQHKkVFjW=e$uQn=R|24uvzfr zjVMELI?CzopGp-hUtvhY1{@FtqeZ@@4Yw-1fMPJ{m3*@)9kXEY$K#55x-J~0@c~G9 zPsD5Pf+)f7$DD3%s5H+4FZhY$vG@3PfOL*s!IvLtB+LLB&QAQrlKpxj0YUtZK;R>W z<+1|#(wQf_W!#pPz(-2oC1Ms%0j{q+dEzol2{^>C>CaLbg-vubc|^}o)zp)0(-jJ* zNs}2JiN?5APt9bV9xKj|!KV6BW&iSUufGk4eLwTfO!)C5@Lx`t#lIPj@c(_n{-?0@ z53d!kC-HqR1oKvR695CFNV3Rc=Wn7r_32s~WrTZU)JiRzrY90*Rd#PRI4cZej~DMx zW*pJD9KnVs2EZt#i|zKpnhX3AA|oVO!u3?$(sA0&y<)rcElg7c?TCZn%M4|1Yk0+D zr-_9v%_U3hBO8rjV@e4Sp=abJAj%3J? zI}%HlY@TF5Ye9I~>bSw&fR+Z<&A=m!v*xN{gHDf2UU8-6fM%4kSP7=*&?FAbDZ5B8 zsZo2ik~EP^W-!zIQ4DBQO?ByY%ixXNDwGcQ-ei8zo(Ee-#HIYjl-?Vo(tp+PoL9iN zJM0VhKkMg1~9)&h%%E} z+WdsjgfUn{=yTNB{2j?r^IPXT>0MAxRyN-D6&uA(c(kimttf=AiP^!)+xx7)+poGK zZ~rm=%Q1fI3;*}JG5WXHM)aSr&B4*l(#V1KAAhDsf3>7>aL7^^wO*%#=U(>x((~0s z6wrt)piY#S(Oy%4d+|qLk6)1`;)^yQ75eOC+8hhdC$JL2X_@F?9nY`_X>M-W-Ewcq z2L+;Js-Z}UV6>s`k2%=W+Pbt4k*Annv{h@}oV35`&=f>ct~iNdIjL(5zRS%n34T}{ zeMnYZc7Tamv>&icD&iD|CA1FM>$-C`Fr=wy>H>W<^%sx{HhG7v1U&L@_+$;W2hm9+ z3T>2?q32nE4{TOvJ?_sVL5lBJ5PW%erLchjc`1KJ5u7t|`vw3g32(k=GwFEY z$_n1l^eXmBO@N&LdI6Oi?llq*M{YfSC(%57&$f!s{fs|m&U%`2BM?3)&F>16tGccs zygUNBim_y_yUP{fHj;w$)}u1SsybDh09mp<4WeYO%@c3-hrZZK8~U>LsG1@}Jr2V! zLOzA@*tvB5OAB3FHt9*jUxwW&C1(k!{P>Az31(4#$1e>yFDnc~ z*0?WG%AuFFx?!=MrOn?+HgE2S-}P@1Or@ zO%$YPp|CED%xwXb9MrP_uYkypJ%_rE-{Qx07P}DG5t<-L?|;rOk?C>VWNlet>2@Ho#JO)t-u=PH@S}X6ogY1eAXIW9`{GOBENGb$hNY zW$x~E5?ybh%N1(1&|LvPhrAndHrm{ZRT#=n^MpMy$@;yB!0AMeR_b=@{xY<>BsO3g zvrxXUT?UAg3?vX0;63aiKp*96`ce}KAxwvJLP9#Uz5JZz%wRS}lsnbX-SnQ9Pp8G$ zC;P4VXh&8uW)hR_-S*|xyWc&Nk~!|(#baI3SeT&n1v|+-uJxT(n2U~!3pk{ci{jmS zq%KSw2gpPB>QUU3xpPs2Kur4#tM^zSsqGr9aUyV##nPq7psW$6F=R-*pfyc!&C z?fybm1j!CgYHR*|`W8_-^t9;ZOv!wnEA4OuN^=Y+vNz4dW0u^_M4ndHM3dEYa_bn9 zyC9u8uyD70Jp`9h>X4uKLPoIBWhmqt+L<*J{)mo$tRi#<-g#h#rdhgyFZ^|_YtR7e z7SjLht2_zzGt6#nls-$)mgZNXsmbqch~AdU4V}~tNokzDJ^uPJ{i=+8cHZoG5nMOuJhiKeIW9um&o9 zP>?$1lcPKTc)4CG?R{|-YzKc4>{-Dj!y)RAklr(FfcQ%R#)(Yj9voZVNC2&jzD8vq{NEb7kdbeq)C2$KUF@C#lYw zP(@4*Oe>6BMWmJhh1JEREDN}oNY^6b>vHQ16SG;!Lma3k`msat@ zpi!JPSN8;44g8$q8jMrKq)e_41$!cGl*&RB3d7m z%%w9?;U(*}qJ63z=O?IX$ds%4B>_eVJGC#5MV=*dlh@?koYJLNp)UT?O$rB5Jsvcd zKB73E#3(JWUd)AUcD-Q-Se&I}=#?fcELV*P0%TV6(yV9V6i5VMa;>#y5g(2Z*Rad* zeQHS_JK-jDfNy(}g~)?rH3$fgGDTKxp_G{)6Q3PKVM^$&NQyO4SKRPZ&Ym3_7HjLl zVI2_V*~x;u{KGxsC|{F?Kb8i~PHAPMWQW#t<9r_JA+9U5&vm|4*%uLH?;HauG}lO^ zpX%;H!(E|4Z^c$Po;Xg_pr~07-Rd03qo~&mt&v0*jj84rJn~SN(W*#|fvCEf!e~WU zIUH5NXMsOIv660OI8m`bi3t;ZP=dZu9%D%5Rg8uM_^>>xh__a&0a~Xc1mnqM$SEsN zKDl(4-to+EhDebX3BHkNy9h|AOoC%7J45{wvtUpgC8VcMjgBS@9GFsjzGG&@4%&|P z6=Z3u+)=~QK6BI^{fQA^-JKv#Hu47au=x~Q_Y`IifR;yw<7iRw=4Uh=52}tfLCkF( zW;H;Y?k7I9M_yb#qEQ}IT9XpdFB+vuH7BmjdHIf(C=`k!ewabxO9YgB2B8#0)toBg zb(n|@3=4T{Op2jzCXaKUXyI>&Ny~AyWa~A>Q+kC2X)Mgd>lF=2!xat9pIL#uD`i+< zTBLC0HNSKzvPSk|_3T>9WTDvCML`6T3#Ejijs4-+C0*?_b=`ujP)L6*FRJY$Z1TY; zgLnNbbxk~(b=~o;xE>B|irHSJ&n#C5Y)?RS z;N9hWR%*J7H@3riX*k;|H$Z9r5n=*lCU}#oo9gN*6Uo$>xQLUpUCe5ZcLw?sZ472F zq!iw#iPjC><&+DoxjS45RMk70UEY074LyQG;>LF*{Uka}!gZ#%+@zKvOlJ1tgAE1@ z8#r)6eHW14Q%p2A@{lwMe(YS7&%_!ZnzvdHaH}N7POW`SL|VPW--dE9>^`LYuAGMD z`ftZ`ju!cECor*7%1h1tPRmT1F-oG>s=9$SO-dP6?JFwPup&sGQj@34YCE!BeNf0oNFax*?gZ2X0ui)1>5u`#iz$8ID8nJeJ9+ z)lxE#)E+=i8qAn(fP2buiVZu9uPk`~8Tnj+(Y3(zAmR&+l9=ix!lNUe`U%zr`Eo|i zvrp+=lILA!6MRl#x&C$nX0RPlhrE8Dml{n;flUDBORcrU8BGX|)<;?R$dygwa(7W2 zcWTn;E$S=iLZOS{s*vde%=x+%_hH4CM0pgIiJ>-QCJYt&x?Xj^Bp(H1X_Cm|oQP$Y z=}WM4bb8H&J=!)2cq`%B!zr#Q%GSG#vwzBY6paf4LZAf^Qce>SpHAB#f> z{d?F#^v4QqFgz+KY^WVJa?B0VHM?V&0~^uS;qaDF^n7S|3<6Tl?fivIXhipJ%`>US zZTF4ua0D3lxZQxlup4(Umz3+sJnUqB&ORTs$7}30Hs2h1uX=OCEwSp=YC!cqmNe_L zaiRV|K9%t5r22=AGm6at_1uQMH^TO)jv&{PsT&C^vFAd(FRXa0Db~7EJp7Yqv;MXF zFRT&r93gxBt+Q`||G)V2-?0Y9zs3&#MwU{FP)1Tg-t4Eu7tRd`FF7`a0uB8CO8W}1 zsJEbRRFIH%L6GiFK|mUol4c3%?v_p!VWp&$mhMhz>F!WckPbz5AAITK8aFK4Nff%!G6g2_pP z7^|j^PG6*CMEs=*a2<44N(KZxHX!F*B+qWfG!BrJ)2{9VyG9;(QWEvB^M!BsVNL?} z^6?44HuwjD0!f;8-Q$#T;hK!>9^Q*6TPs~U zPdAL_YdI(dF9orZqoA)+aTOs3EG>Y|10o8Or=P0!*7=+(53ySFONKqzk@wRc1nv0K zO-e^8YEZpht23U=bWsO*60_=Cdk(^qfLWX#v zksG5(8usLYHyrO4Ra!&=2xT}ScL^$_5S2=1rxMix>Sadk!@<_PiF2_=9G#1WtCj+2 z`<4b1_HD{YqA15RO*-%|xyhO^BuX%bGUa1;3 zjGF)L&`)xFtFP==MO9#{qWlnQA3Pt81>$ z**y$St)o2sTnh5eBSLs!%d*>gB4jZM1jXp{EudkBX@bJ?Cd9+lc(*gNGwU!Bo*G#* z1<39wvU9<&;PypRs!0!5kv(&eqkj}rHbGRTV7+;p;Vn}0xRC@ymPWlNA;v=d^mln> zO7>cF_fL3V*ub;mc#shD_z+O1b-IEk&f48YGg&AJvT#4R2 z4&v;ZuY?&6C!LmrMb%F5@!Bt>%dOyEb7$<*J*u2t2>e7tXq>FBO8VVhVn)wyn^Ueq z#A=aBTt~Rb?>kEOx*^-El+*@hPg_n;1GS^F$gPG!&~eCzrfM;#9Zu#t;Kf4$72c%< zc9N;8xcfhdN*?f5d25~-I3ng~%r7j01VT&pZoF&69BDf;CtT+4j)YH*ysP{imG!#_ zoW6g(dA|nt00)BFqK?9N=fjOW-Fl36Bm(~K3A_ZS9oD9=PK3=yM8Wka!3sFy@w!Hhys*^=lND<@sqKkw+Vk<@Qw@Y|M=p5{&S0rjh^IGz=oy*OPr6p z(v44f-kOMWC)1e9>UfM>+H>gB{!+Dnt~8emnM_-gv1a%4V7%nCYomTLV=j5 z?8`VY@QECA9TEjSwsFF%`;?QkPT?k6M>6y=sNTsh_LK5*SbY#jASv8>dFg$;Qs)!1 z-)c_H;2QA7(An;;_C`hDCGL%+Y9&P8T}r4z%-M;+&(Zv(vHg61ltu~E-RkDJUdNNt zNHNYnDIhPeCVSB~A?1&orNT(j((1Cu&WMSs%@}(xwB&31yfPuwt5|>*r^!c-ld4zh^S;KBlJhy7% z0AK?aoq4{gojKjxyrZ47t$^atzIovxfJ>?q^Ea!2>{@!%_3fDl7C7WDGl7G3NHqg_ zgfbaQDO%sOK5rX~0YAniJ|TaX(|nV~h^FP!#`cHokCjFQKO~1a-dCE7Ixk?_em+ta zc6n)yW9w%UZCsb8WV_E8k$Q|7j4Z$RAjc$gxtX&(IWkIf_0_A2%66NTq)N3y-X+F^ zBU1alkw80D?n>X#p|e-M`)`Glca1}GZ#Ck0Qd-(a_RFT*E08#tkS@_7YinH)3y5vj zd~pc=_AKQK+hhittIt$cRT=U5-LovDq7#Wz!wx*bcA)#Xpp$QH%egP%rUM`1Y57ep z;_yZ+WJ1V4&y+&$sAM(m%Ec-o<41<&wu5D^b4f5U)8DX`ZHEaOB*tXs*b!#oN$P zakvO?J{sM!yax`J=|!*s_wa+&uh4cVnaQuk-an+ ztAG`Aov2z)pqD9diiFgn0CNp}C}b19=q4>Ua5$GHZq73aScVpWejmFa5@(;k%2E$GDG@_}rMPyXjd7XVg?$=I5j8~zgv zujvXf#rH;x>MccmMTtoYkfHFS6smwniA*;aebU_=(Kg+8Gv#4|6}Yc)!zv~gVc3HI zJ@MD-PZoKqnt4wK5bleV;CwfHui{poIB?M15j5kvP_TV=k-$$CkMbhc^VJ1!-4}gt zowLndmYd@tV=BF%;#N*Vu9#Vh2HSHuDX88zi8Z&4TF)$x>FPOWtXlJScOba*p;vz*odX$fHo7F89IS z!{T=uE*5`Ts_HxaxZFH>`lQffczm?KFgA zoffN&)CfVtV2{rJ*8~;3DSDz)>o+S0OqhIqnC{~y%ONmSJwzAY)HPsZv&0+5~QQh(*bq>hng-8s@mV^6wrDzi3pb7>NgFsseErfWFX{(X5YAeWw1PygN8X7WcDdI^QR=<#> zu=MMhJifv29r_z#%FJ(uw9}TV5_&4i1Gz&j@S52IgUiI}H}B7oAxv4~?p(Mc&l2IO z+H*NDWR0#AH)*3C3aKTpMW7^ff5R6kE*X~Af8{i=TuyT~`~1`UipBh1Uy}U8h*8{; z$HqHp;z0_BFRja}dKVl|^dnRvOBxT;!NHCF(WtM`Pnyum7R$}wnXgSvzPCPM;&n#< z!Pd`8H4yJ<-#a=h*}3o%5#yjO18DG4DvK{6T@eu*`$wKZ|$aI5}DMBE84rN zp1_A#W*W`a=gwJjcld=TN=vWzJXSbd>&G)(u;Y7an|lQIIaU`z`w~$dFGtI}A?Ls2 zCDG!t&F?I!Z-DKbxcy@i+&b@ddxcZ0Q7qMAIL4mLyl32+;;7A!+SZPj&nC=QafCxomk6Z5NR7KdYGF=H~QMf6F8Rn+&~iLXPJvWgU}LbZ1^*dMcHiA z-$m~z9F++zZkK|SX0!60G$>0_Mqg-5?^|F2yU-nSGSQqz8IG#Ky8h~0@iW$$tJaAm z4Y`Tm3RC^La-M?}>)Y?Q-KD4U2js_21R0-ZPsVglRp@ucgCSWa{o-ZzX#!7%EyvC$ z&fRBPy^U1or$3O_yNY>AKTfKWOX+KEi83BP!yl3f32abvY7Exye|UFjPTaeB`EHV! z8SWV3`6z0nZ7Ks?o5*kwz1i!%2o!g^wPTc;7kfMS#|-q4+sAo)f_V%_eW%qaJ~}40 z(~K_ObkJPQ3+nh1Y|GW1+g7ui@g?Y9N_QN~KP)nLR+1lAEnquG_kEOFW_JTJ(CT4` z>}=@}C%)q4FYxlbBU+1BIXLpDkkoSz8=dc!;-ai?7&4-T0Q`^y%CDbhuHbfefkicqWSG=Ku5ZP?fWBNOfI9mF0F7rDyd4z z#HdB!3&sUkL66&JG^0f8<+8w>mrK?w1GrIDcPq=1vFg*(4aWn89raPC!i>9iPtO?L z4)qbguP+{Ms`51*zDZ9qIqOd$xaaoT>&0eXx~>R|PTJU53BiR)My7h6()XjvC$%RQ zkQzL*ZxTF>w*tyUF}dvXopCa-|rH}J=AUQ0JQH&rl+`#Pzs3Bb{@a; z>BzdEVvW@u+R?8+yc>Qin7-Nj(eoD1K-YMqO``32H2e96=Pc~QG?JeTXE;U@iXv2h zoGmZX9x3(t$~_<(D(8Pp*30rBuG)bC=g>~nLA^n%liI(HXhEoz2OBxcI?SGu0i2cv zzoll;_P~~CAq6eNTO2dvAm85zTV8(t8|cWHhcT3htZ+c5-GQ*lfxEixmgiT}0=|Zx z%1N4seRo3jKRvB052z#2ZDV+I6iYkGv!_s3PFw#_x-M$|Hk}P>FQ(0Q?h)iG6YbD; zHy=y>O?j08k4}8cGUts_c^|%JS=95fWUP)VlvmumF7}T%Od7sG$~E<#k0PHSWzgZ_ zwc!hpG}`k8atMT5Lfd486`TUs7ZlAp*L$!{I|`XZ+WOS_y*4Dmb~w=U#Q*>p{b@| z(m52nDFyaFB~-PAEKwLVaEQ0{<0UYx>dEFy4!0e3(v=xHYVw9sM2-@5AeW!#D=7i z+1UDc(+5Q6BzJlatmqkdym`N8-mnEHgPS^gIcWog*CA`Xi`$}A z?*X2U^@)UUDF>q#zE`baPM=rer=ut@LMO=$jfkjDn`2(iPN!b1$iBNrS-cg}BMR24 zA1sIzxp6p8i)|?RcXNLS7PNy7h%H=w zJ5bF_UBWRQYA7<}zMTPqpLiRl>Cv4+@;`1xY}IL?Z4Y^7=(P==wra3bo1fI-qIyWR z5O5+;IfQ3*RE%togPDuGb$)bkpz({m!wMz?dk4+~_Q{0pb1CFTq93Mxq>~s3?H3M2 zt%>c%9Y}tLmdx|$yxiI5vl_aQqHm$#mt)GA6r*6zOu7xMJ$#2Vyi`&8fb@7fYxYML$t zpK#8_qc}?W7RmkZQZ3$;6{?^JqU1nR#SXJ=6_04bmV29?ZfL$PgJ;=m2Eb0fhg3c| zY~U_mr90hxNBzzsV;yc?lg8|$jy{2yV zk%`m8g4qhPS`CN&T;2>#Ckh`q1dYBjx?U19c6-*F1uE_eO%(?^6v+hQCXW1?FbR3mb)<8ZsaQY7XfiH8d*Eq*T2 zlb%=2^~m&X_qvb%yxi}#v`Jrq)JHi9b+uP2r*Cz}5=<;OTkJVcQ6&ICr)v5~PB&EM z%Y|yv3?898G>R7`e^MF5!5-NiMiM<9!ro_iKV(6;{h_5Y+N%f^geHtz6LG8CGM}^| zOIbpRhIhvP<}X0vEu`Sij2R0xVcU3_skpURCAQRL56;P^Ja%dNK)+Ke74l!(<(#21nV$ zzuq}XK0I;Y$EeJtPD0jGmTgzhdy=M~K6YBs+;O%sTD!Az`qgWqLOpaQaKV1N9iTPi z#p4A@AGsYJmDLS0Itd7(Fdenf!r2RIFJ41Zz9^g{Iig7-3ql7TaSjFYn`*=>n(2&e zfm^@iyG8e(xk|T8&7XhW3G{wTC4aHGL(Su``{9#1t7%@1rb~>L?n!O7wL6yQ>Ps(D zpU^Y9r0!=wMnv-)e6(Gq-I}WL7J-F$|G;aO7GN@uS*WUq;Jk z8uv3Z_f5`ssOGfRx3?Eu7}V%O0*k-Y`dW`=e0A1ER{qjdv`jZEx4xLIcMk)xcxr62 zMG^15UW|zRH|=)q464(`JL}q&_m+?n*HX`GWUc#_`I#PW?prULgWd%(Q78toHJ?q? z-&W$XGn?MIjXa>^KHfpO89rM30aU+F+vNUyI{-NH{S@9kM%#Dywl-nkfp^G<5+ovi z%4wU-sdo`-w|#o>n3)XA%by|BL~V5nnc=&c9IrmF@h_~pkHNJy-sPJ8qC@)DoUPh9 zff@nrhmSM@TwA-Ys{}GAIraYAbx*_=>3{4CvHoCsw9s@){*Zd1DRi*Q_EzNL*pG2P zf@0hf{sz=qfQJS0d!t)6x1tG-sHjc|j{3U3Aw{&8vBowrYb|16cHDec*ttz#)Rf$6 z5k_@mk9|tO3K%i$ow&NPc4Fsi`|Lg$(i z4S{+xtq$oUrtRPbrqd8fL^FGMmIa*dVc@|bkT+POGn9*0r2f3|E>S(6>*>UURw>olBI%DU{!#y;{Z4GqvGlnHe+VDnC#$FoP`mdqB84)II zZUI^Z6TWtcYBTJd13Z`^yRgN?ZYpqU_5FfXJN}Z`u!Ht4<84=F@NB)MM`=qZ0!X|A%WqUW;f!PmhA>iP&px z@N@k2A*u0ZYdT{{Y`=b+_Qu6}wDr}7Q_j{@*!j4@7&Gbg=bpapg7xf8$k{F7l@yWY zzUNm$RYd=>qEJ%`4Wn>iF?Mx)YHV$6<)&-wYG`b1WNu^n_p^h8xij>AD_a{=T@%P3 z>)PmB8#6n)Iy%@%GBR~_vM6nJ4M?j!FWtp<0J&8^W#3i8LA?RB>4l!oH;5p-$RMvv zn;xpcBnf%r7qLU0e%ka)X&jskOl+-;jGvOam^+%0I@swOLS+2sb2cdRo~~YL6;}A4 z7u}~v&F?_g$8fhHuYc!5`#T>fA}O>@uzapG?YV+@otAklPY%jbEEPUV8$v)=P8&+B z8;V;%bi!ap;IcK*-iEG*40J^iOYpDTdtw0L6Oa!`J_ zVW51C0XgK;YoqtiILSc{za5Qj9L#NPeod9jrh$Jl0d9OThaNHEU11Uz z%XnpqF#OKM@UQ$VMl3Rrc}c4Ovy39?&zWg$#1tfq@|+ZaaOmT5G%K)5Ohl%T6ODX~ z@VCz)`#P*8#PHFYZVkj zR5av7;aw!9I%to0qC9z~3=zfbKZX56kNgt$FFDzmt&Kpce|>q_7nhW7-|~z2IFDV~ z`%YQ(Lqs69PNAxz{GC>xcUREEw*VTbASZdQ)B*BswPYkd|_HcB;%W^eyn$ld_?XKBKhs~Mxgr5`?M~8 zG~gH+%)nva1?)gnLW=l}pCZ>pwXzsO#7(^-@>t!~2}=$6^xAy-r46oX0GK%i`Huel zx_}k#w{wAqsekf2LEJ-ze(hL)^1E>p00)3WLPCPm{-7v@pe!^a2zej+0>DAe53Z1` zn6faFl$(9_QK0u>{(M~F0(mq?H&A&5f%9qjM>xs=Loc7MA-*AaO; zo|;iYE&&rlbmLX=$WZtK8uh>Hbx;o!<4x(=$-FDU|IYkn6G=9J|+6l}F+ zs!JTmQy|1r{0{YZOaHo={AcN4>#vetf%Jq7|0U#~i#A^u5VqhUG@bG#ec<>F4fag@ zIuLBVysIM+$MYM|^*x*GV6ZjEp!upUDIKB-F53uAN%)sgmj(B(W5L!^fm-KFDiFAi z1+`JvYL;DxgRPbTHM5sQ2C+t0Mf-a+|4@$QIx1|wYp8|2q>#tgQGW?}JqzP?P}n4o zSI3rI@j9q1Y~t+eK(L9$uFhf~jq5=Fk|FXsBy24A)e%-&nr~DqoJx&zL`0OlY_>Y{>4_Cdb+RE9S3}`3({p>kAv)361r=q%HgZ8S+=; zFRY}{$WYkO$E&T!cl=k_YjL8mn9!g}*tk53OF1D=N>2ZZ2^&HQ3kVJ8gAMAss_~Is zegphta33rp)F8ozeL#c3E{VtuX5;L)(6H+`uvb8@cAu=*6`cPF8M%%Ld))}SSA9tV z5Nr9<+%1cmj}xb>bk)? zTTs{Sl16@K_-p9~>tJ17#$CsU^{1}d65jOR8U$_HA00SYRH!co>n%YSg_l(IXVhPw z87vaiyMlF#t|GB!{82tA(obIt776M`!TLT>r{R*q{~Za&se;9WdQ7nH3Dje{q=Vn_ zez_!n@Sm;&!#W#R1x?HRt;xTDFMXZsc(9(pRXpab{|OK3u)yL${Rh~U{#A7~_dDLN ZDS3SwBHV^{7!Bly9Fi+kF&p~Z{{!k02`&Ht literal 0 HcmV?d00001 diff --git a/collate.bat b/collate.bat new file mode 100644 index 000000000000..741fde7c69e2 --- /dev/null +++ b/collate.bat @@ -0,0 +1,3 @@ +java -jar Collate-TUI.jar collate from src/main to collated/main include java, fxml, css +java -jar Collate-TUI.jar collate from src/test to collated/test include java +java -jar Collate-TUI.jar collate from docs to collated/docs include md, html From 6a79168169743d4b9955b1ab50219968117c6ea8 Mon Sep 17 00:00:00 2001 From: qhng Date: Sat, 29 Oct 2016 22:39:43 +0800 Subject: [PATCH 4/4] Update collates --- collated/docs/A0139915W.md | 2 +- collated/main/A0097627N.md | 32 +- collated/main/A0139915W.md | 516 ++++++++++++++---- collated/main/A0139915Wreused.md | 17 - collated/test/A0139915W.md | 466 +++++++++++++++- .../savvytasker/commons/util/StringUtil.java | 2 +- .../commons/util/SmartDefaultDatesTest.java | 2 + .../savvytasker/model/task/TaskListTest.java | 2 + 8 files changed, 870 insertions(+), 169 deletions(-) delete mode 100644 collated/main/A0139915Wreused.md diff --git a/collated/docs/A0139915W.md b/collated/docs/A0139915W.md index 5ab0becbc7be..706182214990 100644 --- a/collated/docs/A0139915W.md +++ b/collated/docs/A0139915W.md @@ -79,7 +79,7 @@ Format: `add TASK_NAME [s/START_DATE] [e/END_DATE] [l/LOCATION] [p/PRIORITY_LEVE > LOCATION | `Optional` Specifies the location where the task happens. > PRIORITY_LEVEL | `Optional` Specifies the priority level of the task.
`Accepts` values `low`, `medium`, `high`
`Defaults` to `???` > RECURRING_TYPE | `Optional` Specifies the recurring type of the task.
`Accepts` values `none`, `daily`, `weekly`, `monthly`, `yearly`
`Defaults` to `none` -> NUMBER_OF_RECURRENCE | `Optional` Specifies the number of times the task recurrs. A value of 0 specifies a never-ending recurrence.
`Defaults` to `0`
`Ignored` if RECURRING_TYPE is `none` +> NUMBER_OF_RECURRENCE | `Optional` Specifies the number of times the task recurrs.
`Defaults` to `1`
`Ignored` if RECURRING_TYPE is `none` > CATEGORY | `Optional` Specifies a custom category for the task. This can be used for keeping track of similar tasks. > DESCRIPTION | `Optional` Describes the task. diff --git a/collated/main/A0097627N.md b/collated/main/A0097627N.md index 8e35367693b4..92c44c35f05b 100644 --- a/collated/main/A0097627N.md +++ b/collated/main/A0097627N.md @@ -17,7 +17,7 @@ @Override public boolean redo() { execute(); - return false; + return true; } /** @@ -26,20 +26,18 @@ */ @Override public boolean undo() { - - UnmodifiableObservableList lastShownList = model.getFilteredTaskListTask(); - - for (int i = 0; i < lastShownList.size(); i++) { - if (lastShownList.get(i) == toAdd){ - ReadOnlyTask taskToDelete = lastShownList.get(i); - try { - model.deleteTask(taskToDelete); - } catch (TaskNotFoundException e) { - e.printStackTrace(); - } + Iterator itr = tasksAdded.iterator(); + while (itr.hasNext()) { + try { + model.deleteTask(itr.next()); + } catch (TaskNotFoundException e) { + // do nothing. } - } - return false; + } + // clears the list of tasks added. + // if redo is performed, the list will be populated again. + tasksAdded.clear(); + return true; } /** @@ -501,8 +499,6 @@ } } catch (TaskNotFoundException pnfe) { assert false : "The target task cannot be missing"; - } catch (DuplicateTaskException e) { - e.printStackTrace(); } catch (InvalidDateException e) { assert false : "The target task should be valid, only the archived flag is set"; } @@ -895,9 +891,7 @@ } } catch (TaskNotFoundException pnfe) { assert false : "The target task cannot be missing"; - } catch (DuplicateTaskException e) { - e.printStackTrace(); - }catch (InvalidDateException e) { + } catch (InvalidDateException e) { assert false : "The target task should be valid, only the archived flag is set"; } return new CommandResult(resultSb.toString()); diff --git a/collated/main/A0139915W.md b/collated/main/A0139915W.md index 0942cfeac070..8212334cfbc4 100644 --- a/collated/main/A0139915W.md +++ b/collated/main/A0139915W.md @@ -86,6 +86,7 @@ public class SmartDefaultDates { calendar.set(Calendar.HOUR_OF_DAY, 23); calendar.set(Calendar.MINUTE, 59); calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MILLISECOND, 0); } return calendar.getTime(); } @@ -105,7 +106,13 @@ public class SmartDefaultDates { calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); this.startDateTime = calendar.getTime(); + + if (this.startDateTime.compareTo(this.endDateTime) > 0) { + // end date is before today, leave start date as null + this.startDateTime = null; + } } @@ -134,6 +141,7 @@ public class SmartDefaultDates { calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); } return calendar.getTime(); } @@ -154,6 +162,7 @@ public class SmartDefaultDates { calendar.set(Calendar.HOUR_OF_DAY, 23); calendar.set(Calendar.MINUTE, 59); calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MILLISECOND, 0); this.endDateTime = calendar.getTime(); } @@ -169,11 +178,6 @@ public class SmartDefaultDates { Date end = getEnd(endDateTime); this.startDateTime = start; this.endDateTime = end; - if (this.startDateTime.compareTo(this.endDateTime) > 0) { - calendar.setTime(this.endDateTime); - calendar.add(Calendar.DATE, 7); - this.endDateTime = calendar.getTime(); - } } public Date getStartDate() { @@ -185,6 +189,22 @@ public class SmartDefaultDates { } } ``` +###### \java\seedu\savvytasker\commons\util\StringUtil.java +``` java + // reused original implementation of 'containsIgnoreCase' to find exact matches + public static boolean containsExactIgnoreCase(String source, String query) { + List strings = Arrays.asList(source); + return strings.stream().filter(s -> s.equalsIgnoreCase(query)).count() > 0; + } + + // reused original implementation of 'containsIgnoreCase' to find partial matches + public static boolean containsPartialIgnoreCase(String source, String query) { + if (source == null) return false; + String[] split = source.toLowerCase().split("\\s+"); + List strings = Arrays.asList(split); + return strings.stream().filter(s -> s.contains(query.toLowerCase())).count() > 0; + } +``` ###### \java\seedu\savvytasker\logic\commands\AddCommand.java ``` java /** @@ -202,16 +222,24 @@ public class SmartDefaultDates { this.numberOfRecurrence = numberOfRecurrence; this.category = category; this.description = description; + tasksAdded = new LinkedList(); } private void createTask() { final boolean isArchived = false; // all tasks are first added as active tasks final int taskId = 0; // taskId to be assigned by ModelManager, leave as 0 + final int groupId = 0; // groupId to be assigned by ModelManager, leave as 0 - this.toAdd = new Task(taskId, taskName, startDateTime, endDateTime, + this.toAdd = new Task(taskId, groupId, taskName, startDateTime, endDateTime, location, priority, recurringType, numberOfRecurrence, category, description, isArchived); } + + private void addToListOfTasksAdded(Task... tasks) { + for (Task t : tasks) { + tasksAdded.add(t); + } + } @Override public CommandResult execute() { @@ -219,15 +247,41 @@ public class SmartDefaultDates { createTask(); try { - model.addTask(toAdd); + Task taskAdded = null; + if (toAdd.getRecurringType() == RecurrenceType.None) { + // not a recurring task, add a single task + taskAdded = model.addTask(toAdd); + addToListOfTasksAdded(taskAdded); + } else { + // a recurring task, add a group of recurring tasks + LinkedList tasksAdded = model.addRecurringTask(toAdd); + taskAdded = tasksAdded.peekFirst(); + addToListOfTasksAdded(tasksAdded.toArray(new Task[tasksAdded.size()])); + } + + int targetIndex = getIndexOfTask(taskAdded); + if (targetIndex >= 0) { + EventsCenter.getInstance().post(new JumpToListRequestEvent(targetIndex)); + } else { + // GUI should never ever get here + } return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); - } catch (DuplicateTaskException e) { - return new CommandResult(MESSAGE_DUPLICATE_TASK); } catch (InvalidDateException ex) { return new CommandResult(MESSAGE_INVALID_START_END); } } + + /** + * Helper method to retrieve the index of the task in the tasklist that was added. + * @param task The task to find + * @return Returns the index of the task in the list, -1 if not found. + */ + private int getIndexOfTask(Task task) { + model.updateFilteredListToShowActive(); //because newly added tasks are all active. + UnmodifiableObservableList lastShownList = model.getFilteredTaskList(); + return lastShownList.indexOf(task); + } ``` ###### \java\seedu\savvytasker\logic\commands\DeleteCommand.java ``` java @@ -258,7 +312,7 @@ public class SmartDefaultDates { //tasksToUndo.add((Task)taskToDelete); resultSb.append(String.format(MESSAGE_DELETE_TASK_SUCCESS, taskToDelete)); } - } catch (TaskNotFoundException pnfe) { + } catch (TaskNotFoundException tnfe) { assert false : "The target task cannot be missing"; } @@ -312,8 +366,6 @@ public class SmartDefaultDates { case Archived: model.updateFilteredListToShowArchived(); break; - default: - assert false; // should not reach here } return new CommandResult(getMessageForTaskListShownSummary(model.getFilteredTaskList().size())); } @@ -358,7 +410,13 @@ public class SmartDefaultDates { try { originalTask = (Task)taskToModify; - model.modifyTask(taskToModify, replacement); + Task taskModified = model.modifyTask(taskToModify, replacement); + int targetIndex = getIndexOfTask(taskModified); + if (targetIndex >= 0) { + EventsCenter.getInstance().post(new JumpToListRequestEvent(targetIndex)); + } else { + // GUI should never ever get here + } } catch (TaskNotFoundException e) { assert false : "The target task cannot be missing"; } catch (InvalidDateException ex) { @@ -367,6 +425,16 @@ public class SmartDefaultDates { return new CommandResult(String.format(MESSAGE_SUCCESS, replacement)); } + + /** + * Helper method to retrieve the index of the task in the tasklist that was added. + * @param task The task to find + * @return Returns the index of the task in the list, -1 if not found. + */ + private int getIndexOfTask(Task task) { + UnmodifiableObservableList lastShownList = model.getFilteredTaskList(); + return lastShownList.indexOf(task); + } ``` ###### \java\seedu\savvytasker\logic\parser\FindCommandParser.java ``` java @@ -386,22 +454,37 @@ public class SmartDefaultDates { ``` ###### \java\seedu\savvytasker\model\Model.java ``` java - /** Deletes the given Task. */ - void deleteTask(ReadOnlyTask target) throws TaskNotFoundException; + /** + * Deletes the given Task. + * @throws {@link TaskNotFoundException} if the task does not exist + * @return Returns a Task if the delete operation is successful, an exception is thrown otherwise. + * */ + Task deleteTask(ReadOnlyTask target) throws TaskNotFoundException; - /** Modifies the given Task. */ - void modifyTask(ReadOnlyTask target, Task replacement) throws TaskNotFoundException, InvalidDateException; + /** + * Modifies the given Task. + * @throws {@link TaskNotFoundException} if the task does not exist + * @throws {@link InvalidDateException} if the end date is earlier than the start date + * @return Returns a Task if the modify operation is successful, an exception is thrown otherwise. + * */ + Task modifyTask(ReadOnlyTask target, Task replacement) throws TaskNotFoundException, InvalidDateException; /** Adds the given Task. * @throws {@link DuplicateTaskException} if a duplicate is found + * @throws {@link InvalidDateException} if the end date is earlier than the start date + * @return Returns a Task if the add operation is successful, an exception is thrown otherwise. * */ - void addTask(Task task) throws DuplicateTaskException, InvalidDateException; + Task addTask(Task task) throws InvalidDateException; + + /** Adds the given Task as a recurring task. The task's recurrence type must not be null. + * @throws {@link DuplicateTaskException} if a duplicate is found + * @throws {@link InvalidDateException} if the end date is earlier than the start date + * @return Returns the list of Tasks added if the add operation is successful, an exception is thrown otherwise. + * */ + LinkedList addRecurringTask(Task task) throws InvalidDateException; /** Returns the filtered task list as an {@code UnmodifiableObservableList} */ UnmodifiableObservableList getFilteredTaskList(); - - /** Returns the filtered task list as an {@code UnmodifiableObservableList} */ - UnmodifiableObservableList getFilteredTaskListTask(); /** Updates the filter of the filtered task list to show all active tasks sorted by due date */ void updateFilteredListToShowActiveSortedByDueDate(); @@ -436,7 +519,7 @@ public class SmartDefaultDates { savvyTasker = new SavvyTasker(src); filteredTasks = new FilteredList<>(savvyTasker.getTasks()); - sortedAndFilteredTasks = new SortedList<>(filteredTasks, new TaskSortedByDefault()); + sortedAndFilteredTasks = new SortedList<>(filteredTasks, new TaskSortedByDueDate()); updateFilteredListToShowActive(); // shows only active tasks on start } @@ -447,30 +530,40 @@ public class SmartDefaultDates { public ModelManager(ReadOnlySavvyTasker initialData) { savvyTasker = new SavvyTasker(initialData); filteredTasks = new FilteredList<>(savvyTasker.getTasks()); - sortedAndFilteredTasks = new SortedList<>(filteredTasks, new TaskSortedByDefault()); + sortedAndFilteredTasks = new SortedList<>(filteredTasks, new TaskSortedByDueDate()); updateFilteredListToShowActive(); // shows only active tasks on start } ``` ###### \java\seedu\savvytasker\model\ModelManager.java ``` java @Override - public synchronized void deleteTask(ReadOnlyTask target) throws TaskNotFoundException { - savvyTasker.removeTask(target); + public synchronized Task deleteTask(ReadOnlyTask target) throws TaskNotFoundException { + Task taskDeleted = savvyTasker.removeTask(target); indicateSavvyTaskerChanged(); + return taskDeleted; } @Override - public void modifyTask(ReadOnlyTask target, Task replacement) throws TaskNotFoundException, InvalidDateException { - savvyTasker.replaceTask(target, replacement); + public synchronized Task modifyTask(ReadOnlyTask target, Task replacement) throws TaskNotFoundException, InvalidDateException { + Task taskModified = savvyTasker.replaceTask(target, replacement); indicateSavvyTaskerChanged(); + return taskModified; } @Override - public synchronized void addTask(Task t) throws DuplicateTaskException, InvalidDateException { - t.setId(savvyTasker.getNextTaskId()); - savvyTasker.addTask(t); + public synchronized Task addTask(Task t) throws InvalidDateException { + Task taskAdded = savvyTasker.addTask(t); updateFilteredListToShowActive(); indicateSavvyTaskerChanged(); + return taskAdded; + } + + @Override + public synchronized LinkedList addRecurringTask(Task recurringTask) throws InvalidDateException { + LinkedList recurringTasks = savvyTasker.addRecurringTasks(recurringTask); + updateFilteredListToShowActive(); + indicateSavvyTaskerChanged(); + return recurringTasks; } ``` ###### \java\seedu\savvytasker\model\ModelManager.java @@ -479,11 +572,6 @@ public class SmartDefaultDates { public UnmodifiableObservableList getFilteredTaskList() { return new UnmodifiableObservableList(sortedAndFilteredTasks); } - - @Override - public UnmodifiableObservableList getFilteredTaskListTask() { - return new UnmodifiableObservableList(sortedAndFilteredTasks); - } @Override public void updateFilteredListToShowActiveSortedByDueDate() { @@ -762,18 +850,7 @@ public class SmartDefaultDates { else if (task1 == null) return 1; else if (task2 == null) return -1; else { - // Priority Level can be nulls - // Check for existence of priorityLevel before comparing - if (task1.getPriority() == null && - task2.getPriority() == null) { - return 0; - } else if (task1.getPriority() == null) { - return 1; - } else if (task2.getPriority() == null) { - return -1; - } else { - return task2.getPriority().compareTo(task1.getPriority()); - } + return task2.getPriority().compareTo(task1.getPriority()); } } @@ -795,40 +872,162 @@ public class SmartDefaultDates { ``` ###### \java\seedu\savvytasker\model\SavvyTasker.java ``` java + /** - * Returns the next available id for use to uniquely identify a task. - * @author A0139915W - * @return The next available id. + * Adds a task to savvy tasker. + * @throws {@link InvalidDateException} if the end date is earlier than the start date + * @return Returns the task added if the operation succeeds, an exception is thrown otherwise. */ - public int getNextTaskId() { - return tasks.getNextId(); + public Task addTask(Task t) throws InvalidDateException { + // guarantees unique ID + t.setId(tasks.getNextId()); + try { + return tasks.add(t); + } catch (DuplicateTaskException e) { + // should never get here. + return null; + } } /** - * Adds a task to savvy tasker. - * @throws TaskList.DuplicateTaskException if an equivalent task already exists. + * Adds a group of recurring tasks to savvy tasker. + * @throws {@link InvalidDateException} if the end date is earlier than the start date + * @return Returns the list of recurring tasks if the operation succeeds, an exception is thrown otherwise + */ + public LinkedList addRecurringTasks(Task recurringTask) throws InvalidDateException { + LinkedList tasksToAdd = + createRecurringTasks(recurringTask, recurringTask.getRecurringType(), + recurringTask.getNumberOfRecurrence()); + Iterator itr = tasksToAdd.iterator(); + + while(itr.hasNext()) { + // this will be an atomic operation + // guaranteed no duplicates + // if the start/end dates are invalid, + // the first task to be added will fail immediately, + // subsequent tasks will not be added + try { + tasks.add(itr.next()); + } catch (DuplicateTaskException e) { + // should never get here. + return null; + } + } + return tasksToAdd; + } + + /** + * Creates a list of recurring tasks to be added into savvy tasker. + * @param recurringTask the task that recurs + * @param recurringType the type of recurrence + * @param numberOfRecurrences the number of recurrences + * @return A list of tasks that represents a recurring task. + */ + private LinkedList createRecurringTasks(Task recurringTask, RecurrenceType recurringType, int numberOfRecurrences) { + assert recurringTask.getRecurringType() != null; + assert recurringTask.getNumberOfRecurrence() > 0; + + LinkedList listOfTasks = new LinkedList(); + recurringTask.setGroupId(tasks.getNextGroupId()); + + for (int i = 0; i < numberOfRecurrences; ++i) { + Task t = recurringTask.clone(); + // guarantees uniqueness + t.setId(tasks.getNextId()); + listOfTasks.add(setDatesForRecurringType(t, recurringType, i)); + } + + return listOfTasks; + } + + /** + * Helper function for createRecurringTasks(). Sets the respective start/end datetime for the + * i-th recurring task to be added + * @param t The first recurring task + * @param recurringType the type of recurrence + * @param index the index of the loop + * @return A task with its respective datetime set. */ - public void addTask(Task t) throws DuplicateTaskException, InvalidDateException { - tasks.add(t); + private Task setDatesForRecurringType(Task t, RecurrenceType recurringType, int index) { + Date startDate = t.getStartDateTime(); + Date endDate = t.getEndDateTime(); + Calendar calendar = Calendar.getInstance(); + switch (recurringType) { + case Daily: // add one day to the i-th task + if (startDate != null) { + calendar.setTime(startDate); + calendar.add(Calendar.DATE, 1 * index); + startDate = calendar.getTime(); + } + if (endDate != null) { + calendar.setTime(endDate); + calendar.add(Calendar.DATE, 1 * index); + endDate = calendar.getTime(); + } + break; + case Weekly: // add 7 days to the i-th task + if (startDate != null) { + calendar.setTime(startDate); + calendar.add(Calendar.DATE, 7 * index); + startDate = calendar.getTime(); + } + if (endDate != null) { + calendar.setTime(endDate); + calendar.add(Calendar.DATE, 7 * index); + endDate = calendar.getTime(); + } + break; + case Monthly: // add 1 month to the i-th task + if (startDate != null) { + calendar.setTime(startDate); + calendar.add(Calendar.MONTH, 1 * index); + startDate = calendar.getTime(); + } + if (endDate != null) { + calendar.setTime(endDate); + calendar.add(Calendar.MONTH, 1 * index); + endDate = calendar.getTime(); + } + break; + case Yearly: // add 1 year to the i-th task + if (startDate != null) { + calendar.setTime(startDate); + calendar.add(Calendar.YEAR, 1 * index); + startDate = calendar.getTime(); + } + if (endDate != null) { + calendar.setTime(endDate); + calendar.add(Calendar.YEAR, 1 * index); + endDate = calendar.getTime(); + } + break; + case None: + default: + assert false; // should not come here + } + t.setStartDateTime(startDate); + t.setEndDateTime(endDate); + return t; } /** * Removes a task from savvy tasker. * @param key the task to be removed - * @return true if the task is removed successfully - * @throws TaskNotFoundException if the task to be removed does not exist + * @throws {@link TaskNotFoundException} if the task does not exist + * @return Returns a Task if the remove operation is successful, an exception is thrown otherwise. */ - public boolean removeTask(ReadOnlyTask key) throws TaskNotFoundException { + public Task removeTask(ReadOnlyTask key) throws TaskNotFoundException { return tasks.remove(key); } /** * Replaces a task from savvy tasker. * @param key the task to be replaced + * @throws {@link TaskNotFoundException} if the task does not exist + * @throws {@link InvalidDateException} if the end date is earlier than the start date * @return true if the task is removed successfully - * @throws TaskNotFoundException if the task to be removed does not exist */ - public boolean replaceTask(ReadOnlyTask key, Task replacement) throws TaskNotFoundException, InvalidDateException { + public Task replaceTask(ReadOnlyTask key, Task replacement) throws TaskNotFoundException, InvalidDateException { return tasks.replace(key, replacement); } ``` @@ -871,6 +1070,7 @@ public class SmartDefaultDates { public interface ReadOnlyTask { int getId(); + int getGroupId(); boolean isMarked(); boolean isArchived(); String getTaskName(); @@ -900,23 +1100,68 @@ public interface ReadOnlyTask { .append(" Task Name: ") .append(getTaskName()) .append(" Archived: ") - .append(isArchived()) - .append(" Start: ") - .append(getStartDateTime()) - .append(" End: ") - .append(getEndDateTime()) - .append(" Location: ") - .append(getLocation()) - .append(" Priority: ") + .append(isArchived()); + if (getStartDateTime() != null) { + builder.append(" Start: ") + .append(getStartDateTime()); + } + if (getEndDateTime() != null) { + builder.append(" End: ") + .append(getEndDateTime()); + } + if (getLocation() != null && !getLocation().isEmpty()) { + builder.append(" Location: ") + .append(getLocation()); + } + builder.append(" Priority: ") + .append(getPriority()); + if (getCategory() != null && !getCategory().isEmpty()) { + builder.append(" Category: ") + .append(getCategory()); + } + if (getDescription() != null && !getDescription().isEmpty()) { + builder.append(" Description: ") + . append(getDescription()); + } + return builder.toString(); + } + + + /** + * Formats the task as text, showing all task details, formatted for the UI. + */ + default String getTextForUi() { + final StringBuilder builder = new StringBuilder(); + if (getStartDateTime() != null) { + builder.append(" Start: ") + .append(getStartDateTime()) + .append("\n"); + } + if (getEndDateTime() != null) { + builder.append(" End: ") + .append(getEndDateTime()) + .append("\n"); + } + if (getLocation() != null && !getLocation().isEmpty()) { + builder.append(" Location: ") + .append(getLocation()) + .append("\n"); + } + builder.append(" Priority: ") .append(getPriority()) - .append(" Recurring Type: ") - .append(getRecurringType()) - .append(" Nr. Recurrence: ") - .append(getNumberOfRecurrence()) - .append(" Category: ") - .append(getCategory()) - .append(" Description: ") - .append(getDescription()); + .append("\n"); + if (getCategory() != null && !getCategory().isEmpty()) { + builder.append(" Category: ") + .append(getCategory()) + .append("\n"); + } + if (getDescription() != null && !getDescription().isEmpty()) { + builder.append(" Description: ") + .append(getDescription()) + .append("\n"); + } + builder.append(" Archived: ") + .append(isArchived()); return builder.toString(); } @@ -930,6 +1175,7 @@ public interface ReadOnlyTask { public class Task implements ReadOnlyTask { private int id; + private int groupId; private String taskName; private Date startDateTime; private Date endDateTime; @@ -944,12 +1190,13 @@ public class Task implements ReadOnlyTask { /** * Constructor with smart defaults */ - public Task(int id, String taskName, InferredDate startDateTime, InferredDate endDateTime, String location, + public Task(int id, int groupId, String taskName, InferredDate startDateTime, InferredDate endDateTime, String location, PriorityLevel priority, RecurrenceType recurringType, Integer numberOfRecurrence, String category, String description, boolean isArchived) { SmartDefaultDates sdd = new SmartDefaultDates(startDateTime, endDateTime); this.id = id; + this.groupId = groupId; this.taskName = taskName; this.startDateTime = sdd.getStartDate(); this.endDateTime = sdd.getEndDate(); @@ -965,7 +1212,7 @@ public class Task implements ReadOnlyTask { this.recurringType = recurringType; } if (numberOfRecurrence == null) { - this.numberOfRecurrence = 0; + this.numberOfRecurrence = 1; } else { this.numberOfRecurrence = numberOfRecurrence.intValue(); } @@ -977,11 +1224,12 @@ public class Task implements ReadOnlyTask { /** * Constructor without smart defaults */ - public Task(int id, String taskName, Date startDateTime, Date endDateTime, String location, + public Task(int id, int groupId, String taskName, Date startDateTime, Date endDateTime, String location, PriorityLevel priority, RecurrenceType recurringType, Integer numberOfRecurrence, String category, String description, boolean isArchived) { this.id = id; + this.groupId = groupId; this.taskName = taskName; this.startDateTime = startDateTime; this.endDateTime = endDateTime; @@ -997,7 +1245,7 @@ public class Task implements ReadOnlyTask { this.recurringType = recurringType; } if (numberOfRecurrence == null) { - this.numberOfRecurrence = 0; + this.numberOfRecurrence = 1; } else { this.numberOfRecurrence = numberOfRecurrence.intValue(); } @@ -1010,15 +1258,13 @@ public class Task implements ReadOnlyTask { this.taskName = taskName; // sets initial default values this.priority = PriorityLevel.Medium; - this.recurringType = RecurrenceType.None; - this.numberOfRecurrence = 0; } /** * Copy constructor. */ public Task(ReadOnlyTask source) { - this(source.getId(), source.getTaskName(), source.getStartDateTime(), + this(source.getId(), source.getGroupId(), source.getTaskName(), source.getStartDateTime(), source.getEndDateTime(), source.getLocation(), source.getPriority(), source.getRecurringType(), source.getNumberOfRecurrence(), source.getCategory(), source.getDescription(), source.isArchived()); @@ -1030,12 +1276,13 @@ public class Task implements ReadOnlyTask { public Task(ReadOnlyTask source, String taskName, InferredDate startDateTime, InferredDate endDateTime, String location, PriorityLevel priority, RecurrenceType recurringType, Integer numberOfRecurrence, String category, String description) { - this(source.getId(), source.getTaskName(), source.getStartDateTime(), + this(source.getId(), source.getGroupId(), source.getTaskName(), source.getStartDateTime(), source.getEndDateTime(), source.getLocation(), source.getPriority(), source.getRecurringType(), source.getNumberOfRecurrence(), source.getCategory(), source.getDescription(), source.isArchived()); //this.id should follow that of the source. + //this.groupId should follow that of the source. //this.isArchived should follow that of the source. this.taskName = taskName == null ? this.taskName : taskName; setStartDate(startDateTime); @@ -1089,6 +1336,11 @@ public class Task implements ReadOnlyTask { return this.id; } + @Override + public int getGroupId() { + return this.groupId; + } + @Override public boolean isMarked() { return isArchived(); // all marked tasks are archived @@ -1148,6 +1400,10 @@ public class Task implements ReadOnlyTask { this.id = id; } + public void setGroupId(int groupId) { + this.groupId = groupId; + } + public void setTaskName(String taskName) { this.taskName = taskName; } @@ -1226,6 +1482,17 @@ public class Task implements ReadOnlyTask { public String toString() { return getAsText(); } + + /** + * Creates a deep copy of this object. + */ + public Task clone() { + Task t = new Task(id, groupId, taskName, + (Date)startDateTime.clone(), (Date)endDateTime.clone(), + location, priority, recurringType, numberOfRecurrence, + category, description, isArchived); + return t; + } } ``` @@ -1285,6 +1552,8 @@ public class TaskList implements Iterable { private final ObservableList internalList = FXCollections.observableArrayList(); private int nextId = 0; private boolean isNextIdInitialized = false; + private int nextGroupId = 0; + private boolean isNextGroupIdInitialized = false; /** * Constructs empty TaskList. @@ -1298,10 +1567,8 @@ public class TaskList implements Iterable { */ public int getNextId() { if (!isNextIdInitialized) { - int nextLowest = -1; // first id to be used is 0. Start finding with -1 - LinkedList usedIds = new LinkedList(); + int nextLowest = 0; // first id to be used is 1. Start finding with 0 for (Task t : internalList) { - usedIds.add(t.getId()); if (t.getId() > nextLowest) { nextLowest = t.getId(); } @@ -1315,6 +1582,29 @@ public class TaskList implements Iterable { nextId++; return nextId; } + + /** + * Gets the next available group id for uniquely identifying a group of recurring tasks in + * Savvy Tasker. + * @return The next available group id; + */ + public int getNextGroupId() { + if (!isNextGroupIdInitialized) { + int nextLowest = 0; // first id to be used is 1. Start finding with 0 + for (Task t : internalList) { + if (t.getId() > nextLowest) { + nextLowest = t.getGroupId(); + } + } + // assumption that the number of tasks < 2^31 + // implementation will be buggy if nextId exceeds 2^31 + nextGroupId = nextLowest; + assert nextGroupId < Integer.MAX_VALUE; + isNextGroupIdInitialized = true; + } + nextGroupId++; + return nextGroupId; + } /** * Returns true if the list contains an equivalent task as the given argument. @@ -1338,10 +1628,11 @@ public class TaskList implements Iterable { /** * Adds a task to the list. - * - * @throws DuplicateTaskException if the person to add is a duplicate of an existing task in the list. + * @throws {@link DuplicateTaskException} if a duplicate is found + * @throws {@link InvalidDateException} if the end date is earlier than the start date + * @return Returns the Task added if the add is successful, an exception is thrown otherwise. */ - public void add(Task toAdd) throws DuplicateTaskException, InvalidDateException { + public Task add(Task toAdd) throws DuplicateTaskException, InvalidDateException { assert toAdd != null; if (contains(toAdd)) { throw new DuplicateTaskException(); @@ -1350,28 +1641,36 @@ public class TaskList implements Iterable { throw new InvalidDateException(); } internalList.add(toAdd); + return toAdd; } /** * Removes the equivalent task from the list. - * - * @throws TaskNotFoundException if no such task could be found in the list. + * @throws {@link TaskNotFoundException} if the task does not exist + * @return Returns a Task if the delete operation is successful, an exception is thrown otherwise. */ - public boolean remove(ReadOnlyTask toRemove) throws TaskNotFoundException { + public Task remove(ReadOnlyTask toRemove) throws TaskNotFoundException { assert toRemove != null; - final boolean taskFoundAndDeleted = internalList.remove(toRemove); - if (!taskFoundAndDeleted) { + int index = internalList.indexOf(toRemove); + if (index >= 0) { + final Task taskToDelete = internalList.get(index); + final boolean taskFoundAndDeleted = internalList.remove(toRemove); + if (!taskFoundAndDeleted) { + throw new TaskNotFoundException(); + } + return taskToDelete; + } else { throw new TaskNotFoundException(); } - return taskFoundAndDeleted; } /** * Replaces the equivalent task from the list. - * - * @throws TaskNotFoundException if no such task could be found in the list. + * @throws {@link TaskNotFoundException} if the task does not exist + * @throws {@link InvalidDateException} if the end date is earlier than the start date + * @return Returns the Task replaced if the replace is successful, an exception is thrown otherwise. */ - public boolean replace(ReadOnlyTask toReplace, Task replacement) throws TaskNotFoundException, InvalidDateException { + public Task replace(ReadOnlyTask toReplace, Task replacement) throws TaskNotFoundException, InvalidDateException { assert toReplace != null; assert replacement != null; if (internalList.contains(toReplace)) { @@ -1379,7 +1678,7 @@ public class TaskList implements Iterable { throw new InvalidDateException(); } internalList.set(internalList.indexOf(toReplace), replacement); - return true; + return replacement; } else { throw new TaskNotFoundException(); @@ -1417,6 +1716,8 @@ public class XmlAdaptedTask { @XmlElement(required = true) private int id; + @XmlElement(required = false) + private int groupId; @XmlElement(required = true) private String taskName; @XmlElement(required = false) @@ -1428,10 +1729,6 @@ public class XmlAdaptedTask { @XmlElement(required = false) private PriorityLevel priority; @XmlElement(required = false) - private RecurrenceType recurringType; - @XmlElement(required = false) - private int numberOfRecurrence; - @XmlElement(required = false) private String category; @XmlElement(required = false) private String description; @@ -1451,13 +1748,12 @@ public class XmlAdaptedTask { */ public XmlAdaptedTask(ReadOnlyTask source) { id = source.getId(); + groupId = source.getGroupId(); taskName = source.getTaskName(); startDateTime = source.getStartDateTime(); endDateTime = source.getEndDateTime(); location = source.getLocation(); priority = source.getPriority(); - recurringType = source.getRecurringType(); - numberOfRecurrence = source.getNumberOfRecurrence(); category = source.getCategory(); description = source.getDescription(); isArchived = source.isArchived(); @@ -1475,13 +1771,11 @@ public class XmlAdaptedTask { final Date endDateTime = this.endDateTime; final String location = this.location; final PriorityLevel priority = this.priority; - final RecurrenceType recurringType = this.recurringType; - final int numberOfRecurrence = this.numberOfRecurrence; final String category = this.category; final String description = this.description; final boolean isArchived = this.isArchived; - return new Task(id, taskName, startDateTime, endDateTime, location, priority, - recurringType, numberOfRecurrence, category, description, isArchived); + return new Task(id, groupId, taskName, startDateTime, endDateTime, location, priority, + null, null, category, description, isArchived); } } ``` diff --git a/collated/main/A0139915Wreused.md b/collated/main/A0139915Wreused.md deleted file mode 100644 index 34a4b0d4c6d1..000000000000 --- a/collated/main/A0139915Wreused.md +++ /dev/null @@ -1,17 +0,0 @@ -# A0139915Wreused -###### \java\seedu\savvytasker\commons\util\StringUtil.java -``` java - // reused original implementation of 'containsIgnoreCase' to find exact matches - public static boolean containsExactIgnoreCase(String source, String query) { - List strings = Arrays.asList(source); - return strings.stream().filter(s -> s.equalsIgnoreCase(query)).count() > 0; - } - - // reused original implementation of 'containsIgnoreCase' to find partial matches - public static boolean containsPartialIgnoreCase(String source, String query) { - if (source == null) return false; - String[] split = source.toLowerCase().split("\\s+"); - List strings = Arrays.asList(split); - return strings.stream().filter(s -> s.contains(query.toLowerCase())).count() > 0; - } -``` diff --git a/collated/test/A0139915W.md b/collated/test/A0139915W.md index aaae7a2d2735..5c272158d7c0 100644 --- a/collated/test/A0139915W.md +++ b/collated/test/A0139915W.md @@ -23,6 +23,16 @@ public class AddCommandTest extends SavvyTaskerGuiTest { //invalid command commandBox.runCommand("adds Bad Command Task"); assertResultMessage(String.format(MESSAGE_UNKNOWN_COMMAND, HelpCommand.MESSAGE_USAGE)); + + //invalid start end date + commandBox.runCommand("add bad start-end pair s/31-12-2015 e/30-12-2015"); + assertResultMessage(String.format(AddCommand.MESSAGE_INVALID_START_END)); + + commandBox.runCommand("clear"); + //add recurring tasks + commandBox.runCommand("add recurring yall s/04-11-2016 e/05-11-2016 l/home r/daily p/high n/5 c/recurs d/AHAHA"); + assertResultMessage("New task added: Id: 0 Task Name: recurring yall Archived: false Start: Fri Nov 04 00:00:00 SGT 2016 End: Sat Nov 05 23:59:59 SGT 2016 Location: home Priority: High Category: recurs Description: AHAHA"); + } private void assertAddSuccess(TestTask taskToAdd, TestTask... currentList) { @@ -32,7 +42,7 @@ public class AddCommandTest extends SavvyTaskerGuiTest { TaskCardHandle addedCard = taskListPanel.navigateToTask(taskToAdd.getTaskName()); assertMatching(taskToAdd, addedCard); - //confirm the list now contains all previous persons plus the new person + //confirm the list now contains all previous tasks plus the new task TestTask[] expectedList = TestUtil.addTasksToList(currentList, taskToAdd); assertTrue(taskListPanel.isListMatching(expectedList)); } @@ -118,6 +128,11 @@ public class FindCommandTest extends SavvyTaskerGuiTest { public void find_nonEmptyList_byExactMatch() { assertFindResult("find t/exact Nearer Due Task", td.nearerDue); // one matching result only } + + @Test + public void find_nonEmptyList_byCategory() { + assertFindResult("find t/category priority", td.highPriority, td.medPriority, td.lowPriority); // matching 3 results + } @Test public void find_emptyList(){ @@ -146,6 +161,7 @@ public class FindCommandTest extends SavvyTaskerGuiTest { */ public class TaskCardHandle extends GuiHandle { private static final String TASKNAME_FIELD_ID = "#taskName"; + private static final String DETAILS_FIELD_ID = "#details"; private Node node; @@ -161,16 +177,21 @@ public class TaskCardHandle extends GuiHandle { public String getTaskName() { return getTextFromLabel(TASKNAME_FIELD_ID); } + + public String getDetails() { + return getTextFromLabel(DETAILS_FIELD_ID); + } public boolean isSameTask(ReadOnlyTask task) { - return getTaskName().equals(task.getTaskName()); + return getTaskName().equals(task.getTaskName()) && getDetails().equals(task.getTextForUi()); } @Override public boolean equals(Object obj) { if(obj instanceof TaskCardHandle) { TaskCardHandle handle = (TaskCardHandle) obj; - return getTaskName().equals(handle.getTaskName()); //TODO: compare the rest + return getTaskName().equals(handle.getTaskName()) && + getDetails().equals(handle.getDetails()); } return super.equals(obj); } @@ -354,6 +375,12 @@ public class ListCommandTest extends SavvyTaskerGuiTest { td.highPriority, td.medPriority, td.lowPriority); } + @Test + public void list_nonEmptyList_byInvalidSwitch() { + commandBox.runCommand("list t/badswitch"); + assertResultMessage("LIST_TYPE: Unknown type \'badswitch\'"); + } + @Test public void list_nonEmptyList_byDueDate() { // covered by list_nonEmptyList() @@ -390,6 +417,367 @@ public class ListCommandTest extends SavvyTaskerGuiTest { } } ``` +###### \java\guitests\ModifyCommandTest.java +``` java +public class ModifyCommandTest extends SavvyTaskerGuiTest { + + @Test + public void add() { + //modify task + TestTask[] currentList = td.getTypicalTasks(); + TestTask taskToModify = currentList[0]; + taskToModify.setStartDateTime(getDate("30/12/2016")); + taskToModify.setEndDateTime(getDate("31/12/2016")); + assertModifySuccess("modify 1 s/30-12-2016 e/31-12-2016", taskToModify, currentList); + currentList = TestUtil.replaceTaskFromList(currentList, taskToModify); + + taskToModify.setStartDateTime(null); + taskToModify.setEndDateTime(null); + assertModifySuccess("modify 1 s/ e/", taskToModify, currentList); + currentList = TestUtil.replaceTaskFromList(currentList, taskToModify); + + //modify invalid index + commandBox.runCommand("modify " + currentList.length + "1" + " s/sat"); + assertResultMessage(String.format(Messages.MESSAGE_INVALID_TASK_DISPLAYED_INDEX)); + + //modify with invalid end date + commandBox.runCommand("modify 1 s/31-12-2016 e/30-12-2016"); + assertResultMessage(String.format(Messages.MESSAGE_INVALID_START_END)); + } + + private void assertModifySuccess(String command, TestTask taskToModify, TestTask... currentList) { + commandBox.runCommand(command); + + //confirm the new card contains the right data + TaskCardHandle modifiedCard = taskListPanel.navigateToTask(taskToModify.getTaskName()); + assertMatching(taskToModify, modifiedCard); + + //confirm the list now contains all previous persons plus the new person + TestTask[] expectedList = TestUtil.replaceTaskFromList(currentList, taskToModify); + assertTrue(taskListPanel.isListMatching(expectedList)); + } + + private SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy"); + private Date getDate(String ddmmyyyy) { + try { + return format.parse(ddmmyyyy); + } catch (Exception e) { + assert false; //should not get an invalid date.... + } + return null; + } + +} +``` +###### \java\seedu\savvytasker\commons\util\SmartDefaultDatesTest.java +``` java +public class SmartDefaultDatesTest { + + @Test + public void smartDefaultDates_parseStart() { + DateParser dateParser = new DateParser(); + InferredDate inferredStart = null; + try { + //use MM-dd-yyyy + inferredStart = dateParser.parseSingle("12/31/2016"); + } catch (ParseException e) { + assert false; //won't get here + } + InferredDate inferredEnd = null; + SmartDefaultDates sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // specifying only start date, assumed to on the given date at 12am + // and to end on the given date at 2359:59 + Date expectedStartTime = getDate("31/12/2016 000000"); + Date expectedEndTime = getDate("31/12/2016 235959"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + Date today = today(0, 0); + try { + //use MM-dd-yyyy + inferredStart = dateParser.parseSingle("3pm"); + } catch (ParseException e) { + assert false; //won't get here + } + sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // specifying only start time, assumed to start today at the given time + // and to end today 2359:59 + expectedStartTime = getDate(sdf.format(today) + " 150000"); + expectedEndTime = getDate(sdf.format(today) + " 235959"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + } + + @Test + public void smartDefaultDates_parseEnd() { + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + Date today = today(0, 0); + DateParser dateParser = new DateParser(); + InferredDate inferredEnd = null; + try { + //use MM-dd-yyyy + inferredEnd = dateParser.parseSingle("12/31/2016"); + } catch (ParseException e) { + assert false; //won't get here + } + InferredDate inferredStart = null; + SmartDefaultDates sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // specified only the end date, assumed to start today at 12am + // and to end on the given date at 2359:59 + Date expectedStartTime = getDate(sdf.format(today) + " 000000"); + Date expectedEndTime = getDate("31/12/2016 235959"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + + try { + //use MM-dd-yyyy + inferredEnd = dateParser.parseSingle("3pm"); + } catch (ParseException e) { + assert false; //won't get here + } + sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // specified only the end time, assumed to start today at 12am + // and to end at the given time today + expectedStartTime = getDate(sdf.format(today) + " 000000"); + expectedEndTime = getDate(sdf.format(today) + " 150000"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + + + try { + //use MM-dd-yyyy + inferredEnd = dateParser.parseSingle("12/31/2000"); + } catch (ParseException e) { + assert false; //won't get here + } + sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // specified only the end date in the past, start date will be null + // and to end on the given date at 2359:59 + expectedStartTime = null; + expectedEndTime = getDate("31/12/2000 235959"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + } + + @Test + public void smartDefaultDates_parseStartEnd() { + // START_TIME + // date not supplied -> today + // time not supplied -> 0000 + // END_TIME + // date not supplied -> today + // time not supplied -> 2359 + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + Date today = today(0, 0); + DateParser dateParser = new DateParser(); + InferredDate inferredStart = null; + InferredDate inferredEnd = null; + try { + //use MM-dd-yyyy + inferredStart = dateParser.parseSingle("12/31/2016"); + inferredEnd = dateParser.parseSingle("12/31/2016"); + } catch (ParseException e) { + assert false; //won't get here + } + SmartDefaultDates sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // no time supplied for start and end + // start defaults to 0000 + // end defaults to 2359:59 + Date expectedStartTime = getDate("31/12/2016 000000"); + Date expectedEndTime = getDate("31/12/2016 235959"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + + inferredStart = null; + inferredEnd = null; + try { + //use MM-dd-yyyy + inferredStart = dateParser.parseSingle("12/31/2016"); + inferredEnd = dateParser.parseSingle("12/30/2016"); + } catch (ParseException e) { + assert false; //won't get here + } + sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // no time supplied for start and end, end date earlier than start + // start defaults to 0000 + // end defaults to 2359:59 + // no restrictions imposed on end time earlier than start time + expectedStartTime = getDate("31/12/2016 000000"); + expectedEndTime = getDate("30/12/2016 235959"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + + inferredStart = null; + inferredEnd = null; + try { + //use MM-dd-yyyy + inferredStart = dateParser.parseSingle("10am"); + inferredEnd = dateParser.parseSingle("10pm"); + } catch (ParseException e) { + assert false; //won't get here + } + sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // no date supplied for start and end + // start and end defaults to the given time today + expectedStartTime = getDate(sdf.format(today) + " 100000"); + expectedEndTime = getDate(sdf.format(today) + " 220000"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + + inferredStart = null; + inferredEnd = null; + try { + //use MM-dd-yyyy + inferredStart = dateParser.parseSingle("10pm"); + inferredEnd = dateParser.parseSingle("10am"); + } catch (ParseException e) { + assert false; //won't get here + } + sdd = new SmartDefaultDates(inferredStart, inferredEnd); + // no date supplied for start and end, end time ends before start time + // start and end defaults to the given time today + // no restrictions imposed on end time being earlier + expectedStartTime = getDate(sdf.format(today) + " 220000"); + expectedEndTime = getDate(sdf.format(today) + " 100000"); + assertEquals(expectedStartTime, sdd.getStartDate()); + assertEquals(expectedEndTime, sdd.getEndDate()); + } + + @Test + public void smartDefaultDates_defaultParse() { + SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); + Date today = today(0, 0); + DateParser dateParser = new DateParser(); + SmartDefaultDates sdd = new SmartDefaultDates(null, null); + Date actualStart = sdd.getStart(dateParser.new InferredDate(new Date(), true, true)); + Date actualEnd = sdd.getEnd(dateParser.new InferredDate(new Date(), true, true)); + Date expectedStart = null; + Date expectedEnd = null; + assertEquals(expectedStart, actualStart); + assertEquals(expectedEnd, actualEnd); + + try { + //use MM-dd-yyyy + actualStart = sdd.getStart(dateParser.parseSingle("10pm")); + actualEnd = sdd.getEnd(dateParser.parseSingle("10am")); + } catch (ParseException e) { + assert false; //won't get here + } + expectedStart = getDate(sdf.format(today) + " 220000"); + expectedEnd = getDate(sdf.format(today) + " 100000"); + assertEquals(expectedStart, actualStart); + assertEquals(expectedEnd, actualEnd); + + try { + //use MM-dd-yyyy + actualStart = sdd.getStart(dateParser.parseSingle("12/31/2016")); + actualEnd = sdd.getEnd(dateParser.parseSingle("12/31/2016")); + } catch (ParseException e) { + assert false; //won't get here + } + expectedStart = getDate("31/12/2016 000000"); + expectedEnd = getDate("31/12/2016 235959"); + assertEquals(expectedStart, actualStart); + assertEquals(expectedEnd, actualEnd); + } + + private SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy HHmmss"); + private Date getDate(String ddmmyyyy) { + try { + return format.parse(ddmmyyyy); + } catch (Exception e) { + assert false; //should not get an invalid date.... + } + return null; + } + + private Date today(int hours_24, int minute_60) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(new Date()); + calendar.set(Calendar.HOUR_OF_DAY, hours_24); + calendar.set(Calendar.MINUTE, minute_60); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } +} +``` +###### \java\seedu\savvytasker\model\task\TaskListTest.java +``` java +public class TaskListTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void taskList_addDuplicate() throws DuplicateTaskException, InvalidDateException { + thrown.expect(DuplicateTaskException.class); + TaskList tasks = new TaskList(); + Task t = new Task("Test Task"); + t.setId(1); + tasks.add(t); // passes + assertEquals(1, tasks.getInternalList().size()); + tasks.add(t); // fails + } + + @Test + public void taskList_addInvalidDate() throws DuplicateTaskException, InvalidDateException { + thrown.expect(InvalidDateException.class); + TaskList tasks = new TaskList(); + Task t = new Task("Test Task"); + t.setId(1); + t.setStartDateTime(getDate("31/12/2016")); + t.setEndDateTime(getDate("31/12/2015")); + tasks.add(t); // fails, end date earlier than start date + } + + @Test + public void taskList_removeNonExistent() throws TaskNotFoundException { + thrown.expect(TaskNotFoundException.class); + TaskList tasks = new TaskList(); + Task t = new Task("Test Task"); + t.setId(1); + assertEquals(0, tasks.getInternalList().size()); + tasks.remove(t); // fails + } + + @Test + public void taskList_replaceNonExistent() throws TaskNotFoundException, InvalidDateException { + thrown.expect(TaskNotFoundException.class); + TaskList tasks = new TaskList(); + Task t = new Task("Test Task"); + t.setId(1); + assertEquals(0, tasks.getInternalList().size()); + tasks.replace(t, t); // fails + } + + @Test + public void taskList_replaceInvalidDate() throws TaskNotFoundException, InvalidDateException, DuplicateTaskException { + thrown.expect(InvalidDateException.class); + TaskList tasks = new TaskList(); + Task t = new Task("Test Task"); + t.setId(1); + t.setStartDateTime(getDate("30/12/2016")); + t.setEndDateTime(getDate("31/12/2016")); + tasks.add(t); + assertEquals(1, tasks.getInternalList().size()); + t.setStartDateTime(getDate("31/12/2016")); + t.setEndDateTime(getDate("31/12/2015")); + tasks.replace(t, t); // fails, end date earlier than start date + } + + private SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy"); + private Date getDate(String ddmmyyyy) { + try { + return format.parse(ddmmyyyy); + } catch (Exception e) { + assert false; //should not get an invalid date.... + } + return null; + } +} +``` ###### \java\seedu\savvytasker\testutil\SavvyTaskerBuilder.java ``` java /** @@ -497,6 +885,7 @@ public class TaskBuilder { public class TestTask implements ReadOnlyTask { private int id; + private int groupId; private String taskName; private Date startDateTime; private Date endDateTime; @@ -515,10 +904,16 @@ public class TestTask implements ReadOnlyTask { this.numberOfRecurrence = 0; } + @Override public int getId() { return id; } + @Override + public int getGroupId() { + return groupId; + } + @Override public String getTaskName() { return taskName; @@ -577,6 +972,10 @@ public class TestTask implements ReadOnlyTask { public void setId(int id) { this.id = id; } + + public void setGroupId(int groupId) { + this.groupId = groupId; + } public void setTaskName(String taskName) { this.taskName = taskName; @@ -624,8 +1023,32 @@ public class TestTask implements ReadOnlyTask { } public String getAddCommand() { + SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy HHmm"); StringBuilder sb = new StringBuilder(); sb.append("add " + this.getTaskName()); + if (startDateTime != null) { + sb.append(" s/ ").append(sdf.format(startDateTime)); + } + if (endDateTime != null) { + sb.append(" e/ ").append(sdf.format(endDateTime)); + } + if (location != null && !location.isEmpty()) { + sb.append(" l/ ").append(location); + } + if (priority != null && priority != PriorityLevel.Medium) { + // p/ defaults to medium, if set to medium, take as non-existent + sb.append(" p/ ").append(priority.toString()); + } + if (recurringType != null && recurringType != RecurrenceType.None) { + // r/ defaults to none, if set to none, take as non-existent + sb.append(" r/ ").append(recurringType.toString()); + } + if (category != null && !category.isEmpty()) { + sb.append(" c/ ").append(category); + } + if (description != null && !description.isEmpty()) { + sb.append(" d/ ").append(description); + } return sb.toString(); } } @@ -674,8 +1097,13 @@ public class TestTask implements ReadOnlyTask { * @param index The index of the task to be replaced. * @return */ - public static TestTask[] replaceTaskFromList(TestTask[] tasks, TestTask task, int index) { - tasks[index] = task; + public static TestTask[] replaceTaskFromList(TestTask[] tasks, TestTask task) { + for (int i = 0; i < tasks.length; ++i) { + if (tasks[i].getId() == task.getId()) { + tasks[i] = task; + break; + } + } return tasks; } @@ -710,26 +1138,26 @@ public class TypicalTestTasks { public TypicalTestTasks() { try { - highPriority = new TaskBuilder().withId(0).withTaskName("High Priority Task") - .withPriority(PriorityLevel.High).build(); - medPriority = new TaskBuilder().withId(1).withTaskName("Medium Priority Task") - .withPriority(PriorityLevel.Medium).build(); - lowPriority = new TaskBuilder().withId(2).withTaskName("Low Priority Task") - .withPriority(PriorityLevel.Low).build(); - furthestDue = new TaskBuilder().withId(3).withTaskName("Furthest Due Task") + highPriority = new TaskBuilder().withId(1).withTaskName("High Priority Task") + .withPriority(PriorityLevel.High).withCategory("priority").build(); + medPriority = new TaskBuilder().withId(2).withTaskName("Medium Priority Task") + .withPriority(PriorityLevel.Medium).withCategory("priority").build(); + lowPriority = new TaskBuilder().withId(3).withTaskName("Low Priority Task") + .withPriority(PriorityLevel.Low).withCategory("priority").build(); + furthestDue = new TaskBuilder().withId(4).withTaskName("Furthest Due Task") .withEndDateTime(getDate("01/12/2016")).build(); - nearerDue = new TaskBuilder().withId(4).withTaskName("Nearer Due Task") + nearerDue = new TaskBuilder().withId(5).withTaskName("Nearer Due Task") .withEndDateTime(getDate("01/11/2016")).build(); - notSoNearerDue = new TaskBuilder().withId(5).withTaskName("Not So Nearer Due Task") + notSoNearerDue = new TaskBuilder().withId(6).withTaskName("Not So Nearer Due Task") .withEndDateTime(getDate("02/11/2016")).build(); - earliestDue = new TaskBuilder().withId(6).withTaskName("Earliest Due Task") + earliestDue = new TaskBuilder().withId(7).withTaskName("Earliest Due Task") .withEndDateTime(getDate("01/10/2016")).build(); - longDue = new TaskBuilder().withId(7).withTaskName("Long Due Task") + longDue = new TaskBuilder().withId(8).withTaskName("Long Due Task") .withEndDateTime(getDate("01/1/2016")).withArchived(true).build(); //Manually added - happy = new TaskBuilder().withId(8).withTaskName("Happy Task").build(); - haloween = new TaskBuilder().withId(9).withTaskName("Haloween Task").build(); + happy = new TaskBuilder().withId(9).withTaskName("Happy Task").build(); + haloween = new TaskBuilder().withId(10).withTaskName("Haloween Task").build(); } catch (IllegalValueException e) { e.printStackTrace(); assert false : "not possible"; @@ -747,8 +1175,6 @@ public class TypicalTestTasks { st.addTask(new Task(td.notSoNearerDue)); st.addTask(new Task(td.earliestDue)); st.addTask(new Task(td.longDue)); - } catch (DuplicateTaskException e) { - assert false : "not possible"; } catch (InvalidDateException e) { assert false : "not possible"; } diff --git a/src/main/java/seedu/savvytasker/commons/util/StringUtil.java b/src/main/java/seedu/savvytasker/commons/util/StringUtil.java index 3c0fc9ea185e..115b96e7d353 100644 --- a/src/main/java/seedu/savvytasker/commons/util/StringUtil.java +++ b/src/main/java/seedu/savvytasker/commons/util/StringUtil.java @@ -9,7 +9,7 @@ * Helper functions for handling strings. */ public class StringUtil { - //@@author A0139915W-reused + //@@author A0139915W // reused original implementation of 'containsIgnoreCase' to find exact matches public static boolean containsExactIgnoreCase(String source, String query) { List strings = Arrays.asList(source); diff --git a/src/test/java/seedu/savvytasker/commons/util/SmartDefaultDatesTest.java b/src/test/java/seedu/savvytasker/commons/util/SmartDefaultDatesTest.java index eea2059370c2..6fe7ca6fc358 100644 --- a/src/test/java/seedu/savvytasker/commons/util/SmartDefaultDatesTest.java +++ b/src/test/java/seedu/savvytasker/commons/util/SmartDefaultDatesTest.java @@ -12,6 +12,7 @@ import java.util.Calendar; import java.util.Date; +//@@author A0139915W public class SmartDefaultDatesTest { @Test @@ -243,3 +244,4 @@ private Date today(int hours_24, int minute_60) { return calendar.getTime(); } } +//@@author diff --git a/src/test/java/seedu/savvytasker/model/task/TaskListTest.java b/src/test/java/seedu/savvytasker/model/task/TaskListTest.java index 9f3a5ab2bafa..eac412d95d72 100644 --- a/src/test/java/seedu/savvytasker/model/task/TaskListTest.java +++ b/src/test/java/seedu/savvytasker/model/task/TaskListTest.java @@ -16,6 +16,7 @@ import org.junit.Rule; +//@@author A0139915W public class TaskListTest { @Rule @@ -88,3 +89,4 @@ private Date getDate(String ddmmyyyy) { return null; } } +//@@author