Skip to content

Commit 18c7eac

Browse files
chtituxclaude
andcommitted
Add getCalendars, unfiltered getCalendarDates, getFeedInfo, getFrequencies
Closes #46. Four read-only accessors covering tables that were imported but unreachable through the query API: - getCalendars(filters?): bulk read of calendar (serviceId, limit) - getCalendarDates(filters?): now optional (serviceId, date, limit); the legacy getCalendarDates('SERVICE_ID') string form still works - getFeedInfo(): feed_info rows as an array (spec allows multiple rows) - getFrequencies(filters?): frequencies (tripId, limit); exact_times: 0 is preserved, not coerced to undefined Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNBx9fdd9ks7ppr6Vjd4mZ
1 parent d9dc937 commit 18c7eac

10 files changed

Lines changed: 385 additions & 22 deletions

File tree

.claude/skills/gtfs-sqljs/SKILL.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,22 @@ const geojson = await gtfs.getShapesToGeojson({ routeId: 'ROUTE_1' });
252252

253253
// Calendar
254254
const serviceIds = await gtfs.getActiveServiceIds('20240115'); // YYYYMMDD
255+
const calendars = await gtfs.getCalendars(); // whole calendar table
256+
const someCalendars = await gtfs.getCalendars({ serviceId: ['WEEKDAY', 'WEEKEND'] });
255257
const calendar = await gtfs.getCalendarByServiceId('WEEKDAY');
256-
const exceptions = await gtfs.getCalendarDates('WEEKDAY');
258+
const allExceptions = await gtfs.getCalendarDates(); // whole calendar_dates table
259+
const exceptions = await gtfs.getCalendarDates({ serviceId: 'WEEKDAY' });
257260
const exceptionsForDate = await gtfs.getCalendarDatesForDate('20240115');
258261

262+
// Feed info (array — the spec allows multiple rows, e.g. translations)
263+
const [feedInfo] = await gtfs.getFeedInfo();
264+
feedInfo?.feed_start_date; feedInfo?.feed_end_date; feedInfo?.feed_version;
265+
266+
// Frequencies — if a trip appears here, its stop_times are offsets from each
267+
// start_time, not absolute times (exact_times 0 or undefined = frequency-based)
268+
const frequencies = await gtfs.getFrequencies();
269+
const tripFrequencies = await gtfs.getFrequencies({ tripId: 'TRIP_1' });
270+
259271
// Build ordered stop list across multiple trip variants (express/local, etc.)
260272
const orderedStops = await gtfs.buildOrderedStopList(['TRIP_1', 'TRIP_2', 'TRIP_3']);
261273

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
## Upcoming release
44

