makes a Task from the text, pushes it and returns it
done(n)
marks task n done; n counts from 1, as the report does
open()
gives the tasks not done yet
The file imports nothing, not even node:fs: a web page can run it unchanged.
Step 7 of 20 · Exercise
Your turn: add a task
Finish add(text) in lib/tasks.js. The words that start with # are the tags, without their #. The other
words, joined again with spaces, are the title.
The program must print:
Added 3: Water plants
Added 4: Call Bo
1 [ ] Buy milk #home
2 [ ] Write report #work
3 [ ] Water plants #home
4 [ ] Call Bo #work #phone
A tag is stored without its #: the report adds one back. Keep it, and the report shows ##home. Drop it from each tag with slice(1).
text.split(" ") gives the words (lesson 9). filter with startsWith("#") keeps the tags, and map with slice(1) drops each # (lesson 12).
const title = words
.filter((word)=>!word.startsWith("#")).join(" ");
Step 8 of 20 · Concept
lib/store.js
load(path) reads the file and JSON.parses it. That gives plain objects, linked to Object.prototype: no
hasTag on their chain, so t.hasTag("home") stops with TypeError: t.hasTag is not a function.
So load makes each one a new Task(...), linked to Task.prototype (lesson 17). A missing file (ENOENT, lesson
18) gives an empty list.
save(list, path) writes list.tasks as JSON text. JSON.stringify saves only each task's own properties.
Step 9 of 20 · Exercise
Your turn: save the list
Finish save(list, path) in lib/store.js: write list.tasks to path as JSON text, indented by 2 spaces.
main.js saves three tasks, loads them again and checks one. Until save writes, it loads the old file's two.
The program must print:
Loaded 3 tasks
true
writeFile takes text. Give it the list itself and it stops with TypeError [ERR_INVALID_ARG_TYPE], ending Received an instance of TaskList. Make the text first.
JSON.stringify(x, null, 2) makes the text, indented by 2 (lesson 11). writeFile gives a promise: await it (lesson 19).
A bad line prints its message and the next line runs. A bug in the tool still stops it with its own error.
Step 14 of 20 · Exercise
Your turn: run a command
Finish runCommand(list, line) in main.js, one branch per command:
add: add rest to the list, and log Added 3: Water plants.
done: Number(rest) is the task's number. Mark it done and log Done: Buy milk. With no such task, throw
new CommandError(`No task ${rest}`).
list: log report(list.tasks).
The program must print:
Added 3: Water plants
No task 9
Done: Buy milk
1 [x] Buy milk #home
2 [ ] Write report #work
3 [ ] Water plants #home
done 9 makes list.tasks[8]undefined, and done(9) stops with TypeError: Cannot set properties of undefined (setting 'done'). Check that the number is from 1 to list.tasks.length first.
An if (command === "add"), then else if for done and list. The last else keeps the throw that is there now.
if(n >=1&& n <= list.tasks.length){
Step 15 of 20 · Quiz
What does the bad line print?
The tool starts with its two tasks, and gets this input. Type the missing line.
add Read #school
delete 1
list
Output
Added 3: Read
1 [ ] Buy milk #home
2 [ ] Write report #work
3 [ ] Read #school
Two tries
Step 16 of 20 · Demo
See it all run
Every part together on that input. Run it and compare with your answer.
Step 17 of 20 · Check yourself
Check yourself: the tags command
Add the command tags to runCommand. It counts the open tasks of each tag with a Map (lesson 13). Then it logs
one line per tag, in the order the tags first appear.
No comments mark the place this time. The program must print:
Added 3: Water plants
Added 4: Call Bo
Done: Write report
#home 2
#work 1
#phone 1
One more else if, before the last else. A loop over list.open(), a loop over each task's tags inside it, then a loop over the Map.
Step 18 of 20 · Challenge · optional
Challenge: list one tag
Optional.
Run types node main.js home: a word after the file's name. Make list show only the tasks with that tag, each
with its own number. Without the word, list shows them all.
The program must print:
Added 3: Water plants
1 [ ] Buy milk #home
3 [ ] Water plants #home
The words after node main.js are the program's arguments: like a function's, but given on the command line. Node keeps them in the array process.argv, after the paths of node and of the file: "home" is the third item.
process.argv[2] is "home", or undefined with no word. In list, loop over the tasks with their numbers, as report does, and log formatTask(task, i + 1) for the ones that hasTag. Import it too: import { report, formatTask } from "./lib/report.js";.
if(tag ===undefined|| task.hasTag(tag)){
Step 19 of 20 · Concept
Ideas for your tool
You can now write a JavaScript program of several modules. It reads input, keeps its data in classes, arrays and
Maps, saves it to a file, and fails well.
The Playground, linked from the course page, is an editor with the same Node. Ideas for
this tool:
a remove command that takes a task out,
a due date for each task, kept as text,
a report sorted by tag.
The next chapter puts this tasks.js in a web page.
A project splits its work into modules, one job each (lesson 8).
The input's lines are commands; a CommandError reports a bad one (lesson 18).
load and save turn JSON in storage into Task objects and back (lessons 11, 19).
padStart and padEnd line up the report; a Map counts the tags (lessons 9 and 13).
Next: lesson 21 builds a web page; lesson 23 runs this tasks.js in one.
Review
2 questions from this lesson, in a new order each time. Answer from memory: 2 of 2 right on the first try passes. On a day after you finish the lesson, a pass earns 15 XP.
The review needs JavaScript.
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";
class CommandError extends Error {
constructor(message) {
super(message);
this.name = "CommandError";
}
}
function runCommand(list, line) {
const command = line.split(" ")[0];
const rest = line.slice(command.length + 1);
if (command === "add") {
const task = list.add(rest);
console.log(`Added ${list.tasks.length}: ${task.title}`);
} else if (command === "done") {
const n = Number(rest);
if (n >= 1 && n <= list.tasks.length) {
list.done(n);
console.log(`Done: ${list.tasks[n - 1].title}`);
} else {
throw new CommandError(`No task ${rest}`);
}
} else if (command === "list") {
console.log(report(list.tasks));
} else {
throw new CommandError(`Unknown command: ${command}`);
}
}
const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);
const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
try {
runCommand(list, line);
} catch (e) {
if (e instanceof CommandError) {
console.log(e.message);
} else {
throw e;
}
}
}
await save(list, FILE);
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
const words = text.split(" ");
const tags = words
.filter((word) => word.startsWith("#"))
.map((word) => word.slice(1));
const title = words
.filter((word) => !word.startsWith("#"))
.join(" ");
const task = new Task(title, tags);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
import { readFile, writeFile } from "node:fs/promises";
import { Task, TaskList } from "./tasks.js";
export async function load(path) {
try {
const text = await readFile(path, "utf8");
const items = JSON.parse(text);
const tasks = items.map(
(t) => new Task(t.title, t.tags, t.done)
);
return new TaskList(tasks);
} catch (e) {
if (e.code === "ENOENT") {
return new TaskList();
}
throw e;
}
}
export async function save(list, path) {
await writeFile(path, JSON.stringify(list.tasks, null, 2));
}
// " 1 [ ] Buy milk #home"
export function formatTask(task, n) {
const mark = task.done ? "[x]" : "[ ]";
const tags = task.tags.map((tag) => `#${tag}`).join(" ");
const number = String(n).padStart(2);
const title = task.title.padEnd(20);
return `${number} ${mark} ${title} ${tags}`;
}
export function report(tasks) {
const lines = [];
for (let i = 0; i < tasks.length; i++) {
lines.push(formatTask(tasks[i], i + 1));
}
return lines.join("\n");
}
[
{
"title": "Buy milk",
"tags": [
"home"
],
"done": false
},
{
"title": "Write report",
"tags": [
"work"
],
"done": false
}
]
add Water plants #home
add Call Bo #work #phone
done 1
list
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";
class CommandError extends Error {
constructor(message) {
super(message);
this.name = "CommandError";
}
}
function runCommand(list, line) {
const command = line.split(" ")[0];
const rest = line.slice(command.length + 1);
if (command === "add") {
const task = list.add(rest);
console.log(`Added ${list.tasks.length}: ${task.title}`);
} else if (command === "done") {
const n = Number(rest);
if (n >= 1 && n <= list.tasks.length) {
list.done(n);
console.log(`Done: ${list.tasks[n - 1].title}`);
} else {
throw new CommandError(`No task ${rest}`);
}
} else if (command === "list") {
console.log(report(list.tasks));
} else {
throw new CommandError(`Unknown command: ${command}`);
}
}
const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);
const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
try {
runCommand(list, line);
} catch (e) {
if (e instanceof CommandError) {
console.log(e.message);
} else {
throw e;
}
}
}
await save(list, FILE);
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
// words that start with # are tags (no #);
// the other words, joined again, the title
const task = new Task(text, []);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";
class CommandError extends Error {
constructor(message) {
super(message);
this.name = "CommandError";
}
}
function runCommand(list, line) {
const command = line.split(" ")[0];
const rest = line.slice(command.length + 1);
if (command === "add") {
const task = list.add(rest);
console.log(`Added ${list.tasks.length}: ${task.title}`);
} else if (command === "done") {
const n = Number(rest);
if (n >= 1 && n <= list.tasks.length) {
list.done(n);
console.log(`Done: ${list.tasks[n - 1].title}`);
} else {
throw new CommandError(`No task ${rest}`);
}
} else if (command === "list") {
console.log(report(list.tasks));
} else {
throw new CommandError(`Unknown command: ${command}`);
}
}
const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);
const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
try {
runCommand(list, line);
} catch (e) {
if (e instanceof CommandError) {
console.log(e.message);
} else {
throw e;
}
}
}
await save(list, FILE);
import { readFile, writeFile } from "node:fs/promises";
import { Task, TaskList } from "./tasks.js";
export async function load(path) {
try {
const text = await readFile(path, "utf8");
const items = JSON.parse(text);
const tasks = items.map(
(t) => new Task(t.title, t.tags, t.done)
);
return new TaskList(tasks);
} catch (e) {
if (e.code === "ENOENT") {
return new TaskList();
}
throw e;
}
}
export async function save(list, path) {
await writeFile(path, JSON.stringify(list.tasks, null, 2));
}
// " 1 [ ] Buy milk #home"
export function formatTask(task, n) {
const mark = task.done ? "[x]" : "[ ]";
const tags = task.tags.map((tag) => `#${tag}`).join(" ");
const number = String(n).padStart(2);
const title = task.title.padEnd(20);
return `${number} ${mark} ${title} ${tags}`;
}
export function report(tasks) {
const lines = [];
for (let i = 0; i < tasks.length; i++) {
lines.push(formatTask(tasks[i], i + 1));
}
return lines.join("\n");
}
[
{
"title": "Buy milk",
"tags": [
"home"
],
"done": false
},
{
"title": "Write report",
"tags": [
"work"
],
"done": false
}
]
add Water plants #home
add Call Bo #work #phone
list
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
// words that start with # are tags (no #);
// the other words, joined again, the title
const task = new Task(text, []);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
const list = new TaskList();
list.add("Buy milk #home");
list.add("Write report #work");
list.add("Water plants #home");
await save(list, "data/tasks.json");
const again = await load("data/tasks.json");
console.log(`Loaded ${again.tasks.length} tasks`);
console.log(again.tasks[2].hasTag("home"));
import { readFile, writeFile } from "node:fs/promises";
import { Task, TaskList } from "./tasks.js";
export async function load(path) {
try {
const text = await readFile(path, "utf8");
const items = JSON.parse(text);
const tasks = items.map(
(t) => new Task(t.title, t.tags, t.done)
);
return new TaskList(tasks);
} catch (e) {
if (e.code === "ENOENT") {
return new TaskList();
}
throw e;
}
}
export async function save(list, path) {
// write list.tasks to path as JSON text
// (2 spaces: JSON.stringify's 3rd argument)
}
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
const words = text.split(" ");
const tags = words
.filter((word) => word.startsWith("#"))
.map((word) => word.slice(1));
const title = words
.filter((word) => !word.startsWith("#"))
.join(" ");
const task = new Task(title, tags);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
[
{
"title": "Buy milk",
"tags": [
"home"
],
"done": false
},
{
"title": "Write report",
"tags": [
"work"
],
"done": false
}
]
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
const list = new TaskList();
list.add("Buy milk #home");
list.add("Write report #work");
list.add("Water plants #home");
await save(list, "data/tasks.json");
const again = await load("data/tasks.json");
console.log(`Loaded ${again.tasks.length} tasks`);
console.log(again.tasks[2].hasTag("home"));
// " 1 [ ] Buy milk #home"
export function formatTask(task, n) {
// n in 2 places, [x] or [ ], the title in 20,
// then the tags with their #, one space between
return task.title;
}
export function report(tasks) {
const lines = [];
for (let i = 0; i < tasks.length; i++) {
lines.push(formatTask(tasks[i], i + 1));
}
return lines.join("\n");
}
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";
class CommandError extends Error {
constructor(message) {
super(message);
this.name = "CommandError";
}
}
function runCommand(list, line) {
const command = line.split(" ")[0];
const rest = line.slice(command.length + 1);
if (command === "add") {
const task = list.add(rest);
console.log(`Added ${list.tasks.length}: ${task.title}`);
} else if (command === "done") {
const n = Number(rest);
if (n >= 1 && n <= list.tasks.length) {
list.done(n);
console.log(`Done: ${list.tasks[n - 1].title}`);
} else {
throw new CommandError(`No task ${rest}`);
}
} else if (command === "list") {
console.log(report(list.tasks));
} else {
throw new CommandError(`Unknown command: ${command}`);
}
}
const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);
const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
try {
runCommand(list, line);
} catch (e) {
if (e instanceof CommandError) {
console.log(e.message);
} else {
throw e;
}
}
}
await save(list, FILE);
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
const words = text.split(" ");
const tags = words
.filter((word) => word.startsWith("#"))
.map((word) => word.slice(1));
const title = words
.filter((word) => !word.startsWith("#"))
.join(" ");
const task = new Task(title, tags);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
import { readFile, writeFile } from "node:fs/promises";
import { Task, TaskList } from "./tasks.js";
export async function load(path) {
try {
const text = await readFile(path, "utf8");
const items = JSON.parse(text);
const tasks = items.map(
(t) => new Task(t.title, t.tags, t.done)
);
return new TaskList(tasks);
} catch (e) {
if (e.code === "ENOENT") {
return new TaskList();
}
throw e;
}
}
export async function save(list, path) {
await writeFile(path, JSON.stringify(list.tasks, null, 2));
}
[
{
"title": "Buy milk",
"tags": [
"home"
],
"done": false
},
{
"title": "Write report",
"tags": [
"work"
],
"done": false
}
]
done 2
list
// " 1 [ ] Buy milk #home"
export function formatTask(task, n) {
// n in 2 places, [x] or [ ], the title in 20,
// then the tags with their #, one space between
return task.title;
}
export function report(tasks) {
const lines = [];
for (let i = 0; i < tasks.length; i++) {
lines.push(formatTask(tasks[i], i + 1));
}
return lines.join("\n");
}
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";
class CommandError extends Error {
constructor(message) {
super(message);
this.name = "CommandError";
}
}
function runCommand(list, line) {
const command = line.split(" ")[0];
const rest = line.slice(command.length + 1);
// add: add rest, log Added 3: Water plants
// done: mark task n, log Done: Buy milk;
// no task n: throw a CommandError
// list: log the report
throw new CommandError(`Unknown command: ${command}`);
}
const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);
const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
try {
runCommand(list, line);
} catch (e) {
if (e instanceof CommandError) {
console.log(e.message);
} else {
throw e;
}
}
}
await save(list, FILE);
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
const words = text.split(" ");
const tags = words
.filter((word) => word.startsWith("#"))
.map((word) => word.slice(1));
const title = words
.filter((word) => !word.startsWith("#"))
.join(" ");
const task = new Task(title, tags);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
import { readFile, writeFile } from "node:fs/promises";
import { Task, TaskList } from "./tasks.js";
export async function load(path) {
try {
const text = await readFile(path, "utf8");
const items = JSON.parse(text);
const tasks = items.map(
(t) => new Task(t.title, t.tags, t.done)
);
return new TaskList(tasks);
} catch (e) {
if (e.code === "ENOENT") {
return new TaskList();
}
throw e;
}
}
export async function save(list, path) {
await writeFile(path, JSON.stringify(list.tasks, null, 2));
}
// " 1 [ ] Buy milk #home"
export function formatTask(task, n) {
const mark = task.done ? "[x]" : "[ ]";
const tags = task.tags.map((tag) => `#${tag}`).join(" ");
const number = String(n).padStart(2);
const title = task.title.padEnd(20);
return `${number} ${mark} ${title} ${tags}`;
}
export function report(tasks) {
const lines = [];
for (let i = 0; i < tasks.length; i++) {
lines.push(formatTask(tasks[i], i + 1));
}
return lines.join("\n");
}
[
{
"title": "Buy milk",
"tags": [
"home"
],
"done": false
},
{
"title": "Write report",
"tags": [
"work"
],
"done": false
}
]
add Water plants #home
done 9
done 1
list
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";
class CommandError extends Error {
constructor(message) {
super(message);
this.name = "CommandError";
}
}
function runCommand(list, line) {
const command = line.split(" ")[0];
const rest = line.slice(command.length + 1);
// add: add rest, log Added 3: Water plants
// done: mark task n, log Done: Buy milk;
// no task n: throw a CommandError
// list: log the report
throw new CommandError(`Unknown command: ${command}`);
}
const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);
const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
try {
runCommand(list, line);
} catch (e) {
if (e instanceof CommandError) {
console.log(e.message);
} else {
throw e;
}
}
}
await save(list, FILE);
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";
class CommandError extends Error {
constructor(message) {
super(message);
this.name = "CommandError";
}
}
function runCommand(list, line) {
const command = line.split(" ")[0];
const rest = line.slice(command.length + 1);
if (command === "add") {
const task = list.add(rest);
console.log(`Added ${list.tasks.length}: ${task.title}`);
} else if (command === "done") {
const n = Number(rest);
if (n >= 1 && n <= list.tasks.length) {
list.done(n);
console.log(`Done: ${list.tasks[n - 1].title}`);
} else {
throw new CommandError(`No task ${rest}`);
}
} else if (command === "list") {
console.log(report(list.tasks));
} else {
throw new CommandError(`Unknown command: ${command}`);
}
}
const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);
const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
try {
runCommand(list, line);
} catch (e) {
if (e instanceof CommandError) {
console.log(e.message);
} else {
throw e;
}
}
}
await save(list, FILE);
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
const words = text.split(" ");
const tags = words
.filter((word) => word.startsWith("#"))
.map((word) => word.slice(1));
const title = words
.filter((word) => !word.startsWith("#"))
.join(" ");
const task = new Task(title, tags);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
import { readFile, writeFile } from "node:fs/promises";
import { Task, TaskList } from "./tasks.js";
export async function load(path) {
try {
const text = await readFile(path, "utf8");
const items = JSON.parse(text);
const tasks = items.map(
(t) => new Task(t.title, t.tags, t.done)
);
return new TaskList(tasks);
} catch (e) {
if (e.code === "ENOENT") {
return new TaskList();
}
throw e;
}
}
export async function save(list, path) {
await writeFile(path, JSON.stringify(list.tasks, null, 2));
}
// " 1 [ ] Buy milk #home"
export function formatTask(task, n) {
const mark = task.done ? "[x]" : "[ ]";
const tags = task.tags.map((tag) => `#${tag}`).join(" ");
const number = String(n).padStart(2);
const title = task.title.padEnd(20);
return `${number} ${mark} ${title} ${tags}`;
}
export function report(tasks) {
const lines = [];
for (let i = 0; i < tasks.length; i++) {
lines.push(formatTask(tasks[i], i + 1));
}
return lines.join("\n");
}
[
{
"title": "Buy milk",
"tags": [
"home"
],
"done": false
},
{
"title": "Write report",
"tags": [
"work"
],
"done": false
}
]
add Read #school
delete 1
list
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";
class CommandError extends Error {
constructor(message) {
super(message);
this.name = "CommandError";
}
}
function runCommand(list, line) {
const command = line.split(" ")[0];
const rest = line.slice(command.length + 1);
if (command === "add") {
const task = list.add(rest);
console.log(`Added ${list.tasks.length}: ${task.title}`);
} else if (command === "done") {
const n = Number(rest);
if (n >= 1 && n <= list.tasks.length) {
list.done(n);
console.log(`Done: ${list.tasks[n - 1].title}`);
} else {
throw new CommandError(`No task ${rest}`);
}
} else if (command === "list") {
console.log(report(list.tasks));
} else {
throw new CommandError(`Unknown command: ${command}`);
}
}
const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);
const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
try {
runCommand(list, line);
} catch (e) {
if (e instanceof CommandError) {
console.log(e.message);
} else {
throw e;
}
}
}
await save(list, FILE);
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";
class CommandError extends Error {
constructor(message) {
super(message);
this.name = "CommandError";
}
}
function runCommand(list, line) {
const command = line.split(" ")[0];
const rest = line.slice(command.length + 1);
if (command === "add") {
const task = list.add(rest);
console.log(`Added ${list.tasks.length}: ${task.title}`);
} else if (command === "done") {
const n = Number(rest);
if (n >= 1 && n <= list.tasks.length) {
list.done(n);
console.log(`Done: ${list.tasks[n - 1].title}`);
} else {
throw new CommandError(`No task ${rest}`);
}
} else if (command === "list") {
console.log(report(list.tasks));
} else {
throw new CommandError(`Unknown command: ${command}`);
}
}
const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);
const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
try {
runCommand(list, line);
} catch (e) {
if (e instanceof CommandError) {
console.log(e.message);
} else {
throw e;
}
}
}
await save(list, FILE);
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
const words = text.split(" ");
const tags = words
.filter((word) => word.startsWith("#"))
.map((word) => word.slice(1));
const title = words
.filter((word) => !word.startsWith("#"))
.join(" ");
const task = new Task(title, tags);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
import { readFile, writeFile } from "node:fs/promises";
import { Task, TaskList } from "./tasks.js";
export async function load(path) {
try {
const text = await readFile(path, "utf8");
const items = JSON.parse(text);
const tasks = items.map(
(t) => new Task(t.title, t.tags, t.done)
);
return new TaskList(tasks);
} catch (e) {
if (e.code === "ENOENT") {
return new TaskList();
}
throw e;
}
}
export async function save(list, path) {
await writeFile(path, JSON.stringify(list.tasks, null, 2));
}
// " 1 [ ] Buy milk #home"
export function formatTask(task, n) {
const mark = task.done ? "[x]" : "[ ]";
const tags = task.tags.map((tag) => `#${tag}`).join(" ");
const number = String(n).padStart(2);
const title = task.title.padEnd(20);
return `${number} ${mark} ${title} ${tags}`;
}
export function report(tasks) {
const lines = [];
for (let i = 0; i < tasks.length; i++) {
lines.push(formatTask(tasks[i], i + 1));
}
return lines.join("\n");
}
[
{
"title": "Buy milk",
"tags": [
"home"
],
"done": false
},
{
"title": "Write report",
"tags": [
"work"
],
"done": false
}
]
add Water plants #home
add Call Bo #work #phone
done 2
tags
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";
class CommandError extends Error {
constructor(message) {
super(message);
this.name = "CommandError";
}
}
function runCommand(list, line) {
const command = line.split(" ")[0];
const rest = line.slice(command.length + 1);
if (command === "add") {
const task = list.add(rest);
console.log(`Added ${list.tasks.length}: ${task.title}`);
} else if (command === "done") {
const n = Number(rest);
if (n >= 1 && n <= list.tasks.length) {
list.done(n);
console.log(`Done: ${list.tasks[n - 1].title}`);
} else {
throw new CommandError(`No task ${rest}`);
}
} else if (command === "list") {
console.log(report(list.tasks));
} else {
throw new CommandError(`Unknown command: ${command}`);
}
}
const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);
const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
try {
runCommand(list, line);
} catch (e) {
if (e instanceof CommandError) {
console.log(e.message);
} else {
throw e;
}
}
}
await save(list, FILE);
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";
class CommandError extends Error {
constructor(message) {
super(message);
this.name = "CommandError";
}
}
function runCommand(list, line) {
const command = line.split(" ")[0];
const rest = line.slice(command.length + 1);
if (command === "add") {
const task = list.add(rest);
console.log(`Added ${list.tasks.length}: ${task.title}`);
} else if (command === "done") {
const n = Number(rest);
if (n >= 1 && n <= list.tasks.length) {
list.done(n);
console.log(`Done: ${list.tasks[n - 1].title}`);
} else {
throw new CommandError(`No task ${rest}`);
}
} else if (command === "list") {
console.log(report(list.tasks));
} else {
throw new CommandError(`Unknown command: ${command}`);
}
}
const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);
const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
try {
runCommand(list, line);
} catch (e) {
if (e instanceof CommandError) {
console.log(e.message);
} else {
throw e;
}
}
}
await save(list, FILE);
export class Task {
constructor(title, tags, done = false) {
this.title = title;
this.tags = tags;
this.done = done;
}
hasTag(tag) {
return this.tags.includes(tag);
}
}
export class TaskList {
constructor(tasks = []) {
this.tasks = tasks;
}
// "Buy milk #home": "Buy milk", tags ["home"]
add(text) {
const words = text.split(" ");
const tags = words
.filter((word) => word.startsWith("#"))
.map((word) => word.slice(1));
const title = words
.filter((word) => !word.startsWith("#"))
.join(" ");
const task = new Task(title, tags);
this.tasks.push(task);
return task;
}
// n counts from 1, as the report numbers them
done(n) {
this.tasks[n - 1].done = true;
}
open() {
return this.tasks.filter((task) => !task.done);
}
}
import { readFile, writeFile } from "node:fs/promises";
import { Task, TaskList } from "./tasks.js";
export async function load(path) {
try {
const text = await readFile(path, "utf8");
const items = JSON.parse(text);
const tasks = items.map(
(t) => new Task(t.title, t.tags, t.done)
);
return new TaskList(tasks);
} catch (e) {
if (e.code === "ENOENT") {
return new TaskList();
}
throw e;
}
}
export async function save(list, path) {
await writeFile(path, JSON.stringify(list.tasks, null, 2));
}
// " 1 [ ] Buy milk #home"
export function formatTask(task, n) {
const mark = task.done ? "[x]" : "[ ]";
const tags = task.tags.map((tag) => `#${tag}`).join(" ");
const number = String(n).padStart(2);
const title = task.title.padEnd(20);
return `${number} ${mark} ${title} ${tags}`;
}
export function report(tasks) {
const lines = [];
for (let i = 0; i < tasks.length; i++) {
lines.push(formatTask(tasks[i], i + 1));
}
return lines.join("\n");
}
[
{
"title": "Buy milk",
"tags": [
"home"
],
"done": false
},
{
"title": "Write report",
"tags": [
"work"
],
"done": false
}
]
add Water plants #home
list
import { readFileSync } from "node:fs";
import { TaskList } from "./lib/tasks.js";
import { load, save } from "./lib/store.js";
import { report } from "./lib/report.js";
class CommandError extends Error {
constructor(message) {
super(message);
this.name = "CommandError";
}
}
function runCommand(list, line) {
const command = line.split(" ")[0];
const rest = line.slice(command.length + 1);
if (command === "add") {
const task = list.add(rest);
console.log(`Added ${list.tasks.length}: ${task.title}`);
} else if (command === "done") {
const n = Number(rest);
if (n >= 1 && n <= list.tasks.length) {
list.done(n);
console.log(`Done: ${list.tasks[n - 1].title}`);
} else {
throw new CommandError(`No task ${rest}`);
}
} else if (command === "list") {
console.log(report(list.tasks));
} else {
throw new CommandError(`Unknown command: ${command}`);
}
}
const FILE = "data/tasks.json";
// every run starts from the same two tasks
const start = new TaskList();
start.add("Buy milk #home");
start.add("Write report #work");
await save(start, FILE);
const list = await load(FILE);
const lines = readFileSync(0, "utf8").trim().split("\n");
for (const line of lines) {
try {
runCommand(list, line);
} catch (e) {
if (e instanceof CommandError) {
console.log(e.message);
} else {
throw e;
}
}
}
await save(list, FILE);