Skip to content

Latest commit

 

History

History
858 lines (675 loc) · 29.4 KB

api.md

File metadata and controls

858 lines (675 loc) · 29.4 KB

gtfs-utils API

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 = {})

  1. reads stops.txt into a store stop_id => stop,
  2. adds stop IDs of a station as station.stop,
  3. adds entrance IDs of a stop as stop.entrances,
  4. 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.

  • dateStr must be in the YYYYMMDD format, as specific in GTFS.
  • timezone must 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.

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.

  • readFile must be a function that, when called with a file name, returns a readable stream in objectMode.
  • filters must be an object; It may have the fields trip, stopTime & frequencies, each with a filter function.
  • opt must be an object; It may optionally have a custom computeSig 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.

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.

  • readFile must be a function that, when called with a file name, returns a readable stream in objectMode.
  • timezone must a timezone name from the tz database.
  • filters must be an object; It may have the fields trip, 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.

  • readFile must be a function that, when called with a file name, returns a readable stream in objectMode.
  • timezone must a timezone name from the tz database.
  • filters must be an object; It may have the fields service, 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 default
  • gtfs-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 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.

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.

  • readFile must be a function that, when called with a file name, returns a readable stream in objectMode.
  • filters must be an object; It may have the fields trip, 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:

  1. longitude
  2. latitude
  3. altitude
  4. arrival time
  5. 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 a exception_type=1 (added) exception – In many of such cases, we can reduce the number of exceptions by adding a row in calendar.txt with the respective day(s) turned on (e.g. tuesday=1).
  • feeds with calendar.txt, where some services have more exception_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 use exception_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 is true,
    • the service's calendar.txt row or calendar_dates.txt rows (or both) have been optimised,
    • service contains the optimised service,
    • exceptions contains all calendar_dates.txt rows applying to the optimised service.
  • If changed is false,
    • the service cannot be optimised,
    • service contains the calendar.txt as it was before, or a mock service if there was none before,
    • exceptions contains the calendar_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