Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,26 @@ Example: `keyword (optional arguments)`
expected output
```

## Archiving tasks

Move every task out of your active list into a separate archive file,
keeping a record of them without cluttering your day-to-day list.
Archiving is cumulative: doing it again later adds to the same archive
file instead of overwriting it.

Example: `archive`

```
Archived 2 task(s). Your list is now empty.
```

If your list is already empty, Echo tells you there's nothing to
archive instead:

```
There's nothing to archive - your list is already empty.
```

## Feature ABC

// Feature details
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/echo/Echo.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ private void executeCommand(String cmd, String rest) throws EchoException {
ui.showTasksOnDate(date, matches);
break;
}
case "archive": {
List<Task> archived = tasks.clearAll();
ui.showArchived(archived.size());
storage.archive(archived);
storage.save(tasks.getAll());
break;
}
case "find": {
String keyword = Parser.parseFindKeyword(rest);
List<Task> matches = new ArrayList<>();
Expand Down
55 changes: 41 additions & 14 deletions src/main/java/echo/storage/Storage.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -14,22 +15,25 @@
import echo.task.Todo;

/**
* Reads and writes the task list to a save file on disk. The file location
* is fixed at construction time; every other method operates on that one
* path. Load/save never throw: any IO problem is reported with a short
* warning so a save-file issue never prevents the chatbot from starting or
* running.
* Reads and writes the task list to a save file on disk, and appends
* archived tasks to a separate archive file. Both file locations are
* fixed at construction time. Load/save/archive never throw: any IO
* problem is reported with a short warning so a save-file issue never
* prevents the chatbot from starting or running.
*/
public class Storage {
private final Path filePath;
private final Path archiveFilePath;

/**
* Creates a Storage bound to the given save-file path.
* Creates a Storage bound to the given save-file path. Archived tasks
* go to a sibling file named "archive.txt" in the same folder.
*
* @param filePath Relative path to the save file, e.g. "data/echo.txt".
*/
public Storage(String filePath) {
this.filePath = Path.of(filePath);
this.archiveFilePath = this.filePath.resolveSibling("archive.txt");
}

/**
Expand All @@ -40,7 +44,7 @@ public Storage(String filePath) {
public List<Task> load() {
List<Task> tasks = new ArrayList<>();
try {
ensureFileExists();
ensureFileExists(filePath);
for (String line : Files.readAllLines(filePath)) {
if (line.isBlank()) {
continue;
Expand All @@ -62,7 +66,7 @@ public List<Task> load() {
*/
public void save(List<Task> tasks) {
try {
ensureFileExists();
ensureFileExists(filePath);
List<String> lines = new ArrayList<>();
for (Task task : tasks) {
lines.add(task.toFileFormat());
Expand All @@ -74,16 +78,39 @@ public void save(List<Task> tasks) {
}

/**
* Creates the save file's parent folder and the file itself if either
* is missing, so load()/save() never have to handle a missing path.
* Appends the given tasks to the archive file, creating it (and its
* parent folder) if missing. Previously archived tasks are kept, so
* archiving is cumulative across multiple uses rather than
* overwriting what came before.
*
* @param tasks The tasks to archive.
*/
public void archive(List<Task> tasks) {
try {
ensureFileExists(archiveFilePath);
List<String> lines = new ArrayList<>();
for (Task task : tasks) {
lines.add(task.toFileFormat());
}
Files.write(archiveFilePath, lines, StandardOpenOption.APPEND);
} catch (IOException e) {
System.out.println("Warning: could not archive tasks (" + e.getMessage() + ").");
}
}

/**
* Creates the given file's parent folder and the file itself if either
* is missing, so load()/save()/archive() never have to handle a
* missing path. Shared by both the save file and the archive file,
* which otherwise need this exact same setup.
*/
private void ensureFileExists() throws IOException {
Path parent = filePath.getParent();
private void ensureFileExists(Path path) throws IOException {
Path parent = path.getParent();
if (parent != null && Files.notExists(parent)) {
Files.createDirectories(parent);
}
if (Files.notExists(filePath)) {
Files.createFile(filePath);
if (Files.notExists(path)) {
Files.createFile(path);
}
}

Expand Down
12 changes: 12 additions & 0 deletions src/main/java/echo/task/TaskList.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package echo.task;

import java.util.ArrayList;
import java.util.List;

/**
Expand Down Expand Up @@ -50,6 +51,17 @@ public int size() {
return tasks.size();
}

/**
* Removes every task from the list and returns them, in their
* previous order, so a caller (e.g. an archive command) can do
* something with them before the list becomes empty.
*/
public List<Task> clearAll() {
List<Task> removed = new ArrayList<>(tasks);
tasks.clear();
return removed;
}

/**
* Returns the underlying list, for callers (Ui rendering, Storage
* saving) that need to read or persist every task.
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/echo/ui/Ui.java
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,21 @@ public void showTasksOnDate(LocalDate date, List<Task> matches) {
}
}

/**
* Prints confirmation that the given number of tasks were archived
* and the active list is now empty, or a "nothing to archive"
* message if the count is zero.
*
* @param count How many tasks were archived.
*/
public void showArchived(int count) {
if (count == 0) {
System.out.println("There's nothing to archive - your list is already empty.");
return;
}
System.out.println("Archived " + count + " task(s). Your list is now empty.");
}

/**
* Prints every task in the given list as a keyword match, numbered
* from 1, or a "(none)" fallback if the list is empty.
Expand Down
43 changes: 43 additions & 0 deletions src/test/java/echo/storage/StorageTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package echo.storage; // same package as the class being tested

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import echo.task.Task;
import echo.task.Todo;

public class StorageTest {
@TempDir
Path tempDir;

@Test
public void archive_nonEmptyList_appendsToArchiveFile() throws IOException {
Storage storage = new Storage(tempDir.resolve("echo.txt").toString());
List<Task> tasks = List.of(new Todo("read book"), new Todo("write essay"));

storage.archive(tasks);

Path archiveFile = tempDir.resolve("archive.txt");
List<String> lines = Files.readAllLines(archiveFile);
assertEquals(List.of("T | 0 | read book", "T | 0 | write essay"), lines);
}

@Test
public void archive_calledTwice_accumulatesAcrossCalls() throws IOException {
Storage storage = new Storage(tempDir.resolve("echo.txt").toString());

storage.archive(List.of(new Todo("first task")));
storage.archive(List.of(new Todo("second task")));

Path archiveFile = tempDir.resolve("archive.txt");
List<String> lines = Files.readAllLines(archiveFile);
assertEquals(List.of("T | 0 | first task", "T | 0 | second task"), lines);
}
}
36 changes: 36 additions & 0 deletions src/test/java/echo/task/TaskListTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package echo.task; // same package as the class being tested

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.Test;

public class TaskListTest {
@Test
public void clearAll_nonEmptyList_returnsRemovedTasksAndEmptiesList() {
List<Task> initial = new ArrayList<>();
initial.add(new Todo("read book"));
initial.add(new Todo("write essay"));
TaskList taskList = new TaskList(initial);

List<Task> removed = taskList.clearAll();

assertEquals(2, removed.size());
assertEquals("[T][ ] read book", removed.get(0).toString());
assertEquals("[T][ ] write essay", removed.get(1).toString());
assertEquals(0, taskList.size());
}

@Test
public void clearAll_emptyList_returnsEmptyListAndStaysEmpty() {
TaskList taskList = new TaskList(new ArrayList<>());

List<Task> removed = taskList.clearAll();

assertTrue(removed.isEmpty());
assertEquals(0, taskList.size());
}
}
22 changes: 22 additions & 0 deletions tests/test-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Manual test plan

This file did not exist before the `archive` command was added, so it
currently only covers that feature. It is not (yet) a full manual test
plan for the whole app.

## `archive`

Archives every task currently in the list: appends each one to
`data/archive.txt` (relative to wherever Echo is run from) and clears
the active list. Archiving is cumulative — running `archive` again
later appends to the same archive file rather than overwriting it.

| # | Steps | Expected result |
|---|-------|------------------|
| 1 | Add a few tasks (`todo`, `deadline`, `event`), then run `archive`. | Echo replies `Archived <n> task(s). Your list is now empty.` where `<n>` matches the number of tasks added. |
| 2 | Run `list` right after test 1. | Echo replies `Here are the tasks in your list:` with no tasks listed. |
| 3 | Inspect `data/echo.txt`. | The file is empty (or contains no task lines). |
| 4 | Inspect `data/archive.txt`. | Contains one save-file-format line per task archived in test 1, in the order they were added. |
| 5 | With an empty task list, run `archive`. | Echo replies `There's nothing to archive - your list is already empty.` `data/archive.txt` is unchanged. |
| 6 | Add a new task, run `archive`, then inspect `data/archive.txt` again. | The file now contains the lines from test 4 **plus** the new task's line appended after them — nothing from the earlier archive run is lost or overwritten. |
| 7 | Restart Echo (reload from `data/echo.txt`) after archiving. | The reloaded list is empty; archived tasks do not reappear (they only exist in `data/archive.txt`). |