diff --git a/data/CONTRIBUTING.md b/data/CONTRIBUTING.md index 74444fc..50e2e59 100644 --- a/data/CONTRIBUTING.md +++ b/data/CONTRIBUTING.md @@ -37,6 +37,7 @@ below is also machine-enforced by [`places.schema.json`](places.schema.json), wh **Fields:** - `id`: `--`. Prefix is a short slug for the city (e.g. `mum`, `thane`, `navi`, `ldn`, `jkt`). Increment from the highest existing number in the file for that prefix. - `city`: lowercase, underscore-separated slug (e.g. `mumbai`, `navi_mumbai`, `jakarta`). Any city worldwide is welcome, not just Mumbai/Thane/Navi Mumbai. +- `country`: optional for now. The place's ISO 3166-1 alpha-2 country code in capitals (e.g. `IN`, `GB`, `KE`), the same codes competitions use. Lowercase or a country name fails `npm run validate`. Existing records are being backfilled, after which it becomes required (#288). - `type`: must match the filename exactly - `lat`/`lng`: from Google Maps (right-click pin, "What's here?"). Any real-world coordinate works: `places.schema.json` checks latitude is between -90 and 90 and longitude between -180 and 180. - `address`: optional, short, human-readable diff --git a/data/places.schema.json b/data/places.schema.json index 288e32f..fed19d5 100644 --- a/data/places.schema.json +++ b/data/places.schema.json @@ -27,6 +27,11 @@ "pattern": "^[a-z0-9]+(_[a-z0-9]+)*$", "description": "Lowercase, underscore-separated slug, e.g. \"mumbai\", \"navi_mumbai\". Any city worldwide is welcome, not a fixed enum." }, + "country": { + "type": "string", + "pattern": "^[A-Z]{2}$", + "description": "Optional for now. ISO 3166-1 alpha-2 country code in capitals, e.g. \"IN\", \"GB\". Competitions already use the same codes." + }, "lat": { "type": "number", "minimum": -90, diff --git a/scripts/lib/data-validation.mjs b/scripts/lib/data-validation.mjs index 0674755..09fa679 100644 --- a/scripts/lib/data-validation.mjs +++ b/scripts/lib/data-validation.mjs @@ -69,3 +69,8 @@ export class Reporter { this.totalWarnings++; } } + +/** True when `value` is an ISO 3166-1 alpha-2 style country code: exactly two capital letters. */ +export function isCountryCode(value) { + return typeof value === "string" && /^[A-Z]{2}$/.test(value); +} diff --git a/scripts/validate-places.mjs b/scripts/validate-places.mjs index 59d1d61..8bfc632 100644 --- a/scripts/validate-places.mjs +++ b/scripts/validate-places.mjs @@ -11,6 +11,7 @@ * - type is one of the schema's enum values * - type matches the filename it lives in * - city is a lowercase, underscore-separated slug + * - country, when present, is a two-letter upper-case ISO 3166-1 code * - id matches the -- convention, or is a bare * numeric id (the College Board SAT centre code convention) * - lat/lng are numbers within the schema's min/max bounds @@ -29,6 +30,7 @@ import { fileURLToPath } from "url"; import { Reporter, hasEmDash, + isCountryCode, isLatLngPlaceholderGmapsLink, isRealIsoDate, } from "./lib/data-validation.mjs"; @@ -173,6 +175,14 @@ for (const file of files) { ); } + // country, when present, is an upper-case ISO 3166-1 alpha-2 code (see #288) + if (r.country !== undefined && !isCountryCode(r.country)) { + err( + loc, + `country must be a two-letter upper-case ISO 3166-1 alpha-2 code like "IN", got ${JSON.stringify(r.country)}`, + ); + } + // valid_till format, when present if (r.valid_till !== undefined && !isRealIsoDate(r.valid_till)) { err(loc, `valid_till must be a real ISO date (YYYY-MM-DD), got "${r.valid_till}"`); diff --git a/src/lib/country-code.test.ts b/src/lib/country-code.test.ts new file mode 100644 index 0000000..66faf14 --- /dev/null +++ b/src/lib/country-code.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; + +import { isCountryCode } from "../../scripts/lib/data-validation.mjs"; + +describe("isCountryCode", () => { + it("accepts two capital letters", () => { + for (const code of ["IN", "GB", "KE", "US"]) expect(isCountryCode(code)).toBe(true); + }); + + it("rejects names, lower case, wrong length and non-strings", () => { + for (const value of [ + "india", + "in", + "In", + "IND", + "I", + "", + "I1", + "I N", + undefined, + null, + 7, + ]) { + expect(isCountryCode(value), String(value)).toBe(false); + } + }); +}); diff --git a/src/lib/types.ts b/src/lib/types.ts index 9e7e694..7ea2969 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -280,6 +280,8 @@ export interface Place { name: string; type: PlaceType; city: City; + /** ISO 3166-1 alpha-2 country code in capitals (e.g. "IN"). Optional while records are backfilled (#288). */ + country?: string; lat: number; lng: number; address?: string;