feat: resolve times on excel import, refs #508

This commit is contained in:
cv
2023-09-29 20:12:53 +02:00
parent a0d4f40bec
commit cf7f6bdbf7
6 changed files with 137 additions and 42 deletions
+26 -10
View File
@@ -1,14 +1,23 @@
import { import {
generateId, generateId,
calculateDuration,
isExcelImportMap, isExcelImportMap,
type ExcelImportMap, type ExcelImportMap,
defaultExcelImportMap, defaultExcelImportMap,
validateEndAction, validateEndAction,
validateTimerType, validateTimerType,
type ExcelImportOptions, type ExcelImportOptions,
validateTimes,
} from 'ontime-utils'; } from 'ontime-utils';
import { DatabaseModel, OntimeEvent, OntimeRundown, SupportedEvent, ProjectData, UserFields } from 'ontime-types'; import {
DatabaseModel,
OntimeEvent,
OntimeRundown,
SupportedEvent,
ProjectData,
UserFields,
EndAction,
TimerType,
} from 'ontime-types';
import fs from 'fs'; import fs from 'fs';
import xlsx from 'node-xlsx'; import xlsx from 'node-xlsx';
@@ -73,9 +82,9 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
let skipIndex: number | null = null; let skipIndex: number | null = null;
// times: numbers // times: numbers
// TODO: handle duration
let timeStartIndex: number | null = null; let timeStartIndex: number | null = null;
let timeEndIndex: number | null = null; let timeEndIndex: number | null = null;
let durationIndex: number | null = null;
// options: enum properties // options: enum properties
let endActionIndex: number | null = null; let endActionIndex: number | null = null;
@@ -130,6 +139,8 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
event.timeStart = parseExcelDate(column); event.timeStart = parseExcelDate(column);
} else if (j === timeEndIndex) { } else if (j === timeEndIndex) {
event.timeEnd = parseExcelDate(column); event.timeEnd = parseExcelDate(column);
} else if (j === durationIndex) {
event.duration = parseExcelDate(column);
} else if (j === titleIndex) { } else if (j === titleIndex) {
event.title = makeString(column, ''); event.title = makeString(column, '');
} else if (j === cueIndex) { } else if (j === cueIndex) {
@@ -202,6 +213,9 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
case importMap.timeEnd: case importMap.timeEnd:
timeEndIndex = j; timeEndIndex = j;
break; break;
case importMap.duration:
durationIndex = j;
break;
case importMap.cue: case importMap.cue:
cueIndex = j; cueIndex = j;
break; break;
@@ -273,10 +287,10 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
if (Object.keys(event).length > 0) { if (Object.keys(event).length > 0) {
// if any data was found, push to array // if any data was found, push to array
// take care of it in the next step
rundown.push({ ...event, type: SupportedEvent.Event } as OntimeEvent); rundown.push({ ...event, type: SupportedEvent.Event } as OntimeEvent);
} }
}); });
return { return {
rundown, rundown,
project: projectData, project: projectData,
@@ -344,16 +358,18 @@ export const validateEvent = (eventArgs: Partial<OntimeEvent>, cueFallback: stri
const start = e.timeStart != null && typeof e.timeStart === 'number' ? e.timeStart : d.timeStart; const start = e.timeStart != null && typeof e.timeStart === 'number' ? e.timeStart : d.timeStart;
const end = e.timeEnd != null && typeof e.timeEnd === 'number' ? e.timeEnd : d.timeEnd; const end = e.timeEnd != null && typeof e.timeEnd === 'number' ? e.timeEnd : d.timeEnd;
const { timeStart, timeEnd, duration } = validateTimes(start, end, e.duration);
event = { event = {
...d, ...d,
title: makeString(e.title, d.title), title: makeString(e.title, d.title),
subtitle: makeString(e.subtitle, d.subtitle), subtitle: makeString(e.subtitle, d.subtitle),
presenter: makeString(e.presenter, d.presenter), presenter: makeString(e.presenter, d.presenter),
timeStart: start, timeStart,
timeEnd: end, timeEnd,
endAction: makeString(e.endAction, d.endAction), duration,
timerType: makeString(e.timerType, d.timerType), endAction: validateEndAction(e.endAction, EndAction.None),
duration: calculateDuration(start, end), timerType: validateTimerType(e.timerType, TimerType.CountDown),
isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic, isPublic: typeof e.isPublic === 'boolean' ? e.isPublic : d.isPublic,
skip: typeof e.skip === 'boolean' ? e.skip : d.skip, skip: typeof e.skip === 'boolean' ? e.skip : d.skip,
note: makeString(e.note, d.note), note: makeString(e.note, d.note),
@@ -368,8 +384,8 @@ export const validateEvent = (eventArgs: Partial<OntimeEvent>, cueFallback: stri
user8: makeString(e.user8, d.user8), user8: makeString(e.user8, d.user8),
user9: makeString(e.user9, d.user9), user9: makeString(e.user9, d.user9),
colour: makeString(e.colour, d.colour), colour: makeString(e.colour, d.colour),
id,
cue: makeString(e.cue, cueFallback), cue: makeString(e.cue, cueFallback),
id,
type: 'event', type: 'event',
}; };
} }
-14
View File
@@ -1,7 +1,6 @@
import { generateId } from 'ontime-utils'; import { generateId } from 'ontime-utils';
import { import {
Alias, Alias,
EndAction,
OntimeRundown, OntimeRundown,
OSCSettings, OSCSettings,
OscSubscription, OscSubscription,
@@ -9,7 +8,6 @@ import {
ProjectData, ProjectData,
Settings, Settings,
TimerLifeCycle, TimerLifeCycle,
TimerType,
UserFields, UserFields,
ViewSettings, ViewSettings,
} from 'ontime-types'; } from 'ontime-types';
@@ -45,18 +43,6 @@ export const parseRundown = (data): OntimeRundown => {
continue; continue;
} }
// validate the right endAction is used
if (e.endAction && !Object.values(EndAction).includes(e.endAction)) {
e.endAction = EndAction.None;
console.log('WARNING: invalid End Action provided, using default');
}
// validate the right timerType is used
if (e.timerType && !Object.values(TimerType).includes(e.timerType)) {
e.timerType = TimerType.CountDown;
console.log('WARNING: invalid Timer Type provided, using default');
}
if (e.type === 'event') { if (e.type === 'event') {
eventIndex += 1; eventIndex += 1;
const event = validateEvent(e, eventIndex.toString()); const event = validateEvent(e, eventIndex.toString());
+2 -1
View File
@@ -1,12 +1,13 @@
// runtime utils // runtime utils
export { getFirst, getFirstEvent, getLastEvent, getNext, getPrevious } from './src/rundown-utils/rundownUtils.js'; export { getFirst, getFirstEvent, getLastEvent, getNext, getPrevious } from './src/rundown-utils/rundownUtils.js';
export { validatePlayback } from './src/validate-action/validatePlayback.js'; export { validatePlayback } from './src/validate-action/validatePlayback.js';
export { validateTimes } from './src/validate-events/validateEvent.js';
export { calculateDuration } from './src/validate-events/validateEvent.js';
// rundown utils // rundown utils
export { sanitiseCue } from './src/cue-utils/cueUtils.js'; export { sanitiseCue } from './src/cue-utils/cueUtils.js';
export { getCueCandidate } from './src/cue-utils/cueUtils.js'; export { getCueCandidate } from './src/cue-utils/cueUtils.js';
export { generateId } from './src/generate-id/generateId.js'; export { generateId } from './src/generate-id/generateId.js';
export { calculateDuration } from './src/rundown-utils/rundownUtils.js';
export { swapOntimeEvents } from './src/rundown-utils/rundownUtils.js'; export { swapOntimeEvents } from './src/rundown-utils/rundownUtils.js';
// format utils // format utils
@@ -1,7 +1,5 @@
import { isOntimeEvent, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types'; import { isOntimeEvent, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
import { dayInMs } from '../timeConstants.js';
/** /**
* Gets first event in rundown, if it exists * Gets first event in rundown, if it exists
* @param {OntimeRundownEntry[]} rundown * @param {OntimeRundownEntry[]} rundown
@@ -113,20 +111,6 @@ export function getPreviousEvent(rundown: OntimeRundownEntry[], currentId: strin
return null; return null;
} }
/**
* @description calculates event duration considering midnight
* @param {number} timeStart
* @param {number} timeEnd
* @returns {number}
*/
export const calculateDuration = (timeStart: number, timeEnd: number): number => {
// Durations must be positive
if (timeEnd < timeStart) {
return timeEnd + dayInMs - timeStart;
}
return timeEnd - timeStart;
};
/** /**
* @description swaps two OntimeEvents in the rundown * @description swaps two OntimeEvents in the rundown
* @param {OntimeRundown} rundown * @param {OntimeRundown} rundown
@@ -1,7 +1,8 @@
import { EndAction, TimerType } from 'ontime-types'; import { EndAction, TimerType } from 'ontime-types';
import { expect } from 'vitest'; import { expect } from 'vitest';
import { validateEndAction, validateTimerType } from './validateEvent'; import { dayInMs } from '../timeConstants.js';
import { validateEndAction, validateTimerType, validateTimes } from './validateEvent.js';
describe('validateEndAction()', () => { describe('validateEndAction()', () => {
it('recognises a string representation of an action', () => { it('recognises a string representation of an action', () => {
@@ -28,3 +29,61 @@ describe('validateTimerType()', () => {
expect(invalidType).toBe(TimerType.CountDown); expect(invalidType).toBe(TimerType.CountDown);
}); });
}); });
describe('validateTimes()', () => {
it('passes through a well defined time list', () => {
const { timeStart, timeEnd, duration } = validateTimes(5, 10, 5);
expect(timeStart).toBe(5);
expect(timeEnd).toBe(10);
expect(duration).toBe(5);
});
it('handles cases when no times are given', () => {
const { timeStart, timeEnd, duration } = validateTimes(null, undefined, null);
expect(timeStart).toBe(0);
expect(timeEnd).toBe(0);
expect(duration).toBe(0);
});
it('calculates duration', () => {
const { timeStart, timeEnd, duration } = validateTimes(5, 10);
expect(timeStart).toBe(5);
expect(timeEnd).toBe(10);
expect(duration).toBe(5);
});
it('calculates end time', () => {
const { timeStart, timeEnd, duration } = validateTimes(5, undefined, 10);
expect(timeStart).toBe(5);
expect(timeEnd).toBe(15);
expect(duration).toBe(10);
});
it('handles events that finish the day after', () => {
const { timeStart, timeEnd, duration } = validateTimes(100, 10);
expect(timeStart).toBe(100);
expect(timeEnd).toBe(10);
expect(duration).toBe(dayInMs - 90);
});
it('corrects time in case of conflicts', () => {
const { timeStart, timeEnd, duration } = validateTimes(5, 15, 15);
expect(timeStart).toBe(5);
expect(timeEnd).toBe(15);
expect(duration).toBe(10);
});
it('calculates start time', () => {
const { timeStart, timeEnd, duration } = validateTimes(undefined, 15, 10);
expect(timeStart).toBe(5);
expect(timeEnd).toBe(15);
expect(duration).toBe(10);
});
it('calculates start and end time', () => {
const { timeStart, timeEnd, duration } = validateTimes(undefined, undefined, 10);
expect(timeStart).toBe(0);
expect(timeEnd).toBe(10);
expect(duration).toBe(10);
});
});
@@ -1,5 +1,7 @@
import { EndAction, TimerType } from 'ontime-types'; import { EndAction, TimerType } from 'ontime-types';
import { dayInMs } from '../timeConstants.js';
export function validateEndAction(maybeAction: unknown, fallback = EndAction.None) { export function validateEndAction(maybeAction: unknown, fallback = EndAction.None) {
if (typeof maybeAction !== 'string') { if (typeof maybeAction !== 'string') {
return fallback; return fallback;
@@ -23,3 +25,50 @@ export function validateTimerType(maybeTimerType: unknown, fallback = TimerType.
} }
return fallback; return fallback;
} }
/**
* @description calculates event duration considering midnight
* @param {number} timeStart
* @param {number} timeEnd
* @returns {number}
*/
export const calculateDuration = (timeStart: number, timeEnd: number): number => {
// Durations must be positive
if (timeEnd < timeStart) {
return timeEnd + dayInMs - timeStart;
}
return timeEnd - timeStart;
};
export function validateTimes(_start?: number | null, _end?: number | null, _duration?: number | null) {
const timeStart = _start ?? 0;
const timeEnd = _end ?? 0;
const duration = _duration ?? 0;
if (_start != null && _end != null) {
// Case 1. if we have start and end, duration must be derived
return { timeStart, duration: calculateDuration(timeStart, timeEnd), timeEnd };
}
if (_start == null && _end == null) {
if (_duration == null) {
// Case 2. no valid times were given
return { timeStart, duration, timeEnd };
}
// Case 3. we have a duration and infer the rest
return { timeStart, duration: _duration, timeEnd: _duration };
}
if (_start != null) {
// Case 5. with only start, we can calculate the rest
return { timeStart, duration, timeEnd: timeStart + duration };
}
if (_end != null) {
// Case 6. with only end, we can calculate the rest
return { timeStart: timeEnd - duration, duration, timeEnd };
}
// we should have covered all cases
return { timeStart, duration, timeEnd };
}