chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -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({});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user