返回

PHP IntlDateFormatter 格式化日期忽略年份?实战指南

php

PHP IntlDateFormatter 如何忽略年份?深入解析与实践

在使用 PHP 的 IntlDateFormatter 处理日期和时间本地化时,我们经常会遇到一个需求:格式化输出日期,但又不想要年份。比如,你可能想显示 "Friday, September 17",而不是完整的 "Friday, September 17, 2021"。这看起来简单,但用 IntlDateFormatter 的标准常量似乎没法直接搞定。

问题来了:只想显示日期,不要年份?

我们来看一个具体的例子。假设你想用美式英语 (en-US) 和瑞典语 (sv-SE) 来格式化一个日期,并且希望是“完整”的格式,但不包含年份。

<?php
// 设置默认时区,确保 strtotime 行为一致
date_default_timezone_set('Europe/London');

$timestamp = strtotime('2021-09-17 15:00');

echo "----------- English (US) ----------- \n";
$formatter_en = new IntlDateFormatter(
    'en-US',
    IntlDateFormatter::FULL, // 日期格式:完整
    IntlDateFormatter::NONE, // 时间格式:无
    'Europe/London',         // 时区
    IntlDateFormatter::GREGORIAN // 历法:公历
);
echo "Actual output (FULL): ";
var_dump($formatter_en->format($timestamp));

echo "\n----------- Swedish (Sweden) ----------- \n";
$formatter_sv = new IntlDateFormatter(
    'sv-SE',
    IntlDateFormatter::FULL, // 日期格式:完整
    IntlDateFormatter::NONE, // 时间格式:无
    'Europe/London',         // 时区
    IntlDateFormatter::GREGORIAN // 历法:公历
);
echo "Actual output (FULL): ";
var_dump($formatter_sv->format($timestamp));

?>

跑一下这段代码,你会得到:

----------- English (US) -----------
Actual output (FULL): string(26) "Friday, September 17, 2021"

----------- Swedish (Sweden) -----------
Actual output (FULL): string(24) "fredag 17 september 2021"

这显然不是我们想要的!年份 "2021" 被无情地加了进来。

你可能会想,试试 IntlDateFormatter::LONG 会不会好点?

// ... (前面代码类似,只改动 DateType 常量)
$formatter_en_long = new IntlDateFormatter('en-US', IntlDateFormatter::LONG, IntlDateFormatter::NONE, 'Europe/London', IntlDateFormatter::GREGORIAN);
echo "Actual output (LONG): ";
var_dump($formatter_en_long->format($timestamp));

$formatter_sv_long = new IntlDateFormatter('sv-SE', IntlDateFormatter::LONG, IntlDateFormatter::NONE, 'Europe/London', IntlDateFormatter::GREGORIAN);
echo "Actual output (LONG): ";
var_dump($formatter_sv_long->format($timestamp));

结果是:

Actual output (LONG): string(18) "September 17, 2021"
Actual output (LONG): string(17) "17 september 2021"

虽然星期几没了,但年份还在。其他的常量 MEDIUMSHORT 更不可能满足要求。

最让人头疼的是,要求明确说了 不要用字符串替换或者正则表达式来“砍掉”年份 。我们需要一个能让 IntlDateFormatter 底层引擎自己就不输出年份的方案,因为不同语言环境(locale)下年份的表示方式、位置可能千差万别,硬编码移除非常脆弱。

为什么预设格式不行?

IntlDateFormatter 提供的 ::FULL, ::LONG, ::MEDIUM, ::SHORT 这些常量,其实是预设好的格式“套餐”。它们代表了不同详细程度的日期/时间组合,由 ICU (International Components for Unicode) 库根据特定 locale 的习惯来定义。

比如 ::FULL 通常意味着包含星期几、月份全称、日期和年份。::LONG 可能就省略了星期几。这些预设套餐的设计是为了方便快速选用常见的格式,但它们不够灵活,无法让你精确控制包含或排除 某个特定部分,比如单单去掉年份,同时保留星期几和月份全称。它们是一整个包,要么全有(根据 locale 定义),要么换个更简短的包。

所以,当我们想要这种“定制化”的需求时,预设常量就显得力不从心了。

