34

How can I convert duration with JavaScript, for example:

PT16H30M

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
cvelinho
  • 556
  • 1
  • 4
  • 13
  • 4
    What have you tried? What do you want to convert it to? This may be a dupe of http://stackoverflow.com/questions/4829569/help-parsing-iso-8601-date-in-javascript – Dutts Feb 18 '13 at 10:30
  • How from this format get time 16:30; – cvelinho Feb 18 '13 at 10:36
  • 1
    There is a package for that: https://www.npmjs.com/package/iso8601-duration – str Aug 27 '18 at 09:01

9 Answers9

43

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.

Community
  • 1
  • 1
crush
  • 16,713
  • 9
  • 59
  • 100
8

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

Sir hennihau
  • 1,495
  • 4
  • 18
  • 31
5
"PT16H30M".replace(/PT(\d+)H(\d+)M/, "$1:$2");
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
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

Melle
  • 7,639
  • 1
  • 30
  • 31
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);
}
  • 1
    this is cool, only bug is that when minutes are not provided, it outputs three zeros instead of two. – wiherek Jul 03 '14 at 13:47
  • 1
    Thank you, I didn't notice that. I did manage to fix this "by mistake" when rewriting the script. Updating now. – Miko Lukasik Jul 11 '14 at 18:47
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
Luke Stevenson
  • 10,357
  • 2
  • 26
  • 41
1

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.

Ajith Renjala
  • 4,934
  • 5
  • 34
  • 42
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.

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") )
malko
  • 2,292
  • 18
  • 26