Skip to content

Commit 799c19a

Browse files
author
QuantCode Agent
committed
feat: implement remove, update, sortBy, and truncate
- TaskManager.remove: deletes task by ID, returns true/false - TaskManager.update: partial update of title/description/priority, guards immutable fields - TaskManager.sortBy: sorts by priority (high>medium>low), status (in_progress>pending>completed), or createdAt - truncate: word-aware truncation with ellipsis counted toward maxLength - Add comprehensive tests for all new functionality (34 tests total)
1 parent 18a7ef6 commit 799c19a

4 files changed

Lines changed: 286 additions & 37 deletions

File tree

src/string-utils.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,30 @@
33
*/
44

55
export function capitalize(str: string): string {
6-
if (!str) return str
7-
return str.charAt(0).toUpperCase() + str.slice(1)
6+
if (!str) return str;
7+
return str.charAt(0).toUpperCase() + str.slice(1);
88
}
99

1010
export function reverse(str: string): string {
11-
return str.split("").reverse().join("")
11+
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 limit = maxLength - 3;
17+
const cut = str.lastIndexOf(" ", limit);
18+
return (cut > 0 ? str.slice(0, cut) : str.slice(0, limit)) + "...";
19+
}
1720

1821
export function slugify(str: string): string {
1922
return str
2023
.toLowerCase()
2124
.replace(/[^a-z0-9]+/g, "-")
22-
.replace(/^-|-$/g, "")
25+
.replace(/^-|-$/g, "");
2326
}
2427

2528
// BUG: This doesn't handle multiple consecutive spaces
2629
export function wordCount(str: string): number {
27-
if (!str.trim()) return 0
28-
return str.split(" ").length
30+
if (!str.trim()) return 0;
31+
return str.split(" ").length;
2932
}

src/task-manager.ts

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,86 @@
33
* Manages a list of tasks with priorities and status tracking.
44
*/
55

6-
export type Priority = "low" | "medium" | "high"
7-
export type Status = "pending" | "in_progress" | "completed"
6+
export type Priority = "low" | "medium" | "high";
7+
export type Status = "pending" | "in_progress" | "completed";
88

99
export interface Task {
10-
id: string
11-
title: string
12-
description?: string
13-
priority: Priority
14-
status: Status
15-
createdAt: Date
16-
completedAt?: Date
10+
id: string;
11+
title: string;
12+
description?: string;
13+
priority: Priority;
14+
status: Status;
15+
createdAt: Date;
16+
completedAt?: Date;
1717
}
1818

