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)