/** * Parse an offset following a prefix and set the ZoneId if it is valid. * To matching the parsing of ZoneId.of the values are not normalized * to ZoneOffsets. * * @param DateTimeParseContext $context the parse context * @param string $text the input text * @param int $prefixPos start of the prefix * @param int $position start of text after the prefix * @param OffsetIdPrinterParser $parser parser for the value after the prefix * @return int the position after the parse */ private function parseOffsetBased(DateTimeParseContext $context, $text, $prefixPos, $position, OffsetIdPrinterParser $parser) { $prefix = strtoupper(substr($text, $prefixPos, $position - $prefixPos)); if ($position >= strlen($text)) { $context->setParsedZone(ZoneId::of($prefix)); return $position; } // '0' or 'Z' after prefix is not part of a valid ZoneId; use bare prefix if ($text[$position] === '0' || $context->charEquals($text[$position], 'Z')) { $context->setParsedZone(ZoneId::of($prefix)); return $position; } $newContext = $context->copy(); $endPos = $parser->parse($newContext, $text, $position); try { if ($endPos < 0) { if ($parser == OffsetIdPrinterParser::INSTANCE_ID_Z()) { return ~$prefixPos; } $context->setParsedZone(ZoneId::of($prefix)); return $position; } $offset = $newContext->getParsed(ChronoField::OFFSET_SECONDS()); $zoneOffset = ZoneOffset::ofTotalSeconds($offset); $context->setParsedZone(ZoneId::ofOffset($prefix, $zoneOffset)); return $endPos; } catch (DateTimeException $dte) { return ~$prefixPos; } }
public function parse(DateTimeParseContext $context, $text, $position) { // TODO cache formatter // new context to avoid overwriting fields like year/month/day $minDigits = $this->fractionalDigits < 0 ? 0 : $this->fractionalDigits; $maxDigits = $this->fractionalDigits < 0 ? 9 : $this->fractionalDigits; $parser = (new DateTimeFormatterBuilder())->append(DateTimeFormatter::ISO_LOCAL_DATE())->appendLiteral('T')->appendValue2(ChronoField::HOUR_OF_DAY(), 2)->appendLiteral(':')->appendValue2(ChronoField::MINUTE_OF_HOUR(), 2)->appendLiteral(':')->appendValue2(ChronoField::SECOND_OF_MINUTE(), 2)->appendFraction(ChronoField::NANO_OF_SECOND(), $minDigits, $maxDigits, true)->appendLiteral('Z')->toFormatter()->toPrinterParser(false); $newContext = $context->copy(); $pos = $parser->parse($newContext, $text, $position); if ($pos < 0) { return $pos; } // parser restricts most fields to 2 digits, so definitely int // correctly parsed nano is also guaranteed to be valid $yearParsed = $newContext->getParsed(ChronoField::YEAR()); $month = $newContext->getParsed(ChronoField::MONTH_OF_YEAR()); $day = $newContext->getParsed(ChronoField::DAY_OF_MONTH()); $hour = $newContext->getParsed(ChronoField::HOUR_OF_DAY()); $min = $newContext->getParsed(ChronoField::MINUTE_OF_HOUR()); $secVal = $newContext->getParsed(ChronoField::SECOND_OF_MINUTE()); $nanoVal = $newContext->getParsed(ChronoField::NANO_OF_SECOND()); $sec = $secVal !== null ? $secVal : 0; $nano = $nanoVal !== null ? $nanoVal : 0; $days = 0; if ($hour === 24 && $min === 0 && $sec === 0 && $nano === 0) { $hour = 0; $days = 1; } else { if ($hour === 23 && $min === 59 && $sec === 60) { $context->setParsedLeapSecond(); $sec = 59; } } $year = $yearParsed % 10000; try { $ldt = LocalDateTime::of($year, $month, $day, $hour, $min, $sec, 0)->plusDays($days); $instantSecs = $ldt->toEpochSecond(ZoneOffset::UTC()); $instantSecs += Math::multiplyExact(Math::div($yearParsed, 10000), self::SECONDS_PER_10000_YEARS); } catch (RuntimeException $ex) { // TODO What do we actually catch here and why return ~$position; } $successPos = $pos; $successPos = $context->setParsedField(ChronoField::INSTANT_SECONDS(), $instantSecs, $position, $successPos); return $context->setParsedField(ChronoField::NANO_OF_SECOND(), $nano, $position, $successPos); }