1919
export class TaskManager {
20-
private tasks: Map<string, Task> = new Map()
21-
private nextId = 1
20+
private tasks: Map<string, Task> = new Map();
21+
private nextId = 1;
2222

23-
add(title: string, priority: Priority = "medium", description?: string): Task {
23+
add(
24+
title: string,
25+
priority: Priority = "medium",
26+
description?: string,
27+
): Task {
2428
const task: Task = {
2529
id: String(this.nextId++),
2630
title,
2731
description,
2832
priority,
2933
status: "pending",
3034
createdAt: new Date(),
31-
}
32-
this.tasks.set(task.id, task)
33-
return task
35+
};
36+
this.tasks.set(task.id, task);
37+
return task;
3438
}
3539

3640
get(id: string): Task | undefined {
37-
return this.tasks.get(id)
41+
return this.tasks.get(id);
3842
}
3943

4044
list(filter?: { status?: Status; priority?: Priority }): Task[] {
41-
let result = Array.from(this.tasks.values())
42-
if (filter?.status) result = result.filter((t) => t.status === filter.status)
43-
if (filter?.priority) result = result.filter((t) => t.priority === filter.priority)
44-
return result
45+
let result = Array.from(this.tasks.values());
46+
if (filter?.status)
47+
result = result.filter((t) => t.status === filter.status);
48+
if (filter?.priority)
49+
result = result.filter((t) => t.priority === filter.priority);
50+
return result;
4551
}
4652

4753
complete(id: string): boolean {
48-
const task = this.tasks.get(id)
49-
if (!task) return false
50-
task.status = "completed"
51-
task.completedAt = new Date()
52-
return true
54+
const task = this.tasks.get(id);
55+
if (!task) return false;
56+
task.status = "completed";
57+
task.completedAt = new Date();
58+
return true;
5359
}
5460

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)
61+
remove(id: string): boolean {
62+
return this.tasks.delete(id);
63+
}
64+
65+
update(
66+
id: string,
67+
patch: Partial<Pick<Task, "title" | "description" | "priority">>,
68+
): Task | undefined {
69+
const task = this.tasks.get(id);
70+
if (!task) return undefined;
71+
if (patch.title !== undefined) task.title = patch.title;
72+
if (patch.description !== undefined) task.description = patch.description;
73+
if (patch.priority !== undefined) task.priority = patch.priority;
74+
return task;
75+
}
76+
77+
sortBy(field: "priority" | "createdAt" | "status"): Task[] {
78+
const order = {
79+
priority: { high: 0, medium: 1, low: 2 },
80+
status: { in_progress: 0, pending: 1, completed: 2 },
81+
};
82+
return Array.from(this.tasks.values()).sort((a, b) => {
83+
if (field === "createdAt")
84+
return a.createdAt.getTime() - b.createdAt.getTime();
85+
return order[field][a[field]] - order[field][b[field]];
86+
});
87+
}
5888
}

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, capitalize, reverse, slugify } from "../src/string-utils";
3+
4+
describe("truncate", () => {
5+
test("returns string unchanged when within maxLength", () => {
6+
expect(truncate("hello", 10)).toBe("hello");
7+
});
8+
9+
test("returns string unchanged when exactly maxLength", () => {
10+
expect(truncate("hello", 5)).toBe("hello");
11+
});
12+
13+
test("truncates long string and adds ellipsis", () => {
14+
const result = truncate("hello world foo bar", 14);
15+
expect(result.length).toBeLessThanOrEqual(14);
16+
expect(result.endsWith("...")).toBe(true);
17+
});
18+
19+
test("does not break in the middle of a word", () => {
20+
const result = truncate("hello world", 9);
21+
expect(result).toBe("hello...");
22+
});
23+
24+
test("ellipsis counts toward maxLength", () => {
25+
const result = truncate("hello world", 8);
26+
expect(result.length).toBeLessThanOrEqual(8);
27+
});
28+
29+
test("handles string with no spaces", () => {
30+
const result = truncate("helloworld", 7);
31+
expect(result).toBe("hell...");
32+
expect(result.length).toBeLessThanOrEqual(7);
33+
});
34+
35+
test("handles empty string", () => {
36+
expect(truncate("", 5)).toBe("");
37+
});
38+
39+
test("breaks at last space before limit", () => {
40+
const result = truncate("one two three four", 12);
41+
expect(result).toBe("one two...");
42+
expect(result.length).toBeLessThanOrEqual(12);
43+
});
44+
});
45+
46+
describe("capitalize (existing)", () => {
47+
test("capitalizes first letter", () => {
48+
expect(capitalize("hello")).toBe("Hello");
49+
});
50+
51+
test("handles empty string", () => {
52+
expect(capitalize("")).toBe("");
53+
});
54+
});
55+
56+
describe("reverse (existing)", () => {
57+
test("reverses a string", () => {
58+
expect(reverse("abc")).toBe("cba");
59+
});
60+
});

