将DateInterval格式化为ISO8601

5

我目前正在进行一个PHP项目,需要将DateInterval格式化为ISO8601(类似于这样):

P5D

这种格式可用于创建DateTime和DateInterval对象,但我无法想出一种方法将DateInterval格式化为此格式。是否有任何解决方案?如果没有,那么可能有什么轻量级的解决方案呢?

3个回答

5

Dave提供的解决方案Guss提供的解决方案的基础上,有一个解决办法可以添加ISO 8601标准的持续时间中的T部分,而不需要默认为P0M,即在用Y替换P0Y后插入两个附加检查。

  • Y0M替换为Y
  • P0M替换为P
  • D0H更改为TD0H并替换为DT
  • 删除多余的Y0M并替换为Y0M
这是因为间隔格式始终生成相同的结构,没有前导0,并且str_replace从左到右遍历数组中的每个替换。结果可能会收到P1YTPT作为返回值。通过使用rtrim来删除任何尾随的PT字符。然后使用所需的默认值替换空值,就像Guss的答案中所示。
ISO 8601断言: https://3v4l.org/c55kL 与PHP 5.3+兼容
使用static关键字是为了提高对函数的重复调用的性能,因为静态变量在离开函数范围后不会失去它们的值。
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;
}

测试

为了比较,我创建了一个包含每种可能的持续时间组合(不包括微秒)且值为1的数组。可以在上面的示例链接中查看。

$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));

结果

PT0S

使用微秒的PHP 7.1+ https://3v4l.org/VFN7N

您可以通过将第一个数组值的S0F替换为S并在DateInterval :: format()中添加%fF轻松添加微秒。

但是,F(微秒)目前不是DateInterval :: __construct()或ISO 8601标准支持的时间间隔规范。

注意:PHP <= 7.2.13存在 DateTime :: diff 错误,当差异小于一秒时,会阻止正确检索DateInterval

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;
}

测试

$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));

结果

PT0F
PT21F

3

关于@dave的问题,我重新实现了他的解决方案,并解决了一些问题,特别是始终保留一个月份字段的要求。根据ISO-8601的维基百科文章,似乎T标识是必须的,即使在上述实现中完全缺失。通过引入它,我们可以解决大部分问题并编写更清晰的代码:

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;
}

请注意,如果不需要T,我们会将其删除,所以现在M的两种用法都可以正常工作。
"P5M" == date_interval_iso_format(new DateInterval("P5M")); // => true
"PT5M" == date_interval_iso_format(new DateInterval("PT5M")); // => true

我已经更新了上面的原始代码,以处理“零间隔”这种边缘情况,原始代码将其表示为“P”- 这会破坏PHP的DateInterval解析器(在@dave的实现中不存在该问题,因为他总是至少保留0M)。 - Guss

2

好的,如果您在构建格式时查看规范:

Y年

M月

D日

W周。这些将被转换为天数,因此不能与D组合使用。

H小时

M分钟

S秒

然后查看您要使用的内容(http://php.net/manual/en/dateinterval.format.php),似乎您需要做的是:

$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

现在,我做的唯一不同之处是我总是包括月份,即使为0。原因是分钟月份都由M表示 - 如果我们始终包括月份,那么如果有分钟,我们就知道它是分钟。否则,我们必须进行大量逻辑来查看是否需要将P更改为PT,以便它知道此时M代表分钟

例如:

// For 3 Months
new DateInterval("P3M");
// For 3 Minutes
new DateInterval("PT3M"));

但实际上我们做的是:

// For 3 Months
new DateInterval("P3M");
// For 3 Minutes
new DateInterval("P0M3M"));

6
有点遗憾的是,PHP没有提供这个功能。需要另一个函数来修复错误或缺失的功能,耶! - Zwirbelbart

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接