Question

I'm trying to convert ISO 8601 string to seconds in JS/Node. The best I could come up with was:

function convert_time(duration) {
    var a = duration.match(/\d+/g)
    var duration = 0

    if(a.length == 3) {
        duration = duration + parseInt(a[0]) * 3600;
        duration = duration + parseInt(a[1]) * 60;
        duration = duration + parseInt(a[2]);
    }

    if(a.length == 2) {
        duration = duration + parseInt(a[0]) * 60;
        duration = duration + parseInt(a[1]);
    }

    if(a.length == 1) {
        duration = duration + parseInt(a[0]);
    }
    return duration
}

It works when I input strings such as "PT48S", "PT3M20S" or "PT3H2M31S", but fails miserably if the string is "PT1H11S". Does anyone have a better idea?

Was it helpful?

Solution 3

I suggest this little hack to prevent your problematic case:

function convert_time(duration) {
    var a = duration.match(/\d+/g);

    if (duration.indexOf('M') >= 0 && duration.indexOf('H') == -1 && duration.indexOf('S') == -1) {
        a = [0, a[0], 0];
    }

    if (duration.indexOf('H') >= 0 && duration.indexOf('M') == -1) {
        a = [a[0], 0, a[1]];
    }
    if (duration.indexOf('H') >= 0 && duration.indexOf('M') == -1 && duration.indexOf('S') == -1) {
        a = [a[0], 0, 0];
    }

    duration = 0;

    if (a.length == 3) {
        duration = duration + parseInt(a[0]) * 3600;
        duration = duration + parseInt(a[1]) * 60;
        duration = duration + parseInt(a[2]);
    }

    if (a.length == 2) {
        duration = duration + parseInt(a[0]) * 60;
        duration = duration + parseInt(a[1]);
    }

    if (a.length == 1) {
        duration = duration + parseInt(a[0]);
    }
    return duration
}

Fiddle

OTHER TIPS

If you're using moment.js you can simply call...

moment.duration('PT15M33S').asMilliseconds();

= 933000 ms

EDIT 2021: While this works, and still gets upvotes, I wouldn't advise including moment.js just for this. I'd recommend using a regex answer like @redgetan's

function YTDurationToSeconds(duration) {
  var match = duration.match(/PT(\d+H)?(\d+M)?(\d+S)?/);

  match = match.slice(1).map(function(x) {
    if (x != null) {
        return x.replace(/\D/, '');
    }
  });

  var hours = (parseInt(match[0]) || 0);
  var minutes = (parseInt(match[1]) || 0);
  var seconds = (parseInt(match[2]) || 0);

  return hours * 3600 + minutes * 60 + seconds;
}

works for these cases:

PT1H
PT23M
PT45S
PT1H23M
PT1H45S
PT23M45S
PT1H23M45S

Here's my solution:

function parseDuration(duration) {
    var matches = duration.match(/[0-9]+[HMS]/g);

    var seconds = 0;

    matches.forEach(function (part) {
        var unit = part.charAt(part.length-1);
        var amount = parseInt(part.slice(0,-1));

        switch (unit) {
            case 'H':
                seconds += amount*60*60;
                break;
            case 'M':
                seconds += amount*60;
                break;
            case 'S':
                seconds += amount;
                break;
            default:
                // noop
        }
    });

    return seconds;
}

You can find a very simple PHP solution here - How To Convert Youtube API Time (ISO 8601 String Video Duration) to Seconds In PHP - Code

This function convert_time() takes one parameter as input - the Youtube API Time (Video Duration) which is in ISO 8601 string format and returns its duration in seconds.

function convert_time($str) 
{
    $n = strlen($str);
    $ans = 0;
    $curr = 0;
    for($i=0; $i<$n; $i++)
    {
        if($str[$i] == 'P' || $str[$i] == 'T')
        {

        }
        else if($str[$i] == 'H')
        {
            $ans = $ans + 3600*$curr;
            $curr = 0;
        }
        else if($str[$i] == 'M')
        {
            $ans = $ans + 60*$curr;
            $curr = 0;
        }
        else if($str[$i] == 'S')
        {
            $ans = $ans + $curr;
            $curr = 0;
        }
        else
        {
            $curr = 10*$curr + $str[$i];
        }
    }
    return($ans);
}

Testing Some Inputs:

"PT2M23S" => 143
"PT2M" => 120
"PT28S" => 28
"PT5H22M31S" => 19351
"PT3H" => 10800
"PT1H6M" => 3660
"PT1H6S" => 3606

My solution:

function convert_time(duration) {
  var total = 0;
  var hours = duration.match(/(\d+)H/);
  var minutes = duration.match(/(\d+)M/);
  var seconds = duration.match(/(\d+)S/);
  if (hours) total += parseInt(hours[1]) * 3600;
  if (minutes) total += parseInt(minutes[1]) * 60;
  if (seconds) total += parseInt(seconds[1]);
  return total;
}

Fiddle

Here's @redgetan 's solution in ES6.