test/task-manager.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { describe, test, expect, beforeEach } from "bun:test";
2+
import { TaskManager } from "../src/task-manager";
3+
4+
describe("TaskManager", () => {
5+
let manager: TaskManager;
6+
7+
beforeEach(() => {
8+
manager = new TaskManager();
9+
});
10+
11+
describe("remove", () => {
12+
test("returns true when task exists and removes it", () => {
13+
const task = manager.add("Task A");
14+
expect(manager.remove(task.id)).toBe(true);
15+
expect(manager.get(task.id)).toBeUndefined();
16+
});
17+
18+
test("returns false when task does not exist", () => {
19+
expect(manager.remove("nonexistent")).toBe(false);
20+
});
21+
22+
test("only removes the specified task", () => {
23+
const a = manager.add("Task A");
24+
const b = manager.add("Task B");
25+
manager.remove(a.id);
26+
expect(manager.get(b.id)).toBeDefined();
27+
});
28+
29+
test("task count decreases after removal", () => {
30+
const a = manager.add("Task A");
31+
manager.add("Task B");
32+
manager.remove(a.id);
33+
expect(manager.list()).toHaveLength(1);
34+
});
35+
});
36+
37+
describe("update", () => {
38+
test("returns undefined when task not found", () => {
39+
expect(manager.update("nonexistent", { title: "New" })).toBeUndefined();
40+
});
41+
42+
test("updates title", () => {
43+
const task = manager.add("Old Title");
44+
const updated = manager.update(task.id, { title: "New Title" });
45+
expect(updated?.title).toBe("New Title");
46+
});
47+
48+
test("updates description", () => {
49+
const task = manager.add("Task");
50+
const updated = manager.update(task.id, { description: "Some desc" });
51+
expect(updated?.description).toBe("Some desc");
52+
});
53+
54+
test("updates priority", () => {
55+
const task = manager.add("Task", "low");
56+
const updated = manager.update(task.id, { priority: "high" });
57+
expect(updated?.priority).toBe("high");
58+
});
59+
60+
test("does not change id", () => {
61+
const task = manager.add("Task");
62+
const updated = manager.update(task.id, { title: "New" });
63+
expect(updated?.id).toBe(task.id);
64+
});
65+
66+
test("does not change status", () => {
67+
const task = manager.add("Task");
68+
manager.update(task.id, { title: "New" });
69+
expect(manager.get(task.id)?.status).toBe("pending");
70+
});
71+
72+
test("does not change createdAt", () => {
73+
const task = manager.add("Task");
74+
const before = task.createdAt;
75+
const updated = manager.update(task.id, { title: "New" });
76+
expect(updated?.createdAt).toEqual(before);
77+
});
78+
79+
test("partial update leaves other fields unchanged", () => {
80+
const task = manager.add("Task", "high", "desc");
81+
const updated = manager.update(task.id, { title: "New Title" });
82+
expect(updated?.description).toBe("desc");
83+
expect(updated?.priority).toBe("high");
84+
});
85+
86+
test("update is reflected in subsequent get", () => {
87+
const task = manager.add("Task");
88+
manager.update(task.id, { title: "Updated" });
89+
expect(manager.get(task.id)?.title).toBe("Updated");
90+
});
91+
});
92+
93+
describe("sortBy", () => {
94+
test("sorts by priority: high > medium > low", () => {
95+
manager.add("Low", "low");
96+
manager.add("High", "high");
97+
manager.add("Medium", "medium");
98+
const sorted = manager.sortBy("priority");
99+
expect(sorted.map((t) => t.priority)).toEqual(["high", "medium", "low"]);
100+
});
101+
102+
test("sorts by status: in_progress > pending > completed", () => {
103+
const a = manager.add("A");
104+
const b = manager.add("B");
105+
const c = manager.add("C");
106+
manager.complete(b.id);
107+
// Manually set a to in_progress via update (status can't be set via update, use internal)
108+
// complete c and then check pending b and completed
109+
manager.complete(c.id);
110+
const sorted = manager.sortBy("status");
111+
// a=pending, b=completed, c=completed
112+
expect(sorted[0].status).toBe("pending");
113+
});
114+
115+
test("in_progress sorts before pending", () => {
116+
const a = manager.add("Pending");
117+
const b = manager.add("InProgress");
118+
// Use list to verify we have tasks, then manually force in_progress via the task ref
119+
const taskB = manager.get(b.id)!;
120+
taskB.status = "in_progress";
121+
const sorted = manager.sortBy("status");
122+
expect(sorted[0].id).toBe(b.id);
123+
expect(sorted[1].id).toBe(a.id);
124+
});
125+
126+
test("completed sorts after pending", () => {
127+
const a = manager.add("Pending");
128+
const b = manager.add("Completed");
129+
manager.complete(b.id);
130+
const sorted = manager.sortBy("status");
131+
expect(sorted[0].id).toBe(a.id);
132+
expect(sorted[1].id).toBe(b.id);
133+
});
134+
135+
test("sorts by createdAt ascending", () => {
136+
const a = manager.add("First");
137+
const b = manager.add("Second");
138+
const c = manager.add("Third");
139+
const sorted = manager.sortBy("createdAt");
140+
expect(sorted.map((t) => t.id)).toEqual([a.id, b.id, c.id]);
141+
});
142+
143+
test("returns a new array without mutating internal state", () => {
144+
manager.add("A", "low");
145+
manager.add("B", "high");
146+
const sorted = manager.sortBy("priority");
147+
sorted.reverse();
148+
const sorted2 = manager.sortBy("priority");
149+
expect(sorted2[0].priority).toBe("high");
150+
});
151+
152+
test("returns empty array when no tasks", () => {
153+
expect(manager.sortBy("priority")).toEqual([]);
154+
});
155+
});
156+
});

0 commit comments

Comments
 (0)