Parse and create ISO 8601 Date and time intervals, like PT15M in PHP
Asked Answered
I

5

34

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.

Iseabal answered 15/9, 2010 at 19:27 Comment(3)
A very late caution ... converting P1MT to seconds would be impossible unless you know whether you're converting a month with 28, 29, 30, or 31 days. For that matter, you can't even convert P1YT into 365 days without being wrong one year in four. (There's a reason why intervals aren't specified in days or seconds in the standard.)Ike
@Bevan: The Answer by Lee goes into details with this. TL;DR: you need a starting date.Iseabal
Ahh - thanks @berkes, I missed than when skimming the page (I found it while looking for something unrelated).Ike
P
19

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.

Plantaineater answered 15/9, 2010 at 19:42 Comment(2)
DateInterval('PT1H') works likea charm! Now, I might want to look into a nicer way to rebuild these strings, other then PTXXXS, everyting in second :)Iseabal
⚠️ PHP's DateInterval() has a bug that doesn't allow fractions in the ISO 8601 representations it parses: bugs.php.net/bug.php?id=53831Recapitulation
C
8

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
Conwell answered 14/12, 2012 at 20:25 Comment(4)
Hi, is there a more straightforward method by now with PHP 5.4? Not that I don't want to thank you very much for this function, but maybe there's a cleaner method already.Ecchymosis
I wrote this ages ago heheh. I guess I could rewrite the function. Little short on time now, but I'll check it out later.Conwell
Can you explain the pipe | characters? They are not followed by an alternative branch, but they are not preceded by a question mark ?| to join backreferences together either. php.net/manual/de/regexp.reference.subpatterns.php An empty alternative isn't needed, because the ? quantifier is used.Xenocryst
I wrote this ages ago :-) I cannot recall the reason. You may very well be right.Conwell
C
7

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!

Carrera answered 15/9, 2010 at 21:12 Comment(2)
Thinking about this intuitively I'd expect that rewriting an absolute number of seconds as years + months + days + hours + minutes + seconds to make it more readable will not change its actual value, meaning that wherever there's a variable interval, the standard value will be chosen. So, for months, that would be 30 days = 1 calendar month. I'm trying to find where exactly they specify this in the standard itself but it's getting late, and I can only see the final draft without paying: xml.coverpages.org/ISO-FDIS-8601.pdfPlantaineater
At first it does seem that that would be the intuitive answer, but if you really think about it, it becomes apparent that things really can't work like that. Turns out that "calendar time" (Days, Months, Years) is just as important as "absolute time" (Seconds, Minutes, Hours) -- when we use calendar time, we mean something fundamentally different than when we use absolute time. For example: How far apart are your birthdays? Always 1-year, not always 8760-hours. If last month's credit card bill was dated Aug 21, what will be the date of this month's? Sept 21. Always 1-month, not 30-days.Carrera
C
1
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
Cheney answered 12/5, 2017 at 7:47 Comment(0)
P
0

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

Penny answered 15/9, 2010 at 19:53 Comment(1)
I fail to see how this helps to convert to and fro the PT10M -alike ISO 8601 formats.Iseabal

© 2022 - 2024 — McMap. All rights reserved.