refactor: calculate rundown duration

This commit is contained in:
Carlos Valente
2024-08-26 09:43:41 +02:00
committed by Carlos Valente
parent d71df886f2
commit 66774321a5
13 changed files with 217 additions and 110 deletions
@@ -187,6 +187,15 @@ export const useRuntimePlaybackOverview = () => {
return useRuntimeStore(featureSelector); return useRuntimeStore(featureSelector);
}; };
export const useTimelineOverview = () => {
const featureSelector = (state: RuntimeStore) => ({
plannedStart: state.runtime.plannedStart,
plannedEnd: state.runtime.plannedEnd,
});
return useRuntimeStore(featureSelector);
};
export const useTimelineStatus = () => { export const useTimelineStatus = () => {
const featureSelector = (state: RuntimeStore) => ({ const featureSelector = (state: RuntimeStore) => ({
clock: state.clock, clock: state.clock,
@@ -39,7 +39,7 @@ type WithDataProps = {
publicSelectedId: string | null; publicSelectedId: string | null;
runtime: Runtime; runtime: Runtime;
selectedId: string | null; selectedId: string | null;
settings: Settings | undefined; settings: Settings | undefined; // TODO: what is the case for this being undefined?
time: ViewExtendedTimer; time: ViewExtendedTimer;
viewSettings: ViewSettings; viewSettings: ViewSettings;
}; };
@@ -1,7 +1,9 @@
import { memo } from 'react'; import { memo } from 'react';
import { useViewportSize } from '@mantine/hooks'; import { useViewportSize } from '@mantine/hooks';
import { isOntimeEvent, MaybeNumber, OntimeEvent } from 'ontime-types'; 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 TimelineMarkers from './timeline-markers/TimelineMarkers';
import ProgressBar from './timeline-progress-bar/TimelineProgressBar'; import ProgressBar from './timeline-progress-bar/TimelineProgressBar';
@@ -10,26 +12,6 @@ import { ProgressStatus, TimelineEntry } from './TimelineEntry';
import style from './Timeline.module.scss'; 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 { interface TimelineProps {
selectedEventId: string | null; selectedEventId: string | null;
rundown: OntimeEvent[]; rundown: OntimeEvent[];
@@ -38,15 +20,17 @@ interface TimelineProps {
export default memo(Timeline); export default memo(Timeline);
function Timeline(props: TimelineProps) { function Timeline(props: TimelineProps) {
const { selectedEventId, rundown: baseRundown } = props; const { selectedEventId, rundown } = props;
const { width: screenWidth } = useViewportSize(); const { width: screenWidth } = useViewportSize();
const timelineData = useTimeline(baseRundown); const { plannedStart, plannedEnd } = useTimelineOverview();
if (timelineData === null) { if (plannedStart === null || plannedEnd === null) {
return 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 hasTimelinePassedMidnight = false;
let previousEventStartTime: MaybeNumber = null; let previousEventStartTime: MaybeNumber = null;
@@ -95,7 +79,7 @@ function Timeline(props: TimelineProps) {
duration={event.duration} duration={event.duration}
left={elementLeftPosition} left={elementLeftPosition}
status={eventStatus} status={eventStatus}
start={normalisedStart} // solve issues related to crossing midnight start={normalisedStart} // dataset solves issues related to crossing midnight
title={event.title} title={event.title}
width={elementWidth} width={elementWidth}
/> />
@@ -38,6 +38,7 @@ export default function TimelinePage(props: TimelinePageProps) {
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
const clock = formatTime(time.clock); const clock = formatTime(time.clock);
// holds copy of the rundown with only relevant events
const scopedRundown = useMemo(() => { const scopedRundown = useMemo(() => {
return getScopedRundown(backstageEvents, selectedId); return getScopedRundown(backstageEvents, selectedId);
}, [backstageEvents, selectedId]); }, [backstageEvents, selectedId]);
@@ -70,13 +70,13 @@ describe('generate()', () => {
it('accounts for gaps in rundown when calculating delays', () => { it('accounts for gaps in rundown when calculating delays', () => {
const testRundown: OntimeRundown = [ 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.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.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.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); const initResult = generate(testRundown);
@@ -91,13 +91,13 @@ describe('generate()', () => {
it('handles negative delays', () => { it('handles negative delays', () => {
const testRundown: OntimeRundown = [ 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.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.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.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); const initResult = generate(testRundown);
@@ -172,10 +172,17 @@ describe('generate()', () => {
it('calculates total duration', () => { it('calculates total duration', () => {
const testRundown: OntimeRundown = [ 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.Event, id: '2', timeStart: 200, timeEnd: 300 } 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 } as OntimeEvent, {
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500 } 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); const initResult = generate(testRundown);
@@ -186,9 +193,16 @@ describe('generate()', () => {
it('calculates total duration with 0 duration events without causing a next day', () => { it('calculates total duration with 0 duration events without causing a next day', () => {
const testRundown: OntimeRundown = [ const testRundown: OntimeRundown = [
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 100, duration: 0 } as OntimeEvent, { 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: '2', timeStart: 100, timeEnd: 300, duration: 200 } 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: '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); const initResult = generate(testRundown);
@@ -201,27 +215,28 @@ describe('generate()', () => {
{ {
type: SupportedEvent.Event, type: SupportedEvent.Event,
id: '1', id: '1',
timeStart: new Date(0).setHours(9), timeStart: 9 * MILLIS_PER_HOUR,
timeEnd: new Date(0).setHours(23), timeEnd: 23 * MILLIS_PER_HOUR,
duration: (23 - 9) * MILLIS_PER_HOUR,
} as OntimeEvent, } as OntimeEvent,
{ {
type: SupportedEvent.Event, type: SupportedEvent.Event,
id: '2', id: '2',
timeStart: new Date(0).setHours(9), timeStart: 9 * MILLIS_PER_HOUR,
timeEnd: new Date(0).setHours(23), timeEnd: 23 * MILLIS_PER_HOUR,
duration: (23 - 9) * MILLIS_PER_HOUR,
} as OntimeEvent, } as OntimeEvent,
{ {
type: SupportedEvent.Event, type: SupportedEvent.Event,
id: '3', id: '3',
timeStart: new Date(0).setHours(9), timeStart: 9 * MILLIS_PER_HOUR,
timeEnd: new Date(0).setHours(23), timeEnd: 23 * MILLIS_PER_HOUR,
duration: (23 - 9) * MILLIS_PER_HOUR,
} as OntimeEvent, } as OntimeEvent,
]; ];
const initResult = generate(testRundown); const initResult = generate(testRundown);
const expectedDuration = (23 - 9 + 48) * MILLIS_PER_HOUR; expect(initResult.totalDuration).toBe((23 - 9 + 48) * MILLIS_PER_HOUR);
expect(millisToString(initResult.totalDuration)).toBe('62:00:00');
expect(initResult.totalDuration).toBe(expectedDuration);
}); });
it('calculates total duration across days', () => { it('calculates total duration across days', () => {
@@ -229,20 +244,21 @@ describe('generate()', () => {
{ {
type: SupportedEvent.Event, type: SupportedEvent.Event,
id: '1', id: '1',
timeStart: new Date(0).setHours(12), timeStart: 12 * MILLIS_PER_HOUR,
timeEnd: new Date(0).setHours(22), timeEnd: 22 * MILLIS_PER_HOUR,
duration: 10 * MILLIS_PER_HOUR,
} as OntimeEvent, } as OntimeEvent,
{ {
type: SupportedEvent.Event, type: SupportedEvent.Event,
id: '2', id: '2',
timeStart: new Date(0).setHours(22), timeStart: 22 * MILLIS_PER_HOUR,
timeEnd: new Date(0).setHours(8), timeEnd: 8 * MILLIS_PER_HOUR,
duration: (24 - 22 + 8) * MILLIS_PER_HOUR,
} as OntimeEvent, } as OntimeEvent,
]; ];
const initResult = generate(testRundown); const initResult = generate(testRundown);
const expectedDuration = 8 * MILLIS_PER_HOUR + (dayInMs - 12 * MILLIS_PER_HOUR); const expectedDuration = 8 * MILLIS_PER_HOUR + (dayInMs - 12 * MILLIS_PER_HOUR);
expect(millisToString(initResult.totalDuration)).toBe('20:00:00');
expect(initResult.totalDuration).toBe(expectedDuration); expect(initResult.totalDuration).toBe(expectedDuration);
}); });
@@ -4,16 +4,16 @@ import {
CustomFields, CustomFields,
isOntimeDelay, isOntimeDelay,
isOntimeEvent, isOntimeEvent,
isPlayableEvent,
MaybeNumber, MaybeNumber,
OntimeEvent, OntimeEvent,
OntimeRundown, OntimeRundown,
OntimeRundownEntry, OntimeRundownEntry,
PlayableEvent,
} from 'ontime-types'; } 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 { getDataProvider } from '../../classes/data-provider/DataProvider.js';
import { createPatch } from '../../utils/parser.js'; import { createPatch } from '../../utils/parser.js';
import { getTotalDuration } from '../timerUtils.js';
import { apply } from './delayUtils.js'; import { apply } from './delayUtils.js';
import { handleCustomField, handleLink, hasChanges, isDataStale } from './rundownCacheUtils.js'; import { handleCustomField, handleLink, hasChanges, isDataStale } from './rundownCacheUtils.js';
@@ -83,73 +83,72 @@ export function generate(
totalDuration = 0; totalDuration = 0;
totalDelay = 0; totalDelay = 0;
let accumulatedDelay = 0; let previousEntry: PlayableEvent | null = null;
let daySpan = 0; let lastEntry: PlayableEvent | null = null;
let previousStart: MaybeNumber = null;
let previousEnd: MaybeNumber = null;
let previousDuration: MaybeNumber = null;
for (let i = 0; i < initialRundown.length; i++) { for (let i = 0; i < initialRundown.length; i++) {
const currentEvent = initialRundown[i]; // TODO: filter properties that should not be persisted (eg: delay)
const updatedEvent = { ...currentEvent }; // we assign a reference to the current entry, this will be mutated in place
const currentEntry = initialRundown[i];
if (isOntimeEvent(updatedEvent)) { if (isOntimeEvent(currentEntry)) {
// 1. handle links // 1. handle links - mutates updatedEvent
handleLink(i, initialRundown, updatedEvent, links); handleLink(i, initialRundown, currentEntry, links);
// 2. handle custom fields // 2. handle custom fields - mutates updatedEvent
handleCustomField(customFields, customFieldChangelog, updatedEvent, assignedCustomFields); handleCustomField(customFields, customFieldChangelog, currentEntry, assignedCustomFields);
// update the persisted event // update rundown metadata, it only concerns playable events
initialRundown[i] = updatedEvent; if (isPlayableEvent(currentEntry)) {
// we need to generate the skip event, but dont want to use its times
if (!updatedEvent.skip) {
// update rundown duration
if (firstStart === null) { 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 timeFromPrevious: number = getTimeFromPrevious(
const gapOverMidnight = currentEntry.timeStart,
previousStart !== null && checkIsNextDay(previousStart, updatedEvent.timeStart, previousDuration); currentEntry.timeEnd,
const durationOverMidnight = updatedEvent.timeStart > updatedEvent.timeEnd; previousEntry?.timeStart,
if (gapOverMidnight || durationOverMidnight) { previousEntry?.timeEnd,
daySpan++; 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 // calculate delays
// !!! this must happen after handling the links // !!! this must happen after handling the links
if (isOntimeDelay(updatedEvent)) { if (isOntimeDelay(currentEntry)) {
accumulatedDelay += updatedEvent.duration; totalDelay += currentEntry.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;
} }
order.push(updatedEvent.id); // eslint-disable-next-line no-unused-labels -- dev code path
rundown[updatedEvent.id] = { ...updatedEvent }; 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; isStale = false;
customFieldChangelog.clear(); customFieldChangelog.clear();
totalDelay = accumulatedDelay;
if (lastEnd !== null && firstStart !== null) {
totalDuration = getTotalDuration(firstStart, lastEnd, daySpan);
}
return { rundown, order, links, totalDelay, totalDuration, assignedCustomProperties: assignedCustomFields }; return { rundown, order, links, totalDelay, totalDuration, assignedCustomProperties: assignedCustomFields };
} }
+4 -3
View File
@@ -31,9 +31,9 @@ const initialRuntime: Runtime = {
numEvents: 0, // change initiated by user numEvents: 0, // change initiated by user
offset: 0, // changes at runtime offset: 0, // changes at runtime
plannedStart: 0, // only changes if event changes 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 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; } as const;
const initialTimer: TimerState = { const initialTimer: TimerState = {
@@ -60,7 +60,7 @@ export type RuntimeState = {
timer: TimerState; timer: TimerState;
// private properties of the timer calculations // private properties of the timer calculations
_timer: { _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 totalDelay: number; // this value comes from rundown service
pausedAt: MaybeNumber; pausedAt: MaybeNumber;
secondaryTarget: MaybeNumber; secondaryTarget: MaybeNumber;
@@ -149,6 +149,7 @@ type RundownData = {
* @param playableRundown * @param playableRundown
*/ */
export function updateRundownData(rundownData: RundownData) { 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._timer.totalDelay = rundownData.totalDelay;
runtimeState.runtime.numEvents = rundownData.numEvents; runtimeState.runtime.numEvents = rundownData.numEvents;
+2
View File
@@ -75,6 +75,8 @@ export { validateEndAction, validateTimerType } from './src/validate-events/vali
// feature business logic - rundown // feature business logic - rundown
export { checkIsNow } from './src/date-utils/checkIsNow.js'; export { checkIsNow } from './src/date-utils/checkIsNow.js';
export { checkIsNextDay } from './src/date-utils/checkIsNextDay.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 // feature business logic - spreadsheet import
export { export {
@@ -8,6 +8,6 @@
* 09:00 - 10:00 * 09:00 - 10:00
* 09:30 - 10:30 * 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; return previousDuration === 0 ? false : timeStart <= previousStart;
} }
@@ -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);
});
});
@@ -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;
}
@@ -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);
});
});
@@ -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;
}