Skip to content

Commit

Permalink
feat: add extractLocalisationFromAST
Browse files Browse the repository at this point in the history
  • Loading branch information
EmileRolley committed Jan 27, 2025
1 parent c01742b commit d5d8c42
Show file tree
Hide file tree
Showing 5 changed files with 430 additions and 88 deletions.
191 changes: 182 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import _communes from "@etalab/decoupage-administratif/data/communes.json";
import _departements from "@etalab/decoupage-administratif/data/departements.json";
import _regions from "@etalab/decoupage-administratif/data/regions.json";
import _epci from "@etalab/decoupage-administratif/data/epci.json";
import { Situation } from "../publicodes-build/";
import { RuleName, Situation } from "../publicodes-build/";
import { ASTNode, reduceAST } from "publicodes";

/**
* NOTE: this type has been inferred from the
Expand Down Expand Up @@ -52,15 +55,34 @@ export type EPCI = {
>;
};

export type Region = {
code: string;
chefLieu: string;
nom: string;
typeLiaison?: 0 | 1 | 2 | 3 | 4;
zone: "metro" | "drom" | "com";
};

export type Departement = {
code: string;
region: Region["code"];
chefLieu: string;
nom: string;
typeLiaison?: 0 | 1 | 2 | 3 | 4 | 5;
zone: "metro" | "dom" | "com";
};

const communes = _communes as Commune[];
const epci = _epci as EPCI[];
const departements = _departements as Departement[];
const regions = _regions as Region[];

/** Associate each commune INSEE code to its EPCI SIREN code.
*
* PERF: should we do this at build time?
*/
const epci_by_communes = Object.fromEntries(
epci.flatMap((epci) => epci.membres.map(({ code }) => [code, epci.code])),
epci.flatMap((epci) => epci.membres.map(({ code }) => [code, epci]))
);

