From 530845fa653994c066826aedbb9d661601276050 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 31 Jan 2026 15:37:33 +0000 Subject: [PATCH 1/2] 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. --- ext/intl/dateformat/dateformat.stub.php | 2 + ext/intl/dateformat/dateformat_arginfo.h | 8 +++- ext/intl/dateformat/dateformat_helpers.cpp | 20 +++++++-- ext/intl/tests/gh20255.phpt | 52 ++++++++++++++++++++++ 4 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 ext/intl/tests/gh20255.phpt diff --git a/ext/intl/dateformat/dateformat.stub.php b/ext/intl/dateformat/dateformat.stub.php index 89ebc5f61c0ec..dd935c2b819a2 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 = -1; + /** * @param IntlCalendar|int|null $calendar */ diff --git a/ext/intl/dateformat/dateformat_arginfo.h b/ext/intl/dateformat/dateformat_arginfo.h index 2d297b26a0422..46ef812073d0b 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: 4648fa60e269507946ef15db7be1493e16528d45 */ 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, -1); + 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 18dc594dedddc..b2f67858059a8 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 -1 + 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::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/gh20255.phpt b/ext/intl/tests/gh20255.phpt new file mode 100644 index 0000000000000..1bfbcc0a81465 --- /dev/null +++ b/ext/intl/tests/gh20255.phpt @@ -0,0 +1,52 @@ +--TEST-- +IntlDateFormatter with PROLEPTIC_GREGORIAN calendar +--EXTENSIONS-- +intl +--FILE-- +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(-1) +Proleptic: 1200-03-01 +Matches workaround: bool(true) +Differs from hybrid: bool(true) From aa58cbc147c31890e16ace34b8019b627410c351 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 31 Jan 2026 16:02:09 +0000 Subject: [PATCH 2/2] assign another safer value for the internal flag and fix tests. --- ext/intl/dateformat/dateformat.stub.php | 2 +- ext/intl/dateformat/dateformat_arginfo.h | 4 ++-- ext/intl/dateformat/dateformat_helpers.cpp | 2 +- ext/intl/tests/dateformat___construct_bad_tz_cal.phpt | 2 +- ext/intl/tests/dateformat_errors.phpt | 6 +++--- ext/intl/tests/gh20255.phpt | 6 ++++-- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/ext/intl/dateformat/dateformat.stub.php b/ext/intl/dateformat/dateformat.stub.php index dd935c2b819a2..dfc6c09f95b5d 100644 --- a/ext/intl/dateformat/dateformat.stub.php +++ b/ext/intl/dateformat/dateformat.stub.php @@ -31,7 +31,7 @@ class IntlDateFormatter /** @cvalue UCAL_TRADITIONAL */ public const int TRADITIONAL = UNKNOWN; - public const int PROLEPTIC_GREGORIAN = -1; + 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 46ef812073d0b..07c546648f482 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: 4648fa60e269507946ef15db7be1493e16528d45 */ + * Stub hash: 13d9fe2cca75fcda5365a589ee318f76fcba3b84 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_IntlDateFormatter___construct, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, locale, IS_STRING, 1) @@ -220,7 +220,7 @@ static zend_class_entry *register_class_IntlDateFormatter(void) zend_string_release_ex(const_TRADITIONAL_name, true); zval const_PROLEPTIC_GREGORIAN_value; - ZVAL_LONG(&const_PROLEPTIC_GREGORIAN_value, -1); + 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); diff --git a/ext/intl/dateformat/dateformat_helpers.cpp b/ext/intl/dateformat/dateformat_helpers.cpp index b2f67858059a8..4f379a74af06d 100644 --- a/ext/intl/dateformat/dateformat_helpers.cpp +++ b/ext/intl/dateformat/dateformat_helpers.cpp @@ -27,7 +27,7 @@ extern "C" { } // Artificial value to set for a pure proleptic gregorian calendar (until icu provides it eventually) -#define UCAL_PHP_PROLEPTIC_GREGORIAN -1 +#define UCAL_PHP_PROLEPTIC_GREGORIAN -16 using icu::GregorianCalendar; diff --git a/ext/intl/tests/dateformat___construct_bad_tz_cal.phpt b/ext/intl/tests/dateformat___construct_bad_tz_cal.phpt index e33464a40a446..36880f3afc8be 100644 --- a/ext/intl/tests/dateformat___construct_bad_tz_cal.phpt +++ b/ext/intl/tests/dateformat___construct_bad_tz_cal.phpt @@ -23,6 +23,6 @@ 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 2c14c559b69a9..23617bd36e877 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 index 1bfbcc0a81465..55e9b7b37afe2 100644 --- a/ext/intl/tests/gh20255.phpt +++ b/ext/intl/tests/gh20255.phpt @@ -2,11 +2,13 @@ IntlDateFormatter with PROLEPTIC_GREGORIAN calendar --EXTENSIONS-- intl +--SKIPIF-- + --FILE-- --EXPECT-- -int(-1) +int(-16) Proleptic: 1200-03-01 Matches workaround: bool(true) Differs from hybrid: bool(true)