mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 11:53:49 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user