I also fixed it for years, weeks and days.

https://www.digi.com/resources/documentation/digidocs/90001437-13/reference/r_iso_8601_duration_format.htm

// Copied from:
// https://stackoverflow.com/questions/22148885/converting-youtube-data-api-v3-video-duration-format-to-seconds-in-javascript-no
function parseISO8601Duration(duration) {
    const match = duration.match(/P(\d+Y)?(\d+W)?(\d+D)?T(\d+H)?(\d+M)?(\d+S)?/)
    // An invalid case won't crash the app.
    if (!match) {
        console.error(`Invalid YouTube video duration: ${duration}`)
        return 0
    }
    const [
        years,
        weeks,
        days,
        hours,
        minutes,
        seconds
    ] = match.slice(1).map(_ => _ ? parseInt(_.replace(/\D/, '')) : 0)
  return (((years * 365 + weeks * 7 + days) * 24 + hours) * 60 + minutes) * 60 + seconds
}

if (parseISO8601Duration('PT1H') !== 3600) {
    throw new Error()
}

if (parseISO8601Duration('PT23M') !== 1380) {
    throw new Error()
}

if (parseISO8601Duration('PT45S') !== 45) {
    throw new Error()
}

if (parseISO8601Duration('PT1H23M') !== 4980) {
    throw new Error()
}

if (parseISO8601Duration('PT1H45S') !== 3645) {
    throw new Error()
}

if (parseISO8601Duration('PT1H23M45S') !== 5025) {
    throw new Error()
}

if (parseISO8601Duration('P43W5DT5M54S') !== 26438754) {
    throw new Error()
}

if (parseISO8601Duration('P1Y43W5DT5M54S') !== 57974754) {
    throw new Error()
}

I've written a CoffeeScript variation (you can easily compile it at coffeescript.org when desired)

DIFFERENCE: the returning duration comes in a human readable format (e.g. 04:20, 01:05:48)

String.prototype.parseDuration = ->
    m = @.match /[0-9]+[HMS]/g
    res = ""
    fS = fM = !1
    for part in m
        unit = part.slice -1
        val = part.slice 0, part.length - 1
        switch unit
            when "H" then res += val.zeros( 2 ) + ":"
            when "M"
                fM = 1
                res += val.zeros( 2 ) + ":"
            when "S"
                fS = 1
                res += if fM then val.zeros 2 else "00:" + val.zeros 2

     if !fS then res += "00"
     res

I've also implemented this helper function to fill < 10 values with a leading zero:

String.prototype.zeros = ( x ) ->
    len = @length
    if !x or len >= x then return @
    zeros = ""
    zeros += "0" for [0..(x-len-1)]
    zeros + @

3nj0y!!!

I realize eval is unpopular, but here's the easiest and fastest approach I can imagine. Enjoy.

function formatDuration(x) {
   return eval(x.replace('PT','').replace('H','*3600+').replace('M','*60+').replace('S', '+').slice(0, -1));
}

I think using moment.js will be an easier solution. But if someone is looking for a custom solution, here is a simple regex one for you:

var regex = /PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/;
var regex_result = regex.exec("PT1H11S"); //Can be anything like PT2M23S / PT2M / PT28S / PT5H22M31S / PT3H/ PT1H6M /PT1H6S
var hours = parseInt(regex_result[1] || 0);
var minutes = parseInt(regex_result[2] || 0);
var seconds = parseInt(regex_result[3] || 0);
var total_seconds = hours * 60 * 60 + minutes * 60 + seconds;

I ran into issues with the above solution. I decided to write it as obtuse as possible. I also use my own "getIntValue" in place of parseInt for extra sanity.

Just thought other searching might appreciate the update.

