chore(repo): initialize planarchy workspace

This commit is contained in:
2026-03-14 14:31:09 +01:00
commit dd55d0e78b
769 changed files with 166461 additions and 0 deletions
@@ -0,0 +1,177 @@
import { describe, expect, it } from "vitest";
import {
computeEvenSpread,
getEstimateMonthRange,
rebalanceSpread,
summarizeMonthlySpread,
} from "../estimate/monthly-spread.js";
describe("getEstimateMonthRange", () => {
it("returns months between two dates in the same year", () => {
expect(
getEstimateMonthRange(new Date("2026-03-01"), new Date("2026-06-30")),
).toEqual(["2026-03", "2026-04", "2026-05", "2026-06"]);
});
it("returns a single month when start and end are in the same month", () => {
expect(
getEstimateMonthRange(new Date("2026-05-10"), new Date("2026-05-20")),
).toEqual(["2026-05"]);
});
it("spans across years", () => {
expect(
getEstimateMonthRange(new Date("2025-11-01"), new Date("2026-02-28")),
).toEqual(["2025-11", "2025-12", "2026-01", "2026-02"]);
});
it("returns empty when endDate < startDate", () => {
expect(
getEstimateMonthRange(new Date("2026-06-01"), new Date("2026-03-01")),
).toEqual([]);
});
});
describe("computeEvenSpread", () => {
it("distributes hours across months weighted by working days", () => {
const result = computeEvenSpread({
totalHours: 100,
startDate: new Date("2026-03-01"),
endDate: new Date("2026-05-31"),
});
expect(result.months).toEqual(["2026-03", "2026-04", "2026-05"]);
// Hours should sum to exactly 100
const sum = Object.values(result.spread).reduce((a, b) => a + b, 0);
expect(Math.round(sum * 10) / 10).toBe(100);
// Each month should have a positive value
for (const month of result.months) {
expect(result.spread[month]).toBeGreaterThan(0);
}
});
it("handles a single month", () => {
const result = computeEvenSpread({
totalHours: 40,
startDate: new Date("2026-04-01"),
endDate: new Date("2026-04-30"),
});
expect(result.months).toEqual(["2026-04"]);
expect(result.spread["2026-04"]).toBe(40);
});
it("handles partial first and last months", () => {
const result = computeEvenSpread({
totalHours: 80,
startDate: new Date("2026-03-15"),
endDate: new Date("2026-04-15"),
});
expect(result.months).toEqual(["2026-03", "2026-04"]);
const sum = Object.values(result.spread).reduce((a, b) => a + b, 0);
expect(Math.round(sum * 10) / 10).toBe(80);
// March 15-31 has fewer working days than April 1-15
// Both should be positive
expect(result.spread["2026-03"]).toBeGreaterThan(0);
expect(result.spread["2026-04"]).toBeGreaterThan(0);
});
it("returns empty spread for empty date range", () => {
const result = computeEvenSpread({
totalHours: 100,
startDate: new Date("2026-06-01"),
endDate: new Date("2026-03-01"),
});
expect(result.months).toEqual([]);
expect(result.spread).toEqual({});
});
it("handles zero total hours", () => {
const result = computeEvenSpread({
totalHours: 0,
startDate: new Date("2026-03-01"),
endDate: new Date("2026-05-31"),
});
expect(result.months).toEqual(["2026-03", "2026-04", "2026-05"]);
for (const month of result.months) {
expect(result.spread[month]).toBe(0);
}
});
});
describe("rebalanceSpread", () => {
it("distributes remaining hours after locked months", () => {
const result = rebalanceSpread({
totalHours: 100,
startDate: new Date("2026-03-01"),
endDate: new Date("2026-05-31"),
lockedMonths: { "2026-03": 50 },
});
expect(result.months).toEqual(["2026-03", "2026-04", "2026-05"]);
expect(result.spread["2026-03"]).toBe(50);
// Remaining 50 hours across April + May
const remaining = (result.spread["2026-04"] ?? 0) + (result.spread["2026-05"] ?? 0);
expect(Math.round(remaining * 10) / 10).toBe(50);
});
it("handles all months locked", () => {
const result = rebalanceSpread({
totalHours: 100,
startDate: new Date("2026-03-01"),
endDate: new Date("2026-04-30"),
lockedMonths: { "2026-03": 60, "2026-04": 40 },
});
expect(result.spread["2026-03"]).toBe(60);
expect(result.spread["2026-04"]).toBe(40);
});
it("clamps remaining to zero when locked exceeds total", () => {
const result = rebalanceSpread({
totalHours: 50,
startDate: new Date("2026-03-01"),
endDate: new Date("2026-04-30"),
lockedMonths: { "2026-03": 60 },
});
expect(result.spread["2026-03"]).toBe(60);
// Remaining is max(0, 50-60) = 0
expect(result.spread["2026-04"]).toBe(0);
});
it("ignores locked months outside the date range", () => {
const result = rebalanceSpread({
totalHours: 80,
startDate: new Date("2026-03-01"),
endDate: new Date("2026-04-30"),
lockedMonths: { "2026-01": 30, "2026-03": 40 },
});
// 2026-01 is outside range, so remaining = 80 - 40 = 40
expect(result.spread["2026-03"]).toBe(40);
expect(result.spread["2026-04"]).toBe(40);
});
});
describe("summarizeMonthlySpread", () => {
it("aggregates hours per month across multiple spreads", () => {
const result = summarizeMonthlySpread([
{ "2026-03": 30, "2026-04": 20 },
{ "2026-03": 10, "2026-04": 15, "2026-05": 25 },
]);
expect(result).toEqual({
"2026-03": 40,
"2026-04": 35,
"2026-05": 25,
});
});
it("returns empty for empty input", () => {
expect(summarizeMonthlySpread([])).toEqual({});
});
});