gtfs-utils API
February 26, 2022 · View on GitHub
- store API
readCsv(path)readStops(readFile, filter)readTrips(readFile, filter)parseDate(dateStr, timezone)formatDate(t, timezone)parseTime(timeStr)routeTypesreadServicesAndExceptions(readFile, timezone, filters)computeConnections(readFile, timezone, filter)computeSchedules(readFile, filters, [computeSignature])computeSortedConnections(readFile, filters, timezone)findAlternativeTrips(trips, services, schedules) => (fromId, tDep, toId, tArr)computeServiceBreaks(sortedConnections)computeStopovers(readFile, timezone, filters)readPathways(readFile, filters)readShapes(readFile, filters)computeTrajectories(readFile, filters)optimiseServicesAndExceptions(readFile, timezone, filters)
readCsv
const readCsv = require('gtfs-utils/read-csv')
readCsv('path-to-file.txt')
.on('error', console.error)
.on('data', row => console.log(row))
// or
for await (const row of await readCsv('path-to-file.txt')) {
console.log(row)
}
readCsv(path) is an async function that returns a readable stream in objectMode.
path can also be a readable stream like process.stdin.
readStops
const readCsv = require('gtfs-utils/read-csv')
const readStops = require('gtfs-utils/read-stops')
const readFile = name => readCsv('path/to/gtfs/' + name + '.txt')
const stops = await readStops(readFile, {
stop: s => s.stop_id[0] === 'a',
})
for await (const stop of stops.values()) {
console.log(stop)
break
}
{
stop_id: 'airport',
stop_name: 'International Airport (ABC)',
stop_lat: '52',
stop_lon: '14',
stop_code: '🛫',
stop_desc: 'train station at the Internationl Airport (ABC)',
stop_url: 'https://fta.example.org/stations/airport.html',
location_type: '1',
stop_timezone: 'Europe/Berlin',
wheelchair_boarding: '1',
parent_station: '',
level_id: '',
platform_code: '',
stops: ['airport-1', 'airport-2'],
entrances: ['airport-entrance'],
boardingAreas: [],
}
readStops(readFile, filters = {}, opt = {})
- reads
stops.txtinto a storestop_id => stop, - adds stop IDs of a station as
station.stop, - adds entrance IDs of a stop as
stop.entrances, - adds boarding area IDs of a stop as
stop.boardingAreas.
readTrips
const readCsv = require('gtfs-utils/read-csv')
const readTrips = require('gtfs-utils/read-trips')
const readFile = name => readCsv('path/to/gtfs/' + name + '.txt')
const trips = await readTrips(readFile, {
trip: t => t.route_id === 'A',
})
for await (const trip of trips.values()) {
console.log(trip)
break
}
{
trip_id: 'a-downtown-all-day',
route_id: 'A',
service_id: 'all-day'
}
readTrips(readFile, filters = {}, opt = {}) reads trips.txt and reduces it into a map tripId => trip. Returns a store.
parseDate
const parseDate = require('gtfs-utils/parse-date')
parseDate('20190303', 'Europe/Berlin')
// 1551567600
parseDate(dateStr, timezone) parses a GTFS Date value.
dateStrmust be in theYYYYMMDDformat, as specific in GTFS.timezonemust a timezone name from the tz database.
formatDate(t, timezone)
const formatDate = require('gtfs-utils/format-date')
formatDate(1551567600, 'Europe/Berlin')
// '20190303'
formatDate(t, timezone) formats a timestamp as a GTFS Date value.
tmust be a Unix timestamp.timezonemust a timezone name from the tz database.
parseTime
const parseTime = require('gtfs-utils/parse-date')
parseTime('21:30')
// {hours: 21, minutes: 30, seconds: null}
parseTime('21:30:45')
// {hours: 21, minutes: 30, seconds: 45}
parseTime(timeStr) parses a GTFS Time value.
routeTypes
const routeTypes = require('gtfs-utils/route-types')
console.log(routeTypes.basic.find(type => type.gtfs === 3))
// { gtfs: 3, fptf: 'bus' }
fptf contains the Friendly Public Transport Format (FPTF) mode.
pickupTypes
const pickupTypes = require('gtfs-utils/pickup-types')
console.log(pickupTypes.MUST_PHONE_AGENCY) // 2
Contains all possible values of the pickup_type field in stop_times.txt.
dropOffTypes
const dropOffTypes = require('gtfs-utils/drop-off-types')
console.log(dropOffTypes.MUST_COORDINATE_WITH_DRIVER) // 3
Contains all possible values of the drop_off_type field in stop_times.txt.
bookingTypes
const bookingTypes = require('gtfs-utils/booking-types')
console.log(bookingTypes.SAME_DAY) // 1
Contains all possible values of the booking_type field in booking_rules.txt of the GTFS-BookingRules extension (from GTFS-Flex).
readServicesAndExceptions
const readCsv = require('gtfs-utils/read-csv')
const readServices = require('gtfs-utils/read-services-and-exceptions')
const readFile = name => readCsv('path/to/gtfs/' + name + '.txt')
const filters = {
service: s => s.monday === '1',
serviceException: e => e.exception_type === '2'
}
const services = readServices(readFile, 'Europe/Berlin', filters)
for await (const [id, days, svc] of services) console.log(id, days, svc)
readServicesAndExceptions(readFile, timezone, filters = {}) reads calendar.txt and calendar_dates.txt and condenses each service into the a list of days it is valid for. Returns an async iterable of [serviceId, daysOfOperation] entries.
The code above will print the following:
service-1 [
1551394800,
1551481200,
1551567600,
1551654000,
// …
] {
service_id: 'service-1',
start_date: '20190301',
end_date: '20190531',
monday: '1',
tuesday: '1',
wednesday: '0',
thursday: '0',
friday: '1',
saturday: '1',
sunday: '1',
}
service-2 [
1551567600,
1552690800,
1555797600
] {
service_id: 'service-1',
start_date: '20190227',
end_date: '20190510',
monday: '0',
// …
}
// …
Note: Be careful when filtering services by day of the week! Time values in stop_times.txt can be >24h, so the day in calendar.txt does not necessarily indicate the day of every stopover.
computeConnections
const readCsv = require('gtfs-utils/read-csv')
const computeConnections = require('gtfs-utils/compute-connections')
const readFile = name => readCsv('path/to/gtfs/' + name + '.txt')
const connectionsByTrip = computeConnections(readFile, {
stopTime: s => s.stop_id === 'some-stop-id',
})
for await (const connectionsOfTrip of connectionsByTrip) {
for (const connection of connectionsOfTrip) console.log(connection)
break
}
{
tripId: 'a-downtown-all-day',
fromStop: 'airport',
// .departure and .arrival are *not* "wall clock times", but seconds since
// noon minus 12 hours!
// see https://gist.github.com/derhuerst/574edc94981a21ef0ce90713f1cff7f6)
departure: 55440, // 15h, 24m
toStop: 'museum',
arrival: 55800, // 15h, 30m
}
{
tripId: 'a-downtown-all-day',
fromStop: 'museum',
departure: 55860, // 15h, 31m
toStop: 'center',
arrival: 56100, // 15h, 35m
}
A "connection" is a pair of stop_times. computeConnections(readFile, filters = {}) iterates over stop_times.txt and frequencies.txt, and emits all connections in the whole dataset. It returns an async iterable.
computeSchedules
This utility computes what we call "schedules", temporal patterns by which vehicles visit stops. An example schedule:
{
id: '248tGP', // signature, just a hash of the schedule's data
trips: [
// The trip `a downtown-all-day-1` follows this schedule and starts
// 55380 seconds after `noon - 12h` on each day it runs.
{tripId: 'a-downtown-all-day-1', start: 55380}
],
// Arrives at `airport` after 0s, departs 30s later.
// Arrives at `museum` after 420s, departs 60s later.
// Arrives at `center` after 720s, departs 90s later.
stops: ['airport', 'museum', 'center'],
arrivals: [0, 420, 720],
departures: [30, 480, 810],
}
In schedule-based public transport systems, schedules reduce the implicit complexity of GTFS data sets a lot, because one schedule summarizes many trips with a certain "pattern". Paired with readServicesAndExceptions, you can easily answer questions like Which vehicles run from X to Y at T? and Which other vehicles run as well?.
const readCsv = require('gtfs-utils/read-csv')
const computeSchedules = require('gtfs-utils/compute-schedules')
const readFile = name => readCsv('path/to/gtfs/' + name + '.txt')
const schedules = await computeSchedules(readFile)
for await (const schedule of schedules.values()) {
console.log(schedule)
}
computeSchedules(readFile, filters = {}, opt = {}) reads trips.txt and stop_times.txt and computes schedules from it. Returns a store.
readFilemust be a function that, when called with a file name, returns a readable stream inobjectMode.filtersmust be an object; It may have the fieldstrip,stopTime&frequencies, each with a filter function.optmust be an object; It may optionally have a customcomputeSigfunction that, given a schedule, computes a signature of it.
Note: In order to work, it must load (a reduced form of) trips.txt, stop_times.txt and frequencies.txt into memory. See store API for more details.
computeStopovers
const readCsv = require('gtfs-utils/read-csv')
const computeStopovers = require('gtfs-utils/compute-stopovers')
const readFile = name => readCsv('path/to/gtfs/' + name + '.txt')
const stopovers = computeStopovers(readFile, 'Europe/Berlin', {
trip: t => t.route_id === 'A',
stopTime: s => s.stop_id === 'some-stop-id',
})
for await (const stopover of stopovers) {
console.log(stopover)
}
computeStopovers(readFile, timezone, filters = {}) reads per-day stop times from trips.txt, stop_times.txt and frequencies.txt, and applies them to the days of operation returned by readServicesAndExceptions(readFile, timezone, filters), in order to compute absolute stop times.
readFilemust be a function that, when called with a file name, returns a readable stream inobjectMode.timezonemust a timezone name from the tz database.filtersmust be an object; It may have the fieldstrip,service,serviceException,stopTime,frequencies, each with a filter function.
Returns an async iterable of {stop_id, trip_id, service_id, route_id, start_of_trip, arrival, departure} entries.
The code above will print the following:
{
stop_id: 'airport',
trip_id: 'a-downtown-all-day',
service_id: 'all-day',
route_id: 'A',
shape_id: 'a-downtown-all-day-s0',
start_of_trip: 1551394800, // 2019-03-01T00:00:00+01:00
arrival: 1551450180, // 2019-03-01T15:23:00+01:00
departure: 1551450240, // 2019-03-01T15:24:00+01:00
// Items have this additional entry if they're based on an exact_times=0
// entry in frequencies.txt. See https://gtfs.org/reference/static/#frequenciestxt
// headwayBased: true,
}
{
stop_id: 'museum',
trip_id: 'a-downtown-all-day',
service_id: 'all-day',
route_id: 'A',
shape_id: 'a-downtown-all-day-s0',
start_of_trip: 1551394800, // 2019-03-01T00:00:00+01:00
arrival: 1551450600, // 2019-03-01T15:30:00+01:00
departure: 1551450660, // 2019-03-01T15:31:00+01:00
}
// …
Note: In order to work, it must load reduced forms of trips.txt, calendar.txt and calendar_dates.txt into memory. See store API for more details.
computeSortedConnections
const readCsv = require('gtfs-utils/read-csv')
const computeSortedConnections = require('gtfs-utils/compute-sorted-connections')
const readFile = name => readCsv('path/to/gtfs/' + name + '.txt')
const sortedConnections = await computeSortedConnections(readFile, 'Europe/Berlin')
const from = 1552324800 // UNIX timestamp
const to = 1552393800 // UNIX timestamp
const fromI = sortedConnections.findIndex(c => c.departure >= from)
const endI = sortedConnections.findIndex(c => c.departure > to)
for (let i = 0; i < endI; i++) {
console.log(sortedConnections[i])
}
{
tripId: 'b-outbound-on-working-days',
fromStop: 'lake',
departure: 1552324920, // 2019-03-11T18:22:00+01:00
toStop: 'airport',
arrival: 1552325400, // 2019-03-11T18:30:00+01:00
routeId: 'B',
serviceId: 'on-working-days',
}
{
tripId: 'b-downtown-on-working-days',
fromStop: 'airport',
departure: 1552392840, // 2019-03-12T13:14:00+01:00
toStop: 'lake',
arrival: 1552393200, // 2019-03-12T13:20:00+01:00
routeId: 'B',
serviceId: 'on-working-days',
}
computeSortedConnections(readFile, timezone, filters = {}) reads all connections and applies each to the respective service, to compute all absolute-time connections in the dataset. Returns a Promise that will resolve with an array of connections.
readFilemust be a function that, when called with a file name, returns a readable stream inobjectMode.timezonemust a timezone name from the tz database.filtersmust be an object; It may have the fieldsservice,trip&stopover, each with a filter function.
Note: computeSortedConnections must load (reduced forms of) trips.txt, stop_times.txt and frequencies.txt into memory. See store API for more details.
findAlternativeTrips
fromId --time window--> toId
departure at tDep arrival at tArr
For a time window (tDep, tArr) to get from stop fromId to stop toId, findAlternativeTrips will return a list of all trips that run from fromId to toId equally fast or faster.
// signature:
async findAlternativeTrips(readFile, timezone, services, schedules) => async function* findAltTrips(fromId, tDep, toId, tArr) {}
services must be a store with a serviceId => daysOfOperation mapping. schedules must be in the format returned by computeSchedules.
Note: The purpose of this function is to identify direct alternative trips to a given trip; It is not a replacement for a proper routing engine. (There might be a faster way from fromId to toId via transfer, and findAlternativeTrips won't return it.)
As an example, we're gonna use sample-gtfs-feed:
const readCsv = require('gtfs-utils/read-csv')
const inMemoryStore = require('gtfs-utils/lib/in-memory-store')
const readServices = require('gtfs-utils/read-services-and-exceptions')
const computeSchedules = require('gtfs-utils/compute-schedules')
const findAlternativeTrips = require('gtfs-utils/find-alternative-trips')
const readFile = name => readCsv('path/to/gtfs/' + name + '.txt')
const timezone = 'Europe/Berlin'
// read services into in-memory store
const services = inMemoryStore()
for await (const [id, svc] of readServices(readFile, timezone)) {
await services.set(id, svc)
}
// read schedules
const schedules = await computeSchedules(readFile)
// travel times of a downtown trip of the A line
const fromId = 'airport'
const departure = 1551795840 // 2019-03-05T15:24:00+01:00
const toId = 'center'
const arrival = 1551796500 // 2019-03-05T15:35:00+01:00
// find an alternative trip of the C line
const findAltTrips = await findAlternativeTrips(
readFile,
timezone,
services,
schedules,
)
const altTrips = findAltTrips(fromId, departure, toId, arrival)
for await (const alt of altTrips) console.log(alt)
{ // This is the trip we were using as query.
tripId: 'a-downtown-all-day',
routeId: 'A',
serviceId: 'all-day',
departure: 1551795840, // 2019-03-05T15:24:00+01:00
arrival: 1551796500, // 2019-03-05T15:35:00+01:00
}
{ // This is an alternative trip.
tripId: 'c-downtown-all-day',
routeId: 'C',
serviceId: 'all-day',
departure: 1551796080, // 2019-03-05T15:28:00+01:00
arrival: 1551796380, // 2019-03-05T15:33:00+01:00
}
computeServiceBreaks
Most public transport networks don't run 24/7, but instead have regular scheduled "service breaks", e.g. at night or on Sundays.
Given sorted connections, computeServiceBreaks(sortedConnections, opt = {}) finds periods of time without service between two stations.
It depends on the specific network what period of time can be considered a "break": In a large city, it could be no bus/train running from 2am to 3am; In a small town there might only be a bus/train every hour, with a break of 8 hours at night. You can pass a custom opt.minLength value in seconds.
const readCsv = require('gtfs-utils/read-csv')
const computeSortedConnections = require('gtfs-utils/compute-sorted-connections')
const computeServiceBreaks = require('gtfs-utils/compute-service-breaks')
const readFile = name => readCsv('path/to/gtfs/' + name + '.txt')
let connections = await computeSortedConnections(readFile, 'Europe/Berlin')
// select time frame
const start = 1557313200 // 2019-05-08T12:00:00+01:00
const startI = connections.findIndex(c => c.departure >= start)
const end = 1557496800 // 2019-05-10T15:00:00+01:00
const endI = connections.findIndex(c => c.departure > end)
connections = connections.slice(startI, endI)
const serviceBreaks = computeServiceBreaks(connections, {
minLength: 30 * 60, // 30 minutes
})
for await (const serviceBreak of serviceBreaks) {
console.log(serviceBreak)
}
store API
Some of the tools above need to read data into a map in order to work with it. In memory-constrained environments (such as FaaSes), the amount of data to be read might be bigger than the available memory; This is why gtfs-utils allows you to pass in your own store implementation.
In this case you could pass in a Redis-backed store instead:
const createRedisStore = require('gtfs-utils/lib/redis-store')
computeSchedules(readFile, filters, {
// let computeSchedules use a Redis-backed store
createStore: createRedisStore,
})
These stores are available:
gtfs-utils/lib/in-memory-store– stores data in memory; used by defaultgtfs-utils/lib/redis-store– stores data in Redis
readPathways
const readCsv = require('gtfs-utils/read-csv')
const readPathways = require('gtfs-utils/read-pathways')
const readFile = name => readCsv('path/to/gtfs/' + name + '.txt')
const pathways = readPathways(readFile)
for await (const [stationId, node, allNodes] of pathways) {
console.log(stationId, node)
}
readPathways(readFile, filters = {}, opt = {}) reads all pathways into memory, reads all pathways of a station into a directed graph. Each node of this graph has two fields id and connectedTo:
{ // node `stop-123`
id: 'stop-123',
connectedTo: {
'stop-321': [
{ // pathway connecting `stop-123` to `stop-321`
pathway_id: 'pw-1234',
from_stop_id: 'stop-123',
to_stop_id: 'stop-321',
pathway_mode: '4',
},
{ // node `stop-321`
id: 'stop-321',
connectedTo: {
// …
},
},
// …
],
},
}
readPathways returns an async iterable of [stationId, node, allNodesById] triple.
- For each station, it will only emit one triple, with
nodebeing the first pathway (that is connected to this station) that it came across. allNodesByIdas an object, with all nodes of the graph stored by their IDs.
readShapes
const readCsv = require('gtfs-utils/read-csv')
const readShapes = require('gtfs-utils/read-shapes')
const readFile = name => readCsv('path/to/gtfs/' + name + '.txt')
const shapes = readShapes(readFile)
for await (const [shapeId, points] of shapes) {
console.log(shapeId, points)
}
readShapes(readFile, filters = {}) reads all shapes from shapes.txt. It returns an async iterable of [stationId, points] pairs, with points looking like this:
[
{
shape_pt_lat: 48.59430,
shape_pt_lon: 8.86477,
shape_pt_sequence: 1,
shape_dist_traveled: 0,
}, {
shape_pt_lat: 48.59394,
shape_pt_lon: 8.86377,
shape_pt_sequence: 3,
shape_dist_traveled: 83.98,
},
// …
{
shape_pt_lat: 48.59431,
shape_pt_lon: 8.86476,
shape_pt_sequence: 82,
shape_dist_traveled: 3112.71,
}
]
computeTrajectories
const readCsv = require('gtfs-utils/read-csv')
const computeTrajectories = require('gtfs-utils/compute-trajectories')
const readFile = name => readCsv('path/to/gtfs/' + name + '.txt')
for await (const trajectory of computeTrajectories(readFile)) {
console.log(trajectory)
}
computeTrajectories(readFile, filters = {}) reads per-day stop times from trips.txt, stop_times.txt and frequencies.txt, and applies them to the days of operation returned by readServicesAndExceptions(readFile, timezone, filters), in order to compute absolute stop times.
readFilemust be a function that, when called with a file name, returns a readable stream inobjectMode.filtersmust be an object; It may have the fieldstrip,service,serviceException,stopTime,frequencies, each with a filter function.
Returns an async iterable of trajectories; Each trajectory is a GeoJSON LineString, with additional items in each coordinate:
- longitude
- latitude
- altitude
- arrival time
- departure time
As an example, we're gonna use sample-gtfs-feed:
const readCsv = require('gtfs-utils/read-csv')
const computeTrajectories = require('gtfs-utils/compute-trajectories')
const readFile = async (name) => {
const path = require.resolve('sample-gtfs-feed/gtfs/' + name + '.txt')
return await readCsv(path)
}
const filters = {
trip: t => t.route_id === 'A',
}
for await (const trajectory of computeTrajectories(readFile, filters)) {
console.log(trajectory)
}
{
type: 'Feature',
properties: {
id: 'ZGB8W9-a-downtown-all-day-s0',
scheduleId: 'Z2gvHvF',
shapeId: 'a-downtown-all-day-s0',
tripId: 'a-downtown-all-day',
serviceId: 'all-day',
},
geometry: {
type: 'LineString',
coordinates: [
[13.510294914, 52.364833832, null, 61, 61],
[13.510567665, 52.364398956, null, 63, 63],
[13.510860443, 52.363952637, null, 64, 64],
// …
[13.452836037, 52.44562149, null, 387, 387],
[13.451435089, 52.445671082, null, 390, 390],
[13.449950218, 52.445732117, null, 392, 392],
// …
[13.495876312, 52.500293732, null, 713, 713],
[13.496304512, 52.500156403, null, 714, 714],
[13.497889519, 52.499641418, null, 717, 717],
],
},
}
{
type: 'Feature',
properties: {
id: 'R8lSc-a-outbound-all-day-s0',
scheduleId: 'Z1bgqY0',
shapeId: 'a-outbound-all-day-s0',
tripId: 'a-outbound-all-day',
serviceId: 'all-day',
},
geometry: {
type: 'LineString',
coordinates: [
[13.497889519, 52.499641418, null, 65, 65],
[13.496304512, 52.500156403, null, 69, 69],
[13.495876312, 52.500293732, null, 70, 70],
[13.495686531, 52.500354767, null, 71, 71],
[13.495450974, 52.500431061, null, 71, 71],
// …
[13.465647697, 52.49892807, null, 153, 153],
[13.465513229, 52.498714447, null, 154, 154],
// …
[13.509624481, 52.366386414, null, 720, 720],
[13.509587288, 52.366352081, null, 720, 720],
[13.509503365, 52.366222382, null, 720, 720],
[13.509493828, 52.366146088, null, 721, 721],
[13.509539604, 52.366039276, null, 721, 721],
[13.510294914, 52.364833832, null, 725, 725],
],
},
}
Note: In order to work, computeTrajectories must load reduced forms of trips.txt, stop_times.txt, frequencies.txt and shapes.txt into memory. See store API for more details.
optimiseServicesAndExceptions
A GTFS feed may have a set of calendar.txt and/or calendar_dates.txt rows that express service days in an overly verbose way. Some examples:
- feeds without
calendar.txt, where every service day is expressed as aexception_type=1(added) exception – In many of such cases, we can reduce the number of exceptions by adding a row incalendar.txtwith the respective day(s) turned on (e.g.tuesday=1). - feeds with
calendar.txt, where some services have moreexception_type=2(removed) exceptions than "regular" day-of-the-week-based service dates (e.g.thursday=1) – In this case, we can turn off the "regular" service dates (thursday=0) and useexception_type=1(added) exceptions.
For each service, optimiseServicesAndExceptions computes the optimal combination of day of the week flags (e.g. monday=1) and exceptions, minimalising the number of exceptions necessary to express the set of service dates.
const readCsv = require('gtfs-utils/read-csv')
const optimiseServices = require('gtfs-utils/optimise-services-and-exceptions')
const readFile = name => readCsv('path/to/gtfs/' + name + '.txt')
const services = readServices(readFile, 'Europe/Berlin')
for await (const [id, changed, service, exceptions] of services) {
if (changed) {
console.log(id, 'changed!')
console.log('service:', service)
console.log('exceptions:', exceptions)
} else {
console.log(id, 'unchanged!', id)
}
}
optimiseServicesAndExceptions(readFile, timezone, filters = {}) reads calendar.txt and calendar_dates.txt. It returns an async iterable of [serviceId, changed, service, exceptions] entries.
- If
changedistrue,- the service's
calendar.txtrow orcalendar_dates.txtrows (or both) have been optimised, servicecontains the optimised service,exceptionscontains allcalendar_dates.txtrows applying to the optimised service.
- the service's
- If
changedisfalse,- the service cannot be optimised,
servicecontains thecalendar.txtas it was before, or a mock service if there was none before,exceptionscontains thecalendar_dates.txtrows as they were before.
The test fixture contains three services (more-exceptions-than-regular, more-regular-than-exceptions, should-stay-unchanged), of which the first two can be optimised. With its files as input, the code above will print the following:
more-exceptions-than-regular changed!
service: {
service_id: 'more-exceptions-than-regular',
start_date: '20220301',
end_date: '20220410',
monday: '0',
tuesday: '0',
wednesday: '0',
thursday: '0',
friday: '0',
saturday: '0',
sunday: '0',
}
exceptions: [{
service_id: 'more-exceptions-than-regular',
date: '20220302',
exception_type: '1',
}, {
service_id: 'more-exceptions-than-regular',
date: '20220324',
exception_type: '1',
}, {
service_id: 'more-exceptions-than-regular',
date: '20220330',
exception_type: '1',
}, {
service_id: 'more-exceptions-than-regular',
date: '20220331',
exception_type: '1',
}]
more-regular-than-exceptions changed!
service: {
service_id: 'more-regular-than-exceptions',
monday: '1',
tuesday: '0',
wednesday: '0',
thursday: '0',
friday: '1',
saturday: '0',
sunday: '0',
start_date: '20220301',
end_date: '20220410',
}
exceptions: []
should-stay-unchanged unchanged! should-stay-unchanged