Skip to content

Commit 653fbf0

Browse files
committed
feat: implement remove, update, sortBy, and truncate with tests
- TaskManager.remove(id): removes task by ID, returns true/false - TaskManager.update(id, changes): updates title/description/priority only - TaskManager.sortBy(field): sorts by priority, createdAt, or status without mutating state - string-utils truncate(): truncates at word boundaries, '...' counts toward maxLength - Added comprehensive test suites for all new functionality (33 tests total)
1 parent 18a7ef6 commit 653fbf0

4 files changed

Lines changed: 260 additions & 6 deletions

File tree

src/string-utils.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ export function reverse(str: string): string {
1111
return str.split("").reverse().join("")
1212
}
1313

14-
// TODO: implement truncate function
15-
// Should truncate a string to maxLength and add "..." if truncated
16-
// Should not truncate in the middle of a word
14+
export function truncate(str: string, maxLength: number): string {
15+
if (str.length <= maxLength) return str
16+
const cutoff = maxLength - 3 // reserve 3 chars for "..."
17+
const lastSpace = str.lastIndexOf(" ", cutoff)
18+
const end = lastSpace > 0 ? lastSpace : cutoff
19+
return str.slice(0, end) + "..."
20+
}
1721

1822
export function slugify(str: string): string {
1923
return str

src/task-manager.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,26 @@ export class TaskManager {
5252
return true
5353
}
5454

55-
// TODO: implement remove method
56-
// TODO: implement update method to change title/description/priority
57-
// TODO: implement sortBy method (by priority, createdAt, or status)
55+
remove(id: string): boolean {
56+
return this.tasks.delete(id)
57+
}
58+
59+
update(id: string, changes: Partial<Pick<Task, "title" | "description" | "priority">>): Task | undefined {
60+
const task = this.tasks.get(id)
61+
if (!task) return undefined
62+
if (changes.title !== undefined) task.title = changes.title
63+
if (changes.description !== undefined) task.description = changes.description
64+
if (changes.priority !== undefined) task.priority = changes.priority
65+
return task
66+
}
67+
68+
sortBy(field: "priority" | "createdAt" | "status"): Task[] {
69+
const priorityOrder: Record<Priority, number> = { high: 0, medium: 1, low: 2 }
70+
const statusOrder: Record<Status, number> = { in_progress: 0, pending: 1, completed: 2 }
71+
return Array.from(this.tasks.values()).sort((a, b) => {
72+
if (field === "priority") return priorityOrder[a.priority] - priorityOrder[b.priority]
73+
if (field === "status") return statusOrder[a.status] - statusOrder[b.status]
74+
return a.createdAt.getTime() - b.createdAt.getTime()
75+
})
76+
}
5877
}

