Convert ISO 8601 duration with JavaScript
Asked Answered
M

9

35

How can I convert duration with JavaScript, for example:

PT16H30M

Muldon answered 18/2, 2013 at 10:27 Comment(3)
What have you tried? What do you want to convert it to? This may be a dupe of #4830069Metapsychology
How from this format get time 16:30;Muldon
There is a package for that: npmjs.com/package/iso8601-durationTaster
P
48

You could theoretically get an ISO8601 Duration that looks like the following:

P1Y4M3W2DT10H31M3.452S

I wrote the following regular expression 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 regular expressions might be able to write a better one.

The groups boil down into the following:

  1. Sign
  2. Years
  3. Months
  4. Weeks
  5. Days
  6. Hours
  7. Minutes
  8. Seconds

I wrote the following function to convert it into a nice 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 out there.


Update

If you are using momentjs, they have ISO8601 duration parsing functionality available. You'll need a plugin to format it, and it doesn't seem to handle durations that have weeks specified in the period as of the writing of this note.

Pumpernickel answered 19/3, 2015 at 19:10 Comment(4)
P1D is a valid duration, so you should not expect the T inside the regex. Here is a more valid regex : (-)?P(?:([.,\d]+)Y)?(?:([.,\d]+)M)?(?:([.,\d]+)W)?(?:([.,\d]+)D)?(?:T(?:([.,\d]+)H)?(?:([.,\d]+)M)?(?:([.,\d]+)S)?)?Misnomer
I specially logged in to upvote it. Great and timesaving, thanks!Merle
Your code can be greatly simplified by using Nullish coalescing operator '??'.Disappear
I've been looking for solutions for literal hours, you got "famous" libraries such as date-fns that don't even consider this despite giving a conversion the other way around. As usual with JS, I end up having to add a function in my helpers file while this should be a standard in all time related libraries. Thanks for that.Dempsey
T
9

Moment.js released with version 2.3 a duration support.

const iso8601Duration = "PT16H30M"

moment.duration(iso8601Duration)
// -> { _data: { days: 0, hours: 16, milliseconds: 0, minutes: 30, months: 0, seconds: 0, years: 0} ... 

moment.duration(iso8601Duration).asSeconds()
// -> 59400

Read more https://momentjs.com/docs/#/durations/ .

Tishatishri answered 28/6, 2020 at 18:6 Comment(0)
P
5
"PT16H30M".replace(/PT(\d+)H(\d+)M/, "$1:$2");
Peba answered 18/2, 2013 at 10:47 Comment(0)
A
5

Wrapped up a small package to facilitate this:

import { parse, serialize } from 'tinyduration';
 
// Basic parsing
const durationObj = parse('P1Y2M3DT4H5M6S');
assert(durationObj, {
    years: 1,
    months: 2,
    days: 3,
    hours: 4,
    minutes: 5,
    seconds: 6
});
 
// Serialization
assert(serialize(durationObj), 'P1Y2M3DT4H5M6S');

Install using npm install --save tinyduration or yarn add tinyduration

See: https://www.npmjs.com/package/tinyduration

Art answered 15/10, 2020 at 13:25 Comment(0)
I
3

I have just done this for durations that are even over a year long.
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);
}
Isolecithal answered 18/6, 2014 at 16:16 Comment(2)
this is cool, only bug is that when minutes are not provided, it outputs three zeros instead of two.Merl
Thank you, I didn't notice that. I did manage to fix this "by mistake" when rewriting the script. Updating now.Isolecithal
S
2

Specifically solving DateTime strings which can be used within the HTML5 <time/> tags, as they are limited to Days, Minutes and Seconds (as only these can be converted to a precise number of seconds, as Months and Years can have varying 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
Swingletree answered 8/1, 2019 at 22:58 Comment(1)
This regular expression fails with "P0D" (real value taken from YouTube API)Pyrope
B
2

Alternatively, you can use the Duration.fromISO method from the luxon library.

Here's an example of how to use it:

const { Duration } = require('luxon');

// Parse an ISO 8601 duration string
const duration = Duration.fromISO('P3Y6M4DT12H30M5S');

// Print the total number of seconds in the duration
console.log(duration.as('seconds'));

This will log the total number of seconds in the duration, which in this case would be '117536305'.

Note that the luxon library is part of Moment project btw.

Brumbaugh answered 9/12, 2022 at 12:17 Comment(0)
M
0

Basic solution to ISO8601 period support.

Due to lack of a 'duration' type in JavaScript and weird date semantics, this uses date arithmetic to apply a 'period' to an 'anchor' date (defaults to current date and time). Default is to add the period.

Specify ago: true to provide 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;
    }

No warranty. This may have quirks - test for your use case.

Mcmaster answered 18/8, 2019 at 12:53 Comment(0)
N
0

This is an update of solution proposed by James Caradoc-Davies

  • It add support for +/- sign at the start of the period
  • correctly handle floating seconds
  • work with many input format for the date to apply duration to
  • Force the T to be present as required by spec
const durationExp = /^(?<sign>\+|-)?P((?<Y>\d+)Y)?((?<M>\d+)M)?((?<D>\d+)D)?T((?<H>\d+)H)?((?<m>\d+)M)?((?<S>\d+(\.\d+))?S)?$/
/**
 * # applyISODuration
 * Apply ISO 8601 duration string to given date
 * - **duration** must be a strng compliant with ISO 8601 duration
 *   /!\ it doesn't support the Week parameter but it can easily be replaced by adding a number of days
 * - **date** can be omit and will then default to Date.now()
 *   It can be any valid value for Date constructor so you can pass date
 *   as strings or number as well as Date object
 * returns a new Date with the duration applied
 */
const  applyISODuration = (duration/*:string*/, date/*?: Date|string|number*/) => {
  date = date ? new Date(date) : new Date()
  const parts = duration.match(durationExp)?.groups
  if (!parts) {
    throw new Error(`Invalid duration(${duration})`)
  }
  const addOrSubstract = parts.sign === '-' ? (value) => -value : (value) => +value;
  parts.Y && date.setFullYear(date.getFullYear() + addOrSubstract(parts.Y))
  parts.M && date.setMonth(date.getMonth() + addOrSubstract(parts.M))
  parts.D && date.setDate(date.getDate() + addOrSubstract(parts.D))
  parts.H && date.setHours(date.getHours() + addOrSubstract(parts.H))
  parts.m && date.setMinutes(date.getMinutes() + addOrSubstract(parts.m))
  parts.s && date.setSeconds(date.getSeconds() + addOrSubstract(parseFloat(parts.S)))
  return date
}

usage:

applyISODuration('P2DT12H5.555S')
applyISODuration('P2DT12H5.555S', new Date("2022-06-23T09:52:38.298Z") )
Nadabus answered 23/6, 2022 at 9:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.