Something went wrong. Try again.
A functional toolkit for authoring resource-leveling algorithms — compose constraint and scoring blocks, enumerate feasible schedules
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281import { test, expect, describe } from "bun:test";import { Duration } from "../src/model/Duration.ts";import { TimeUnit } from "../src/model/types.ts";import { createEmptyProject } from "../src/model/Project.ts";import { parseProjectDate, formatProjectDate } from "../src/dateTime.ts";
describe("Duration", () => { describe("constructor and from()", () => { test("creates duration with value and unit", () => { const d = new Duration(5, TimeUnit.Days); expect(d.value).toBe(5); expect(d.unit).toBe(TimeUnit.Days); });
test("static from() creates same as constructor", () => { const d = Duration.from(8, TimeUnit.Hours); expect(d.value).toBe(8); expect(d.unit).toBe(TimeUnit.Hours); }); });
describe("parseSimple()", () => { test("parses minutes", () => { const d = Duration.parseSimple("30m"); expect(d).not.toBeNull(); expect(d!.value).toBe(30); expect(d!.unit).toBe(TimeUnit.Minutes); });
test("parses hours", () => { const d = Duration.parseSimple("8h"); expect(d).not.toBeNull(); expect(d!.value).toBe(8); expect(d!.unit).toBe(TimeUnit.Hours); });
test("parses days", () => { const d = Duration.parseSimple("5d"); expect(d).not.toBeNull(); expect(d!.value).toBe(5); expect(d!.unit).toBe(TimeUnit.Days); });
test("parses weeks", () => { const d = Duration.parseSimple("2w"); expect(d).not.toBeNull(); expect(d!.value).toBe(2); expect(d!.unit).toBe(TimeUnit.Weeks); });
test("parses decimal values", () => { const d = Duration.parseSimple("2.5d"); expect(d).not.toBeNull(); expect(d!.value).toBe(2.5); expect(d!.unit).toBe(TimeUnit.Days); });
test("parses negative values", () => { const d = Duration.parseSimple("-3d"); expect(d).not.toBeNull(); expect(d!.value).toBe(-3); expect(d!.unit).toBe(TimeUnit.Days); });
test("is case insensitive", () => { const d = Duration.parseSimple("5D"); expect(d).not.toBeNull(); expect(d!.value).toBe(5); expect(d!.unit).toBe(TimeUnit.Days); });
test("trims whitespace", () => { const d = Duration.parseSimple(" 5d "); expect(d).not.toBeNull(); expect(d!.value).toBe(5); });
test("returns null for null input", () => { expect(Duration.parseSimple(null)).toBeNull(); });
test("returns null for undefined input", () => { expect(Duration.parseSimple(undefined)).toBeNull(); });
test("returns null for empty string", () => { expect(Duration.parseSimple("")).toBeNull(); });
test("returns null for invalid format", () => { expect(Duration.parseSimple("abc")).toBeNull(); expect(Duration.parseSimple("5x")).toBeNull(); expect(Duration.parseSimple("5")).toBeNull(); }); });
describe("toIso8601()", () => { test("formats weeks", () => { expect(new Duration(2, TimeUnit.Weeks).toIso8601()).toBe("P2W"); });
test("formats days", () => { expect(new Duration(5, TimeUnit.Days).toIso8601()).toBe("P5D"); });
test("formats hours", () => { expect(new Duration(8, TimeUnit.Hours).toIso8601()).toBe("PT8H0M0S"); });
test("formats minutes", () => { expect(new Duration(30, TimeUnit.Minutes).toIso8601()).toBe("PT0H30M0S"); });
test("formats months", () => { expect(new Duration(3, TimeUnit.Months).toIso8601()).toBe("P3M"); });
test("formats percent", () => { expect(new Duration(50, TimeUnit.Percent).toIso8601()).toBe("PT50M0S"); });
test("formats decimal values", () => { expect(new Duration(2.5, TimeUnit.Days).toIso8601()).toBe("P2.5D"); }); });
describe("toSimpleString()", () => { test("formats integer minutes with .0", () => { expect(new Duration(30, TimeUnit.Minutes).toSimpleString()).toBe("30.0m"); });
test("formats integer hours with .0", () => { expect(new Duration(8, TimeUnit.Hours).toSimpleString()).toBe("8.0h"); });
test("formats integer days with .0", () => { expect(new Duration(5, TimeUnit.Days).toSimpleString()).toBe("5.0d"); });
test("formats integer weeks with .0", () => { expect(new Duration(2, TimeUnit.Weeks).toSimpleString()).toBe("2.0w"); });
test("formats months", () => { expect(new Duration(3, TimeUnit.Months).toSimpleString()).toBe("3.0mo"); });
test("formats percent", () => { expect(new Duration(50, TimeUnit.Percent).toSimpleString()).toBe("50.0%"); });
test("preserves decimal values without adding .0", () => { expect(new Duration(2.5, TimeUnit.Days).toSimpleString()).toBe("2.5d"); }); });
describe("round-trip: parseSimple -> toSimpleString -> parseSimple", () => { test("round-trips minutes", () => { const original = Duration.parseSimple("30m")!; const text = original.toSimpleString(); const parsed = Duration.parseSimple(text)!; expect(parsed.value).toBe(original.value); expect(parsed.unit).toBe(original.unit); });
test("round-trips hours", () => { const original = Duration.parseSimple("8h")!; const text = original.toSimpleString(); const parsed = Duration.parseSimple(text)!; expect(parsed.value).toBe(original.value); expect(parsed.unit).toBe(original.unit); });
test("round-trips days", () => { const original = Duration.parseSimple("5d")!; const text = original.toSimpleString(); const parsed = Duration.parseSimple(text)!; expect(parsed.value).toBe(original.value); expect(parsed.unit).toBe(original.unit); });
test("round-trips weeks", () => { const original = Duration.parseSimple("2w")!; const text = original.toSimpleString(); const parsed = Duration.parseSimple(text)!; expect(parsed.value).toBe(original.value); expect(parsed.unit).toBe(original.unit); });
test("round-trips decimal values", () => { const original = Duration.parseSimple("2.5d")!; const text = original.toSimpleString(); const parsed = Duration.parseSimple(text)!; expect(parsed.value).toBe(original.value); expect(parsed.unit).toBe(original.unit); }); });});
describe("createEmptyProject()", () => { test("returns project with default properties", () => { const project = createEmptyProject(); expect(project.properties.title).toBeNull(); expect(project.properties.author).toBeNull(); expect(project.properties.startDate).toBeNull(); expect(project.properties.finishDate).toBeNull(); expect(project.properties.statusDate).toBeNull(); expect(project.properties.defaultCalendarUniqueId).toBeNull(); expect(project.properties.minutesPerDay).toBe(480); expect(project.properties.minutesPerWeek).toBe(2400); expect(project.properties.daysPerMonth).toBe(20); expect(project.properties.saveVersion).toBeNull(); });
test("returns project with empty collections", () => { const project = createEmptyProject(); expect(project.tasks).toEqual([]); expect(project.resources).toEqual([]); expect(project.assignments).toEqual([]); expect(project.calendars).toEqual([]); });});
describe("parseProjectDate()", () => { test("parses ISO date string", () => { const d = parseProjectDate("2024-01-15T09:00:00"); expect(d).toBeInstanceOf(Date); expect(d!.getFullYear()).toBe(2024); expect(d!.getMonth()).toBe(0); expect(d!.getDate()).toBe(15); });
test("returns null for null", () => { expect(parseProjectDate(null)).toBeNull(); });
test("returns null for undefined", () => { expect(parseProjectDate(undefined)).toBeNull(); });
test("returns null for empty string", () => { expect(parseProjectDate("")).toBeNull(); });});
describe("formatProjectDate()", () => { test("formats date as ISO-like string", () => { const d = new Date(2024, 0, 15, 9, 30, 45); const result = formatProjectDate(d); expect(result).toBe("2024-01-15T09:30:45"); });
test("pads single-digit components", () => { const d = new Date(2024, 0, 5, 1, 2, 3); const result = formatProjectDate(d); expect(result).toBe("2024-01-05T01:02:03"); });
test("returns null for null", () => { expect(formatProjectDate(null)).toBeNull(); });
test("returns null for undefined", () => { expect(formatProjectDate(undefined)).toBeNull(); });});
describe("parseProjectDate/formatProjectDate round-trip", () => { test("round-trips a date", () => { const original = new Date(2024, 5, 15, 14, 30, 0); const formatted = formatProjectDate(original)!; const parsed = parseProjectDate(formatted)!; expect(parsed.getFullYear()).toBe(original.getFullYear()); expect(parsed.getMonth()).toBe(original.getMonth()); expect(parsed.getDate()).toBe(original.getDate()); expect(parsed.getHours()).toBe(original.getHours()); expect(parsed.getMinutes()).toBe(original.getMinutes()); expect(parsed.getSeconds()).toBe(original.getSeconds()); });});