plan/react-rewrite #1
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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<string, never> {
|
||||
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("/")}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user