Commit 25a2d9755eb for php.net
commit 25a2d9755eb7638732a4775efb3dff1a3fa5d142
Author: David Carlier <devnexen@gmail.com>
Date: Mon Aug 3 08:00:29 2026 +0100
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
diff --git a/NEWS b/NEWS
index 4e95bed34c8..1af715a1bc7 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 89ebc5f61c0..dfc6c09f95b 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 2d297b26a04..07c546648f4 100644
Binary files a/ext/intl/dateformat/dateformat_arginfo.h and b/ext/intl/dateformat/dateformat_arginfo.h differ
diff --git a/ext/intl/dateformat/dateformat_helpers.cpp b/ext/intl/dateformat/dateformat_helpers.cpp
index 747105bb4f9..d10acb7273f 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 480074dbd82..1d0d17ba42a 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 @@
?>
--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 2c14c559b69..23617bd36e8 100644
--- a/ext/intl/tests/dateformat_errors.phpt
+++ b/ext/intl/tests/dateformat_errors.phpt
@@ -26,8 +26,8 @@
?>
--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 00000000000..55e9b7b37af
--- /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)