fix: carry pickup times as local clock readings, not instants

This commit is contained in:
fromme committed 2026-09-30 14:57:46 +02:00
1 parent c2dc88b8ab
commit b41aeb7a25
2 files changed
+37 -8

No files matched your search

@@ -458,7 +458,6 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
$date['services_general'] = serialize($combinedServices);
$pickups = [];
$timezone = new \DateTimeZone('Europe/Berlin');
foreach ($row['pickups'] as $pickup) {
$busProId = (int) $pickup['busProId'];
if ($busProId === 0) {
@@ -467,9 +466,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
$pickupDate = $dateStart->format('d.m.Y');
$pickupTime = '';
if ($pickup['datetime'] !== null) {
$pickupDateTime = (new \DateTimeImmutable($pickup['datetime']))->setTimezone($timezone);
$pickupDate = $pickupDateTime->format('d.m.Y');
$pickupTime = $pickupDateTime->format('H:i');
[$pickupDate, $pickupTime] = $this->parsePickupDateTime($pickup['datetime']);
}
$pickups[] = [
'bus_pro_id' => $busProId,
@@ -803,6 +800,26 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
}
}
/**
* A pickup time is the local date and time as BusPro states it
* ("2027-03-28T02:45:00"), taken over as is. An ISO-8601 time with an
* offset is still accepted and converted to Europe/Berlin, which cannot
* express a time inside the daylight saving gap.
*
* @param string $datetime
* @return string[] date as d.m.Y and time as H:i
*/
protected function parsePickupDateTime(string $datetime): array
{
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::\d{2}(?:\.\d+)?)?$/', $datetime, $parts)) {
return [$parts[3] . '.' . $parts[2] . '.' . $parts[1], $parts[4] . ':' . $parts[5]];
}
$pickupDateTime = (new \DateTimeImmutable($datetime))->setTimezone(new \DateTimeZone('Europe/Berlin'));
return [$pickupDateTime->format('d.m.Y'), $pickupDateTime->format('H:i')];
}
/**
* Reads back a price parseService() formatted for display ("1.234,50").
*
@@ -25,6 +25,7 @@ final class XmlProductReader implements \IteratorAggregate
private const MARKER_TS_FORMAT = 'd.m.Y H:i:s';
private const CODE_PSEUDOPRICE = 'PDGS';
private const TIMEZONE = 'Europe/Berlin';
private const LOCAL_DATETIME_FORMAT = 'Y-m-d\TH:i:s';
/**
* @var string
@@ -41,6 +42,15 @@ final class XmlProductReader implements \IteratorAggregate
*/
private $timezone;
/**
* Pickup times are clock readings, not instants. They are handled in UTC,
* which has no daylight saving gaps, so a time BusPro plans inside one
* (02:45 on the night clocks go forward) is kept as stated.
*
* @var \DateTimeZone
*/
private $wallClock;
/**
* @var array|null pickup master data by BusPro id
*/
@@ -51,6 +61,7 @@ final class XmlProductReader implements \IteratorAggregate
$this->path = rtrim($path, '/');
$this->logger = $logger ?? new NullLogger();
$this->timezone = new \DateTimeZone(self::TIMEZONE);
$this->wallClock = new \DateTimeZone('UTC');
}
public function getPath(): string
@@ -245,7 +256,8 @@ final class XmlProductReader implements \IteratorAggregate
/**
* A bare HH:mm takes the travel date's start, and is rolled onto the next
* day when it would otherwise fall before the latest full date-time listed
* before it: a coach does not board backwards.
* before it: a coach does not board backwards. The result is the local
* date and time as BusPro states it, without an offset.
*/
private function readDatePickups(\SimpleXMLElement $xmlDate, \DateTimeImmutable $from): array
{
@@ -257,12 +269,12 @@ final class XmlProductReader implements \IteratorAggregate
$datetime = null;
if ($time !== '' && strpos($time, ' ') !== false) {
$datetime = \DateTimeImmutable::createFromFormat('!d.m.Y H:i:s', $time, $this->timezone) ?: null;
$datetime = \DateTimeImmutable::createFromFormat('!d.m.Y H:i:s', $time, $this->wallClock) ?: null;
if ($datetime !== null && ($latest === null || $datetime > $latest)) {
$latest = $datetime;
}
} elseif ($time !== '') {
$datetime = \DateTimeImmutable::createFromFormat('!Y-m-d H:i', $from->format('Y-m-d') . ' ' . $time, $this->timezone) ?: null;
$datetime = \DateTimeImmutable::createFromFormat('!Y-m-d H:i', $from->format('Y-m-d') . ' ' . $time, $this->wallClock) ?: null;
if ($datetime !== null && $latest !== null && $datetime < $latest) {
$datetime = $datetime->modify('+1 day');
}
@@ -275,7 +287,7 @@ final class XmlProductReader implements \IteratorAggregate
'city' => $masterData['city'] ?? null,
'street' => $masterData['street'] ?? null,
'price' => $this->parsePrice((string) $xmlPickup['preis']),
'datetime' => $datetime !== null ? $datetime->format(\DateTimeInterface::ATOM) : null,
'datetime' => $datetime !== null ? $datetime->format(self::LOCAL_DATETIME_FORMAT) : null,
];
}