diff --git a/apps/client/src/common/components/timer-display/TimerDisplay.tsx b/apps/client/src/common/components/timer-display/TimerDisplay.tsx index f7b190332..7d2492c04 100644 --- a/apps/client/src/common/components/timer-display/TimerDisplay.tsx +++ b/apps/client/src/common/components/timer-display/TimerDisplay.tsx @@ -1,6 +1,5 @@ import { memo } from 'react'; - -import { formatDisplay } from '../../utils/dateConfig'; +import { formatDisplay } from 'ontime-utils'; import './TimerDisplay.scss'; diff --git a/apps/client/src/common/utils/__tests__/dateConfig.test.js b/apps/client/src/common/utils/__tests__/dateConfig.test.js index 7518675e7..eba30d62f 100644 --- a/apps/client/src/common/utils/__tests__/dateConfig.test.js +++ b/apps/client/src/common/utils/__tests__/dateConfig.test.js @@ -1,100 +1,4 @@ -import { - forgivingStringToMillis, - formatDisplay, - isTimeString, - millisToDelayString, - millisToMinutes, - millisToSeconds, -} from '../dateConfig'; - -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' }; - 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); - }); -}); +import { forgivingStringToMillis, millisToDelayString, millisToMinutes, millisToSeconds } from '../dateConfig'; describe('test millisToSeconds function', () => { it('test with null values', () => { @@ -170,31 +74,6 @@ describe('test millisToMinutes function', () => { }); }); -describe('test isTimeString() function', () => { - it('it validates time strings', () => { - const ts = ['2', '2:10', '2:10:22']; - for (const s of ts) { - expect(isTimeString(s)).toBe(true); - } - }); - - it('it fails overloaded times', () => { - const ts = ['70', '89:10', '26:10:22']; - for (const s of ts) { - expect(isTimeString(s)).toBe(false); - } - }); -}); - -describe('test isTimeString() function handle different separators', () => { - const ts = ['2:10', '2,10', '2.10']; - for (const s of ts) { - it(`it handles ${s}`, () => { - expect(isTimeString(s)).toBe(true); - }); - } -}); - describe('test forgivingStringToMillis()', () => { describe('function handles time with no separators', () => { const testData = [ diff --git a/apps/client/src/common/utils/dateConfig.ts b/apps/client/src/common/utils/dateConfig.ts index 228255895..e001d9836 100644 --- a/apps/client/src/common/utils/dateConfig.ts +++ b/apps/client/src/common/utils/dateConfig.ts @@ -5,29 +5,6 @@ import { mth, mtm, mts } from './timeConstants'; export const timeFormat = 'HH:mm'; export const timeFormatSeconds = 'HH:mm:ss'; -/** - * another go at simpler string formatting (counters) - * @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(':'); -} - /** * @description Converts milliseconds to seconds * @param {number | null} millis - time in seconds @@ -49,26 +26,6 @@ export const millisToMinutes = (millis: number): number => { return millis < 0 ? Math.ceil(millis / mtm) : Math.floor(millis / mtm); }; -/** - * @description Validates a time string - * @param {string} string - time string "23:00:12" - * @returns {boolean} string represents time - */ -export const isTimeString = (string: 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)$/; - return regex.test(string); -}; - /** * @description safe parse string to int * @param {string} valueAsString diff --git a/apps/client/src/features/modals/integration-modal/IntegrationModal.tsx b/apps/client/src/features/modals/integration-modal/IntegrationModal.tsx index a08ccbf9f..e71e3fb19 100644 --- a/apps/client/src/features/modals/integration-modal/IntegrationModal.tsx +++ b/apps/client/src/features/modals/integration-modal/IntegrationModal.tsx @@ -12,7 +12,7 @@ interface IntegrationModalProps { onClose: () => void; } -const oscDocsUrl = 'https://ontime.gitbook.io/v2/control-and-feedback/osc'; +const oscDocsUrl = 'https://ontime.gitbook.io/v2/control-and-feedback/integrations'; export default function IntegrationModal(props: IntegrationModalProps) { const { isOpen, onClose } = props; diff --git a/apps/client/src/features/table/TableHeader.jsx b/apps/client/src/features/table/TableHeader.jsx index ffa4cbeef..2707ef7fb 100644 --- a/apps/client/src/features/table/TableHeader.jsx +++ b/apps/client/src/features/table/TableHeader.jsx @@ -5,13 +5,13 @@ import { FiTarget } from '@react-icons/all-files/fi/FiTarget'; import { IoContract } from '@react-icons/all-files/io5/IoContract'; import { IoExpand } from '@react-icons/all-files/io5/IoExpand'; import { IoMoon } from '@react-icons/all-files/io5/IoMoon'; +import { formatDisplay } from 'ontime-utils'; import PropTypes from 'prop-types'; import { TableSettingsContext } from '../../common/context/TableSettingsContext'; import useFullscreen from '../../common/hooks/useFullscreen'; import { useTimer } from '../../common/hooks/useSocket'; import useEventData from '../../common/hooks-query/useEventData'; -import { formatDisplay } from '../../common/utils/dateConfig'; import { formatTime } from '../../common/utils/time'; import { tooltipDelayFast } from '../../ontimeConfig'; diff --git a/apps/client/src/features/viewers/backstage/Backstage.tsx b/apps/client/src/features/viewers/backstage/Backstage.tsx index 11fb8487c..ce8b1bf9a 100644 --- a/apps/client/src/features/viewers/backstage/Backstage.tsx +++ b/apps/client/src/features/viewers/backstage/Backstage.tsx @@ -2,6 +2,7 @@ import { useEffect } from 'react'; import QRCode from 'react-qr-code'; import { AnimatePresence, motion } from 'framer-motion'; import { EventData, Message, OntimeEvent, ViewSettings } from 'ontime-types'; +import { formatDisplay } from 'ontime-utils'; import { overrideStylesURL } from '../../../common/api/apiConstants'; import { TIME_FORMAT_OPTION } from '../../../common/components/edit-form-drawer/constants'; @@ -14,7 +15,6 @@ import ScheduleNav from '../../../common/components/schedule/ScheduleNav'; import TitleCard from '../../../common/components/title-card/TitleCard'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { TimeManagerType } from '../../../common/models/TimeManager.type'; -import { formatDisplay } from '../../../common/utils/dateConfig'; import { getEventsWithDelay } from '../../../common/utils/eventsManager'; import { formatTime } from '../../../common/utils/time'; import { useTranslation } from '../../../translation/TranslationProvider'; diff --git a/apps/client/src/features/viewers/common/viewerUtils.ts b/apps/client/src/features/viewers/common/viewerUtils.ts index 54430d253..637146a70 100644 --- a/apps/client/src/features/viewers/common/viewerUtils.ts +++ b/apps/client/src/features/viewers/common/viewerUtils.ts @@ -1,7 +1,7 @@ import { TimerType } from 'ontime-types'; +import { formatDisplay } from 'ontime-utils'; import { TimeManagerType } from '../../../common/models/TimeManager.type'; -import { formatDisplay } from '../../../common/utils/dateConfig'; import { formatTime } from '../../../common/utils/time'; const formatOptions = { diff --git a/apps/client/src/features/viewers/countdown/Countdown.tsx b/apps/client/src/features/viewers/countdown/Countdown.tsx index 7095f4c55..24483aaf9 100644 --- a/apps/client/src/features/viewers/countdown/Countdown.tsx +++ b/apps/client/src/features/viewers/countdown/Countdown.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent, ViewSettings } from 'ontime-types'; +import { formatDisplay } from 'ontime-utils'; import { overrideStylesURL } from '../../../common/api/apiConstants'; import { TIME_FORMAT_OPTION } from '../../../common/components/edit-form-drawer/constants'; @@ -8,7 +9,6 @@ import EditFormDrawer from '../../../common/components/edit-form-drawer/EditForm import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; import { TimeManagerType } from '../../../common/models/TimeManager.type'; -import { formatDisplay } from '../../../common/utils/dateConfig'; import getDelayTo from '../../../common/utils/getDelayTo'; import { formatTime } from '../../../common/utils/time'; import { useTranslation } from '../../../translation/TranslationProvider'; diff --git a/apps/client/src/features/viewers/studio/StudioClock.jsx b/apps/client/src/features/viewers/studio/StudioClock.jsx index 9c403a79e..255314f7a 100644 --- a/apps/client/src/features/viewers/studio/StudioClock.jsx +++ b/apps/client/src/features/viewers/studio/StudioClock.jsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { millisToString } from 'ontime-utils'; +import { formatDisplay, millisToString } from 'ontime-utils'; import PropTypes from 'prop-types'; import { overrideStylesURL } from '../../../common/api/apiConstants'; @@ -9,7 +9,6 @@ import EditFormDrawer from '../../../common/components/edit-form-drawer/EditForm import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu'; import useFitText from '../../../common/hooks/useFitText'; import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet'; -import { formatDisplay } from '../../../common/utils/dateConfig'; import { formatEventList, getEventsWithDelay, trimRundown } from '../../../common/utils/eventsManager'; import { formatTime } from '../../../common/utils/time'; diff --git a/apps/server/src/services/TimerService.ts b/apps/server/src/services/TimerService.ts index e19da9512..6197ff239 100644 --- a/apps/server/src/services/TimerService.ts +++ b/apps/server/src/services/TimerService.ts @@ -302,7 +302,6 @@ export class TimerService { this.pausedTime, this.timer.clock, ); - this.timer.elapsed = getElapsed(this.timer.startedAt, this.timer.clock); } } diff --git a/apps/server/src/services/integration-service/integrationUtils.test.ts b/apps/server/src/services/integration-service/integrationUtils.test.ts index 3425366a5..8f1d797ea 100644 --- a/apps/server/src/services/integration-service/integrationUtils.test.ts +++ b/apps/server/src/services/integration-service/integrationUtils.test.ts @@ -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}}'); + }); +}); diff --git a/apps/server/src/services/integration-service/integrationUtils.ts b/apps/server/src/services/integration-service/integrationUtils.ts index 31bde6d8c..f9e7b01c7 100644 --- a/apps/server/src/services/integration-service/integrationUtils.ts +++ b/apps/server/src/services/integration-service/integrationUtils.ts @@ -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 }>; +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); } } diff --git a/apps/server/src/utils/time.js b/apps/server/src/utils/time.ts similarity index 74% rename from apps/server/src/utils/time.js rename to apps/server/src/utils/time.ts index 0ad212451..ef761464e 100644 --- a/apps/server/src/utils/time.js +++ b/apps/server/src/utils/time.ts @@ -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); +}; diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 18ccdd28e..323704a96 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -1,3 +1,5 @@ +export { formatDisplay } from './src/date-utils/formatDisplay.js'; export { formatFromMillis } from './src/date-utils/formatFromMillis.js'; +export { isTimeString } from './src/date-utils/isTimeString.js'; export { millisToString } from './src/date-utils/millisToString.js'; export { generateId } from './src/generate-id/generateId.js'; diff --git a/packages/utils/src/date-utils/formatDisplay.test.ts b/packages/utils/src/date-utils/formatDisplay.test.ts new file mode 100644 index 000000000..c9ffa76b8 --- /dev/null +++ b/packages/utils/src/date-utils/formatDisplay.test.ts @@ -0,0 +1,91 @@ +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); + }); +}); diff --git a/packages/utils/src/date-utils/formatDisplay.ts b/packages/utils/src/date-utils/formatDisplay.ts new file mode 100644 index 000000000..ce9f1f759 --- /dev/null +++ b/packages/utils/src/date-utils/formatDisplay.ts @@ -0,0 +1,31 @@ +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); +}; diff --git a/packages/utils/src/date-utils/isTimeString.test.ts b/packages/utils/src/date-utils/isTimeString.test.ts new file mode 100644 index 000000000..27a6e565f --- /dev/null +++ b/packages/utils/src/date-utils/isTimeString.test.ts @@ -0,0 +1,26 @@ +import { isTimeString } from './isTimeString'; + +describe('test isTimeString() function', () => { + it('it validates time strings', () => { + const ts = ['2', '2:10', '2:10:22']; + for (const s of ts) { + expect(isTimeString(s)).toBe(true); + } + }); + + it('it fails overloaded times', () => { + const ts = ['70', '89:10', '26:10:22']; + for (const s of ts) { + expect(isTimeString(s)).toBe(false); + } + }); +}); + +describe('test isTimeString() function handle different separators', () => { + const ts = ['2:10', '2,10', '2.10']; + for (const s of ts) { + it(`it handles ${s}`, () => { + expect(isTimeString(s)).toBe(true); + }); + } +}); diff --git a/packages/utils/src/date-utils/isTimeString.ts b/packages/utils/src/date-utils/isTimeString.ts new file mode 100644 index 000000000..62a52bf81 --- /dev/null +++ b/packages/utils/src/date-utils/isTimeString.ts @@ -0,0 +1,19 @@ +/** + * @description Validates a time string + * @param {string} text - time string "23:00:12" + * @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)$/; + return regex.test(text); +}; diff --git a/packages/utils/src/timeConstants.ts b/packages/utils/src/timeConstants.ts new file mode 100644 index 000000000..3afa656a7 --- /dev/null +++ b/packages/utils/src/timeConstants.ts @@ -0,0 +1 @@ +export const mts = 1000; // millis to seconds