Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions spec/bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ be handed it, so nothing is skipped for being inconvenient, and the count is vis
| `isValidCnpj` | regular expressions, check digits, JavaScript's own type coercion |
| `formatCnpj` | a shared mask helper, and three options that interact |
| `getAddressInfoByCep` | HTTP, JSON, retries, an error hierarchy, three providers raced |
| `getMunicipalities` | 5,571 baked rows, and a pt-BR order no two targets agree on |

## Adding a utility

Expand Down
54 changes: 54 additions & 0 deletions spec/bridge/conformance/cases/get-municipalities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* What `getMunicipalities` is replayed with: every state, and the keys that are not one.
*
* The whole list comes back per key rather than a sample, so one municipality out of order in
* one of the seven targets fails the check. That is the point of this utility being here: the
* order is pt-BR collation, and no two targets ship the same collator.
*/
import { type Recorder } from "../cases.ts";

/**
* The keys to ask for. Omitting the state code is the only way to ask for the combined list;
* every other value, an unknown state and an inherited `Object` property name included, has an
* answer of its own.
*/
const KEYS = [
"AC",
"AL",
"AP",
"AM",
"BA",
"CE",
"DF",
"ES",
"GO",
"MA",
"MT",
"MS",
"MG",
"PA",
"PB",
"PR",
"PE",
"PI",
"RJ",
"RN",
"RS",
"RO",
"RR",
"SC",
"SP",
"SE",
"TO",
"ZZ",
"sp",
"",
"toString",
"constructor",
"hasOwnProperty",
];

export const recorder: Recorder = {
module: "get-municipalities",
inputs: () => [[], ...KEYS.map((key) => [key])],
};
64 changes: 64 additions & 0 deletions spec/bridge/data/get-municipalities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* The municipalities table, with the pt-BR order the JavaScript package returns baked in.
*
* Two orders are resolved here: the per state one, which is the order the package already
* returns for a state, and the combined one, which is every state's rows merged and sorted by
* name with the pt-BR collator. Neither is recomputed at run time, because no two targets
* would agree on it.
*/
import { type DataDecl } from "../compiler/ir.ts";
import { type Builder } from "./_builder.ts";

type Municipality = { code: string; name: string; stateCode: string };

export const builder: Builder = {
module: "get-municipalities",

build: (shipped): Omit<DataDecl, "name"> => {
const getStates = shipped["getStates"] as () => { code: string }[];
const getMunicipalities = shipped["getMunicipalities"] as (
stateCode?: string,
) => Municipality[];

const rows: string[][] = [];
const groups: Record<string, number[]> = {};

// The per state order is the order the package itself returns, already the pt-BR one.
for (const state of getStates()) {
groups[state.code] = [];

for (const municipality of getMunicipalities(state.code)) {
groups[state.code].push(rows.length);
rows.push([municipality.stateCode, municipality.name, municipality.code]);
}
}

// Matching the combined list by (name, code) rather than by position keeps this honest
// even if the package ever changes how it merges the states.
const byKey = new Map<string, number>();

for (const [index, row] of rows.entries()) byKey.set(`${row[1]}\u0000${row[2]}`, index);

const fullOrder = getMunicipalities().map((municipality) => {
const index = byKey.get(`${municipality.name}\u0000${municipality.code}`);

if (index === undefined)
throw new Error(`the full list holds ${municipality.name} but no state does`);

return index;
});

if (fullOrder.length !== rows.length)
throw new Error(
`the full list has ${fullOrder.length} rows but the states have ${rows.length}`,
);

return {
doc: "Brazilian municipalities published by the IBGE, by state, in the pt-BR collation order the JavaScript package returns.",
columns: ["stateCode", "name", "code"],
rows,
groups,
fullOrder,
};
},
};
1 change: 1 addition & 0 deletions spec/bridge/source/get-municipalities.data.json

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions spec/bridge/source/get-municipalities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Brazilian municipalities, written once.
*/
import { type Dataset, dataAll, dataRows, dataset } from "./_std.ts";

/**
* The two letter code of a Brazilian state. Checked against the dataset by `data/build.ts`, so
* the union and the table can never drift apart.
*/
export type StateCode =
| "AC"
| "AL"
| "AP"
| "AM"
| "BA"
| "CE"
| "DF"
| "ES"
| "GO"
| "MA"
| "MT"
| "MS"
| "MG"
| "PA"
| "PB"
| "PR"
| "PE"
| "PI"
| "RJ"
| "RN"
| "RS"
| "RO"
| "RR"
| "SC"
| "SP"
| "SE"
| "TO";

/** One Brazilian municipality, as the IBGE publishes it. */
export type Municipality = {
/** The 7-digit IBGE municipality code. */
code: string;
/** The municipality name. */
name: string;
/** The two-letter code of the state the municipality belongs to. */
stateCode: StateCode;
};

/**
* The municipalities table: `[stateCode, name, code]` per row, grouped by state, with the
* combined pt-BR order baked in at build time.
*/
const MUNICIPALITIES: Dataset = dataset("get-municipalities");

/**
* Reads one dataset row as a municipality.
*
* @param {string[]} row - The `[stateCode, name, code]` row.
* @returns {Municipality} The municipality.
*/
const rowToMunicipality = (row: string[]): Municipality => ({
code: row[2],
name: row[1],
// The dataset holds plain strings; `data/build.ts` is what keeps the closed set true.
stateCode: row[0] as StateCode,
});

/**
* Returns Brazilian municipalities published by the IBGE, optionally filtered by state.
*
* If `stateCode` is provided, only municipalities of that state are returned. If it is
* omitted, every municipality of every state is returned, sorted with `localeCompare` in the
* "pt-BR" locale so accented names land where a Brazilian reader expects them. Every per-state
* list is sorted the same way.
*
* Only an omitted (or `undefined`) `stateCode` asks for the full list: any other value that is
* not a known state code, `null` and `""` included, returns `[]`.
*
* The state code is matched exactly, case included: `getMunicipalities("sp")` returns `[]` where
* `getMunicipalities("SP")` returns the 645 São Paulo municipalities.
*
* @param {StateCode} [stateCode] - The two letter code of the Brazilian state to filter by.
* @returns {Municipality[]} A fresh array of fresh `Municipality` objects. Empty when
* `stateCode` is not a known state.
*
* @example
* ```typescript
* getMunicipalities("SP")[0]; // { code: "3500105", name: "Adamantina", stateCode: "SP" }
* getMunicipalities().length; // every municipality of every state
* getMunicipalities("ZZ"); // []
* getMunicipalities("sp"); // [] (the state code is case-sensitive here)
* getMunicipalities(null); // [] (only an omitted state code asks for the full list)
* ```
*
* @see Official: https://servicodados.ibge.gov.br/api/docs/localidades
*/
export const getMunicipalities = (stateCode?: StateCode): Municipality[] => {
const municipalities: Municipality[] = [];
let rows = dataAll(MUNICIPALITIES);

if (stateCode !== undefined) {
rows = dataRows(MUNICIPALITIES, stateCode);
}

for (const row of rows) {
municipalities.push(rowToMunicipality(row));
}

return municipalities;
};
Loading