55
- Fix: `ProgressInfo` and `ProgressCallback` types are now exported from the package entry point, as documented in the README (`import { type ProgressInfo } from 'gtfs-sqljs'` previously failed).
6+
- **Add `getCalendars(filters?)`** — bulk read of the `calendar` table (filters: `serviceId` single value or array, `limit`). `getCalendarByServiceId()` remains as a convenience wrapper. ([#46](https://github.com/sysdevrun/gtfs-sqljs/issues/46))
7+
- **`getCalendarDates()` no longer requires a service id** — it now accepts an optional filters object (`serviceId`, `date`, `limit`) and returns the whole `calendar_dates` table when called without arguments. The legacy `getCalendarDates('SERVICE_ID')` string form is still accepted. ([#46](https://github.com/sysdevrun/gtfs-sqljs/issues/46))
8+
- **Add `getFeedInfo()`** — returns the `feed_info` rows (an array, since the spec allows multiple rows). Useful for `feed_start_date`/`feed_end_date` bounds and `feed_version` display/cache keys. ([#46](https://github.com/sysdevrun/gtfs-sqljs/issues/46))
9+
- **Add `getFrequencies(filters?)`** — read the `frequencies` table (filters: `tripId` single value or array, `limit`), e.g. to detect frequency-based trips whose `stop_times` are offsets from `start_time`. `exact_times: 0` is preserved (not coerced to `undefined`). ([#46](https://github.com/sysdevrun/gtfs-sqljs/issues/46))
610

711
## 0.8.0
812

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,14 @@ All methods support flexible filtering with both single values and arrays:
169169
#### Calendar Methods
170170
- `getActiveServiceIds(date)` - Get active service IDs for a date (YYYYMMDD format)
171171
- `getCalendars(filters?)` - Get calendars (filters: serviceId, limit)
172-
- `getCalendarDates(serviceId)` - Get calendar date exceptions for a service
172+
- `getCalendarByServiceId(serviceId)` - Get a single calendar entry
173+
- `getCalendarDates(filters?)` - Get calendar date exceptions (filters: serviceId, date, limit; a plain serviceId string is still accepted)
173174
- `getCalendarDatesForDate(date)` - Get calendar exceptions for a specific date
174175

176+
#### Feed Info and Frequency Methods
177+
- `getFeedInfo()` - Get feed_info rows (array — the spec allows multiple rows)
178+
- `getFrequencies(filters?)` - Get headway-based service patterns (filters: tripId, limit). If a trip appears here, its stop_times are offsets from each `start_time` rather than absolute times.
179+
175180
#### GTFS Realtime Methods
176181
- `fetchRealtimeData(urls?)` - Fetch and load RT data from protobuf feeds
177182
- `clearRealtimeData()` - Clear all realtime data from database

src/gtfs-sqljs.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@ import { getStops, type StopFilters } from './queries/stops';
2222
import { getRoutes, type RouteFilters } from './queries/routes';
2323
import {
2424
getActiveServiceIds,
25+
getCalendars,
2526
getCalendarByServiceId,
2627
getCalendarDates,
2728
getCalendarDatesForDate,
29+
type CalendarFilters,
30+
type CalendarDateFilters,
2831
} from './queries/calendar';
32+
import { getFeedInfo } from './queries/feed-info';
33+
import { getFrequencies, type FrequencyFilters } from './queries/frequencies';
2934
import { getTrips, type TripFilters, type TripWithRealtime } from './queries/trips';
3035
import { getStopTimes, buildOrderedStopList, type StopTimeFilters, type StopTimeWithRealtime } from './queries/stop-times';
3136
import { getTripSchedules, type TripScheduleFilters, type TripSchedule, type TripScheduleStop } from './queries/trip-schedules';
@@ -37,11 +42,11 @@ import { getTripUpdates, getAllTripUpdates, type TripUpdateFilters } from './que
3742
import { getStopTimeUpdates, getAllStopTimeUpdates, type StopTimeUpdateFilters } from './queries/rt-stop-time-updates';
3843

3944
// Types
40-
import type { Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, Shape } from './types/gtfs';
45+
import type { Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate, Shape, FeedInfo, Frequency } from './types/gtfs';
4146
import type { Alert, VehiclePosition, TripUpdate, StopTimeUpdate } from './types/gtfs-rt';
4247

4348
// Export filter types for users
44-
export type { AgencyFilters, StopFilters, RouteFilters, TripFilters, StopTimeFilters, ShapeFilters, AlertFilters, VehiclePositionFilters, TripUpdateFilters, StopTimeUpdateFilters, TripScheduleFilters };
49+
export type { AgencyFilters, StopFilters, RouteFilters, TripFilters, StopTimeFilters, ShapeFilters, CalendarFilters, CalendarDateFilters, FrequencyFilters, AlertFilters, VehiclePositionFilters, TripUpdateFilters, StopTimeUpdateFilters, TripScheduleFilters };
4550
// Export RT types
4651
export type { Alert, VehiclePosition, TripUpdate, TripWithRealtime, StopTimeWithRealtime, TripSchedule, TripScheduleStop };
4752
// Export GeoJSON types
@@ -677,6 +682,15 @@ export class GtfsSqlJs {
677682
return getActiveServiceIds(this.db, date);
678683
}
679684

685+
/**
686+
* Get calendar entries with optional filters
687+
* Pass serviceId filter to get specific calendars
688+
*/
689+
async getCalendars(filters?: CalendarFilters): Promise<Calendar[]> {
690+
if (!this.db) throw new Error('Database not initialized');
691+
return getCalendars(this.db, filters);
692+
}
693+
680694
/**
681695
* Get calendar entry by service_id
682696
*/
@@ -686,11 +700,18 @@ export class GtfsSqlJs {
686700
}
687701

688702
/**
689-
* Get calendar date exceptions for a service
703+
* Get calendar date exceptions with optional filters
704+
* Accepts a filters object; a plain service_id string is still supported
705+
* for backward compatibility.
690706
*/
691-
async getCalendarDates(serviceId: string): Promise<CalendarDate[]> {
707+
async getCalendarDates(filters?: CalendarDateFilters): Promise<CalendarDate[]>;
708+
async getCalendarDates(serviceId: string): Promise<CalendarDate[]>;
709+
async getCalendarDates(filtersOrServiceId?: CalendarDateFilters | string): Promise<CalendarDate[]> {
692710
if (!this.db) throw new Error('Database not initialized');
693-
return getCalendarDates(this.db, serviceId);
711+
const filters = typeof filtersOrServiceId === 'string'
712+
? { serviceId: filtersOrServiceId }
713+
: filtersOrServiceId;
714+
return getCalendarDates(this.db, filters);
694715
}
695716

696717
/**
@@ -701,6 +722,27 @@ export class GtfsSqlJs {
701722
return getCalendarDatesForDate(this.db, date);
702723
}
703724

725+
// ==================== Feed Info Methods ====================
726+
727+
/**
728+
* Get feed_info rows (the GTFS spec allows multiple rows, e.g. translations)
729+
*/
730+
async getFeedInfo(): Promise<FeedInfo[]> {
731+
if (!this.db) throw new Error('Database not initialized');
732+
return getFeedInfo(this.db);
733+
}
734+
735+
// ==================== Frequency Methods ====================
736+
737+
/**
738+
* Get frequencies with optional filters
739+
* Pass tripId filter to get frequencies for specific trips
740+
*/
741+
async getFrequencies(filters?: FrequencyFilters): Promise<Frequency[]> {
742+
if (!this.db) throw new Error('Database not initialized');
743+
return getFrequencies(this.db, filters);
744+
}
745+
704746
// ==================== Trip Methods ====================
705747

706748
/**

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export {
1616
type TripFilters,
1717
type StopTimeFilters,
1818
type ShapeFilters,
19+
type CalendarFilters,
20+
type CalendarDateFilters,
21+
type FrequencyFilters,
1922
type AlertFilters,
2023
type VehiclePositionFilters,
2124
type TripUpdateFilters,

src/queries/calendar.ts

Lines changed: 92 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@
55
import type { GtfsDatabase, Row } from '../adapters/types';
66
import type { Calendar, CalendarDate } from '../types/gtfs';
77

8+
export interface CalendarFilters {
9+
serviceId?: string | string[];
10+
limit?: number;
11+
}
12+
13+
export interface CalendarDateFilters {
14+
serviceId?: string | string[];
15+
date?: string;
16+
limit?: number;
17+
}
18+
819
/**
920
* Get active service IDs for a given date
1021
*/
@@ -56,6 +67,50 @@ export async function getActiveServiceIds(db: GtfsDatabase, date: string): Promi
5667
return Array.from(serviceIds);
5768
}
5869

70+
/**
71+
* Get calendar entries with optional filters
72+
* - Filters support both single values and arrays
73+
*/
74+
export async function getCalendars(db: GtfsDatabase, filters: CalendarFilters = {}): Promise<Calendar[]> {
75+
const { serviceId, limit } = filters;
76+
77+
const conditions: string[] = [];
78+
const params: (string | number)[] = [];
79+
80+
if (serviceId) {
81+
const serviceIds = Array.isArray(serviceId) ? serviceId : [serviceId];
82+
if (serviceIds.length > 0) {
83+
const placeholders = serviceIds.map(() => '?').join(', ');
84+
conditions.push(`service_id IN (${placeholders})`);
85+
params.push(...serviceIds);
86+
}
87+
}
88+
89+
let sql = 'SELECT * FROM calendar';
90+
if (conditions.length > 0) {
91+
sql += ' WHERE ' + conditions.join(' AND ');
92+
}
93+
sql += ' ORDER BY service_id';
94+
if (limit) {
95+
sql += ' LIMIT ?';
96+
params.push(limit);
97+
}
98+
99+
const stmt = await db.prepare(sql);
100+
if (params.length > 0) {
101+
await stmt.bind(params);
102+
}
103+
104+
const calendars: Calendar[] = [];
105+
while (await stmt.step()) {
106+
const row = await stmt.getAsObject();
107+
calendars.push(rowToCalendar(row));
108+
}
109+
110+
await stmt.free();
111+
return calendars;
112+
}
113+
59114
/**
60115
* Get calendar entry by service_id
61116
*/
@@ -74,11 +129,43 @@ export async function getCalendarByServiceId(db: GtfsDatabase, serviceId: string
74129
}
75130

76131
/**
77-
* Get calendar date exceptions for a service
132+
* Get calendar date exceptions with optional filters
133+
* - Filters support both single values and arrays
78134
*/
79-
export async function getCalendarDates(db: GtfsDatabase, serviceId: string): Promise<CalendarDate[]> {
80-
const stmt = await db.prepare('SELECT * FROM calendar_dates WHERE service_id = ? ORDER BY date');
81-
await stmt.bind([serviceId]);
135+
export async function getCalendarDates(db: GtfsDatabase, filters: CalendarDateFilters = {}): Promise<CalendarDate[]> {
136+
const { serviceId, date, limit } = filters;
137+
138+
const conditions: string[] = [];
139+
const params: (string | number)[] = [];
140+
141+
if (serviceId) {
142+
const serviceIds = Array.isArray(serviceId) ? serviceId : [serviceId];
143+
if (serviceIds.length > 0) {
144+
const placeholders = serviceIds.map(() => '?').join(', ');
145+
conditions.push(`service_id IN (${placeholders})`);
146+
params.push(...serviceIds);
147+
}
148+
}
149+
150+
if (date) {
151+
conditions.push('date = ?');
152+
params.push(date);
153+
}
154+
155+
let sql = 'SELECT * FROM calendar_dates';
156+
if (conditions.length > 0) {
157+
sql += ' WHERE ' + conditions.join(' AND ');
158+
}
159+
sql += ' ORDER BY service_id, date';
160+
if (limit) {
161+
sql += ' LIMIT ?';
162+
params.push(limit);
163+
}
164+
165+
const stmt = await db.prepare(sql);
166+
if (params.length > 0) {
167+
await stmt.bind(params);
168+
}
82169

83170
const dates: CalendarDate[] = [];
84171
while (await stmt.step()) {
@@ -94,17 +181,7 @@ export async function getCalendarDates(db: GtfsDatabase, serviceId: string): Pro
94181
* Get calendar date exceptions for a specific date
95182
*/
96183
export async function getCalendarDatesForDate(db: GtfsDatabase, date: string): Promise<CalendarDate[]> {
97-
const stmt = await db.prepare('SELECT * FROM calendar_dates WHERE date = ?');
98-
await stmt.bind([date]);
99-
100-
const dates: CalendarDate[] = [];
101-
while (await stmt.step()) {
102-
const row = await stmt.getAsObject();
103-
dates.push(rowToCalendarDate(row));
104-
}
105-
106-
await stmt.free();
107-
return dates;
184+
return getCalendarDates(db, { date });
108185
}
109186

110187
/**

src/queries/feed-info.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Feed Info Query Methods
3+
*/
4+
5+
import type { GtfsDatabase, Row } from '../adapters/types';
6+
import type { FeedInfo } from '../types/gtfs';
7+
8+
/**
9+
* Get feed_info rows
10+
* - The GTFS spec allows multiple rows (e.g. translations), so an array is returned
11+
*/
12+
export async function getFeedInfo(db: GtfsDatabase): Promise<FeedInfo[]> {
13+
const stmt = await db.prepare('SELECT * FROM feed_info');
14+
15+
const feedInfos: FeedInfo[] = [];
16+
while (await stmt.step()) {
17+
const row = await stmt.getAsObject();
18+
feedInfos.push(rowToFeedInfo(row));
19+
}
20+
21+
await stmt.free();
22+
return feedInfos;
23+
}
24+
25+
/**
26+
* Convert database row to FeedInfo object
27+
*/
28+
function rowToFeedInfo(row: Row): FeedInfo {
29+
return {
30+
feed_publisher_name: String(row.feed_publisher_name),
31+
feed_publisher_url: String(row.feed_publisher_url),
32+
feed_lang: String(row.feed_lang),
33+
default_lang: row.default_lang ? String(row.default_lang) : undefined,
34+
feed_start_date: row.feed_start_date ? String(row.feed_start_date) : undefined,
35+
feed_end_date: row.feed_end_date ? String(row.feed_end_date) : undefined,
36+
feed_version: row.feed_version ? String(row.feed_version) : undefined,
37+
feed_contact_email: row.feed_contact_email ? String(row.feed_contact_email) : undefined,
38+
feed_contact_url: row.feed_contact_url ? String(row.feed_contact_url) : undefined,
39+
};
40+
}

0 commit comments

Comments
 (0)