Add filterRoutes pure helper with airport-code normalization
This commit is contained in:
@@ -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"]);
|
||||
});
|
||||
});
|
||||
@@ -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)));
|
||||
}
|
||||
Reference in New Issue
Block a user