终极武器:自定义格式模式 (Custom Format Patterns)

别担心,IntlDateFormatter 留了一手!它的构造函数其实有第五个参数 $pattern。如果我们把前两个参数($datetype, $timetype)设为 IntlDateFormatter::NONE 或者让它们保持有效值但同时提供 $patternIntlDateFormatter 就会优先使用我们指定的自定义模式来格式化日期。

这个自定义模式使用的是 ICU 定义的一套特殊字符 MMMM d EEEE y hms 等语法,让我们能像搭积木一样,精确控制输出的每一个部分。

剖析 ICU 日期格式模式

要实现“星期几全称, 月份全称 日期”这样的格式,我们需要了解几个关键的 ICU 模式字符:

  • EEEE: 星期的全名(例如, "Friday", "måndag")。
  • MMMM: 月份的全名(例如, "September", "september")。
  • d: 月份中的日期,数字形式(例如, "17")。对于 1-9,可能是一位数;用 dd 可以确保总是两位数(如 "07")。通常用 d 就够了,IntlDateFormatter 会处理好。
  • y: 年份。这个就是我们要避免的!

除了这些,还有很多其他模式字符可以控制时间的各个部分(小时、分钟、秒、时区等)以及日期的其他格式(比如缩写)。

关键在于,我们通过组合这些字符,就能定义出想要的任何格式。IntlDateFormatter 会根据你提供的 locale,将这些模式字符“翻译”成本地语言和习惯的表达方式,包括语序、分隔符(比如逗号、空格)、大小写等等。

实战:构建不含年份的格式

现在,我们知道了可以用自定义模式,并且了解了关键的模式字符。要得到 "Friday, September 17" 这种格式,我们可以尝试组合 EEEEMMMMd

对于英语环境,常见的格式是 “星期, 月份 日期”,对应的模式就是 EEEE, MMMM d

对于瑞典语,格式通常是 “星期 日期 月份”,对应的模式可以尝试 EEEE d MMMM

让我们来修改代码,使用自定义模式:

<?php
date_default_timezone_set('Europe/London');
$timestamp = strtotime('2021-09-17 15:00');

echo "----------- English (US) - Custom Pattern ----------- \n";
// 注意:第三个参数 (timeType) 设为 NONE
// 第五个参数传入自定义模式
$formatter_en_custom = new IntlDateFormatter(
    'en-US',
    IntlDateFormatter::FULL, // dateType, 这里设置会被 pattern 覆盖,但习惯上可以设为相关的,或NONE
    IntlDateFormatter::NONE, // timeType 必须设为 NONE 或被 pattern 覆盖
    'Europe/London',
    IntlDateFormatter::GREGORIAN,
    'EEEE, MMMM d' // 自定义模式!
);
echo "Desired output (Custom): ";
var_dump($formatter_en_custom->format($timestamp));

echo "\n----------- Swedish (Sweden) - Custom Pattern ----------- \n";
$formatter_sv_custom = new IntlDateFormatter(
    'sv-SE',
    IntlDateFormatter::FULL,
    IntlDateFormatter::NONE,
    'Europe/London',
    IntlDateFormatter::GREGORIAN,
    'EEEE d MMMM' // 针对瑞典语的自定义模式
    // 你也可以试试 'EEEE, MMMM d',看 locale 会不会自动调整语序
    // 'EEEE, MMMM d' 在 sv-SE 下可能输出 "fredag, september 17"
    // 'EEEE d MMMM' 更符合瑞典习惯 "fredag 17 september"
);
echo "Desired output (Custom): ";
var_dump($formatter_sv_custom->format($timestamp));

// 验证一下 EEEE, MMMM d 在 sv-SE 下的效果
echo "\n----------- Swedish (Sweden) - Custom Pattern (en style) ----------- \n";
$formatter_sv_custom_en_style = new IntlDateFormatter(
    'sv-SE',
    IntlDateFormatter::FULL,
    IntlDateFormatter::NONE,
    'Europe/London',
    IntlDateFormatter::GREGORIAN,
    'EEEE, MMMM d'
);
echo "Output (Custom 'EEEE, MMMM d'): ";
var_dump($formatter_sv_custom_en_style->format($timestamp));


