Question

A library and webservice I am using communicates time-intervals in ISO 8601 format: PnYnMnDTnHnMnS. I want to convert such formats to seconds. And vice versa. Seconds are a lot easier to calculate with.

Example interval values are:

  • PT1M or PT60S (1 minute)
  • PT1H, PT60M or PT3600S (1 hour)

I need two functions: parse from such values to seconds: iso8601_interval_to_seconds() and from seconds into such intervals: iso8601_interval_from_seconds().

The latter is rather simple, because it could be done as `"PT{$seconds}S", just pass seconds along, at all times. Maybe this can be done nicer with a parser that switches to H(hour) or M(minute)?

The first is harder, but maybe there is a trick with one of the many string-to-date converters in PHP? I would love to learn how to use such a function for parsing intervals. Or learn an alternative.

Was it helpful?

Solution

It looks like PHP 5.3's DateInterval supports this.

If you can't use 5.3, I suppose any such conversion function would know how many seconds are in a year, a month, a day, an hour, and a minute. Then, when converting from seconds, it would divide in that order, each time taking the modulo of the previous operation, until only <60 seconds are left. When converting from an ISO 8601 interval representation it should be trivial to parse the string and multiply each found element accordingly.

OTHER TIPS

strtotime won't work with the ISO 8601 format directly (eg. P1Y1DT1S), but the format that it does understand (1Year1Day1Second) is not too far off -- it would a pretty straight-forward conversion. (a little "hacky"... but that's PHP for you).

Thanks Lee, I was not aware strtotime accepted this format. This was the missing part of my puzzle. Perhaps my functions can complete your answer.

function parse_duration($iso_duration, $allow_negative = true){
    // Parse duration parts
    $matches = array();
    preg_match('/^(-|)?P([0-9]+Y|)?([0-9]+M|)?([0-9]+D|)?T?([0-9]+H|)?([0-9]+M|)?([0-9]+S|)?$/', $iso_duration, $matches);
    if(!empty($matches)){       
        // Strip all but digits and -
        foreach($matches as &$match){
            $match = preg_replace('/((?!([0-9]|-)).)*/', '', $match);
        }   
        // Fetch min/plus symbol
        $result['symbol'] = ($matches[1] == '-') ? $matches[1] : '+'; // May be needed for actions outside this function.
        // Fetch duration parts
        $m = ($allow_negative) ? $matches[1] : '';
        $result['year']   = intval($m.$matches[2]);
        $result['month']  = intval($m.$matches[3]);
        $result['day']    = intval($m.$matches[4]);
        $result['hour']   = intval($m.$matches[5]);
        $result['minute'] = intval($m.$matches[6]);
        $result['second'] = intval($m.$matches[7]);     
        return $result; 
    }
    else{
        return false;
    }
}

The function also supports negative formats. -P10Y9MT7M5S will return an array like: [year] => -10 [month] => -9 [day] => 0 [hour] => 0 [minute] => -7 [second] => -5 If this behaviour is not desired pass false as second parameter. This way the function will always return positive values. The min/plus symbol will still be available in result key ['symbol'].

And a little update: This function uses the first function to get the total amount of seconds.

function get_duration_seconds($iso_duration){
    // Get duration parts
    $duration = parse_duration($iso_duration, false);
    if($duration){
        extract($duration);
        $dparam  = $symbol; // plus/min symbol
        $dparam .= (!empty($year)) ? $year . 'Year' : '';
        $dparam .= (!empty($month)) ? $month . 'Month' : '';
        $dparam .= (!empty($day)) ? $day . 'Day' : '';
        $dparam .= (!empty($hour)) ? $hour . 'Hour' : '';
        $dparam .= (!empty($minute)) ? $minute . 'Minute' : '';
        $dparam .= (!empty($second)) ? $second . 'Second' : '';
        $date = '19700101UTC';
        return strtotime($date.$dparam) - strtotime($date);
    }
    else{
        // Not a valid iso duration
        return false;
    }
}

$foo = '-P1DT1S';
echo get_duration_seconds($foo); // Output -86399
$bar = 'P1DT1S';
echo get_duration_seconds($bar); // Output 86401

Be aware that converting durations that contain Days, Months, and/or Years into a duration like seconds can not be done accurately without knowing an actual starting date/time.

For Example

1 SEP 2010:  +P1M2D means +2764800 seconds
1 OCT 2010:  +P1M2D means +2851200 seconds

That's because September has 30-days, and October has 31-days. The same problem occurs with converting Year intervals, because of leap-years and leap-seconds. Leap-years introduce further complexity to the Month conversion as well - since February is one day longer in leap-years than it is otherwise. Days are problematic in areas where daylight saving time is practiced - a one-day period occurring during the DST transition, is actually 1-hour longer than it would be otherwise.

All that being said -- you can, of course, compress values containing only Hours, Minutes, and Seconds into values containing just Seconds. I'd suggest that you build a simple parser to do the job (or maybe consider a regular expression).

Just be aware of the pitfalls outlined above -- there there be dragons. If you intend to deal with Days, Months, and/or Years, you need to use one of the built-in mechanisms to do the math for you in the context of a known date/time. As others have mentioned: the DateInterval class, in combination withe the functions provided on the DateTime class is probably the most intuitive way to deal with this. But that's only available in PHP version 5.3.0 or greater.

If you have to work with less than v5.3.0, you can try to build something around this little gem:

$startDateTime = '19700101UTC';
$duration = strtotime( $startDateTime.'+1Year1Day1Second' ) - strtotime($startDateTime);
print("Duration in Seconds:  $duration");

strtotime won't work with the ISO 8601 format directly (eg. P1Y1DT1S), but the format that it does understand (1Year1Day1Second) is not too far off -- it would a pretty straight-forward conversion. (a little "hacky"... but that's PHP for you).

good luck!

function parsePTTime($pt_time)
{
    $string = str_replace('PT', '', $pt_time);
    $string = str_replace('H', 'Hour', $string);
    $string = str_replace('M', 'Minute', $string);
    $string = str_replace('S', 'Second', $string);

    $startDateTime = '19700101UTC';
    $seconds = strtotime($startDateTime . '+' . $string) - strtotime($startDateTime);

    return $seconds;
}

Tests:

PT1H         - OK
PT23M        - OK
PT45S        - OK
PT1H23M      - OK
PT1H45S      - OK
PT23M45S     - OK
PT1H23M45S   - OK

What you are looking for is DateTime::diff

The DateInterval object representing the difference between the two dates or FALSE on failure as illustrated below:

$datetime1 = new DateTime('2009-10-11');
$datetime2 = new DateTime('2009-10-13');
$interval = $datetime1->diff($datetime2);
echo $interval->format('%R%d days');

Just use seconds instead of dates.

This is from http://www.php.net/manual/en/datetime.diff.php

For a reference to DateInterval see http://www.php.net/manual/en/class.dateinterval.php

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top