Parsing GTFS times beyond midnight
Parse GTFS service times as seconds from the service day’s start, allowing hours above 23 while rejecting malformed values.
Service time can pass midnight
A GTFS time can describe an event after midnight while still belonging to the same service day. For example, 24:00:00 is one day’s midnight boundary, and 27:15:00 is three hours and fifteen minutes beyond it. Treating the hour as a clock value restricted to 0–23 would reject these useful offsets. The GTFS schedule reference documents this service-time convention [1].
The supplied function maps a time string into a nonnegative number of seconds from the service-day origin. That makes the offset straightforward to compare and avoids confusing a service-day value with an ordinary wall-clock reading.
Validate before converting
The parser first requires a string matching hours, a colon, two minute digits and two second digits. Minutes and seconds are restricted to 00 through 59; the hour component may have one or more digits and is not limited to 23. The code then converts the three components to integers and computes the total seconds.
That validation intentionally rejects values such as 24:60:00, 24:00:60 and 1:2:03. It also rejects negative hours, surrounding whitespace, an empty string and None. These cases are covered by the local tests. The function raises ValueError for each invalid input rather than guessing how to normalize it.
"""Python 3.10+; parse service-day offsets, not timezone instants."""
import re
import unittest
def service_seconds(value):
if not isinstance(value, str) or not re.fullmatch(r"[0-9]+:[0-5][0-9]:[0-5][0-9]", value):
raise ValueError("expected nonnegative H:MM:SS")
hours, minutes, seconds = map(int, value.split(":"))
return hours * 3600 + minutes * 60 + seconds
class ServiceTimeTests(unittest.TestCase):
def test_midnight_and_next_day(self):
self.assertEqual(service_seconds("23:59:59"), 86399)
self.assertEqual(service_seconds("24:00:00"), 86400)
self.assertEqual(service_seconds("27:15:00"), 98100)
self.assertEqual(service_seconds("8:05:00"), 29100)
def test_order_is_numeric(self):
self.assertLess(service_seconds("9:00:00"), service_seconds("10:00:00"))
self.assertGreater(service_seconds("25:00:00"), service_seconds("23:00:00"))
def test_bad_values(self):
for bad in ("-1:00:00", "24:60:00", "24:00:60", "1:2:03", " 24:00:00", "", None):
with self.subTest(value=bad), self.assertRaises(ValueError):
service_seconds(bad)
if __name__ == "__main__":
unittest.main()
Use numeric ordering
The returned integer preserves chronological ordering within the service-day offset model. A lexical comparison of strings can misorder values when hour components have different widths: for instance, text ordering does not reliably express that nine hours precedes ten hours. The test checks the numeric comparisons for 9:00:00 and 10:00:00, as well as the later-day values 23:00:00 and 25:00:00.
Seconds are convenient for comparisons and arithmetic, but the number alone is not a date or an instant. It is an offset relative to a particular service day. Applications should retain that context when combining it with calendar dates or other schedule data.
Offsets are not UTC instants
This parser does not compute UTC timestamps. To place an event on a timeline, an application also needs the service date, the agency’s timezone and the applicable service calendar. GTFS’s local-time rules use a noon-minus-12-hours anchor to handle daylight-saving transitions; simply adding an offset to midnight UTC can yield the wrong result around such changes [1].
Calendar and exception data determine which service operates on a given date. That is separate from parsing a time string. This function can validate and convert the offset, but it cannot decide whether a trip runs on a date or resolve the date and timezone into an instant.
Run the local checks
Download the example as why-can-gtfs-times-exceed-24-00-00.py and run python3 why-can-gtfs-times-exceed-24-00-00.py. The script uses only the Python standard library and requires Python 3.10 or newer. A successful unittest run reports Ran 3 tests and OK.
Those three methods check selected valid offsets, numeric ordering, and invalid values. They establish behavior for the supplied function and cases; they do not establish timezone conversion correctness, schedule applicability, or compliance of a full GTFS feed.