From 4ca9fb9f72cd4254c884d5aca29d511a6c0ddb67 Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Fri, 21 Apr 2023 09:20:34 +0200 Subject: [PATCH] refactor: parse time input (#351) --- .../common/utils/__tests__/dateConfig.test.js | 51 +++++- apps/client/src/common/utils/dateConfig.ts | 170 +++++++++++++----- 2 files changed, 173 insertions(+), 48 deletions(-) diff --git a/apps/client/src/common/utils/__tests__/dateConfig.test.js b/apps/client/src/common/utils/__tests__/dateConfig.test.js index 4eed5ec8f0..4dbee84cc8 100644 --- a/apps/client/src/common/utils/__tests__/dateConfig.test.js +++ b/apps/client/src/common/utils/__tests__/dateConfig.test.js @@ -237,8 +237,53 @@ describe('test forgivingStringToMillis()', () => { } }); + describe('parses time strings', () => { + const ts = [ + { value: '1h2m3s', expect: 60 * 60 * 1000 + 2 * 60 * 1000 + 3 * 1000 }, + { value: '1h3s', expect: 60 * 60 * 1000 + 3 * 1000 }, + { value: '1h2m', expect: 60 * 60 * 1000 + 2 * 60 * 1000 }, + { value: '10h', expect: 10 * 60 * 60 * 1000 }, + { value: '10m', expect: 10 * 60 * 1000 }, + { value: '10s', expect: 10 * 1000 }, + { value: '120h', expect: 120 * 60 * 60 * 1000 }, + { value: '120m', expect: 120 * 60 * 1000 }, + { value: '120s', expect: 120 * 1000 }, + ]; + + for (const s of ts) { + it(`handles ${s.value}`, () => { + expect(forgivingStringToMillis(s.value)).toBe(s.expect); + }); + } + }); + + describe('handles am/pm', () => { + const ampm = [ + { value: '9:10:11am', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 }, + { value: '9:10:11a', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 }, + { value: '9:10:11pm', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 }, + { value: '9:10:11p', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 + 11 * 1000 }, + { value: '9:10am', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 }, + { value: '9:10a', expect: 9 * 60 * 60 * 1000 + 10 * 60 * 1000 }, + { value: '9:10pm', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 }, + { value: '9:10p', expect: (12 + 9) * 60 * 60 * 1000 + 10 * 60 * 1000 }, + { value: '9am', expect: 9 * 60 * 60 * 1000 }, + { value: '9a', expect: 9 * 60 * 60 * 1000 }, + { value: '9pm', expect: (12 + 9) * 60 * 60 * 1000 }, + { value: '9p', expect: (12 + 9) * 60 * 60 * 1000 }, + { value: '12am', expect: 0 }, + { value: '12pm', expect: 12 * 60 * 60 * 1000 }, + ]; + + for (const s of ampm) { + it(`handles ${s.value}`, () => { + expect(forgivingStringToMillis(s.value)).toBe(s.expect); + }); + } + }); + describe('it infers separators when non existent', () => { - const docs = [ + const testCases = [ { value: '1', expect: 1000 * 60 }, // 00:01:00 { value: '12', expect: 1000 * 60 * 12 }, // 00:12:00 { value: '123', expect: 1000 * 60 * 23 + 1000 * 60 * 60 }, // 01:23:00 @@ -247,7 +292,7 @@ describe('test forgivingStringToMillis()', () => { { value: '123456', expect: 1000 * 60 * 34 + 1000 * 60 * 60 * 12 + 56 * 1000 }, // 12:34:56 ]; - for (const s of docs) { + for (const s of testCases) { it(`handles basic strings digits: ${s.value}`, () => { expect(forgivingStringToMillis(s.value)).toBe(s.expect); }); @@ -362,7 +407,7 @@ describe('test forgivingStringToMillis()', () => { } }); - describe('test with fillLeft', () => { + describe('test fillLeft', () => { describe('function handles separators', () => { const testData = [ { value: '1:2:3:10', expect: 3723000 }, diff --git a/apps/client/src/common/utils/dateConfig.ts b/apps/client/src/common/utils/dateConfig.ts index 5bf35edfb5..ad3f8c9fee 100644 --- a/apps/client/src/common/utils/dateConfig.ts +++ b/apps/client/src/common/utils/dateConfig.ts @@ -83,64 +83,144 @@ const parse = (valueAsString: string): number => { }; /** - * @description Parses a time string to millis, auto-filling to the left - * @param {string} value - time string - * @returns {number} - time string in millis + * @description Utility function to check if a string contain am/pm indicators + * @param {string} value */ -export const forgivingStringToMillis = (value: string): number => { - let millis = 0; +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) { const hoursMatch = value.match(/(\d+)h/); - const hours = hoursMatch ? parse(hoursMatch[1]) : 0; + const hoursMatchValue = hoursMatch ? parse(hoursMatch[1]) : 0; const minutesMatch = value.match(/(\d+)m/); - const minutes = minutesMatch ? parse(minutesMatch[1]) : 0; + const minutesMatchValue = minutesMatch ? parse(minutesMatch[1]) : 0; const secondsMatch = value.match(/(\d+)s/); - const seconds = secondsMatch ? parse(secondsMatch[1]) : 0; + const secondsMatchValue = secondsMatch ? parse(secondsMatch[1]) : 0; - if (hours > 0 || minutes > 0 || seconds > 0) { - millis = hours * mth + minutes * mtm + seconds * mts; - } else { - // split string at known separators : , . - const separatorRegex = /[\s,:.]+/; - const [first, second, third] = value.split(separatorRegex); - - if (first != null && second != null && third != null) { - // if string has three sections, treat as [hours] [minutes] [seconds] - millis = parse(first) * mth; - millis += parse(second) * mtm; - millis += parse(third) * mts; - } else if (first != null && second == null && third == null) { - // we only have one section, infer separators - - const length = first.length; - if (length === 1) { - millis = parse(first) * mtm; - } else if (length === 2) { - millis = parse(first) * mtm; - } else if (length === 3) { - millis = parse(first[0]) * mth + parse(first.substring(1)) * mtm; - } else if (length === 4) { - millis = parse(first.substring(0, 2)) * mth + parse(first.substring(2)) * mtm; - } else if (length === 5) { - const hours = parse(first.substring(0, 2)); - const minutes = parse(first.substring(2, 4)); - const seconds = parse(first.substring(4)); - millis = hours * mth + minutes * mtm + seconds * mts; - } else if (length >= 6) { - const hours = parse(first.substring(0, 2)); - const minutes = parse(first.substring(2, 4)); - const seconds = parse(first.substring(4)); - millis = hours * mth + minutes * mtm + seconds * mts; + if (hoursMatchValue > 0 || minutesMatchValue > 0 || secondsMatchValue > 0) { + return hoursMatchValue * mth + minutesMatchValue * mtm + secondsMatchValue * mts; + } + return { hoursMatchValue }; +} + +/** + * @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) * mth; + if (isAM) { + // this ensures we dont add 12 hours in the end + addAM = inferredMillis; } + } else { + inferredMillis = parse(value) * mtm; } - if (first != null && second != null && third == null) { - millis = parse(first) * mth; - millis += parse(second) * mtm; + } else if (length === 2) { + if (isPM || isAM) { + inferredMillis = parse(value) * inferredMillis; + if (isAM) { + // this ensures we dont add 12 hours in the end + addAM = 12; + } + } else { + inferredMillis = parse(value) * mtm; } + } else if (length === 3) { + inferredMillis = parse(value[0]) * mth + parse(value.substring(1)) * mtm; + } else if (length === 4) { + inferredMillis = parse(value.substring(0, 2)) * mth + parse(value.substring(2)) * mtm; + } 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 * mth + minutes * mtm + seconds * mts; + } else if (length >= 6) { + const hours = parse(value.substring(0, 2)); + const minutes = parse(value.substring(2, 4)); + const seconds = parse(value.substring(4)); + inferredMillis = hours * mth + minutes * mtm + seconds * mts; + } + 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 => { + if (value === '12am') { + return 0; } + const { isAM, isPM, value: parsingValue } = checkAmPm(value); + const maybeMillisFromMatchers = checkMatchers(parsingValue); + if (typeof maybeMillisFromMatchers === 'number') { + return maybeMillisFromMatchers; + } + + let { hoursMatchValue } = maybeMillisFromMatchers; + + let millis = 0; + + // split string at known separators : , . + const separatorRegex = /[\s,:.]+/; + const [first, second, third] = parsingValue.split(separatorRegex); + + if (first != null && second != null && third != null) { + // if string has three sections, treat as [hours] [minutes] [seconds] + millis = parse(first) * mth; + millis += parse(second) * mtm; + millis += parse(third) * mts; + } else if (first != null && second == null && third == null) { + // we only have one section, infer separators + const { inferredMillis, addAM } = inferSeparators(first, isAM, isPM); + millis = inferredMillis; + hoursMatchValue = addAM; + } + if (first != null && second != null && third == null) { + millis = parse(first) * mth; + millis += parse(second) * mtm; + } + + // Add 12 hours if it is PM + if (isPM && hoursMatchValue < 12) { + millis += 12 * mth; + } return millis; };