mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 18:33:53 +00:00
fix(timer): correct count-to-end expected end across modes and days
The first pass pinned the count-to-end end with `Math.max(expectedStart, plannedEnd)`, but those operands live in different coordinate spaces: `getExpectedStart` bakes in the day shift, the relative-start shift and the live runtime offset, while `plannedEnd` was raw time-of-day. The result was only correct in single-day Absolute mode and drifted in Relative mode and across day boundaries. A count-to-end event is anchored to its fixed wall-clock end, so its expected end is the scheduled end normalised for the day, with no runtime offset and no relative-start shift (matching getExpectedFinish, which returns the raw timeEnd). Compute it directly as `normalisedTimeStart + duration`. Add relative-mode and multi-day unit tests covering the cases the previous formula got wrong. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACGuFWt5aN7Fv3AkYXgxLm
This commit is contained in:
@@ -381,4 +381,44 @@ describe('getExpectedEnd()', () => {
|
||||
|
||||
expect(getExpectedEnd(testEvent, { ...baseState, offset: 0 })).toBe(timeStart + duration);
|
||||
});
|
||||
|
||||
test('a countToEnd event is NOT shifted by the relative-start offset', () => {
|
||||
const testEvent = {
|
||||
timeStart: 100,
|
||||
duration: 50,
|
||||
delay: 0,
|
||||
dayOffset: 0 as Day,
|
||||
countToEnd: true,
|
||||
};
|
||||
|
||||
// in relative mode a regular event would be shifted by actualStart - plannedStart (+30),
|
||||
// but a countToEnd event is anchored to its wall-clock end and stays at 150
|
||||
const relativeState = {
|
||||
...baseState,
|
||||
mode: OffsetMode.Relative,
|
||||
actualStart: 30,
|
||||
plannedStart: 0,
|
||||
offset: 0,
|
||||
};
|
||||
|
||||
// sanity: a regular event in the same state is shifted to 180
|
||||
expect(getExpectedEnd({ ...testEvent, countToEnd: false }, relativeState)).toBe(180);
|
||||
// the countToEnd event is not shifted
|
||||
expect(getExpectedEnd(testEvent, relativeState)).toBe(150);
|
||||
});
|
||||
|
||||
test('a countToEnd event on a later day adds the day offset', () => {
|
||||
const testEvent = {
|
||||
timeStart: 100,
|
||||
duration: 50,
|
||||
delay: 0,
|
||||
dayOffset: 1 as Day,
|
||||
countToEnd: true,
|
||||
};
|
||||
|
||||
// dayOffset 1 with currentDay 0 -> end normalised one day forward
|
||||
expect(getExpectedEnd(testEvent, { ...baseState, currentDay: 0, offset: 0 })).toBe(150 + dayInMs);
|
||||
// when the running event is already on the same day, no extra day is added
|
||||
expect(getExpectedEnd({ ...testEvent, dayOffset: 0 as Day }, { ...baseState, currentDay: 0, offset: 0 })).toBe(150);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,17 +63,21 @@ export function getExpectedStart(
|
||||
|
||||
/**
|
||||
* Computes the normalised expected end of an event.
|
||||
* A countToEnd event ends at its fixed end time regardless of how much overtime
|
||||
* precedes it, so its expected end is pinned to the planned end - it absorbs the
|
||||
* accumulated offset (mirrors getExpectedFinish in the running timer). The end can
|
||||
* only drift later if the event is projected to start after its own end time.
|
||||
* A countToEnd event is anchored to its fixed wall-clock end: it absorbs the accumulated
|
||||
* runtime offset and is not shifted by the relative-start offset, so its expected end is the
|
||||
* scheduled end normalised for the day (mirrors getExpectedFinish in the running timer, which
|
||||
* returns the raw timeEnd). A regular event's end moves with the runtime offset.
|
||||
* The result lives in the same day-normalised space as getExpectedStart (it may exceed dayInMs).
|
||||
*/
|
||||
export function getExpectedEnd(
|
||||
event: Pick<OntimeEvent, 'timeStart' | 'duration' | 'delay' | 'dayOffset' | 'countToEnd'>,
|
||||
state: Parameters<typeof getExpectedStart>[1],
|
||||
): number {
|
||||
const expectedStart = getExpectedStart(event, state);
|
||||
const plannedEnd = event.timeStart + event.duration + event.delay;
|
||||
return event.countToEnd ? Math.max(expectedStart, plannedEnd) : expectedStart + event.duration;
|
||||
if (!event.countToEnd) {
|
||||
return getExpectedStart(event, state) + event.duration;
|
||||
}
|
||||
|
||||
const delayedStart = Math.max(0, event.timeStart + event.delay);
|
||||
const relativeDayOffset = event.dayOffset - state.currentDay;
|
||||
return delayedStart + relativeDayOffset * dayInMs + event.duration;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user