Convert ISO 8601 duration using JavaScript
In theory, you can get an ISO8601 duration that looks like this:
P1Y4M3W2DT10H31M3.452S
I wrote the following regex to parse this into groups:
(-)?P(?:([.,\d]+)Y)?(?:([.,\d]+)M)?(?:([.,\d]+)W)?(?:([.,\d]+)D)?T(?:([.,\d]+)H)?(?:([.,\d]+)M)?(?:([.,\d]+)S)?
It's not pretty, and someone better versed in regex could have written better.
The groups boil down to the following:
- entrance
- Years
- Months
- Weeks
- Days
- Clock
- Protocol
- Seconds
I wrote the following function to convert it to a pretty object:
var iso8601DurationRegex = /(-)?P(?:([.,\d]+)Y)?(?:([.,\d]+)M)?(?:([.,\d]+)W)?(?:([.,\d]+)D)?T(?:([.,\d]+)H)?(?:([.,\d]+)M)?(?:([.,\d]+)S)?/;
window.parseISO8601Duration = function (iso8601Duration) {
var matches = iso8601Duration.match(iso8601DurationRegex);
return {
sign: matches[1] === undefined ? '+' : '-',
years: matches[2] === undefined ? 0 : matches[2],
months: matches[3] === undefined ? 0 : matches[3],
weeks: matches[4] === undefined ? 0 : matches[4],
days: matches[5] === undefined ? 0 : matches[5],
hours: matches[6] === undefined ? 0 : matches[6],
minutes: matches[7] === undefined ? 0 : matches[7],
seconds: matches[8] === undefined ? 0 : matches[8]
};
};
Used like this:
window.parseISO8601Duration('P1Y4M3W2DT10H31M3.452S');
Hope this helps someone.
Update
If you are using momentjs they have ISO8601 duration parsing function. You will need a plugin to format it and it doesn't seem to handle periods that have weeks specified in the period at the time of this writing.
source to share
I just did this for a duration, even over a year.
Here is a fiddle .
function convertDuration(t){
//dividing period from time
var x = t.split('T'),
duration = '',
time = {},
period = {},
//just shortcuts
s = 'string',
v = 'variables',
l = 'letters',
// store the information about ISO8601 duration format and the divided strings
d = {
period: {
string: x[0].substring(1,x[0].length),
len: 4,
// years, months, weeks, days
letters: ['Y', 'M', 'W', 'D'],
variables: {}
},
time: {
string: x[1],
len: 3,
// hours, minutes, seconds
letters: ['H', 'M', 'S'],
variables: {}
}
};
//in case the duration is a multiple of one day
if (!d.time.string) {
d.time.string = '';
}
for (var i in d) {
var len = d[i].len;
for (var j = 0; j < len; j++) {
d[i][s] = d[i][s].split(d[i][l][j]);
if (d[i][s].length>1) {
d[i][v][d[i][l][j]] = parseInt(d[i][s][0], 10);
d[i][s] = d[i][s][1];
} else {
d[i][v][d[i][l][j]] = 0;
d[i][s] = d[i][s][0];
}
}
}
period = d.period.variables;
time = d.time.variables;
time.H += 24 * period.D +
24 * 7 * period.W +
24 * 7 * 4 * period.M +
24 * 7 * 4 * 12 * period.Y;
if (time.H) {
duration = time.H + ':';
if (time.M < 10) {
time.M = '0' + time.M;
}
}
if (time.S < 10) {
time.S = '0' + time.S;
}
duration += time.M + ':' + time.S;
alert(duration);
}
source to share
Specifically tackling DateTime strings that can be used in <time/>
HTML5 tags as they are limited to days, minutes and seconds (as only these can be converted to exact number of seconds, since months and years can have different durations)
function parseDurationString( durationString ){
var stringPattern = /^PT(?:(\d+)D)?(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d{1,3})?)S)?$/;
var stringParts = stringPattern.exec( durationString );
return (
(
(
( stringParts[1] === undefined ? 0 : stringParts[1]*1 ) /* Days */
* 24 + ( stringParts[2] === undefined ? 0 : stringParts[2]*1 ) /* Hours */
)
* 60 + ( stringParts[3] === undefined ? 0 : stringParts[3]*1 ) /* Minutes */
)
* 60 + ( stringParts[4] === undefined ? 0 : stringParts[4]*1 ) /* Seconds */
);
}
Test data
"PT1D" returns 86400
"PT3H" returns 10800
"PT15M" returns 900
"PT1D12H30M" returns 131400
"PT1D3M15.23S" returns 86595.23
source to share
Basic solution to support ISO8601 period.
Due to the lack of a "duration" type in JavaScript and the strange semantics of a date, date arithmetic is used here to apply a "period" to the "anchor" date (defaults to the current date and time). A period is added by default.
Specify back: true to indicate a date in the past.
// Adds ISO8601 period: P<dateparts>(T<timeparts>)?
// E.g. period 1 year 3 months 2 days: P1Y3M2D
// E.g. period 1H: PT1H
// E.g. period 2 days 12 hours: P2DT12H
// @param period string: ISO8601 period string
// @param ago bool [optiona] true: Subtract the period, false: add (Default)
// @param anchor Date [optional] Anchor date for period, default is current date
function addIso8601Period(period /*:string */, ago /*: bool? */, anchor /*: Date? */) {
var re = /^P((?<y>\d+)Y)?((?<m>\d+)M)?((?<d>\d+)D)?(T((?<th>\d+)H)?((?<tm>\d+)M)?((?<ts>\d+(.\d+)?)S)?)?$/;
var match = re.exec(period);
var direction = ago || false ? -1 : 1;
anchor = new Date(anchor || new Date());
anchor.setFullYear(anchor.getFullYear() + (match.groups['y'] || 0) * direction);
anchor.setMonth(anchor.getMonth() + (match.groups['m'] || 0) * direction);
anchor.setDate(anchor.getDate() + (match.groups['d'] || 0) * direction);
anchor.setHours(anchor.getHours() + (match.groups['th'] || 0) * direction);
anchor.setMinutes(anchor.getMinutes() + (match.groups['tm'] || 0) * direction);
anchor.setSeconds(anchor.getSeconds() + (match.groups['ts'] || 0) * direction);
return anchor;
}
There is no guarantee. This can have quirks - a test for your use case.
source to share