[PHP-CVS] [php-src] master: ext/intl: GH-20255 IntlDateFormatter adding proleptic gregorian calendar support.
[email protected] (David Carlier) Mon, 3 Aug 2026 07:02:02 +0000
| Newsgroups | php.cvs |
|---|---|
| Message-ID | <[email protected]> |
Author: David Carlier (devnexen)
Date: 2026-08-03T08:01:46+01:00
Commit: https://github.com/php/php-src/commit/25a2d9755eb7638732a4775efb3dff1a3fa5d142
Raw diff: https://github.com/php/php-src/commit/25a2d9755eb7638732a4775efb3dff1a3fa5d142.diff
ext/intl: GH-20255 IntlDateFormatter adding proleptic gregorian calendar support.
To be consistent with DateImmutable class, we add the possibility to set
the calendar in a (real) proleptic gregorian via a new flag constant.
For now, intention needs to be clear but can be made default eventually.
Close GH-21101
Changed paths:
A ext/intl/tests/gh20255.phpt
M NEWS
M ext/intl/dateformat/dateformat.stub.php
M ext/intl/dateformat/dateformat_arginfo.h
M ext/intl/dateformat/dateformat_helpers.cpp
M ext/intl/tests/dateformat___construct_bad_tz_cal.phpt
M ext/intl/tests/dateformat_errors.phpt
Diff:
diff --git a/NEWS b/NEWS
index 4e95bed34c84..1af715a1bc75 100644
--- a/NEWS
+++ b/NEWS
@@ -14,6 +14,8 @@ PHP NEWS
string. (Weilin Du)
. Fixed IntlListFormatter::__construct() leaving stale global error state
after successful calls. (Weilin Du)
+ . Implemented GH-20255 (Add a predefined calendar constant in
+ IntlDateFormatter for the proleptic gregorian calendar). (David Carlier)
- Reflection:
. Added ReflectionAttribute::inNamespace(),
diff --git a/ext/intl/dateformat/dateformat.stub.php b/ext/intl/dateformat/dateformat.stub.php
index 89ebc5f61c0e..dfc6c09f95b5 100644
--- a/ext/intl/dateformat/dateformat.stub.php
+++ b/ext/intl/dateformat/dateformat.stub.php
@@ -31,6 +31,8 @@ class IntlDateFormatter
/** @cvalue UCAL_TRADITIONAL */
public const int TRADITIONAL = UNKNOWN;
+ public const int PROLEPTIC_GREGORIAN = -16;
+
/**
* @param IntlCalendar|int|null $calendar
*/
diff --git a/ext/intl/dateformat/dateformat_arginfo.h b/ext/intl/dateformat/dateformat_arginfo.h
index 2d297b26a042..07c546648f48 100644
--- a/ext/intl/dateformat/dateformat_arginfo.h
+++ b/ext/intl/dateformat/dateformat_arginfo.h
@@ -1,5 +1,5 @@
/* This is a generated file, edit dateformat.stub.php instead.
- * Stub hash: 160d05ec65c45b66b13eaecbef20b3c59bfb33d1 */
+ * Stub hash: 13d9fe2cca75fcda5365a589ee318f76fcba3b84 */
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_IntlDateFormatter___construct, 0, 0, 1)
ZEND_ARG_TYPE_INFO(0, locale, IS_STRING, 1)
@@ -219,5 +219,11 @@ static zend_class_entry *register_class_IntlDateFormatter(void)
zend_declare_typed_class_constant(class_entry, const_TRADITIONAL_name, &const_TRADITIONAL_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
zend_string_release_ex(const_TRADITIONAL_name, true);
+ zval const_PROLEPTIC_GREGORIAN_value;
+ ZVAL_LONG(&const_PROLEPTIC_GREGORIAN_value, -16);
+ zend_string *const_PROLEPTIC_GREGORIAN_name = zend_string_init_interned("PROLEPTIC_GREGORIAN", sizeof("PROLEPTIC_GREGORIAN") - 1, true);
+ zend_declare_typed_class_constant(class_entry, const_PROLEPTIC_GREGORIAN_name, &const_PROLEPTIC_GREGORIAN_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG));
+ zend_string_release_ex(const_PROLEPTIC_GREGORIAN_name, true);
+
return class_entry;
}
diff --git a/ext/intl/dateformat/dateformat_helpers.cpp b/ext/intl/dateformat/dateformat_helpers.cpp
index 747105bb4f90..d10acb7273fe 100644
--- a/ext/intl/dateformat/dateformat_helpers.cpp
+++ b/ext/intl/dateformat/dateformat_helpers.cpp
@@ -26,6 +26,9 @@ extern "C" {
#include "../calendar/calendar_class.h"
}
+// Artificial value to set for a pure proleptic gregorian calendar (until icu provides it eventually)
+#define UCAL_PHP_PROLEPTIC_GREGORIAN -16
+
using icu::GregorianCalendar;
zend_result datefmt_process_calendar_arg(
@@ -43,22 +46,31 @@ zend_result datefmt_process_calendar_arg(
} else if (!calendar_obj) {
zend_long v = calendar_long;
- if (v != (zend_long)UCAL_TRADITIONAL && v != (zend_long)UCAL_GREGORIAN) {
+ if (v != (zend_long)UCAL_TRADITIONAL && v != (zend_long)UCAL_GREGORIAN &&
+ v != (zend_long)UCAL_PHP_PROLEPTIC_GREGORIAN) {
intl_errors_set(err, U_ILLEGAL_ARGUMENT_ERROR,
"Invalid value for calendar type; it must be one of "
"IntlDateFormatter::TRADITIONAL (locale's default calendar) or"
- " IntlDateFormatter::GREGORIAN. Alternatively, it can be an "
+ " IntlDateFormatter::GREGORIAN or IntlDateFormatter::PROLEPTIC_GREGORIAN."
+ " Alternatively, it can be an "
"IntlCalendar object");
return FAILURE;
} else if (v == (zend_long)UCAL_TRADITIONAL) {
cal = Calendar::createInstance(locale, status);
} else { //UCAL_GREGORIAN
- cal = new GregorianCalendar(locale, status);
+ GregorianCalendar *gcal = new GregorianCalendar(locale, status);
+ if (v == (zend_long)UCAL_PHP_PROLEPTIC_GREGORIAN) {
+ // set the Julian to gregorian cutover date to -infinity
+ // to make it a proleptic gregorian calendar
+ // TODO: consider making it default behavior over typical "gregorian" icu like calendar
+ gcal->setGregorianChange(-std::numeric_limits<double>::infinity(), status);
+ }
+ cal = gcal;
}
+
calendar_owned = true;
cal_int_type = calendar_long;
-
} else if (calendar_obj) {
cal = calendar_fetch_native_calendar(calendar_obj);
if (cal == NULL) {
diff --git a/ext/intl/tests/dateformat___construct_bad_tz_cal.phpt b/ext/intl/tests/dateformat___construct_bad_tz_cal.phpt
index 480074dbd822..1d0d17ba42a3 100644
--- a/ext/intl/tests/dateformat___construct_bad_tz_cal.phpt
+++ b/ext/intl/tests/dateformat___construct_bad_tz_cal.phpt
@@ -23,5 +23,5 @@ try {
?>
--EXPECT--
IntlException: IntlDateFormatter::__construct(): No such time zone: "bad timezone"
-IntlException: IntlDateFormatter::__construct(): Invalid value for calendar type; it must be one of IntlDateFormatter::TRADITIONAL (locale's default calendar) or IntlDateFormatter::GREGORIAN. Alternatively, it can be an IntlCalendar object
+IntlException: IntlDateFormatter::__construct(): Invalid value for calendar type; it must be one of IntlDateFormatter::TRADITIONAL (locale's default calendar) or IntlDateFormatter::GREGORIAN or IntlDateFormatter::PROLEPTIC_GREGORIAN. Alternatively, it can be an IntlCalendar object
TypeError: IntlDateFormatter::__construct(): Argument #5 ($calendar) must be of type IntlCalendar|int|null, stdClass given
diff --git a/ext/intl/tests/dateformat_errors.phpt b/ext/intl/tests/dateformat_errors.phpt
index 2c14c559b69a..23617bd36e87 100644
--- a/ext/intl/tests/dateformat_errors.phpt
+++ b/ext/intl/tests/dateformat_errors.phpt
@@ -26,8 +26,8 @@ var_dump(intl_get_error_message());
?>
--EXPECT--
-IntlException: IntlDateFormatter::__construct(): Invalid value for calendar type; it must be one of IntlDateFormatter::TRADITIONAL (locale's default calendar) or IntlDateFormatter::GREGORIAN. Alternatively, it can be an IntlCalendar object
+IntlException: IntlDateFormatter::__construct(): Invalid value for calendar type; it must be one of IntlDateFormatter::TRADITIONAL (locale's default calendar) or IntlDateFormatter::GREGORIAN or IntlDateFormatter::PROLEPTIC_GREGORIAN. Alternatively, it can be an IntlCalendar object
NULL
-string(245) "IntlDateFormatter::create(): Invalid value for calendar type; it must be one of IntlDateFormatter::TRADITIONAL (locale's default calendar) or IntlDateFormatter::GREGORIAN. Alternatively, it can be an IntlCalendar object: U_ILLEGAL_ARGUMENT_ERROR"
+string(287) "IntlDateFormatter::create(): Invalid value for calendar type; it must be one of IntlDateFormatter::TRADITIONAL (locale's default calendar) or IntlDateFormatter::GREGORIAN or IntlDateFormatter::PROLEPTIC_GREGORIAN. Alternatively, it can be an IntlCalendar object: U_ILLEGAL_ARGUMENT_ERROR"
NULL
-string(234) "datefmt_create(): Invalid value for calendar type; it must be one of IntlDateFormatter::TRADITIONAL (locale's default calendar) or IntlDateFormatter::GREGORIAN. Alternatively, it can be an IntlCalendar object: U_ILLEGAL_ARGUMENT_ERROR"
+string(276) "datefmt_create(): Invalid value for calendar type; it must be one of IntlDateFormatter::TRADITIONAL (locale's default calendar) or IntlDateFormatter::GREGORIAN or IntlDateFormatter::PROLEPTIC_GREGORIAN. Alternatively, it can be an IntlCalendar object: U_ILLEGAL_ARGUMENT_ERROR"
diff --git a/ext/intl/tests/gh20255.phpt b/ext/intl/tests/gh20255.phpt
new file mode 100644
index 000000000000..55e9b7b37afe
--- /dev/null
+++ b/ext/intl/tests/gh20255.phpt
@@ -0,0 +1,54 @@
+--TEST--
+IntlDateFormatter with PROLEPTIC_GREGORIAN calendar
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php if (PHP_INT_SIZE < 8) die('skip 64-bit only'); ?>
+--FILE--
+<?php
+var_dump(IntlDateFormatter::PROLEPTIC_GREGORIAN);
+
+// A pre-cutover date: DateTime uses proleptic Gregorian internally (cannot be represented in 32 bits systems)
+$dt = new DateTime('1200-03-01 12:00:00 UTC');
+
+// New constant
+$fmt_proleptic = new IntlDateFormatter(
+ 'en_US', IntlDateFormatter::NONE, IntlDateFormatter::NONE,
+ 'UTC', IntlDateFormatter::PROLEPTIC_GREGORIAN, 'yyyy-MM-dd'
+);
+
+// Existing workaround
+$cal = new IntlGregorianCalendar('UTC', 'en_US');
+$cal->setGregorianChange(-INF);
+$fmt_workaround = new IntlDateFormatter(
+ 'en_US', IntlDateFormatter::NONE, IntlDateFormatter::NONE,
+ 'UTC', $cal, 'yyyy-MM-dd'
+);
+
+// Default hybrid Gregorian
+$fmt_hybrid = new IntlDateFormatter(
+ 'en_US', IntlDateFormatter::NONE, IntlDateFormatter::NONE,
+ 'UTC', IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd'
+);
+
+$proleptic = $fmt_proleptic->format($dt);
+$workaround = $fmt_workaround->format($dt);
+$hybrid = $fmt_hybrid->format($dt);
+
+// Should round-trip the proleptic Gregorian date correctly
+echo "Proleptic: $proleptic\n";
+
+// Must match the manual workaround
+echo "Matches workaround: ";
+var_dump($proleptic === $workaround);
+
+// Must differ from hybrid for pre-cutover dates
+echo "Differs from hybrid: ";
+var_dump($proleptic !== $hybrid);
+
+?>
+--EXPECT--
+int(-16)
+Proleptic: 1200-03-01
+Matches workaround: bool(true)
+Differs from hybrid: bool(true)