Fiddle

    function convertYouTubeTimeFormatToSeconds(timeFormat) {

    if ( timeFormat === null || timeFormat.indexOf("PT") !== 0 ) {
        return 0;
    }

    // match the digits into an array
    // each set of digits into an item
    var digitArray      = timeFormat.match(/\d+/g);
    var totalSeconds    = 0;

    // only 1 value in array
    if (timeFormat.indexOf('H') > -1 && timeFormat.indexOf('M') == -1 && timeFormat.indexOf('S') == -1) {
        totalSeconds    += getIntValue(digitArray[0]) * 60 * 60;
    }

    else if (timeFormat.indexOf('H') == -1 && timeFormat.indexOf('M') > -1 && timeFormat.indexOf('S') == -1) {
        totalSeconds    += getIntValue(digitArray[0]) * 60;
    }

    else if (timeFormat.indexOf('H') == -1 && timeFormat.indexOf('M') == -1 && timeFormat.indexOf('S') > -1) {
        totalSeconds    += getIntValue(digitArray[0]);
    }


    // 2 values in array
    else if (timeFormat.indexOf('H') > -1 && timeFormat.indexOf('M') > -1 && timeFormat.indexOf('S') == -1) {
        totalSeconds    += getIntValue(digitArray[0]) * 60 * 60;
        totalSeconds    += getIntValue(digitArray[1]) * 60;
    }

    else if (timeFormat.indexOf('H') > -1 && timeFormat.indexOf('M') == -1 && timeFormat.indexOf('S') > -1) {
        totalSeconds    += getIntValue(digitArray[0]) * 60 * 60;
        totalSeconds    += getIntValue(digitArray[1]);
    }

    else if (timeFormat.indexOf('H') == -1 && timeFormat.indexOf('M') > -1 && timeFormat.indexOf('S') > -1) {
        totalSeconds    += getIntValue(digitArray[0]) * 60;
        totalSeconds    += getIntValue(digitArray[1]);
    }


    // all 3 values
    else if (timeFormat.indexOf('H') > -1 && timeFormat.indexOf('M') > -1 && timeFormat.indexOf('S') > -1) {
        totalSeconds    += getIntValue(digitArray[0]) * 60 * 60;
        totalSeconds    += getIntValue(digitArray[1]) * 60;
        totalSeconds    += getIntValue(digitArray[2]);
    }

//  console.log(timeFormat, totalSeconds);

    return totalSeconds;
}
function getIntValue(value) {
    if (value === null) {
        return 0;
    }

    else {

        var intValue = 0;
        try {
            intValue        = parseInt(value);
            if (isNaN(intValue)) {
                intValue    = 0;
            }
        } catch (ex) { }

        return Math.floor(intValue);
    }
}

Python

It works by parsing the input string 1 character at a time, if the character is numerical it simply adds it (string add, not mathematical add) to the current value being parsed. If it is one of 'wdhms' the current value is assigned to the appropriate variable (week, day, hour, minute, second), and value is then reset ready to take the next value. Finally it sum the number of seconds from the 5 parsed values.

def ytDurationToSeconds(duration): #eg P1W2DT6H21M32S
    week = 0
    day  = 0
    hour = 0
    min  = 0
    sec  = 0

    duration = duration.lower()

    value = ''
    for c in duration:
        if c.isdigit():
            value += c
            continue

        elif c == 'p':
            pass
        elif c == 't':
            pass
        elif c == 'w':
            week = int(value) * 604800
        elif c == 'd':
            day = int(value)  * 86400
        elif c == 'h':
            hour = int(value) * 3600
        elif c == 'm':
            min = int(value)  * 60
        elif c == 's':
            sec = int(value)

        value = ''

    return week + day + hour + min + sec

This is not java specific, but i would like to add JAVA snippet as that may helpful to other users

String duration = "PT1H23M45S";
Pattern pattern = Pattern.compile("PT(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?");
Matcher matcher = pattern.matcher(duration);
long sec = 0;
long min = 0;
long hour = 0;
if (matcher.find())
{
    if(matcher.group(1)!=null)
        hour = NumberUtils.toInt(matcher.group(1));
    if(matcher.group(2)!=null)
        min = NumberUtils.toInt(matcher.group(2));
    if(matcher.group(3)!=null)
        sec = NumberUtils.toInt(matcher.group(3));

}
long totalSec = (hour*3600)+(min*60)+sec;
System.out.println(totalSec);

Assuming the input is valid, we can use the regex exec method to iterate on the string and extract the group sequentially:

const YOUTUBE_TIME_RE = /(\d+)([HMS])/g;
const YOUTUBE_TIME_UNITS = {
    'H': 3600,
    'M': 60,
    'S': 1
}

/**
 * Returns the # of seconds in a youtube time string
 */
function parseYoutubeDate(date: string): number {
    let ret = 0;
    let match: RegExpExecArray;
    while (match = YOUTUBE_TIME_RE.exec(date)) {
        ret += (YOUTUBE_TIME_UNITS[match[2]]) * Number(match[1]);
    }
    return ret;
}

ES6:

const durationToSec = formatted =>
  formatted
    .match(/PT(?:(\d*)H)?(?:(\d*)M)?(?:(\d*)S)?/)
    .slice(1)
    .map(v => (!v ? 0 : v))
    .reverse()
    .reduce((acc, v, k) => (acc += v * 60 ** k), 0);

Kotlin version:

private val youtubeDurationPattern: Pattern =
    Pattern.compile("PT(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?")

fun String.parseDuration(): Int {
    val matcher: Matcher = youtubeDurationPattern.matcher(this)
    if (!matcher.find()) {
        throw IllegalStateException("Cannot parse $this.")
    }
    val hour = matcher.group(1)?.toInt() ?: 0
    val min = matcher.group(2)?.toInt() ?: 0
    val sec = matcher.group(3)?.toInt() ?: 0
    return hour * 3600 + min * 60 + sec
}

and test:

    @Test
    fun testParseDuration() {
        assertEquals(10 * 60, "PT10M".parseDuration())
        assertEquals(10 * 60 + 30, "PT10M30S".parseDuration())
        assertEquals(30, "PT30S".parseDuration())
        assertEquals(2 * 3600 + 3 * 60 + 16, "PT2H3M16S".parseDuration())
    }
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top