const MOST_ACCURATE_DURATION_FORMAT = 'h:mm:ss.sss' // taken from backend timedelta.max.total_seconds() == 1_000_000_000 days export const MAX_BACKEND_DURATION_VALUE_NUMBER_OF_SECS = 86400000000000 export const DEFAULT_DURATION_FORMAT = 'h:mm' const D_H = 'd h' const D_H_M = 'd h:mm' const D_H_M_S = 'd h:mm:ss' const H_M = 'h:mm' const H_M_S = 'h:mm:ss' const H_M_S_S = 'h:mm:ss.s' const H_M_S_SS = 'h:mm:ss.ss' const H_M_S_SSS = 'h:mm:ss.sss' const SECS_IN_DAY = 86400 const SECS_IN_HOUR = 3600 const SECS_IN_MIN = 60 function totalSecs({ secs = 0, mins = 0, hours = 0, days = 0 }) { return ( parseInt(days) * SECS_IN_DAY + parseInt(hours) * SECS_IN_HOUR + parseInt(mins) * SECS_IN_MIN + parseFloat(secs) ) } const DURATION_REGEXPS = new Map([ [ /^(\d+)(?:d\s*|\s+)(\d+):(\d+):(\d+|\d+\.\d+)$/, { default: (days, hours, mins, secs) => totalSecs({ days, hours, mins, secs }), }, ], [ /^(\d+):(\d+):(\d+|\d+\.\d+)$/, { default: (hours, mins, secs) => totalSecs({ hours, mins, secs }), }, ], [ /^(\d+)(?:d\s*|\s+)(\d+):(\d+)$/, { [D_H]: (days, hours, mins) => totalSecs({ days, hours, mins }), [D_H_M]: (days, hours, mins) => totalSecs({ days, hours, mins }), default: (days, mins, secs) => totalSecs({ days, mins, secs }), }, ], [ /^(\d+)(?:d\s*|\s+)(\d+):(\d+\.\d+)$/, { default: (days, mins, secs) => totalSecs({ days, mins, secs }) }, ], [/^(\d+)h$/, { default: (hours) => totalSecs({ hours }) }], [ /^(\d+)(?:d\s*|\s+)(\d+)h$/, { default: (days, hours) => totalSecs({ days, hours }), }, ], [/^(\d+)d$/, { default: (days) => totalSecs({ days }) }], [ /^(\d+)(?:d\s*|\s+)(\d+)$/, { [D_H]: (days, hours) => totalSecs({ days, hours }), [D_H_M]: (days, mins) => totalSecs({ days, mins }), [H_M]: (days, mins) => totalSecs({ days, mins }), default: (days, secs) => totalSecs({ days, secs }), }, ], [ /^(\d+)(?:d\s*|\s+)(\d+\.\d+)$/, { default: (days, secs) => totalSecs({ days, secs }) }, ], [ /^(\d+):(\d+)$/, { [D_H]: (hours, mins) => totalSecs({ hours, mins }), [D_H_M]: (hours, mins) => totalSecs({ hours, mins }), [H_M]: (hours, mins) => totalSecs({ hours, mins }), default: (mins, secs) => totalSecs({ mins, secs }), }, ], [ /^(\d+):(\d+\.\d+)$/, { default: (mins, secs) => totalSecs({ mins, secs }) }, ], [/^(\d+\.\d+)$/, { default: (secs) => totalSecs({ secs }) }], [ /^(\d+)$/, { [D_H]: (hours) => totalSecs({ hours }), [D_H_M]: (mins) => totalSecs({ mins }), [H_M]: (mins) => totalSecs({ mins }), default: (secs) => totalSecs({ secs }), }, ], ]) // Map guarantees the order of the entries export const DURATION_FORMATS = new Map([ [ H_M, { description: 'h:mm (1:23)', example: '1:23', toString(d, h, m, s) { return `${d * 24 + h}:${m.toString().padStart(2, '0')}` }, round: (value) => Math.round(value / 60) * 60, }, ], [ H_M_S, { description: 'h:mm:ss (1:23:40)', example: '1:23:40', toString(d, h, m, s) { return `${d * 24 + h}:${m.toString().padStart(2, '0')}:${s .toFixed(0) .padStart(2, '0')}` }, round: (value) => Math.round(value), }, ], [ H_M_S_S, { description: 'h:mm:ss.s (1:23:40.0)', example: '1:23:40.0', toString(d, h, m, s) { return `${d * 24 + h}:${m.toString().padStart(2, '0')}:${s .toFixed(1) .padStart(4, '0')}` }, round: (value) => Math.round(value * 10) / 10, }, ], [ H_M_S_SS, { description: 'h:mm:ss.ss (1:23:40.00)', example: '1:23:40.00', toString(d, h, m, s) { return `${d * 24 + h}:${m.toString().padStart(2, '0')}:${s .toFixed(2) .padStart(5, '0')}` }, round: (value) => Math.round(value * 100) / 100, }, ], [ H_M_S_SSS, { description: 'h:mm:ss.sss (1:23:40.000)', example: '1:23:40.000', toString(d, h, m, s) { return `${d * 24 + h}:${m.toString().padStart(2, '0')}:${s .toFixed(3) .padStart(6, '0')}` }, round: (value) => Math.round(value * 1000) / 1000, }, ], [ D_H, { description: 'd h (1d 2h)', example: '1d 2h', toString(d, h, m, s) { return `${d}d ${h}h` }, round: (value) => Math.round(value / 3600) * 3600, }, ], [ D_H_M, { description: 'd h:mm (1d 2:34)', example: '1d 2:34', toString(d, h, m, s) { return `${d}d ${h}:${m.toString().padStart(2, '0')}` }, round: (value) => Math.round(value / 60) * 60, }, ], [ D_H_M_S, { description: 'd h:mm:ss (1d 2:34:56)', example: '1d 2:34:56', toString(d, h, m, s) { return `${d}d ${h}:${m.toString().padStart(2, '0')}:${s .toFixed(0) .padStart(2, '0')}` }, round: (value) => Math.round(value), }, ], ]) export const roundDurationValueToFormat = (value, format) => { if (value === null) { return null } const durationFormatOptions = DURATION_FORMATS.get(format) if (!durationFormatOptions) { throw new Error(`Unknown duration format ${format}`) } return durationFormatOptions.round(value) } /** * It tries to parse the input value using the given format. * If the input value does not match the format, it tries to parse it using * the most accurate format if strict is false, otherwise it throws an error. */ export const parseDurationValue = ( inputValue, format = MOST_ACCURATE_DURATION_FORMAT ) => { if (inputValue === null || inputValue === undefined || inputValue === '') { return null } // If the value is a number, we assume it's already in seconds (i.e. from the backend). if (Number.isFinite(inputValue)) { return inputValue > 0 ? inputValue : null } for (const [fmtRegExp, formatFuncs] of DURATION_REGEXPS) { const match = inputValue.match(fmtRegExp) if (match) { const formatFunc = formatFuncs[format] || formatFuncs.default return formatFunc(...match.slice(1)) } } return null } /** * It formats the given duration value using the given format. */ export const formatDurationValue = (value, format) => { if (value === null || value === undefined || value === '') { return '' } const days = Math.floor(value / 86400) const hours = Math.floor((value % 86400) / 3600) const mins = Math.floor((value % 3600) / 60) const secs = value % 60 const formatFunc = DURATION_FORMATS.get(format).toString return formatFunc(days, hours, mins, secs) }