- store API
readCsv(path)
readStops(readFile, filter)
readTrips(readFile, filter)
parseDate(dateStr, timezone)
formatDate(t, timezone)
parseTime(timeStr)
routeTypes
readServicesAndExceptions(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)
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
.
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.txt
into 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
.
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.
const parseDate = require('gtfs-utils/parse-date')
parseDate('20190303', 'Europe/Berlin')
// 1551567600
parseDate(dateStr, timezone)
parses a GTFS Date value.
dateStr
must be in theYYYYMMDD
format, as specific in GTFS.timezone
must a timezone name from the tz database.
const formatDate = require('gtfs-utils/format-date')
formatDate(1551567600, 'Europe/Berlin')
// '20190303'
formatDate(t, timezone)
formats a timestamp as a GTFS Date value.
t
must be a Unix timestamp.timezone
must a timezone name from the tz database.
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.
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.
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
.
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
.
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).
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.
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_time
s. computeConnections(readFile, filters = {})
iterates over stop_times.txt
and frequencies.txt
, and emits all connections in the whole dataset. It returns an async iterable.
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.
readFile
must be a function that, when called with a file name, returns a readable stream inobjectMode
.filters
must be an object; It may have the fieldstrip
,stopTime
&frequencies
, each with a filter function.opt
must be an object; It may optionally have a customcomputeSig
function 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.
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.
readFile
must be a function that, when called with a file name, returns a readable stream inobjectMode
.timezone
must a timezone name from the tz database.filters
must 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.
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.
readFile
must be a function that, when called with a file name, returns a readable stream inobjectMode
.timezone
must a timezone name from the tz database.filters
must 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.
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
}
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)
}
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
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
node
being the first pathway (that is connected to this station) that it came across. allNodesById
as an object, with all nodes of the graph stored by their IDs.
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,
}
]
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.
readFile
must be a function that, when called with a file name, returns a readable stream inobjectMode
.filters
must 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.
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.txt
with 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
changed
istrue
,- the service's
calendar.txt
row orcalendar_dates.txt
rows (or both) have been optimised, service
contains the optimised service,exceptions
contains allcalendar_dates.txt
rows applying to the optimised service.
- the service's
- If
changed
isfalse
,- the service cannot be optimised,
service
contains thecalendar.txt
as it was before, or a mock service if there was none before,exceptions
contains thecalendar_dates.txt
rows 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