feat(stats): show work and time-off lengths (#66)

This commit is contained in:
nessar committed 2026-10-07 22:39:05 +02:00
1 parent d9f3f8d34a
commit 3fbf9c3172
12 files changed
+373 -20

No files matched your search

+152 -3
View File
@@ -48,6 +48,129 @@ async function runInNonBrowser(request: Parameters<typeof planLeave>[0]) {
beforeEach(() => vi.setSystemTime(new Date('2024-01-01T12:00:00Z')));
afterEach(() => vi.useRealTimers());
describe('calculated work-sequence and time-off-period statistics', () => {
it('keeps fractional work means and merges consecutive declared days across a weekend', async () => {
const result = isPlan(
await runInNonBrowser({
horizon: { start: '2024-01-02', end: '2024-01-11' },
balances: [],
rules: [],
declaredDays: ['2024-01-05', '2024-01-08'].map((date) => ({
date,
type: 'other',
})),
}),
);
expect(result.metrics).toMatchObject({
nonWorkingDayGain: 2,
averageWorkSequence: 2.5,
longestWorkSequence: 3,
averageTimeOffPeriod: 4,
longestTimeOffPeriod: 4,
});
});
it.each([
{
scenario: 'averages the 10, 5 and 15 working days including both horizon edges',
horizon: { start: '2024-01-08', end: '2024-02-21' },
declaredDates: ['2024-01-22', '2024-01-30'],
metrics: {
averageWorkSequence: 10,
longestWorkSequence: 15,
averageTimeOffPeriod: 2,
longestTimeOffPeriod: 3,
nonWorkingDayGain: 2,
},
},
{
scenario: 'keeps the unrounded mean of work sequences of 3, 1 and 6 days',
horizon: { start: '2024-01-02', end: '2024-01-18' },
declaredDates: ['2024-01-05', '2024-01-09'],
metrics: {
averageWorkSequence: 10 / 3,
longestWorkSequence: 6,
averageTimeOffPeriod: 2,
longestTimeOffPeriod: 3,
nonWorkingDayGain: 2,
},
},
{
scenario: 'averages one-day and two-day breaks without adding zero-length work samples',
horizon: { start: '2024-01-02', end: '2024-01-12' },
declaredDates: ['2024-01-03', '2024-01-09', '2024-01-10'],
metrics: {
averageWorkSequence: 5 / 3,
longestWorkSequence: 3,
averageTimeOffPeriod: 1.5,
longestTimeOffPeriod: 2,
nonWorkingDayGain: 0,
},
},
{
scenario: 'does not count or split work on intervening holidays and weekends',
horizon: { start: '2024-05-06', end: '2024-05-15' },
declaredDates: [],
metrics: {
averageWorkSequence: 5,
longestWorkSequence: 5,
averageTimeOffPeriod: null,
longestTimeOffPeriod: null,
nonWorkingDayGain: 0,
},
},
{
scenario:
'retains an empty work average and a measured zero when every workday is declared',
horizon: { start: '2024-01-08', end: '2024-01-13' },
declaredDates: ['2024-01-08', '2024-01-09', '2024-01-10', '2024-01-11', '2024-01-12'],
metrics: {
averageWorkSequence: null,
longestWorkSequence: 0,
averageTimeOffPeriod: 9,
longestTimeOffPeriod: 9,
nonWorkingDayGain: 4,
},
},
{
scenario: 'distinguishes no samples from zero gain in a weekend-only horizon',
horizon: { start: '2024-01-06', end: '2024-01-08' },
declaredDates: [],
metrics: {
averageWorkSequence: null,
longestWorkSequence: 0,
averageTimeOffPeriod: null,
longestTimeOffPeriod: null,
nonWorkingDayGain: 0,
},
},
{
scenario:
'counts Christmas on a Saturday once alongside its Sunday in a declared break',
horizon: { start: '2021-12-20', end: '2021-12-28' },
declaredDates: ['2021-12-24'],
metrics: {
averageWorkSequence: 2.5,
longestWorkSequence: 4,
averageTimeOffPeriod: 3,
longestTimeOffPeriod: 3,
nonWorkingDayGain: 2,
},
},
])('$scenario', async ({ horizon, declaredDates, metrics }) => {
const result = isPlan(
await runInNonBrowser({
horizon,
balances: [],
rules: [],
declaredDays: declaredDates.map((date) => ({ date, type: 'other' })),
}),
);
expect(result.placements).toEqual([]);
expect(result.metrics).toMatchObject(metrics);
});
});
describe('declared holiday patterns and captured entitlement date', () => {
const producer = {
id: 'producer',
@@ -1214,7 +1337,13 @@ describe('planLeave', () => {
nonWorkingDayGain: 2,
},
]);
expect(result.metrics).toMatchObject({ nonWorkingDayGain: 2, longestWorkSequence: 3 });
expect(result.metrics).toMatchObject({
nonWorkingDayGain: 2,
averageWorkSequence: 2,
longestWorkSequence: 3,
averageTimeOffPeriod: 3,
longestTimeOffPeriod: 3,
});
});
it('merges declarations, placements, weekends and national holidays without counting dates twice', async () => {
@@ -1254,7 +1383,14 @@ describe('planLeave', () => {
nonWorkingDayGain: 6,
},
]);
expect(result.metrics).toMatchObject({ nonWorkingDayGain: 6, holidayWeekValue: 5 });
expect(result.metrics).toMatchObject({
nonWorkingDayGain: 6,
holidayWeekValue: 5,
averageWorkSequence: null,
longestWorkSequence: 0,
averageTimeOffPeriod: 10,
longestTimeOffPeriod: 10,
});
});
it('uses declared days to split work sequences while weekends and holidays alone never split them', async () => {
@@ -1319,6 +1455,13 @@ describe('planLeave', () => {
declaredDates: ['2024-01-02', '2024-01-03', '2024-01-05'],
nonWorkingDayGain: 5,
});
expect(result.metrics).toMatchObject({
nonWorkingDayGain: 5,
averageWorkSequence: null,
longestWorkSequence: 0,
averageTimeOffPeriod: 9,
longestTimeOffPeriod: 9,
});
const unanchored = isPlan(
await runInNonBrowser({
horizon: selectedHorizon,
@@ -1328,7 +1471,13 @@ describe('planLeave', () => {
}),
);
expect(unanchored.timeOffPeriods).toEqual([]);
expect(unanchored.metrics.nonWorkingDayGain).toBe(0);
expect(unanchored.metrics).toMatchObject({
nonWorkingDayGain: 0,
averageWorkSequence: 2,
longestWorkSequence: 2,
averageTimeOffPeriod: null,
longestTimeOffPeriod: null,
});
});
it.each([