test/string-utils.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { describe, test, expect } from "bun:test"
2+
import { truncate } from "../src/string-utils"
3+
4+
describe("truncate", () => {
5+
test("returns string unchanged when shorter than maxLength", () => {
6+
expect(truncate("hello", 10)).toBe("hello")
7+
})
8+
9+
test("returns string unchanged when equal to maxLength", () => {
10+
expect(truncate("hello", 5)).toBe("hello")
11+
})
12+
13+
test("truncates and appends '...' when longer than maxLength", () => {
14+
const result = truncate("hello world", 8)
15+
expect(result).toBe("hello...")
16+
})
17+
18+
test("result length does not exceed maxLength", () => {
19+
const result = truncate("the quick brown fox", 10)
20+
expect(result.length).toBeLessThanOrEqual(10)
21+
})
22+
23+
test("'...' counts toward maxLength", () => {
24+
// maxLength=8 means 5 chars of content + "..."
25+
const result = truncate("abcdefghij", 8)
26+
expect(result.length).toBe(8)
27+
expect(result.endsWith("...")).toBe(true)
28+
})
29+
30+
test("does not cut in the middle of a word — finds last space", () => {
31+
// "the quick" = 9 chars, maxLength=12 → cutoff=9, last space before 9 is at index 3
32+
const result = truncate("the quick brown fox", 12)
33+
expect(result).toBe("the quick...")
34+
})
35+
36+
test("handles single long word with no spaces — cuts at character boundary", () => {
37+
// cutoff = 10 - 3 = 7, no space found, so slice(0, 7) + "..." = 10 chars total
38+
const result = truncate("superlongwordwithoutspaces", 10)
39+
expect(result).toBe("superlo...")
40+
expect(result.length).toBeLessThanOrEqual(10)
41+
})
42+
43+
test("handles single long word — result ends with '...'", () => {
44+
const result = truncate("pneumonoultramicroscopicsilicovolcanoconiosis", 15)
45+
expect(result.endsWith("...")).toBe(true)
46+
expect(result.length).toBeLessThanOrEqual(15)
47+
})
48+
49+
test("truncates sentence at word boundary, not mid-word", () => {
50+
// cutoff = 14 - 3 = 11, lastIndexOf(" ", 11) = 7 ("one two")
51+
// slice(0, 7) + "..." = "one two..." (10 chars, within maxLength 14)
52+
const result = truncate("one two three four five", 14)
53+
expect(result).toBe("one two...")
54+
expect(result.length).toBeLessThanOrEqual(14)
55+
})
56+
57+
test("empty string returns empty string", () => {
58+
expect(truncate("", 5)).toBe("")
59+
})
60+
})

