diff --git a/.changeset/brown-trainers-lay.md b/.changeset/brown-trainers-lay.md new file mode 100644 index 0000000000..b121f7154b --- /dev/null +++ b/.changeset/brown-trainers-lay.md @@ -0,0 +1,5 @@ +--- +'@evidence-dev/component-utilities': minor +--- + +Add built-in duration formats for human-friendly time display. Two new format tags: `duration` (input in minutes) and `duration_hrs` (input in hours) that render values like "14 mins", "2.3 hrs", "1.5 days". diff --git a/packages/lib/component-utilities/src/builtInFormats.js b/packages/lib/component-utilities/src/builtInFormats.js index f20ed348c4..9b8018f908 100644 --- a/packages/lib/component-utilities/src/builtInFormats.js +++ b/packages/lib/component-utilities/src/builtInFormats.js @@ -478,5 +478,52 @@ export const BUILT_IN_FORMATS = [ valueType: 'number', exampleInput: 0.731, titleTagReplacement: '' + }, + + // Duration (input in minutes): + { + formatTag: 'duration', + formatCode: AUTO_FORMAT_CODE, + formatCategory: 'number', + valueType: 'number', + exampleInput: 142, + titleTagReplacement: '', + _autoFormat: { + autoFormatFunction: (typedValue) => { + if (typedValue == null || isNaN(typedValue)) return '-'; + const mins = Number(typedValue); + if (mins < 1) return '<1 min'; + if (mins < 60) return Math.round(mins) + ' mins'; + if (mins < 1440) { + const hrs = mins / 60; + return (hrs >= 10 ? Math.round(hrs) : hrs.toFixed(1)) + ' hrs'; + } + const days = mins / 1440; + return (days >= 10 ? Math.round(days) : days.toFixed(1)) + ' days'; + } + } + }, + // Duration (input in hours): + { + formatTag: 'duration_hrs', + formatCode: AUTO_FORMAT_CODE, + formatCategory: 'number', + valueType: 'number', + exampleInput: 3.7, + titleTagReplacement: '', + _autoFormat: { + autoFormatFunction: (typedValue) => { + if (typedValue == null || isNaN(typedValue)) return '-'; + const hrs = Number(typedValue); + const mins = hrs * 60; + if (mins < 1) return '<1 min'; + if (mins < 60) return Math.round(mins) + ' mins'; + if (mins < 1440) { + return (hrs >= 10 ? Math.round(hrs) : hrs.toFixed(1)) + ' hrs'; + } + const days = hrs / 24; + return (days >= 10 ? Math.round(days) : days.toFixed(1)) + ' days'; + } + } } ]; diff --git a/packages/lib/component-utilities/src/tests/autoFormatting.spec.js b/packages/lib/component-utilities/src/tests/autoFormatting.spec.js index 0eee362b5d..e84b436801 100644 --- a/packages/lib/component-utilities/src/tests/autoFormatting.spec.js +++ b/packages/lib/component-utilities/src/tests/autoFormatting.spec.js @@ -277,3 +277,104 @@ describe('autoFormat', () => { expect(computeNumberAutoFormatCode(10000, defaultMaxDecimals, 6)).toBe('#,##0.0'); }); }); + +describe('duration format (input in minutes)', () => { + const durationFormat = BUILT_IN_FORMATS.find((f) => f.formatTag === 'duration'); + + it('should be defined with auto format function', () => { + expect(durationFormat).toBeDefined(); + expect(durationFormat.formatCode).toBe(AUTO_FORMAT_CODE); + expect(durationFormat._autoFormat.autoFormatFunction).toBeDefined(); + }); + + it('should return dash for null/undefined/NaN', () => { + expect(autoFormat(null, durationFormat)).toBe('-'); + expect(autoFormat(undefined, durationFormat)).toBe('-'); + expect(autoFormat(NaN, durationFormat)).toBe('-'); + }); + + it('should format sub-minute values', () => { + expect(autoFormat(0, durationFormat)).toBe('<1 min'); + expect(autoFormat(0.5, durationFormat)).toBe('<1 min'); + }); + + it('should format minute values', () => { + expect(autoFormat(1, durationFormat)).toBe('1 mins'); + expect(autoFormat(14, durationFormat)).toBe('14 mins'); + expect(autoFormat(59, durationFormat)).toBe('59 mins'); + }); + + it('should format hour values with one decimal when under 10', () => { + expect(autoFormat(60, durationFormat)).toBe('1.0 hrs'); + expect(autoFormat(90, durationFormat)).toBe('1.5 hrs'); + expect(autoFormat(142, durationFormat)).toBe('2.4 hrs'); + expect(autoFormat(570, durationFormat)).toBe('9.5 hrs'); + }); + + it('should format hour values as integers when 10 or more', () => { + expect(autoFormat(600, durationFormat)).toBe('10 hrs'); + expect(autoFormat(720, durationFormat)).toBe('12 hrs'); + expect(autoFormat(1439, durationFormat)).toBe('24 hrs'); + }); + + it('should format day values with one decimal when under 10', () => { + expect(autoFormat(1440, durationFormat)).toBe('1.0 days'); + expect(autoFormat(2160, durationFormat)).toBe('1.5 days'); + expect(autoFormat(10080, durationFormat)).toBe('7.0 days'); + }); + + it('should format day values as integers when 10 or more', () => { + expect(autoFormat(14400, durationFormat)).toBe('10 days'); + expect(autoFormat(43200, durationFormat)).toBe('30 days'); + }); +}); + +describe('duration_hrs format (input in hours)', () => { + const durationHrsFormat = BUILT_IN_FORMATS.find((f) => f.formatTag === 'duration_hrs'); + + it('should be defined with auto format function', () => { + expect(durationHrsFormat).toBeDefined(); + expect(durationHrsFormat.formatCode).toBe(AUTO_FORMAT_CODE); + expect(durationHrsFormat._autoFormat.autoFormatFunction).toBeDefined(); + }); + + it('should return dash for null/undefined/NaN', () => { + expect(autoFormat(null, durationHrsFormat)).toBe('-'); + expect(autoFormat(undefined, durationHrsFormat)).toBe('-'); + expect(autoFormat(NaN, durationHrsFormat)).toBe('-'); + }); + + it('should format sub-minute values', () => { + expect(autoFormat(0, durationHrsFormat)).toBe('<1 min'); + expect(autoFormat(0.01, durationHrsFormat)).toBe('<1 min'); + }); + + it('should format minute values (less than 1 hour input)', () => { + expect(autoFormat(0.5, durationHrsFormat)).toBe('30 mins'); + expect(autoFormat(0.25, durationHrsFormat)).toBe('15 mins'); + }); + + it('should format hour values with one decimal when under 10', () => { + expect(autoFormat(1, durationHrsFormat)).toBe('1.0 hrs'); + expect(autoFormat(1.5, durationHrsFormat)).toBe('1.5 hrs'); + expect(autoFormat(3.7, durationHrsFormat)).toBe('3.7 hrs'); + expect(autoFormat(9.5, durationHrsFormat)).toBe('9.5 hrs'); + }); + + it('should format hour values as integers when 10 or more', () => { + expect(autoFormat(10, durationHrsFormat)).toBe('10 hrs'); + expect(autoFormat(12, durationHrsFormat)).toBe('12 hrs'); + expect(autoFormat(23, durationHrsFormat)).toBe('23 hrs'); + }); + + it('should format day values with one decimal when under 10', () => { + expect(autoFormat(24, durationHrsFormat)).toBe('1.0 days'); + expect(autoFormat(36, durationHrsFormat)).toBe('1.5 days'); + expect(autoFormat(168, durationHrsFormat)).toBe('7.0 days'); + }); + + it('should format day values as integers when 10 or more', () => { + expect(autoFormat(240, durationHrsFormat)).toBe('10 days'); + expect(autoFormat(720, durationHrsFormat)).toBe('30 days'); + }); +}); diff --git a/sites/docs/pages/core-concepts/formatting/index.md b/sites/docs/pages/core-concepts/formatting/index.md index 193906859f..3997f18126 100644 --- a/sites/docs/pages/core-concepts/formatting/index.md +++ b/sites/docs/pages/core-concepts/formatting/index.md @@ -60,7 +60,7 @@ E.g., `fmt = '#,##0.00 "mpg"'` ## Built-in Formats -Evidence supports a variety of date/time, number, percentage, and currency formats. You can find the full list of formats below. +Evidence supports a variety of date/time, number, percentage, currency, and duration formats. You can find the full list of formats below. @@ -215,6 +215,32 @@ order by row_num +### Durations + +Duration formats display numeric time values as human-friendly strings. Two input units are supported: minutes and hours. + +Values are automatically scaled to the most appropriate unit (minutes, hours, or days). Values below 1 minute are shown as `<1 min`. Values under 10 in the display unit show one decimal place; values of 10 or more are rounded to whole numbers. + +```sql duration_formats +select 'duration' as format_name, 'auto' as format_code, 'minutes' as input_unit, 0.5 as example_input, '<1 min' as example_output, 0 as row_num union all +select 'duration', 'auto', 'minutes', 14, '14 mins', 1 union all +select 'duration', 'auto', 'minutes', 142, '2.4 hrs', 2 union all +select 'duration', 'auto', 'minutes', 4320, '3.0 days', 3 union all +select 'duration_hrs', 'auto', 'hours', 0.25, '15 mins', 4 union all +select 'duration_hrs', 'auto', 'hours', 3.7, '3.7 hrs', 5 union all +select 'duration_hrs', 'auto', 'hours', 12, '12 hrs', 6 union all +select 'duration_hrs', 'auto', 'hours', 36, '1.5 days', 7 +order by row_num +``` + + + + + + + + + ## Custom Formats