diff --git a/apps/client/src/common/utils/__tests__/timesManager.test.ts b/apps/client/src/common/utils/__tests__/timesManager.test.ts deleted file mode 100644 index d8ac400e7..000000000 --- a/apps/client/src/common/utils/__tests__/timesManager.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { calculateDuration, DAY_TO_MS } from '../timesManager'; - -describe('calculateDuration()', () => { - describe('Given start and end values', () => { - it('calculates duration correctly', () => { - const testStart = 1; - const testEnd = 2; - const val = calculateDuration(testStart, testEnd); - expect(val).toBe(testEnd - testStart); - }); - }); - - describe('Handles edge cases', () => { - it('when start is after end', () => { - const testStart = 3; - const testEnd = 2; - const val = calculateDuration(testStart, testEnd); - expect(val).toBe(testEnd + DAY_TO_MS - testStart); - }); - it('when both are equal', () => { - const testStart = 1; - const testEnd = 1; - const val = calculateDuration(testStart, testEnd); - expect(val).toBe(testEnd - testStart); - }); - }); -}); diff --git a/apps/client/src/common/utils/timeConstants.js b/apps/client/src/common/utils/timeConstants.js index 39a5c34aa..10ed97a8e 100644 --- a/apps/client/src/common/utils/timeConstants.js +++ b/apps/client/src/common/utils/timeConstants.js @@ -10,15 +10,8 @@ export const mts = 1000; */ export const mtm = 1000 * 60; - /** * millis to hours * @type {number} */ export const mth = 1000 * 60 * 60; - -/** - * milliseconds in a day - * @type {number} - */ -export const DAY_TO_MS = 86400000; diff --git a/apps/client/src/common/utils/timesManager.ts b/apps/client/src/common/utils/timesManager.ts index 435fdf1ab..4c17af915 100644 --- a/apps/client/src/common/utils/timesManager.ts +++ b/apps/client/src/common/utils/timesManager.ts @@ -1,16 +1,5 @@ export type TimeEntryField = 'timeStart' | 'timeEnd' | 'durationOverride'; -/** - * @description Milliseconds in a day - */ -export const DAY_TO_MS = 86400000; - -/** - * @description calculates duration from given values - */ -export const calculateDuration = (start: number, end: number): number => - start > end ? end + DAY_TO_MS - start : end - start; - /** * @description Checks which field the value relates to */ diff --git a/apps/client/src/features/event-editor/composite/EventEditorTimes.tsx b/apps/client/src/features/event-editor/composite/EventEditorTimes.tsx index 21021b99b..b568d5166 100644 --- a/apps/client/src/features/event-editor/composite/EventEditorTimes.tsx +++ b/apps/client/src/features/event-editor/composite/EventEditorTimes.tsx @@ -1,13 +1,13 @@ import { memo, useState } from 'react'; import { Select, Switch } from '@chakra-ui/react'; import { EndAction, OntimeEvent, TimerType } from 'ontime-types'; -import { millisToString } from 'ontime-utils'; +import { calculateDuration, millisToString } from 'ontime-utils'; import TimeInput from '../../../common/components/input/time-input/TimeInput'; import { useEventAction } from '../../../common/hooks/useEventAction'; import { millisToDelayString } from '../../../common/utils/dateConfig'; import { cx } from '../../../common/utils/styleUtils'; -import { calculateDuration, TimeEntryField, validateEntry } from '../../../common/utils/timesManager'; +import { TimeEntryField, validateEntry } from '../../../common/utils/timesManager'; import style from '../EventEditor.module.scss'; diff --git a/apps/client/src/features/rundown/RundownEntry.tsx b/apps/client/src/features/rundown/RundownEntry.tsx index c727646a0..095b20998 100644 --- a/apps/client/src/features/rundown/RundownEntry.tsx +++ b/apps/client/src/features/rundown/RundownEntry.tsx @@ -1,12 +1,12 @@ import { useCallback } from 'react'; import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types'; +import { calculateDuration } from 'ontime-utils'; import { useEventAction } from '../../common/hooks/useEventAction'; import { useAppMode } from '../../common/stores/appModeStore'; import { useEditorSettings } from '../../common/stores/editorSettings'; import { useEmitLog } from '../../common/stores/logger'; import { cloneEvent } from '../../common/utils/eventsManager'; -import { calculateDuration } from '../../common/utils/timesManager'; import BlockBlock from './block-block/BlockBlock'; import DelayBlock from './delay-block/DelayBlock'; diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockProgressBar.tsx b/apps/client/src/features/rundown/event-block/composite/EventBlockProgressBar.tsx index 10d6504b5..91d1dc7d8 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockProgressBar.tsx +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockProgressBar.tsx @@ -1,4 +1,4 @@ -import { Playback } from 'ontime-types'; +import { MaybeNumber, Playback } from 'ontime-types'; import { useTimer } from '../../../../common/hooks/useSocket'; import { clamp } from '../../../../common/utils/math'; @@ -9,18 +9,26 @@ interface EventBlockProgressBarProps { playback?: Playback; } +export function getPercentComplete(remaining: MaybeNumber, total: MaybeNumber): number { + if (remaining === null || total === null) { + return 0; + } + + if (remaining <= 0) { + return 100; + } + + if (remaining === total) { + return 0; + } + + return clamp(100 - (remaining * 100) / total, 0, 100); +} + export default function EventBlockProgressBar(props: EventBlockProgressBarProps) { const { playback } = props; const timer = useTimer(); - const now = Math.floor(Math.max((timer?.current ?? 1) / 1000, 0)); - const complete = (timer?.duration ?? 1) / 1000; - const elapsed = clamp(100 - (now * 100) / complete, 0, 100); - const progress = `${elapsed}%`; - - if ((timer?.current ?? 0) < 0) { - return
; - } - + const progress = `${getPercentComplete(timer.current, timer.duration)}%`; return
; } diff --git a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx b/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx index 81ac70f97..4a4fc648a 100644 --- a/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx +++ b/apps/client/src/features/rundown/event-block/composite/EventBlockTimers.tsx @@ -1,11 +1,11 @@ import { memo, useCallback, useState } from 'react'; import { OntimeEvent } from 'ontime-types'; -import { millisToString } from 'ontime-utils'; +import { calculateDuration, millisToString } from 'ontime-utils'; import TimeInput from '../../../../common/components/input/time-input/TimeInput'; import { useEventAction } from '../../../../common/hooks/useEventAction'; import { millisToDelayString } from '../../../../common/utils/dateConfig'; -import { calculateDuration, TimeEntryField, validateEntry } from '../../../../common/utils/timesManager'; +import { TimeEntryField, validateEntry } from '../../../../common/utils/timesManager'; import style from '../EventBlock.module.scss'; diff --git a/apps/client/src/features/rundown/event-block/composite/__tests__/EventBlockProgressBar.test.ts b/apps/client/src/features/rundown/event-block/composite/__tests__/EventBlockProgressBar.test.ts new file mode 100644 index 000000000..cf07ab972 --- /dev/null +++ b/apps/client/src/features/rundown/event-block/composite/__tests__/EventBlockProgressBar.test.ts @@ -0,0 +1,27 @@ +import { dayInMs } from 'ontime-utils'; + +import { getPercentComplete } from '../EventBlockProgressBar'; + +describe('getPercentComplete()', () => { + describe('calculates progress in normal cases', () => { + const testScenarios = [ + { current: 0, duration: 0, expect: 100 }, + { current: 0, duration: 100, expect: 100 }, + { current: 0, duration: dayInMs, expect: 100 }, + { current: 10, duration: 100, expect: 90 }, + { current: 50, duration: 100, expect: 50 }, + { current: 100, duration: 100, expect: 0 }, + ]; + + testScenarios.forEach((testCase) => { + it(`handles ${testCase.current} / ${testCase.duration}`, () => { + const progress = getPercentComplete(testCase.current, testCase.duration); + expect(progress).toBe(testCase.expect); + }); + }); + }); + it('is 0 if we dont have a current or duration', () => { + const progress = getPercentComplete(null, null); + expect(progress).toBe(0); + }); +}); diff --git a/apps/client/src/features/viewers/countdown/__tests__/Countdown.test.js b/apps/client/src/features/viewers/countdown/__tests__/Countdown.test.js index fdf64106e..b87f3fed4 100644 --- a/apps/client/src/features/viewers/countdown/__tests__/Countdown.test.js +++ b/apps/client/src/features/viewers/countdown/__tests__/Countdown.test.js @@ -1,4 +1,5 @@ -import { DAY_TO_MS } from '../../../../common/utils/timeConstants'; +import { dayInMs } from 'ontime-utils'; + import { fetchTimerData, sanitiseTitle, TimerMessage } from '../countdown.helpers'; describe('sanitiseTitle() function', () => { @@ -73,11 +74,11 @@ describe('fetchTimerData() function', () => { const timeNow = 15000; const followId = 'testId'; const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue }; - const time = { clock: timeNow, current: DAY_TO_MS + endMockValue - startMockValue }; + const time = { clock: timeNow, current: dayInMs + endMockValue - startMockValue }; const { message, timer } = fetchTimerData(time, follow, 'notthesameevent'); expect(message).toBe(TimerMessage.waiting); - expect(timer).toBe(DAY_TO_MS + endMockValue - startMockValue); + expect(timer).toBe(dayInMs + endMockValue - startMockValue); }); it('handle an current event that finishes after midnight', () => { @@ -86,11 +87,11 @@ describe('fetchTimerData() function', () => { const timeNow = 15000; const followId = 'testId'; const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue }; - const time = { clock: timeNow, current: DAY_TO_MS + endMockValue - startMockValue }; + const time = { clock: timeNow, current: dayInMs + endMockValue - startMockValue }; const { message, timer } = fetchTimerData(time, follow, followId); expect(message).toBe(TimerMessage.running); - expect(timer).toBe(DAY_TO_MS + endMockValue - startMockValue); + expect(timer).toBe(dayInMs + endMockValue - startMockValue); }); it('handle an event that finishes after midnight but hasnt started', () => { @@ -99,7 +100,7 @@ describe('fetchTimerData() function', () => { const timeNow = 2000; const followId = 'testId'; const follow = { id: followId, timeStart: startMockValue, timeEnd: endMockValue }; - const time = { clock: timeNow, current: DAY_TO_MS + endMockValue - startMockValue }; + const time = { clock: timeNow, current: dayInMs + endMockValue - startMockValue }; const { message, timer } = fetchTimerData(time, follow, 'notthesameevent'); expect(message).toBe(TimerMessage.toStart); diff --git a/apps/server/src/classes/event-loader/EventLoader.ts b/apps/server/src/classes/event-loader/EventLoader.ts index e2821d443..4fd9d77eb 100644 --- a/apps/server/src/classes/event-loader/EventLoader.ts +++ b/apps/server/src/classes/event-loader/EventLoader.ts @@ -354,46 +354,83 @@ export class EventLoader { * @private */ private _loadThisTitles(event, type) { - if (!event) { - return; + if (type === 'now') { + if (event === null) { + // public + this.titlesPublic.titleNow = null; + this.titlesPublic.subtitleNow = null; + this.titlesPublic.presenterNow = null; + this.titlesPublic.noteNow = null; + this.loaded.selectedPublicEventId = null; + + // private + this.titles.titleNow = null; + this.titles.subtitleNow = null; + this.titles.presenterNow = null; + this.titles.noteNow = null; + this.loaded.selectedEventId = null; + } else { + // public + this.titlesPublic.titleNow = event.title; + this.titlesPublic.subtitleNow = event.subtitle; + this.titlesPublic.presenterNow = event.presenter; + this.titlesPublic.noteNow = event.note; + this.loaded.selectedPublicEventId = event.id; + + // private + this.titles.titleNow = event.title; + this.titles.subtitleNow = event.subtitle; + this.titles.presenterNow = event.presenter; + this.titles.noteNow = event.note; + this.loaded.selectedEventId = event.id; + } + } else if (type === 'now-public') { + if (event === null) { + this.titlesPublic.titleNow = null; + this.titlesPublic.subtitleNow = null; + this.titlesPublic.presenterNow = null; + this.titlesPublic.noteNow = null; + this.loaded.selectedPublicEventId = null; + } else { + this.titlesPublic.titleNow = event.title; + this.titlesPublic.subtitleNow = event.subtitle; + this.titlesPublic.presenterNow = event.presenter; + this.titlesPublic.noteNow = event.note; + this.loaded.selectedPublicEventId = event.id; + } + } else if (type === 'now-private') { + if (event === null) { + this.titles.titleNow = null; + this.titles.subtitleNow = null; + this.titles.presenterNow = null; + this.titles.noteNow = null; + this.loaded.selectedEventId = null; + } else { + this.titles.titleNow = event.title; + this.titles.subtitleNow = event.subtitle; + this.titles.presenterNow = event.presenter; + this.titles.noteNow = event.note; + this.loaded.selectedEventId = event.id; + } } - switch (type) { - // now, load to both public and private - case 'now': + // next, load to both public and private + else if (type === 'next') { + if (event === null) { // public - this.titlesPublic.titleNow = event.title; - this.titlesPublic.subtitleNow = event.subtitle; - this.titlesPublic.presenterNow = event.presenter; - this.titlesPublic.noteNow = event.note; - this.loaded.selectedPublicEventId = event.id; + this.titlesPublic.titleNext = null; + this.titlesPublic.subtitleNext = null; + this.titlesPublic.presenterNext = null; + this.titlesPublic.noteNext = null; + this.loaded.nextPublicEventId = null; // private - this.titles.titleNow = event.title; - this.titles.subtitleNow = event.subtitle; - this.titles.presenterNow = event.presenter; - this.titles.noteNow = event.note; - this.loaded.selectedEventId = event.id; - break; - - case 'now-public': - this.titlesPublic.titleNow = event.title; - this.titlesPublic.subtitleNow = event.subtitle; - this.titlesPublic.presenterNow = event.presenter; - this.titlesPublic.noteNow = event.note; - this.loaded.selectedPublicEventId = event.id; - break; - - case 'now-private': - this.titles.titleNow = event.title; - this.titles.subtitleNow = event.subtitle; - this.titles.presenterNow = event.presenter; - this.titles.noteNow = event.note; - this.loaded.selectedEventId = event.id; - break; - - // next, load to both public and private - case 'next': + this.titles.titleNext = null; + this.titles.subtitleNext = null; + this.titles.presenterNext = null; + this.titles.noteNext = null; + this.loaded.nextEventId = null; + } else { // public this.titlesPublic.titleNext = event.title; this.titlesPublic.subtitleNext = event.subtitle; @@ -407,26 +444,37 @@ export class EventLoader { this.titles.presenterNext = event.presenter; this.titles.noteNext = event.note; this.loaded.nextEventId = event.id; - break; - - case 'next-public': + } + } else if (type === 'next-public') { + if (event === null) { + this.titlesPublic.titleNext = null; + this.titlesPublic.subtitleNext = null; + this.titlesPublic.presenterNext = null; + this.titlesPublic.noteNext = null; + this.loaded.nextPublicEventId = null; + } else { this.titlesPublic.titleNext = event.title; this.titlesPublic.subtitleNext = event.subtitle; this.titlesPublic.presenterNext = event.presenter; this.titlesPublic.noteNext = event.note; this.loaded.nextPublicEventId = event.id; - break; - - case 'next-private': + } + } else if (type === 'next-private') { + if (event === null) { + this.titles.titleNext = null; + this.titles.subtitleNext = null; + this.titles.presenterNext = null; + this.titles.noteNext = null; + this.loaded.nextEventId = null; + } else { this.titles.titleNext = event.title; this.titles.subtitleNext = event.subtitle; this.titles.presenterNext = event.presenter; this.titles.noteNext = event.note; this.loaded.nextEventId = event.id; - break; - - default: - throw new Error(`Unhandled title type: ${type}`); + } + } else { + throw new Error(`Unhandled title type: ${type}`); } } } diff --git a/apps/server/src/services/PlaybackService.ts b/apps/server/src/services/PlaybackService.ts index c463e5cce..cb099c0c7 100644 --- a/apps/server/src/services/PlaybackService.ts +++ b/apps/server/src/services/PlaybackService.ts @@ -1,4 +1,4 @@ -import { LogOrigin, OntimeEvent, Playback } from 'ontime-types'; +import { LogOrigin, OntimeEvent } from 'ontime-types'; import { validatePlayback } from 'ontime-utils'; import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js'; diff --git a/apps/server/src/services/TimerService.ts b/apps/server/src/services/TimerService.ts index 8831692fb..ce76befab 100644 --- a/apps/server/src/services/TimerService.ts +++ b/apps/server/src/services/TimerService.ts @@ -1,9 +1,9 @@ import { EndAction, OntimeEvent, Playback, TimerLifeCycle, TimerState } from 'ontime-types'; +import { calculateDuration, dayInMs } from 'ontime-utils'; import { eventStore } from '../stores/EventStore.js'; import { PlaybackService } from './PlaybackService.js'; import { updateRoll } from './rollUtils.js'; -import { DAY_TO_MS } from '../utils/time.js'; import { integrationService } from './integration-service/IntegrationService.js'; import { getCurrent, getElapsed, getExpectedFinish } from './timerUtils.js'; import { clock } from './Clock.js'; @@ -90,7 +90,7 @@ export class TimerService { // TODO: check if any relevant information warrants update // update relevant information and force update - this.timer.duration = timer.duration; + this.timer.duration = calculateDuration(timer.timeStart, timer.timeEnd); this.timer.timerType = timer.timerType; this.timer.endAction = timer.endAction; @@ -104,7 +104,7 @@ export class TimerService { this.timer.addedTime, ); if (this.timer.startedAt === null) { - this.timer.current = timer.duration; + this.timer.current = this.timer.duration; } this.update(true); } @@ -112,6 +112,7 @@ export class TimerService { /** * Loads given timer to object * @param {object} timer + * @param initialData * @param {number} timer.id * @param {number} timer.timeStart * @param {number} timer.timeEnd @@ -128,8 +129,8 @@ export class TimerService { this._clear(); this.loadedTimerId = timer.id; - this.timer.duration = timer.duration; - this.timer.current = timer.duration; + this.timer.duration = calculateDuration(timer.timeStart, timer.timeEnd); + this.timer.current = this.timer.duration; this.playback = Playback.Armed; this.timer.timerType = timer.timerType; this.timer.endAction = timer.endAction; @@ -168,6 +169,8 @@ export class TimerService { } this.timer.clock = clock.timeNow(); + this.timer.secondaryTimer = null; + this.secondaryTarget = null; // add paused time if it exists if (this.pausedTime) { @@ -267,7 +270,7 @@ export class TimerService { _finishAt: this.timer.expectedFinish >= this.timer.startedAt ? this.timer.expectedFinish - : this.timer.expectedFinish + DAY_TO_MS, + : this.timer.expectedFinish + dayInMs, clock: this.timer.clock, secondaryTimer: this.timer.secondaryTimer, @@ -382,16 +385,20 @@ export class TimerService { this.timer.secondaryTimer = null; this.secondaryTarget = null; + // account for event that finishes the day after + const endTime = + currentEvent.timeEnd < currentEvent.timeStart ? currentEvent.timeEnd + dayInMs : currentEvent.timeEnd; + // when we load a timer in roll, we do the same things as before // but also pre-populate some data as to the running state this.load(currentEvent, { startedAt: currentEvent.timeStart, expectedFinish: currentEvent.timeEnd, - current: currentEvent.timeEnd - this.timer.clock, + current: endTime - this.timer.clock, }); } else if (nextEvent) { // account for day after - const nextStart = nextEvent.timeStart < this.timer.clock ? nextEvent.timeStart + DAY_TO_MS : nextEvent.timeStart; + const nextStart = nextEvent.timeStart < this.timer.clock ? nextEvent.timeStart + dayInMs : nextEvent.timeStart; // nothing now, but something coming up this.timer.secondaryTimer = nextStart - this.timer.clock; this.secondaryTarget = nextStart; diff --git a/apps/server/src/services/__tests__/rollUtils.test.js b/apps/server/src/services/__tests__/rollUtils.test.ts similarity index 69% rename from apps/server/src/services/__tests__/rollUtils.test.js rename to apps/server/src/services/__tests__/rollUtils.test.ts index 498dde448..329ece996 100644 --- a/apps/server/src/services/__tests__/rollUtils.test.js +++ b/apps/server/src/services/__tests__/rollUtils.test.ts @@ -1,10 +1,7 @@ -import { - DAY_TO_MS, - getRollTimers, - normaliseEndTime, - sortArrayByProperty, - updateRoll, -} from '../rollUtils.ts'; +import { OntimeEvent } from 'ontime-types'; +import { dayInMs } from 'ontime-utils'; + +import { getRollTimers, normaliseEndTime, sortArrayByProperty, updateRoll } from '../rollUtils.js'; // test sortArrayByProperty() describe('sort simple arrays of objects', () => { @@ -43,51 +40,51 @@ describe('sort simple arrays of objects', () => { // test getRollTimers() describe('test that roll loads selection in right order', () => { - const eventlist = [ + const eventlist: Partial[] = [ { - id: 1, + id: '1', timeStart: 5, timeEnd: 10, isPublic: false, }, { - id: 2, + id: '2', timeStart: 10, timeEnd: 20, isPublic: false, }, { - id: 3, + id: '3', timeStart: 20, timeEnd: 30, isPublic: false, }, { - id: 4, + id: '4', timeStart: 30, timeEnd: 40, isPublic: false, }, { - id: 5, + id: '5', timeStart: 40, timeEnd: 50, isPublic: true, }, { - id: 6, + id: '6', timeStart: 50, timeEnd: 60, isPublic: false, }, { - id: 7, + id: '7', timeStart: 60, timeEnd: 70, isPublic: true, }, { - id: 8, + id: '8', timeStart: 70, timeEnd: 80, isPublic: false, @@ -109,7 +106,7 @@ describe('test that roll loads selection in right order', () => { currentPublicEvent: null, }; - const state = getRollTimers(eventlist, now); + const state = getRollTimers(eventlist as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); @@ -128,7 +125,7 @@ describe('test that roll loads selection in right order', () => { currentPublicEvent: null, }; - const state = getRollTimers(eventlist, now); + const state = getRollTimers(eventlist as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); @@ -147,7 +144,7 @@ describe('test that roll loads selection in right order', () => { currentPublicEvent: null, }; - const state = getRollTimers(eventlist, now); + const state = getRollTimers(eventlist as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); @@ -166,7 +163,7 @@ describe('test that roll loads selection in right order', () => { currentPublicEvent: null, }; - const state = getRollTimers(eventlist, now); + const state = getRollTimers(eventlist as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); @@ -185,7 +182,7 @@ describe('test that roll loads selection in right order', () => { currentPublicEvent: eventlist[4], }; - const state = getRollTimers(eventlist, now); + const state = getRollTimers(eventlist as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); @@ -204,7 +201,7 @@ describe('test that roll loads selection in right order', () => { currentPublicEvent: eventlist[6], }; - const state = getRollTimers(eventlist, now); + const state = getRollTimers(eventlist as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); @@ -223,11 +220,11 @@ describe('test that roll loads selection in right order', () => { currentPublicEvent: eventlist[6], }; - const state = getRollTimers(eventlist, now); + const state = getRollTimers(eventlist as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); - it('if timer is at 100', () => { + it('if timer is at 100 we roll to day after', () => { const now = 100; const expected = { nowIndex: null, @@ -235,21 +232,21 @@ describe('test that roll loads selection in right order', () => { publicIndex: null, nextIndex: 0, publicNextIndex: 4, - timeToNext: DAY_TO_MS - now + eventlist[0].timeStart, + timeToNext: dayInMs - now + eventlist[0].timeStart, nextEvent: eventlist[0], nextPublicEvent: eventlist[4], currentEvent: null, currentPublicEvent: null, }; - const state = getRollTimers(eventlist, now); + const state = getRollTimers(eventlist as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); it('handles rolls to next day with real values', () => { - const singleEventList = [ + const singleEventList: Partial[] = [ { - id: 1, + id: '1', timeStart: 36000000, // 10:00 timeEnd: 39600000, // 11:00 isPublic: true, @@ -262,34 +259,111 @@ describe('test that roll loads selection in right order', () => { publicIndex: null, nextIndex: 0, publicNextIndex: 0, - timeToNext: DAY_TO_MS - now + singleEventList[0].timeStart, + timeToNext: dayInMs - now + singleEventList[0].timeStart, nextEvent: singleEventList[0], nextPublicEvent: singleEventList[0], currentEvent: null, currentPublicEvent: null, }; - const state = getRollTimers(singleEventList, now); + const state = getRollTimers(singleEventList as OntimeEvent[], now); + expect(state).toStrictEqual(expected); + }); + + it('handles rolls to next day with real values', () => { + const singleEventList: Partial[] = [ + { + id: '1', + timeStart: 36000000, // 10:00 + timeEnd: 3600000, // 01:00 + isPublic: true, + }, + ]; + const now = 60000; // 00:01 + const expected = { + nowIndex: 0, + nowId: singleEventList[0].id, + publicIndex: 0, + nextIndex: null, + publicNextIndex: null, + timeToNext: null, + nextEvent: null, + nextPublicEvent: null, + currentEvent: singleEventList[0], + currentPublicEvent: singleEventList[0], + }; + const state = getRollTimers(singleEventList as OntimeEvent[], now); + expect(state).toStrictEqual(expected); + }); + it('handles rolls to next day with real values', () => { + const singleEventList: Partial[] = [ + { + id: '1', + timeStart: 36000000, // 10:00 + timeEnd: 3600000, // 01:00 + isPublic: true, + }, + ]; + const now = 60000; // 00:01 + const expected = { + nowIndex: 0, + nowId: singleEventList[0].id, + publicIndex: 0, + nextIndex: null, + publicNextIndex: null, + timeToNext: null, + nextEvent: null, + nextPublicEvent: null, + currentEvent: singleEventList[0], + currentPublicEvent: singleEventList[0], + }; + const state = getRollTimers(singleEventList as OntimeEvent[], now); + expect(state).toStrictEqual(expected); + }); + + it('handles roll that goes over midnight', () => { + const singleEventList: Partial[] = [ + { + id: '1', + timeStart: 72000000, // 20:00 + timeEnd: 60000, // 00:10 + isPublic: true, + }, + ]; + const now = 6000; // 00:01 + const expected = { + nowIndex: 0, + nowId: singleEventList[0].id, + publicIndex: 0, + nextIndex: null, + publicNextIndex: null, + timeToNext: null, + nextEvent: null, + nextPublicEvent: null, + currentEvent: singleEventList[0], + currentPublicEvent: singleEventList[0], + }; + const state = getRollTimers(singleEventList as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); }); // test getRollTimers() describe('test that roll behaviour with overlapping times', () => { - const eventlist = [ + const eventlist: Partial[] = [ { - id: 1, + id: '1', timeStart: 10, timeEnd: 10, isPublic: false, }, { - id: 2, + id: '2', timeStart: 10, timeEnd: 20, isPublic: true, }, { - id: 3, + id: '3', timeStart: 10, timeEnd: 30, isPublic: false, @@ -311,7 +385,7 @@ describe('test that roll behaviour with overlapping times', () => { currentPublicEvent: null, }; - const state = getRollTimers(eventlist, now); + const state = getRollTimers(eventlist as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); @@ -330,7 +404,7 @@ describe('test that roll behaviour with overlapping times', () => { currentPublicEvent: eventlist[1], }; - const state = getRollTimers(eventlist, now); + const state = getRollTimers(eventlist as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); @@ -349,7 +423,7 @@ describe('test that roll behaviour with overlapping times', () => { currentPublicEvent: eventlist[1], }; - const state = getRollTimers(eventlist, now); + const state = getRollTimers(eventlist as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); @@ -368,7 +442,7 @@ describe('test that roll behaviour with overlapping times', () => { currentPublicEvent: eventlist[1], }; - const state = getRollTimers(eventlist, now); + const state = getRollTimers(eventlist as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); @@ -387,7 +461,7 @@ describe('test that roll behaviour with overlapping times', () => { currentPublicEvent: eventlist[1], }; - const state = getRollTimers(eventlist, now); + const state = getRollTimers(eventlist as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); }); @@ -396,9 +470,9 @@ describe('test that roll behaviour with overlapping times', () => { describe('test that roll behaviour multi day event edge cases', () => { it('if the start time is the day after end time, and start time is earlier than now', () => { const now = 66600000; // 19:30 - const eventlist = [ + const eventlist: Partial[] = [ { - id: 1, + id: '1', timeStart: 66000000, // 19:20 timeEnd: 54600000, // 16:10 isPublic: false, @@ -406,7 +480,7 @@ describe('test that roll behaviour multi day event edge cases', () => { ]; const expected = { nowIndex: 0, - nowId: 1, + nowId: '1', publicIndex: null, nextIndex: null, publicNextIndex: null, @@ -417,34 +491,39 @@ describe('test that roll behaviour multi day event edge cases', () => { currentPublicEvent: null, }; - const state = getRollTimers(eventlist, now); + const state = getRollTimers(eventlist as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); it('if the start time is the day after end time, and both are later than now', () => { const now = 66840000; // 19:34 - const eventlist = [ + const eventlist: Partial[] = [ { - id: 1, + id: '1', timeStart: 67200000, // 19:40 timeEnd: 66900000, // 19:35 isPublic: false, }, ]; const expected = { - nowIndex: null, - nowId: null, - publicIndex: null, - nextIndex: 0, - publicNextIndex: null, - timeToNext: eventlist[0].timeStart - now, - nextEvent: eventlist[0], - nextPublicEvent: null, - currentEvent: null, + currentEvent: { + id: '1', + isPublic: false, + timeEnd: 66900000, + timeStart: 67200000, + }, currentPublicEvent: null, + nextEvent: null, + nextIndex: null, + nextPublicEvent: null, + nowId: '1', + nowIndex: 0, + publicIndex: null, + publicNextIndex: null, + timeToNext: null, }; - const state = getRollTimers(eventlist, now); + const state = getRollTimers(eventlist as OntimeEvent[], now); expect(state).toStrictEqual(expected); }); }); @@ -460,10 +539,10 @@ test('test typical scenarios', () => { expect(normaliseEndTime(t1.start, t1.end)).toBe(t1_expected); const t2 = { - start: 10 + DAY_TO_MS, + start: 10 + dayInMs, end: 20, }; - const t2_expected = 20 + DAY_TO_MS; + const t2_expected = 20 + dayInMs; expect(normaliseEndTime(t2.start, t2.end)).toBe(t2_expected); @@ -480,7 +559,7 @@ test('test typical scenarios', () => { describe('typical scenarios', () => { it('it updates running events correctly', () => { const timers = { - selectedEventId: 1, + selectedEventId: '1', current: 10, _finishAt: 15, clock: 11, @@ -527,7 +606,7 @@ describe('typical scenarios', () => { it('flags an event end', () => { const timers = { - selectedEventId: 1, + selectedEventId: '1', current: 10, _finishAt: 11, clock: 12, @@ -584,4 +663,44 @@ describe('typical scenarios', () => { expect(updateRoll(timers)).toStrictEqual(expected); }); + + it('counts over midnight', () => { + const timers = { + selectedEventId: '1', + current: 25, + _finishAt: 10 + dayInMs, + clock: dayInMs - 10, + secondaryTimer: null, + secondaryTarget: null, + }; + + const expected = { + updatedTimer: 20, + updatedSecondaryTimer: null, + doRollLoad: false, + isFinished: false, + }; + + expect(updateRoll(timers)).toStrictEqual(expected); + }); + + it('rolls over midnight', () => { + const timers = { + selectedEventId: '1', + current: dayInMs, + _finishAt: 10 + dayInMs, + clock: 10, + secondaryTimer: null, + secondaryTarget: null, + }; + + const expected = { + updatedTimer: dayInMs, + updatedSecondaryTimer: null, + doRollLoad: false, + isFinished: false, + }; + + expect(updateRoll(timers)).toStrictEqual(expected); + }); }); diff --git a/apps/server/src/services/__tests__/timerUtils.test.ts b/apps/server/src/services/__tests__/timerUtils.test.ts index 99127a555..30d3b946d 100644 --- a/apps/server/src/services/__tests__/timerUtils.test.ts +++ b/apps/server/src/services/__tests__/timerUtils.test.ts @@ -1,3 +1,5 @@ +import { dayInMs } from 'ontime-utils'; + import { getCurrent, getElapsed, getExpectedFinish } from '../timerUtils.js'; describe('getExpectedFinish()', () => { @@ -64,6 +66,15 @@ describe('getExpectedFinish()', () => { const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); expect(calculatedFinish).toBe(1); }); + it('finish can be the day after', () => { + const startedAt = 10; + const finishedAt = null; + const duration = dayInMs; + const pausedTime = 0; + const addedTime = 0; + const calculatedFinish = getExpectedFinish(startedAt, finishedAt, duration, pausedTime, addedTime); + expect(calculatedFinish).toBe(10); + }); }); describe('getCurrent()', () => { @@ -94,6 +105,33 @@ describe('getCurrent()', () => { const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); expect(current).toBe(19); }); + it('counts over midnight', () => { + const startedAt = 10; + const duration = dayInMs + 10; + const pausedTime = 0; + const addedTime = 0; + const clock = 10; + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + expect(current).toBe(dayInMs + 10); + }); + it('rolls over midnight', () => { + const startedAt = 10; + const duration = dayInMs + 10; + const pausedTime = 0; + const addedTime = 0; + const clock = 5; + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + expect(current).toBe(15); + }); + it('midnight holds delays', () => { + const startedAt = 10; + const duration = dayInMs + 10; + const pausedTime = 10; + const addedTime = 10; + const clock = 5; + const current = getCurrent(startedAt, duration, addedTime, pausedTime, clock); + expect(current).toBe(35); + }); }); describe('getElapsedTime()', () => { @@ -103,10 +141,12 @@ describe('getElapsedTime()', () => { const elapsed = getElapsed(startedAt, clock); expect(elapsed).toBe(5); }); - it('clock cannot be lower than started time', () => { + it('rolls past midnight', () => { const startedAt = 10; const clock = 5; - expect(() => getElapsed(startedAt, clock)).toThrow(); + const elapsed = getElapsed(startedAt, clock); + + expect(elapsed).toBe(dayInMs - startedAt + clock); }); }); diff --git a/apps/server/src/services/rollUtils.ts b/apps/server/src/services/rollUtils.ts index ef2562898..98a6feac2 100644 --- a/apps/server/src/services/rollUtils.ts +++ b/apps/server/src/services/rollUtils.ts @@ -1,14 +1,10 @@ import { OntimeEvent } from 'ontime-types'; - -/** - * Utility variable: 24 hour in milliseconds . - */ -export const DAY_TO_MS = 86400000; +import { dayInMs } from 'ontime-utils'; /** * handle events that span over midnight */ -export const normaliseEndTime = (start: number, end: number) => (end < start ? end + DAY_TO_MS : end); +export const normaliseEndTime = (start: number, end: number) => (end < start ? end + dayInMs : end); /** * @description Sorts an array of objects by given property @@ -23,13 +19,6 @@ export const sortArrayByProperty = (arr: T[], property: string): T[] => { }); }; -type Timer = { - _startedAt: number; - _finishAt: number; - duration: number; - current: number; -}; - /** * Finds loading information given a current rundown and time * @param {OntimeEvent[]} rundown - List of playable events @@ -61,7 +50,7 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => { const firstEvent = orderedEvents[0]; nextIndex = 0; nextEvent = firstEvent; - timeToNext = firstEvent.timeStart + DAY_TO_MS - timeNow; + timeToNext = firstEvent.timeStart + dayInMs - timeNow; if (firstEvent.isPublic) { nextPublicEvent = firstEvent; @@ -89,6 +78,10 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => { // When does the event end (handle midnight) const normalEnd = normaliseEndTime(event.timeStart, event.timeEnd); + const hasNotEnded = normalEnd > timeNow; + const isFromDayBefore = normalEnd > dayInMs && timeNow < event.timeEnd; + const hasStarted = isFromDayBefore || timeNow >= event.timeStart; + if (normalEnd <= timeNow) { // event ran already @@ -98,7 +91,7 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => { currentPublicEvent = event; publicIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id); } - } else if (normalEnd > timeNow && timeNow >= event.timeStart && !nowFound) { + } else if (hasNotEnded && hasStarted && !nowFound) { // event is running currentEvent = event; nowIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id); @@ -185,6 +178,10 @@ export const updateRoll = (currentTimers: CurrentTimers) => { // if we have something selected and a timer, we are running updatedTimer = _finishAt - clock; + if (updatedTimer > dayInMs) { + updatedTimer -= dayInMs; + } + if (updatedTimer < 0) { isPrimaryFinished = true; // we need a new event diff --git a/apps/server/src/services/rundown-service/RundownService.ts b/apps/server/src/services/rundown-service/RundownService.ts index f1db79314..e75244919 100644 --- a/apps/server/src/services/rundown-service/RundownService.ts +++ b/apps/server/src/services/rundown-service/RundownService.ts @@ -5,26 +5,20 @@ import { OntimeDelay, OntimeEvent, OntimeRundown, + Playback, SupportedEvent, } from 'ontime-types'; import { generateId } from 'ontime-utils'; import { DataProvider } from '../../classes/data-provider/DataProvider.js'; -import { block as blockDef, delay, delay as delayDef, event as eventDef } from '../../models/eventsDefinition.js'; +import { block as blockDef, delay as delayDef, event as eventDef } from '../../models/eventsDefinition.js'; import { MAX_EVENTS } from '../../settings.js'; import { EventLoader, eventLoader } from '../../classes/event-loader/EventLoader.js'; import { eventTimer } from '../TimerService.js'; import { sendRefetch } from '../../adapters/websocketAux.js'; import { runtimeCacheStore } from '../../stores/cachingStore.js'; -import { - cachedAdd, - cachedDelete, - cachedEdit, - cachedReorder, - calculateRuntimeDelaysFrom, - delayedRundownCacheKey, - getDelayedRundown, -} from './delayedRundown.utils.js'; +import { cachedAdd, cachedDelete, cachedEdit, cachedReorder, delayedRundownCacheKey } from './delayedRundown.utils.js'; import { logger } from '../../classes/Logger.js'; +import { clock } from '../Clock.js'; /** * Forces rundown to be recalculated @@ -97,8 +91,9 @@ const isNewNext = () => { */ export function updateTimer(affectedIds?: string[]) { const runningEventId = eventLoader.loaded.selectedEventId; + const nextEventId = eventLoader.loaded.nextEventId; - if (runningEventId === null) { + if (runningEventId === null && nextEventId === null) { return false; } @@ -119,11 +114,22 @@ export function updateTimer(affectedIds?: string[]) { if (eventInMemory) { eventLoader.reset(); - const { loadedEvent } = eventLoader.loadById(runningEventId) || {}; - if (!loadedEvent) { - eventTimer.stop(); + + if (eventTimer.playback === Playback.Roll) { + const rollTimers = eventLoader.findRoll(clock.timeNow()); + if (rollTimers === null) { + eventTimer.stop(); + } else { + const { currentEvent, nextEvent } = rollTimers; + eventTimer.roll(currentEvent, nextEvent); + } } else { - eventTimer.hotReload(loadedEvent); + const { loadedEvent } = eventLoader.loadById(runningEventId) || {}; + if (loadedEvent) { + eventTimer.hotReload(loadedEvent); + } else { + eventTimer.stop(); + } } return true; } diff --git a/apps/server/src/services/timerUtils.ts b/apps/server/src/services/timerUtils.ts index 720045aae..619eae5bb 100644 --- a/apps/server/src/services/timerUtils.ts +++ b/apps/server/src/services/timerUtils.ts @@ -1,4 +1,5 @@ -type MaybeNumber = number | null; +import { MaybeNumber } from 'ontime-types'; +import { dayInMs } from 'ontime-utils'; /** * Calculates expected finish time of a running timer @@ -18,7 +19,14 @@ export function getExpectedFinish( return finishedAt; } - return Math.max(startedAt + duration + pausedTime + addedTime, startedAt); + // handle events that finish the day after + const expectedFinish = startedAt + duration + pausedTime + addedTime; + if (expectedFinish > dayInMs) { + return expectedFinish - dayInMs; + } + + // an event cannot finish before it started (user added too much negative time) + return Math.max(expectedFinish, startedAt); } /** @@ -34,15 +42,23 @@ export function getCurrent( if (startedAt === null) { return null; } + if (startedAt > clock) { + return startedAt + duration + addedTime + pausedTime - clock - dayInMs; + } return startedAt + duration + addedTime + pausedTime - clock; } /** * Calculates elapsed time */ -export function getElapsed(startedAt: number, clock: number) { +export function getElapsed(startedAt: number | null, clock: number): number | null { + if (startedAt === null) { + return null; + } + + // we are in the day after if (startedAt > clock) { - throw new Error('clock cannot be higher than startedAt'); + return dayInMs - startedAt + clock; } return clock - startedAt; } diff --git a/apps/server/src/utils/__tests__/parser.test.js b/apps/server/src/utils/__tests__/parser.test.js index 16ae1d4aa..65ab9a783 100644 --- a/apps/server/src/utils/__tests__/parser.test.js +++ b/apps/server/src/utils/__tests__/parser.test.js @@ -1,9 +1,10 @@ import { vi } from 'vitest'; import { dbModel } from '../../models/dataModel.ts'; import { parseExcel, parseJson, validateEvent } from '../parser.ts'; -import { makeString, validateDuration } from '../parserUtils.js'; +import { makeString } from '../parserUtils.ts'; import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.ts'; import { EndAction, TimerType } from 'ontime-types'; +import { dayInMs } from 'ontime-utils'; describe('test json parser with valid def', () => { const testData = { @@ -898,34 +899,3 @@ describe('test views import', () => { expect(parsed).toStrictEqual(expectedParsedViewSettings); }); }); - -describe('test validateDuration()', () => { - describe('handles valid inputs', () => { - const valid = [ - { test: 'zero values', timeStart: 0, timeEnd: 0 }, - { test: 'end after start', timeStart: 0, timeEnd: 1 }, - ]; - - valid.forEach((t) => { - it(t.test, () => { - const d = validateDuration(t.timeStart, t.timeEnd); - expect(d).toBe(t.timeEnd - t.timeStart); - }); - }); - }); - - describe('handles edge cases', () => { - // edge cases - const testData = [ - { test: 'negative 0', timeStart: -0, timeEnd: -0, expected: 0 }, - { test: 'end before start', timeStart: 2, timeEnd: 1, expected: 0 }, - ]; - - testData.forEach((t) => { - it(t.test, () => { - const d = validateDuration(t.timeStart, t.timeEnd); - expect(d).toBe(t.expected); - }); - }); - }); -}); diff --git a/apps/server/src/utils/__tests__/parserUtils.tests.js b/apps/server/src/utils/__tests__/parserUtils.test.ts similarity index 95% rename from apps/server/src/utils/__tests__/parserUtils.tests.js rename to apps/server/src/utils/__tests__/parserUtils.test.ts index 6bef1ca22..0b348a083 100644 --- a/apps/server/src/utils/__tests__/parserUtils.tests.js +++ b/apps/server/src/utils/__tests__/parserUtils.test.ts @@ -5,9 +5,6 @@ describe('isEmptyObject()', () => { const isEmpty = isEmptyObject({}); expect(isEmpty).toBe(true); }); - test('throws on other types', () => { - expect(() => isEmptyObject(12)).toThrow(); - }); test('resolves an object with methods', () => { const isEmpty = isEmptyObject({ test: 'yes' }); expect(isEmpty).toBe(false); diff --git a/apps/server/src/utils/parser.ts b/apps/server/src/utils/parser.ts index 9848226fa..f7201ce72 100644 --- a/apps/server/src/utils/parser.ts +++ b/apps/server/src/utils/parser.ts @@ -3,11 +3,11 @@ import fs from 'fs'; import xlsx from 'node-xlsx'; -import { generateId } from 'ontime-utils'; +import { generateId, calculateDuration } from 'ontime-utils'; import { DatabaseModel, EventData, OntimeEvent, OntimeRundown, UserFields } from 'ontime-types'; import { event as eventDef } from '../models/eventsDefinition.js'; import { dbModel } from '../models/dataModel.js'; -import { deleteFile, makeString, validateDuration } from './parserUtils.js'; +import { deleteFile, makeString } from './parserUtils.js'; import { parseAliases, parseEventData, @@ -322,7 +322,7 @@ export const validateEvent = (eventArgs) => { timeEnd: end, endAction: makeString(e.endAction, d.endAction), timerType: makeString(e.timerType, d.timerType), - duration: validateDuration(start, end), + duration: calculateDuration(start, end), isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic, skip: typeof e.skip === 'boolean' ? e.skip : d.skip, note: makeString(e.note, d.note), diff --git a/apps/server/src/utils/parserUtils.js b/apps/server/src/utils/parserUtils.ts similarity index 80% rename from apps/server/src/utils/parserUtils.js rename to apps/server/src/utils/parserUtils.ts index a66d17916..3db806970 100644 --- a/apps/server/src/utils/parserUtils.js +++ b/apps/server/src/utils/parserUtils.ts @@ -1,4 +1,5 @@ import fs from 'fs'; +import { dayInMs } from 'ontime-utils'; /** * @description Ensures variable is string, it skips object types @@ -6,23 +7,12 @@ import fs from 'fs'; * @param {string} [fallback=''] - fallback value * @returns {string} - value as string or fallback if not possible */ -export const makeString = (val, fallback = '') => { +export const makeString = (val: any, fallback = ''): string => { if (typeof val === 'string') return val; else if (val == null || val.constructor === Object) return fallback; return val.toString(); }; -/** - * @description validates a duration value against options - * @param {number} timeStart - * @param {number} timeEnd - * @returns {number} - */ -export const validateDuration = (timeStart, timeEnd) => { - // Durations must be positive - return Math.max(timeEnd - timeStart, 0); -}; - /** * @description Delete file from system * @param {string} file - reference to file @@ -54,7 +44,7 @@ export const validateFile = (file) => { * @description Verifies if object is empty * @param {object} obj */ -export const isEmptyObject = (obj) => { +export const isEmptyObject = (obj: object) => { if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) { return Object.keys(obj).length === 0; } @@ -78,7 +68,7 @@ export const mergeObject = (a, b) => { * @description Removes undefined * @param {object} obj */ -export const removeUndefined = (obj) => { +export const removeUndefined = (obj: object) => { const patched = {}; Object.keys({ ...obj }) .filter((key) => typeof obj[key] !== 'undefined') diff --git a/apps/server/src/utils/time.ts b/apps/server/src/utils/time.ts index ef761464e..edac0e539 100644 --- a/apps/server/src/utils/time.ts +++ b/apps/server/src/utils/time.ts @@ -6,7 +6,6 @@ const mth = 1000 * 60 * 60; // millis to hours export const timeFormat = 'HH:mm'; export const timeFormatSeconds = 'HH:mm:ss'; -export const DAY_TO_MS = 86400000; /** * @description Converts an excel date to milliseconds diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 8dd8526c4..b78d6629c 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -23,6 +23,7 @@ import { TimerType } from './definitions/TimerType.type.js'; import { TitleBlock } from './definitions/runtime/TitleBlock.type.js'; import { UserFields } from './definitions/core/UserFields.type.js'; import { ViewSettings } from './definitions/core/Views.type.js'; +import { MaybeNumber } from './utils/utils.type.js'; // DATA MODEL export type { DatabaseModel }; @@ -69,3 +70,6 @@ export type { TimerState }; export type { TitleBlock }; // CLIENT + +// UTILITIES +export type { MaybeNumber }; diff --git a/packages/types/src/utils/utils.type.ts b/packages/types/src/utils/utils.type.ts new file mode 100644 index 000000000..1a22c2055 --- /dev/null +++ b/packages/types/src/utils/utils.type.ts @@ -0,0 +1 @@ +export type MaybeNumber = number | null; diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 6fceea01a..358d33256 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -1,6 +1,15 @@ +// runtime utils +export { validatePlayback } from './src/validate-action/validatePlayback.js'; + +// rundown utils +export { generateId } from './src/generate-id/generateId.js'; +export { calculateDuration } from './src/rundown-utils/rundownUtils.js'; + +// format utils export { formatDisplay } from './src/date-utils/formatDisplay.js'; export { formatFromMillis } from './src/date-utils/formatFromMillis.js'; export { isTimeString } from './src/date-utils/isTimeString.js'; export { millisToString } from './src/date-utils/millisToString.js'; -export { generateId } from './src/generate-id/generateId.js'; -export { validatePlayback } from './src/validate-action/validatePlayback.js'; + +// time utils +export { dayInMs, mts } from './src/timeConstants.js'; diff --git a/packages/utils/src/rundown-utils/rundownUtils.test.ts b/packages/utils/src/rundown-utils/rundownUtils.test.ts new file mode 100644 index 000000000..88fed6ca6 --- /dev/null +++ b/packages/utils/src/rundown-utils/rundownUtils.test.ts @@ -0,0 +1,32 @@ +import { dayInMs } from '../timeConstants.js'; +import { calculateDuration } from './rundownUtils.js'; + +describe('calculateDuration()', () => { + describe('Given start and end values', () => { + it('is the difference between end and start', () => { + const duration = calculateDuration(10, 20); + expect(duration).toBe(10); + }); + }); + + describe('Handles edge cases', () => { + it('handles events that go over midnight', () => { + const duration = calculateDuration(51, 50); + expect(duration).not.toBe(-50); + expect(duration).toBe(dayInMs - 1); + }); + it('when both are equal', () => { + const testStart = 1; + const testEnd = 1; + const val = calculateDuration(testStart, testEnd); + expect(val).toBe(testEnd - testStart); + }); + + it('handles no difference', () => { + const duration1 = calculateDuration(0, 0); + const duration2 = calculateDuration(dayInMs, dayInMs); + expect(duration1).toBe(0); + expect(duration2).toBe(0); + }); + }); +}); diff --git a/packages/utils/src/rundown-utils/rundownUtils.ts b/packages/utils/src/rundown-utils/rundownUtils.ts new file mode 100644 index 000000000..b5e101dfa --- /dev/null +++ b/packages/utils/src/rundown-utils/rundownUtils.ts @@ -0,0 +1,15 @@ +import { dayInMs } from '../timeConstants.js'; + +/** + * @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; +}; diff --git a/packages/utils/src/timeConstants.ts b/packages/utils/src/timeConstants.ts index 3afa656a7..e3e7e2f47 100644 --- a/packages/utils/src/timeConstants.ts +++ b/packages/utils/src/timeConstants.ts @@ -1 +1,9 @@ -export const mts = 1000; // millis to seconds +/** + * Milliseconds in a second + */ +export const mts = 1000; + +/** + * Milliseconds in a day + */ +export const dayInMs = 86400000;