fix: excel parsing

This commit is contained in:
Carlos Valente
2024-05-16 17:49:36 +02:00
committed by Carlos Valente
parent ad46fac2d7
commit dae6f84af0
6 changed files with 188 additions and 26 deletions
+128 -1
View File
@@ -19,7 +19,7 @@ import { dbModel } from '../../models/dataModel.js';
import { createEvent, getCustomFieldData, parseExcel, parseJson } from '../parser.js';
import { makeString } from '../parserUtils.js';
import { parseRundown, parseUrlPresets, parseViewSettings } from '../parserFunctions.js';
import { ImportMap } from 'ontime-utils';
import { ImportMap, MILLIS_PER_MINUTE } from 'ontime-utils';
import * as cache from '../../services/rundown-service/rundownCache.js';
describe('test json parser with valid def', () => {
@@ -1515,4 +1515,131 @@ describe('parseExcel()', () => {
expect((rundown[sixthId] as OntimeEvent).timeStart).toEqual((rundown[fourthId] as OntimeEvent).timeEnd);
expect((rundown[sixthId] as OntimeEvent).linkStart).toEqual((rundown[fourthId] as OntimeEvent).id);
});
it('#971 BUG: parses time fields and booleans', () => {
const testData = [
[
'Cue',
'Colour',
'Time Start',
'Time End',
'Duration',
'Link Start',
'Title',
'Note',
'Timer Type',
'End Action',
'Warning time',
'Danger time',
'Public',
'Skip',
],
[
'SETUP',
'',
'1899-12-30T07:15:00.000Z',
'1899-12-30T08:30:00.000Z',
'',
'false',
'Setup',
'',
'time-to-end',
'none',
'15',
'00:05:00',
'FALSE',
'TRUE',
],
[
'MEET1',
'#779BE7',
'1899-12-30T08:30:00.000Z',
'1899-12-30T10:00:00.000Z',
'',
'false',
'Meeting 1',
'',
'time-to-end',
'none',
15,
'00:05:00',
'TRUE',
'FALSE',
],
[
'MEET2',
'#779BE7',
'1899-12-30T10:00:00.000Z',
'',
'60',
'false',
'Meeting 2',
'',
'time-to-end',
'none',
'13',
'5',
'TRUE',
'FALSE',
],
[
'lunch',
'#77C785',
'',
'1899-12-30T11:30:00.000Z',
'',
'true',
'Lunch',
'',
'time-to-end',
'none',
13,
5,
'FALSE',
'FALSE',
],
[
'MEET3',
'#779BE7',
'1899-12-30T11:30:00.000Z',
'',
90,
false,
'Meeting 3',
'',
'count-up',
'none',
'11',
5,
'TRUE',
'FALSE',
],
['MEET4', '#779BE7', '', '', 30, true, 'Meeting 4', '', 'count-up', 'none', 11, '00:05:00', 'TRUE', 'FALSE'],
];
const parsedData = parseExcel(testData);
const { rundown } = parsedData;
// elements in bug report
// 15 is a number, in which case we parse it as a minutes value
expect((rundown.at(1) as OntimeEvent).timeWarning).toBe(15 * MILLIS_PER_MINUTE);
// in the case where a string is passed, we need to check whether it is an ISO 8601 date
expect((rundown.at(2) as OntimeEvent).duration).toBe(60 * MILLIS_PER_MINUTE);
expect((rundown.at(2) as OntimeEvent).timeDanger).toBe(5 * MILLIS_PER_MINUTE);
expect((rundown.at(3) as OntimeEvent).timeWarning).toBe(13 * MILLIS_PER_MINUTE);
expect((rundown.at(3) as OntimeEvent).timeDanger).toBe(5 * MILLIS_PER_MINUTE);
expect((rundown.at(4) as OntimeEvent).duration).toBe(90 * MILLIS_PER_MINUTE);
expect((rundown.at(4) as OntimeEvent).linkStart).toBe(false);
expect((rundown.at(4) as OntimeEvent).timeWarning).toBe(11 * MILLIS_PER_MINUTE);
expect((rundown.at(4) as OntimeEvent).timeDanger).toBe(5 * MILLIS_PER_MINUTE);
expect((rundown.at(5) as OntimeEvent).duration).toBe(30 * MILLIS_PER_MINUTE);
// if we get a boolean, we should just use that
expect((rundown.at(5) as OntimeEvent).linkStart).toBe(true);
expect((rundown.at(5) as OntimeEvent).timeWarning).toBe(11 * MILLIS_PER_MINUTE);
});
});
+12 -1
View File
@@ -1,3 +1,4 @@
import { MILLIS_PER_MINUTE } from 'ontime-utils';
import { parseExcelDate } from '../time.js';
describe('parseExcelDate', () => {
@@ -46,8 +47,18 @@ describe('parseExcelDate', () => {
});
});
describe('uses numeric fields as minutes', () => {
const invalidFields = [1, 10, 100];
invalidFields.forEach((field) => {
it(`handles ${field}`, () => {
const millis = parseExcelDate(field);
expect(millis).toBe(field * MILLIS_PER_MINUTE);
});
});
});
describe('returns 0 on other strings', () => {
const invalidFields = ['10', 'test', ''];
const invalidFields = ['test', ''];
invalidFields.forEach((field) => {
it(`handles ${field}`, () => {
const millis = parseExcelDate(field);
+4
View File
@@ -43,6 +43,10 @@ type ExcelData = Pick<DatabaseModel, 'rundown' | 'customFields'> & {
};
function parseBooleanString(value: unknown): boolean {
if (typeof value === 'boolean') {
return value;
}
// falsy values would be nullish or empty string
if (!value || typeof value !== 'string') {
return false;
+20 -19
View File
@@ -1,4 +1,5 @@
import { isTimeString } from 'ontime-utils';
import { MILLIS_PER_MINUTE } from 'ontime-utils';
import { isISO8601 } from '../../../../packages/utils/src/date-utils/isTimeString.js';
const mts = 1000; // millis to seconds
const mtm = 1000 * 60; // millis to minutes
@@ -49,10 +50,9 @@ const stripAMPM = (value: string) => {
/**
* @description Parses a time string to millis, copied from client code
* @param {string} value - time string
* @param {boolean} fillLeft - autofill left = hours / right = seconds
* @returns {number} - time string in millis
*/
export const forgivingStringToMillis = (value: string, fillLeft: boolean = true): number => {
export const forgivingStringToMillis = (value: string): number => {
let millis = 0;
// check for AM/PM indicators
@@ -87,16 +87,9 @@ export const forgivingStringToMillis = (value: string, fillLeft: boolean = true)
}
}
if (first != null && second != null && third == null) {
// if string has two sections
if (fillLeft) {
// treat as [hours] [minutes]
millis += parse(first) * mth;
millis += parse(second) * mtm;
} else {
// treat as [minutes] [seconds]
millis += parse(first) * mtm;
millis += parse(second) * mts;
}
// if string has two sections treat as [hours] [minutes]
millis += parse(first) * mth;
millis += parse(second) * mtm;
}
return millis;
};
@@ -109,13 +102,21 @@ export const forgivingStringToMillis = (value: string, fillLeft: boolean = true)
export const parseExcelDate = (excelDate: unknown): number => {
if (excelDate instanceof Date) {
return dateToMillis(excelDate);
} else if (typeof excelDate === 'string') {
const date = new Date(excelDate);
if (date instanceof Date && !isNaN(date.getTime())) {
return dateToMillis(date);
} else if (isTimeString(excelDate)) {
return forgivingStringToMillis(excelDate);
}
if (typeof excelDate === 'string') {
if (isISO8601(excelDate)) {
const date = new Date(excelDate);
if (date instanceof Date && !isNaN(date.getTime())) {
return dateToMillis(date);
}
}
return forgivingStringToMillis(excelDate);
}
// if the user uses a number value eg. 15, excel could format the cell as number
if (typeof excelDate === 'number') {
return excelDate * MILLIS_PER_MINUTE;
}
return 0;
@@ -1,4 +1,4 @@
import { isTimeString } from './isTimeString';
import { isISO8601, isTimeString } from './isTimeString';
describe('test isTimeString() function', () => {
it('it validates time strings', () => {
@@ -33,3 +33,16 @@ describe('test isTimeString() function handle AM/PM', () => {
});
}
});
describe('isISO8601()', () => {
it('returns true for valid ISO 8601 date-time strings', () => {
expect(isISO8601('1899-12-30T08:30:00.000Z')).toBe(true);
expect(isISO8601('2022-01-01T00:00:00.000Z')).toBe(true);
});
it('returns false for invalid ISO 8601 date-time strings', () => {
expect(isISO8601('not a date')).toBe(false);
expect(isISO8601('1899-12-30T08:30:00Z')).toBe(false); // missing milliseconds
expect(isISO8601('1899-12-30 08:30:00.000Z')).toBe(false); // space instead of 'T'
});
});
+10 -4
View File
@@ -1,9 +1,15 @@
/**
* @description Validates a time string
* @param {string} text - time string "23:00:12"
* @returns {boolean} string represents time
*/
export const isTimeString = (text: string): boolean => {
export function isTimeString(text: string): boolean {
const regex = /^(?:(?:([01]?\d|2[0-3])[:,.])?([0-5]?\d)[:,.])?([0-5]?\d)?(\s)?([APap][Mm])?$/;
return regex.test(text);
};
}
/**
* @description Validates a ISO8601 date-time string
*/
export function isISO8601(text: string): boolean {
const regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
return regex.test(text);
}