@task-mcp/shared 1.0.22 → 1.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/algorithms/critical-path.d.ts +47 -0
- package/dist/algorithms/critical-path.d.ts.map +1 -0
- package/dist/algorithms/critical-path.js +340 -0
- package/dist/algorithms/critical-path.js.map +1 -0
- package/dist/algorithms/critical-path.test.d.ts +2 -0
- package/dist/algorithms/critical-path.test.d.ts.map +1 -0
- package/dist/algorithms/critical-path.test.js +184 -0
- package/dist/algorithms/critical-path.test.js.map +1 -0
- package/dist/algorithms/dependency-integrity.d.ts +81 -0
- package/dist/algorithms/dependency-integrity.d.ts.map +1 -0
- package/dist/algorithms/dependency-integrity.js +209 -0
- package/dist/algorithms/dependency-integrity.js.map +1 -0
- package/dist/algorithms/dependency-integrity.test.d.ts +2 -0
- package/dist/algorithms/dependency-integrity.test.d.ts.map +1 -0
- package/dist/algorithms/dependency-integrity.test.js +296 -0
- package/dist/algorithms/dependency-integrity.test.js.map +1 -0
- package/dist/algorithms/index.d.ts +5 -0
- package/dist/algorithms/index.d.ts.map +1 -0
- package/dist/algorithms/index.js +5 -0
- package/dist/algorithms/index.js.map +1 -0
- package/dist/algorithms/tech-analysis.d.ts +106 -0
- package/dist/algorithms/tech-analysis.d.ts.map +1 -0
- package/dist/algorithms/tech-analysis.js +351 -0
- package/dist/algorithms/tech-analysis.js.map +1 -0
- package/dist/algorithms/tech-analysis.test.d.ts +2 -0
- package/dist/algorithms/tech-analysis.test.d.ts.map +1 -0
- package/dist/algorithms/tech-analysis.test.js +330 -0
- package/dist/algorithms/tech-analysis.test.js.map +1 -0
- package/dist/algorithms/topological-sort.d.ts +58 -0
- package/dist/algorithms/topological-sort.d.ts.map +1 -0
- package/dist/algorithms/topological-sort.js +201 -0
- package/dist/algorithms/topological-sort.js.map +1 -0
- package/dist/algorithms/topological-sort.test.d.ts +2 -0
- package/dist/algorithms/topological-sort.test.d.ts.map +1 -0
- package/dist/algorithms/topological-sort.test.js +154 -0
- package/dist/algorithms/topological-sort.test.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/inbox.d.ts +55 -0
- package/dist/schemas/inbox.d.ts.map +1 -0
- package/dist/schemas/inbox.js +25 -0
- package/dist/schemas/inbox.js.map +1 -0
- package/dist/schemas/index.d.ts +7 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +17 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/llm-guide.d.ts +147 -0
- package/dist/schemas/llm-guide.d.ts.map +1 -0
- package/dist/schemas/llm-guide.js +72 -0
- package/dist/schemas/llm-guide.js.map +1 -0
- package/dist/schemas/project.d.ts +177 -0
- package/dist/schemas/project.d.ts.map +1 -0
- package/dist/schemas/project.js +56 -0
- package/dist/schemas/project.js.map +1 -0
- package/dist/schemas/response-format.d.ts +148 -0
- package/dist/schemas/response-format.d.ts.map +1 -0
- package/dist/schemas/response-format.js +18 -0
- package/dist/schemas/response-format.js.map +1 -0
- package/dist/schemas/response-schema.d.ts +307 -0
- package/dist/schemas/response-schema.d.ts.map +1 -0
- package/dist/schemas/response-schema.js +78 -0
- package/dist/schemas/response-schema.js.map +1 -0
- package/dist/schemas/response-schema.test.d.ts +2 -0
- package/dist/schemas/response-schema.test.d.ts.map +1 -0
- package/dist/schemas/response-schema.test.js +256 -0
- package/dist/schemas/response-schema.test.js.map +1 -0
- package/dist/schemas/state.d.ts +17 -0
- package/dist/schemas/state.d.ts.map +1 -0
- package/dist/schemas/state.js +17 -0
- package/dist/schemas/state.js.map +1 -0
- package/dist/schemas/task.d.ts +881 -0
- package/dist/schemas/task.d.ts.map +1 -0
- package/dist/schemas/task.js +177 -0
- package/dist/schemas/task.js.map +1 -0
- package/dist/schemas/view.d.ts +143 -0
- package/dist/schemas/view.d.ts.map +1 -0
- package/dist/schemas/view.js +48 -0
- package/dist/schemas/view.js.map +1 -0
- package/dist/utils/dashboard-renderer.d.ts +93 -0
- package/dist/utils/dashboard-renderer.d.ts.map +1 -0
- package/dist/utils/dashboard-renderer.js +416 -0
- package/dist/utils/dashboard-renderer.js.map +1 -0
- package/dist/utils/dashboard-renderer.test.d.ts +2 -0
- package/dist/utils/dashboard-renderer.test.d.ts.map +1 -0
- package/dist/utils/dashboard-renderer.test.js +772 -0
- package/dist/utils/dashboard-renderer.test.js.map +1 -0
- package/dist/utils/date.d.ts +94 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/date.js +323 -0
- package/dist/utils/date.js.map +1 -0
- package/dist/utils/date.test.d.ts +2 -0
- package/dist/utils/date.test.d.ts.map +1 -0
- package/dist/utils/date.test.js +276 -0
- package/dist/utils/date.test.js.map +1 -0
- package/dist/utils/hierarchy.d.ts +102 -0
- package/dist/utils/hierarchy.d.ts.map +1 -0
- package/dist/utils/hierarchy.js +236 -0
- package/dist/utils/hierarchy.js.map +1 -0
- package/dist/utils/hierarchy.test.d.ts +2 -0
- package/dist/utils/hierarchy.test.d.ts.map +1 -0
- package/dist/utils/hierarchy.test.js +423 -0
- package/dist/utils/hierarchy.test.js.map +1 -0
- package/dist/utils/id.d.ts +60 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +118 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/id.test.d.ts +2 -0
- package/dist/utils/id.test.d.ts.map +1 -0
- package/dist/utils/id.test.js +193 -0
- package/dist/utils/id.test.js.map +1 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +34 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/natural-language.d.ts +111 -0
- package/dist/utils/natural-language.d.ts.map +1 -0
- package/dist/utils/natural-language.js +297 -0
- package/dist/utils/natural-language.js.map +1 -0
- package/dist/utils/natural-language.test.d.ts +2 -0
- package/dist/utils/natural-language.test.d.ts.map +1 -0
- package/dist/utils/natural-language.test.js +197 -0
- package/dist/utils/natural-language.test.js.map +1 -0
- package/dist/utils/priority-queue.d.ts +17 -0
- package/dist/utils/priority-queue.d.ts.map +1 -0
- package/dist/utils/priority-queue.js +62 -0
- package/dist/utils/priority-queue.js.map +1 -0
- package/dist/utils/priority-queue.test.d.ts +2 -0
- package/dist/utils/priority-queue.test.d.ts.map +1 -0
- package/dist/utils/priority-queue.test.js +82 -0
- package/dist/utils/priority-queue.test.js.map +1 -0
- package/dist/utils/projection.d.ts +65 -0
- package/dist/utils/projection.d.ts.map +1 -0
- package/dist/utils/projection.js +180 -0
- package/dist/utils/projection.js.map +1 -0
- package/dist/utils/projection.test.d.ts +2 -0
- package/dist/utils/projection.test.d.ts.map +1 -0
- package/dist/utils/projection.test.js +341 -0
- package/dist/utils/projection.test.js.map +1 -0
- package/dist/utils/terminal-ui.d.ts +208 -0
- package/dist/utils/terminal-ui.d.ts.map +1 -0
- package/dist/utils/terminal-ui.js +614 -0
- package/dist/utils/terminal-ui.js.map +1 -0
- package/dist/utils/terminal-ui.test.d.ts +2 -0
- package/dist/utils/terminal-ui.test.d.ts.map +1 -0
- package/dist/utils/terminal-ui.test.js +683 -0
- package/dist/utils/terminal-ui.test.js.map +1 -0
- package/dist/utils/workspace.d.ts +102 -0
- package/dist/utils/workspace.d.ts.map +1 -0
- package/dist/utils/workspace.js +183 -0
- package/dist/utils/workspace.js.map +1 -0
- package/dist/utils/workspace.test.d.ts +2 -0
- package/dist/utils/workspace.test.d.ts.map +1 -0
- package/dist/utils/workspace.test.js +97 -0
- package/dist/utils/workspace.test.js.map +1 -0
- package/package.json +5 -1
- package/src/algorithms/critical-path.test.ts +227 -0
- package/src/algorithms/critical-path.ts +14 -34
- package/src/algorithms/dependency-integrity.test.ts +335 -0
- package/src/algorithms/dependency-integrity.ts +4 -13
- package/src/algorithms/tech-analysis.test.ts +405 -0
- package/src/algorithms/tech-analysis.ts +27 -27
- package/src/algorithms/topological-sort.test.ts +182 -0
- package/src/algorithms/topological-sort.ts +6 -10
- package/src/schemas/index.ts +2 -13
- package/src/schemas/response-format.ts +6 -6
- package/src/schemas/response-schema.test.ts +314 -0
- package/src/schemas/response-schema.ts +25 -20
- package/src/schemas/task.ts +4 -22
- package/src/utils/dashboard-renderer.test.ts +976 -0
- package/src/utils/dashboard-renderer.ts +27 -59
- package/src/utils/date.test.ts +329 -0
- package/src/utils/date.ts +2 -10
- package/src/utils/hierarchy.test.ts +488 -0
- package/src/utils/hierarchy.ts +4 -5
- package/src/utils/id.test.ts +235 -0
- package/src/utils/index.ts +7 -1
- package/src/utils/natural-language.test.ts +234 -0
- package/src/utils/priority-queue.test.ts +103 -0
- package/src/utils/projection.test.ts +430 -0
- package/src/utils/terminal-ui.test.ts +831 -0
- package/src/utils/terminal-ui.ts +53 -54
- package/src/utils/workspace.test.ts +125 -0
|
@@ -41,11 +41,13 @@ export interface DashboardStats {
|
|
|
41
41
|
export interface DependencyMetrics {
|
|
42
42
|
readyToWork: number;
|
|
43
43
|
blockedByDependencies: number;
|
|
44
|
-
mostDependedOn?:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
mostDependedOn?:
|
|
45
|
+
| {
|
|
46
|
+
id: string;
|
|
47
|
+
title: string;
|
|
48
|
+
dependentCount: number;
|
|
49
|
+
}
|
|
50
|
+
| undefined;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export interface WorkspaceInfo {
|
|
@@ -107,9 +109,7 @@ export function calculateStats(tasks: Task[]): DashboardStats {
|
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
export function calculateDependencyMetrics(tasks: Task[]): DependencyMetrics {
|
|
110
|
-
const completedIds = new Set(
|
|
111
|
-
tasks.filter((t) => t.status === "completed").map((t) => t.id)
|
|
112
|
-
);
|
|
112
|
+
const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
|
|
113
113
|
|
|
114
114
|
let readyToWork = 0;
|
|
115
115
|
let blockedByDependencies = 0;
|
|
@@ -134,10 +134,7 @@ export function calculateDependencyMetrics(tasks: Task[]): DependencyMetrics {
|
|
|
134
134
|
|
|
135
135
|
// Track dependent counts
|
|
136
136
|
for (const dep of deps) {
|
|
137
|
-
dependentCounts.set(
|
|
138
|
-
dep.taskId,
|
|
139
|
-
(dependentCounts.get(dep.taskId) ?? 0) + 1
|
|
140
|
-
);
|
|
137
|
+
dependentCounts.set(dep.taskId, (dependentCounts.get(dep.taskId) ?? 0) + 1);
|
|
141
138
|
}
|
|
142
139
|
}
|
|
143
140
|
|
|
@@ -192,8 +189,7 @@ export function renderStatusWidget(tasks: Task[]): string {
|
|
|
192
189
|
const today = getTodayTasks(tasks);
|
|
193
190
|
const overdue = getOverdueTasks(tasks);
|
|
194
191
|
const activeTasks = stats.total - stats.cancelled;
|
|
195
|
-
const percent =
|
|
196
|
-
activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
|
|
192
|
+
const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
|
|
197
193
|
|
|
198
194
|
const lines: string[] = [];
|
|
199
195
|
|
|
@@ -252,16 +248,10 @@ export function renderActionsWidget(tasks: Task[]): string {
|
|
|
252
248
|
|
|
253
249
|
// Get top 4 ready tasks sorted by priority
|
|
254
250
|
const readyTasks = tasks
|
|
255
|
-
.filter(
|
|
256
|
-
(t) =>
|
|
257
|
-
t.status === "pending" &&
|
|
258
|
-
(!t.dependencies || t.dependencies.length === 0)
|
|
259
|
-
)
|
|
251
|
+
.filter((t) => t.status === "pending" && (!t.dependencies || t.dependencies.length === 0))
|
|
260
252
|
.sort((a, b) => {
|
|
261
253
|
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
262
|
-
return (
|
|
263
|
-
(priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2)
|
|
264
|
-
);
|
|
254
|
+
return (priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2);
|
|
265
255
|
})
|
|
266
256
|
.slice(0, 4);
|
|
267
257
|
|
|
@@ -341,17 +331,13 @@ export function renderWorkspacesTable(
|
|
|
341
331
|
const stats = calculateStats(tasks);
|
|
342
332
|
const depMetrics = calculateDependencyMetrics(tasks);
|
|
343
333
|
const activeTasks = stats.total - stats.cancelled;
|
|
344
|
-
const percent =
|
|
345
|
-
activeTasks > 0
|
|
346
|
-
? Math.round((stats.completed / activeTasks) * 100)
|
|
347
|
-
: 0;
|
|
334
|
+
const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
|
|
348
335
|
|
|
349
336
|
// Create mini progress bar
|
|
350
337
|
const barWidth = 8;
|
|
351
338
|
const filled = Math.round((percent / 100) * barWidth);
|
|
352
339
|
const empty = barWidth - filled;
|
|
353
|
-
const miniBar =
|
|
354
|
-
c.green("█".repeat(filled)) + c.gray("░".repeat(empty));
|
|
340
|
+
const miniBar = c.green("█".repeat(filled)) + c.gray("░".repeat(empty));
|
|
355
341
|
|
|
356
342
|
rows.push({
|
|
357
343
|
name: truncateStr(ws.name, 20),
|
|
@@ -392,9 +378,7 @@ export const renderProjectsTable = renderWorkspacesTable;
|
|
|
392
378
|
* Render Tasks table for single workspace view
|
|
393
379
|
*/
|
|
394
380
|
export function renderTasksTable(tasks: Task[], limit: number = 10): string {
|
|
395
|
-
const activeTasks = tasks.filter(
|
|
396
|
-
(t) => t.status !== "completed" && t.status !== "cancelled"
|
|
397
|
-
);
|
|
381
|
+
const activeTasks = tasks.filter((t) => t.status !== "completed" && t.status !== "cancelled");
|
|
398
382
|
|
|
399
383
|
if (activeTasks.length === 0) {
|
|
400
384
|
return c.gray("No active tasks.");
|
|
@@ -421,10 +405,7 @@ export function renderTasksTable(tasks: Task[], limit: number = 10): string {
|
|
|
421
405
|
},
|
|
422
406
|
];
|
|
423
407
|
|
|
424
|
-
let result = table(
|
|
425
|
-
displayTasks as unknown as Record<string, unknown>[],
|
|
426
|
-
columns
|
|
427
|
-
);
|
|
408
|
+
let result = table(displayTasks as unknown as Record<string, unknown>[], columns);
|
|
428
409
|
|
|
429
410
|
if (activeTasks.length > limit) {
|
|
430
411
|
result += `\n${c.gray(`(+${activeTasks.length - limit} more tasks)`)}`;
|
|
@@ -440,21 +421,14 @@ export function renderTasksTable(tasks: Task[], limit: number = 10): string {
|
|
|
440
421
|
function getTodayTasks(tasks: Task[]): Task[] {
|
|
441
422
|
const today = new Date().toISOString().split("T")[0];
|
|
442
423
|
return tasks.filter(
|
|
443
|
-
(t) =>
|
|
444
|
-
t.dueDate === today &&
|
|
445
|
-
t.status !== "completed" &&
|
|
446
|
-
t.status !== "cancelled"
|
|
424
|
+
(t) => t.dueDate === today && t.status !== "completed" && t.status !== "cancelled"
|
|
447
425
|
);
|
|
448
426
|
}
|
|
449
427
|
|
|
450
428
|
function getOverdueTasks(tasks: Task[]): Task[] {
|
|
451
429
|
const today = new Date().toISOString().split("T")[0] ?? "";
|
|
452
430
|
return tasks.filter(
|
|
453
|
-
(t) =>
|
|
454
|
-
t.dueDate &&
|
|
455
|
-
t.dueDate < today &&
|
|
456
|
-
t.status !== "completed" &&
|
|
457
|
-
t.status !== "cancelled"
|
|
431
|
+
(t) => t.dueDate && t.dueDate < today && t.status !== "completed" && t.status !== "cancelled"
|
|
458
432
|
);
|
|
459
433
|
}
|
|
460
434
|
|
|
@@ -538,9 +512,7 @@ export function renderDashboard(
|
|
|
538
512
|
|
|
539
513
|
// Tasks table (for single workspace view)
|
|
540
514
|
if (showTasks && (currentWorkspace || workspaces.length === 1)) {
|
|
541
|
-
const activeTasks = tasks.filter(
|
|
542
|
-
(t) => t.status !== "completed" && t.status !== "cancelled"
|
|
543
|
-
);
|
|
515
|
+
const activeTasks = tasks.filter((t) => t.status !== "completed" && t.status !== "cancelled");
|
|
544
516
|
if (activeTasks.length > 0) {
|
|
545
517
|
lines.push(c.bold(`Tasks (${activeTasks.length})`));
|
|
546
518
|
lines.push("");
|
|
@@ -565,7 +537,7 @@ export function renderWorkspaceDashboard(
|
|
|
565
537
|
const wsInfo: WorkspaceInfo = {
|
|
566
538
|
name: workspace,
|
|
567
539
|
taskCount: tasks.length,
|
|
568
|
-
completedCount: tasks.filter(t => t.status === "completed").length,
|
|
540
|
+
completedCount: tasks.filter((t) => t.status === "completed").length,
|
|
569
541
|
};
|
|
570
542
|
|
|
571
543
|
const data: DashboardData = {
|
|
@@ -576,17 +548,13 @@ export function renderWorkspaceDashboard(
|
|
|
576
548
|
activeTag: options.activeTag,
|
|
577
549
|
};
|
|
578
550
|
|
|
579
|
-
return renderDashboard(
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
showTasks: true,
|
|
587
|
-
stripAnsiCodes: options.stripAnsiCodes,
|
|
588
|
-
}
|
|
589
|
-
);
|
|
551
|
+
return renderDashboard(data, () => tasks, {
|
|
552
|
+
showBanner: true,
|
|
553
|
+
showInbox: false,
|
|
554
|
+
showWorkspaces: false,
|
|
555
|
+
showTasks: true,
|
|
556
|
+
stripAnsiCodes: options.stripAnsiCodes,
|
|
557
|
+
});
|
|
590
558
|
}
|
|
591
559
|
|
|
592
560
|
// Legacy export for backwards compatibility
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
now,
|
|
4
|
+
parseRelativeDate,
|
|
5
|
+
parseRelativeDateSafe,
|
|
6
|
+
parseDateString,
|
|
7
|
+
formatDate,
|
|
8
|
+
formatDisplayDate,
|
|
9
|
+
isToday,
|
|
10
|
+
isPastDue,
|
|
11
|
+
isWithinDays,
|
|
12
|
+
isValidDate,
|
|
13
|
+
DateParseError,
|
|
14
|
+
} from "./date.js";
|
|
15
|
+
|
|
16
|
+
describe("now", () => {
|
|
17
|
+
test("returns ISO timestamp string", () => {
|
|
18
|
+
const result = now();
|
|
19
|
+
expect(typeof result).toBe("string");
|
|
20
|
+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("returns current time within tolerance", () => {
|
|
24
|
+
const before = Date.now();
|
|
25
|
+
const result = now();
|
|
26
|
+
const after = Date.now();
|
|
27
|
+
|
|
28
|
+
const resultTime = new Date(result).getTime();
|
|
29
|
+
expect(resultTime).toBeGreaterThanOrEqual(before);
|
|
30
|
+
expect(resultTime).toBeLessThanOrEqual(after);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("parseRelativeDate", () => {
|
|
35
|
+
test("parses 'today'", () => {
|
|
36
|
+
const result = parseRelativeDate("today");
|
|
37
|
+
expect(result).not.toBeNull();
|
|
38
|
+
const today = new Date();
|
|
39
|
+
expect(result!.getDate()).toBe(today.getDate());
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("parses '오늘' (Korean today)", () => {
|
|
43
|
+
const result = parseRelativeDate("오늘");
|
|
44
|
+
expect(result).not.toBeNull();
|
|
45
|
+
const today = new Date();
|
|
46
|
+
expect(result!.getDate()).toBe(today.getDate());
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("parses 'tomorrow'", () => {
|
|
50
|
+
const result = parseRelativeDate("tomorrow");
|
|
51
|
+
expect(result).not.toBeNull();
|
|
52
|
+
const tomorrow = new Date();
|
|
53
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
54
|
+
expect(result!.getDate()).toBe(tomorrow.getDate());
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("parses '내일' (Korean tomorrow)", () => {
|
|
58
|
+
const result = parseRelativeDate("내일");
|
|
59
|
+
expect(result).not.toBeNull();
|
|
60
|
+
const tomorrow = new Date();
|
|
61
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
62
|
+
expect(result!.getDate()).toBe(tomorrow.getDate());
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("parses 'yesterday'", () => {
|
|
66
|
+
const result = parseRelativeDate("yesterday");
|
|
67
|
+
expect(result).not.toBeNull();
|
|
68
|
+
const yesterday = new Date();
|
|
69
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
70
|
+
expect(result!.getDate()).toBe(yesterday.getDate());
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("parses 'next week'", () => {
|
|
74
|
+
const result = parseRelativeDate("next week");
|
|
75
|
+
expect(result).not.toBeNull();
|
|
76
|
+
const nextWeek = new Date();
|
|
77
|
+
nextWeek.setDate(nextWeek.getDate() + 7);
|
|
78
|
+
expect(result!.getDate()).toBe(nextWeek.getDate());
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("parses 'in 3 days'", () => {
|
|
82
|
+
const result = parseRelativeDate("in 3 days");
|
|
83
|
+
expect(result).not.toBeNull();
|
|
84
|
+
const future = new Date();
|
|
85
|
+
future.setDate(future.getDate() + 3);
|
|
86
|
+
expect(result!.getDate()).toBe(future.getDate());
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("parses '5일 후' (Korean days later)", () => {
|
|
90
|
+
const result = parseRelativeDate("5일 후");
|
|
91
|
+
expect(result).not.toBeNull();
|
|
92
|
+
const future = new Date();
|
|
93
|
+
future.setDate(future.getDate() + 5);
|
|
94
|
+
expect(result!.getDate()).toBe(future.getDate());
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("parses weekday names", () => {
|
|
98
|
+
const result = parseRelativeDate("monday");
|
|
99
|
+
expect(result).not.toBeNull();
|
|
100
|
+
expect(result!.getDay()).toBe(1); // Monday = 1
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("parses ISO date string", () => {
|
|
104
|
+
const result = parseRelativeDate("2025-12-31");
|
|
105
|
+
expect(result).not.toBeNull();
|
|
106
|
+
expect(result!.getFullYear()).toBe(2025);
|
|
107
|
+
expect(result!.getMonth()).toBe(11); // December = 11
|
|
108
|
+
expect(result!.getDate()).toBe(31);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("returns null for invalid input", () => {
|
|
112
|
+
const result = parseRelativeDate("invalid date string");
|
|
113
|
+
expect(result).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("parseRelativeDateSafe", () => {
|
|
118
|
+
test("returns success result for valid date", () => {
|
|
119
|
+
const result = parseRelativeDateSafe("tomorrow");
|
|
120
|
+
expect(result.success).toBe(true);
|
|
121
|
+
if (result.success === true) {
|
|
122
|
+
const tomorrow = new Date();
|
|
123
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
124
|
+
expect(result.date.getDate()).toBe(tomorrow.getDate());
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("returns error for empty input", () => {
|
|
129
|
+
const result = parseRelativeDateSafe("");
|
|
130
|
+
expect(result.success).toBe(false);
|
|
131
|
+
if (result.success === false) {
|
|
132
|
+
expect(result.reason).toBe("empty_input");
|
|
133
|
+
expect(result.error).toContain("empty");
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("returns error for whitespace-only input", () => {
|
|
138
|
+
const result = parseRelativeDateSafe(" ");
|
|
139
|
+
expect(result.success).toBe(false);
|
|
140
|
+
if (result.success === false) {
|
|
141
|
+
expect(result.reason).toBe("empty_input");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("returns error for invalid date format", () => {
|
|
146
|
+
const result = parseRelativeDateSafe("not a date");
|
|
147
|
+
expect(result.success).toBe(false);
|
|
148
|
+
if (result.success === false) {
|
|
149
|
+
expect(result.reason).toBe("invalid_format");
|
|
150
|
+
expect(result.error).toContain("Supported formats");
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("returns error for malformed ISO date", () => {
|
|
155
|
+
const result = parseRelativeDateSafe("2025-1-5");
|
|
156
|
+
expect(result.success).toBe(false);
|
|
157
|
+
if (result.success === false) {
|
|
158
|
+
expect(result.reason).toBe("invalid_format");
|
|
159
|
+
expect(result.error).toContain("zero-padded");
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("returns error for invalid date like Feb 30", () => {
|
|
164
|
+
const result = parseRelativeDateSafe("2025-02-30");
|
|
165
|
+
expect(result.success).toBe(false);
|
|
166
|
+
if (result.success === false) {
|
|
167
|
+
expect(result.reason).toBe("invalid_date");
|
|
168
|
+
expect(result.error).toContain("February 30");
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("returns error for year out of range", () => {
|
|
173
|
+
const result = parseRelativeDateSafe("1800-01-01");
|
|
174
|
+
expect(result.success).toBe(false);
|
|
175
|
+
if (result.success === false) {
|
|
176
|
+
expect(result.reason).toBe("out_of_range");
|
|
177
|
+
expect(result.error).toContain("1900-2100");
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("returns error for excessive day offset", () => {
|
|
182
|
+
const result = parseRelativeDateSafe("in 5000 days");
|
|
183
|
+
expect(result.success).toBe(false);
|
|
184
|
+
if (result.success === false) {
|
|
185
|
+
expect(result.reason).toBe("out_of_range");
|
|
186
|
+
expect(result.error).toContain("3650");
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("isValidDate", () => {
|
|
192
|
+
test("returns true for valid Date", () => {
|
|
193
|
+
expect(isValidDate(new Date())).toBe(true);
|
|
194
|
+
expect(isValidDate(new Date("2025-01-01"))).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("returns false for Invalid Date", () => {
|
|
198
|
+
expect(isValidDate(new Date("invalid"))).toBe(false);
|
|
199
|
+
expect(isValidDate(new Date(NaN))).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("parseDateString", () => {
|
|
204
|
+
test("parses valid date string", () => {
|
|
205
|
+
const result = parseDateString("2025-01-15");
|
|
206
|
+
expect(result).toBeInstanceOf(Date);
|
|
207
|
+
expect(result.getFullYear()).toBe(2025);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("throws DateParseError for empty string", () => {
|
|
211
|
+
expect(() => parseDateString("")).toThrow(DateParseError);
|
|
212
|
+
try {
|
|
213
|
+
parseDateString("");
|
|
214
|
+
} catch (e) {
|
|
215
|
+
expect(e).toBeInstanceOf(DateParseError);
|
|
216
|
+
expect((e as DateParseError).reason).toBe("empty_input");
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("throws DateParseError for invalid string", () => {
|
|
221
|
+
expect(() => parseDateString("not a date")).toThrow(DateParseError);
|
|
222
|
+
try {
|
|
223
|
+
parseDateString("not a date");
|
|
224
|
+
} catch (e) {
|
|
225
|
+
expect(e).toBeInstanceOf(DateParseError);
|
|
226
|
+
expect((e as DateParseError).reason).toBe("invalid_format");
|
|
227
|
+
expect((e as DateParseError).input).toBe("not a date");
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("formatDisplayDate error handling", () => {
|
|
233
|
+
test("throws DateParseError for invalid string", () => {
|
|
234
|
+
expect(() => formatDisplayDate("invalid")).toThrow(DateParseError);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("throws DateParseError for Invalid Date object", () => {
|
|
238
|
+
expect(() => formatDisplayDate(new Date("invalid"))).toThrow(DateParseError);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("formats valid string date", () => {
|
|
242
|
+
const result = formatDisplayDate("2025-06-15");
|
|
243
|
+
expect(typeof result).toBe("string");
|
|
244
|
+
expect(result).toMatch(/\d+\/\d+\/\d+/);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("formatDate", () => {
|
|
249
|
+
test("formats date as YYYY-MM-DD", () => {
|
|
250
|
+
const date = new Date("2025-06-15T10:30:00");
|
|
251
|
+
expect(formatDate(date)).toBe("2025-06-15");
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("isToday", () => {
|
|
256
|
+
test("returns true for today", () => {
|
|
257
|
+
expect(isToday(new Date())).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("returns false for tomorrow", () => {
|
|
261
|
+
const tomorrow = new Date();
|
|
262
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
263
|
+
expect(isToday(tomorrow)).toBe(false);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("accepts string input", () => {
|
|
267
|
+
expect(isToday(new Date().toISOString())).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("returns false for invalid date string", () => {
|
|
271
|
+
expect(isToday("invalid")).toBe(false);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("returns false for Invalid Date object", () => {
|
|
275
|
+
expect(isToday(new Date("invalid"))).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("isPastDue", () => {
|
|
280
|
+
test("returns true for yesterday", () => {
|
|
281
|
+
const yesterday = new Date();
|
|
282
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
283
|
+
expect(isPastDue(yesterday)).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("returns false for tomorrow", () => {
|
|
287
|
+
const tomorrow = new Date();
|
|
288
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
289
|
+
expect(isPastDue(tomorrow)).toBe(false);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("returns false for invalid date", () => {
|
|
293
|
+
expect(isPastDue("invalid")).toBe(false);
|
|
294
|
+
expect(isPastDue(new Date("invalid"))).toBe(false);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("isWithinDays", () => {
|
|
299
|
+
test("returns true for date within range", () => {
|
|
300
|
+
const inThreeDays = new Date();
|
|
301
|
+
inThreeDays.setDate(inThreeDays.getDate() + 3);
|
|
302
|
+
expect(isWithinDays(inThreeDays, 7)).toBe(true);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("returns false for date outside range", () => {
|
|
306
|
+
const inTenDays = new Date();
|
|
307
|
+
inTenDays.setDate(inTenDays.getDate() + 10);
|
|
308
|
+
expect(isWithinDays(inTenDays, 7)).toBe(false);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("returns false for past date", () => {
|
|
312
|
+
const yesterday = new Date();
|
|
313
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
314
|
+
expect(isWithinDays(yesterday, 7)).toBe(false);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("returns false for invalid date", () => {
|
|
318
|
+
expect(isWithinDays("invalid", 7)).toBe(false);
|
|
319
|
+
expect(isWithinDays(new Date("invalid"), 7)).toBe(false);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("returns false for invalid days parameter", () => {
|
|
323
|
+
const tomorrow = new Date();
|
|
324
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
325
|
+
expect(isWithinDays(tomorrow, -1)).toBe(false);
|
|
326
|
+
expect(isWithinDays(tomorrow, NaN)).toBe(false);
|
|
327
|
+
expect(isWithinDays(tomorrow, Infinity)).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
});
|
package/src/utils/date.ts
CHANGED
|
@@ -247,11 +247,7 @@ export function parseDateString(input: string): Date {
|
|
|
247
247
|
|
|
248
248
|
const d = new Date(input);
|
|
249
249
|
if (isNaN(d.getTime())) {
|
|
250
|
-
throw new DateParseError(
|
|
251
|
-
`Unable to parse "${input}" as a date`,
|
|
252
|
-
input,
|
|
253
|
-
"invalid_format"
|
|
254
|
-
);
|
|
250
|
+
throw new DateParseError(`Unable to parse "${input}" as a date`, input, "invalid_format");
|
|
255
251
|
}
|
|
256
252
|
|
|
257
253
|
return d;
|
|
@@ -280,11 +276,7 @@ export function formatDisplayDate(date: Date | string): string {
|
|
|
280
276
|
}
|
|
281
277
|
|
|
282
278
|
if (!isValidDate(d)) {
|
|
283
|
-
throw new DateParseError(
|
|
284
|
-
"Invalid Date object provided",
|
|
285
|
-
String(date),
|
|
286
|
-
"invalid_date"
|
|
287
|
-
);
|
|
279
|
+
throw new DateParseError("Invalid Date object provided", String(date), "invalid_date");
|
|
288
280
|
}
|
|
289
281
|
|
|
290
282
|
const parts = new Intl.DateTimeFormat(undefined, {
|