/**
Expand All @@ -74,27 +96,178 @@ const epci_by_communes = Object.fromEntries(
* console.log(getSituationFromCodeINSEE("38185"));
* // Output:
* {
* "localisation . code insee": "'38185'", // Grenoble
* "localisation . code commune": "'38185'", // Grenoble
* "localisation . code epci": "'200040715'", // Grenoble-Alpes-Métropole
* "localisation . code département": "'38'", // Isère
* "localisation . code région": "'84'" // Auvergne-Rhône-Alpes
* }
* ```
*/
export function getSituationFromCodeINSEE(
code: string,
export function getFullSituationFromCommune(
code: string
): Omit<Situation, "localisation"> | undefined {
const commune = communes.find((c) => c.code === code);
if (commune) {
return {
"localisation . code insee": fmt(code),
"localisation . code epci": fmt(epci_by_communes[code]),
"localisation . code département": fmt(commune?.departement),
"localisation . code région": fmt(commune?.region),
"commune . code": fmt(code),
"epci . code": fmt(epci_by_communes[code].code),
"epci . nom": fmt(epci_by_communes[code].nom),
"département . code": fmt(commune?.departement),
"région . code": fmt(commune?.region),
};
}
}

function fmt(v: string | undefined): `'${string}'` {
return `'${v ?? ""}'`;
}

export type Localisation = {
type: "région" | "département" | "epci" | "commune";
valeur: string;
code: string;
nom: string;
};

const LOCALISATION_KINDS: RuleName[] = [
"commune . code",
"région . code",
"département . code",
"epci . code",
"epci . nom",
];

export function extractLocalisationFromAST(
rule: ASTNode,
namespace?: string
): Localisation[] {
const toKind = (dottedName: string): Localisation["type"] | undefined => {
if (dottedName.endsWith("commune . code")) {
return "commune";
}
if (dottedName.endsWith("région . code")) {
return "région";
}
if (dottedName.endsWith("département . code")) {
return "département";
}
if (
dottedName.endsWith("epci . code") ||
dottedName.endsWith("epci . nom")
) {
return "epci";
}
};
const _namespace = namespace ? `${namespace} .` : "";
const extract = (
ref: ASTNode<"reference">,
value: ASTNode<"constant">
): Localisation | undefined => {
// TODO: should we throw an error if the value is not a string?
if (value.type !== "string") {
return;
}
for (let kind of LOCALISATION_KINDS) {
if (ref.dottedName === `${_namespace} ${kind}`) {
const valeur = value.nodeValue?.toString();
const infos = getInfosFor(kind, valeur);

if (valeur && infos) {
return {
type: toKind(ref.dottedName)!,
valeur,
nom: infos.nom,
code: infos.code,
};
}
}
}
};

return reduceAST(
(acc, node) => {
if (node.nodeKind === "operation" && node.operationKind === "=") {
const ref =
node.explanation[0]?.nodeKind === "reference"
? node.explanation[0]
: node.explanation[1].nodeKind === "reference"
? node.explanation[1]
: undefined;
const value =
node.explanation[0]?.nodeKind === "constant"
? node.explanation[0]
: node.explanation[1].nodeKind === "constant"
? node.explanation[1]
: undefined;
if (ref && value) {
const localisation = extract(ref, value);
if (
localisation &&
!acc.find(
(l: Localisation) =>
l.type === localisation.type && l.valeur === localisation.valeur
)
) {
acc.push(localisation);
}
return acc;
}
}
},
new Array<Localisation>(),
rule
);
//
// if (localisations.length === 0) {
// console.warn(`No localisation found for ${rule.dottedName}`);
// return;
// }

// const normalizedLocalisations = localisations.map((loc) => {
// // In our rule basis we reference EPCI by their name but for iteroperability
// // with third-party systems it is more robust to expose their SIREN code.
// if (loc.kind === "epci") {
// const code = epci.find(({ nom }) => nom === loc.value)?.code;
//
// if (!code) {
// console.error(`Bad EPCI code: ${loc.value}`);
// exit(1);
// }
//
// return { ...loc, code };
// }
//
// return loc;
// });

// FIXME: should handle multiple localisations
// return normalizedLocalisations[0];
}

function getInfosFor(
kind: RuleName,
value: string | undefined
): Pick<Localisation, "code" | "nom"> | undefined {
if (!value) {
return;
}
switch (kind) {
case "commune . code": {
return communes.find((c) => c.code === value);
}
case "epci . nom": {
return epci.find((c) => c.nom === value);
}
case "epci . code": {
return epci.find((c) => c.code === value);
}

case "région . code": {
return regions.find((r) => r.code === value);
}

case "département . code": {
return departements.find((d) => d.code === value);
}
}
}
111 changes: 60 additions & 51 deletions src/localisation.publicodes
Original file line number Diff line number Diff line change
@@ -1,40 +1,35 @@
localisation:
titre: Localisation géographique
description: >
Règles permettant de conditionner des calculs en fonction d'une
localisation correspondante au découpage administratif français.
Le découpage est basé sur les données du paquet
[`@etalab/decoupage-administratif`](https://github.com/datagouv/decoupage-administratif).
# TODO: to move into the README
# localisation:
# titre: Localisation géographique
# description: >
# Règles permettant de conditionner des calculs en fonction d'une
# localisation correspondante au découpage administratif français.
#
#
# Le découpage est basé sur les données du paquet
# [`@etalab/decoupage-administratif`](https://github.com/datagouv/decoupage-administratif).
#
#
# Des fonctions utilitaires pour manipuler ces données et construire une
# situation complète à partir du nom d'une commune sont disponibles dans ce
# paquet (voir [la documentation](TODO)).

Des fonctions utilitaires pour manipuler ces données et construire une
situation complète à partir du nom d'une commune sont disponibles dans ce
paquet (voir [la documentation](TODO)).
localisation . code insee:
titre: Code INSEE de la commune
description: >
Le code INSEE est un code numérique unique de 5 chiffres attribué à chaque
commune française. A noter qu'il est différent du code postal qui peut être
partagé par plusieurs communes.
par défaut: "''"
note: >
Voir [Code officiel
géographique](https://fr.wikipedia.org/wiki/Code_officiel_g%C3%A9ographique)
commune:
avec:
code:
titre: Code INSEE
description: >
Le code INSEE est un code numérique unique de 5 chiffres attribué à chaque
commune française. A noter qu'il est différent du code postal qui peut être
partagé par plusieurs communes.
par défaut: "''"
note: >
Voir [Code officiel
géographique](https://fr.wikipedia.org/wiki/Code_officiel_g%C3%A9ographique)
# NOTE: à la base il était utilisé le nom de l'EPCI dans mes aides vélo, mais
# je pense qu'utiliser le code est plus cohérent avec le reste de la
# nomenclature et de toute façons c'est ce dernier qui est utilisé pour les
# calculs.
localisation . code epci:
titre: Code de l'établissement public de coopération intercommunale (EPCI)
epci:
titre: Établissement public de coopération intercommunale (EPCI)
description: >
Le code de l'EPCI est un code numérique unique de 9 chiffres attribué à
chaque établissement public de coopération intercommunale (EPCI) français.
Les EPCI sont des structures administratives permettant à plusieurs
communes d'exercer des compétences en commun.
Expand All @@ -46,27 +41,41 @@ localisation . code epci:
> Source : [insee.fr](https://www.insee.fr/fr/metadonnees/definition/c1160)
par défaut: "''"
avec:
nom:
par défaut: "''"

localisation . code département:
titre: Code du département
description: >
Le code du département est un code numérique unique de 2 chiffres attribué
à chaque département français. Il est utilisé pour identifier les
départements dans les adresses postales.
par défaut: "''"
code:
titre: Code SIREN
par défaut: "''"
description: >
Le code siren de l'EPCI est un code numérique unique de 9 chiffres attribué
à chaque établissement public de coopération intercommunale (EPCI)
français.
localisation . code région:
titre: Code de la région
description: >
Le code de la région est un code numérique unique de 2 chiffres attribué à
chaque région française. Il est utilisé pour identifier les régions dans
les adresses postales.
par défaut: "''"
département:
avec:
code:
titre: Code du département
description: >
Le code du département est un code numérique unique de 2 chiffres attribué
à chaque département français. Il est utilisé pour identifier les
départements dans les adresses postales.
par défaut: "''"

région:
avec:
code:
titre: Code de la région
description: >
Le code de la région est un code numérique unique de 2 chiffres attribué à
chaque région française. Il est utilisé pour identifier les régions dans
les adresses postales.
par défaut: "''"

# NOTE: potentiellement non pertinent ici, à voir si on la garde ou si on la
# met dans un autre fichier.
# localisation . pays:
# pays:
# question: Quel est le votre pays ?
# une possibilité:
# - France
Expand All @@ -85,7 +94,7 @@ localisation . code région:
# Luxembourg:

# NOTE: souhaitons-nous la garder ici ou la mettre dans un autre fichier ?
# localisation . ZFE:
# ZFE:
# type: boolean
# question: Êtes-vous dans une zone à faibles émissions (ZFE) ?
# par défaut: non
Loading

0 comments on commit d5d8c42

Please sign in to comment.