diff --git a/apps/client/src/common/hooks/useSocket.ts b/apps/client/src/common/hooks/useSocket.ts index eab5f0f52..46d384c9c 100644 --- a/apps/client/src/common/hooks/useSocket.ts +++ b/apps/client/src/common/hooks/useSocket.ts @@ -187,6 +187,15 @@ export const useRuntimePlaybackOverview = () => { return useRuntimeStore(featureSelector); }; +export const useTimelineOverview = () => { + const featureSelector = (state: RuntimeStore) => ({ + plannedStart: state.runtime.plannedStart, + plannedEnd: state.runtime.plannedEnd, + }); + + return useRuntimeStore(featureSelector); +}; + export const useTimelineStatus = () => { const featureSelector = (state: RuntimeStore) => ({ clock: state.clock, diff --git a/apps/client/src/features/viewers/ViewWrapper.tsx b/apps/client/src/features/viewers/ViewWrapper.tsx index 63b502329..f44a41103 100644 --- a/apps/client/src/features/viewers/ViewWrapper.tsx +++ b/apps/client/src/features/viewers/ViewWrapper.tsx @@ -39,7 +39,7 @@ type WithDataProps = { publicSelectedId: string | null; runtime: Runtime; selectedId: string | null; - settings: Settings | undefined; + settings: Settings | undefined; // TODO: what is the case for this being undefined? time: ViewExtendedTimer; viewSettings: ViewSettings; }; diff --git a/apps/client/src/features/viewers/timeline/Timeline.tsx b/apps/client/src/features/viewers/timeline/Timeline.tsx index eddba2c5c..fc16b1294 100644 --- a/apps/client/src/features/viewers/timeline/Timeline.tsx +++ b/apps/client/src/features/viewers/timeline/Timeline.tsx @@ -1,7 +1,9 @@ import { memo } from 'react'; import { useViewportSize } from '@mantine/hooks'; import { isOntimeEvent, MaybeNumber, OntimeEvent } from 'ontime-types'; -import { dayInMs, getFirstEvent, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils'; +import { dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils'; + +import { useTimelineOverview } from '../../../common/hooks/useSocket'; import TimelineMarkers from './timeline-markers/TimelineMarkers'; import ProgressBar from './timeline-progress-bar/TimelineProgressBar'; @@ -10,26 +12,6 @@ import { ProgressStatus, TimelineEntry } from './TimelineEntry'; import style from './Timeline.module.scss'; -function useTimeline(rundown: OntimeEvent[]) { - const { firstEvent } = getFirstEvent(rundown); - const { lastEvent } = getLastEvent(rundown); - const firstStart = firstEvent?.timeStart ?? 0; - const lastEnd = lastEvent?.timeEnd ?? 0; - const normalisedLastEnd = lastEnd < firstStart ? lastEnd + dayInMs : lastEnd; - - // we make sure the end accounts for delays - const accumulatedDelay = lastEvent?.delay ?? 0; - // timeline is padded to nearest hours (floor and ceil) - const startHour = getStartHour(firstStart); - const endHour = getEndHour(normalisedLastEnd + accumulatedDelay); - - return { - rundown: rundown, - startHour, - endHour, - }; -} - interface TimelineProps { selectedEventId: string | null; rundown: OntimeEvent[]; @@ -38,15 +20,17 @@ interface TimelineProps { export default memo(Timeline); function Timeline(props: TimelineProps) { - const { selectedEventId, rundown: baseRundown } = props; + const { selectedEventId, rundown } = props; const { width: screenWidth } = useViewportSize(); - const timelineData = useTimeline(baseRundown); + const { plannedStart, plannedEnd } = useTimelineOverview(); - if (timelineData === null) { + if (plannedStart === null || plannedEnd === null) { return null; } - const { rundown, startHour, endHour } = timelineData; + const { lastEvent } = getLastEvent(rundown); + const startHour = getStartHour(plannedStart); + const endHour = getEndHour(plannedEnd + (lastEvent?.delay ?? 0)); let hasTimelinePassedMidnight = false; let previousEventStartTime: MaybeNumber = null; @@ -95,7 +79,7 @@ function Timeline(props: TimelineProps) { duration={event.duration} left={elementLeftPosition} status={eventStatus} - start={normalisedStart} // solve issues related to crossing midnight + start={normalisedStart} // dataset solves issues related to crossing midnight title={event.title} width={elementWidth} /> diff --git a/apps/client/src/features/viewers/timeline/TimelinePage.tsx b/apps/client/src/features/viewers/timeline/TimelinePage.tsx index be339fe09..0cd335b32 100644 --- a/apps/client/src/features/viewers/timeline/TimelinePage.tsx +++ b/apps/client/src/features/viewers/timeline/TimelinePage.tsx @@ -38,6 +38,7 @@ export default function TimelinePage(props: TimelinePageProps) { const { getLocalizedString } = useTranslation(); const clock = formatTime(time.clock); + // holds copy of the rundown with only relevant events const scopedRundown = useMemo(() => { return getScopedRundown(backstageEvents, selectedId); }, [backstageEvents, selectedId]); diff --git a/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts b/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts index 9f2bdc9da..b0cabe21a 100644 --- a/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts +++ b/apps/server/src/services/rundown-service/__tests__/rundownCache.test.ts @@ -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); }); diff --git a/apps/server/src/services/rundown-service/rundownCache.ts b/apps/server/src/services/rundown-service/rundownCache.ts index 4241310f7..c020d3e4e 100644 --- a/apps/server/src/services/rundown-service/rundownCache.ts +++ b/apps/server/src/services/rundown-service/rundownCache.ts @@ -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 }; } diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index 9ee612b7c..b5cd5ece9 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -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; diff --git a/packages/utils/index.ts b/packages/utils/index.ts index faaf9b656..4800c38f0 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -75,6 +75,8 @@ export { validateEndAction, validateTimerType } from './src/validate-events/vali // feature business logic - rundown export { checkIsNow } from './src/date-utils/checkIsNow.js'; export { checkIsNextDay } from './src/date-utils/checkIsNextDay.js'; +export { checkOverlap } from './src/date-utils/checkOverlap.js'; +export { getTimeFromPrevious } from './src/date-utils/getTimeFromPrevious.js'; // feature business logic - spreadsheet import export { diff --git a/packages/utils/src/date-utils/checkIsNextDay.ts b/packages/utils/src/date-utils/checkIsNextDay.ts index 66bb2c6e1..6e337dd36 100644 --- a/packages/utils/src/date-utils/checkIsNextDay.ts +++ b/packages/utils/src/date-utils/checkIsNextDay.ts @@ -8,6 +8,6 @@ * 09:00 - 10:00 * 09:30 - 10:30 */ -export function checkIsNextDay(previousStart: number, timeStart: number, previousDuration?: number): boolean { +export function checkIsNextDay(previousStart: number, timeStart: number, previousDuration: number): boolean { return previousDuration === 0 ? false : timeStart <= previousStart; } diff --git a/packages/utils/src/date-utils/checkOverlap.test.ts b/packages/utils/src/date-utils/checkOverlap.test.ts new file mode 100644 index 000000000..504ce00d9 --- /dev/null +++ b/packages/utils/src/date-utils/checkOverlap.test.ts @@ -0,0 +1,16 @@ +import { checkOverlap } from './checkOverlap'; + +describe('checkOverlap', () => { + it('should return true if events fully overlap', () => { + expect(checkOverlap(1000, 2000, 1000, 2000)).toBe(true); + }); + + it('should return true if one of the events is inside the other', () => { + expect(checkOverlap(1000, 2000, 1000, 1000)).toBe(true); + expect(checkOverlap(1000, 2000, 1500, 1750)).toBe(true); + }); + + it("should return false if events don't overlap", () => { + expect(checkOverlap(1000, 2000, 2000, 3000)).toBe(false); + }); +}); diff --git a/packages/utils/src/date-utils/checkOverlap.ts b/packages/utils/src/date-utils/checkOverlap.ts new file mode 100644 index 000000000..976f4468c --- /dev/null +++ b/packages/utils/src/date-utils/checkOverlap.ts @@ -0,0 +1,19 @@ +/** + * Check if two Ontime events overlap + * @link https://stackoverflow.com/questions/3269434/whats-the-most-efficient-way-to-test-if-two-ranges-overlap + * We use the deconstructed times to facilitate implementation in UI + */ +export function checkOverlap( + previousStart: number, + previousEnd: number, + currentStart: number, + currentEnd: number, +): boolean { + // deal with simple case where the event is later + if (currentStart >= previousEnd) { + return false; + } + + // at this point we know there may be an overlap + return Math.max(previousStart, currentStart) - Math.min(previousEnd, currentEnd) <= 0; +} diff --git a/packages/utils/src/date-utils/getTimeFromPrevious.test.ts b/packages/utils/src/date-utils/getTimeFromPrevious.test.ts new file mode 100644 index 000000000..30e75d524 --- /dev/null +++ b/packages/utils/src/date-utils/getTimeFromPrevious.test.ts @@ -0,0 +1,14 @@ +import { getTimeFromPrevious } from './getTimeFromPrevious'; + +describe('getTimeFromPrevious', () => { + it('returns the time elapsed (gap or overlap) from the previous', () => { + const previousStart = 69600000; // 19:20 + const previousEnd = 71700000; // 19:55 + const previousDuration = 2100000; // 35 minutes + const currentStart = 75600000; // 21:00 + const currentEnd = 81000000; // 22:30 + const expected = 75600000 - 71700000; // current staart - previousEnd + + expect(getTimeFromPrevious(currentStart, currentEnd, previousStart, previousEnd, previousDuration)).toBe(expected); + }); +}); diff --git a/packages/utils/src/date-utils/getTimeFromPrevious.ts b/packages/utils/src/date-utils/getTimeFromPrevious.ts new file mode 100644 index 000000000..39b877615 --- /dev/null +++ b/packages/utils/src/date-utils/getTimeFromPrevious.ts @@ -0,0 +1,46 @@ +import { checkIsNextDay } from './checkIsNextDay'; +import { checkOverlap } from './checkOverlap'; +import { dayInMs } from './conversionUtils'; + +/** + * Utility returns the time elapsed (gap or overlap) from the previous + * It uses deconstructed parameters to simplify implementation in UI + */ +export function getTimeFromPrevious( + currentStart: number, + currentEnd: number, + previousStart?: number, + previousEnd?: number, + previousDuration?: number, +): number { + // there is no previous event + if (previousStart == null || previousEnd == null || previousDuration == null) { + return 0; + } + + // event is linked to previous + if (currentStart === previousEnd) { + return 0; + } + + // event is the day after + if (checkIsNextDay(previousStart, currentStart, previousDuration)) { + // duration is difference between normalised start and previous end + return currentStart + dayInMs - previousEnd; + } + + // event has a gap from previous + if (currentStart > previousEnd) { + return currentStart - previousEnd; + } + + // event overlaps with previous + if (checkOverlap(previousStart, previousEnd, currentStart, currentEnd)) { + // duration is the amount of time the current event has over the previous + // this value must be capped at 0 + return Math.max(currentEnd - previousEnd, 0); + } + + // we need to make sure we return a number, but there are no business cases for this + return 0; +}