ISO 8601 is the international standard document covering date and time-related data. It is commonly used to represent dates and times in code (e.g. Date.toISOString). There is one less known specification in this standard related to duration.
What is duration standard?
Duration defines the interval in time and is represented by the following format:
P{n}Y{n}M{n}W{n}DT{n}H{n}M{n}S
Letters P and T represent, respectively, makers for period and time blocks. The capitals letters Y, M, W, D, H, M, S represent the segments in order:
years, months, weeks, days, hours, minutes, and seconds. The {n}
represents a number. Each of the duration
segments are optional.
The following are all valid durations:
P3Y - 3 years
P24W6D - 24 weeks, 6 days
P5MT7M - 5 months, 7 minutes
PT3H5S - 3 hours, 5 seconds
Human-readable format
Using the specification it's easy to implement a parser that would parse ISO standard into human-readable form. First, we need the regex that would extract the necessary segments:
/P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/
Let's dissect this regex to understand what it does:
- First character
P
matches P literally - Group
(?:(\d+)Y)?
is non-capturing group (due to?:
modifier)- The group can 0 or 1 appearances (due to
?
at the end) - The inner part
(\d+)Y
matches 1 to many digits followed byY
- The digits part
(\d+)
is a capturing group (due to surrounding brackets)
- The group can 0 or 1 appearances (due to
- Same logic applies for
(?:(\d+)M)?
,(?:(\d+)W)?
and(?:(\d+)D)?
- Group
(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?
is also non-capturing group- Group starts with
T
literal - The group is optional (due to
?
at the end) - Group consist on sub-groups
(?:(\d+)H)?
,(?:(\d+)M)?
and(?:(\d+)S)?
to which the above mentioned logic applies
- Group starts with
If we run this regex on an arbitrary string it will try to match P
at the beginning and then extract numbers for
years, months, weeks, days, hours, minutes, and seconds. For those that are not available, it will return undefined.
We can use array destructuring in ES6 to extract those values:
const REGEX = /P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
function parseDuration(input: string) {
const [, years, months, weeks, days, hours, minutes, secs] = input.match(
REGEX
);
// combine the values into output
}
We can use those values to export something like 3 years 5 days 23:11:05
. We will first
create an array of parsed segments:
[3, undefined, undefined, 5, 23, 11, 5] -> ['3 years', '5 days', '23:11:05']
And then simply flatten/join the array using whitespace. Parsing time has an additional logic:
- we return the time segment only if at least one of hours, minutes or seconds is specified (and different than 0)
- we map every time subsection into two digits signature
Here is the full parser function:
const REGEX = /P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
export function parseDuration(input: string) {
const [, years, months, weeks, days, hours, mins, secs] =
input.match(REGEX) || [];
return [
...(years ? [`${years} years`] : []),
...(months ? [`${months} months`] : []),
...(weeks ? [`${weeks} weeks`] : []),
...(days ? [`${days} days`] : []),
...(hours || mins || secs
? [
[hours || '00', mins || '00', secs || '00']
.map((num) => (num.length < 2 ? `0${num}` : num))
.join(':'),
]
: []),
].join(' ');
}
// usage
parseDuration('P2Y'); // -> 2 years
parseDuration('PT12H34M'); // -> 12:34:00
parseDuration('P4WT5M'); // -> 4 weeks 00:05:00
Extra: Angular Pipe
Wrapping the above function into an angular pipe is straight forward:
import { Pipe, PipeTransform } from '@angular/core';
import { parseDuration } from './parse-duration'; // our parser function
@Pipe({
name: 'duration',
pure: true,
})
export class DurationPipe implements PipeTransform {
transform(value: string): string {
return parseDuration(value);
}
}
We can now use our pipe in the template:
{{ input | duration }}
Understanding the structure of the ISO 8601 standard allowed us to easily parse the segments and then construct the mapper that would map the segments into a desired format. With minimal changes, it's easy to construct a parser that would map duration into different output string or add localization and internationalization.