PHP IntlDateFormatter 格式化日期忽略年份?实战指南
2025-04-22 00:21:31
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"
虽然星期几没了,但年份还在。其他的常量 MEDIUM
和 SHORT
更不可能满足要求。
最让人头疼的是,要求明确说了 不要用字符串替换或者正则表达式来“砍掉”年份 。我们需要一个能让 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
或者让它们保持有效值但同时提供 $pattern
,IntlDateFormatter
就会优先使用我们指定的自定义模式来格式化日期。
这个自定义模式使用的是 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" 这种格式,我们可以尝试组合 EEEE
、MMMM
和 d
。
对于英语环境,常见的格式是 “星期, 月份 日期”,对应的模式就是 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 d
,sv-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
时,有几点要注意:
- 明确指定 Locale :始终传递一个明确的 locale 字符串(如
'en-US'
,'sv-SE'
,'zh-CN'
)给构造函数。不要依赖null
让它使用系统默认 locale,因为服务器环境可能变化,导致输出格式不一致。 - 时区的重要性 :日期时间格式化离不开正确的时区。务必在构造函数中提供准确的时区标识符(如
'Europe/London'
,'Asia/Shanghai'
,'UTC'
) 或者一个DateTimeZone
对象。否则,格式化的结果可能跟你预期的时间对不上,尤其是在处理跨时区的用户或事件时。 - 验证输入 :传递给
format()
方法的时间戳或DateTime
对象应该是有效的。虽然IntlDateFormatter
对无效输入有一定的容错,但最好的做法是在调用format()
之前就确保你的日期/时间数据是合法的。 - ICU 版本差异 :PHP 的
intl
扩展依赖于系统上安装的 ICU 库。极少数情况下,不同 ICU 版本之间,某些复杂的模式行为或本地化规则可能有细微差别。了解你服务器上的 ICU 版本(可以通过phpinfo()
查看intl
部分)有助于排查极端情况下的问题。不过,对于像EEEE MMMM d
这样基础的模式,兼容性通常非常好。 - Pattern 的 Locale 适应性 :虽然你可以自由组合 pattern,但最好的模式通常是那些能良好适应多种 locale 的。尽量使用标准的 ICU 模式字符,让 locale 数据发挥作用,而不是在模式中硬编码太多特定语言的文本或复杂的标点。例如,用
,
作为分隔符在某些语言中可能不合适,但如果这是 locale 的标准做法,IntlDateFormatter
通常会处理好。
通过掌握自定义模式,你就解锁了 IntlDateFormatter
的全部潜力,能够精准控制日期时间的本地化输出,满足各种刁钻的格式要求,同时保持代码的健壮性和跨语言环境的适应性。再也不用去 R&D(Regex & Despair)部门求助了!