diff --git a/apps/server/src/utils/parser.ts b/apps/server/src/utils/parser.ts index 59998bba2..ef88b3653 100644 --- a/apps/server/src/utils/parser.ts +++ b/apps/server/src/utils/parser.ts @@ -1,14 +1,23 @@ import { generateId, - calculateDuration, isExcelImportMap, type ExcelImportMap, defaultExcelImportMap, validateEndAction, validateTimerType, type ExcelImportOptions, + validateTimes, } from 'ontime-utils'; -import { DatabaseModel, OntimeEvent, OntimeRundown, SupportedEvent, ProjectData, UserFields } from 'ontime-types'; +import { + DatabaseModel, + OntimeEvent, + OntimeRundown, + SupportedEvent, + ProjectData, + UserFields, + EndAction, + TimerType, +} from 'ontime-types'; import fs from 'fs'; import xlsx from 'node-xlsx'; @@ -73,9 +82,9 @@ export const parseExcel = (excelData: unknown[][], options?: Partial 0) { // if any data was found, push to array - // take care of it in the next step rundown.push({ ...event, type: SupportedEvent.Event } as OntimeEvent); } }); + return { rundown, project: projectData, @@ -344,16 +358,18 @@ export const validateEvent = (eventArgs: Partial, cueFallback: stri const start = e.timeStart != null && typeof e.timeStart === 'number' ? e.timeStart : d.timeStart; const end = e.timeEnd != null && typeof e.timeEnd === 'number' ? e.timeEnd : d.timeEnd; + const { timeStart, timeEnd, duration } = validateTimes(start, end, e.duration); + event = { ...d, title: makeString(e.title, d.title), subtitle: makeString(e.subtitle, d.subtitle), presenter: makeString(e.presenter, d.presenter), - timeStart: start, - timeEnd: end, - endAction: makeString(e.endAction, d.endAction), - timerType: makeString(e.timerType, d.timerType), - duration: calculateDuration(start, end), + timeStart, + timeEnd, + duration, + endAction: validateEndAction(e.endAction, EndAction.None), + timerType: validateTimerType(e.timerType, TimerType.CountDown), isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic, skip: typeof e.skip === 'boolean' ? e.skip : d.skip, note: makeString(e.note, d.note), @@ -368,8 +384,8 @@ export const validateEvent = (eventArgs: Partial, cueFallback: stri user8: makeString(e.user8, d.user8), user9: makeString(e.user9, d.user9), colour: makeString(e.colour, d.colour), - id, cue: makeString(e.cue, cueFallback), + id, type: 'event', }; } diff --git a/apps/server/src/utils/parserFunctions.ts b/apps/server/src/utils/parserFunctions.ts index 8b4287ad2..3282e48f3 100644 --- a/apps/server/src/utils/parserFunctions.ts +++ b/apps/server/src/utils/parserFunctions.ts @@ -1,7 +1,6 @@ import { generateId } from 'ontime-utils'; import { Alias, - EndAction, OntimeRundown, OSCSettings, OscSubscription, @@ -9,7 +8,6 @@ import { ProjectData, Settings, TimerLifeCycle, - TimerType, UserFields, ViewSettings, } from 'ontime-types'; @@ -45,18 +43,6 @@ export const parseRundown = (data): OntimeRundown => { continue; } - // validate the right endAction is used - if (e.endAction && !Object.values(EndAction).includes(e.endAction)) { - e.endAction = EndAction.None; - console.log('WARNING: invalid End Action provided, using default'); - } - - // validate the right timerType is used - if (e.timerType && !Object.values(TimerType).includes(e.timerType)) { - e.timerType = TimerType.CountDown; - console.log('WARNING: invalid Timer Type provided, using default'); - } - if (e.type === 'event') { eventIndex += 1; const event = validateEvent(e, eventIndex.toString()); diff --git a/packages/utils/index.ts b/packages/utils/index.ts index b9e92aba4..ac0060d56 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -1,12 +1,13 @@ // runtime utils export { getFirst, getFirstEvent, getLastEvent, getNext, getPrevious } from './src/rundown-utils/rundownUtils.js'; export { validatePlayback } from './src/validate-action/validatePlayback.js'; +export { validateTimes } from './src/validate-events/validateEvent.js'; +export { calculateDuration } from './src/validate-events/validateEvent.js'; // rundown utils export { sanitiseCue } from './src/cue-utils/cueUtils.js'; export { getCueCandidate } from './src/cue-utils/cueUtils.js'; export { generateId } from './src/generate-id/generateId.js'; -export { calculateDuration } from './src/rundown-utils/rundownUtils.js'; export { swapOntimeEvents } from './src/rundown-utils/rundownUtils.js'; // format utils diff --git a/packages/utils/src/rundown-utils/rundownUtils.ts b/packages/utils/src/rundown-utils/rundownUtils.ts index 685a8ddf9..dbf75ad25 100644 --- a/packages/utils/src/rundown-utils/rundownUtils.ts +++ b/packages/utils/src/rundown-utils/rundownUtils.ts @@ -1,7 +1,5 @@ import { isOntimeEvent, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types'; -import { dayInMs } from '../timeConstants.js'; - /** * Gets first event in rundown, if it exists * @param {OntimeRundownEntry[]} rundown @@ -113,20 +111,6 @@ export function getPreviousEvent(rundown: OntimeRundownEntry[], currentId: strin return null; } -/** - * @description calculates event duration considering midnight - * @param {number} timeStart - * @param {number} timeEnd - * @returns {number} - */ -export const calculateDuration = (timeStart: number, timeEnd: number): number => { - // Durations must be positive - if (timeEnd < timeStart) { - return timeEnd + dayInMs - timeStart; - } - return timeEnd - timeStart; -}; - /** * @description swaps two OntimeEvents in the rundown * @param {OntimeRundown} rundown diff --git a/packages/utils/src/validate-events/validateEvent.test.ts b/packages/utils/src/validate-events/validateEvent.test.ts index 8958150a6..02e25480d 100644 --- a/packages/utils/src/validate-events/validateEvent.test.ts +++ b/packages/utils/src/validate-events/validateEvent.test.ts @@ -1,7 +1,8 @@ import { EndAction, TimerType } from 'ontime-types'; import { expect } from 'vitest'; -import { validateEndAction, validateTimerType } from './validateEvent'; +import { dayInMs } from '../timeConstants.js'; +import { validateEndAction, validateTimerType, validateTimes } from './validateEvent.js'; describe('validateEndAction()', () => { it('recognises a string representation of an action', () => { @@ -28,3 +29,61 @@ describe('validateTimerType()', () => { expect(invalidType).toBe(TimerType.CountDown); }); }); + +describe('validateTimes()', () => { + it('passes through a well defined time list', () => { + const { timeStart, timeEnd, duration } = validateTimes(5, 10, 5); + expect(timeStart).toBe(5); + expect(timeEnd).toBe(10); + expect(duration).toBe(5); + }); + + it('handles cases when no times are given', () => { + const { timeStart, timeEnd, duration } = validateTimes(null, undefined, null); + expect(timeStart).toBe(0); + expect(timeEnd).toBe(0); + expect(duration).toBe(0); + }); + + it('calculates duration', () => { + const { timeStart, timeEnd, duration } = validateTimes(5, 10); + expect(timeStart).toBe(5); + expect(timeEnd).toBe(10); + expect(duration).toBe(5); + }); + + it('calculates end time', () => { + const { timeStart, timeEnd, duration } = validateTimes(5, undefined, 10); + expect(timeStart).toBe(5); + expect(timeEnd).toBe(15); + expect(duration).toBe(10); + }); + + it('handles events that finish the day after', () => { + const { timeStart, timeEnd, duration } = validateTimes(100, 10); + expect(timeStart).toBe(100); + expect(timeEnd).toBe(10); + expect(duration).toBe(dayInMs - 90); + }); + + it('corrects time in case of conflicts', () => { + const { timeStart, timeEnd, duration } = validateTimes(5, 15, 15); + expect(timeStart).toBe(5); + expect(timeEnd).toBe(15); + expect(duration).toBe(10); + }); + + it('calculates start time', () => { + const { timeStart, timeEnd, duration } = validateTimes(undefined, 15, 10); + expect(timeStart).toBe(5); + expect(timeEnd).toBe(15); + expect(duration).toBe(10); + }); + + it('calculates start and end time', () => { + const { timeStart, timeEnd, duration } = validateTimes(undefined, undefined, 10); + expect(timeStart).toBe(0); + expect(timeEnd).toBe(10); + expect(duration).toBe(10); + }); +}); diff --git a/packages/utils/src/validate-events/validateEvent.ts b/packages/utils/src/validate-events/validateEvent.ts index 536acae63..85b2e6e07 100644 --- a/packages/utils/src/validate-events/validateEvent.ts +++ b/packages/utils/src/validate-events/validateEvent.ts @@ -1,5 +1,7 @@ import { EndAction, TimerType } from 'ontime-types'; +import { dayInMs } from '../timeConstants.js'; + export function validateEndAction(maybeAction: unknown, fallback = EndAction.None) { if (typeof maybeAction !== 'string') { return fallback; @@ -23,3 +25,50 @@ export function validateTimerType(maybeTimerType: unknown, fallback = TimerType. } return fallback; } + +/** + * @description calculates event duration considering midnight + * @param {number} timeStart + * @param {number} timeEnd + * @returns {number} + */ +export const calculateDuration = (timeStart: number, timeEnd: number): number => { + // Durations must be positive + if (timeEnd < timeStart) { + return timeEnd + dayInMs - timeStart; + } + return timeEnd - timeStart; +}; + +export function validateTimes(_start?: number | null, _end?: number | null, _duration?: number | null) { + const timeStart = _start ?? 0; + const timeEnd = _end ?? 0; + const duration = _duration ?? 0; + + if (_start != null && _end != null) { + // Case 1. if we have start and end, duration must be derived + return { timeStart, duration: calculateDuration(timeStart, timeEnd), timeEnd }; + } + + if (_start == null && _end == null) { + if (_duration == null) { + // Case 2. no valid times were given + return { timeStart, duration, timeEnd }; + } + // Case 3. we have a duration and infer the rest + return { timeStart, duration: _duration, timeEnd: _duration }; + } + + if (_start != null) { + // Case 5. with only start, we can calculate the rest + return { timeStart, duration, timeEnd: timeStart + duration }; + } + + if (_end != null) { + // Case 6. with only end, we can calculate the rest + return { timeStart: timeEnd - duration, duration, timeEnd }; + } + + // we should have covered all cases + return { timeStart, duration, timeEnd }; +}