test/task-manager.test.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { describe, test, expect, beforeEach } from "bun:test"
2+
import { TaskManager } from "../src/task-manager"
3+
4+
describe("TaskManager", () => {
5+
let tm: TaskManager
6+
7+
beforeEach(() => {
8+
tm = new TaskManager()
9+
})
10+
11+
describe("remove", () => {
12+
test("returns true and removes an existing task", () => {
13+
const task = tm.add("Task A")
14+
expect(tm.remove(task.id)).toBe(true)
15+
})
16+
17+
test("task is no longer accessible after removal", () => {
18+
const task = tm.add("Task A")
19+
tm.remove(task.id)
20+
expect(tm.get(task.id)).toBeUndefined()
21+
})
22+
23+
test("removed task no longer appears in list()", () => {
24+
const task = tm.add("Task A")
25+
tm.remove(task.id)
26+
expect(tm.list().find((t) => t.id === task.id)).toBeUndefined()
27+
})
28+
29+
test("returns false for a non-existent ID", () => {
30+
expect(tm.remove("999")).toBe(false)
31+
})
32+
33+
test("removing one task does not affect others", () => {
34+
const a = tm.add("Task A")
35+
const b = tm.add("Task B")
36+
tm.remove(a.id)
37+
expect(tm.get(b.id)).toBeDefined()
38+
})
39+
})
40+
41+
describe("update", () => {
42+
test("updates title", () => {
43+
const task = tm.add("Original")
44+
const updated = tm.update(task.id, { title: "Updated" })
45+
expect(updated?.title).toBe("Updated")
46+
})
47+
48+
test("updates description", () => {
49+
const task = tm.add("Task", "medium", "old desc")
50+
const updated = tm.update(task.id, { description: "new desc" })
51+
expect(updated?.description).toBe("new desc")
52+
})
53+
54+
test("updates priority", () => {
55+
const task = tm.add("Task", "low")
56+
const updated = tm.update(task.id, { priority: "high" })
57+
expect(updated?.priority).toBe("high")
58+
})
59+
60+
test("returns undefined for non-existent ID", () => {
61+
expect(tm.update("999", { title: "X" })).toBeUndefined()
62+
})
63+
64+
test("does not change id", () => {
65+
const task = tm.add("Task")
66+
const originalId = task.id
67+
tm.update(task.id, { title: "New" })
68+
expect(tm.get(originalId)?.id).toBe(originalId)
69+
})
70+
71+
test("does not change status", () => {
72+
const task = tm.add("Task")
73+
tm.complete(task.id)
74+
tm.update(task.id, { title: "New" })
75+
expect(tm.get(task.id)?.status).toBe("completed")
76+
})
77+
78+
test("does not change createdAt", () => {
79+
const task = tm.add("Task")
80+
const originalCreatedAt = task.createdAt
81+
tm.update(task.id, { title: "New" })
82+
expect(tm.get(task.id)?.createdAt).toEqual(originalCreatedAt)
83+
})
84+
85+
test("does not change completedAt", () => {
86+
const task = tm.add("Task")
87+
tm.complete(task.id)
88+
const completedAt = tm.get(task.id)!.completedAt
89+
tm.update(task.id, { title: "New" })
90+
expect(tm.get(task.id)?.completedAt).toEqual(completedAt)
91+
})
92+
93+
test("partial update only changes specified fields", () => {
94+
const task = tm.add("Task", "low", "desc")
95+
tm.update(task.id, { title: "New Title" })
96+
const updated = tm.get(task.id)!
97+
expect(updated.title).toBe("New Title")
98+
expect(updated.priority).toBe("low")
99+
expect(updated.description).toBe("desc")
100+
})
101+
102+
test("returns the updated task object", () => {
103+
const task = tm.add("Task")
104+
const result = tm.update(task.id, { title: "Changed" })
105+
expect(result).toBeDefined()
106+
expect(result?.title).toBe("Changed")
107+
})
108+
})
109+
110+
describe("sortBy priority", () => {
111+
test("high tasks come before medium and low", () => {
112+
tm.add("Low task", "low")
113+
tm.add("High task", "high")
114+
tm.add("Medium task", "medium")
115+
const sorted = tm.sortBy("priority")
116+
expect(sorted[0].priority).toBe("high")
117+
expect(sorted[1].priority).toBe("medium")
118+
expect(sorted[2].priority).toBe("low")
119+
})
120+
121+
test("tasks with same priority maintain relative order", () => {
122+
const a = tm.add("High A", "high")
123+
const b = tm.add("High B", "high")
124+
const sorted = tm.sortBy("priority")
125+
const ids = sorted.filter((t) => t.priority === "high").map((t) => t.id)
126+
expect(ids).toEqual([a.id, b.id])
127+
})
128+
})
129+
130+
describe("sortBy status", () => {
131+
test("in_progress comes first, then pending, then completed", () => {
132+
const pending = tm.add("Pending")
133+
const completed = tm.add("Completed")
134+
tm.complete(completed.id)
135+
const inProgress = tm.add("In Progress")
136+
inProgress.status = "in_progress"
137+
138+
const sorted = tm.sortBy("status")
139+
expect(sorted[0].status).toBe("in_progress")
140+
expect(sorted[1].status).toBe("pending")
141+
expect(sorted[2].status).toBe("completed")
142+
})
143+
})
144+
145+
describe("sortBy createdAt", () => {
146+
test("oldest tasks come first", async () => {
147+
const a = tm.add("First")
148+
await new Promise((r) => setTimeout(r, 5))
149+
const b = tm.add("Second")
150+
await new Promise((r) => setTimeout(r, 5))
151+
const c = tm.add("Third")
152+
153+
const sorted = tm.sortBy("createdAt")
154+
expect(sorted.map((t) => t.id)).toEqual([a.id, b.id, c.id])
155+
})
156+
})
157+
158+
describe("sortBy does not mutate internal state", () => {
159+
test("list() returns original insertion order after sortBy", () => {
160+
tm.add("Low", "low")
161+
tm.add("High", "high")
162+
tm.add("Medium", "medium")
163+
164+
const beforeSort = tm.list().map((t) => t.id)
165+
tm.sortBy("priority")
166+
const afterSort = tm.list().map((t) => t.id)
167+
168+
expect(afterSort).toEqual(beforeSort)
169+
})
170+
})
171+
})

0 commit comments

Comments
 (0)