Add filterRoutes pure helper with airport-code normalization

This commit is contained in:
2026-04-17 10:52:27 +03:00
parent 5225df0dd1
commit 40f170f87a
2 changed files with 187 additions and 0 deletions
@@ -0,0 +1,142 @@
import { describe, it, expect } from "vitest";
import { filterRoutes } from "./filterRoutes.js";
import { transformDictionaries } from "@/shared/dictionaries/index.js";
import type { IFlightRoute, IFlightsMapFilterState } from "./types.js";
import type { IDictionaries, IRawDictionaries } from "@/shared/dictionaries/index.js";
const raw: IRawDictionaries = {
regions: [],
countries: [
{ code: "RU", title: { ru: "Россия" }, world_region_id: 500374 },
{ code: "US", title: { ru: "США" }, world_region_id: 1 },
],
cities: [
{ code: "MOW", title: { ru: "Москва" }, country_code: "RU", has_afl_flights: true, location: { lat: 55, lon: 37 } },
{ code: "LED", title: { ru: "Санкт-Петербург" }, country_code: "RU", has_afl_flights: true, location: { lat: 60, lon: 30 } },
{ code: "AER", title: { ru: "Сочи" }, country_code: "RU", has_afl_flights: true, location: { lat: 43, lon: 39 } },
{ code: "NYC", title: { ru: "Нью-Йорк" }, country_code: "US", has_afl_flights: true, location: { lat: 40, lon: -74 } },
],
airports: [
{ code: "SVO", city_code: "MOW", title: { ru: "Шереметьево" }, has_afl_flights: true, location: { lat: 55, lon: 37 } },
{ code: "JFK", city_code: "NYC", title: { ru: "Джон Кеннеди" }, has_afl_flights: true, location: { lat: 40, lon: -74 } },
{ code: "LED", city_code: "LED", title: { ru: "Пулково" }, has_afl_flights: true, location: { lat: 60, lon: 30 } },
{ code: "AER", city_code: "AER", title: { ru: "Сочи" }, has_afl_flights: true, location: { lat: 43, lon: 39 } },
],
};
const d: IDictionaries = transformDictionaries(raw, "ru");
function filter(
overrides: Partial<IFlightsMapFilterState> = {},
): IFlightsMapFilterState {
return {
connections: false,
domestic: false,
international: false,
...overrides,
};
}
describe("filterRoutes — empty and passthrough", () => {
it("returns [] for no routes", () => {
expect(filterRoutes([], filter(), d)).toEqual([]);
});
it("returns routes unchanged when no toggles are active", () => {
const routes: IFlightRoute[] = [
{ route: ["MOW", "LED"], isDirect: true },
{ route: ["MOW", "NYC"], isDirect: true },
];
expect(filterRoutes(routes, filter(), d)).toEqual(routes);
});
});
describe("filterRoutes — domestic", () => {
it("keeps only routes where every city is RU", () => {
const routes: IFlightRoute[] = [
{ route: ["MOW", "LED"], isDirect: true },
{ route: ["MOW", "NYC"], isDirect: true },
];
const out = filterRoutes(routes, filter({ domestic: true }), d);
expect(out).toHaveLength(1);
expect(out[0]!.route).toEqual(["MOW", "LED"]);
});
});
describe("filterRoutes — international", () => {
it("keeps only routes with at least one non-RU city", () => {
const routes: IFlightRoute[] = [
{ route: ["MOW", "LED"], isDirect: true },
{ route: ["MOW", "NYC"], isDirect: true },
];
const out = filterRoutes(routes, filter({ international: true }), d);
expect(out).toHaveLength(1);
expect(out[0]!.route).toEqual(["MOW", "NYC"]);
});
});
describe("filterRoutes — both domestic AND international", () => {
it("applies no domestic/intl filter when both are true", () => {
const routes: IFlightRoute[] = [
{ route: ["MOW", "LED"], isDirect: true },
{ route: ["MOW", "NYC"], isDirect: true },
];
const out = filterRoutes(
routes,
filter({ domestic: true, international: true }),
d,
);
expect(out).toEqual(routes);
});
});
describe("filterRoutes — connections", () => {
it("keeps only non-direct routes", () => {
const routes: IFlightRoute[] = [
{ route: ["MOW", "LED"], isDirect: true },
{ route: ["MOW", "X", "LED"], isDirect: false },
];
const out = filterRoutes(routes, filter({ connections: true }), d);
expect(out).toHaveLength(1);
expect(out[0]!.route).toEqual(["MOW", "X", "LED"]);
});
});
describe("filterRoutes — airport-code normalization", () => {
it("treats airport codes (SVO, JFK) as their city codes (MOW, NYC)", () => {
const routes: IFlightRoute[] = [
{ route: ["SVO", "JFK"], isDirect: true },
{ route: ["SVO", "LED"], isDirect: true },
];
const intl = filterRoutes(routes, filter({ international: true }), d);
expect(intl).toHaveLength(1);
expect(intl[0]!.route).toEqual(["SVO", "JFK"]);
const dom = filterRoutes(routes, filter({ domestic: true }), d);
expect(dom).toHaveLength(1);
expect(dom[0]!.route).toEqual(["SVO", "LED"]);
});
it("drops routes containing unknown codes when any toggle is active", () => {
const routes: IFlightRoute[] = [{ route: ["MOW", "???"], isDirect: true }];
expect(filterRoutes(routes, filter({ domestic: true }), d)).toEqual([]);
expect(filterRoutes(routes, filter({ international: true }), d)).toEqual([]);
});
});
describe("filterRoutes — combo", () => {
it("applies domestic + connections together", () => {
const routes: IFlightRoute[] = [
{ route: ["MOW", "LED"], isDirect: true },
{ route: ["MOW", "AER", "LED"], isDirect: false },
{ route: ["MOW", "NYC"], isDirect: false },
];
const out = filterRoutes(
routes,
filter({ domestic: true, connections: true }),
d,
);
expect(out).toHaveLength(1);
expect(out[0]!.route).toEqual(["MOW", "AER", "LED"]);
});
});
+45
View File
@@ -0,0 +1,45 @@
/**
* Client-side route filter matching Angular `filterRoutes`.
*
* Rules:
* - domestic && !international → keep routes where every city is RU.
* - international && !domestic → keep routes with at least one non-RU city.
* - both domestic && international → no domestic/intl filter (show all).
* - connections → keep only routes where isDirect is false.
*
* City codes and airport codes both appear in route arrays; we normalize each
* entry to its city code before set-membership checks.
*/
import type { IDictionaries } from "@/shared/dictionaries/index.js";
import { getCityCodeByAirportCode } from "@/shared/dictionaries/index.js";
import type { IFlightRoute, IFlightsMapFilterState } from "./types.js";
export function filterRoutes(
routes: IFlightRoute[],
filter: Pick<IFlightsMapFilterState, "domestic" | "international" | "connections">,
dictionaries: IDictionaries,
): IFlightRoute[] {
const { domestic, international, connections } = filter;
const toCityCode = (code: string): string =>
getCityCodeByAirportCode(dictionaries, code) ?? code;
const isDomestic = (r: IFlightRoute): boolean =>
r.route.every((code) => dictionaries.ruCityCodes.has(toCityCode(code)));
const isInternational = (r: IFlightRoute): boolean =>
r.route.some((code) => dictionaries.otherCityCodes.has(toCityCode(code)));
const hasConnections = (r: IFlightRoute): boolean => !r.isDirect;
const predicates: Array<(r: IFlightRoute) => boolean> = [];
if (domestic && !international) predicates.push(isDomestic);
else if (international && !domestic) predicates.push(isInternational);
if (connections) predicates.push(hasConnections);
if (predicates.length === 0) return routes;
return routes.filter((r) => predicates.every((p) => p(r)));
}