Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brown-trainers-lay.md
Original file line number Diff line number Diff line change
@@ -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".
47 changes: 47 additions & 0 deletions packages/lib/component-utilities/src/builtInFormats.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
}
}
];
101 changes: 101 additions & 0 deletions packages/lib/component-utilities/src/tests/autoFormatting.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
28 changes: 27 additions & 1 deletion sites/docs/pages/core-concepts/formatting/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ E.g., `fmt = '#,##0.00 "mpg"'`
</Alert>

## 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.

<!-- These are pasted in from the settings menu HTML, with edits -->

Expand Down Expand Up @@ -215,6 +215,32 @@ order by row_num
</DataTable>


### 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
```

<DataTable data={duration_formats} rows=all>
<Column id="format_name" />
<Column id="format_code" />
<Column id="input_unit" />
<Column id="example_input" align=right/>
<Column id="example_output" align=right/>
</DataTable>


## Custom Formats

Expand Down
Loading