Integration/human readable time (#405)

* refactor: remove unused code

* feat: add human readable values to message parsing

---------

Co-authored-by: Fabian Posenau <fabian@fphome.de>
Co-authored-by: cv <34649812+cpvalente@users.noreply.github.com>
This commit is contained in:
Fabian Posenau
2023-06-05 20:48:53 +02:00
committed by GitHub
parent bfb483508d
commit 7cfe7c0d78
19 changed files with 302 additions and 271 deletions
-1
View File
@@ -302,7 +302,6 @@ export class TimerService {
this.pausedTime,
this.timer.clock,
);
this.timer.elapsed = getElapsed(this.timer.startedAt, this.timer.clock);
}
}
@@ -1,56 +1,4 @@
import { parseTemplate, parseTemplateNested } from './integrationUtils.js';
describe('parseTemplate()', () => {
it('correctly parses a given string', () => {
const mockState = { test: 'this' };
const testString = 'That should replace {{test}}';
const expected = `That should replace ${mockState.test}`;
const result = parseTemplate(testString, mockState);
expect(result).toStrictEqual(expected);
});
it('parses string with multiple variables', () => {
const mockState = { test1: 'that', test2: 'this' };
const testString = '{{test1}} should replace {{test2}}';
const expected = `${mockState.test1} should replace ${mockState.test2}`;
const result = parseTemplate(testString, mockState);
expect(result).toStrictEqual(expected);
});
it('correctly parses a string without templates', () => {
const testString = 'That should replace {test}';
const result = parseTemplate(testString, {});
expect(result).toStrictEqual(testString);
});
it('handles scenarios with missing variables', () => {
// by failing to provide a value, we give visibility to
// potential issues in the given string
const mockState = { test1: 'that', test2: 'this' };
const testString = '{{test1}} should replace {{test2}}, but not {{test3}}';
const expected = `${mockState.test1} should replace ${mockState.test2}, but not {{test3}}`;
const result = parseTemplate(testString, mockState);
expect(result).toStrictEqual(expected);
});
it('doesnt yet handle nested variables', () => {
const mockState = {
timer: {
time: '10',
},
enabled: 'is',
};
const testString = 'Timer {{enabled}} enabled with {{timer.time}}ms interval';
const expected = 'Timer is enabled with {{timer.time}}ms interval';
const result = parseTemplate(testString, mockState);
expect(result).toStrictEqual(expected);
});
});
import { parseTemplateNested } from './integrationUtils.js';
describe('parseTemplateNested()', () => {
it('parses string with a single-level variable name', () => {
@@ -94,3 +42,57 @@ describe('parseTemplateNested()', () => {
expect(result).toStrictEqual(expected);
});
});
describe('parseNestedTemplate() -> resolveAliasData()', () => {
it('resolves data through callback', () => {
const data = {
not: {
so: {
easy: '3',
},
},
};
const aliases = {
easy: { key: 'not.so.easy', cb: (value: string) => `testing-${value}` },
};
const easyParse = parseTemplateNested('{{human.easy}}', data, aliases);
expect(easyParse).toBe('testing-3');
});
it('handles a mixed operation', () => {
const data = {
not: {
so: {
easy: '3',
},
},
other: {
value: 42,
},
};
const aliases = {
easy: { key: 'not.so.easy', cb: (value: string) => `testing-${value}` },
};
const easyParse = parseTemplateNested('{{other.value}} to {{human.easy}}', data, aliases);
expect(easyParse).toBe('42 to testing-3');
});
it('returns given key when not found', () => {
const data = {
not: {
so: {
easy: '3',
},
},
other: {
value: 5,
},
};
const aliases = {
easy: { key: 'not.so.easy', cb: (value: string) => `testing-${value}` },
};
const easyParse = parseTemplateNested('{{other.value}} to {{human.easy}} {{human.not.found}}', data, aliases);
expect(easyParse).toBe('5 to testing-3 {{human.not.found}}');
});
});
@@ -1,34 +1,65 @@
// any value inside double curly braces {{val}}
import { formatDisplay } from 'ontime-utils';
const placeholderRegex = /{{(.*?)}}/g;
/**
* Parses a templated string
*/
export function parseTemplate(template: string, state: object): string {
let parsedTemplate = template;
let match;
while ((match = placeholderRegex.exec(template)) !== null) {
const variableName = match[1];
if (Object.hasOwn(state, variableName)) {
parsedTemplate = parsedTemplate.replace(match[0], state[variableName]);
function formatDisplayFromString(value: string, hideZero = false): string {
let valueInNumber = null;
if (value !== 'null') {
const parsedValue = Number(value);
if (!Number.isNaN(parsedValue)) {
valueInNumber = parsedValue;
}
}
return parsedTemplate;
return formatDisplay(valueInNumber, hideZero);
}
type AliasesDefinition = Record<string, { key: string; cb: (value: unknown) => string }>;
const quickAliases: AliasesDefinition = {
clock: { key: 'timer.clock', cb: (value: string) => formatDisplayFromString(value) },
duration: { key: 'timer.duration', cb: (value: string) => formatDisplayFromString(value, true) },
expectedEnd: {
key: 'timer.expectedFinish',
cb: (value: string) => formatDisplayFromString(value),
},
runningTimer: {
key: 'timer.current',
cb: (value: string) => formatDisplayFromString(value, true),
},
elapsedTime: {
key: 'timer.elapsed',
cb: (value: string) => formatDisplayFromString(value, true),
},
startedAt: { key: 'timer.startedAt', cb: (value: string) => formatDisplayFromString(value) },
};
/**
* Parses a templated string to values in a nested object
*/
export function parseTemplateNested(template: string, state: object): string {
export function parseTemplateNested(template: string, state: object, humanReadable = quickAliases): string {
let parsedTemplate = template;
let match;
while ((match = placeholderRegex.exec(template)) !== null) {
const matches = Array.from(parsedTemplate.matchAll(placeholderRegex));
for (const match of matches) {
const variableName = match[1];
const variableParts = variableName.split('.');
// iterate through variable parts, and look for the property in the state object
const value = variableParts.reduce((obj, key) => obj && obj[key], state);
if (value !== undefined) {
let value = undefined;
if (variableParts[0] === 'human') {
const lookupKey = variableParts[1];
if (lookupKey in humanReadable) {
const newTemplate = `{{${humanReadable[lookupKey].key}}}`;
const parsed = parseTemplateNested(newTemplate, state, humanReadable);
value = humanReadable[lookupKey].cb(parsed);
} else {
value = undefined;
}
} else {
// iterate through variable parts, and look for the property in the state object
value = variableParts.reduce((obj, key) => obj && obj[key], state);
}
if (typeof value !== 'undefined') {
parsedTemplate = parsedTemplate.replace(match[0], value);
}
}
@@ -1,3 +1,5 @@
import { isTimeString } from 'ontime-utils';
const mts = 1000; // millis to seconds
const mtm = 1000 * 60; // millis to minutes
const mth = 1000 * 60 * 60; // millis to hours
@@ -6,32 +8,13 @@ export const timeFormat = 'HH:mm';
export const timeFormatSeconds = 'HH:mm:ss';
export const DAY_TO_MS = 86400000;
/**
* @description Validates a time string
* @param {string} string - time string "23:00:12"
* @returns {boolean} string represents time
*/
export const isTimeString = (string) => {
// ^ # 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)$/;
return regex.test(string);
};
/**
* @description Converts an excel date to milliseconds
* @argument {string} date - excel string date
* @returns {number} - time in milliseconds
*/
export const dateToMillis = (date) => {
export const dateToMillis = (date: Date): number => {
const h = date.getHours();
const m = date.getMinutes();
const s = date.getSeconds();
@@ -44,7 +27,7 @@ export const dateToMillis = (date) => {
* @param valueAsString
* @return {number}
*/
const parse = (valueAsString) => {
const parse = (valueAsString: string): number => {
const parsed = parseInt(valueAsString, 10);
if (isNaN(parsed)) {
return 0;
@@ -58,7 +41,7 @@ const parse = (valueAsString) => {
* @param {boolean} fillLeft - autofill left = hours / right = seconds
* @returns {number} - time string in millis
*/
export const forgivingStringToMillis = (value, fillLeft = true) => {
export const forgivingStringToMillis = (value: string, fillLeft = true): number => {
let millis = 0;
// split string at known separators : , .
@@ -106,13 +89,25 @@ export const forgivingStringToMillis = (value, fillLeft = true) => {
* @returns {number} - time in milliseconds
*/
export const parseExcelDate = (excelDate) => {
export const parseExcelDate = (excelDate: string): number => {
// attempt converting to date object
const date = new Date(excelDate);
if (date instanceof Date && !isNaN(date)) {
if (date instanceof Date && !isNaN(date.getTime())) {
return dateToMillis(date);
} else if (isTimeString(excelDate)) {
return forgivingStringToMillis(excelDate);
}
return 0;
};
/**
* @description Converts milliseconds to seconds -- Copied from client code
* @param {number | null} millis - time in seconds
* @returns {number} Amount in seconds
*/
export const millisToSeconds = (millis: number | null): number => {
if (millis === null) {
return 0;
}
return millis < 0 ? Math.ceil(millis / mts) : Math.floor(millis / mts);
};