Add buildCountryCityRows helper for regional picker grid
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { buildCountryCityRows } from "./buildCountryCityRows.js";
|
||||
import { transformDictionaries } from "@/shared/dictionaries/index.js";
|
||||
import type { IDictionaries, IRawDictionaries } from "@/shared/dictionaries/index.js";
|
||||
|
||||
function makeRaw(overrides: Partial<IRawDictionaries> = {}): IRawDictionaries {
|
||||
return { regions: [], countries: [], cities: [], airports: [], ...overrides };
|
||||
}
|
||||
|
||||
function dict(raw: Partial<IRawDictionaries>): IDictionaries {
|
||||
return transformDictionaries(makeRaw(raw), "ru");
|
||||
}
|
||||
|
||||
describe("buildCountryCityRows — empty and missing", () => {
|
||||
it("returns [] for missing region id", () => {
|
||||
const d = dict({
|
||||
regions: [{ world_region_id: 500374, title: { ru: "Россия и страны СНГ" } }],
|
||||
});
|
||||
expect(buildCountryCityRows(d, 99999)).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns [] when region has no countries", () => {
|
||||
const d = dict({
|
||||
regions: [{ world_region_id: 500374, title: { ru: "Россия и страны СНГ" } }],
|
||||
});
|
||||
expect(buildCountryCityRows(d, 500374)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildCountryCityRows — RU pinning", () => {
|
||||
it("pins RU country first within its region, others alphabetical", () => {
|
||||
const d = dict({
|
||||
regions: [{ world_region_id: 500374, title: { ru: "Россия и страны СНГ" } }],
|
||||
countries: [
|
||||
{ code: "BY", title: { ru: "Беларусь" }, world_region_id: 500374 },
|
||||
{ code: "RU", title: { ru: "Россия" }, world_region_id: 500374 },
|
||||
{ code: "AM", title: { ru: "Армения" }, world_region_id: 500374 },
|
||||
],
|
||||
cities: [
|
||||
{ code: "BYN", title: { ru: "Минск" }, country_code: "BY", has_afl_flights: true, location: { lat: 53, lon: 27 } },
|
||||
{ code: "YER", title: { ru: "Ереван" }, country_code: "AM", has_afl_flights: true, location: { lat: 40, lon: 44 } },
|
||||
{ code: "LED", title: { ru: "Санкт-Петербург" }, country_code: "RU", has_afl_flights: true, location: { lat: 60, lon: 30 } },
|
||||
],
|
||||
airports: [
|
||||
{ code: "MSQ", city_code: "BYN", title: { ru: "Минск аэропорт" }, has_afl_flights: true, location: { lat: 53, lon: 27 } },
|
||||
{ code: "EVN", city_code: "YER", title: { ru: "Звартноц" }, has_afl_flights: true, location: { lat: 40, lon: 44 } },
|
||||
{ code: "LED", city_code: "LED", title: { ru: "Пулково" }, has_afl_flights: true, location: { lat: 60, lon: 30 } },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = buildCountryCityRows(d, 500374);
|
||||
const countryOrder = rows
|
||||
.map((r) => r.countryName)
|
||||
.filter((n): n is string => Boolean(n));
|
||||
expect(countryOrder[0]).toBe("Россия");
|
||||
expect(countryOrder).toEqual(["Россия", "Армения", "Беларусь"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildCountryCityRows — MOW pinning inside RU", () => {
|
||||
it("pins MOW first among RU cities", () => {
|
||||
const d = dict({
|
||||
regions: [{ world_region_id: 500374, title: { ru: "Россия" } }],
|
||||
countries: [{ code: "RU", title: { ru: "Россия" }, world_region_id: 500374 }],
|
||||
cities: [
|
||||
{ code: "KZN", title: { ru: "Казань" }, country_code: "RU", has_afl_flights: true, location: { lat: 55, lon: 49 } },
|
||||
{ code: "LED", title: { ru: "Санкт-Петербург" }, country_code: "RU", has_afl_flights: true, location: { lat: 60, lon: 30 } },
|
||||
{ code: "MOW", title: { ru: "Москва" }, country_code: "RU", has_afl_flights: true, location: { lat: 55, lon: 37 } },
|
||||
],
|
||||
airports: [
|
||||
{ code: "KZN", city_code: "KZN", title: { ru: "Казань аэропорт" }, has_afl_flights: true, location: { lat: 55, lon: 49 } },
|
||||
{ code: "LED", city_code: "LED", title: { ru: "Пулково" }, has_afl_flights: true, location: { lat: 60, lon: 30 } },
|
||||
{ code: "SVO", city_code: "MOW", title: { ru: "Шереметьево" }, has_afl_flights: true, location: { lat: 55, lon: 37 } },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = buildCountryCityRows(d, 500374);
|
||||
expect(rows.length).toBeGreaterThan(0);
|
||||
expect(rows[0]!.city1!.code).toBe("MOW");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildCountryCityRows — pairing", () => {
|
||||
it("pairs two single-airport cities on the same row", () => {
|
||||
const d = dict({
|
||||
regions: [{ world_region_id: 500374, title: { ru: "Россия" } }],
|
||||
countries: [{ code: "RU", title: { ru: "Россия" }, world_region_id: 500374 }],
|
||||
cities: [
|
||||
{ code: "AAA", title: { ru: "Гор А" }, country_code: "RU", has_afl_flights: true, location: { lat: 1, lon: 1 } },
|
||||
{ code: "BBB", title: { ru: "Гор Б" }, country_code: "RU", has_afl_flights: true, location: { lat: 2, lon: 2 } },
|
||||
],
|
||||
airports: [
|
||||
{ code: "AAA", city_code: "AAA", title: { ru: "Аэропорт А" }, has_afl_flights: true, location: { lat: 1, lon: 1 } },
|
||||
{ code: "BBB", city_code: "BBB", title: { ru: "Аэропорт Б" }, has_afl_flights: true, location: { lat: 2, lon: 2 } },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = buildCountryCityRows(d, 500374);
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]!.city1!.code).toBe("AAA");
|
||||
expect(rows[0]!.city2!.code).toBe("BBB");
|
||||
expect(rows[0]!.city1Airports).toBeUndefined();
|
||||
});
|
||||
|
||||
it("puts a multi-airport city on its own row with city1Airports populated", () => {
|
||||
const d = dict({
|
||||
regions: [{ world_region_id: 500374, title: { ru: "Россия" } }],
|
||||
countries: [{ code: "RU", title: { ru: "Россия" }, world_region_id: 500374 }],
|
||||
cities: [
|
||||
{ code: "MOW", title: { ru: "Москва" }, country_code: "RU", has_afl_flights: true, location: { lat: 55, lon: 37 } },
|
||||
{ code: "ZZZ", title: { ru: "Город Я" }, country_code: "RU", has_afl_flights: true, location: { lat: 3, lon: 3 } },
|
||||
],
|
||||
airports: [
|
||||
{ code: "SVO", city_code: "MOW", title: { ru: "Шереметьево" }, has_afl_flights: true, location: { lat: 55, lon: 37 } },
|
||||
{ code: "DME", city_code: "MOW", title: { ru: "Домодедово" }, has_afl_flights: true, location: { lat: 55, lon: 37 } },
|
||||
{ code: "ZZZ", city_code: "ZZZ", title: { ru: "Аэропорт Я" }, has_afl_flights: true, location: { lat: 3, lon: 3 } },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = buildCountryCityRows(d, 500374);
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows[0]!.city1!.code).toBe("MOW");
|
||||
expect(rows[0]!.city2).toBeNull();
|
||||
expect(rows[0]!.city1Airports?.length).toBe(2);
|
||||
expect(rows[1]!.city1!.code).toBe("ZZZ");
|
||||
expect(rows[1]!.city2).toBeNull();
|
||||
});
|
||||
|
||||
it("handles odd-count cities by leaving city2 null on last row", () => {
|
||||
const d = dict({
|
||||
regions: [{ world_region_id: 500374, title: { ru: "Россия" } }],
|
||||
countries: [{ code: "RU", title: { ru: "Россия" }, world_region_id: 500374 }],
|
||||
cities: [
|
||||
{ code: "AAA", title: { ru: "Гор А" }, country_code: "RU", has_afl_flights: true, location: { lat: 1, lon: 1 } },
|
||||
{ code: "BBB", title: { ru: "Гор Б" }, country_code: "RU", has_afl_flights: true, location: { lat: 2, lon: 2 } },
|
||||
{ code: "CCC", title: { ru: "Гор В" }, country_code: "RU", has_afl_flights: true, location: { lat: 3, lon: 3 } },
|
||||
],
|
||||
airports: [
|
||||
{ code: "AAA", city_code: "AAA", title: { ru: "Аэропорт А" }, has_afl_flights: true, location: { lat: 1, lon: 1 } },
|
||||
{ code: "BBB", city_code: "BBB", title: { ru: "Аэропорт Б" }, has_afl_flights: true, location: { lat: 2, lon: 2 } },
|
||||
{ code: "CCC", city_code: "CCC", title: { ru: "Аэропорт В" }, has_afl_flights: true, location: { lat: 3, lon: 3 } },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = buildCountryCityRows(d, 500374);
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows[1]!.city2).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildCountryCityRows — countryName placement", () => {
|
||||
it("sets countryName only on the first row of each country block", () => {
|
||||
const d = dict({
|
||||
regions: [{ world_region_id: 500374, title: { ru: "Россия и страны СНГ" } }],
|
||||
countries: [
|
||||
{ code: "RU", title: { ru: "Россия" }, world_region_id: 500374 },
|
||||
{ code: "BY", title: { ru: "Беларусь" }, world_region_id: 500374 },
|
||||
],
|
||||
cities: [
|
||||
{ code: "AAA", title: { ru: "Гор А" }, country_code: "RU", has_afl_flights: true, location: { lat: 1, lon: 1 } },
|
||||
{ code: "BBB", title: { ru: "Гор Б" }, country_code: "RU", has_afl_flights: true, location: { lat: 2, lon: 2 } },
|
||||
{ code: "CCC", title: { ru: "Гор В" }, country_code: "RU", has_afl_flights: true, location: { lat: 3, lon: 3 } },
|
||||
{ code: "DDD", title: { ru: "Гор Д" }, country_code: "BY", has_afl_flights: true, location: { lat: 4, lon: 4 } },
|
||||
],
|
||||
airports: [
|
||||
{ code: "AAA", city_code: "AAA", title: { ru: "Аэропорт А" }, has_afl_flights: true, location: { lat: 1, lon: 1 } },
|
||||
{ code: "BBB", city_code: "BBB", title: { ru: "Аэропорт Б" }, has_afl_flights: true, location: { lat: 2, lon: 2 } },
|
||||
{ code: "CCC", city_code: "CCC", title: { ru: "Аэропорт В" }, has_afl_flights: true, location: { lat: 3, lon: 3 } },
|
||||
{ code: "DDD", city_code: "DDD", title: { ru: "Аэропорт Д" }, has_afl_flights: true, location: { lat: 4, lon: 4 } },
|
||||
],
|
||||
});
|
||||
|
||||
const rows = buildCountryCityRows(d, 500374);
|
||||
expect(rows[0]!.countryName).toBe("Россия");
|
||||
expect(rows[1]!.countryName).toBeUndefined();
|
||||
expect(rows[2]!.countryName).toBe("Беларусь");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Build rows for the regional-picker country/city grid.
|
||||
*
|
||||
* Matches Angular `DictionariesService.handleLoading` logic:
|
||||
* 1. Countries in the region: alphabetical by localized name, RU first.
|
||||
* 2. Cities within a country: alphabetical, excluding MOW initially;
|
||||
* MOW re-inserted at front for RU country.
|
||||
* 3. Row packing: city with >1 airport gets its own row (airports below);
|
||||
* others paired into city1 + city2 columns.
|
||||
* 4. `countryName` set only on the first row of each country's block.
|
||||
*/
|
||||
|
||||
import type {
|
||||
IAirport,
|
||||
ICity,
|
||||
IDictionaries,
|
||||
} from "@/shared/dictionaries/index.js";
|
||||
|
||||
export interface ICountryCityRow {
|
||||
countryName?: string;
|
||||
city1: ICity | null;
|
||||
city2: ICity | null;
|
||||
city1Airports?: IAirport[];
|
||||
}
|
||||
|
||||
export function buildCountryCityRows(
|
||||
dictionaries: IDictionaries,
|
||||
regionId: number,
|
||||
): ICountryCityRow[] {
|
||||
const region = dictionaries.regions.find((r) => r.id === regionId);
|
||||
if (!region || region.countries.length === 0) return [];
|
||||
|
||||
const countries = [...region.countries].sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
const ruIdx = countries.findIndex((c) => c.code === "RU");
|
||||
if (ruIdx > 0) {
|
||||
const [ru] = countries.splice(ruIdx, 1);
|
||||
if (ru) countries.unshift(ru);
|
||||
}
|
||||
|
||||
const rows: ICountryCityRow[] = [];
|
||||
|
||||
for (const country of countries) {
|
||||
const cities = dictionaries.cities
|
||||
.filter((c) => c.country_code === country.code && c.code !== "MOW")
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
if (country.code === "RU") {
|
||||
const mow = dictionaries.cities.find((c) => c.code === "MOW");
|
||||
if (mow) cities.unshift(mow);
|
||||
}
|
||||
|
||||
if (cities.length === 0) continue;
|
||||
|
||||
let firstRow = true;
|
||||
let i = 0;
|
||||
while (i < cities.length) {
|
||||
const city1 = cities[i]!;
|
||||
const multiAirport = city1.airports.length > 1;
|
||||
|
||||
if (multiAirport) {
|
||||
rows.push({
|
||||
...(firstRow ? { countryName: country.name } : {}),
|
||||
city1,
|
||||
city2: null,
|
||||
city1Airports: city1.airports,
|
||||
});
|
||||
i += 1;
|
||||
} else {
|
||||
const city2 = cities[i + 1] ?? null;
|
||||
rows.push({
|
||||
...(firstRow ? { countryName: country.name } : {}),
|
||||
city1,
|
||||
city2,
|
||||
});
|
||||
i += 2;
|
||||
}
|
||||
firstRow = false;
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
Reference in New Issue
Block a user