From ce910c9cc63b913fc73f9d9de7bd988fa91c7adc Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sun, 26 May 2024 22:09:12 +0200 Subject: [PATCH] refactor: extract parseUserTime --- .../input/delay-input/DelayInput.tsx | 5 +- .../components/input/time-input/TimeInput.tsx | 5 +- .../client/src/common/hooks/useEventAction.ts | 12 +- .../common/utils/__tests__/dateConfig.test.js | 335 +----------------- apps/client/src/common/utils/dateConfig.ts | 176 +-------- .../interface-panel/EditorSettingsForm.tsx | 8 +- .../control/playback/add-time/AddTime.tsx | 5 +- .../control/playback/aux-timer/AuxTimer.tsx | 4 +- .../composite/EventEditorTimes.tsx | 6 +- packages/utils/index.ts | 1 + .../src/date-utils/parseUserTime.test.ts | 333 +++++++++++++++++ .../utils/src/date-utils/parseUserTime.ts | 174 +++++++++ 12 files changed, 534 insertions(+), 530 deletions(-) create mode 100644 packages/utils/src/date-utils/parseUserTime.test.ts create mode 100644 packages/utils/src/date-utils/parseUserTime.ts diff --git a/apps/client/src/common/components/input/delay-input/DelayInput.tsx b/apps/client/src/common/components/input/delay-input/DelayInput.tsx index 61bcb7e70..1187eb6c8 100644 --- a/apps/client/src/common/components/input/delay-input/DelayInput.tsx +++ b/apps/client/src/common/components/input/delay-input/DelayInput.tsx @@ -1,9 +1,8 @@ import { KeyboardEvent, useEffect, useRef, useState } from 'react'; import { Input, Radio, RadioGroup } from '@chakra-ui/react'; -import { millisToString } from 'ontime-utils'; +import { millisToString, parseUserTime } from 'ontime-utils'; import { useEventAction } from '../../../hooks/useEventAction'; -import { forgivingStringToMillis } from '../../../utils/dateConfig'; import style from './DelayInput.module.scss'; @@ -40,7 +39,7 @@ export default function DelayInput(props: DelayInputProps) { } const isNegative = newValue.startsWith('-'); - let newMillis = forgivingStringToMillis(newValue); + let newMillis = parseUserTime(newValue); if (isNegative) { newMillis = newMillis * -1; diff --git a/apps/client/src/common/components/input/time-input/TimeInput.tsx b/apps/client/src/common/components/input/time-input/TimeInput.tsx index f2a912670..ef3789286 100644 --- a/apps/client/src/common/components/input/time-input/TimeInput.tsx +++ b/apps/client/src/common/components/input/time-input/TimeInput.tsx @@ -1,9 +1,8 @@ import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react'; import { Input } from '@chakra-ui/react'; -import { millisToString } from 'ontime-utils'; +import { millisToString, parseUserTime } from 'ontime-utils'; import { useEmitLog } from '../../../stores/logger'; -import { forgivingStringToMillis } from '../../../utils/dateConfig'; interface TimeInputProps { name: T; @@ -57,7 +56,7 @@ export default function TimeInput(props: TimeInputProps) { submitHandler(name, newValue); } - const valueInMillis = forgivingStringToMillis(newValue); + const valueInMillis = parseUserTime(newValue); if (valueInMillis === time) { return false; } diff --git a/apps/client/src/common/hooks/useEventAction.ts b/apps/client/src/common/hooks/useEventAction.ts index 53cf2972e..629b236af 100644 --- a/apps/client/src/common/hooks/useEventAction.ts +++ b/apps/client/src/common/hooks/useEventAction.ts @@ -6,6 +6,7 @@ import { getLinkedTimes, getPreviousEventNormal, MILLIS_PER_SECOND, + parseUserTime, reorderArray, swapEventData, } from 'ontime-utils'; @@ -25,7 +26,6 @@ import { } from '../api/rundown'; import { logAxiosError } from '../api/utils'; import { useEditorSettings } from '../stores/editorSettings'; -import { forgivingStringToMillis } from '../utils/dateConfig'; /** * @description Set of utilities for events //TODO: should this be called useEntryAction and so on @@ -94,15 +94,15 @@ export const useEventAction = () => { } if (newEvent.duration === undefined && newEvent.timeEnd === undefined) { - newEvent.duration = forgivingStringToMillis(defaultDuration); + newEvent.duration = parseUserTime(defaultDuration); } if (newEvent.timeDanger === undefined) { - newEvent.timeDanger = forgivingStringToMillis(defaultDangerTime); + newEvent.timeDanger = parseUserTime(defaultDangerTime); } if (newEvent.timeWarning === undefined) { - newEvent.timeWarning = forgivingStringToMillis(defaultWarnTime); + newEvent.timeWarning = parseUserTime(defaultWarnTime); } } @@ -217,9 +217,9 @@ export const useEventAction = () => { } else if (value.startsWith('+') || value.startsWith('p+') || value.startsWith('p +')) { // TODO: is this logic solid? const remainingString = value.substring(1); - newValMillis = getPreviousEnd() + forgivingStringToMillis(remainingString); + newValMillis = getPreviousEnd() + parseUserTime(remainingString); } else { - newValMillis = forgivingStringToMillis(value); + newValMillis = parseUserTime(value); } // dont allow timer values over 23:59:59 diff --git a/apps/client/src/common/utils/__tests__/dateConfig.test.js b/apps/client/src/common/utils/__tests__/dateConfig.test.js index 2fe45e67b..b70dc1802 100644 --- a/apps/client/src/common/utils/__tests__/dateConfig.test.js +++ b/apps/client/src/common/utils/__tests__/dateConfig.test.js @@ -1,337 +1,4 @@ -import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils'; - -import { forgivingStringToMillis, millisToDelayString } from '../dateConfig'; - -describe('test forgivingStringToMillis()', () => { - describe('function handles time with no separators', () => { - const testData = [ - { value: '', expect: 0 }, - { value: '0', expect: 0 }, - { value: '-0', expect: 0 }, - { value: '1', expect: MILLIS_PER_MINUTE }, - { value: '-1', expect: MILLIS_PER_MINUTE }, - { value: '0h0m0s', expect: 0 }, - { value: '0h0m1s', expect: MILLIS_PER_SECOND }, - { value: '0h1m0s', expect: MILLIS_PER_MINUTE }, - { value: '1h0m0s', expect: MILLIS_PER_HOUR }, - { value: '23h0m0s', expect: 23 * MILLIS_PER_HOUR }, - { value: '12h12m12s', expect: 12 * MILLIS_PER_SECOND + 12 * MILLIS_PER_MINUTE + 12 * MILLIS_PER_HOUR }, - { value: '12H12M12S', expect: 12 * MILLIS_PER_SECOND + 12 * MILLIS_PER_MINUTE + 12 * MILLIS_PER_HOUR }, - { value: '2m', expect: 2 * MILLIS_PER_MINUTE }, - { value: '1h5s', expect: MILLIS_PER_HOUR + 5 * MILLIS_PER_SECOND }, - { value: '1h2m', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE }, - ]; - - for (const s of testData) { - it(`handles ${s.value}`, () => { - expect(typeof forgivingStringToMillis(s.value)).toBe('number'); - expect(forgivingStringToMillis(s.value)).toBe(s.expect); - }); - } - }); - - describe('parses strings correctly', () => { - const ts = [ - { value: '1.1.1', expect: MILLIS_PER_HOUR + MILLIS_PER_MINUTE + MILLIS_PER_SECOND }, - { value: '12.1.1', expect: 12 * MILLIS_PER_HOUR + MILLIS_PER_MINUTE + MILLIS_PER_SECOND }, - { value: '12.55.1', expect: 12 * MILLIS_PER_HOUR + 55 * MILLIS_PER_MINUTE + MILLIS_PER_SECOND }, - { value: '12.55.40', expect: 12 * MILLIS_PER_HOUR + 55 * MILLIS_PER_MINUTE + 40 * MILLIS_PER_SECOND }, - ]; - - for (const s of ts) { - it(`handles ${s.value} to the left`, () => { - expect(forgivingStringToMillis(s.value)).toBe(s.expect); - }); - } - }); - - describe('#33 separators are parsed according to doc examples ', () => { - const ts = [ - { value: '0.1', expect: MILLIS_PER_MINUTE }, - { value: '0 1', expect: MILLIS_PER_MINUTE }, - { value: '0:1', expect: MILLIS_PER_MINUTE }, - { value: '0,1', expect: MILLIS_PER_MINUTE }, - { value: '2.2.2', expect: 2 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 2 * MILLIS_PER_SECOND }, - { value: '2 2 2', expect: 2 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 2 * MILLIS_PER_SECOND }, - { value: '2:2:2', expect: 2 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 2 * MILLIS_PER_SECOND }, - { value: '2,2,2', expect: 2 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 2 * MILLIS_PER_SECOND }, - { value: '2,2,2', expect: 2 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 2 * MILLIS_PER_SECOND }, - { value: '10:', expect: 10 * MILLIS_PER_HOUR }, - { value: ':10', expect: 10 * MILLIS_PER_MINUTE }, - { value: '10', expect: 10 * MILLIS_PER_MINUTE }, - { value: '120', expect: MILLIS_PER_HOUR + 20 * MILLIS_PER_MINUTE }, - { value: '90m', expect: 90 * MILLIS_PER_MINUTE }, - { value: '1.2', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE }, - { value: '1.2.3', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 3 * MILLIS_PER_SECOND }, - { value: '123456', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE + 56 * MILLIS_PER_SECOND }, - ]; - - for (const s of ts) { - it(`handles ${s.value}`, () => { - expect(forgivingStringToMillis(s.value)).toBe(s.expect); - }); - } - }); - - describe('parses time strings', () => { - const ts = [ - { value: '1h2m3s', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 3 * MILLIS_PER_SECOND }, - { value: '1h3s', expect: MILLIS_PER_HOUR + 3 * MILLIS_PER_SECOND }, - { value: '1h2m', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE }, - { value: '10h', expect: 10 * MILLIS_PER_HOUR }, - { value: '10m', expect: 10 * MILLIS_PER_MINUTE }, - { value: '10s', expect: 10 * MILLIS_PER_SECOND }, - { value: '120h', expect: 120 * MILLIS_PER_HOUR }, - { value: '120m', expect: 120 * MILLIS_PER_MINUTE }, - { value: '120s', expect: 120 * MILLIS_PER_SECOND }, - ]; - - for (const s of ts) { - it(`handles ${s.value}`, () => { - expect(forgivingStringToMillis(s.value)).toBe(s.expect); - }); - } - }); - - describe('handles am/pm', () => { - const ampm = [ - { value: '1am', expect: 1 * MILLIS_PER_HOUR }, - { value: '1pm', expect: (12 + 1) * MILLIS_PER_HOUR }, - { value: '1 am', expect: 1 * MILLIS_PER_HOUR }, - { value: '1 pm', expect: (12 + 1) * MILLIS_PER_HOUR }, - { value: '1AM', expect: 1 * MILLIS_PER_HOUR }, - { value: '1PM', expect: (12 + 1) * MILLIS_PER_HOUR }, - - { value: '9am', expect: 9 * MILLIS_PER_HOUR }, - { value: '9pm', expect: (12 + 9) * MILLIS_PER_HOUR }, - - { value: '10am', expect: 10 * MILLIS_PER_HOUR }, - { value: '10pm', expect: (12 + 10) * MILLIS_PER_HOUR }, - { value: '10 am', expect: 10 * MILLIS_PER_HOUR }, - { value: '10 pm', expect: (12 + 10) * MILLIS_PER_HOUR }, - { value: '10AM', expect: 10 * MILLIS_PER_HOUR }, - { value: '10PM', expect: (12 + 10) * MILLIS_PER_HOUR }, - - { value: '12am', expect: 0 * MILLIS_PER_HOUR }, - { value: '12pm', expect: 12 * MILLIS_PER_HOUR }, - - { value: '1:10am', expect: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - { value: '1:10pm', expect: (12 + 1) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - { value: '1:10 am', expect: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - { value: '1:10 pm', expect: (12 + 1) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - { value: '1:10AM', expect: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - { value: '1:10PM', expect: (12 + 1) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - - { value: '9:10am', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - { value: '9:10pm', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - - { value: '10:10am', expect: 10 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - { value: '10:10pm', expect: (12 + 10) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - { value: '10:10 am', expect: 10 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - { value: '10:10 pm', expect: (12 + 10) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - { value: '10:10AM', expect: 10 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - { value: '10:10PM', expect: (12 + 10) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - - { value: '12:10am', expect: 0 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - { value: '12:10pm', expect: 12 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - - { value: '1:10:10am', expect: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - { value: '1:10:10pm', expect: (12 + 1) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - { value: '1:10:10 am', expect: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - { value: '1:10:10 pm', expect: (12 + 1) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - { value: '1:10:10AM', expect: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - { value: '1:10:10PM', expect: (12 + 1) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - - { value: '9:10:10am', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - { value: '9:10:10pm', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - - { value: '10:10:10am', expect: 10 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - { value: '10:10:10pm', expect: (12 + 10) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - { value: '10:10:10 am', expect: 10 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - { value: '10:10:10 pm', expect: (12 + 10) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - { value: '10:10:10AM', expect: 10 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - { value: '10:10:10PM', expect: (12 + 10) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - - { value: '12:10:10am', expect: 0 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - { value: '12:10:10pm', expect: 12 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, - ]; - - for (const s of ampm) { - it(`handles ${s.value}`, () => { - expect(forgivingStringToMillis(s.value)).toBe(s.expect); - }); - } - }); - - describe('it infers separators when non existent', () => { - const testCases = [ - { value: '1', expect: MILLIS_PER_MINUTE }, // 00:01:00 - { value: '12', expect: 12 * MILLIS_PER_MINUTE }, // 00:12:00 - { value: '123', expect: MILLIS_PER_HOUR + 23 * MILLIS_PER_MINUTE }, // 01:23:00 - { value: '1234', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE }, // 12:34:00 - { value: '12345', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE + 5 * MILLIS_PER_SECOND }, // 12:34:05 - { value: '123456', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE + 56 * MILLIS_PER_SECOND }, // 12:34:56 - ]; - - for (const s of testCases) { - it(`handles basic strings digits: ${s.value}`, () => { - expect(forgivingStringToMillis(s.value)).toBe(s.expect); - }); - } - - const sixDigits = [ - { value: '000000', expect: 0 }, // 00:00:00 - { value: '000001', expect: MILLIS_PER_SECOND }, // 00:00:01 - { value: '000100', expect: MILLIS_PER_MINUTE }, // 00:01:00 - { value: '010000', expect: MILLIS_PER_HOUR }, // 01:00:00 - { value: '230000', expect: MILLIS_PER_HOUR * 23 }, // 23:00:00 - { value: '121212', expect: 12 * MILLIS_PER_HOUR + 12 * MILLIS_PER_MINUTE + 12 * MILLIS_PER_SECOND }, // 12:12:12 - ]; - - for (const s of sixDigits) { - it(`handles string with 6 digits: ${s.value}`, () => { - expect(forgivingStringToMillis(s.value)).toBe(s.expect); - }); - } - - const fiveDigits = [ - { value: '00000', expect: 0 }, - { value: '00001', expect: MILLIS_PER_SECOND }, // 00:00:01 - { value: '00010', expect: MILLIS_PER_MINUTE }, // 00:01:00 - { value: '00100', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00 - { value: '01000', expect: MILLIS_PER_HOUR }, // 01:00:00 - { value: '10000', expect: 10 * MILLIS_PER_HOUR }, // 10:00:00 - { value: '23000', expect: 23 * MILLIS_PER_HOUR }, // 23:00:00 - { value: '12121', expect: 12 * MILLIS_PER_HOUR + 12 * MILLIS_PER_MINUTE + 1 * MILLIS_PER_SECOND }, // 12:12:01 - ]; - - for (const s of fiveDigits) { - it(`handles string with 5 digits: ${s.value}`, () => { - expect(forgivingStringToMillis(s.value)).toBe(s.expect); - }); - } - - const fourDigits = [ - { value: '0000', expect: 0 }, - { value: '0001', expect: MILLIS_PER_MINUTE }, // 00:01:00 - { value: '0010', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00 - { value: '0100', expect: MILLIS_PER_HOUR }, // 01:00:00 - { value: '1000', expect: 10 * MILLIS_PER_HOUR }, // 10:00:00 - { value: '2300', expect: 23 * MILLIS_PER_HOUR }, // 23:00:00 - { value: '1212', expect: 12 * MILLIS_PER_HOUR + 12 * MILLIS_PER_MINUTE }, // 12:12:00 - ]; - - for (const s of fourDigits) { - it(`handles string with 4 digits: ${s.value}`, () => { - expect(forgivingStringToMillis(s.value)).toBe(s.expect); - }); - } - - const threeDigits = [ - { value: '000', expect: 0 }, - { value: '001', expect: MILLIS_PER_MINUTE }, // 00:01:00 - { value: '010', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00 - { value: '100', expect: MILLIS_PER_HOUR }, // 01:00:00 - { value: '230', expect: 2 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE }, // 02:30:00 - { value: '121', expect: MILLIS_PER_HOUR + 21 * MILLIS_PER_MINUTE }, // 01:21:00 - ]; - - for (const s of threeDigits) { - it(`handles string with 3 digits: ${s.value}`, () => { - expect(forgivingStringToMillis(s.value)).toBe(s.expect); - }); - } - - const twoDigits = [ - { value: '00', expect: 0 }, - { value: '01', expect: MILLIS_PER_MINUTE }, // 00:01:00 - { value: '10', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00 - { value: '23', expect: 23 * MILLIS_PER_MINUTE }, // 00:23:00 - ]; - - for (const s of twoDigits) { - it(`handles string with 2 digits: ${s.value}`, () => { - expect(forgivingStringToMillis(s.value)).toBe(s.expect); - }); - } - - const singleDigit = [...Array(10).keys()]; - - for (const s of singleDigit) { - it(`handles string with a single digits ${s}`, () => { - expect(forgivingStringToMillis(`${s}`)).toBe(s * 1000 * 60); - }); - } - }); - - describe('handles overflows', () => { - const ts = [ - // minutes overflow - { value: '99', expect: 99 * MILLIS_PER_MINUTE }, - { value: '1.39.0', expect: 99 * MILLIS_PER_MINUTE }, - // seconds overflow - { value: '0.0.120', expect: 120 * MILLIS_PER_SECOND }, - { value: '0.2.0', expect: 120 * MILLIS_PER_SECOND }, - { value: '0.0.99', expect: 99 * MILLIS_PER_SECOND }, - { value: '0.1.39', expect: 99 * MILLIS_PER_SECOND }, - // hours overflow - { value: '25.0.0', expect: 25 * MILLIS_PER_HOUR }, - // hours overflow - { value: '50.0.0', expect: 50 * MILLIS_PER_HOUR }, - ]; - - for (const s of ts) { - it(`handles ${s.value} to the left`, () => { - expect(forgivingStringToMillis(s.value)).toBe(s.expect); - }); - } - }); - - describe('test fillLeft', () => { - describe('function handles separators', () => { - const testData = [ - { value: '1:2:3:10', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 3 * MILLIS_PER_SECOND }, // 01:02:03 - { value: '2,10', expect: 2 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - { value: '2.10', expect: 2 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - { value: '2 10', expect: 2 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, - ]; - - for (const s of testData) { - it(`handles ${s.value}`, () => { - expect(typeof forgivingStringToMillis(s.value)).toBe('number'); - expect(forgivingStringToMillis(s.value)).toBe(s.expect); - }); - } - }); - - describe('parses strings correctly', () => { - const ts = [ - { value: '1.2', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE }, - { value: '1.70', expect: MILLIS_PER_HOUR + 70 * MILLIS_PER_MINUTE }, - ]; - - for (const s of ts) { - it(`handles ${s.value}`, () => { - expect(forgivingStringToMillis(s.value)).toBe(s.expect); - }); - } - }); - - describe('handles overflows', () => { - const ts = [ - // minutes overflow - { value: '0.120', expect: 120 * MILLIS_PER_MINUTE }, - { value: '0.99', expect: 99 * MILLIS_PER_MINUTE }, - ]; - - for (const s of ts) { - it(`handles ${s.value}`, () => { - expect(forgivingStringToMillis(s.value)).toBe(s.expect); - }); - } - }); - }); -}); +import { millisToDelayString } from '../dateConfig'; describe('millisToDelayString()', () => { it('returns null for null values', () => { diff --git a/apps/client/src/common/utils/dateConfig.ts b/apps/client/src/common/utils/dateConfig.ts index 3e027e2da..132e5bc95 100644 --- a/apps/client/src/common/utils/dateConfig.ts +++ b/apps/client/src/common/utils/dateConfig.ts @@ -1,179 +1,11 @@ import { MaybeNumber } from 'ontime-types'; -import { formatFromMillis, MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils'; +import { formatFromMillis, MILLIS_PER_HOUR, MILLIS_PER_MINUTE } from 'ontime-utils'; /** - * @description safe parse string to int - * @param {string} valueAsString - * @return {number} + * Parses a value in millis to a string which encodes a delay + * @param millis + * @param format */ -const parse = (valueAsString: string): number => { - const parsed = parseInt(valueAsString, 10); - if (isNaN(parsed)) { - return 0; - } - return Math.abs(parsed); -}; - -/** - * @description Utility function to check if a string contain am/pm indicators - * @param {string} value - */ -function checkAmPm(value: string) { - let isPM = false; - let isAM = false; - if (value.toLowerCase().includes('pm')) { - isPM = true; - value = value.replace(/pm/i, ''); - } else if (value.toLowerCase().includes('p')) { - isPM = true; - value = value.replace(/p/i, ''); - } - - // we need to remove am, but it doesn't actually change anything - if (value.toLowerCase().includes('am')) { - isAM = true; - value = value.replace(/am/i, ''); - } else if (value.toLowerCase().includes('a')) { - isAM = true; - value = value.replace(/a/i, ''); - } - - return { isAM, isPM, value }; -} - -/** - * @description Utility function to check if a string contain h / m / s indicators - * @param {string} value - */ -function checkMatchers(value: string): number | null { - const hoursMatch = /(\d+)h/i.exec(value); - const hoursMatchValue = hoursMatch ? parse(hoursMatch[1]) : 0; - - const minutesMatch = /(\d+)m/i.exec(value); - const minutesMatchValue = minutesMatch ? parse(minutesMatch[1]) : 0; - - const secondsMatch = /(\d+)s/i.exec(value); - const secondsMatchValue = secondsMatch ? parse(secondsMatch[1]) : 0; - - if (hoursMatchValue > 0 || minutesMatchValue > 0 || secondsMatchValue > 0) { - return ( - hoursMatchValue * MILLIS_PER_HOUR + minutesMatchValue * MILLIS_PER_MINUTE + secondsMatchValue * MILLIS_PER_SECOND - ); - } - return null; -} - -/** - * @description Utility function to infer separators from a whole string - * @param {string} value - * @param {boolean} isAM - * @param {boolean} isPM - */ -function inferSeparators(value: string, isAM: boolean, isPM: boolean) { - const length = value.length; - let inferredMillis = 0; - let addAM = 0; - if (length === 1) { - if (isPM || isAM) { - inferredMillis = parse(value) * MILLIS_PER_HOUR; - if (isAM) { - // this ensures we dont add 12 hours in the end - addAM = inferredMillis; - } - } else { - inferredMillis = parse(value) * MILLIS_PER_MINUTE; - } - } else if (length === 2) { - if (isPM || isAM) { - if (value === '12' && isAM) { - inferredMillis = 0; - } else { - inferredMillis = parse(value) * MILLIS_PER_HOUR; - } - - if (isAM || value === '12') { - // this ensures we dont add 12 hours in the end - addAM = 12; - } - } else { - inferredMillis = parse(value) * MILLIS_PER_MINUTE; - } - } else if (length === 3) { - inferredMillis = parse(value[0]) * MILLIS_PER_HOUR + parse(value.substring(1)) * MILLIS_PER_MINUTE; - } else if (length === 4) { - inferredMillis = parse(value.substring(0, 2)) * MILLIS_PER_HOUR + parse(value.substring(2)) * MILLIS_PER_MINUTE; - } else if (length >= 5) { - const hours = parse(value.substring(0, 2)); - const minutes = parse(value.substring(2, 4)); - const seconds = parse(value.substring(4)); - inferredMillis = hours * MILLIS_PER_HOUR + minutes * MILLIS_PER_MINUTE + seconds * MILLIS_PER_SECOND; - } - return { inferredMillis, addAM }; -} - -/** - * @description Parses a time string to millis, auto-filling to the left - * @param {string} value - time string - * @returns {number} - time string in millis - */ -export const forgivingStringToMillis = (value: string): number => { - value = value.toLowerCase(); - - const { isAM, isPM, value: parsingValue } = checkAmPm(value); - const maybeMillisFromMatchers = checkMatchers(parsingValue); - if (maybeMillisFromMatchers !== null) { - return maybeMillisFromMatchers; - } - - // split string at known separators : , . - const separatorRegex = /[\s,:.]+/; - const [first, second, third] = parsingValue.split(separatorRegex); - - let addTwelve = isPM; - - let millis = 0; - - if (first != null && second != null && third != null) { - // if string has three sections, treat as [hours] [minutes] [seconds] - let hours = parse(first); - if (hours === 12) { - if (isAM) { - hours = 0; - } - addTwelve = false; - } - - millis = hours * MILLIS_PER_HOUR; - millis += parse(second) * MILLIS_PER_MINUTE; - millis += parse(third) * MILLIS_PER_SECOND; - } else if (first != null && second == null && third == null) { - // we only have one section, infer separators - const { inferredMillis, addAM } = inferSeparators(first, isAM, isPM); - millis = inferredMillis; - addTwelve = addAM < 12 && isPM; - } - if (first != null && second != null && third == null) { - // if string has two sections, treat as [hours] [minutes] - let hours = parse(first); - if (hours === 12) { - if (isAM) { - hours = 0; - } - addTwelve = false; - } - - millis = hours * MILLIS_PER_HOUR; - millis += parse(second) * MILLIS_PER_MINUTE; - } - - // Add 12 hours if needed - if (addTwelve) { - millis += 12 * MILLIS_PER_HOUR; - } - - return millis; -}; - export function millisToDelayString(millis: MaybeNumber, format: 'compact' | 'expanded' = 'compact'): string { if (millis == null || millis === 0) { return ''; diff --git a/apps/client/src/features/app-settings/panel/interface-panel/EditorSettingsForm.tsx b/apps/client/src/features/app-settings/panel/interface-panel/EditorSettingsForm.tsx index 398a812b5..b673d97c0 100644 --- a/apps/client/src/features/app-settings/panel/interface-panel/EditorSettingsForm.tsx +++ b/apps/client/src/features/app-settings/panel/interface-panel/EditorSettingsForm.tsx @@ -1,9 +1,9 @@ import { Select, Switch } from '@chakra-ui/react'; import { EndAction, TimerType } from 'ontime-types'; +import { parseUserTime } from 'ontime-utils'; import TimeInput from '../../../../common/components/input/time-input/TimeInput'; import { editorSettingsDefaults, useEditorSettings } from '../../../../common/stores/editorSettings'; -import { forgivingStringToMillis } from '../../../../common/utils/dateConfig'; import * as Panel from '../PanelUtils'; export default function EditorSettingsForm() { @@ -15,9 +15,9 @@ export default function EditorSettingsForm() { const setDangerTime = eventSettings.setDangerTime; const setDefaultPublic = eventSettings.setDefaultPublic; - const durationInMs = forgivingStringToMillis(eventSettings.defaultDuration); - const warnTimeInMs = forgivingStringToMillis(eventSettings.defaultWarnTime); - const dangerTimeInMs = forgivingStringToMillis(eventSettings.defaultDangerTime); + const durationInMs = parseUserTime(eventSettings.defaultDuration); + const warnTimeInMs = parseUserTime(eventSettings.defaultWarnTime); + const dangerTimeInMs = parseUserTime(eventSettings.defaultDangerTime); return ( diff --git a/apps/client/src/features/control/playback/add-time/AddTime.tsx b/apps/client/src/features/control/playback/add-time/AddTime.tsx index f2987b29b..d497aa71c 100644 --- a/apps/client/src/features/control/playback/add-time/AddTime.tsx +++ b/apps/client/src/features/control/playback/add-time/AddTime.tsx @@ -2,12 +2,11 @@ import { Tooltip } from '@chakra-ui/react'; import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; import { IoRemove } from '@react-icons/all-files/io5/IoRemove'; import { Playback } from 'ontime-types'; -import { MILLIS_PER_HOUR, MILLIS_PER_SECOND } from 'ontime-utils'; +import { MILLIS_PER_HOUR, MILLIS_PER_SECOND, parseUserTime } from 'ontime-utils'; import TimeInput from '../../../../common/components/input/time-input/TimeInput'; import { useLocalStorage } from '../../../../common/hooks/useLocalStorage'; import { setPlayback } from '../../../../common/hooks/useSocket'; -import { forgivingStringToMillis } from '../../../../common/utils/dateConfig'; import { tooltipDelayMid } from '../../../../ontimeConfig'; import TapButton from '../tap-button/TapButton'; @@ -22,7 +21,7 @@ export default function AddTime(props: AddTimeProps) { const [time, setTime] = useLocalStorage('add-time', 300_000); // 5 minutes const handleTimeChange = (_field: string, value: string) => { - const newTime = forgivingStringToMillis(value); + const newTime = parseUserTime(value); // cap add time to 1 hour setTime(Math.min(newTime, MILLIS_PER_HOUR)); }; diff --git a/apps/client/src/features/control/playback/aux-timer/AuxTimer.tsx b/apps/client/src/features/control/playback/aux-timer/AuxTimer.tsx index 9e5573d85..f0f40bacf 100644 --- a/apps/client/src/features/control/playback/aux-timer/AuxTimer.tsx +++ b/apps/client/src/features/control/playback/aux-timer/AuxTimer.tsx @@ -4,10 +4,10 @@ import { IoPause } from '@react-icons/all-files/io5/IoPause'; import { IoPlay } from '@react-icons/all-files/io5/IoPlay'; import { IoStop } from '@react-icons/all-files/io5/IoStop'; import { Playback, SimpleDirection, SimplePlayback } from 'ontime-types'; +import { parseUserTime } from 'ontime-utils'; import TimeInput from '../../../../common/components/input/time-input/TimeInput'; import { setAuxTimer, useAuxTimerControl, useAuxTimerTime } from '../../../../common/hooks/useSocket'; -import { forgivingStringToMillis } from '../../../../common/utils/dateConfig'; import TapButton from '../tap-button/TapButton'; import style from './AuxTimer.module.scss'; @@ -64,7 +64,7 @@ function AuxTimerInput() { const { setDuration } = setAuxTimer; const handleTimeUpdate = (_field: string, value: string) => { - const newTime = forgivingStringToMillis(value); + const newTime = parseUserTime(value); setDuration(newTime / 1000); //frontend api is seconds based; }; diff --git a/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx b/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx index 0d7fa853d..147450dcb 100644 --- a/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx +++ b/apps/client/src/features/rundown/event-editor/composite/EventEditorTimes.tsx @@ -1,11 +1,11 @@ import { memo } from 'react'; import { Select, Switch } from '@chakra-ui/react'; import { EndAction, MaybeString, TimerType, TimeStrategy } from 'ontime-types'; -import { millisToString } from 'ontime-utils'; +import { millisToString, parseUserTime } from 'ontime-utils'; import TimeInput from '../../../../common/components/input/time-input/TimeInput'; import { useEventAction } from '../../../../common/hooks/useEventAction'; -import { forgivingStringToMillis, millisToDelayString } from '../../../../common/utils/dateConfig'; +import { millisToDelayString } from '../../../../common/utils/dateConfig'; import TimeInputFlow from '../../time-input-flow/TimeInputFlow'; import style from '../EventEditor.module.scss'; @@ -51,7 +51,7 @@ const EventEditorTimes = (props: EventEditorTimesProps) => { } if (field === 'timeWarning' || field === 'timeDanger') { - const newTime = forgivingStringToMillis(value as string); + const newTime = parseUserTime(value as string); updateEvent({ id: eventId, [field]: newTime }); return; } diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 8296b10b2..ee8eb68ad 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -45,6 +45,7 @@ export { removeSeconds, removeTrailingZero, } from './src/date-utils/timeFormatting.js'; +export { parseUserTime } from './src/date-utils/parseUserTime.js'; export { isAlphanumeric } from './src/regex-utils/isAlphanumeric.js'; export { isColourHex } from './src/regex-utils/isColourHex.js'; diff --git a/packages/utils/src/date-utils/parseUserTime.test.ts b/packages/utils/src/date-utils/parseUserTime.test.ts new file mode 100644 index 000000000..76bd85e75 --- /dev/null +++ b/packages/utils/src/date-utils/parseUserTime.test.ts @@ -0,0 +1,333 @@ +import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from "./conversionUtils"; +import { parseUserTime } from "./parseUserTime"; + +describe('test parseUserTime()', () => { + describe('function handles time with no separators', () => { + const testData = [ + { value: '', expect: 0 }, + { value: '0', expect: 0 }, + { value: '-0', expect: 0 }, + { value: '1', expect: MILLIS_PER_MINUTE }, + { value: '-1', expect: MILLIS_PER_MINUTE }, + { value: '0h0m0s', expect: 0 }, + { value: '0h0m1s', expect: MILLIS_PER_SECOND }, + { value: '0h1m0s', expect: MILLIS_PER_MINUTE }, + { value: '1h0m0s', expect: MILLIS_PER_HOUR }, + { value: '23h0m0s', expect: 23 * MILLIS_PER_HOUR }, + { value: '12h12m12s', expect: 12 * MILLIS_PER_SECOND + 12 * MILLIS_PER_MINUTE + 12 * MILLIS_PER_HOUR }, + { value: '12H12M12S', expect: 12 * MILLIS_PER_SECOND + 12 * MILLIS_PER_MINUTE + 12 * MILLIS_PER_HOUR }, + { value: '2m', expect: 2 * MILLIS_PER_MINUTE }, + { value: '1h5s', expect: MILLIS_PER_HOUR + 5 * MILLIS_PER_SECOND }, + { value: '1h2m', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE }, + ]; + + for (const s of testData) { + it(`handles ${s.value}`, () => { + expect(typeof parseUserTime(s.value)).toBe('number'); + expect(parseUserTime(s.value)).toBe(s.expect); + }); + } + }); + + describe('parses strings correctly', () => { + const ts = [ + { value: '1.1.1', expect: MILLIS_PER_HOUR + MILLIS_PER_MINUTE + MILLIS_PER_SECOND }, + { value: '12.1.1', expect: 12 * MILLIS_PER_HOUR + MILLIS_PER_MINUTE + MILLIS_PER_SECOND }, + { value: '12.55.1', expect: 12 * MILLIS_PER_HOUR + 55 * MILLIS_PER_MINUTE + MILLIS_PER_SECOND }, + { value: '12.55.40', expect: 12 * MILLIS_PER_HOUR + 55 * MILLIS_PER_MINUTE + 40 * MILLIS_PER_SECOND }, + ]; + + for (const s of ts) { + it(`handles ${s.value} to the left`, () => { + expect(parseUserTime(s.value)).toBe(s.expect); + }); + } + }); + + describe('#33 separators are parsed according to doc examples ', () => { + const ts = [ + { value: '0.1', expect: MILLIS_PER_MINUTE }, + { value: '0 1', expect: MILLIS_PER_MINUTE }, + { value: '0:1', expect: MILLIS_PER_MINUTE }, + { value: '0,1', expect: MILLIS_PER_MINUTE }, + { value: '2.2.2', expect: 2 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 2 * MILLIS_PER_SECOND }, + { value: '2 2 2', expect: 2 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 2 * MILLIS_PER_SECOND }, + { value: '2:2:2', expect: 2 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 2 * MILLIS_PER_SECOND }, + { value: '2,2,2', expect: 2 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 2 * MILLIS_PER_SECOND }, + { value: '2,2,2', expect: 2 * MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 2 * MILLIS_PER_SECOND }, + { value: '10:', expect: 10 * MILLIS_PER_HOUR }, + { value: ':10', expect: 10 * MILLIS_PER_MINUTE }, + { value: '10', expect: 10 * MILLIS_PER_MINUTE }, + { value: '120', expect: MILLIS_PER_HOUR + 20 * MILLIS_PER_MINUTE }, + { value: '90m', expect: 90 * MILLIS_PER_MINUTE }, + { value: '1.2', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE }, + { value: '1.2.3', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 3 * MILLIS_PER_SECOND }, + { value: '123456', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE + 56 * MILLIS_PER_SECOND }, + ]; + + for (const s of ts) { + it(`handles ${s.value}`, () => { + expect(parseUserTime(s.value)).toBe(s.expect); + }); + } + }); + + describe('parses time strings', () => { + const ts = [ + { value: '1h2m3s', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 3 * MILLIS_PER_SECOND }, + { value: '1h3s', expect: MILLIS_PER_HOUR + 3 * MILLIS_PER_SECOND }, + { value: '1h2m', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE }, + { value: '10h', expect: 10 * MILLIS_PER_HOUR }, + { value: '10m', expect: 10 * MILLIS_PER_MINUTE }, + { value: '10s', expect: 10 * MILLIS_PER_SECOND }, + { value: '120h', expect: 120 * MILLIS_PER_HOUR }, + { value: '120m', expect: 120 * MILLIS_PER_MINUTE }, + { value: '120s', expect: 120 * MILLIS_PER_SECOND }, + ]; + + for (const s of ts) { + it(`handles ${s.value}`, () => { + expect(parseUserTime(s.value)).toBe(s.expect); + }); + } + }); + + describe('handles am/pm', () => { + const ampm = [ + { value: '1am', expect: 1 * MILLIS_PER_HOUR }, + { value: '1pm', expect: (12 + 1) * MILLIS_PER_HOUR }, + { value: '1 am', expect: 1 * MILLIS_PER_HOUR }, + { value: '1 pm', expect: (12 + 1) * MILLIS_PER_HOUR }, + { value: '1AM', expect: 1 * MILLIS_PER_HOUR }, + { value: '1PM', expect: (12 + 1) * MILLIS_PER_HOUR }, + + { value: '9am', expect: 9 * MILLIS_PER_HOUR }, + { value: '9pm', expect: (12 + 9) * MILLIS_PER_HOUR }, + + { value: '10am', expect: 10 * MILLIS_PER_HOUR }, + { value: '10pm', expect: (12 + 10) * MILLIS_PER_HOUR }, + { value: '10 am', expect: 10 * MILLIS_PER_HOUR }, + { value: '10 pm', expect: (12 + 10) * MILLIS_PER_HOUR }, + { value: '10AM', expect: 10 * MILLIS_PER_HOUR }, + { value: '10PM', expect: (12 + 10) * MILLIS_PER_HOUR }, + + { value: '12am', expect: 0 * MILLIS_PER_HOUR }, + { value: '12pm', expect: 12 * MILLIS_PER_HOUR }, + + { value: '1:10am', expect: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + { value: '1:10pm', expect: (12 + 1) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + { value: '1:10 am', expect: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + { value: '1:10 pm', expect: (12 + 1) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + { value: '1:10AM', expect: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + { value: '1:10PM', expect: (12 + 1) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + + { value: '9:10am', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + { value: '9:10pm', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + + { value: '10:10am', expect: 10 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + { value: '10:10pm', expect: (12 + 10) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + { value: '10:10 am', expect: 10 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + { value: '10:10 pm', expect: (12 + 10) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + { value: '10:10AM', expect: 10 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + { value: '10:10PM', expect: (12 + 10) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + + { value: '12:10am', expect: 0 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + { value: '12:10pm', expect: 12 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + + { value: '1:10:10am', expect: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + { value: '1:10:10pm', expect: (12 + 1) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + { value: '1:10:10 am', expect: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + { value: '1:10:10 pm', expect: (12 + 1) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + { value: '1:10:10AM', expect: 1 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + { value: '1:10:10PM', expect: (12 + 1) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + + { value: '9:10:10am', expect: 9 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + { value: '9:10:10pm', expect: (12 + 9) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + + { value: '10:10:10am', expect: 10 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + { value: '10:10:10pm', expect: (12 + 10) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + { value: '10:10:10 am', expect: 10 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + { value: '10:10:10 pm', expect: (12 + 10) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + { value: '10:10:10AM', expect: 10 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + { value: '10:10:10PM', expect: (12 + 10) * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + + { value: '12:10:10am', expect: 0 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + { value: '12:10:10pm', expect: 12 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE + 10 * MILLIS_PER_SECOND }, + ]; + + for (const s of ampm) { + it(`handles ${s.value}`, () => { + expect(parseUserTime(s.value)).toBe(s.expect); + }); + } + }); + + describe('it infers separators when non existent', () => { + const testCases = [ + { value: '1', expect: MILLIS_PER_MINUTE }, // 00:01:00 + { value: '12', expect: 12 * MILLIS_PER_MINUTE }, // 00:12:00 + { value: '123', expect: MILLIS_PER_HOUR + 23 * MILLIS_PER_MINUTE }, // 01:23:00 + { value: '1234', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE }, // 12:34:00 + { value: '12345', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE + 5 * MILLIS_PER_SECOND }, // 12:34:05 + { value: '123456', expect: 12 * MILLIS_PER_HOUR + 34 * MILLIS_PER_MINUTE + 56 * MILLIS_PER_SECOND }, // 12:34:56 + ]; + + for (const s of testCases) { + it(`handles basic strings digits: ${s.value}`, () => { + expect(parseUserTime(s.value)).toBe(s.expect); + }); + } + + const sixDigits = [ + { value: '000000', expect: 0 }, // 00:00:00 + { value: '000001', expect: MILLIS_PER_SECOND }, // 00:00:01 + { value: '000100', expect: MILLIS_PER_MINUTE }, // 00:01:00 + { value: '010000', expect: MILLIS_PER_HOUR }, // 01:00:00 + { value: '230000', expect: MILLIS_PER_HOUR * 23 }, // 23:00:00 + { value: '121212', expect: 12 * MILLIS_PER_HOUR + 12 * MILLIS_PER_MINUTE + 12 * MILLIS_PER_SECOND }, // 12:12:12 + ]; + + for (const s of sixDigits) { + it(`handles string with 6 digits: ${s.value}`, () => { + expect(parseUserTime(s.value)).toBe(s.expect); + }); + } + + const fiveDigits = [ + { value: '00000', expect: 0 }, + { value: '00001', expect: MILLIS_PER_SECOND }, // 00:00:01 + { value: '00010', expect: MILLIS_PER_MINUTE }, // 00:01:00 + { value: '00100', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00 + { value: '01000', expect: MILLIS_PER_HOUR }, // 01:00:00 + { value: '10000', expect: 10 * MILLIS_PER_HOUR }, // 10:00:00 + { value: '23000', expect: 23 * MILLIS_PER_HOUR }, // 23:00:00 + { value: '12121', expect: 12 * MILLIS_PER_HOUR + 12 * MILLIS_PER_MINUTE + 1 * MILLIS_PER_SECOND }, // 12:12:01 + ]; + + for (const s of fiveDigits) { + it(`handles string with 5 digits: ${s.value}`, () => { + expect(parseUserTime(s.value)).toBe(s.expect); + }); + } + + const fourDigits = [ + { value: '0000', expect: 0 }, + { value: '0001', expect: MILLIS_PER_MINUTE }, // 00:01:00 + { value: '0010', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00 + { value: '0100', expect: MILLIS_PER_HOUR }, // 01:00:00 + { value: '1000', expect: 10 * MILLIS_PER_HOUR }, // 10:00:00 + { value: '2300', expect: 23 * MILLIS_PER_HOUR }, // 23:00:00 + { value: '1212', expect: 12 * MILLIS_PER_HOUR + 12 * MILLIS_PER_MINUTE }, // 12:12:00 + ]; + + for (const s of fourDigits) { + it(`handles string with 4 digits: ${s.value}`, () => { + expect(parseUserTime(s.value)).toBe(s.expect); + }); + } + + const threeDigits = [ + { value: '000', expect: 0 }, + { value: '001', expect: MILLIS_PER_MINUTE }, // 00:01:00 + { value: '010', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00 + { value: '100', expect: MILLIS_PER_HOUR }, // 01:00:00 + { value: '230', expect: 2 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE }, // 02:30:00 + { value: '121', expect: MILLIS_PER_HOUR + 21 * MILLIS_PER_MINUTE }, // 01:21:00 + ]; + + for (const s of threeDigits) { + it(`handles string with 3 digits: ${s.value}`, () => { + expect(parseUserTime(s.value)).toBe(s.expect); + }); + } + + const twoDigits = [ + { value: '00', expect: 0 }, + { value: '01', expect: MILLIS_PER_MINUTE }, // 00:01:00 + { value: '10', expect: 10 * MILLIS_PER_MINUTE }, // 00:10:00 + { value: '23', expect: 23 * MILLIS_PER_MINUTE }, // 00:23:00 + ]; + + for (const s of twoDigits) { + it(`handles string with 2 digits: ${s.value}`, () => { + expect(parseUserTime(s.value)).toBe(s.expect); + }); + } + + const singleDigit = [...Array(10).keys()]; + + for (const s of singleDigit) { + it(`handles string with a single digits ${s}`, () => { + expect(parseUserTime(`${s}`)).toBe(s * 1000 * 60); + }); + } + }); + + describe('handles overflows', () => { + const ts = [ + // minutes overflow + { value: '99', expect: 99 * MILLIS_PER_MINUTE }, + { value: '1.39.0', expect: 99 * MILLIS_PER_MINUTE }, + // seconds overflow + { value: '0.0.120', expect: 120 * MILLIS_PER_SECOND }, + { value: '0.2.0', expect: 120 * MILLIS_PER_SECOND }, + { value: '0.0.99', expect: 99 * MILLIS_PER_SECOND }, + { value: '0.1.39', expect: 99 * MILLIS_PER_SECOND }, + // hours overflow + { value: '25.0.0', expect: 25 * MILLIS_PER_HOUR }, + // hours overflow + { value: '50.0.0', expect: 50 * MILLIS_PER_HOUR }, + ]; + + for (const s of ts) { + it(`handles ${s.value} to the left`, () => { + expect(parseUserTime(s.value)).toBe(s.expect); + }); + } + }); + + describe('test fillLeft', () => { + describe('function handles separators', () => { + const testData = [ + { value: '1:2:3:10', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE + 3 * MILLIS_PER_SECOND }, // 01:02:03 + { value: '2,10', expect: 2 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + { value: '2.10', expect: 2 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + { value: '2 10', expect: 2 * MILLIS_PER_HOUR + 10 * MILLIS_PER_MINUTE }, + ]; + + for (const s of testData) { + it(`handles ${s.value}`, () => { + expect(typeof parseUserTime(s.value)).toBe('number'); + expect(parseUserTime(s.value)).toBe(s.expect); + }); + } + }); + + describe('parses strings correctly', () => { + const ts = [ + { value: '1.2', expect: MILLIS_PER_HOUR + 2 * MILLIS_PER_MINUTE }, + { value: '1.70', expect: MILLIS_PER_HOUR + 70 * MILLIS_PER_MINUTE }, + ]; + + for (const s of ts) { + it(`handles ${s.value}`, () => { + expect(parseUserTime(s.value)).toBe(s.expect); + }); + } + }); + + describe('handles overflows', () => { + const ts = [ + // minutes overflow + { value: '0.120', expect: 120 * MILLIS_PER_MINUTE }, + { value: '0.99', expect: 99 * MILLIS_PER_MINUTE }, + ]; + + for (const s of ts) { + it(`handles ${s.value}`, () => { + expect(parseUserTime(s.value)).toBe(s.expect); + }); + } + }); + }); +}); \ No newline at end of file diff --git a/packages/utils/src/date-utils/parseUserTime.ts b/packages/utils/src/date-utils/parseUserTime.ts new file mode 100644 index 000000000..81aa1b4d9 --- /dev/null +++ b/packages/utils/src/date-utils/parseUserTime.ts @@ -0,0 +1,174 @@ +import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from './conversionUtils.js'; + +/** + * @description Parses a time string to millis, auto-filling to the left + * @param {string} value - time string + * @returns {number} - time string in millis + */ +export function parseUserTime(value: string): number { + value = value.toLowerCase(); + + const { isAM, isPM, value: parsingValue } = checkAmPm(value); + const maybeMillisFromMatchers = checkMatchers(parsingValue); + if (maybeMillisFromMatchers !== null) { + return maybeMillisFromMatchers; + } + + // split string at known separators : , . + const separatorRegex = /[\s,:.]+/; + const [first, second, third] = parsingValue.split(separatorRegex); + + let addTwelve = isPM; + + let millis = 0; + + if (first != null && second != null && third != null) { + // if string has three sections, treat as [hours] [minutes] [seconds] + let hours = parse(first); + if (hours === 12) { + if (isAM) { + hours = 0; + } + addTwelve = false; + } + + millis = hours * MILLIS_PER_HOUR; + millis += parse(second) * MILLIS_PER_MINUTE; + millis += parse(third) * MILLIS_PER_SECOND; + } else if (first != null && second == null && third == null) { + // we only have one section, infer separators + const { inferredMillis, addAM } = inferSeparators(first, isAM, isPM); + millis = inferredMillis; + addTwelve = addAM < 12 && isPM; + } + if (first != null && second != null && third == null) { + // if string has two sections, treat as [hours] [minutes] + let hours = parse(first); + if (hours === 12) { + if (isAM) { + hours = 0; + } + addTwelve = false; + } + + millis = hours * MILLIS_PER_HOUR; + millis += parse(second) * MILLIS_PER_MINUTE; + } + + // Add 12 hours if needed + if (addTwelve) { + millis += 12 * MILLIS_PER_HOUR; + } + + return millis; +} + +/** + * @description Utility function to infer separators from a whole string + * @param {string} value + * @param {boolean} isAM + * @param {boolean} isPM + */ +function inferSeparators(value: string, isAM: boolean, isPM: boolean) { + const length = value.length; + let inferredMillis = 0; + let addAM = 0; + if (length === 1) { + if (isPM || isAM) { + inferredMillis = parse(value) * MILLIS_PER_HOUR; + if (isAM) { + // this ensures we dont add 12 hours in the end + addAM = inferredMillis; + } + } else { + inferredMillis = parse(value) * MILLIS_PER_MINUTE; + } + } else if (length === 2) { + if (isPM || isAM) { + if (value === '12' && isAM) { + inferredMillis = 0; + } else { + inferredMillis = parse(value) * MILLIS_PER_HOUR; + } + + if (isAM || value === '12') { + // this ensures we dont add 12 hours in the end + addAM = 12; + } + } else { + inferredMillis = parse(value) * MILLIS_PER_MINUTE; + } + } else if (length === 3) { + inferredMillis = parse(value[0]) * MILLIS_PER_HOUR + parse(value.substring(1)) * MILLIS_PER_MINUTE; + } else if (length === 4) { + inferredMillis = parse(value.substring(0, 2)) * MILLIS_PER_HOUR + parse(value.substring(2)) * MILLIS_PER_MINUTE; + } else if (length >= 5) { + const hours = parse(value.substring(0, 2)); + const minutes = parse(value.substring(2, 4)); + const seconds = parse(value.substring(4)); + inferredMillis = hours * MILLIS_PER_HOUR + minutes * MILLIS_PER_MINUTE + seconds * MILLIS_PER_SECOND; + } + return { inferredMillis, addAM }; +} + +/** + * @description Utility function to check if a string contain h / m / s indicators + * @param {string} value + */ +function checkMatchers(value: string): number | null { + const hoursMatch = /(\d+)h/i.exec(value); + const hoursMatchValue = hoursMatch ? parse(hoursMatch[1]) : 0; + + const minutesMatch = /(\d+)m/i.exec(value); + const minutesMatchValue = minutesMatch ? parse(minutesMatch[1]) : 0; + + const secondsMatch = /(\d+)s/i.exec(value); + const secondsMatchValue = secondsMatch ? parse(secondsMatch[1]) : 0; + + if (hoursMatchValue > 0 || minutesMatchValue > 0 || secondsMatchValue > 0) { + return ( + hoursMatchValue * MILLIS_PER_HOUR + minutesMatchValue * MILLIS_PER_MINUTE + secondsMatchValue * MILLIS_PER_SECOND + ); + } + return null; +} + +/** + * @description Utility function to check if a string contain am/pm indicators + * @param {string} value + */ +function checkAmPm(value: string) { + let isPM = false; + let isAM = false; + if (value.toLowerCase().includes('pm')) { + isPM = true; + value = value.replace(/pm/i, ''); + } else if (value.toLowerCase().includes('p')) { + isPM = true; + value = value.replace(/p/i, ''); + } + + // we need to remove am, but it doesn't actually change anything + if (value.toLowerCase().includes('am')) { + isAM = true; + value = value.replace(/am/i, ''); + } else if (value.toLowerCase().includes('a')) { + isAM = true; + value = value.replace(/a/i, ''); + } + + return { isAM, isPM, value }; +} + +/** + * @description safe parse string to int + * @param {string} valueAsString + * @return {number} + */ +function parse(valueAsString: string): number { + const parsed = parseInt(valueAsString, 10); + if (isNaN(parsed)) { + return 0; + } + return Math.abs(parsed); +}