mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 06:04:05 +00:00
refactor: calculate rundown duration
This commit is contained in:
committed by
Carlos Valente
parent
d71df886f2
commit
66774321a5
@@ -70,13 +70,13 @@ describe('generate()', () => {
|
||||
|
||||
it('accounts for gaps in rundown when calculating delays', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200, duration: 100 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Delay, id: 'delay', duration: 200 } as OntimeDelay,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300, duration: 100 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Block, id: 'block', title: 'break' } as OntimeBlock,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Block, id: 'another-block', title: 'another-break' } as OntimeBlock,
|
||||
{ type: SupportedEvent.Event, id: '4', timeStart: 600, timeEnd: 700 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '4', timeStart: 600, timeEnd: 700, duration: 100 } as OntimeEvent,
|
||||
];
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
@@ -91,13 +91,13 @@ describe('generate()', () => {
|
||||
|
||||
it('handles negative delays', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200, duration: 100 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Delay, id: 'delay', duration: -200 } as OntimeDelay,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300, duration: 100 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Block, id: 'block', title: 'break' } as OntimeBlock,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Block, id: 'another-block', title: 'another-break' } as OntimeBlock,
|
||||
{ type: SupportedEvent.Event, id: '4', timeStart: 600, timeEnd: 700 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '4', timeStart: 600, timeEnd: 700, duration: 100 } as OntimeEvent,
|
||||
];
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
@@ -172,10 +172,17 @@ describe('generate()', () => {
|
||||
|
||||
it('calculates total duration', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: 'skipped', skip: true, timeStart: 300, timeEnd: 400 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200, duration: 100 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300, duration: 100 } as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: 'skipped',
|
||||
skip: true,
|
||||
timeStart: 300,
|
||||
timeEnd: 400,
|
||||
duration: 100,
|
||||
} as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
||||
];
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
@@ -186,9 +193,16 @@ describe('generate()', () => {
|
||||
it('calculates total duration with 0 duration events without causing a next day', () => {
|
||||
const testRundown: OntimeRundown = [
|
||||
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 100, duration: 0 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 100, timeEnd: 300 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: 'skipped', skip: true, timeStart: 300, timeEnd: 400 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500 } as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '2', timeStart: 100, timeEnd: 300, duration: 200 } as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: 'skipped',
|
||||
skip: true,
|
||||
timeStart: 300,
|
||||
timeEnd: 400,
|
||||
duration: 0,
|
||||
} as OntimeEvent,
|
||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
||||
];
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
@@ -201,27 +215,28 @@ describe('generate()', () => {
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '1',
|
||||
timeStart: new Date(0).setHours(9),
|
||||
timeEnd: new Date(0).setHours(23),
|
||||
timeStart: 9 * MILLIS_PER_HOUR,
|
||||
timeEnd: 23 * MILLIS_PER_HOUR,
|
||||
duration: (23 - 9) * MILLIS_PER_HOUR,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '2',
|
||||
timeStart: new Date(0).setHours(9),
|
||||
timeEnd: new Date(0).setHours(23),
|
||||
timeStart: 9 * MILLIS_PER_HOUR,
|
||||
timeEnd: 23 * MILLIS_PER_HOUR,
|
||||
duration: (23 - 9) * MILLIS_PER_HOUR,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '3',
|
||||
timeStart: new Date(0).setHours(9),
|
||||
timeEnd: new Date(0).setHours(23),
|
||||
timeStart: 9 * MILLIS_PER_HOUR,
|
||||
timeEnd: 23 * MILLIS_PER_HOUR,
|
||||
duration: (23 - 9) * MILLIS_PER_HOUR,
|
||||
} as OntimeEvent,
|
||||
];
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const expectedDuration = (23 - 9 + 48) * MILLIS_PER_HOUR;
|
||||
expect(millisToString(initResult.totalDuration)).toBe('62:00:00');
|
||||
expect(initResult.totalDuration).toBe(expectedDuration);
|
||||
expect(initResult.totalDuration).toBe((23 - 9 + 48) * MILLIS_PER_HOUR);
|
||||
});
|
||||
|
||||
it('calculates total duration across days', () => {
|
||||
@@ -229,20 +244,21 @@ describe('generate()', () => {
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '1',
|
||||
timeStart: new Date(0).setHours(12),
|
||||
timeEnd: new Date(0).setHours(22),
|
||||
timeStart: 12 * MILLIS_PER_HOUR,
|
||||
timeEnd: 22 * MILLIS_PER_HOUR,
|
||||
duration: 10 * MILLIS_PER_HOUR,
|
||||
} as OntimeEvent,
|
||||
{
|
||||
type: SupportedEvent.Event,
|
||||
id: '2',
|
||||
timeStart: new Date(0).setHours(22),
|
||||
timeEnd: new Date(0).setHours(8),
|
||||
timeStart: 22 * MILLIS_PER_HOUR,
|
||||
timeEnd: 8 * MILLIS_PER_HOUR,
|
||||
duration: (24 - 22 + 8) * MILLIS_PER_HOUR,
|
||||
} as OntimeEvent,
|
||||
];
|
||||
|
||||
const initResult = generate(testRundown);
|
||||
const expectedDuration = 8 * MILLIS_PER_HOUR + (dayInMs - 12 * MILLIS_PER_HOUR);
|
||||
expect(millisToString(initResult.totalDuration)).toBe('20:00:00');
|
||||
expect(initResult.totalDuration).toBe(expectedDuration);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,16 +4,16 @@ import {
|
||||
CustomFields,
|
||||
isOntimeDelay,
|
||||
isOntimeEvent,
|
||||
isPlayableEvent,
|
||||
MaybeNumber,
|
||||
OntimeEvent,
|
||||
OntimeRundown,
|
||||
OntimeRundownEntry,
|
||||
PlayableEvent,
|
||||
} from 'ontime-types';
|
||||
import { generateId, insertAtIndex, reorderArray, swapEventData, checkIsNextDay } from 'ontime-utils';
|
||||
|
||||
import { generateId, insertAtIndex, reorderArray, swapEventData, getTimeFromPrevious } from 'ontime-utils';
|
||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
import { createPatch } from '../../utils/parser.js';
|
||||
import { getTotalDuration } from '../timerUtils.js';
|
||||
import { apply } from './delayUtils.js';
|
||||
import { handleCustomField, handleLink, hasChanges, isDataStale } from './rundownCacheUtils.js';
|
||||
|
||||
@@ -83,73 +83,72 @@ export function generate(
|
||||
totalDuration = 0;
|
||||
totalDelay = 0;
|
||||
|
||||
let accumulatedDelay = 0;
|
||||
let daySpan = 0;
|
||||
let previousStart: MaybeNumber = null;
|
||||
let previousEnd: MaybeNumber = null;
|
||||
let previousDuration: MaybeNumber = null;
|
||||
let previousEntry: PlayableEvent | null = null;
|
||||
let lastEntry: PlayableEvent | null = null;
|
||||
|
||||
for (let i = 0; i < initialRundown.length; i++) {
|
||||
const currentEvent = initialRundown[i];
|
||||
const updatedEvent = { ...currentEvent };
|
||||
// TODO: filter properties that should not be persisted (eg: delay)
|
||||
// we assign a reference to the current entry, this will be mutated in place
|
||||
const currentEntry = initialRundown[i];
|
||||
|
||||
if (isOntimeEvent(updatedEvent)) {
|
||||
// 1. handle links
|
||||
handleLink(i, initialRundown, updatedEvent, links);
|
||||
if (isOntimeEvent(currentEntry)) {
|
||||
// 1. handle links - mutates updatedEvent
|
||||
handleLink(i, initialRundown, currentEntry, links);
|
||||
|
||||
// 2. handle custom fields
|
||||
handleCustomField(customFields, customFieldChangelog, updatedEvent, assignedCustomFields);
|
||||
// 2. handle custom fields - mutates updatedEvent
|
||||
handleCustomField(customFields, customFieldChangelog, currentEntry, assignedCustomFields);
|
||||
|
||||
// update the persisted event
|
||||
initialRundown[i] = updatedEvent;
|
||||
|
||||
// we need to generate the skip event, but dont want to use its times
|
||||
if (!updatedEvent.skip) {
|
||||
// update rundown duration
|
||||
// update rundown metadata, it only concerns playable events
|
||||
if (isPlayableEvent(currentEntry)) {
|
||||
if (firstStart === null) {
|
||||
firstStart = updatedEvent.timeStart;
|
||||
firstStart = currentEntry.timeStart;
|
||||
}
|
||||
lastEnd = updatedEvent.timeEnd;
|
||||
// TODO: carry on last event
|
||||
lastEnd = currentEntry.timeEnd;
|
||||
|
||||
// check if we go over midnight, account for eventual gaps
|
||||
const gapOverMidnight =
|
||||
previousStart !== null && checkIsNextDay(previousStart, updatedEvent.timeStart, previousDuration);
|
||||
const durationOverMidnight = updatedEvent.timeStart > updatedEvent.timeEnd;
|
||||
if (gapOverMidnight || durationOverMidnight) {
|
||||
daySpan++;
|
||||
const timeFromPrevious: number = getTimeFromPrevious(
|
||||
currentEntry.timeStart,
|
||||
currentEntry.timeEnd,
|
||||
previousEntry?.timeStart,
|
||||
previousEntry?.timeEnd,
|
||||
previousEntry?.duration,
|
||||
);
|
||||
totalDuration += timeFromPrevious + currentEntry.duration;
|
||||
|
||||
// remove eventual gaps from the accumulated delay
|
||||
// we only affect positive delays (time forwards)
|
||||
if (totalDelay > 0 && previousEntry) {
|
||||
const gap = Math.max(currentEntry.timeStart - previousEntry.timeEnd, 0);
|
||||
totalDelay = Math.max(totalDelay - gap, 0);
|
||||
}
|
||||
// current event delay is the current accumulated delay
|
||||
currentEntry.delay = totalDelay;
|
||||
// keep copy of event
|
||||
previousEntry = currentEntry;
|
||||
}
|
||||
}
|
||||
|
||||
// calculate delays
|
||||
// !!! this must happen after handling the links
|
||||
if (isOntimeDelay(updatedEvent)) {
|
||||
accumulatedDelay += updatedEvent.duration;
|
||||
} else if (isOntimeEvent(updatedEvent) && !updatedEvent.skip) {
|
||||
const eventStart = updatedEvent.timeStart;
|
||||
|
||||
// we only affect positive delays (time forwards)
|
||||
if (accumulatedDelay > 0 && previousEnd) {
|
||||
const gap = Math.max(eventStart - previousEnd, 0);
|
||||
accumulatedDelay = Math.max(accumulatedDelay - gap, 0);
|
||||
}
|
||||
updatedEvent.delay = accumulatedDelay;
|
||||
previousStart = updatedEvent.timeStart;
|
||||
previousEnd = updatedEvent.timeEnd;
|
||||
previousDuration = updatedEvent.duration;
|
||||
if (isOntimeDelay(currentEntry)) {
|
||||
totalDelay += currentEntry.duration;
|
||||
}
|
||||
|
||||
order.push(updatedEvent.id);
|
||||
rundown[updatedEvent.id] = { ...updatedEvent };
|
||||
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||
DEV: {
|
||||
if (totalDuration < 0) {
|
||||
throw new Error('rundownCache.generate: invalid data');
|
||||
}
|
||||
}
|
||||
|
||||
// add id to order
|
||||
order.push(currentEntry.id);
|
||||
// add entry to rundown
|
||||
rundown[currentEntry.id] = currentEntry;
|
||||
}
|
||||
|
||||
isStale = false;
|
||||
customFieldChangelog.clear();
|
||||
totalDelay = accumulatedDelay;
|
||||
if (lastEnd !== null && firstStart !== null) {
|
||||
totalDuration = getTotalDuration(firstStart, lastEnd, daySpan);
|
||||
}
|
||||
|
||||
return { rundown, order, links, totalDelay, totalDuration, assignedCustomProperties: assignedCustomFields };
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ const initialRuntime: Runtime = {
|
||||
numEvents: 0, // change initiated by user
|
||||
offset: 0, // changes at runtime
|
||||
plannedStart: 0, // only changes if event changes
|
||||
plannedEnd: 0, // only changes if event changes
|
||||
plannedEnd: 0, // only changes if event changes, overflows over dayInMs
|
||||
actualStart: null, // set once we start the timer
|
||||
expectedEnd: null, // changes with runtime, based on offset
|
||||
expectedEnd: null, // changes with runtime, based on offset, overflows over dayInMs
|
||||
} as const;
|
||||
|
||||
const initialTimer: TimerState = {
|
||||
@@ -60,7 +60,7 @@ export type RuntimeState = {
|
||||
timer: TimerState;
|
||||
// private properties of the timer calculations
|
||||
_timer: {
|
||||
forceFinish: MaybeNumber; // wether we should declare an event as finished, will contain the finish time
|
||||
forceFinish: MaybeNumber; // whether we should declare an event as finished, will contain the finish time
|
||||
totalDelay: number; // this value comes from rundown service
|
||||
pausedAt: MaybeNumber;
|
||||
secondaryTarget: MaybeNumber;
|
||||
@@ -149,6 +149,7 @@ type RundownData = {
|
||||
* @param playableRundown
|
||||
*/
|
||||
export function updateRundownData(rundownData: RundownData) {
|
||||
// we keep this in private state since there is no UI use case for it
|
||||
runtimeState._timer.totalDelay = rundownData.totalDelay;
|
||||
|
||||
runtimeState.runtime.numEvents = rundownData.numEvents;
|
||||
|
||||
Reference in New Issue
Block a user