From 4d12b335c78efc656e82a2e76a31f70139cc5c91 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sun, 19 Jul 2026 03:37:29 +0000 Subject: [PATCH] Stdlib: IntlCalendar getType/add/roll/clear/equals/DateTime bridges (#20756) Extend the Gregorian IntlCalendar subset with field arithmetic, clear/isSet, getNow, and toDateTime/fromDateTime so calendar OOP matches php-src on the minimal cases frameworks need after #6151. Co-authored-by: Cursor --- docs/bootstrap-inventory.md | 78 +- ext/intl/VmIntlCalendar.php | 1079 +++++++++++++++-- .../cases/intl/intlcalendar_methods.phpt | 55 + test/repro/issue_intlcalendar_methods.php | 46 + 4 files changed, 1125 insertions(+), 133 deletions(-) create mode 100644 test/compliance/cases/intl/intlcalendar_methods.phpt create mode 100644 test/repro/issue_intlcalendar_methods.php diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index 58c91ae129d..c1a2c961a45 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -12,7 +12,7 @@ Regenerate: `php script/bootstrap-inventory.php` | Phase A inventory files (M2 ratio SSOT) | 5844 | | Phase A ratio-deferred paths | 0 | | Source constructs flagged (blockers) | 0 | -| Source constructs flagged (warnings) | 17509 | +| Source constructs flagged (warnings) | 17539 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -841,7 +841,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `ext/intl/VmCollator.php` | 0 | 55 | | `ext/intl/VmGrapheme.php` | 0 | 1 | | `ext/intl/VmIdn.php` | 0 | 5 | -| `ext/intl/VmIntlCalendar.php` | 0 | 22 | +| `ext/intl/VmIntlCalendar.php` | 0 | 52 | | `ext/intl/VmIntlChar.php` | 0 | 53 | | `ext/intl/VmIntlDateFormatter.php` | 0 | 5 | | `ext/intl/VmIntlTimeZone.php` | 0 | 34 | @@ -11633,28 +11633,58 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/intl/VmIntlCalendar.php` **Warnings** (review for bootstrap subset): -- new ClassEntry (line 139) -- new Variable (line 143) -- new IntlCalendarCreateInstance (line 151) -- new IntlCalendarGet (line 152) -- new IntlCalendarSet (line 153) -- new IntlCalendarGetTimeZone (line 154) -- new IntlCalendarGetTime (line 155) -- new IntlCalendarSetTime (line 156) -- new Error (line 177) -- new ObjectEntry (line 181) -- new ArgumentCountError (line 439) -- new ArgumentCountError (line 484) -- new Error (line 492) -- new ArgumentCountError (line 526) -- new Error (line 534) -- new ArgumentCountError (line 577) -- new Error (line 585) -- new ArgumentCountError (line 612) -- new Error (line 620) -- new ArgumentCountError (line 647) -- new Error (line 655) -- 17 class method(s) +- new ClassEntry (line 149) +- new Variable (line 153) +- new IntlCalendarCreateInstance (line 161) +- new IntlCalendarGetNow (line 162) +- new IntlCalendarFromDateTime (line 163) +- new IntlCalendarGet (line 164) +- new IntlCalendarSet (line 165) +- new IntlCalendarGetTimeZone (line 166) +- new IntlCalendarGetTime (line 167) +- new IntlCalendarSetTime (line 168) +- new IntlCalendarGetType (line 169) +- new IntlCalendarAdd (line 170) +- new IntlCalendarRoll (line 171) +- new IntlCalendarClear (line 172) +- new IntlCalendarIsSet (line 173) +- new IntlCalendarEquals (line 174) +- new IntlCalendarToDateTime (line 175) +- new IntlCalendarFieldDifference (line 176) +- new Error (line 197) +- new ObjectEntry (line 201) +- new ObjectEntry (line 498) +- new ObjectEntry (line 526) +- new ArgumentCountError (line 926) +- new ArgumentCountError (line 971) +- new Error (line 979) +- new ArgumentCountError (line 1013) +- new Error (line 1021) +- new ArgumentCountError (line 1064) +- new Error (line 1072) +- new ArgumentCountError (line 1099) +- new Error (line 1107) +- new ArgumentCountError (line 1134) +- new Error (line 1142) +- new ArgumentCountError (line 1177) +- new Error (line 1185) +- new ArgumentCountError (line 1212) +- new ArgumentCountError (line 1236) +- new Error (line 1244) +- new ArgumentCountError (line 1268) +- new Error (line 1276) +- new ArgumentCountError (line 1301) +- new Error (line 1309) +- new ArgumentCountError (line 1335) +- new Error (line 1343) +- new ArgumentCountError (line 1365) +- new Error (line 1373) +- new ArgumentCountError (line 1400) +- new Error (line 1408) +- new ArgumentCountError (line 1435) +- new ArgumentCountError (line 1497) +- new Error (line 1505) +- 45 class method(s) ### `ext/intl/VmIntlChar.php` diff --git a/ext/intl/VmIntlCalendar.php b/ext/intl/VmIntlCalendar.php index d1fe2cc8cb1..c85ed67a956 100644 --- a/ext/intl/VmIntlCalendar.php +++ b/ext/intl/VmIntlCalendar.php @@ -11,14 +11,16 @@ use PHPCompiler\VM\Builtin\VmClassMethod; use PHPCompiler\VM\ClassEntry; use PHPCompiler\VM\Context; +use PHPCompiler\VM\DateTimeSupport; use PHPCompiler\VM\ObjectEntry; use PHPCompiler\VM\Variable; use PHPCfg\Func as CfgFunc; /** - * IntlCalendar — Gregorian field get/set via zoneinfo (php-src calendar_*; #6151). + * IntlCalendar — Gregorian field get/set via zoneinfo (php-src calendar_*; #6151, #20756). * - * v1 subset: createInstance, get/set (field + Y/M/D forms), getTimeZone, getTime/setTime. + * Subset: createInstance, get/set, getTimeZone, getTime/setTime, getType, getNow, + * add/roll/clear/isSet/equals, toDateTime/fromDateTime, fieldDifference. * ICU field constants match UCalendarDateFields (unicode/ucal.h). */ final class VmIntlCalendar @@ -75,7 +77,15 @@ final class VmIntlCalendar public const DECEMBER = 11; public const UNDECIMBER = 12; - /** @var array */ + /** + * @var array + * }> + */ private static array $state = []; /** @return array */ @@ -149,11 +159,21 @@ public static function registerClass(Context $ctx): void $pubStatic = $pub | CfgFunc::FLAG_STATIC; $methods = [ 'createinstance' => [new IntlCalendarCreateInstance(), 'createInstance', $pubStatic], + 'getnow' => [new IntlCalendarGetNow(), 'getNow', $pubStatic], + 'fromdatetime' => [new IntlCalendarFromDateTime(), 'fromDateTime', $pubStatic], 'get' => [new IntlCalendarGet(), 'get', $pub], 'set' => [new IntlCalendarSet(), 'set', $pub], 'gettimezone' => [new IntlCalendarGetTimeZone(), 'getTimeZone', $pub], 'gettime' => [new IntlCalendarGetTime(), 'getTime', $pub], 'settime' => [new IntlCalendarSetTime(), 'setTime', $pub], + 'gettype' => [new IntlCalendarGetType(), 'getType', $pub], + 'add' => [new IntlCalendarAdd(), 'add', $pub], + 'roll' => [new IntlCalendarRoll(), 'roll', $pub], + 'clear' => [new IntlCalendarClear(), 'clear', $pub], + 'isset' => [new IntlCalendarIsSet(), 'isSet', $pub], + 'equals' => [new IntlCalendarEquals(), 'equals', $pub], + 'todatetime' => [new IntlCalendarToDateTime(), 'toDateTime', $pub], + 'fielddifference' => [new IntlCalendarFieldDifference(), 'fieldDifference', $pub], ]; foreach ($methods as $lc => [$handler, $name, $vis]) { $entry->methods[$lc] = $handler; @@ -185,161 +205,358 @@ public static function createInstance( 'locale' => $locale, 'timestamp' => VmDate::time(), 'millisecond' => 0, + 'unsetFields' => [], ]; IntlError::clear(); return $object; } - /** - * @return int|false - */ - public static function get(ObjectEntry $cal, int $field) + public static function getType(ObjectEntry $cal): string|false + { + if (!isset(self::$state[$cal->id])) { + IntlError::set( + IntlError::U_ILLEGAL_ARGUMENT_ERROR, + 'intlcal_get_type: bad calendar object: U_ILLEGAL_ARGUMENT_ERROR' + ); + + return false; + } + IntlError::clear(); + + return 'gregorian'; + } + + public static function getNow(): float + { + IntlError::clear(); + + return ((float) VmDate::time()) * 1000.0; + } + + public static function equals(ObjectEntry $a, ObjectEntry $b): bool + { + $ta = self::getTime($a); + $tb = self::getTime($b); + if (false === $ta || false === $tb) { + return false; + } + + return $ta === $tb; + } + + public static function isSet(ObjectEntry $cal, int $field): bool { $state = self::$state[$cal->id] ?? null; if (null === $state) { + return false; + } + if ($field < 0 || $field >= self::FIELD_FIELD_COUNT) { + return false; + } + + return !isset($state['unsetFields'][$field]); + } + + public static function clear(ObjectEntry $cal, ?int $field): bool + { + $state = &self::$state[$cal->id]; + if (!isset($state)) { IntlError::set( IntlError::U_ILLEGAL_ARGUMENT_ERROR, - 'intlcal_get: bad calendar object: U_ILLEGAL_ARGUMENT_ERROR' + 'intlcal_clear: bad calendar object: U_ILLEGAL_ARGUMENT_ERROR' ); return false; } + if (null === $field) { + $state['timestamp'] = 0; + $state['millisecond'] = 0; + $state['unsetFields'] = []; + for ($i = 0; $i < self::FIELD_FIELD_COUNT; ++$i) { + // php-src/ICU: after clear(), YEAR is unset; month/day/hour remain set at epoch defaults. + if (self::FIELD_YEAR === $i || self::FIELD_EXTENDED_YEAR === $i || self::FIELD_ERA === $i) { + $state['unsetFields'][$i] = true; + } + } + IntlError::clear(); + + return true; + } if ($field < 0 || $field >= self::FIELD_FIELD_COUNT) { IntlError::set( IntlError::U_ILLEGAL_ARGUMENT_ERROR, - 'intlcal_get: invalid field: U_ILLEGAL_ARGUMENT_ERROR' + 'intlcal_clear: invalid field: U_ILLEGAL_ARGUMENT_ERROR' ); return false; } - $tz = $state['timezone']; - $ts = $state['timestamp']; - $ms = $state['millisecond']; + $state['unsetFields'][$field] = true; IntlError::clear(); - return match ($field) { - self::FIELD_ERA => 1, // AD - self::FIELD_YEAR, self::FIELD_EXTENDED_YEAR => (int) VmDateTimeNative::format($ts, 0, $tz, 'Y'), - self::FIELD_MONTH => ((int) VmDateTimeNative::format($ts, 0, $tz, 'n')) - 1, - self::FIELD_WEEK_OF_YEAR => (int) VmDateTimeNative::format($ts, 0, $tz, 'W'), - self::FIELD_WEEK_OF_MONTH => (int) ceil(((int) VmDateTimeNative::format($ts, 0, $tz, 'j')) / 7), - self::FIELD_DATE, self::FIELD_DAY_OF_MONTH => (int) VmDateTimeNative::format($ts, 0, $tz, 'j'), - self::FIELD_DAY_OF_YEAR => (int) VmDateTimeNative::format($ts, 0, $tz, 'z') + 1, - self::FIELD_DAY_OF_WEEK => ((int) VmDateTimeNative::format($ts, 0, $tz, 'w')) + 1, - self::FIELD_DAY_OF_WEEK_IN_MONTH => (int) ceil(((int) VmDateTimeNative::format($ts, 0, $tz, 'j')) / 7), - self::FIELD_AM_PM => ((int) VmDateTimeNative::format($ts, 0, $tz, 'G')) >= 12 ? 1 : 0, - self::FIELD_HOUR => ((int) VmDateTimeNative::format($ts, 0, $tz, 'G')) % 12, - self::FIELD_HOUR_OF_DAY => (int) VmDateTimeNative::format($ts, 0, $tz, 'G'), - self::FIELD_MINUTE => (int) VmDateTimeNative::format($ts, 0, $tz, 'i'), - self::FIELD_SECOND => (int) VmDateTimeNative::format($ts, 0, $tz, 's'), - self::FIELD_MILLISECOND => $ms, - self::FIELD_ZONE_OFFSET => VmDateTimeNative::timezoneOffsetSeconds($tz, $ts) * 1000, - self::FIELD_DST_OFFSET => 0, - self::FIELD_YEAR_WOY => (int) VmDateTimeNative::format($ts, 0, $tz, 'o'), - self::FIELD_DOW_LOCAL => ((int) VmDateTimeNative::format($ts, 0, $tz, 'N')), - self::FIELD_JULIAN_DAY => (int) floor($ts / 86400) + 2440588, - self::FIELD_MILLISECONDS_IN_DAY => ( - ((int) VmDateTimeNative::format($ts, 0, $tz, 'G')) * 3600000 - + ((int) VmDateTimeNative::format($ts, 0, $tz, 'i')) * 60000 - + ((int) VmDateTimeNative::format($ts, 0, $tz, 's')) * 1000 - + $ms - ), - self::FIELD_IS_LEAP_MONTH => 0, - default => false, - }; + return true; } - public static function setField(ObjectEntry $cal, int $field, int $value): bool + public static function add(ObjectEntry $cal, int $field, int $amount): bool { $state = &self::$state[$cal->id]; if (!isset($state)) { IntlError::set( IntlError::U_ILLEGAL_ARGUMENT_ERROR, - 'intlcal_set: bad calendar object: U_ILLEGAL_ARGUMENT_ERROR' + 'intlcal_add: bad calendar object: U_ILLEGAL_ARGUMENT_ERROR' ); return false; } - $parts = self::parts($state['timezone'], $state['timestamp'], $state['millisecond']); + if (0 === $amount) { + unset($state['unsetFields'][$field]); + IntlError::clear(); + + return true; + } + $tz = $state['timezone']; + $parts = self::parts($tz, $state['timestamp'], $state['millisecond']); switch ($field) { case self::FIELD_YEAR: case self::FIELD_EXTENDED_YEAR: - $parts['year'] = $value; + $parts['year'] += $amount; + self::applyParts($state, $parts); break; case self::FIELD_MONTH: - $parts['month'] = $value + 1; // ICU 0-based → 1-based + self::addMonths($parts, $amount); + self::applyParts($state, $parts); break; case self::FIELD_DATE: case self::FIELD_DAY_OF_MONTH: - $parts['day'] = $value; + $state['timestamp'] = self::addSecondsInZone($tz, $state['timestamp'], $amount * 86400); + break; + case self::FIELD_DAY_OF_YEAR: + case self::FIELD_WEEK_OF_YEAR: + $mult = self::FIELD_WEEK_OF_YEAR === $field ? 7 : 1; + $state['timestamp'] = self::addSecondsInZone($tz, $state['timestamp'], $amount * $mult * 86400); break; + case self::FIELD_HOUR: case self::FIELD_HOUR_OF_DAY: - $parts['hour'] = $value; + $state['timestamp'] = self::addSecondsInZone($tz, $state['timestamp'], $amount * 3600); break; case self::FIELD_MINUTE: - $parts['minute'] = $value; + $state['timestamp'] = self::addSecondsInZone($tz, $state['timestamp'], $amount * 60); break; case self::FIELD_SECOND: - $parts['second'] = $value; + $state['timestamp'] = self::addSecondsInZone($tz, $state['timestamp'], $amount); break; case self::FIELD_MILLISECOND: - $parts['millisecond'] = max(0, min(999, $value)); + self::addMilliseconds($state, $amount); break; default: IntlError::set( IntlError::U_ILLEGAL_ARGUMENT_ERROR, - 'intlcal_set: unsupported field in this compiler build: U_ILLEGAL_ARGUMENT_ERROR' + 'intlcal_add: unsupported field in this compiler build: U_ILLEGAL_ARGUMENT_ERROR' ); return false; } - $parsed = VmDateTimeNative::parseDateTime( - \sprintf( - '%04d-%02d-%02d %02d:%02d:%02d', - $parts['year'], - $parts['month'], - $parts['day'], - $parts['hour'], - $parts['minute'], - $parts['second'] - ), - $state['timezone'] - ); - $state['timestamp'] = $parsed['timestamp']; - $state['millisecond'] = $parts['millisecond']; + unset($state['unsetFields'][$field]); IntlError::clear(); return true; } - public static function setDate( - ObjectEntry $cal, - int $year, - int $month, - ?int $dayOfMonth, - ?int $hour, - ?int $minute, - ?int $second - ): bool { + public static function roll(ObjectEntry $cal, int $field, int $amount): bool + { $state = &self::$state[$cal->id]; if (!isset($state)) { + IntlError::set( + IntlError::U_ILLEGAL_ARGUMENT_ERROR, + 'intlcal_roll: bad calendar object: U_ILLEGAL_ARGUMENT_ERROR' + ); + return false; } - $parts = self::parts($state['timezone'], $state['timestamp'], $state['millisecond']); - $parts['year'] = $year; - $parts['month'] = $month + 1; // ICU month - if (null !== $dayOfMonth) { - $parts['day'] = $dayOfMonth; + if (0 === $amount) { + unset($state['unsetFields'][$field]); + IntlError::clear(); + + return true; } - if (null !== $hour) { - $parts['hour'] = $hour; + $tz = $state['timezone']; + $parts = self::parts($tz, $state['timestamp'], $state['millisecond']); + switch ($field) { + case self::FIELD_YEAR: + case self::FIELD_EXTENDED_YEAR: + $parts['year'] += $amount; + self::applyParts($state, $parts); + break; + case self::FIELD_MONTH: + $parts['month'] = self::modRange($parts['month'] - 1 + $amount, 12) + 1; + $dim = self::daysInMonth($parts['year'], $parts['month']); + if ($parts['day'] > $dim) { + $parts['day'] = $dim; + } + self::applyParts($state, $parts); + break; + case self::FIELD_DATE: + case self::FIELD_DAY_OF_MONTH: + $dim = self::daysInMonth($parts['year'], $parts['month']); + $parts['day'] = self::modRange($parts['day'] - 1 + $amount, $dim) + 1; + self::applyParts($state, $parts); + break; + case self::FIELD_HOUR_OF_DAY: + $parts['hour'] = self::modRange($parts['hour'] + $amount, 24); + self::applyParts($state, $parts); + break; + case self::FIELD_HOUR: + $h12 = $parts['hour'] % 12; + $h12 = self::modRange($h12 + $amount, 12); + $parts['hour'] = ($parts['hour'] >= 12 ? 12 : 0) + ($h12 % 12); + if (24 === $parts['hour']) { + $parts['hour'] = 12; + } + self::applyParts($state, $parts); + break; + case self::FIELD_MINUTE: + $parts['minute'] = self::modRange($parts['minute'] + $amount, 60); + self::applyParts($state, $parts); + break; + case self::FIELD_SECOND: + $parts['second'] = self::modRange($parts['second'] + $amount, 60); + self::applyParts($state, $parts); + break; + case self::FIELD_MILLISECOND: + $parts['millisecond'] = self::modRange($parts['millisecond'] + $amount, 1000); + self::applyParts($state, $parts); + break; + default: + IntlError::set( + IntlError::U_ILLEGAL_ARGUMENT_ERROR, + 'intlcal_roll: unsupported field in this compiler build: U_ILLEGAL_ARGUMENT_ERROR' + ); + + return false; } - if (null !== $minute) { - $parts['minute'] = $minute; + unset($state['unsetFields'][$field]); + IntlError::clear(); + + return true; + } + + /** + * php-src intlcal_field_difference — advances calendar by the difference amount. + * + * @return int|false + */ + public static function fieldDifference(ObjectEntry $cal, float $targetMs, int $field) + { + $before = self::getTime($cal); + if (false === $before) { + return false; } - if (null !== $second) { - $parts['second'] = $second; + $deltaMs = $targetMs - $before; + $amount = match ($field) { + self::FIELD_MILLISECOND => (int) round($deltaMs), + self::FIELD_SECOND => (int) round($deltaMs / 1000.0), + self::FIELD_MINUTE => (int) round($deltaMs / 60000.0), + self::FIELD_HOUR, self::FIELD_HOUR_OF_DAY => (int) round($deltaMs / 3600000.0), + self::FIELD_DATE, self::FIELD_DAY_OF_MONTH, self::FIELD_DAY_OF_YEAR => (int) round($deltaMs / 86400000.0), + self::FIELD_WEEK_OF_YEAR, self::FIELD_WEEK_OF_MONTH => (int) round($deltaMs / (7 * 86400000.0)), + self::FIELD_MONTH => self::approxMonthDiff($cal, $targetMs), + self::FIELD_YEAR, self::FIELD_EXTENDED_YEAR => self::approxYearDiff($cal, $targetMs), + default => null, + }; + if (null === $amount) { + IntlError::set( + IntlError::U_ILLEGAL_ARGUMENT_ERROR, + 'intlcal_field_difference: unsupported field in this compiler build: U_ILLEGAL_ARGUMENT_ERROR' + ); + + return false; + } + if (!self::add($cal, $field, $amount)) { + return false; + } + IntlError::clear(); + + return $amount; + } + + public static function toDateTime(ObjectEntry $cal, Context $ctx): ObjectEntry|false + { + $state = self::$state[$cal->id] ?? null; + if (null === $state) { + IntlError::set( + IntlError::U_ILLEGAL_ARGUMENT_ERROR, + 'intlcal_to_date_time: bad calendar object: U_ILLEGAL_ARGUMENT_ERROR' + ); + + return false; + } + $class = $ctx->classes[DateTimeSupport::CLASS_DATETIME] ?? null; + if (null === $class) { + throw new \LogicException('DateTime is not registered in this compiler build'); + } + try { + $entry = new ObjectEntry($class); + DateTimeSupport::initDateTimeFromTimestamp($entry, $state['timestamp']); + $zone = DateTimeSupport::newDateTimeZoneVariable($ctx, $state['timezone'])->toObject(); + DateTimeSupport::setTimezone($entry, $zone); + IntlError::clear(); + + return $entry; + } catch (\Throwable) { + IntlError::set( + IntlError::U_ILLEGAL_ARGUMENT_ERROR, + 'intlcal_to_date_time: DateTime create failed: U_ILLEGAL_ARGUMENT_ERROR' + ); + + return false; + } + } + + public static function fromDateTime( + Context $ctx, + ObjectEntry|string $datetime, + ?string $locale + ): ObjectEntry|false { + if (\is_string($datetime)) { + $class = $ctx->classes[DateTimeSupport::CLASS_DATETIME] ?? null; + if (null === $class) { + throw new \LogicException('DateTime is not registered in this compiler build'); + } + try { + $entry = new ObjectEntry($class); + DateTimeSupport::initDateTime($entry, $datetime); + $datetime = $entry; + } catch (\Throwable) { + IntlError::set( + IntlError::U_ILLEGAL_ARGUMENT_ERROR, + 'intlcal_from_date_time: bad datetime string: U_ILLEGAL_ARGUMENT_ERROR' + ); + + return false; + } + } + $ts = DateTimeSupport::getTimestamp($datetime); + $ms = intdiv(DateTimeSupport::getMicrosecond($datetime), 1000); + $tz = DateTimeSupport::timezoneName( + DateTimeSupport::getTimezoneObject($datetime, $ctx) + ); + $cal = self::createInstance($ctx, $tz, $locale ?? ''); + self::$state[$cal->id]['timestamp'] = $ts; + self::$state[$cal->id]['millisecond'] = $ms; + IntlError::clear(); + + return $cal; + } + + /** + * @param array{timezone: string, locale: string, timestamp: int, millisecond: int, unsetFields: array} $state + * @param array{year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int} $parts + */ + private static function applyParts(array &$state, array $parts): void + { + $dim = self::daysInMonth($parts['year'], $parts['month']); + if ($parts['day'] > $dim) { + $parts['day'] = $dim; } $parsed = VmDateTimeNative::parseDateTime( \sprintf( @@ -354,43 +571,312 @@ public static function setDate( $state['timezone'] ); $state['timestamp'] = $parsed['timestamp']; - IntlError::clear(); + $state['millisecond'] = max(0, min(999, $parts['millisecond'])); + } - return true; + /** + * @param array{year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int} $parts + */ + private static function addMonths(array &$parts, int $amount): void + { + $idx = ($parts['year'] * 12) + ($parts['month'] - 1) + $amount; + $parts['year'] = intdiv($idx, 12); + $parts['month'] = ($idx % 12) + 1; + if ($parts['month'] <= 0) { + $parts['month'] += 12; + --$parts['year']; + } + $dim = self::daysInMonth($parts['year'], $parts['month']); + if ($parts['day'] > $dim) { + $parts['day'] = $dim; + } } - public static function getTimeZoneObject(ObjectEntry $cal, Context $ctx): ObjectEntry|false + /** + * @param array{timezone: string, locale: string, timestamp: int, millisecond: int, unsetFields: array} $state + */ + private static function addMilliseconds(array &$state, int $amount): void { - $state = self::$state[$cal->id] ?? null; - if (null === $state) { - return false; + $total = ($state['timestamp'] * 1000) + $state['millisecond'] + $amount; + if ($total >= 0) { + $state['timestamp'] = intdiv($total, 1000); + $state['millisecond'] = $total % 1000; + } else { + $sec = (int) floor($total / 1000); + $ms = $total - ($sec * 1000); + $state['timestamp'] = $sec; + $state['millisecond'] = $ms; } + } - return VmIntlTimeZone::createFromId($ctx, $state['timezone']); + private static function addSecondsInZone(string $tz, int $timestamp, int $seconds): int + { + unset($tz); // Gregorian civil arithmetic via UTC epoch is sufficient for v1 zoneinfo calendars. + + return $timestamp + $seconds; } - public static function getTime(ObjectEntry $cal): float|false + private static function daysInMonth(int $year, int $month): int { - $state = self::$state[$cal->id] ?? null; - if (null === $state) { - return false; + if ($month < 1 || $month > 12) { + return 31; } - IntlError::clear(); + if (\function_exists('cal_days_in_month')) { + return (int) \cal_days_in_month(\CAL_GREGORIAN, $month, $year); + } + $parsed = VmDateTimeNative::parseDateTime( + \sprintf('%04d-%02d-01 00:00:00', $year, $month), + 'UTC' + ); + $nextMonth = $month + 1; + $nextYear = $year; + if ($nextMonth > 12) { + $nextMonth = 1; + ++$nextYear; + } + $next = VmDateTimeNative::parseDateTime( + \sprintf('%04d-%02d-01 00:00:00', $nextYear, $nextMonth), + 'UTC' + ); - return ((float) $state['timestamp']) * 1000.0 + (float) $state['millisecond']; + return (int) (($next['timestamp'] - $parsed['timestamp']) / 86400); } - public static function setTime(ObjectEntry $cal, float $millis): bool + private static function modRange(int $value, int $modulus): int { - $state = &self::$state[$cal->id]; - if (!isset($state)) { - return false; + if ($modulus <= 0) { + return 0; } - if ($millis < 0) { - $sec = (int) ceil($millis / 1000.0); - $ms = (int) round($millis - ($sec * 1000.0)); - } else { - $sec = (int) floor($millis / 1000.0); + $r = $value % $modulus; + if ($r < 0) { + $r += $modulus; + } + + return $r; + } + + private static function approxMonthDiff(ObjectEntry $cal, float $targetMs): int + { + $state = self::$state[$cal->id]; + $parts = self::parts($state['timezone'], $state['timestamp'], $state['millisecond']); + $targetSec = (int) floor($targetMs / 1000.0); + $tp = self::parts($state['timezone'], $targetSec, 0); + + return (($tp['year'] * 12) + $tp['month']) - (($parts['year'] * 12) + $parts['month']); + } + + private static function approxYearDiff(ObjectEntry $cal, float $targetMs): int + { + $state = self::$state[$cal->id]; + $parts = self::parts($state['timezone'], $state['timestamp'], $state['millisecond']); + $targetSec = (int) floor($targetMs / 1000.0); + $tp = self::parts($state['timezone'], $targetSec, 0); + + return $tp['year'] - $parts['year']; + } + + /** + * @return int|false + */ + public static function get(ObjectEntry $cal, int $field) + { + $state = self::$state[$cal->id] ?? null; + if (null === $state) { + IntlError::set( + IntlError::U_ILLEGAL_ARGUMENT_ERROR, + 'intlcal_get: bad calendar object: U_ILLEGAL_ARGUMENT_ERROR' + ); + + return false; + } + if ($field < 0 || $field >= self::FIELD_FIELD_COUNT) { + IntlError::set( + IntlError::U_ILLEGAL_ARGUMENT_ERROR, + 'intlcal_get: invalid field: U_ILLEGAL_ARGUMENT_ERROR' + ); + + return false; + } + $tz = $state['timezone']; + $ts = $state['timestamp']; + $ms = $state['millisecond']; + IntlError::clear(); + + return match ($field) { + self::FIELD_ERA => 1, // AD + self::FIELD_YEAR, self::FIELD_EXTENDED_YEAR => (int) VmDateTimeNative::format($ts, 0, $tz, 'Y'), + self::FIELD_MONTH => ((int) VmDateTimeNative::format($ts, 0, $tz, 'n')) - 1, + self::FIELD_WEEK_OF_YEAR => (int) VmDateTimeNative::format($ts, 0, $tz, 'W'), + self::FIELD_WEEK_OF_MONTH => (int) ceil(((int) VmDateTimeNative::format($ts, 0, $tz, 'j')) / 7), + self::FIELD_DATE, self::FIELD_DAY_OF_MONTH => (int) VmDateTimeNative::format($ts, 0, $tz, 'j'), + self::FIELD_DAY_OF_YEAR => (int) VmDateTimeNative::format($ts, 0, $tz, 'z') + 1, + self::FIELD_DAY_OF_WEEK => ((int) VmDateTimeNative::format($ts, 0, $tz, 'w')) + 1, + self::FIELD_DAY_OF_WEEK_IN_MONTH => (int) ceil(((int) VmDateTimeNative::format($ts, 0, $tz, 'j')) / 7), + self::FIELD_AM_PM => ((int) VmDateTimeNative::format($ts, 0, $tz, 'G')) >= 12 ? 1 : 0, + self::FIELD_HOUR => ((int) VmDateTimeNative::format($ts, 0, $tz, 'G')) % 12, + self::FIELD_HOUR_OF_DAY => (int) VmDateTimeNative::format($ts, 0, $tz, 'G'), + self::FIELD_MINUTE => (int) VmDateTimeNative::format($ts, 0, $tz, 'i'), + self::FIELD_SECOND => (int) VmDateTimeNative::format($ts, 0, $tz, 's'), + self::FIELD_MILLISECOND => $ms, + self::FIELD_ZONE_OFFSET => VmDateTimeNative::timezoneOffsetSeconds($tz, $ts) * 1000, + self::FIELD_DST_OFFSET => 0, + self::FIELD_YEAR_WOY => (int) VmDateTimeNative::format($ts, 0, $tz, 'o'), + self::FIELD_DOW_LOCAL => ((int) VmDateTimeNative::format($ts, 0, $tz, 'N')), + self::FIELD_JULIAN_DAY => (int) floor($ts / 86400) + 2440588, + self::FIELD_MILLISECONDS_IN_DAY => ( + ((int) VmDateTimeNative::format($ts, 0, $tz, 'G')) * 3600000 + + ((int) VmDateTimeNative::format($ts, 0, $tz, 'i')) * 60000 + + ((int) VmDateTimeNative::format($ts, 0, $tz, 's')) * 1000 + + $ms + ), + self::FIELD_IS_LEAP_MONTH => 0, + default => false, + }; + } + + public static function setField(ObjectEntry $cal, int $field, int $value): bool + { + $state = &self::$state[$cal->id]; + if (!isset($state)) { + IntlError::set( + IntlError::U_ILLEGAL_ARGUMENT_ERROR, + 'intlcal_set: bad calendar object: U_ILLEGAL_ARGUMENT_ERROR' + ); + + return false; + } + $parts = self::parts($state['timezone'], $state['timestamp'], $state['millisecond']); + switch ($field) { + case self::FIELD_YEAR: + case self::FIELD_EXTENDED_YEAR: + $parts['year'] = $value; + break; + case self::FIELD_MONTH: + $parts['month'] = $value + 1; // ICU 0-based → 1-based + break; + case self::FIELD_DATE: + case self::FIELD_DAY_OF_MONTH: + $parts['day'] = $value; + break; + case self::FIELD_HOUR_OF_DAY: + $parts['hour'] = $value; + break; + case self::FIELD_MINUTE: + $parts['minute'] = $value; + break; + case self::FIELD_SECOND: + $parts['second'] = $value; + break; + case self::FIELD_MILLISECOND: + $parts['millisecond'] = max(0, min(999, $value)); + break; + default: + IntlError::set( + IntlError::U_ILLEGAL_ARGUMENT_ERROR, + 'intlcal_set: unsupported field in this compiler build: U_ILLEGAL_ARGUMENT_ERROR' + ); + + return false; + } + $parsed = VmDateTimeNative::parseDateTime( + \sprintf( + '%04d-%02d-%02d %02d:%02d:%02d', + $parts['year'], + $parts['month'], + $parts['day'], + $parts['hour'], + $parts['minute'], + $parts['second'] + ), + $state['timezone'] + ); + $state['timestamp'] = $parsed['timestamp']; + $state['millisecond'] = $parts['millisecond']; + unset($state['unsetFields'][$field]); + IntlError::clear(); + + return true; + } + + public static function setDate( + ObjectEntry $cal, + int $year, + int $month, + ?int $dayOfMonth, + ?int $hour, + ?int $minute, + ?int $second + ): bool { + $state = &self::$state[$cal->id]; + if (!isset($state)) { + return false; + } + $parts = self::parts($state['timezone'], $state['timestamp'], $state['millisecond']); + $parts['year'] = $year; + $parts['month'] = $month + 1; // ICU month + if (null !== $dayOfMonth) { + $parts['day'] = $dayOfMonth; + } + if (null !== $hour) { + $parts['hour'] = $hour; + } + if (null !== $minute) { + $parts['minute'] = $minute; + } + if (null !== $second) { + $parts['second'] = $second; + } + $parsed = VmDateTimeNative::parseDateTime( + \sprintf( + '%04d-%02d-%02d %02d:%02d:%02d', + $parts['year'], + $parts['month'], + $parts['day'], + $parts['hour'], + $parts['minute'], + $parts['second'] + ), + $state['timezone'] + ); + $state['timestamp'] = $parsed['timestamp']; + $state['unsetFields'] = []; + IntlError::clear(); + + return true; + } + + public static function getTimeZoneObject(ObjectEntry $cal, Context $ctx): ObjectEntry|false + { + $state = self::$state[$cal->id] ?? null; + if (null === $state) { + return false; + } + + return VmIntlTimeZone::createFromId($ctx, $state['timezone']); + } + + public static function getTime(ObjectEntry $cal): float|false + { + $state = self::$state[$cal->id] ?? null; + if (null === $state) { + return false; + } + IntlError::clear(); + + return ((float) $state['timestamp']) * 1000.0 + (float) $state['millisecond']; + } + + public static function setTime(ObjectEntry $cal, float $millis): bool + { + $state = &self::$state[$cal->id]; + if (!isset($state)) { + return false; + } + if ($millis < 0) { + $sec = (int) ceil($millis / 1000.0); + $ms = (int) round($millis - ($sec * 1000.0)); + } else { + $sec = (int) floor($millis / 1000.0); $ms = (int) round($millis - ($sec * 1000.0)); } if ($ms < 0) { @@ -402,6 +888,7 @@ public static function setTime(ObjectEntry $cal, float $millis): bool } $state['timestamp'] = $sec; $state['millisecond'] = $ms; + $state['unsetFields'] = []; IntlError::clear(); return true; @@ -674,3 +1161,377 @@ public function execute(Frame $frame): void $frame->returnVar->bool($ok); } } + +/** IntlCalendar::getType() — php-src intlcal_get_type (#20756). */ +final class IntlCalendarGetType extends VmClassMethod +{ + public function __construct() + { + parent::__construct('getType'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if (1 !== $argc) { + throw new \ArgumentCountError(\sprintf( + 'IntlCalendar::getType() expects exactly 0 arguments, %d given', + max(0, $argc - 1) + )); + } + $receiver = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $receiver->type + || !VmIntlCalendar::isCalendarObject($receiver->toObject())) { + throw new \Error('IntlCalendar::getType() called on incompatible object'); + } + $type = VmIntlCalendar::getType($receiver->toObject()); + if (null === $frame->returnVar) { + return; + } + if (false === $type) { + $frame->returnVar->bool(false); + + return; + } + $frame->returnVar->string($type); + } +} + +/** IntlCalendar::getNow() — php-src intlcal_get_now (#20756). */ +final class IntlCalendarGetNow extends VmClassMethod +{ + public function __construct() + { + parent::__construct('getNow'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if (0 !== $argc) { + throw new \ArgumentCountError(\sprintf( + 'IntlCalendar::getNow() expects exactly 0 arguments, %d given', + $argc + )); + } + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->float(VmIntlCalendar::getNow()); + } +} + +/** IntlCalendar::add() — php-src intlcal_add (#20756). */ +final class IntlCalendarAdd extends VmClassMethod +{ + public function __construct() + { + parent::__construct('add'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if (3 !== $argc) { + throw new \ArgumentCountError(\sprintf( + 'IntlCalendar::add() expects exactly 2 arguments, %d given', + max(0, $argc - 1) + )); + } + $receiver = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $receiver->type + || !VmIntlCalendar::isCalendarObject($receiver->toObject())) { + throw new \Error('IntlCalendar::add() called on incompatible object'); + } + $field = VmIntlDateFormatter::coerceIntArg($frame->calledArgs[1], 'IntlCalendar::add', 1, 'field'); + $amount = VmIntlDateFormatter::coerceIntArg($frame->calledArgs[2], 'IntlCalendar::add', 2, 'value'); + $ok = VmIntlCalendar::add($receiver->toObject(), $field, $amount); + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->bool($ok); + } +} + +/** IntlCalendar::roll() — php-src intlcal_roll (#20756). */ +final class IntlCalendarRoll extends VmClassMethod +{ + public function __construct() + { + parent::__construct('roll'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if (3 !== $argc) { + throw new \ArgumentCountError(\sprintf( + 'IntlCalendar::roll() expects exactly 2 arguments, %d given', + max(0, $argc - 1) + )); + } + $receiver = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $receiver->type + || !VmIntlCalendar::isCalendarObject($receiver->toObject())) { + throw new \Error('IntlCalendar::roll() called on incompatible object'); + } + $field = VmIntlDateFormatter::coerceIntArg($frame->calledArgs[1], 'IntlCalendar::roll', 1, 'field'); + $amount = VmIntlDateFormatter::coerceIntArg($frame->calledArgs[2], 'IntlCalendar::roll', 2, 'value'); + $ok = VmIntlCalendar::roll($receiver->toObject(), $field, $amount); + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->bool($ok); + } +} + +/** IntlCalendar::clear() — php-src intlcal_clear (#20756). */ +final class IntlCalendarClear extends VmClassMethod +{ + public function __construct() + { + parent::__construct('clear'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + $userArgc = max(0, $argc - 1); + if ($userArgc > 1) { + throw new \ArgumentCountError(\sprintf( + 'IntlCalendar::clear() expects at most 1 argument, %d given', + $userArgc + )); + } + $receiver = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $receiver->type + || !VmIntlCalendar::isCalendarObject($receiver->toObject())) { + throw new \Error('IntlCalendar::clear() called on incompatible object'); + } + $field = null; + if (2 === $argc) { + $field = VmIntlDateFormatter::coerceIntArg($frame->calledArgs[1], 'IntlCalendar::clear', 1, 'field'); + } + $ok = VmIntlCalendar::clear($receiver->toObject(), $field); + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->bool($ok); + } +} + +/** IntlCalendar::isSet() — php-src intlcal_is_set (#20756). */ +final class IntlCalendarIsSet extends VmClassMethod +{ + public function __construct() + { + parent::__construct('isSet'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if (2 !== $argc) { + throw new \ArgumentCountError(\sprintf( + 'IntlCalendar::isSet() expects exactly 1 argument, %d given', + max(0, $argc - 1) + )); + } + $receiver = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $receiver->type + || !VmIntlCalendar::isCalendarObject($receiver->toObject())) { + throw new \Error('IntlCalendar::isSet() called on incompatible object'); + } + $field = VmIntlDateFormatter::coerceIntArg($frame->calledArgs[1], 'IntlCalendar::isSet', 1, 'field'); + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->bool(VmIntlCalendar::isSet($receiver->toObject(), $field)); + } +} + +/** IntlCalendar::equals() — php-src intlcal_equals (#20756). */ +final class IntlCalendarEquals extends VmClassMethod +{ + public function __construct() + { + parent::__construct('equals'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if (2 !== $argc) { + throw new \ArgumentCountError(\sprintf( + 'IntlCalendar::equals() expects exactly 1 argument, %d given', + max(0, $argc - 1) + )); + } + $receiver = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $receiver->type + || !VmIntlCalendar::isCalendarObject($receiver->toObject())) { + throw new \Error('IntlCalendar::equals() called on incompatible object'); + } + $other = $frame->calledArgs[1]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $other->type + || !VmIntlCalendar::isCalendarObject($other->toObject())) { + throw new \TypeError('IntlCalendar::equals(): Argument #1 ($other) must be of type IntlCalendar, ' + .\PHPCompiler\VM\ReflectionSupport::valueTypeLabelPublic($other).' given'); + } + if (null === $frame->returnVar) { + return; + } + $frame->returnVar->bool(VmIntlCalendar::equals($receiver->toObject(), $other->toObject())); + } +} + +/** IntlCalendar::toDateTime() — php-src intlcal_to_date_time (#20756). */ +final class IntlCalendarToDateTime extends VmClassMethod +{ + public function __construct() + { + parent::__construct('toDateTime'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if (1 !== $argc) { + throw new \ArgumentCountError(\sprintf( + 'IntlCalendar::toDateTime() expects exactly 0 arguments, %d given', + max(0, $argc - 1) + )); + } + $receiver = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $receiver->type + || !VmIntlCalendar::isCalendarObject($receiver->toObject())) { + throw new \Error('IntlCalendar::toDateTime() called on incompatible object'); + } + $dt = VmIntlCalendar::toDateTime($receiver->toObject(), $frame->vmContext); + if (null === $frame->returnVar) { + return; + } + if (false === $dt) { + $frame->returnVar->bool(false); + + return; + } + $frame->returnVar->object($dt); + } +} + +/** IntlCalendar::fromDateTime() — php-src intlcal_from_date_time (#20756). */ +final class IntlCalendarFromDateTime extends VmClassMethod +{ + public function __construct() + { + parent::__construct('fromDateTime'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if ($argc < 1 || $argc > 2) { + throw new \ArgumentCountError(\sprintf( + 'IntlCalendar::fromDateTime() expects between 1 and 2 arguments, %d given', + $argc + )); + } + $arg0 = $frame->calledArgs[0]->resolveIndirect(); + $datetime = null; + if (Variable::TYPE_OBJECT === $arg0->type) { + $datetime = DateTimeSupport::requireDateTime( + $arg0, + 'IntlCalendar::fromDateTime', + 1, + 'datetime', + $frame->vmContext + ); + } elseif (Variable::TYPE_STRING === $arg0->type) { + $datetime = $arg0->toString(); + } else { + $datetime = VmString::coerceStringBuiltinArg( + $arg0, + 'IntlCalendar::fromDateTime', + 1, + 'datetime' + ); + } + $locale = null; + if ($argc >= 2) { + $localeVar = $frame->calledArgs[1]->resolveIndirect(); + if (Variable::TYPE_NULL !== $localeVar->type) { + $locale = VmString::coerceStringBuiltinArg( + $localeVar, + 'IntlCalendar::fromDateTime', + 2, + 'locale' + ); + } + } + $cal = VmIntlCalendar::fromDateTime($frame->vmContext, $datetime, $locale); + if (null === $frame->returnVar) { + return; + } + if (false === $cal) { + $frame->returnVar->bool(false); + + return; + } + $frame->returnVar->object($cal); + } +} + +/** IntlCalendar::fieldDifference() — php-src intlcal_field_difference (#20756). */ +final class IntlCalendarFieldDifference extends VmClassMethod +{ + public function __construct() + { + parent::__construct('fieldDifference'); + } + + public function execute(Frame $frame): void + { + $argc = \count($frame->calledArgs); + if (3 !== $argc) { + throw new \ArgumentCountError(\sprintf( + 'IntlCalendar::fieldDifference() expects exactly 2 arguments, %d given', + max(0, $argc - 1) + )); + } + $receiver = $frame->calledArgs[0]->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $receiver->type + || !VmIntlCalendar::isCalendarObject($receiver->toObject())) { + throw new \Error('IntlCalendar::fieldDifference() called on incompatible object'); + } + $tsArg = $frame->calledArgs[1]->resolveIndirect(); + if (Variable::TYPE_INTEGER === $tsArg->type) { + $targetMs = (float) $tsArg->toInt(); + } elseif (Variable::TYPE_FLOAT === $tsArg->type) { + $targetMs = $tsArg->toFloat(); + } else { + $targetMs = (float) VmIntlDateFormatter::coerceIntArg( + $tsArg, + 'IntlCalendar::fieldDifference', + 1, + 'timestamp' + ); + } + $field = VmIntlDateFormatter::coerceIntArg( + $frame->calledArgs[2], + 'IntlCalendar::fieldDifference', + 2, + 'field' + ); + $result = VmIntlCalendar::fieldDifference($receiver->toObject(), $targetMs, $field); + if (null === $frame->returnVar) { + return; + } + if (false === $result) { + $frame->returnVar->bool(false); + + return; + } + $frame->returnVar->int($result); + } +} diff --git a/test/compliance/cases/intl/intlcalendar_methods.phpt b/test/compliance/cases/intl/intlcalendar_methods.phpt new file mode 100644 index 00000000000..a0ed88b9fa3 --- /dev/null +++ b/test/compliance/cases/intl/intlcalendar_methods.phpt @@ -0,0 +1,55 @@ +--TEST-- +IntlCalendar getType/add/roll/clear/equals/toDateTime/fromDateTime/getNow (#20756) +--FILE-- +setTime(1705320000000.0); // 2024-01-15 12:00:00 UTC + +echo 'type=', $cal->getType(), "\n"; +echo 'getNow_float=', (int) is_float(IntlCalendar::getNow()), "\n"; + +$cal->add(IntlCalendar::FIELD_DAY_OF_MONTH, 1); +echo 'after_add_day=', $cal->get(IntlCalendar::FIELD_YEAR), '-', + $cal->get(IntlCalendar::FIELD_MONTH) + 1, '-', + $cal->get(IntlCalendar::FIELD_DAY_OF_MONTH), "\n"; + +$cal->setTime(1705320000000.0); +$cal->roll(IntlCalendar::FIELD_MONTH, 1); +echo 'after_roll_month=', $cal->get(IntlCalendar::FIELD_YEAR), '-', + $cal->get(IntlCalendar::FIELD_MONTH) + 1, '-', + $cal->get(IntlCalendar::FIELD_DAY_OF_MONTH), "\n"; + +$cal->setTime(1706702400000.0); // 2024-01-31 12:00:00 UTC +$cal->roll(IntlCalendar::FIELD_DAY_OF_MONTH, 1); +echo 'roll_dom_wrap=', $cal->get(IntlCalendar::FIELD_DAY_OF_MONTH), "\n"; + +$cal->setTime(1705320000000.0); +$dt = $cal->toDateTime(); +echo 'toDateTime=', $dt->format('Y-m-d H:i:s'), "\n"; + +$from = IntlCalendar::fromDateTime(new DateTime('2024-06-01 00:00:00', new DateTimeZone('UTC'))); +echo 'fromDateTime=', $from->get(IntlCalendar::FIELD_YEAR), '-', + $from->get(IntlCalendar::FIELD_MONTH) + 1, '-', + $from->get(IntlCalendar::FIELD_DAY_OF_MONTH), "\n"; + +$other = IntlCalendar::createInstance('UTC', 'en_US'); +$other->setTime($cal->getTime()); +echo 'equals=', (int) $cal->equals($other), "\n"; + +$cal->clear(IntlCalendar::FIELD_HOUR_OF_DAY); +echo 'isset_hour_after_clear=', (int) $cal->isSet(IntlCalendar::FIELD_HOUR_OF_DAY), "\n"; + +$cal->clear(); +echo 'after_clear_all_year=', $cal->get(IntlCalendar::FIELD_YEAR), "\n"; +?> +--EXPECT-- +type=gregorian +getNow_float=1 +after_add_day=2024-1-16 +after_roll_month=2024-2-15 +roll_dom_wrap=1 +toDateTime=2024-01-15 12:00:00 +fromDateTime=2024-6-1 +equals=1 +isset_hour_after_clear=0 +after_clear_all_year=1970 diff --git a/test/repro/issue_intlcalendar_methods.php b/test/repro/issue_intlcalendar_methods.php new file mode 100644 index 00000000000..ac8b4f33ac0 --- /dev/null +++ b/test/repro/issue_intlcalendar_methods.php @@ -0,0 +1,46 @@ +setTime(1705320000000.0); // 2024-01-15 12:00:00 UTC + +echo 'type=', $cal->getType(), "\n"; +echo 'getNow_float=', (int) is_float(IntlCalendar::getNow()), "\n"; + +$cal->add(IntlCalendar::FIELD_DAY_OF_MONTH, 1); +echo 'after_add_day=', $cal->get(IntlCalendar::FIELD_YEAR), '-', + $cal->get(IntlCalendar::FIELD_MONTH) + 1, '-', + $cal->get(IntlCalendar::FIELD_DAY_OF_MONTH), "\n"; + +$cal->setTime(1705320000000.0); +$cal->roll(IntlCalendar::FIELD_MONTH, 1); +echo 'after_roll_month=', $cal->get(IntlCalendar::FIELD_YEAR), '-', + $cal->get(IntlCalendar::FIELD_MONTH) + 1, '-', + $cal->get(IntlCalendar::FIELD_DAY_OF_MONTH), "\n"; + +$cal->setTime(1705320000000.0); +$dt = $cal->toDateTime(); +echo 'toDateTime=', $dt->format('Y-m-d H:i:s'), ' ', $dt->getTimezone()->getName(), "\n"; + +$from = IntlCalendar::fromDateTime(new DateTime('2024-06-01 00:00:00', new DateTimeZone('UTC'))); +echo 'fromDateTime=', $from->get(IntlCalendar::FIELD_YEAR), '-', + $from->get(IntlCalendar::FIELD_MONTH) + 1, '-', + $from->get(IntlCalendar::FIELD_DAY_OF_MONTH), "\n"; + +$other = IntlCalendar::createInstance('UTC', 'en_US'); +$other->setTime($cal->getTime()); +echo 'equals=', (int) $cal->equals($other), "\n"; + +$cal->clear(IntlCalendar::FIELD_HOUR_OF_DAY); +echo 'isset_hour_after_clear=', (int) $cal->isSet(IntlCalendar::FIELD_HOUR_OF_DAY), "\n"; + +$cal->clear(); +echo 'after_clear_all_year=', $cal->get(IntlCalendar::FIELD_YEAR), "\n"; +echo 'methods=', + (int) method_exists($cal, 'getType'), + (int) method_exists($cal, 'add'), + (int) method_exists($cal, 'roll'), + (int) method_exists($cal, 'clear'), + (int) method_exists($cal, 'toDateTime'), + (int) method_exists(IntlCalendar::class, 'fromDateTime'), + (int) method_exists(IntlCalendar::class, 'getNow'), + "\n";