Commit 906356642d8 for php
commit 906356642d8f67f34817db6a9b2198ebaf83e230
Merge: c03d1aad686 913e01c1598
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Mon Oct 5 08:54:49 2026 -0400
Merge branch 'PHP-8.4' into PHP-8.5
* PHP-8.4:
ext/intl: Use byte offsets in IntlDateFormatter parsing
diff --cc NEWS
index 6d537ffe99b,86683d7b62a..4d949ee7420
--- a/NEWS
+++ b/NEWS
@@@ -77,6 -82,6 +77,9 @@@ PH
haystacks. (Weilin Du)
. Fixed grapheme_strrpos() and grapheme_strripos() skipping overlapping matches
when using a negative offset. (Weilin Du)
++ . Fixed IntlDateFormatter::parse(), localtime() and parseToCalendar()
++ treating the offset as UTF-16 code units instead of bytes.
++ (Ilia Alshanetsky)
- Lexbor:
. Merge patches lexbor/lexbor@8a14bc0 and lexbor/lexbor@f67ce4b, fixing a
diff --cc ext/intl/tests/dateformat_parse_utf8_offset.phpt
index 00000000000,9c0fef9fa94..aa80a39d809
mode 000000,100644..100644
--- a/ext/intl/tests/dateformat_parse_utf8_offset.phpt
+++ b/ext/intl/tests/dateformat_parse_utf8_offset.phpt
@@@ -1,0 -1,68 +1,68 @@@
+ --TEST--
+ IntlDateFormatter parsing uses UTF-8 byte offsets
+ --EXTENSIONS--
+ intl
+ --FILE--
+ <?php
+ $formatter = new IntlDateFormatter('en_US', IntlDateFormatter::NONE, IntlDateFormatter::NONE, 'UTC', IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd');
+ $prefix = "\u{1F600}";
+ $text = $prefix . '2017-10-12 tail';
+
+ echo "parse():\n";
+ $offset = strlen($prefix);
+ var_dump(gmdate('Y-m-d', $formatter->parse($text, $offset)), $offset);
+
+ echo "localtime():\n";
+ $offset = strlen($prefix);
+ $tm = $formatter->localtime($text, $offset);
+ var_dump($tm['tm_year'] + 1900, $tm['tm_mon'] + 1, $tm['tm_mday'], $offset);
+
+ echo "parseToCalendar():\n";
+ $offset = strlen($prefix);
+ var_dump(gmdate('Y-m-d', $formatter->parseToCalendar($text, $offset)), $offset);
+
+ echo "failed parse:\n";
+ $offset = strlen($prefix);
+ var_dump($formatter->parse($prefix . '2017-xx-12', $offset), $offset);
+
+ echo "offset inside a character:\n";
+ $offset = 1;
+ var_dump($formatter->parse($text, $offset), $offset, intl_get_error_message());
+
+ echo "partial match inside a surrogate pair:\n";
+ $formatter = new IntlDateFormatter('en_US', IntlDateFormatter::NONE, IntlDateFormatter::NONE, 'UTC', IntlDateFormatter::GREGORIAN, "\u{20AC}\u{1F600}yyyy-MM-dd");
+ $offset = 0;
+ var_dump($formatter->parse("\u{20AC}\u{1F601}2017-10-12", $offset), $offset);
+ $formatter = new IntlDateFormatter('en_US', IntlDateFormatter::NONE, IntlDateFormatter::NONE, 'UTC', IntlDateFormatter::GREGORIAN, "yyyy-MM-dd\u{1F600}");
+ foreach (['parse', 'localtime', 'parseToCalendar'] as $method) {
+ $offset = 0;
+ $formatter->$method("2017-10-12\u{1F601}", $offset);
+ echo "$method(): ";
+ var_dump($offset);
+ }
+ ?>
+ --EXPECT--
+ parse():
+ string(10) "2017-10-12"
+ int(14)
+ localtime():
+ int(2017)
+ int(10)
+ int(12)
+ int(14)
+ parseToCalendar():
+ string(10) "2017-10-12"
+ int(14)
+ failed parse:
+ bool(false)
+ int(9)
+ offset inside a character:
+ bool(false)
+ int(1)
-string(42) "Invalid UTF-8 offset: U_INVALID_CHAR_FOUND"
++string(70) "IntlDateFormatter::parse(): Invalid UTF-8 offset: U_INVALID_CHAR_FOUND"
+ partial match inside a surrogate pair:
+ bool(false)
+ int(3)
+ parse(): int(10)
+ localtime(): int(10)
+ parseToCalendar(): int(10)