Reputation: 827
I am currently working on a php project and need to format a DateInterval as ISO8601 (something like this):
P5D
This format can be used to create DateTime and DateInterval objects, but I can't figure out a way to format a DateInterval into this format. Is there any? If not, what might be a lightweight solution to that?
Upvotes: 7
Views: 3591
Reputation: 18416
To expand on the solution provided by Dave and the solution provided by Guss.
A workaround to add the T
portion of the duration of the ISO 8601 standard and not need to default as P0M
, is to insert 2 additional checks after the P0Y
replaced by Y
.
Y0M
replaced by Y
P0M
replaced by P
D0H
to TD0H
replaced by DT
Y0M
replaced by Y0M
This works because the interval format will always generate the same structure, without leading 0's and str_replace
walks through each replacement in the arrays from left to right.
As a result there is a possibility that we could receive P1YT
or PT
as a return value. Simply remove any trailing PT
characters by using rtrim
. Then substitute an empty value with a desired default value, as in the answer by Guss.
Compatible with PHP 5.3+
The use of the static keyword is to improve performance of repeated calls to the function, as the static variables do not lose their values after leaving the function scope
function date_interval_iso(DateInterval $interval, $default = 'PT0S') {
static $f = array('M0S', 'H0M', 'DT0H', 'M0D', 'P0Y', 'Y0M', 'P0M');
static $r = array('M', 'H', 'DT', 'M', 'P', 'Y', 'P');
return rtrim(str_replace($f, $r, $interval->format('P%yY%mM%dDT%hH%iM%sS')), 'PT') ?: $default;
}
Test
To compare I created an array containing every possible duration combination (excluding microseconds) with a value of 1. Which can be viewed in the example link above.
$durations ['P1Y', /*...*/ 'P1Y1M1DT1H1M1S'];
$isos = array();
foreach ($durations as $duration) {
$isos[] = date_interval_iso(new DateInterval($duration));
}
$diff = array_diff($durations, $isos);
if (!empty($diff)) {
//output any differences
var_dump($diff);
}
//test 0 duration DateInterval
$date1 = new DateTime();
echo date_interval_iso($date1->diff($date1));
Result
PT0S
You can add microseconds easily, by including S0F
replaced by S
of the first array values and adding %fF
to your DateInterval::format()
.
However F
(microseconds) is not currently a supported interval specification for DateInterval::__construct()
or the ISO 8601 standard.
NOTE: there was a bug in PHP <= 7.2.13 with
DateTime::diff
that prevents the properDateInterval
retrieval when the difference was less than one second.
function date_interval_iso(DateInterval $interval, string $default = 'PT0F') {
static $f = ['S0F', 'M0S', 'H0M', 'DT0H', 'M0D', 'P0Y', 'Y0M', 'P0M'];
static $r = ['S', 'M', 'H', 'DT', 'M', 'P', 'Y', 'P'];
return rtrim(str_replace($f, $r, $interval->format('P%yY%mM%dDT%hH%iM%sS%fF')), 'PT') ?: $default;
}
Test
$date1 = new DateTimeImmutable();
$date2 = new DateTimeImmutable();
//test 0 duration DateInterval
echo date_interval_iso($date1->diff($date1));
//test microseconds
echo date_interval_iso($date2->diff($date1));
Result
PT0F
PT21F
Upvotes: 5
Reputation: 32364
With regards to @dave, I re-implemented his solution in a way that solves some of the problems, specifically the requirement to always leave a month field. From the Wikipedia article on ISO-8601, it doesn't seem like the T
designation is optional, even through its completely missing from the above mentioned implementation. By introducing it we can solve most of our problems and make for some cleaner code:
function date_interval_iso_format(DateInterval $interval) {
list($date,$time) = explode("T",$interval->format("P%yY%mM%dDT%hH%iM%sS"));
// now, we need to remove anything that is a zero, but make sure to not remove
// something like 10D or 20D
$res =
str_replace([ 'M0D', 'Y0M', 'P0Y' ], [ 'M', 'Y', 'P' ], $date) .
rtrim(str_replace([ 'M0S', 'H0M', 'T0H'], [ 'M', 'H', 'T' ], "T$time"),"T");
if ($res == 'P') // edge case - if we remove everything, DateInterval will hate us later
return 'PT0S';
return $res;
}
Note that we remove the T if it is not needed, so now both uses of M
work fine:
"P5M" == date_interval_iso_format(new DateInterval("P5M")); // => true
"PT5M" == date_interval_iso_format(new DateInterval("PT5M")); // => true
Upvotes: 3
Reputation: 64677
Well, if you look at the spec for the format when you construct one:
Y years
M months
D days
W weeks. These get converted into days, so can not be combined with D.
H hours
M minutes
S seconds
Then look at what you have to work with (http://php.net/manual/en/dateinterval.format.php), it seems like what you would do is:
$dateInterval = new DateInterval( /* whatever */ );
$format = $dateInterval->format("P%yY%mM%dD%hH%iM%sS");
//P0Y0M5D0H0M0S
//now, we need to remove anything that is a zero, but make sure to not remove
//something like 10D or 20D
$format = str_replace(["M0S", "H0M", "D0H", "M0D", "Y0M", "P0Y"], ["M", "H", "D", "M", "Y0M", "P"], $format);
echo $format;
//P0M5D
Now, the one thing I did differently is I always include the months, even if it is 0. The reason for this is that minutes
and months
are both represented by M
- if we always include the month, then if there is a minute we know it is minutes. Otherwise we have to do a bunch of logic to see if we need to change the P
to PT
so it knows that the a M
in this instance stands for Minute
.
For example:
// For 3 Months
new DateInterval("P3M");
// For 3 Minutes
new DateInterval("PT3M"));
But instead we do:
// For 3 Months
new DateInterval("P3M");
// For 3 Minutes
new DateInterval("P0M3M"));
Upvotes: 2