mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 20:03:52 +00:00
V3 (#657)
* refactor: cleanup routes * style: smaller base font * chore: upgrade dependencies * chore: lock node version to electron * refactor: pass HTTP to integration controller (#652) * refactor: deprecate onair control * refactor: remove playback router * Several project files user folder (#617) * chore: automated screenshots (#667) * feat: app settings (#658) * refactor: remove deprecated event data (#674) * Studio clock (#663) --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * Feat: reorder events with alt+ctrl + arrow up/down (#645) * Warning and danger per event (#677) --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> * refactor: stabilise actionHandler (#683) Co-authored-by: Fabian Posenau <fabian@fphome.de> * improvement: hide seconds (#675) * wip: overview (#688) * fix: focus cursor (#695) * refactor: update lower third (#665) * Refactor/time formatting (#696) --------- Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> * feat: multiple selection (#703) --------- Co-authored-by: asharonbaltazar <asharonbaltazar@outlook.com> Co-authored-by: Alex <ac@omnivox.dk> * fix: test - go to `Edit mode` befor tying to click `Event options` button (#708) * refactor: runtime service (#715) * fix: issue with loosing cursor position on message (#719) * remove info panel (#721) * Event editor continue (#722) * update API - part (#709) --------- Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> * refactor: update timers (#729) * feat: many timers (#706) --------- Co-authored-by: arc-alex <ac@omnivox.dk> * refactor: excel cleanup (#734) * refactor: allow import of blocks and skip import (#735) * Project manager (#697) * refactor: UI for linking events (#763) * upgraded pipeline actions (#777) * Over under (#771) * custom fields (#744) --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> * Sheets settings (#774) --------- Co-authored-by: arc-alex <ac@omnivox.dk> * style: tweaks to lower thirds (#785) * refactor: delays account for gaps (#784) * refactor: partial state updates (#780) * feat: generate crash report (#787) * Sheet use limited input device auth flow (#782) --------- Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com> Co-authored-by: Carlos Valente <carlosvalente@pm.me> * Custom fields views (#789) * refactor: deprecate presenter and subtitle (#795) * refactor: organise API around resources (#798) --------- Co-authored-by: Bianca Procopio <biancahprocopio@gmail.com> * Time to end (#804) * Skip fixes (#805) * fix: onair derives from playback * Param nav (#822) --------- Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk> * refactor: download files from interface (#831) * Quick options (#814) * End pause (#832) * chore: bump node version in docker (#834) * refactor: follow in run mode (#840) * fix: uncaught error in http integration (#837) * Apply project (#843) Co-authored-by: Matteo Gheza <matteo.gheza07@gmail.com> Co-authored-by: Ary <arylmoraesn@gmail.com> Co-authored-by: Alex Christoffer Rasmussen <ac@omnivox.dk> Co-authored-by: Fabian Posenau <19673098+kellhogs@users.noreply.github.com> Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: Alex Rohleder <alexrohleder96@gmail.com> Co-authored-by: asharonbaltazar <asharonbaltazar@outlook.com> Co-authored-by: Bianca Procopio <biancahprocopio@gmail.com> Co-authored-by: Fabian Posenau <fabianpos99+github@gmail.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { insertAtIndex, reorderArray, sortArrayByProperty } from './arrayUtils.js';
|
||||
|
||||
describe('insertAtIndex', () => {
|
||||
it('should insert an item at the beginning of the array', () => {
|
||||
const array = [2, 3, 4];
|
||||
const result = insertAtIndex(0, 1, array);
|
||||
expect(result).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('should insert an item at the end of the array', () => {
|
||||
const array = [1, 2, 3];
|
||||
const result = insertAtIndex(3, 4, array);
|
||||
expect(result).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('should insert an item in the middle of the array', () => {
|
||||
const array = [1, 2, 4];
|
||||
const result = insertAtIndex(2, 3, array);
|
||||
expect(result).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('should return a new array and not modify the original array', () => {
|
||||
const array = [1, 2, 3];
|
||||
const result = insertAtIndex(1, 5, array);
|
||||
expect(result).toEqual([1, 5, 2, 3]);
|
||||
expect(array).toEqual([1, 2, 3]); // Original array should remain unchanged
|
||||
});
|
||||
});
|
||||
|
||||
describe('reorderArray', () => {
|
||||
it('should reorder an item in the array', () => {
|
||||
const array = ['a', 'b', 'c', 'd'];
|
||||
const result = reorderArray(array, 1, 3);
|
||||
expect(result).toEqual(['a', 'c', 'd', 'b']);
|
||||
});
|
||||
|
||||
it('should return the original array if fromIndex and toIndex are the same', () => {
|
||||
const array = ['a', 'b', 'c'];
|
||||
const result = reorderArray(array, 1, 1);
|
||||
expect(result).toEqual(array);
|
||||
});
|
||||
|
||||
it('should handle reordering to the beginning of the array', () => {
|
||||
const array = ['a', 'b', 'c'];
|
||||
const result = reorderArray(array, 2, 0);
|
||||
expect(result).toEqual(['c', 'a', 'b']);
|
||||
});
|
||||
|
||||
it('should handle reordering to the end of the array', () => {
|
||||
const array = ['a', 'b', 'c'];
|
||||
const result = reorderArray(array, 0, 2);
|
||||
expect(result).toEqual(['b', 'c', 'a']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortArrayByProperty()', () => {
|
||||
it('sort array 1-5', () => {
|
||||
const arr1 = [{ timeStart: 1 }, { timeStart: 5 }, { timeStart: 3 }, { timeStart: 2 }, { timeStart: 4 }];
|
||||
|
||||
const arr1Expected = [{ timeStart: 1 }, { timeStart: 2 }, { timeStart: 3 }, { timeStart: 4 }, { timeStart: 5 }];
|
||||
|
||||
const sorted = sortArrayByProperty(arr1, 'timeStart');
|
||||
expect(sorted).toStrictEqual(arr1Expected);
|
||||
});
|
||||
|
||||
it('sort array 1-5 with null', () => {
|
||||
const arr1 = [
|
||||
{ timeStart: 1 },
|
||||
{ timeStart: 5 },
|
||||
{ timeStart: 3 },
|
||||
{ timeStart: 2 },
|
||||
{ timeStart: 4 },
|
||||
{ timeStart: null },
|
||||
];
|
||||
|
||||
const arr1Expected = [
|
||||
{ timeStart: null },
|
||||
{ timeStart: 1 },
|
||||
{ timeStart: 2 },
|
||||
{ timeStart: 3 },
|
||||
{ timeStart: 4 },
|
||||
{ timeStart: 5 },
|
||||
];
|
||||
|
||||
const sorted = sortArrayByProperty(arr1, 'timeStart');
|
||||
expect(sorted).toStrictEqual(arr1Expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Inserts an item in an array at a given index
|
||||
* @param index
|
||||
* @param item
|
||||
* @param array
|
||||
*/
|
||||
export function insertAtIndex<T>(index: number, item: T, array: T[]): T[] {
|
||||
const modifiedArray = [...array];
|
||||
|
||||
// Insert at beginning
|
||||
if (index === 0) {
|
||||
modifiedArray.unshift(item);
|
||||
}
|
||||
|
||||
// insert at end
|
||||
else if (index >= modifiedArray.length) {
|
||||
modifiedArray.push(item);
|
||||
}
|
||||
|
||||
// insert in the middle
|
||||
else {
|
||||
modifiedArray.splice(index, 0, item);
|
||||
}
|
||||
|
||||
return modifiedArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes array element at a given index
|
||||
* @param index
|
||||
* @param array
|
||||
*/
|
||||
export function deleteAtIndex<T>(index: number, array: T[]) {
|
||||
return array.filter((_, i) => i !== index);
|
||||
}
|
||||
|
||||
export function reorderArray<T>(array: T[], fromIndex: number, toIndex: number) {
|
||||
if (fromIndex === toIndex) {
|
||||
return array; // No change needed, return the original array
|
||||
}
|
||||
|
||||
const modifiedArray = [...array];
|
||||
|
||||
// delete in from
|
||||
const [reorderedItem] = modifiedArray.splice(fromIndex, 1);
|
||||
|
||||
// reinsert item at to
|
||||
modifiedArray.splice(toIndex, 0, reorderedItem);
|
||||
return modifiedArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Sorts an array of objects by given property
|
||||
* @param {array} arr - array to be sorted
|
||||
* @param {string} property - property to compare
|
||||
* @returns {array} copy of array sorted in ascending order
|
||||
*/
|
||||
|
||||
export const sortArrayByProperty = <T>(arr: T[], property: string): T[] => {
|
||||
return [...arr].sort((a, b) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore -- its ok
|
||||
return a[property] - b[property];
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { OntimeDelay, OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||
import type { OntimeDelay, OntimeEvent, OntimeRundown } from 'ontime-types';
|
||||
import { SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { getCueCandidate, getIncrement, sanitiseCue } from './cueUtils.js';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isOntimeEvent, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||
import type { OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||
import { isOntimeEvent } from 'ontime-types';
|
||||
|
||||
import { getFirstEvent, getNextEvent, getPreviousEvent } from '../rundown-utils/rundownUtils.js';
|
||||
import { isNumeric } from '../types/types.js';
|
||||
@@ -24,8 +25,8 @@ export function getIncrement(input: string): string {
|
||||
if (decimalPart === '.99') {
|
||||
decimalPart = '.100';
|
||||
} else {
|
||||
const addDecimal = '0'.repeat(decimalPart.length - 2) + '1';
|
||||
const incrementedDecimal = (Number(decimalPart) + Number('0.' + addDecimal)).toFixed(decimalPart.length - 1);
|
||||
const addDecimal = `${'0'.repeat(decimalPart.length - 2)}1`;
|
||||
const incrementedDecimal = (Number(decimalPart) + Number(`0.${addDecimal}`)).toFixed(decimalPart.length - 1);
|
||||
decimalPart = incrementedDecimal.toString().replace('0.', '.');
|
||||
}
|
||||
return `${prefix}${integerPart}${decimalPart}`;
|
||||
@@ -35,7 +36,7 @@ export function getIncrement(input: string): string {
|
||||
return `${prefix}${integerPart}`;
|
||||
}
|
||||
// If no number is found, append "2" to the string and return the updated string
|
||||
return input + '2';
|
||||
return `${input}2`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +46,7 @@ export function getIncrement(input: string): string {
|
||||
*/
|
||||
export function getCueCandidate(rundown: OntimeRundown, insertAfterId?: string): string {
|
||||
function addAtTop() {
|
||||
const firstEventCue = getFirstEvent(rundown)?.cue;
|
||||
const firstEventCue = getFirstEvent(rundown).firstEvent?.cue;
|
||||
|
||||
if (isNumeric(firstEventCue)) {
|
||||
return (Number(firstEventCue) / 10).toString();
|
||||
@@ -68,11 +69,11 @@ export function getCueCandidate(rundown: OntimeRundown, insertAfterId?: string):
|
||||
// get elements around
|
||||
let previousEvent: OntimeRundownEntry | undefined | null | OntimeEvent = rundown.at(afterIndex);
|
||||
if (!isOntimeEvent(previousEvent)) {
|
||||
previousEvent = getPreviousEvent(rundown, insertAfterId) as null | OntimeEvent;
|
||||
previousEvent = getPreviousEvent(rundown, insertAfterId).previousEvent as null | OntimeEvent;
|
||||
}
|
||||
|
||||
let cue = '1';
|
||||
const nextEvent = getNextEvent(rundown, insertAfterId);
|
||||
const { nextEvent } = getNextEvent(rundown, insertAfterId);
|
||||
|
||||
// try and increment the cue
|
||||
if (isOntimeEvent(previousEvent)) {
|
||||
@@ -84,7 +85,7 @@ export function getCueCandidate(rundown: OntimeRundown, insertAfterId?: string):
|
||||
if (previousEvent === null) {
|
||||
cue = '0.1';
|
||||
} else {
|
||||
cue = previousEvent.cue + '.1';
|
||||
cue = `${previousEvent.cue}.1`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { millisToHours, millisToMinutes, millisToSeconds } from "./conversionUtils";
|
||||
|
||||
describe('millisToSecond()', () => {
|
||||
test('null values', () => {
|
||||
const t = { val: null, result: 0 };
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('valid millis', () => {
|
||||
const t = { val: 3600000, result: 3600 };
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('negative millis', () => {
|
||||
const t = { val: -3600000, result: -3600 };
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('0', () => {
|
||||
const t = { val: 0, result: 0 };
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('-0', () => {
|
||||
const t = { val: -0, result: 0 };
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('86401000 (24 hours and 1 second)', () => {
|
||||
const t = { val: 86401000, result: 86401 };
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('-86401000 (-24 hours and 1 second)', () => {
|
||||
const t = { val: -86401000, result: -86401 };
|
||||
expect(millisToSeconds(t.val)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('millisToMinutes()', () => {
|
||||
test('null values', () => {
|
||||
const t = { val: null, result: 0 };
|
||||
expect(millisToMinutes(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('valid millis', () => {
|
||||
const t = { val: 3600000, result: 60 };
|
||||
expect(millisToMinutes(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('negative millis', () => {
|
||||
const t = { val: -3600000, result: -60 };
|
||||
expect(millisToMinutes(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('0', () => {
|
||||
const t = { val: 0, result: 0 };
|
||||
expect(millisToMinutes(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('-0', () => {
|
||||
const t = { val: -0, result: 0 };
|
||||
expect(millisToMinutes(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('86401000 (24 hours and 1 second)', () => {
|
||||
const t = { val: 86401000, result: 1440 };
|
||||
expect(millisToMinutes(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('-86401000 (-24 hours and 1 second)', () => {
|
||||
// negative numbers are rounded up
|
||||
const t = { val: -86401000, result: -1441 };
|
||||
expect(millisToMinutes(t.val)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('millisToHours()', () => {
|
||||
test('null values', () => {
|
||||
const t = { val: null, result: 0 };
|
||||
expect(millisToHours(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('valid millis', () => {
|
||||
const t = { val: 3600000, result: 1 };
|
||||
expect(millisToHours(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('negative millis', () => {
|
||||
const t = { val: -3600000, result: -1 };
|
||||
expect(millisToHours(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('0', () => {
|
||||
const t = { val: 0, result: 0 };
|
||||
expect(millisToHours(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('-0', () => {
|
||||
const t = { val: -0, result: 0 };
|
||||
expect(millisToHours(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('86401000 (24 hours and 1 second)', () => {
|
||||
const t = { val: 86401000, result: 24 };
|
||||
expect(millisToHours(t.val)).toBe(t.result);
|
||||
});
|
||||
|
||||
test('-86401000 (-24 hours and 1 second)', () => {
|
||||
// negative numbers are rounded up
|
||||
const t = { val: -86401000, result: -25 };
|
||||
expect(millisToHours(t.val)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
type MaybeNumber = number | null;
|
||||
|
||||
export const MILLIS_PER_SECOND = 1000;
|
||||
export const MILLIS_PER_MINUTE = 1000 * 60;
|
||||
export const MILLIS_PER_HOUR = 1000 * 60 * 60;
|
||||
|
||||
function convertMillis(millis: MaybeNumber, conversion: number) {
|
||||
if (millis == null || millis === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// for negative times, we want to round up
|
||||
if (millis < 0) {
|
||||
Math.ceil(millis / conversion);
|
||||
}
|
||||
return Math.floor(millis / conversion);
|
||||
}
|
||||
|
||||
export function millisToSeconds(millis: MaybeNumber) {
|
||||
return convertMillis(millis, MILLIS_PER_SECOND);
|
||||
}
|
||||
|
||||
export function millisToMinutes(millis: MaybeNumber) {
|
||||
return convertMillis(millis, MILLIS_PER_MINUTE);
|
||||
}
|
||||
|
||||
export function millisToHours(millis: MaybeNumber) {
|
||||
return convertMillis(millis, MILLIS_PER_HOUR);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { formatDisplay } from './formatDisplay';
|
||||
|
||||
describe('test string from formatDisplay function', () => {
|
||||
it('test with null values', () => {
|
||||
const t = { val: null, result: '00:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with not numbers', () => {
|
||||
const t = { val: 'test', result: '00:00:00' };
|
||||
// @ts-expect-error -- indulge me for the test
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with valid millis', () => {
|
||||
const t = { val: 3600000, result: '01:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with negative millis', () => {
|
||||
const t = { val: -3600000, result: '01:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 0', () => {
|
||||
const t = { val: 0, result: '00:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -0', () => {
|
||||
const t = { val: -0, result: '00:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86400 (24 hours)', () => {
|
||||
const t = { val: 86400000, result: '00:00:00' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86401 (24 hours and 1 second)', () => {
|
||||
const t = { val: 86401000, result: '00:00:01' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -86401 (-24 hours and 1 second)', () => {
|
||||
const t = { val: -86401000, result: '00:00:01' };
|
||||
expect(formatDisplay(t.val, false)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test string from formatDisplay function with hidezero', () => {
|
||||
it('test with null values', () => {
|
||||
const t = { val: null, result: '00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with valid millis', () => {
|
||||
const t = { val: 3600000, result: '01:00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with negative millis', () => {
|
||||
const t = { val: -3600000, result: '01:00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 0', () => {
|
||||
const t = { val: 0, result: '00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -0', () => {
|
||||
const t = { val: -0, result: '00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86400 (24 hours)', () => {
|
||||
const t = { val: 86400000, result: '00:00' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with 86401 (24 hours and 1 second)', () => {
|
||||
const t = { val: 86401000, result: '00:01' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
|
||||
it('test with -86401 (-24 hours and 1 second)', () => {
|
||||
const t = { val: -86401000, result: '00:01' };
|
||||
expect(formatDisplay(t.val, true)).toBe(t.result);
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import { mts } from '../timeConstants.js';
|
||||
|
||||
/**
|
||||
* another go at simpler string formatting (counters) -- Copied from client code
|
||||
* @description Converts seconds to string representing time
|
||||
* @param {number | null} milliseconds - time in seconds
|
||||
* @param {boolean} [hideZero] - whether to show hours in case its 00
|
||||
* @returns {string} String representing absolute time 00:12:02
|
||||
*/
|
||||
export function formatDisplay(milliseconds: number | null, hideZero = false): string {
|
||||
if (typeof milliseconds !== 'number') {
|
||||
return hideZero ? '00:00' : '00:00:00';
|
||||
}
|
||||
|
||||
// add an extra 0 if necessary
|
||||
const format = (val: number) => `0${Math.floor(val)}`.slice(-2);
|
||||
|
||||
const s = Math.abs(millisToSeconds(milliseconds));
|
||||
const hours = Math.floor((s / 3600) % 24);
|
||||
const minutes = Math.floor((s % 3600) / 60);
|
||||
|
||||
if (hideZero && hours < 1) return [minutes, s % 60].map(format).join(':');
|
||||
return [hours, minutes, s % 60].map(format).join(':');
|
||||
}
|
||||
|
||||
export const millisToSeconds = (millis: number | null): number => {
|
||||
if (millis === null) {
|
||||
return 0;
|
||||
}
|
||||
return millis < 0 ? Math.ceil(millis / mts) : Math.floor(millis / mts);
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
/**
|
||||
* @description utility function to format a date in milliseconds using luxon
|
||||
* @param {number} millis
|
||||
* @param {string} format
|
||||
* @return {string}
|
||||
*/
|
||||
export function formatFromMillis(millis: number, format: string) {
|
||||
return DateTime.fromMillis(millis).toUTC().toFormat(format);
|
||||
}
|
||||
@@ -24,3 +24,12 @@ describe('test isTimeString() function handle different separators', () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('test isTimeString() function handle AM/PM', () => {
|
||||
const ts = ['2:10AM', '2:10PM', '2:10'];
|
||||
for (const s of ts) {
|
||||
it(`it handles ${s}`, () => {
|
||||
expect(isTimeString(s)).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,16 +4,6 @@
|
||||
* @returns {boolean} string represents time
|
||||
*/
|
||||
export const isTimeString = (text: string): boolean => {
|
||||
// ^ # Start of string
|
||||
// (?: # Try to match...
|
||||
// (?: # Try to match...
|
||||
// ([01]?\d|2[0-3]): # HH:
|
||||
// )? # (optionally).
|
||||
// ([0-5]?\d): # MM: (required)
|
||||
// )? # (entire group optional, so either HH:MM:, MM: or nothing)
|
||||
// ([0-5]?\d) # SS (required)
|
||||
// $ # End of string
|
||||
|
||||
const regex = /^(?:(?:([01]?\d|2[0-3])[:,.])?([0-5]?\d)[:,.])?([0-5]?\d)$/;
|
||||
const regex = /^(?:(?:([01]?\d|2[0-3])[:,.])?([0-5]?\d)[:,.])?([0-5]?\d)?(\s)?([APap][Mm])?$/;
|
||||
return regex.test(text);
|
||||
};
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
/**
|
||||
* @description Converts milliseconds to string representing time
|
||||
* @param {number | null} millis - time in milliseconds
|
||||
* @param {boolean} showSeconds - weather to show the seconds
|
||||
* @param {string} fallback - what to return if value is null
|
||||
* @returns {string} String representing time 00:12:02
|
||||
*/
|
||||
export function millisToString(millis: number | null, showSeconds = true, fallback = '...') {
|
||||
if (millis == null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const isNegative = millis < 0;
|
||||
|
||||
const format = `HH:mm${showSeconds ? ':ss' : ''}`;
|
||||
return `${isNegative ? '-' : ''}${DateTime.fromMillis(Math.abs(millis)).toUTC().toFormat(format)}`;
|
||||
}
|
||||
+17
-25
@@ -1,11 +1,11 @@
|
||||
import { expect } from 'vitest';
|
||||
|
||||
import { millisToString } from './millisToString';
|
||||
import { dayInMs } from '../timeConstants';
|
||||
import { MILLIS_PER_HOUR } from './conversionUtils';
|
||||
import { millisToString, removeLeadingZero } from './timeFormatting';
|
||||
|
||||
describe('millisToString()', () => {
|
||||
it('returns fallback if millis is null', () => {
|
||||
const fallback = 'testFallback';
|
||||
expect(millisToString(null, true, fallback)).toBe(fallback);
|
||||
expect(millisToString(null, { fallback })).toBe(fallback);
|
||||
});
|
||||
|
||||
it('returns 00:00:00 if 0 is passed', () => {
|
||||
@@ -22,8 +22,8 @@ describe('millisToString()', () => {
|
||||
{ millis: -3600000, expected: '-01:00:00' },
|
||||
{ millis: -36000000, expected: '-10:00:00' },
|
||||
{ millis: -86399000, expected: '-23:59:59' },
|
||||
{ millis: -86400000, expected: '-00:00:00' },
|
||||
{ millis: -86401000, expected: '-00:00:01' },
|
||||
{ millis: -86400000, expected: '-24:00:00' },
|
||||
{ millis: -86401000, expected: '-24:00:01' },
|
||||
];
|
||||
|
||||
testScenarios.forEach((scenario) => {
|
||||
@@ -31,6 +31,10 @@ describe('millisToString()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles times over 24 hours', () => {
|
||||
expect(millisToString(dayInMs + MILLIS_PER_HOUR)).toBe('25:00:00');
|
||||
});
|
||||
|
||||
test('random properties', () => {
|
||||
const testScenarios = [
|
||||
{ millis: 300, expected: '00:00:00' },
|
||||
@@ -41,31 +45,19 @@ describe('millisToString()', () => {
|
||||
{ millis: 3600000, expected: '01:00:00' },
|
||||
{ millis: 36000000, expected: '10:00:00' },
|
||||
{ millis: 86399000, expected: '23:59:59' },
|
||||
{ millis: 86400000, expected: '00:00:00' },
|
||||
{ millis: 86401000, expected: '00:00:01' },
|
||||
{ millis: 86400000, expected: '24:00:00' },
|
||||
{ millis: 86401000, expected: '24:00:01' },
|
||||
];
|
||||
|
||||
testScenarios.forEach((scenario) => {
|
||||
expect(millisToString(scenario.millis)).toBe(scenario.expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('random properties without seconds', () => {
|
||||
const testScenarios = [
|
||||
{ millis: 300, expected: '00:00' },
|
||||
{ millis: 1000, expected: '00:00' },
|
||||
{ millis: 1500, expected: '00:00' },
|
||||
{ millis: 60000, expected: '00:01' },
|
||||
{ millis: 600000, expected: '00:10' },
|
||||
{ millis: 3600000, expected: '01:00' },
|
||||
{ millis: 36000000, expected: '10:00' },
|
||||
{ millis: 86399000, expected: '23:59' },
|
||||
{ millis: 86400000, expected: '00:00' },
|
||||
{ millis: 86401000, expected: '00:00' },
|
||||
];
|
||||
|
||||
testScenarios.forEach((scenario) => {
|
||||
expect(millisToString(scenario.millis, false)).toBe(scenario.expected);
|
||||
});
|
||||
describe('removeLeadingZero()', () => {
|
||||
test('removes leading zero from timer', () => {
|
||||
expect(removeLeadingZero('00:00:00')).toBe('0:00');
|
||||
expect(removeLeadingZero('-00:08:47')).toBe('-8:47');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import type { MaybeNumber } from 'ontime-types';
|
||||
|
||||
import { millisToHours, millisToMinutes, millisToSeconds } from './conversionUtils.js';
|
||||
|
||||
function pad(val: number): string {
|
||||
return String(val).padStart(2, '0');
|
||||
}
|
||||
|
||||
type FormatOptions = {
|
||||
fallback?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a value in milliseconds to its time tag
|
||||
* @param millis time to convert
|
||||
* @param options optional overloads for format
|
||||
* @returns formatted time such as 12:00:00
|
||||
*/
|
||||
export function millisToString(millis?: MaybeNumber, options?: FormatOptions): string {
|
||||
if (millis == null) {
|
||||
return options?.fallback ?? '...';
|
||||
}
|
||||
|
||||
const absoluteMillis = Math.abs(millis);
|
||||
const seconds = millisToSeconds(absoluteMillis) % 60;
|
||||
const minutes = millisToMinutes(absoluteMillis) % 60;
|
||||
const hours = millisToHours(absoluteMillis);
|
||||
const isNegative = millis < 0;
|
||||
|
||||
return `${isNegative ? '-' : ''}${[hours, minutes, seconds].map(pad).join(':')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a string such as 00:10:10 and removes the hours field if it is 00
|
||||
* @param timer
|
||||
*/
|
||||
export function removeLeadingZero(timer: string): string {
|
||||
if (timer.startsWith('00:0')) {
|
||||
return timer.slice(4);
|
||||
}
|
||||
if (timer.startsWith('00:')) {
|
||||
return timer.slice(3);
|
||||
}
|
||||
if (timer.startsWith('-00:0')) {
|
||||
return `-${timer.slice(5)}`;
|
||||
}
|
||||
if (timer.startsWith('-00:')) {
|
||||
return `-${timer.slice(4)}`;
|
||||
}
|
||||
return timer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a string such as 00:10:10 and removes the seconds field if it is 00
|
||||
* @param timer
|
||||
*/
|
||||
export function removeTrailingZero(timer: string): string {
|
||||
if (timer.endsWith(':00')) {
|
||||
return timer.slice(0, -3);
|
||||
}
|
||||
return timer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a string such as 00:10:10 and removes the seconds field
|
||||
* @param timer
|
||||
*/
|
||||
export function removeSeconds(timer: string): string {
|
||||
return timer.slice(0, -3);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description utility function to format a date in milliseconds using luxon
|
||||
* @param {number} millis
|
||||
* @param {string} format
|
||||
* @return {string}
|
||||
*/
|
||||
export function formatFromMillis(millis: number, format: string): string {
|
||||
return DateTime.fromMillis(millis).toUTC().toFormat(format);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
export type ExcelImportOptions = keyof typeof defaultExcelImportMap;
|
||||
export type ExcelImportMap = typeof defaultExcelImportMap;
|
||||
|
||||
export const defaultExcelImportMap = {
|
||||
worksheet: 'event schedule',
|
||||
projectName: 'project name',
|
||||
projectDescription: 'project description',
|
||||
publicUrl: 'public url',
|
||||
publicInfo: 'public info',
|
||||
backstageUrl: 'backstage url',
|
||||
backstageInfo: 'backstage info',
|
||||
timeStart: 'time start',
|
||||
timeEnd: 'time end',
|
||||
duration: 'duration',
|
||||
cue: 'cue',
|
||||
title: 'title',
|
||||
presenter: 'presenter',
|
||||
subtitle: 'subtitle',
|
||||
isPublic: 'public',
|
||||
skip: 'skip',
|
||||
note: 'notes',
|
||||
colour: 'colour',
|
||||
endAction: 'end action',
|
||||
timerType: 'timer type',
|
||||
user0: 'user0',
|
||||
user1: 'user1',
|
||||
user2: 'user2',
|
||||
user3: 'user3',
|
||||
user4: 'user4',
|
||||
user5: 'user5',
|
||||
user6: 'user6',
|
||||
user7: 'user7',
|
||||
user8: 'user8',
|
||||
user9: 'user9',
|
||||
};
|
||||
|
||||
export function isExcelImportMap(obj: unknown): obj is ExcelImportMap {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keys = Object.keys(obj);
|
||||
return keys.every((key) => Object.hasOwn(defaultExcelImportMap, key));
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { isImportMap } from '../spreadsheetImport';
|
||||
|
||||
describe('isImportMap()', () => {
|
||||
it('validates a v3 default import map', () => {
|
||||
const v3ImportMap = {
|
||||
worksheet: 'event schedule',
|
||||
timeStart: 'time start',
|
||||
timeEnd: 'time end',
|
||||
duration: 'duration',
|
||||
cue: 'cue',
|
||||
title: 'title',
|
||||
isPublic: 'public',
|
||||
skip: 'skip',
|
||||
note: 'notes',
|
||||
colour: 'colour',
|
||||
endAction: 'end action',
|
||||
timerType: 'timer type',
|
||||
timeWarning: 'warning time',
|
||||
timeDanger: 'danger time',
|
||||
custom: {},
|
||||
};
|
||||
|
||||
expect(isImportMap(v3ImportMap)).toBe(true);
|
||||
});
|
||||
|
||||
it('handles custom properties', () => {
|
||||
const v3ImportMap = {
|
||||
worksheet: 'event schedule',
|
||||
timeStart: 'time start',
|
||||
timeEnd: 'time end',
|
||||
duration: 'duration',
|
||||
cue: 'cue',
|
||||
title: 'title',
|
||||
isPublic: 'public',
|
||||
skip: 'skip',
|
||||
note: 'notes',
|
||||
colour: 'colour',
|
||||
endAction: 'end action',
|
||||
timerType: 'timer type',
|
||||
timeWarning: 'warning time',
|
||||
timeDanger: 'danger time',
|
||||
custom: {
|
||||
userDefined: 'userDefined',
|
||||
anotherOne: 'anotherOne',
|
||||
},
|
||||
};
|
||||
|
||||
expect(isImportMap(v3ImportMap)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
export type ImportOptions = keyof typeof defaultImportMap | 'custom';
|
||||
export type ImportCustom = Record<string, string>;
|
||||
export type ImportMap = typeof defaultImportMap & { custom: ImportCustom };
|
||||
|
||||
// Record of ontime name and import name
|
||||
export const defaultImportMap = {
|
||||
worksheet: 'event schedule',
|
||||
timeStart: 'time start',
|
||||
timeEnd: 'time end',
|
||||
duration: 'duration',
|
||||
cue: 'cue',
|
||||
title: 'title',
|
||||
isPublic: 'public',
|
||||
skip: 'skip',
|
||||
note: 'notes',
|
||||
colour: 'colour',
|
||||
endAction: 'end action',
|
||||
timerType: 'timer type',
|
||||
timeWarning: 'warning time',
|
||||
timeDanger: 'danger time',
|
||||
custom: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates whether an object is an Import Map
|
||||
* @param obj
|
||||
*/
|
||||
export function isImportMap(obj: unknown): obj is ImportMap {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keys = Object.keys(defaultImportMap);
|
||||
return keys.every((key) => Object.hasOwn(obj, key));
|
||||
}
|
||||
@@ -1,28 +1,6 @@
|
||||
import { generateId } from './generateId.js';
|
||||
|
||||
test('generate a valid 5 digit id', () => {
|
||||
test('generate a valid 6 digit id', () => {
|
||||
const id = generateId();
|
||||
expect(id.length).toBe(5);
|
||||
});
|
||||
|
||||
test('generate 100 with less than 110 attempts', () => {
|
||||
const ids = new Set<string>();
|
||||
let attempts = 1;
|
||||
while (ids.size < 100) {
|
||||
ids.add(generateId());
|
||||
attempts++;
|
||||
}
|
||||
|
||||
expect(attempts).toBeLessThan(105);
|
||||
});
|
||||
|
||||
test('generate 1000 with less than 1020 attempts', () => {
|
||||
const ids = new Set<string>();
|
||||
let attempts = 1;
|
||||
while (ids.size < 1000) {
|
||||
ids.add(generateId());
|
||||
attempts++;
|
||||
}
|
||||
|
||||
expect(attempts).toBeLessThan(1020);
|
||||
expect(id.length).toBe(6);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
const nanoid = customAlphabet('1234567890abcdef', 5);
|
||||
const nanoid = customAlphabet('1234567890abcdef', 6);
|
||||
|
||||
/**
|
||||
* Generates a random id from the defined alphabet
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { obfuscate, unobfuscate } from '../generic.js';
|
||||
|
||||
describe('obfuscate and unobfuscate', () => {
|
||||
it('should return the obfuscated string', () => {
|
||||
const str = 'abc123';
|
||||
const obfuscated = obfuscate(str);
|
||||
expect(obfuscated).not.toBe(str);
|
||||
expect(obfuscated.startsWith('_')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return the original string after obfuscating and unobfuscating', () => {
|
||||
const str = 'abc123';
|
||||
const obfuscated = obfuscate(str);
|
||||
const unobfuscated = unobfuscate(obfuscated);
|
||||
expect(unobfuscated).toBe(str);
|
||||
expect(unobfuscated.startsWith('_')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obfuscate a string
|
||||
* Uses a variation of ROT13 that handles numeric values
|
||||
* @param str
|
||||
* @returns
|
||||
*/
|
||||
export function obfuscate(str: string): string {
|
||||
const obfuscated = str.replace(/[a-zA-Z0-9]/g, (c) => {
|
||||
if (/[a-zA-Z]/.test(c)) {
|
||||
// @ts-expect-error -- we use some javascript magic here
|
||||
return String.fromCharCode((c <= 'Z' ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);
|
||||
} else {
|
||||
// @ts-expect-error -- we use some javascript magic here
|
||||
|
||||
return String.fromCharCode((c <= '4' ? 57 : 48) >= (c = c.charCodeAt(0) + 5) ? c : c - 10);
|
||||
}
|
||||
});
|
||||
if (str.startsWith('_')) {
|
||||
return obfuscated.replace('_', '');
|
||||
}
|
||||
return `_${obfuscated}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unobfoscate a string
|
||||
* Uses a variation of ROT13 that handles numeric values
|
||||
* @param str
|
||||
* @returns
|
||||
*/
|
||||
export function unobfuscate(str: string): string {
|
||||
if (str.startsWith('_')) {
|
||||
return obfuscate(str);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { isAlphanumeric } from './isAlphanumeric';
|
||||
|
||||
describe('test isAlphanumeric() function', () => {
|
||||
it('it OK strings', () => {
|
||||
const ts = ['abcdefghijklmnopqrstuvwxyz', '0123456798', '123asd'];
|
||||
for (const s of ts) {
|
||||
expect(isAlphanumeric(s)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('it bad strings', () => {
|
||||
const ts = ['!abcd1234', 'åøæ', '*'];
|
||||
for (const s of ts) {
|
||||
expect(isAlphanumeric(s)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @description Validates a alphanumeric string
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isAlphanumeric = (text: string): boolean => {
|
||||
const regex = /^[a-z0-9]+$/i;
|
||||
return regex.test(text);
|
||||
};
|
||||
@@ -1,6 +1,44 @@
|
||||
import { OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||
import type { OntimeEvent, OntimeRundown } from 'ontime-types';
|
||||
import { SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { getNextEvent, getPreviousEvent } from './rundownUtils';
|
||||
import { getLastEvent, getNext, getNextEvent, getPrevious, getPreviousEvent, swapEventData } from './rundownUtils';
|
||||
|
||||
describe('getNext()', () => {
|
||||
it('returns the next event of type event', () => {
|
||||
const testRundown = [
|
||||
{ id: '1', type: SupportedEvent.Event },
|
||||
{ id: '2', type: SupportedEvent.Event },
|
||||
{ id: '3', type: SupportedEvent.Event },
|
||||
];
|
||||
|
||||
const { nextEvent, nextIndex } = getNext(testRundown as OntimeRundown, '1');
|
||||
expect(nextEvent?.id).toBe('2');
|
||||
expect(nextIndex).toBe(1);
|
||||
});
|
||||
it('alows other event types', () => {
|
||||
const testRundown = [
|
||||
{ id: '1', type: SupportedEvent.Event },
|
||||
{ id: '2', type: SupportedEvent.Delay },
|
||||
{ id: '3', type: SupportedEvent.Block },
|
||||
{ id: '4', type: SupportedEvent.Event },
|
||||
];
|
||||
|
||||
const { nextEvent, nextIndex } = getNext(testRundown as OntimeRundown, '1');
|
||||
expect(nextEvent?.id).toBe('2');
|
||||
expect(nextIndex).toBe(1);
|
||||
});
|
||||
it('returns null if none found', () => {
|
||||
const testRundown = [
|
||||
{ id: '1', type: SupportedEvent.Event },
|
||||
{ id: '2', type: SupportedEvent.Delay },
|
||||
{ id: '3', type: SupportedEvent.Block },
|
||||
];
|
||||
|
||||
const { nextEvent, nextIndex } = getNext(testRundown as OntimeRundown, '3');
|
||||
expect(nextEvent).toBe(null);
|
||||
expect(nextIndex).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextEvent()', () => {
|
||||
it('returns the next event of type event', () => {
|
||||
@@ -10,8 +48,9 @@ describe('getNextEvent()', () => {
|
||||
{ id: '3', type: SupportedEvent.Event },
|
||||
];
|
||||
|
||||
const next = getNextEvent(testRundown as OntimeRundown, '1');
|
||||
expect(next?.id).toBe('2');
|
||||
const { nextEvent, nextIndex } = getNextEvent(testRundown as OntimeRundown, '1');
|
||||
expect(nextEvent?.id).toBe('2');
|
||||
expect(nextIndex).toBe(1);
|
||||
});
|
||||
it('ignores other event types', () => {
|
||||
const testRundown = [
|
||||
@@ -21,8 +60,9 @@ describe('getNextEvent()', () => {
|
||||
{ id: '4', type: SupportedEvent.Event },
|
||||
];
|
||||
|
||||
const next = getNextEvent(testRundown as OntimeRundown, '1');
|
||||
expect(next?.id).toBe('4');
|
||||
const { nextEvent, nextIndex } = getNextEvent(testRundown as OntimeRundown, '1');
|
||||
expect(nextEvent?.id).toBe('4');
|
||||
expect(nextIndex).toBe(3);
|
||||
});
|
||||
it('returns null if none found', () => {
|
||||
const testRundown = [
|
||||
@@ -31,8 +71,46 @@ describe('getNextEvent()', () => {
|
||||
{ id: '3', type: SupportedEvent.Block },
|
||||
];
|
||||
|
||||
const next = getNextEvent(testRundown as OntimeRundown, '1');
|
||||
expect(next).toBe(null);
|
||||
const { nextEvent, nextIndex } = getNextEvent(testRundown as OntimeRundown, '1');
|
||||
expect(nextEvent).toBe(null);
|
||||
expect(nextIndex).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPrevious()', () => {
|
||||
it('returns the previous event of type event', () => {
|
||||
const testRundown = [
|
||||
{ id: '1', type: SupportedEvent.Event },
|
||||
{ id: '2', type: SupportedEvent.Event },
|
||||
{ id: '3', type: SupportedEvent.Event },
|
||||
];
|
||||
|
||||
const { previousEvent, previousIndex } = getPrevious(testRundown as OntimeRundown, '3');
|
||||
expect(previousEvent?.id).toBe('2');
|
||||
expect(previousIndex).toBe(1);
|
||||
});
|
||||
it('allow other event types', () => {
|
||||
const testRundown = [
|
||||
{ id: '1', type: SupportedEvent.Event },
|
||||
{ id: '2', type: SupportedEvent.Delay },
|
||||
{ id: '3', type: SupportedEvent.Block },
|
||||
{ id: '4', type: SupportedEvent.Event },
|
||||
];
|
||||
|
||||
const { previousEvent, previousIndex } = getPrevious(testRundown as OntimeRundown, '3');
|
||||
expect(previousEvent?.id).toBe('2');
|
||||
expect(previousIndex).toBe(1);
|
||||
});
|
||||
it('returns null if none found', () => {
|
||||
const testRundown = [
|
||||
{ id: '2', type: SupportedEvent.Delay },
|
||||
{ id: '3', type: SupportedEvent.Block },
|
||||
{ id: '4', type: SupportedEvent.Event },
|
||||
];
|
||||
|
||||
const { previousEvent, previousIndex } = getPrevious(testRundown as OntimeRundown, '2');
|
||||
expect(previousEvent).toBe(null);
|
||||
expect(previousIndex).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,8 +122,9 @@ describe('getPreviousEvent()', () => {
|
||||
{ id: '3', type: SupportedEvent.Event },
|
||||
];
|
||||
|
||||
const previous = getPreviousEvent(testRundown as OntimeRundown, '3');
|
||||
expect(previous?.id).toBe('2');
|
||||
const { previousEvent, previousIndex } = getPreviousEvent(testRundown as OntimeRundown, '3');
|
||||
expect(previousEvent?.id).toBe('2');
|
||||
expect(previousIndex).toBe(1);
|
||||
});
|
||||
it('ignores other event types', () => {
|
||||
const testRundown = [
|
||||
@@ -55,8 +134,9 @@ describe('getPreviousEvent()', () => {
|
||||
{ id: '4', type: SupportedEvent.Event },
|
||||
];
|
||||
|
||||
const previous = getPreviousEvent(testRundown as OntimeRundown, '4');
|
||||
expect(previous?.id).toBe('1');
|
||||
const { previousEvent, previousIndex } = getPreviousEvent(testRundown as OntimeRundown, '4');
|
||||
expect(previousEvent?.id).toBe('1');
|
||||
expect(previousIndex).toBe(0);
|
||||
});
|
||||
it('returns null if none found', () => {
|
||||
const testRundown = [
|
||||
@@ -65,7 +145,68 @@ describe('getPreviousEvent()', () => {
|
||||
{ id: '4', type: SupportedEvent.Event },
|
||||
];
|
||||
|
||||
const previous = getNextEvent(testRundown as OntimeRundown, '1');
|
||||
expect(previous).toBe(null);
|
||||
const { previousEvent, previousIndex } = getPreviousEvent(testRundown as OntimeRundown, '2');
|
||||
expect(previousEvent).toBe(null);
|
||||
expect(previousIndex).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('swapEventData', () => {
|
||||
it('swaps some data between two events', () => {
|
||||
const eventA = {
|
||||
id: '1',
|
||||
cue: 'A',
|
||||
timeStart: 1,
|
||||
timeEnd: 1,
|
||||
duration: 1,
|
||||
delay: 1,
|
||||
} as OntimeEvent;
|
||||
const eventB = {
|
||||
id: '2',
|
||||
cue: 'B',
|
||||
timeStart: 2,
|
||||
timeEnd: 2,
|
||||
duration: 2,
|
||||
delay: 2,
|
||||
} as OntimeEvent;
|
||||
|
||||
const { newA, newB } = swapEventData(eventA, eventB);
|
||||
|
||||
expect(newA).toMatchObject({
|
||||
id: '1',
|
||||
cue: 'B',
|
||||
timeStart: 1,
|
||||
timeEnd: 1,
|
||||
duration: 1,
|
||||
delay: 1,
|
||||
});
|
||||
expect(newB).toMatchObject({
|
||||
id: '2',
|
||||
cue: 'A',
|
||||
timeStart: 2,
|
||||
timeEnd: 2,
|
||||
duration: 2,
|
||||
delay: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLastEvent', () => {
|
||||
it('returns the last event of type event', () => {
|
||||
const testRundown = [
|
||||
{ id: '1', type: SupportedEvent.Event },
|
||||
{ id: '2', type: SupportedEvent.Delay },
|
||||
{ id: '3', type: SupportedEvent.Event },
|
||||
{ id: '4', type: SupportedEvent.Block },
|
||||
];
|
||||
|
||||
const { lastEvent } = getLastEvent(testRundown as OntimeRundown);
|
||||
expect(lastEvent?.id).toBe('3');
|
||||
});
|
||||
it('handles rundowns with a single event', () => {
|
||||
const testRundown = [{ id: '1', type: SupportedEvent.Event }];
|
||||
|
||||
const { lastEvent } = getLastEvent(testRundown as OntimeRundown);
|
||||
expect(lastEvent?.id).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isOntimeEvent, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||
import type { NormalisedRundown, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||
import { isOntimeEvent } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* Gets first event in rundown, if it exists
|
||||
@@ -10,47 +11,146 @@ export function getFirst(rundown: OntimeRundownEntry[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets first scheduled event in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
* @return {OntimeEvent | null}
|
||||
* Gets first event in a normalised rundown, if it exists
|
||||
* @param rundown
|
||||
* @param order
|
||||
* @returns
|
||||
*/
|
||||
export function getFirstEvent(rundown: OntimeRundownEntry[]) {
|
||||
for (let i = 0; i < rundown.length; i++) {
|
||||
const event = rundown[i];
|
||||
if (isOntimeEvent(event)) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getLastEvent(rundown: OntimeRundown): OntimeEvent | null {
|
||||
if (rundown.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = rundown.length - 1; i > 0; i--) {
|
||||
const event = rundown.at(i);
|
||||
if (isOntimeEvent(event)) {
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
export function getFirstNormal(rundown: NormalisedRundown, order: string[]) {
|
||||
const firstId = order[0];
|
||||
return rundown[firstId] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets next event in rundown, if it exists
|
||||
* Gets first scheduled event in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
* @return {{ firstEvent: OntimeEvent | null; firstIndex: number | null } }
|
||||
*/
|
||||
export function getFirstEvent(rundown: OntimeRundownEntry[]): {
|
||||
firstEvent: OntimeEvent | null;
|
||||
firstIndex: number | null;
|
||||
} {
|
||||
for (let i = 0; i < rundown.length; i++) {
|
||||
const firstEvent = rundown[i];
|
||||
if (isOntimeEvent(firstEvent) && !firstEvent.skip) {
|
||||
return { firstEvent, firstIndex: i };
|
||||
}
|
||||
}
|
||||
return { firstEvent: null, firstIndex: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets first scheduled event in a normalised rundown, if it exists
|
||||
* @param rundown
|
||||
* @param order
|
||||
* @returns
|
||||
*/
|
||||
export function getFirstEventNormal(
|
||||
rundown: NormalisedRundown,
|
||||
order: string[],
|
||||
): {
|
||||
firstEvent: OntimeEvent | null;
|
||||
firstIndex: number | null;
|
||||
} {
|
||||
for (let i = 0; i < order.length; i++) {
|
||||
const firstId = order[i];
|
||||
const firstEvent = rundown[firstId];
|
||||
if (isOntimeEvent(firstEvent) && !firstEvent.skip) {
|
||||
return { firstEvent, firstIndex: i };
|
||||
}
|
||||
}
|
||||
return { firstEvent: null, firstIndex: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets last scheduled event in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
* @return {{ firstEvent: OntimeEvent | null; firstIndex: number | null } }
|
||||
*/
|
||||
export function getLastEvent(rundown: OntimeRundown): {
|
||||
lastEvent: OntimeEvent | null;
|
||||
lastIndex: number | null;
|
||||
} {
|
||||
if (rundown.length < 1) {
|
||||
return { lastEvent: null, lastIndex: null };
|
||||
}
|
||||
|
||||
for (let i = rundown.length - 1; i >= 0; i--) {
|
||||
const lastEvent = rundown.at(i);
|
||||
if (isOntimeEvent(lastEvent) && !lastEvent.skip) {
|
||||
return { lastEvent, lastIndex: i };
|
||||
}
|
||||
}
|
||||
return { lastEvent: null, lastIndex: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets last scheduled event in a normalised rundown, if it exists
|
||||
* @param rundown
|
||||
* @param order
|
||||
* @return {{ firstEvent: OntimeEvent | null; firstIndex: number | null } }
|
||||
*/
|
||||
export function getLastEventNormal(
|
||||
rundown: NormalisedRundown,
|
||||
order: string[],
|
||||
): {
|
||||
lastEvent: OntimeEvent | null;
|
||||
lastIndex: number | null;
|
||||
} {
|
||||
if (order.length < 1) {
|
||||
return { lastEvent: null, lastIndex: null };
|
||||
}
|
||||
|
||||
for (let i = order.length - 1; i >= 0; i--) {
|
||||
const lastId = order[i];
|
||||
const lastEvent = rundown[lastId];
|
||||
if (isOntimeEvent(lastEvent) && !lastEvent.skip) {
|
||||
return { lastEvent, lastIndex: i };
|
||||
}
|
||||
}
|
||||
return { lastEvent: null, lastIndex: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets next entry in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
* @param {string} currentId
|
||||
* @return {OntimeRundownEntry | null}
|
||||
* @return {{ nextEvent: OntimeRundownEntry | null; nextIndex: number | null } }
|
||||
*/
|
||||
export function getNext(rundown: OntimeRundownEntry[], currentId: string): OntimeRundownEntry | null {
|
||||
export function getNext(
|
||||
rundown: OntimeRundownEntry[],
|
||||
currentId: string,
|
||||
): { nextEvent: OntimeRundownEntry | null; nextIndex: number | null } {
|
||||
const index = rundown.findIndex((event) => event.id === currentId);
|
||||
if (index !== -1 && index + 1 < rundown.length) {
|
||||
return rundown[index + 1];
|
||||
const nextIndex = index + 1;
|
||||
const nextEvent = rundown[nextIndex];
|
||||
return { nextEvent, nextIndex };
|
||||
} else {
|
||||
return null;
|
||||
return { nextEvent: null, nextIndex: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets next entry in rundown, if it exists
|
||||
* @param rundown
|
||||
* @param order
|
||||
* @param currentId
|
||||
* @returns
|
||||
*/
|
||||
export function getNextNormal(
|
||||
rundown: NormalisedRundown,
|
||||
order: string[],
|
||||
currentId: string,
|
||||
): { nextEvent: OntimeRundownEntry | null; nextIndex: number | null } {
|
||||
const index = order.findIndex((id) => id === currentId);
|
||||
if (index !== -1 && index + 1 < order.length) {
|
||||
const nextIndex = index + 1;
|
||||
const nextId = order[nextIndex];
|
||||
const nextEvent = rundown[nextId];
|
||||
return { nextEvent, nextIndex };
|
||||
} else {
|
||||
return { nextEvent: null, nextIndex: null };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,35 +158,93 @@ export function getNext(rundown: OntimeRundownEntry[], currentId: string): Ontim
|
||||
* Gets next scheduled event in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
* @param {string} currentId
|
||||
* @return {OntimeEvent | null}
|
||||
* @return {{ nextEvent: OntimeEvent | null; nextIndex: number | null } }
|
||||
*/
|
||||
export function getNextEvent(rundown: OntimeRundownEntry[], currentId: string): OntimeEvent | null {
|
||||
export function getNextEvent(
|
||||
rundown: OntimeRundownEntry[],
|
||||
currentId: string,
|
||||
): { nextEvent: OntimeEvent | null; nextIndex: number | null } {
|
||||
const index = rundown.findIndex((event) => event.id === currentId);
|
||||
if (index < 0) {
|
||||
return null;
|
||||
return { nextEvent: null, nextIndex: null };
|
||||
}
|
||||
|
||||
for (let i = index + 1; i < rundown.length; i++) {
|
||||
const event = rundown[i];
|
||||
if (isOntimeEvent(event)) {
|
||||
return event;
|
||||
const nextEvent = rundown[i];
|
||||
if (isOntimeEvent(nextEvent)) {
|
||||
return { nextEvent, nextIndex: i };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return { nextEvent: null, nextIndex: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets previous event in rundown, if it exists
|
||||
* Gets next scheduled event in a normalised rundown, if it exists
|
||||
* @param rundown
|
||||
* @param order
|
||||
* @param {string} currentId
|
||||
* @return {{ nextEvent: OntimeEvent | null; nextIndex: number | null } }
|
||||
*/
|
||||
export function getNextEventNormal(
|
||||
rundown: NormalisedRundown,
|
||||
order: string[],
|
||||
currentId: string,
|
||||
): { nextEvent: OntimeEvent | null; nextIndex: number | null } {
|
||||
const index = order.findIndex((id) => id === currentId);
|
||||
if (index < 0) {
|
||||
return { nextEvent: null, nextIndex: null };
|
||||
}
|
||||
|
||||
for (let i = index + 1; i < order.length; i++) {
|
||||
const nextId = order[i];
|
||||
const nextEvent = rundown[nextId];
|
||||
if (isOntimeEvent(nextEvent)) {
|
||||
return { nextEvent, nextIndex: i };
|
||||
}
|
||||
}
|
||||
return { nextEvent: null, nextIndex: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets previous entry in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
* @param {string} currentId
|
||||
* @return {OntimeRundownEntry | null}
|
||||
* @return {{ previousEvent: OntimeRundownEntry | null; previousIndex: number | null } }
|
||||
*/
|
||||
export function getPrevious(rundown: OntimeRundownEntry[], currentId: string) {
|
||||
export function getPrevious(
|
||||
rundown: OntimeRundownEntry[],
|
||||
currentId: string,
|
||||
): { previousEvent: OntimeRundownEntry | null; previousIndex: number | null } {
|
||||
const index = rundown.findIndex((event) => event.id === currentId);
|
||||
if (index !== -1 && index - 1 >= 0) {
|
||||
return rundown[index - 1];
|
||||
const previousIndex = index - 1;
|
||||
const previousEvent = rundown[previousIndex];
|
||||
return { previousEvent, previousIndex };
|
||||
} else {
|
||||
return null;
|
||||
return { previousEvent: null, previousIndex: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets previous entry in a nornalised rundown, if it exists
|
||||
* @param rundown
|
||||
* @param order
|
||||
* @param {string} currentId
|
||||
* @return {{ previousEvent: OntimeRundownEntry | null; previousIndex: number | null } }
|
||||
*/
|
||||
export function getPreviousNormal(
|
||||
rundown: NormalisedRundown,
|
||||
order: string[],
|
||||
currentId: string,
|
||||
): { previousEvent: OntimeRundownEntry | null; previousIndex: number | null } {
|
||||
const index = order.findIndex((id) => id === currentId);
|
||||
if (index !== -1 && index - 1 >= 0) {
|
||||
const previousIndex = index - 1;
|
||||
const previousId = order[previousIndex];
|
||||
const previousEvent = rundown[previousId];
|
||||
return { previousEvent, previousIndex };
|
||||
} else {
|
||||
return { previousEvent: null, previousIndex: null };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,59 +252,74 @@ export function getPrevious(rundown: OntimeRundownEntry[], currentId: string) {
|
||||
* Gets previous scheduled event in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
* @param {string} currentId
|
||||
* @return {OntimeEvent | null}
|
||||
* @return {{ previousEvent: OntimeRundownEntry | null; previousIndex: number | null } }
|
||||
*/
|
||||
export function getPreviousEvent(rundown: OntimeRundownEntry[], currentId: string): OntimeEvent | null {
|
||||
export function getPreviousEvent(
|
||||
rundown: OntimeRundownEntry[],
|
||||
currentId: string,
|
||||
): { previousEvent: OntimeEvent | null; previousIndex: number | null } {
|
||||
const index = rundown.findIndex((event) => event.id === currentId);
|
||||
if (index < 0) {
|
||||
return null;
|
||||
return { previousEvent: null, previousIndex: null };
|
||||
}
|
||||
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
const event = rundown[i];
|
||||
if (isOntimeEvent(event)) {
|
||||
return event;
|
||||
const previousEvent = rundown[i];
|
||||
if (isOntimeEvent(previousEvent)) {
|
||||
return { previousEvent, previousIndex: i };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return { previousEvent: null, previousIndex: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets previous scheduled event in a normalised rundown, if it exists
|
||||
* @param rundown
|
||||
* @param order
|
||||
* @param {string} currentId
|
||||
* @return {{ previousEvent: OntimeRundownEntry | null; previousIndex: number | null } }
|
||||
*/
|
||||
export function getPreviousEventNormal(
|
||||
rundown: NormalisedRundown,
|
||||
order: string[],
|
||||
currentId: string,
|
||||
): { previousEvent: OntimeEvent | null; previousIndex: number | null } {
|
||||
const index = order.findIndex((id) => id === currentId);
|
||||
if (index < 0) {
|
||||
return { previousEvent: null, previousIndex: null };
|
||||
}
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
const previousId = order[i];
|
||||
const previousEvent = rundown[previousId];
|
||||
if (isOntimeEvent(previousEvent)) {
|
||||
return { previousEvent, previousIndex: i };
|
||||
}
|
||||
}
|
||||
return { previousEvent: null, previousIndex: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* @description swaps two OntimeEvents in the rundown
|
||||
* @param {OntimeRundown} rundown
|
||||
* @param {number} fromEventIndex
|
||||
* @param {number} toEventIndex
|
||||
* @returns {OntimeRundown}
|
||||
* @param {OntimeEvent} eventA
|
||||
* @param {OntimeEvent} eventB
|
||||
*/
|
||||
export const swapOntimeEvents = (
|
||||
rundown: OntimeRundown,
|
||||
fromEventIndex: number,
|
||||
toEventIndex: number,
|
||||
): OntimeRundown => {
|
||||
const updatedRundown = [...rundown];
|
||||
|
||||
if (fromEventIndex < 0 || toEventIndex < 0) {
|
||||
throw new Error('ID not found at index');
|
||||
}
|
||||
|
||||
const fromEvent = updatedRundown.at(fromEventIndex) as OntimeEvent;
|
||||
const toEvent = updatedRundown.at(toEventIndex) as OntimeEvent;
|
||||
|
||||
updatedRundown[fromEventIndex] = {
|
||||
...toEvent,
|
||||
timeStart: fromEvent.timeStart,
|
||||
timeEnd: fromEvent.timeEnd,
|
||||
duration: fromEvent.duration,
|
||||
delay: fromEvent.delay,
|
||||
export const swapEventData = (eventA: OntimeEvent, eventB: OntimeEvent): { newA: OntimeEvent; newB: OntimeEvent } => {
|
||||
const newA = {
|
||||
...eventB,
|
||||
id: eventA.id,
|
||||
timeStart: eventA.timeStart,
|
||||
timeEnd: eventA.timeEnd,
|
||||
duration: eventA.duration,
|
||||
delay: eventA.delay,
|
||||
};
|
||||
|
||||
updatedRundown[toEventIndex] = {
|
||||
...fromEvent,
|
||||
timeStart: toEvent.timeStart,
|
||||
timeEnd: toEvent.timeEnd,
|
||||
duration: toEvent.duration,
|
||||
delay: toEvent.delay,
|
||||
const newB = {
|
||||
...eventA,
|
||||
id: eventB.id,
|
||||
timeStart: eventB.timeStart,
|
||||
timeEnd: eventB.timeEnd,
|
||||
duration: eventB.duration,
|
||||
delay: eventB.delay,
|
||||
};
|
||||
|
||||
return updatedRundown;
|
||||
return { newA, newB };
|
||||
};
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { Playback } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* Simple rules to determine whether a playback action is valid
|
||||
*/
|
||||
export function validatePlayback(currentPlayback: Playback) {
|
||||
return {
|
||||
start: currentPlayback !== Playback.Stop,
|
||||
pause: currentPlayback === Playback.Play || currentPlayback === Playback.Roll,
|
||||
roll: true,
|
||||
pause: currentPlayback === Playback.Play,
|
||||
roll: currentPlayback !== Playback.Roll,
|
||||
stop: currentPlayback !== Playback.Stop,
|
||||
reload: currentPlayback !== Playback.Stop && currentPlayback !== Playback.Roll,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { EndAction, TimerType } from 'ontime-types';
|
||||
import { expect } from 'vitest';
|
||||
|
||||
import { dayInMs } from '../timeConstants.js';
|
||||
import { calculateDuration, validateEndAction, validateTimerType, validateTimes } from './validateEvent.js';
|
||||
import { validateEndAction, validateTimerType } from './validateEvent.js';
|
||||
|
||||
describe('validateEndAction()', () => {
|
||||
it('recognises a string representation of an action', () => {
|
||||
@@ -29,101 +28,3 @@ describe('validateTimerType()', () => {
|
||||
expect(invalidType).toBe(TimerType.CountDown);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateTimes()', () => {
|
||||
it('passes through a well defined time list', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(5, 10, 5);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(10);
|
||||
expect(duration).toBe(5);
|
||||
});
|
||||
|
||||
it('handles cases when no times are given', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(null, undefined, null);
|
||||
expect(timeStart).toBe(0);
|
||||
expect(timeEnd).toBe(0);
|
||||
expect(duration).toBe(0);
|
||||
});
|
||||
|
||||
it('calculates duration', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(5, 10);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(10);
|
||||
expect(duration).toBe(5);
|
||||
});
|
||||
|
||||
it('calculates end time', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(5, undefined, 10);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(15);
|
||||
expect(duration).toBe(10);
|
||||
});
|
||||
|
||||
it('handles events that finish the day after', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(100, 10);
|
||||
expect(timeStart).toBe(100);
|
||||
expect(timeEnd).toBe(10);
|
||||
expect(duration).toBe(dayInMs - 90);
|
||||
});
|
||||
|
||||
it('corrects time in case of conflicts', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(5, 15, 15);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(15);
|
||||
expect(duration).toBe(10);
|
||||
});
|
||||
|
||||
it('calculates start time', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(undefined, 15, 10);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(15);
|
||||
expect(duration).toBe(10);
|
||||
});
|
||||
|
||||
it('calculates start and end time', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(undefined, undefined, 10);
|
||||
expect(timeStart).toBe(0);
|
||||
expect(timeEnd).toBe(10);
|
||||
expect(duration).toBe(10);
|
||||
});
|
||||
|
||||
it('ensures values are integers', () => {
|
||||
const { timeStart, timeEnd, duration } = validateTimes(0.000001, 10.312335342, 10);
|
||||
expect(timeStart).toBe(0);
|
||||
expect(timeEnd).toBe(10);
|
||||
expect(duration).toBe(10);
|
||||
});
|
||||
|
||||
it('ensures values dont overflow dayMs', () => {
|
||||
const start = 86100000;
|
||||
const endOverDay = 87420000;
|
||||
const durationNormal = 1320000;
|
||||
|
||||
const { timeStart, timeEnd, duration } = validateTimes(start, endOverDay, durationNormal);
|
||||
expect(timeStart).toBe(start);
|
||||
expect(timeEnd).toBe(1020000);
|
||||
expect(duration).toBe(durationNormal);
|
||||
});
|
||||
});
|
||||
|
||||
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).toBe(dayInMs - 1);
|
||||
});
|
||||
it('handles no difference', () => {
|
||||
const duration1 = calculateDuration(0, 0);
|
||||
const duration2 = calculateDuration(dayInMs, dayInMs);
|
||||
expect(duration1).toBe(0);
|
||||
expect(duration2).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
import { EndAction, TimerType } from 'ontime-types';
|
||||
import type { MaybeString } from 'ontime-types';
|
||||
import { EndAction, TimerType, TimeStrategy } from 'ontime-types';
|
||||
|
||||
import { dayInMs } from '../timeConstants.js';
|
||||
/**
|
||||
* Check if a given value is a valid type of string, returns null otherwise
|
||||
* @param {MaybeString} maybeLinkStart
|
||||
* @returns {MaybeString}
|
||||
*/
|
||||
export function validateLinkStart(maybeLinkStart: unknown): MaybeString {
|
||||
return typeof maybeLinkStart === 'string' ? maybeLinkStart : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given value is a valid time strategy, returns the fallback otherwise
|
||||
* @param {TimeStrategy} maybeTimeStrategy
|
||||
* @returns {TimeStrategy}
|
||||
*/
|
||||
export function validateTimeStrategy(maybeTimeStrategy: unknown, fallback = TimeStrategy.LockDuration): TimeStrategy {
|
||||
return Object.values(TimeStrategy).includes(maybeTimeStrategy as TimeStrategy)
|
||||
? (maybeTimeStrategy as TimeStrategy)
|
||||
: fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if given value is a valid type of EndAction, returns the fallback otherwise
|
||||
* @param {EndAction} maybeAction
|
||||
* @param {EndAction} [fallback]
|
||||
*/
|
||||
export function validateEndAction(maybeAction: unknown, fallback = EndAction.None) {
|
||||
export function validateEndAction(maybeAction: unknown, fallback = EndAction.None): EndAction {
|
||||
return Object.values(EndAction).includes(maybeAction as EndAction) ? (maybeAction as EndAction) : fallback;
|
||||
}
|
||||
|
||||
@@ -16,70 +35,10 @@ export function validateEndAction(maybeAction: unknown, fallback = EndAction.Non
|
||||
* @param {TimerType} maybeTimerType
|
||||
* @param {TimerType} [fallback]
|
||||
*/
|
||||
export function validateTimerType(maybeTimerType: unknown, fallback = TimerType.CountDown) {
|
||||
export function validateTimerType(maybeTimerType: unknown, fallback = TimerType.CountDown): TimerType {
|
||||
return Object.values(TimerType).includes(maybeTimerType as TimerType) ? (maybeTimerType as TimerType) : 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a given value to an int, returns 0 otherwise
|
||||
* @param value
|
||||
* number
|
||||
*/
|
||||
function convertToInteger(value: unknown): number {
|
||||
const result = Number(value);
|
||||
return isNaN(result) ? 0 : Math.floor(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the time input variables are valid in relationship to each other
|
||||
* Infers values if necessary
|
||||
* @param _start
|
||||
* @param _end
|
||||
* @param _duration
|
||||
*/
|
||||
export function validateTimes(_start?: unknown, _end?: unknown, _duration?: unknown) {
|
||||
const timeStart = convertToInteger(_start) % dayInMs;
|
||||
const timeEnd = convertToInteger(_end) % dayInMs;
|
||||
const duration = convertToInteger(_duration) % dayInMs;
|
||||
|
||||
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, 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 };
|
||||
export function isKnownTimerType(maybeTimerType: unknown) {
|
||||
return Object.values(TimerType).includes(maybeTimerType as TimerType);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import type { OntimeEvent } from 'ontime-types';
|
||||
import { TimeStrategy } from 'ontime-types';
|
||||
|
||||
import { dayInMs } from '../timeConstants';
|
||||
import { calculateDuration, getLinkedTimes, validateTimes } from './validateTimes';
|
||||
|
||||
describe('validateTimes()', () => {
|
||||
describe('when time strategy is inferred', () => {
|
||||
it('passes through a well defined time list', () => {
|
||||
const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(5, 10, 5);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(10);
|
||||
expect(duration).toBe(5);
|
||||
expect(timeStrategy).toBe(TimeStrategy.LockDuration);
|
||||
});
|
||||
|
||||
it('handles cases when no times are given', () => {
|
||||
const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(null, undefined, null);
|
||||
expect(timeStart).toBe(0);
|
||||
expect(timeEnd).toBe(0);
|
||||
expect(duration).toBe(0);
|
||||
expect(timeStrategy).toBe(TimeStrategy.LockDuration);
|
||||
});
|
||||
|
||||
it('calculates duration', () => {
|
||||
const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(5, 10, undefined);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(10);
|
||||
expect(duration).toBe(5);
|
||||
expect(timeStrategy).toBe(TimeStrategy.LockEnd);
|
||||
});
|
||||
|
||||
it('calculates end time', () => {
|
||||
const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(5, undefined, 10);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(15);
|
||||
expect(duration).toBe(10);
|
||||
expect(timeStrategy).toBe(TimeStrategy.LockDuration);
|
||||
});
|
||||
|
||||
it('handles events that finish the day after', () => {
|
||||
const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(100, 10, undefined);
|
||||
expect(timeStart).toBe(100);
|
||||
expect(timeEnd).toBe(10);
|
||||
expect(duration).toBe(dayInMs - 90);
|
||||
expect(timeStrategy).toBe(TimeStrategy.LockEnd);
|
||||
});
|
||||
|
||||
it('corrects time in case of conflicts', () => {
|
||||
const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(5, 15, 15);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(15);
|
||||
expect(duration).toBe(10);
|
||||
expect(timeStrategy).toBe(TimeStrategy.LockDuration);
|
||||
});
|
||||
|
||||
it('calculates start time', () => {
|
||||
const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(undefined, 15, 10);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(15);
|
||||
expect(duration).toBe(10);
|
||||
expect(timeStrategy).toBe(TimeStrategy.LockDuration);
|
||||
});
|
||||
|
||||
it('calculates start and end time', () => {
|
||||
const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(undefined, undefined, 10);
|
||||
expect(timeStart).toBe(0);
|
||||
expect(timeEnd).toBe(10);
|
||||
expect(duration).toBe(10);
|
||||
expect(timeStrategy).toBe(TimeStrategy.LockDuration);
|
||||
});
|
||||
|
||||
it('ensures values are integers', () => {
|
||||
const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(0.000001, 10.312335342, 10);
|
||||
expect(timeStart).toBe(0);
|
||||
expect(timeEnd).toBe(10);
|
||||
expect(duration).toBe(10);
|
||||
expect(timeStrategy).toBe(TimeStrategy.LockDuration);
|
||||
});
|
||||
it('prevents values from overflowing', () => {
|
||||
const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(dayInMs - 5, undefined, 10);
|
||||
expect(timeStart).toBe(dayInMs - 5);
|
||||
expect(timeEnd).toBe(5);
|
||||
expect(duration).toBe(10);
|
||||
expect(timeStrategy).toBe(TimeStrategy.LockDuration);
|
||||
});
|
||||
});
|
||||
describe('when time strategy is given', () => {
|
||||
it('calculates end', () => {
|
||||
const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(5, 20, 20, TimeStrategy.LockDuration);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(25);
|
||||
expect(duration).toBe(20);
|
||||
expect(timeStrategy).toBe(TimeStrategy.LockDuration);
|
||||
});
|
||||
it('calculates duration', () => {
|
||||
const { timeStart, timeEnd, duration, timeStrategy } = validateTimes(5, 20, 20, TimeStrategy.LockEnd);
|
||||
expect(timeStart).toBe(5);
|
||||
expect(timeEnd).toBe(20);
|
||||
expect(duration).toBe(15);
|
||||
expect(timeStrategy).toBe(TimeStrategy.LockEnd);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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).toBe(dayInMs - 1);
|
||||
});
|
||||
it('handles no difference', () => {
|
||||
const duration1 = calculateDuration(0, 0);
|
||||
const duration2 = calculateDuration(dayInMs, dayInMs);
|
||||
expect(duration1).toBe(0);
|
||||
expect(duration2).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLinkedTimes()', () => {
|
||||
it('returns times with lock end', () => {
|
||||
const source = {
|
||||
timeStart: 5,
|
||||
timeEnd: 15,
|
||||
duration: 10,
|
||||
} as OntimeEvent;
|
||||
const target = {
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
duration: 10,
|
||||
timeStrategy: TimeStrategy.LockEnd,
|
||||
} as OntimeEvent;
|
||||
|
||||
const timePatch = getLinkedTimes(target, source);
|
||||
expect(timePatch).toStrictEqual({
|
||||
timeStart: 15,
|
||||
timeEnd: 20,
|
||||
duration: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns times with lock duration', () => {
|
||||
const source = {
|
||||
timeStart: 5,
|
||||
timeEnd: 15,
|
||||
duration: 10,
|
||||
} as OntimeEvent;
|
||||
const target = {
|
||||
timeStart: 10,
|
||||
timeEnd: 20,
|
||||
duration: 10,
|
||||
timeStrategy: TimeStrategy.LockDuration,
|
||||
} as OntimeEvent;
|
||||
|
||||
const timePatch = getLinkedTimes(target, source);
|
||||
expect(timePatch).toStrictEqual({
|
||||
timeStart: 15,
|
||||
timeEnd: 25,
|
||||
duration: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('prevents overflow', () => {
|
||||
const source = {
|
||||
timeStart: 5,
|
||||
timeEnd: dayInMs - 5,
|
||||
duration: 10,
|
||||
} as OntimeEvent;
|
||||
const target = {
|
||||
timeStart: 0,
|
||||
timeEnd: 20,
|
||||
duration: 10,
|
||||
timeStrategy: TimeStrategy.LockDuration,
|
||||
} as OntimeEvent;
|
||||
|
||||
const timePatch = getLinkedTimes(target, source);
|
||||
expect(timePatch).toStrictEqual({
|
||||
timeStart: dayInMs - 5,
|
||||
timeEnd: 5,
|
||||
duration: 10,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { OntimeEvent } from 'ontime-types';
|
||||
import { TimeStrategy } from 'ontime-types';
|
||||
|
||||
import { dayInMs } from '../timeConstants.js';
|
||||
import { validateTimeStrategy } from '../validate-events/validateEvent.js';
|
||||
|
||||
export function getLinkedTimes(
|
||||
target: OntimeEvent,
|
||||
source: OntimeEvent,
|
||||
): { timeStart: number; duration: number; timeEnd: number } {
|
||||
const lockEnd = target.timeStrategy === TimeStrategy.LockEnd;
|
||||
const lockDuration = target.timeStrategy === TimeStrategy.LockDuration;
|
||||
const newStart = source.timeEnd;
|
||||
|
||||
const timePatch = {
|
||||
timeStart: newStart,
|
||||
timeEnd: lockEnd ? target.timeEnd : calculateEnd(newStart, target.duration),
|
||||
duration: lockDuration ? target.duration : calculateDuration(newStart, target.timeEnd),
|
||||
};
|
||||
|
||||
return timePatch;
|
||||
}
|
||||
|
||||
function inferTimes(
|
||||
_start?: unknown,
|
||||
_end?: unknown,
|
||||
_duration?: unknown,
|
||||
): { timeStart: number; duration: number; timeEnd: number; timeStrategy: TimeStrategy } {
|
||||
const timeStart = convertToInteger(_start);
|
||||
const timeEnd = convertToInteger(_end);
|
||||
const duration = convertToInteger(_duration);
|
||||
|
||||
// TODO: prevent overflow
|
||||
|
||||
if (_start != null && _end != null) {
|
||||
// Case 1. if we have start and end, duration must be derived
|
||||
return {
|
||||
timeStart,
|
||||
duration: calculateDuration(timeStart, timeEnd),
|
||||
timeEnd,
|
||||
timeStrategy: _duration != null ? TimeStrategy.LockDuration : TimeStrategy.LockEnd,
|
||||
};
|
||||
}
|
||||
|
||||
if (_start == null && _end == null) {
|
||||
if (_duration == null) {
|
||||
// Case 2. no valid times were given
|
||||
return { timeStart: 0, duration: 0, timeEnd: 0, timeStrategy: TimeStrategy.LockDuration };
|
||||
}
|
||||
// Case 3. we have a duration and infer the rest
|
||||
return { timeStart, duration, timeEnd: duration, timeStrategy: TimeStrategy.LockDuration };
|
||||
}
|
||||
|
||||
if (_start != null) {
|
||||
// Case 5. with only start, we can calculate the rest
|
||||
return { timeStart, duration, timeEnd: (timeStart + duration) % dayInMs, timeStrategy: TimeStrategy.LockDuration };
|
||||
}
|
||||
|
||||
if (_end != null) {
|
||||
// Case 6. with only end, we can calculate the rest
|
||||
return {
|
||||
timeStart: timeEnd - duration,
|
||||
duration,
|
||||
timeEnd,
|
||||
timeStrategy: _duration != null ? TimeStrategy.LockDuration : TimeStrategy.LockEnd,
|
||||
};
|
||||
}
|
||||
|
||||
// we should have covered all cases
|
||||
return { timeStart, duration, timeEnd, timeStrategy: TimeStrategy.LockDuration };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the time input variables are valid in relationship to each other
|
||||
* Infers values if necessary
|
||||
* @param _start
|
||||
* @param _end
|
||||
* @param _duration
|
||||
*/
|
||||
export function validateTimes(
|
||||
_start?: unknown,
|
||||
_end?: unknown,
|
||||
_duration?: unknown,
|
||||
_strategy?: TimeStrategy,
|
||||
): { timeStart: number; duration: number; timeEnd: number; timeStrategy: TimeStrategy } {
|
||||
if (_strategy == null) {
|
||||
// if no strategy is given we infer it from given parameters
|
||||
return inferTimes(_start, _end, _duration);
|
||||
}
|
||||
|
||||
const timeStrategy = validateTimeStrategy(_strategy);
|
||||
const timeStart = convertToInteger(_start);
|
||||
let timeEnd = convertToInteger(_end);
|
||||
let duration = convertToInteger(_duration);
|
||||
|
||||
if (timeStrategy === TimeStrategy.LockEnd) {
|
||||
duration = calculateDuration(timeStart, timeEnd);
|
||||
} else {
|
||||
timeEnd = calculateEnd(timeStart, duration);
|
||||
}
|
||||
return { timeStart, duration, timeEnd, timeStrategy };
|
||||
}
|
||||
|
||||
/**
|
||||
* @description calculates event duration considering midnight
|
||||
* @param {number} timeStart
|
||||
* @param {number} timeEnd
|
||||
* @returns {number}
|
||||
*/
|
||||
export function calculateDuration(timeStart: number, timeEnd: number): number {
|
||||
// Durations must be positive
|
||||
if (timeEnd < timeStart) {
|
||||
return timeEnd + dayInMs - timeStart;
|
||||
}
|
||||
return timeEnd - timeStart;
|
||||
}
|
||||
|
||||
export function calculateEnd(timeStart: number, duration: number): number {
|
||||
return (timeStart + duration) % dayInMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a given value to an int, returns 0 otherwise
|
||||
* @param value
|
||||
* number
|
||||
*/
|
||||
function convertToInteger(value: unknown): number {
|
||||
const result = Number(value);
|
||||
return isNaN(result) ? 0 : Math.floor(result);
|
||||
}
|
||||
Reference in New Issue
Block a user