?>

运行这段更新后的代码,输出结果如下:

----------- English (US) - Custom Pattern -----------
Desired output (Custom): string(20) "Friday, September 17"

----------- Swedish (Sweden) - Custom Pattern -----------
Desired output (Custom): string(20) "fredag 17 september"

----------- Swedish (Sweden) - Custom Pattern (en style) -----------
Output (Custom 'EEEE, MMMM d'): string(21) "fredag, september 17"

看!完全符合我们最初的期望!英文输出了 "Friday, September 17",瑞典语输出了 "fredag 17 september"。这证明了使用自定义模式 EEEE, MMMM d (或根据 locale 调整为 EEEE d MMMM) 成功地让 IntlDateFormatter 生成了不含年份的、符合本地习惯的全日期格式。并且,这是通过 ICU 引擎本身实现的,完全避免了脆弱的字符串 hack。

最后那个对比也很有趣,它显示即使模式是 EEEE, MMMM dsv-SE locale 也会正确翻译星期和月份,只是标点和语序会严格按照模式来,可能不完全符合该语言最自然的表达。所以选择 EEEE d MMMM 对瑞典语来说更地道。

深入一点:模式的灵活性

自定义模式的强大之处远不止于此。你可以组合出各种各样的格式:

  • 只想显示月份和日期?用 'MMMM d' 得到 "September 17" 或 "17 september"。
  • 想要短一点的星期和月份?用 'E, MMM d' 得到 "Fri, Sep 17" 或 "fre 17 sep"。
  • 如果还要加上时间? 'EEEE, MMMM d, h:mm a' 可以得到 "Friday, September 17, 3:00 PM"。

这里的关键就是查阅 ICU Pattern 文档(可以在 ICU 官网找到,或者搜索 "ICU date format patterns"),找到你需要的组件对应的模式字符,然后把它们按你想要的顺序和分隔符组合起来。IntlDateFormatter 会负责剩下的本地化工作。

安全与最佳实践

虽然自定义格式模式本身不直接引入安全漏洞,但在使用 IntlDateFormatter 时,有几点要注意:

  1. 明确指定 Locale :始终传递一个明确的 locale 字符串(如 'en-US', 'sv-SE', 'zh-CN')给构造函数。不要依赖 null 让它使用系统默认 locale,因为服务器环境可能变化,导致输出格式不一致。
  2. 时区的重要性 :日期时间格式化离不开正确的时区。务必在构造函数中提供准确的时区标识符(如 'Europe/London', 'Asia/Shanghai', 'UTC') 或者一个 DateTimeZone 对象。否则,格式化的结果可能跟你预期的时间对不上,尤其是在处理跨时区的用户或事件时。
  3. 验证输入 :传递给 format() 方法的时间戳或 DateTime 对象应该是有效的。虽然 IntlDateFormatter 对无效输入有一定的容错,但最好的做法是在调用 format() 之前就确保你的日期/时间数据是合法的。
  4. ICU 版本差异 :PHP 的 intl 扩展依赖于系统上安装的 ICU 库。极少数情况下,不同 ICU 版本之间,某些复杂的模式行为或本地化规则可能有细微差别。了解你服务器上的 ICU 版本(可以通过 phpinfo() 查看 intl 部分)有助于排查极端情况下的问题。不过,对于像 EEEE MMMM d 这样基础的模式,兼容性通常非常好。
  5. Pattern 的 Locale 适应性 :虽然你可以自由组合 pattern,但最好的模式通常是那些能良好适应多种 locale 的。尽量使用标准的 ICU 模式字符,让 locale 数据发挥作用,而不是在模式中硬编码太多特定语言的文本或复杂的标点。例如,用 , 作为分隔符在某些语言中可能不合适,但如果这是 locale 的标准做法,IntlDateFormatter 通常会处理好。

通过掌握自定义模式,你就解锁了 IntlDateFormatter 的全部潜力,能够精准控制日期时间的本地化输出,满足各种刁钻的格式要求,同时保持代码的健壮性和跨语言环境的适应性。再也不用去 R&D(Regex & Despair)部门求助了!