diff --git a/src/features/schedule/types.ts b/src/features/schedule/types.ts new file mode 100644 index 00000000..ded1db75 --- /dev/null +++ b/src/features/schedule/types.ts @@ -0,0 +1,101 @@ +/** + * Data model types for the Schedule feature. + * + * Reuses flight types from online-board where possible. + * Schedule-specific types cover the different URL/API param shapes. + * + * @module + */ + +import type { IFlight, ISimpleFlight, IBoardResponse } from "../online-board/types.js"; + +// Re-export shared flight types used by schedule +export type { IFlight, ISimpleFlight, IBoardResponse }; + +// --------------------------------------------------------------------------- +// URL parameter types +// --------------------------------------------------------------------------- + +/** + * Parsed route direction params from a single URL segment. + * Format: {dep}-{arr}-{dateFrom}-{dateTo}[-{timeFromTo}][-C{connections}] + */ +export interface IScheduleRouteDirectionParams { + departure: string; + arrival: string; + dateFrom: string; + dateTo: string; + timeFrom?: string; + timeTo?: string; + connections?: number; +} + +/** + * Full parsed schedule route params (one-way or round-trip). + */ +export interface IScheduleRouteParams { + outbound: IScheduleRouteDirectionParams; + inbound?: IScheduleRouteDirectionParams; +} + +/** + * Parsed flight ID for schedule details (reuses online-board's IParsedFlightId shape). + */ +export interface IScheduleFlightId { + carrier: string; + flightNumber: string; + suffix?: string; + date: string; +} + +/** + * Parsed details params from the catch-all URL. + */ +export interface IScheduleDetailsParams { + flights: IScheduleFlightId[]; +} + +// --------------------------------------------------------------------------- +// API request/response types +// --------------------------------------------------------------------------- + +/** + * Parameters for POST schedule/1 search. + */ +export interface IScheduleSearchRequest { + departure: string; + arrival: string; + dateFrom: string; + dateTo: string; + timeFrom?: string; + timeTo?: string; + connections?: number; + attribute?: 1 | 2; +} + +/** + * Schedule search API response -- array of flights. + */ +export type IScheduleResponse = IFlight[]; + +/** + * Schedule details API response -- same as online board response. + */ +export type IScheduleDetailsResponse = IBoardResponse; + +/** + * Parameters for GET days/.../schedule/v1 calendar endpoint. + */ +export interface IScheduleCalendarParams { + date: string; + departure: string; + arrival: string; + connections: boolean; +} + +/** + * Calendar days API response. + */ +export interface IScheduleDaysResponse { + days: string; +} diff --git a/src/features/schedule/url.test.ts b/src/features/schedule/url.test.ts new file mode 100644 index 00000000..ca561852 --- /dev/null +++ b/src/features/schedule/url.test.ts @@ -0,0 +1,502 @@ +import { describe, it, expect } from "vitest"; +import { + parseScheduleRouteParams, + buildScheduleRouteParams, + parseScheduleUrl, + buildScheduleUrl, + type ScheduleParams, +} from "./url"; + +// --------------------------------------------------------------------------- +// parseScheduleRouteParams +// --------------------------------------------------------------------------- + +describe("parseScheduleRouteParams", () => { + it("parses route without time range or connections", () => { + expect(parseScheduleRouteParams("MOW-KUF-20220425-20220501")).toEqual({ + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + }); + }); + + it("parses route with time range", () => { + expect(parseScheduleRouteParams("MOW-KUF-20220425-20220501-06002200")).toEqual({ + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + timeFrom: "0600", + timeTo: "2200", + }); + }); + + it("parses route with connections only", () => { + expect(parseScheduleRouteParams("MOW-KUF-20220425-20220501-C1")).toEqual({ + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + connections: 1, + }); + }); + + it("parses route with connections=0", () => { + expect(parseScheduleRouteParams("MOW-KUF-20220425-20220501-C0")).toEqual({ + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + connections: 0, + }); + }); + + it("parses route with time range and connections", () => { + expect(parseScheduleRouteParams("MOW-KUF-20220425-20220501-06002200-C0")).toEqual({ + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + timeFrom: "0600", + timeTo: "2200", + connections: 0, + }); + }); + + it("returns null for empty string", () => { + expect(parseScheduleRouteParams("")).toBeNull(); + }); + + it("returns null for insufficient segments", () => { + expect(parseScheduleRouteParams("MOW-KUF-20220425")).toBeNull(); + }); + + it("returns null for invalid dateFrom", () => { + expect(parseScheduleRouteParams("MOW-KUF-2022-20220501")).toBeNull(); + }); + + it("returns null for invalid dateTo", () => { + expect(parseScheduleRouteParams("MOW-KUF-20220425-2022")).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// buildScheduleRouteParams +// --------------------------------------------------------------------------- + +describe("buildScheduleRouteParams", () => { + it("builds route without time range or connections", () => { + expect(buildScheduleRouteParams({ + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + })).toBe("MOW-KUF-20220425-20220501"); + }); + + it("builds route with time range", () => { + expect(buildScheduleRouteParams({ + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + timeFrom: "0600", + timeTo: "2200", + })).toBe("MOW-KUF-20220425-20220501-06002200"); + }); + + it("builds route with connections", () => { + expect(buildScheduleRouteParams({ + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + connections: 1, + })).toBe("MOW-KUF-20220425-20220501-C1"); + }); + + it("builds route with connections=0", () => { + expect(buildScheduleRouteParams({ + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + connections: 0, + })).toBe("MOW-KUF-20220425-20220501-C0"); + }); + + it("builds route with time range and connections", () => { + expect(buildScheduleRouteParams({ + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + timeFrom: "0600", + timeTo: "2200", + connections: 0, + })).toBe("MOW-KUF-20220425-20220501-06002200-C0"); + }); + + it("omits time range when only timeFrom is provided", () => { + expect(buildScheduleRouteParams({ + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + timeFrom: "0600", + })).toBe("MOW-KUF-20220425-20220501"); + }); +}); + +// --------------------------------------------------------------------------- +// parseScheduleUrl +// --------------------------------------------------------------------------- + +describe("parseScheduleUrl", () => { + it("parses start page (no trailing slash)", () => { + expect(parseScheduleUrl("/schedule")).toEqual({ type: "start" }); + }); + + it("parses start page (with trailing slash)", () => { + expect(parseScheduleUrl("/schedule/")).toEqual({ type: "start" }); + }); + + it("parses one-way route search", () => { + expect(parseScheduleUrl("/schedule/route/MOW-KUF-20220425-20220501")).toEqual({ + type: "route", + outbound: { + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + }, + }); + }); + + it("parses one-way route with time range", () => { + expect(parseScheduleUrl("/schedule/route/MOW-KUF-20220425-20220501-06002200")).toEqual({ + type: "route", + outbound: { + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + timeFrom: "0600", + timeTo: "2200", + }, + }); + }); + + it("parses one-way route with connections", () => { + expect(parseScheduleUrl("/schedule/route/MOW-KUF-20220425-20220501-C1")).toEqual({ + type: "route", + outbound: { + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + connections: 1, + }, + }); + }); + + it("parses round-trip route search", () => { + expect(parseScheduleUrl("/schedule/route/MOW-KUF-20220425-20220501/KUF-MOW-20220502-20220508")).toEqual({ + type: "roundtrip", + outbound: { + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + }, + inbound: { + departure: "KUF", + arrival: "MOW", + dateFrom: "20220502", + dateTo: "20220508", + }, + }); + }); + + it("parses round-trip with time ranges", () => { + expect(parseScheduleUrl("/schedule/route/MOW-KUF-20220425-20220501-06002200/KUF-MOW-20220502-20220508-06002200")).toEqual({ + type: "roundtrip", + outbound: { + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + timeFrom: "0600", + timeTo: "2200", + }, + inbound: { + departure: "KUF", + arrival: "MOW", + dateFrom: "20220502", + dateTo: "20220508", + timeFrom: "0600", + timeTo: "2200", + }, + }); + }); + + it("parses round-trip with connections", () => { + expect(parseScheduleUrl("/schedule/route/MOW-KUF-20220425-20220501-C0/KUF-MOW-20220502-20220508-C0")).toEqual({ + type: "roundtrip", + outbound: { + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + connections: 0, + }, + inbound: { + departure: "KUF", + arrival: "MOW", + dateFrom: "20220502", + dateTo: "20220508", + connections: 0, + }, + }); + }); + + it("parses single-flight details", () => { + expect(parseScheduleUrl("/schedule/SU0012-20220527")).toEqual({ + type: "details", + flights: [ + { carrier: "SU", flightNumber: "0012", date: "20220527" }, + ], + }); + }); + + it("parses multi-flight details (connecting)", () => { + expect(parseScheduleUrl("/schedule/SU0012-20220501/SU0013-20220507")).toEqual({ + type: "details", + flights: [ + { carrier: "SU", flightNumber: "0012", date: "20220501" }, + { carrier: "SU", flightNumber: "0013", date: "20220507" }, + ], + }); + }); + + it("parses details with airport codes interleaved", () => { + // Format: /{depCode}/{flight-date}/{arrCode}/{depCode2}/{flight2-date}/{arrCode2} + // Airport codes (3-letter) are skipped, flight segments are parsed + expect(parseScheduleUrl("/schedule/SVO/SU0012-20220501/LED/LED/SU0013-20220507/SVO")).toEqual({ + type: "details", + flights: [ + { carrier: "SU", flightNumber: "0012", date: "20220501" }, + { carrier: "SU", flightNumber: "0013", date: "20220507" }, + ], + }); + }); + + it("returns null for empty string", () => { + expect(parseScheduleUrl("")).toBeNull(); + }); + + it("returns null for non-schedule path", () => { + expect(parseScheduleUrl("/some/other/path")).toBeNull(); + }); + + it("returns null for invalid route params", () => { + expect(parseScheduleUrl("/schedule/route/")).toBeNull(); + }); + + it("handles path without leading slash", () => { + expect(parseScheduleUrl("schedule")).toEqual({ type: "start" }); + }); + + it("handles path without leading slash for route", () => { + expect(parseScheduleUrl("schedule/route/MOW-KUF-20220425-20220501")).toEqual({ + type: "route", + outbound: { + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + }, + }); + }); +}); + +// --------------------------------------------------------------------------- +// buildScheduleUrl +// --------------------------------------------------------------------------- + +describe("buildScheduleUrl", () => { + it("builds start page URL", () => { + expect(buildScheduleUrl({ type: "start" })).toBe("schedule"); + }); + + it("builds one-way route URL", () => { + expect(buildScheduleUrl({ + type: "route", + outbound: { + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + }, + })).toBe("schedule/route/MOW-KUF-20220425-20220501"); + }); + + it("builds one-way route with time range and connections", () => { + expect(buildScheduleUrl({ + type: "route", + outbound: { + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + timeFrom: "0600", + timeTo: "2200", + connections: 0, + }, + })).toBe("schedule/route/MOW-KUF-20220425-20220501-06002200-C0"); + }); + + it("builds round-trip route URL", () => { + expect(buildScheduleUrl({ + type: "roundtrip", + outbound: { + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + }, + inbound: { + departure: "KUF", + arrival: "MOW", + dateFrom: "20220502", + dateTo: "20220508", + }, + })).toBe("schedule/route/MOW-KUF-20220425-20220501/KUF-MOW-20220502-20220508"); + }); + + it("builds round-trip with time ranges and connections", () => { + expect(buildScheduleUrl({ + type: "roundtrip", + outbound: { + departure: "MOW", + arrival: "KUF", + dateFrom: "20220425", + dateTo: "20220501", + timeFrom: "0600", + timeTo: "2200", + connections: 0, + }, + inbound: { + departure: "KUF", + arrival: "MOW", + dateFrom: "20220502", + dateTo: "20220508", + timeFrom: "0600", + timeTo: "2200", + connections: 0, + }, + })).toBe("schedule/route/MOW-KUF-20220425-20220501-06002200-C0/KUF-MOW-20220502-20220508-06002200-C0"); + }); + + it("builds single-flight details URL", () => { + expect(buildScheduleUrl({ + type: "details", + flights: [ + { carrier: "SU", flightNumber: "0012", date: "20220527" }, + ], + })).toBe("schedule/SU0012-20220527"); + }); + + it("builds multi-flight details URL", () => { + expect(buildScheduleUrl({ + type: "details", + flights: [ + { carrier: "SU", flightNumber: "0012", date: "20220501" }, + { carrier: "SU", flightNumber: "0013", date: "20220507" }, + ], + })).toBe("schedule/SU0012-20220501/SU0013-20220507"); + }); +}); + +// --------------------------------------------------------------------------- +// Roundtrip tests: build -> parse -> build +// --------------------------------------------------------------------------- + +describe("roundtrip: build -> parse -> build", () => { + const cases: Array<{ name: string; params: ScheduleParams }> = [ + { name: "start", params: { type: "start" } }, + { + name: "one-way route without extras", + params: { + type: "route", + outbound: { departure: "MOW", arrival: "KUF", dateFrom: "20220425", dateTo: "20220501" }, + }, + }, + { + name: "one-way route with time range", + params: { + type: "route", + outbound: { departure: "MOW", arrival: "KUF", dateFrom: "20220425", dateTo: "20220501", timeFrom: "0600", timeTo: "2200" }, + }, + }, + { + name: "one-way route with connections", + params: { + type: "route", + outbound: { departure: "MOW", arrival: "KUF", dateFrom: "20220425", dateTo: "20220501", connections: 1 }, + }, + }, + { + name: "one-way route with time range and connections", + params: { + type: "route", + outbound: { departure: "MOW", arrival: "KUF", dateFrom: "20220425", dateTo: "20220501", timeFrom: "0600", timeTo: "2200", connections: 0 }, + }, + }, + { + name: "round-trip without extras", + params: { + type: "roundtrip", + outbound: { departure: "MOW", arrival: "KUF", dateFrom: "20220425", dateTo: "20220501" }, + inbound: { departure: "KUF", arrival: "MOW", dateFrom: "20220502", dateTo: "20220508" }, + }, + }, + { + name: "round-trip with time range and connections", + params: { + type: "roundtrip", + outbound: { departure: "MOW", arrival: "KUF", dateFrom: "20220425", dateTo: "20220501", timeFrom: "0600", timeTo: "2200", connections: 0 }, + inbound: { departure: "KUF", arrival: "MOW", dateFrom: "20220502", dateTo: "20220508", timeFrom: "0600", timeTo: "2200", connections: 0 }, + }, + }, + { + name: "single-flight details", + params: { + type: "details", + flights: [{ carrier: "SU", flightNumber: "0012", date: "20220527" }], + }, + }, + { + name: "multi-flight details", + params: { + type: "details", + flights: [ + { carrier: "SU", flightNumber: "0012", date: "20220501" }, + { carrier: "SU", flightNumber: "0013", date: "20220507" }, + ], + }, + }, + ]; + + for (const { name, params } of cases) { + it(`roundtrips ${name}`, () => { + const url = buildScheduleUrl(params); + const parsed = parseScheduleUrl(url); + expect(parsed).not.toBeNull(); + if (parsed === null) return; + const rebuilt = buildScheduleUrl(parsed); + expect(rebuilt).toBe(url); + }); + } +}); diff --git a/src/features/schedule/url.ts b/src/features/schedule/url.ts new file mode 100644 index 00000000..eb9b6e3f --- /dev/null +++ b/src/features/schedule/url.ts @@ -0,0 +1,288 @@ +/** + * Schedule URL serializer/parser. + * + * Pure functions -- no side effects, no Angular imports, no Date objects. + * Byte-exact parity with Angular's ScheduleUrlBuilderService / + * ScheduleUrlParserService. + * + * @module + */ + +import { parseFlightUrlParams, buildFlightUrlParams } from "../online-board/url.js"; +import type { IScheduleRouteDirectionParams, IScheduleFlightId } from "./types.js"; + +// --------------------------------------------------------------------------- +// Public types +// --------------------------------------------------------------------------- + +/** Discriminated union of all parsed schedule URL parameter shapes */ +export type ScheduleParams = + | { type: "start" } + | { type: "route"; outbound: IScheduleRouteDirectionParams } + | { type: "roundtrip"; outbound: IScheduleRouteDirectionParams; inbound: IScheduleRouteDirectionParams } + | { type: "details"; flights: IScheduleFlightId[] }; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const SCHEDULE_PREFIX = "schedule"; + +// --------------------------------------------------------------------------- +// Validation helpers +// --------------------------------------------------------------------------- + +function isValidDate(date: string): boolean { + return /^\d{8}$/.test(date); +} + +function isValidTimeRange(timeRange: string): boolean { + return /^\d{8}$/.test(timeRange); +} + +// --------------------------------------------------------------------------- +// Route direction params parser/builder +// --------------------------------------------------------------------------- + +/** + * Parse a schedule route direction params string. + * Format: {dep}-{arr}-{dateFrom}-{dateTo}[-{timeFromTo}][-C{connections}] + * + * Both timeRange and connections are optional. Connections starts with 'C'. + */ +export function parseScheduleRouteParams(raw: string): IScheduleRouteDirectionParams | null { + if (!raw) return null; + + const parts = raw.split("-"); + if (parts.length < 4) return null; + + const departure = parts[0]; + const arrival = parts[1]; + const dateFrom = parts[2]; + const dateTo = parts[3]; + + if (departure === undefined || arrival === undefined || dateFrom === undefined || dateTo === undefined) return null; + if (!isValidDate(dateFrom) || !isValidDate(dateTo)) return null; + + // Remaining parts are optional: timeRange, connections (prefixed with C) + const rest = parts.slice(4); + const { timeFrom, timeTo, connections } = parseOptionalRouteParams(rest); + + const result: IScheduleRouteDirectionParams = { + departure, + arrival, + dateFrom, + dateTo, + }; + + if (timeFrom !== undefined && timeTo !== undefined) { + result.timeFrom = timeFrom; + result.timeTo = timeTo; + } + + if (connections !== undefined) { + result.connections = connections; + } + + return result; +} + +function parseOptionalRouteParams(parts: string[]): { + timeFrom?: string; + timeTo?: string; + connections?: number; +} { + if (parts.length === 0) return {}; + + if (parts.length === 2) { + // Both timeRange and connections + const timeRange = parts[0]; + const connStr = parts[1]; + if (timeRange !== undefined && connStr !== undefined) { + const time = parseTimeRange(timeRange); + const conn = parseConnections(connStr); + return { + ...time, + ...(conn !== undefined ? { connections: conn } : {}), + }; + } + return {}; + } + + // Single part: either connections (starts with C) or timeRange + const part = parts[0]; + if (part === undefined) return {}; + + if (part.startsWith("C") || part.startsWith("\u0421")) { + // C (Latin) or С (Cyrillic) -- Angular spec uses Cyrillic С in one test case + const conn = parseConnections(part); + return conn !== undefined ? { connections: conn } : {}; + } + + const time = parseTimeRange(part); + return time; +} + +function parseTimeRange(raw: string): { timeFrom: string; timeTo: string } | Record { + if (!raw || !isValidTimeRange(raw)) return {}; + return { + timeFrom: raw.slice(0, 4), + timeTo: raw.slice(4, 8), + }; +} + +function parseConnections(raw: string): number | undefined { + if (!raw || raw.length < 2) return undefined; + const numStr = raw.slice(1); + const num = Number(numStr); + return Number.isNaN(num) ? undefined : num; +} + +/** + * Build a schedule route direction params string. + */ +export function buildScheduleRouteParams(params: IScheduleRouteDirectionParams): string { + let base = `${params.departure}-${params.arrival}-${params.dateFrom}-${params.dateTo}`; + + if (params.timeFrom && params.timeTo) { + base = `${base}-${params.timeFrom}${params.timeTo}`; + } + + if (params.connections !== undefined) { + base = `${base}-C${params.connections}`; + } + + return base; +} + +// --------------------------------------------------------------------------- +// Details flight segment parser +// --------------------------------------------------------------------------- + +/** + * Parse a catch-all flight details path into flight IDs. + * Handles two formats: + * 1. Simple: SU0012-20220501/SU0013-20220507 + * 2. With airports: SVO/SU0012-20220501/LED/LED/SU0013-20220507/SVO + * (3-letter airport codes are skipped) + */ +function parseFlightDetailsSegments(raw: string): IScheduleFlightId[] | null { + if (!raw) return null; + + const segments = raw.split("/").filter(Boolean); + if (segments.length === 0) return null; + + const flights: IScheduleFlightId[] = []; + + for (const segment of segments) { + // Skip 3-letter airport codes (no dash, <= 3 chars) + if (segment.length <= 3) continue; + + const parsed = parseFlightUrlParams(segment); + if (parsed) { + const flight: IScheduleFlightId = { + carrier: parsed.carrier, + flightNumber: parsed.flightNumber, + date: parsed.date, + }; + if (parsed.suffix !== undefined) { + flight.suffix = parsed.suffix; + } + flights.push(flight); + } + } + + return flights.length > 0 ? flights : null; +} + +// --------------------------------------------------------------------------- +// Top-level parse / build +// --------------------------------------------------------------------------- + +/** + * Parse a raw URL path into typed schedule params. + * Returns null if the path does not match any known schedule URL shape. + */ +export function parseScheduleUrl(path: string): ScheduleParams | null { + if (!path) return null; + + const normalized = path.startsWith("/") ? path.slice(1) : path; + + if (!normalized.startsWith(SCHEDULE_PREFIX)) return null; + + const rest = normalized.slice(SCHEDULE_PREFIX.length); + + // Start page + if (!rest || rest === "/") return { type: "start" }; + + if (!rest.startsWith("/")) return null; + + const afterPrefix = rest.slice(1); + + // Route search: route/{params}[/{returnParams}] + if (afterPrefix.startsWith("route/")) { + const routeContent = afterPrefix.slice("route/".length); + if (!routeContent) return null; + + // Route params use "-" as separator, "/" separates outbound from inbound. + // Try round-trip first (split on "/"), then fall back to one-way. + const slashIdx = routeContent.indexOf("/"); + if (slashIdx >= 0) { + const outboundRaw = routeContent.slice(0, slashIdx); + const inboundRaw = routeContent.slice(slashIdx + 1); + + const parsedOutbound = parseScheduleRouteParams(outboundRaw); + const parsedInbound = parseScheduleRouteParams(inboundRaw); + + if (parsedOutbound && parsedInbound) { + return { type: "roundtrip", outbound: parsedOutbound, inbound: parsedInbound }; + } + } + + // Try one-way (entire routeContent is a single route param set) + const outbound = parseScheduleRouteParams(routeContent); + if (outbound) { + return { type: "route", outbound }; + } + + return null; + } + + // Route without prefix "route/": must be details (catch-all) + const flights = parseFlightDetailsSegments(afterPrefix); + if (flights) { + return { type: "details", flights }; + } + + return null; +} + +/** + * Build a URL path segment from typed schedule params. + * + * @returns the path segment without leading slash (e.g. "schedule/route/MOW-KUF-20220425-20220501") + */ +export function buildScheduleUrl(params: ScheduleParams): string { + switch (params.type) { + case "start": + return SCHEDULE_PREFIX; + + case "route": + return `${SCHEDULE_PREFIX}/route/${buildScheduleRouteParams(params.outbound)}`; + + case "roundtrip": + return `${SCHEDULE_PREFIX}/route/${buildScheduleRouteParams(params.outbound)}/${buildScheduleRouteParams(params.inbound)}`; + + case "details": { + const segments = params.flights.map((flight) => { + return buildFlightUrlParams({ + carrier: flight.carrier, + flightNumber: flight.flightNumber, + date: flight.date, + ...(flight.suffix !== undefined ? { suffix: flight.suffix } : {}), + }); + }); + return `${SCHEDULE_PREFIX}/${segments.join("/")}`; + } + } +}