mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-15 04:13:47 +00:00
refactor: normalise data (#756)
* chore: remove legal from bundle * refactor: create normalised dataset * refactor: cuesheet uses flat rundown * refactor: multi-selection * refactor: prevent stale data on server restart * refactor: increase ID size * chore: instrument operation * chore: update csv tests * fix: resolve directory to test-db (#758)
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,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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||
import { OntimeEvent, OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||
|
||||
import { getNext, getNextEvent, getPrevious, getPreviousEvent } from './rundownUtils';
|
||||
import { getNext, getNextEvent, getPrevious, getPreviousEvent, swapEventData } from './rundownUtils';
|
||||
|
||||
describe('getNext()', () => {
|
||||
it('returns the next event of type event', () => {
|
||||
@@ -149,3 +149,43 @@ describe('getPreviousEvent()', () => {
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isOntimeEvent, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||
import { isOntimeEvent, NormalisedRundown, OntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||
|
||||
/**
|
||||
* Gets first event in rundown, if it exists
|
||||
@@ -9,6 +9,17 @@ export function getFirst(rundown: OntimeRundownEntry[]) {
|
||||
return rundown.length ? rundown[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets first event in a normalised rundown, if it exists
|
||||
* @param rundown
|
||||
* @param order
|
||||
* @returns
|
||||
*/
|
||||
export function getFirstNormal(rundown: NormalisedRundown, order: string[]) {
|
||||
const firstId = order[0];
|
||||
return rundown[firstId] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets first scheduled event in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
@@ -27,6 +38,34 @@ export function getFirstEvent(rundown: OntimeRundownEntry[]): {
|
||||
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)) {
|
||||
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;
|
||||
@@ -45,7 +84,34 @@ export function getLastEvent(rundown: OntimeRundown): {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets next event in rundown, if it exists
|
||||
* 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)) {
|
||||
return { lastEvent, lastIndex: i };
|
||||
}
|
||||
}
|
||||
return { lastEvent: null, lastIndex: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets next entry in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
* @param {string} currentId
|
||||
* @return {{ nextEvent: OntimeRundownEntry | null; nextIndex: number | null } }
|
||||
@@ -64,6 +130,29 @@ export function getNext(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets next scheduled event in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
@@ -89,7 +178,34 @@ export function getNextEvent(
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {{ previousEvent: OntimeRundownEntry | null; previousIndex: number | null } }
|
||||
@@ -108,6 +224,29 @@ export function getPrevious(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets previous scheduled event in rundown, if it exists
|
||||
* @param {OntimeRundownEntry[]} rundown
|
||||
@@ -132,41 +271,54 @@ export function getPreviousEvent(
|
||||
}
|
||||
|
||||
/**
|
||||
* @description swaps two OntimeEvents in the rundown
|
||||
* @param {OntimeRundown} rundown
|
||||
* @param {number} fromEventIndex
|
||||
* @param {number} toEventIndex
|
||||
* @returns {OntimeRundown}
|
||||
* 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 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');
|
||||
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 };
|
||||
}
|
||||
|
||||
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,
|
||||
/**
|
||||
* @description swaps two OntimeEvents in the rundown
|
||||
* @param {OntimeEvent} eventA
|
||||
* @param {OntimeEvent} eventB
|
||||
*/
|
||||
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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user