diff --git a/README.md b/README.md index 5078eab..72d1970 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ C'est un bon moyen de détecter un éventuel problème dans le dpe ou la librair ```javascript import { calcul_3cl } from 'open3cl'; -// Exemple d'objet JSON issu d'un fichier XML DPE +// Exemple d'objet JSON (partiel) issu d'un fichier XML DPE const dpeData = { numero_dpe: '2113E1018248X', statut: 'ACTIF', @@ -108,7 +108,23 @@ const dpeData = { } }; +// Execution d'un dpe avec la librairie Open3CL avec pré-transformation / nettoyage du dpe (comportement par défaut) const result = calcul_3cl(dpeData); +const result = calcul_3cl(dpeData, { sanitize: true }); + +// Execution d'un dpe avec la librairie Open3CL sans pré-transformation / nettoyage du dpe +const result = calcul_3cl(dpeData, { sanitize: false }); + +// Execution d'un dpe au format xml avec la librairie Open3CL avec pré-transformation / nettoyage du dpe (comportement par défaut) +const result = calcul_3cl_xml('2113E1018248X'); +const result = calcul_3cl_xml('2113E1018248X', { + sanitize: true +}); + +// Execution d'un dpe au format xml avec la librairie Open3CL sans pré-transformation / nettoyage du dpe (comportement par défaut) +const result = calcul_3cl_xml('2113E1018248X', { + sanitize: false +}); ``` ## Variables d'environnements @@ -316,7 +332,7 @@ Nous accueillons les contributions avec plaisir ! Si vous souhaitez améliorer O ## Licence -Distribué sous la license `GPL-3.0 license`. Lire le fichier `LICENSE` pour plus d'informations. +Distribué sous la license `MIT`. Lire le fichier `LICENSE` pour plus d'informations.

(Retour sommaire)

diff --git a/dist/reports/corpus/corpus_list_main.json b/dist/reports/corpus/corpus_list_main.json index d0df516..3b249cc 100644 --- a/dist/reports/corpus/corpus_list_main.json +++ b/dist/reports/corpus/corpus_list_main.json @@ -9,5 +9,5 @@ "dpe_appartement_individuel_chauffage_individuel_2025.csv", "dpe_appartement_individuel_chauffage_collectif_2025.csv" ], - "branches": ["main", "fix_issue_132"] + "branches": ["main"] } diff --git a/index.js b/index.js index 439f4d6..3190677 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,10 @@ import { calcul_3cl, + calcul_3cl_xml, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion } from './src/index.js'; -export { calcul_3cl, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion }; +export { calcul_3cl, calcul_3cl_xml, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion }; import { Umur, Uph, Upb, Uporte, Ubv, Upt, calc_deperdition } from './src/3_deperdition.js'; export { Umur, Uph, Upb, Uporte, Ubv, Upt, calc_deperdition }; diff --git a/src/dpe-sanitizer.service.js b/src/dpe-sanitizer.service.js new file mode 100644 index 0000000..ed16983 --- /dev/null +++ b/src/dpe-sanitizer.service.js @@ -0,0 +1,90 @@ +import { ObjectUtil } from './core/util/infrastructure/object-util.js'; + +const nodesToMap = [ + 'mur', + 'plancher_bas', + 'plancher_haut', + 'baie_vitree', + 'porte', + 'pont_thermique', + 'ventilation', + 'installation_ecs', + 'generateur_ecs', + 'climatisation', + 'installation_chauffage', + 'generateur_chauffage', + 'emetteur_chauffage', + 'sortie_par_energie' +]; + +/** + * Transform single nodes in {@link nodesToMap} into array of nodes. + * Transform string number into digits + * These transformations should be done inside the open3cl library + * + * @example + * // Will transform + * "plancher_haut_collection": { + * "plancher_haut": {"id": 1} + * } + * // Into + * "plancher_haut_collection": { + * "plancher_haut": [{"id": 1}] + * } + * + * @example + * // Will transform + * "surface_paroi_opaque": "40.94" + * // Into + * "surface_paroi_opaque": 40.94 + */ +export default class DpeSanitizerService { + /** + * @param dpe {FullDpe} + * @return {FullDpe} + */ + execute(dpe) { + return ObjectUtil.deepObjectTransform( + dpe, + (key) => key, + (val, key) => { + if (this.#needTransform(key, val)) { + return [val]; + } + + if (this.#isEnum(key)) { + return val; + } + + if (this.#isUndefinedVal(val)) { + return ''; + } + + if (this.#isEmptyArray(val)) { + return val; + } + + if (Number.isNaN(Number(val))) { + return val; + } + return Number(val); + } + ); + } + + #isEnum(key) { + return key.startsWith('enum_') || key.startsWith('original_enum'); + } + + #needTransform(key, val) { + return typeof val === 'object' && !Array.isArray(val) && nodesToMap.includes(key); + } + + #isUndefinedVal(val) { + return val === '' || val === null; + } + + #isEmptyArray(val) { + return Array.isArray(val) && val.length === 0; + } +} diff --git a/src/engine.js b/src/engine.js index e5112ee..d748589 100644 --- a/src/engine.js +++ b/src/engine.js @@ -19,14 +19,17 @@ import { collectionCanBeEmpty, containsAnySubstring, isEffetJoule, - sanitize_dpe + xmlParser } from './utils.js'; import { Inertie } from './7_inertie.js'; import getFicheTechnique from './ficheTechnique.js'; import { ProductionENR } from './16.2_production_enr.js'; +import DpeSanitizerService from './dpe-sanitizer.service.js'; const LIB_VERSION = 'OPEN3CL_VERSION'; +const dpeSanitizerService = new DpeSanitizerService(); + function calc_th(map_id) { const map = enums.methode_application_dpe_log[map_id]; if (map.includes('maison')) return 'maison'; @@ -44,11 +47,26 @@ export function getVersion() { } /** - * @param dpe {FullDpe} + * Run the engine with a full dpe xml content + * @param dpeXmlContent {string} A full dpe xml content + * @param options {{sanitize: boolean}?} + * @return {FullDpe} + */ +export function calcul_3cl_xml(dpeXmlContent, options) { + /** @type {{dpe: FullDpe}} **/ + const xmlDpe = xmlParser.parse(dpeXmlContent); + return calcul_3cl(xmlDpe.dpe, options); +} + +/** + * Run the engine with a javascript plain object dpe + * @param inputDpe {FullDpe} A full plain object dpe + * @param options {{sanitize: boolean}?} * @return {FullDpe} */ -export function calcul_3cl(dpe) { - sanitize_dpe(dpe); +export function calcul_3cl(inputDpe, options) { + if (!options) options = { sanitize: true }; + const dpe = options.sanitize ? dpeSanitizerService.execute(inputDpe) : inputDpe; const modele = enums.modele_dpe[dpe.administratif.enum_modele_dpe_id]; const dateDpe = dpe.administratif.date_etablissement_dpe; if (modele !== 'dpe 3cl 2021 méthode logement') { diff --git a/src/index.js b/src/index.js index 5a11ecc..77cc80c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,10 @@ -import { calcul_3cl, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion } from './engine.js'; -export { calcul_3cl, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion }; +import { + calcul_3cl, + calcul_3cl_xml, + get_classe_ges_dpe, + get_conso_coeff_1_9_2026, + getVersion +} from './engine.js'; +export { calcul_3cl, calcul_3cl_xml, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion }; import { Umur, Uph, Upb, Uporte, Ubv, Upt } from './3_deperdition.js'; export { Umur, Uph, Upb, Uporte, Ubv, Upt }; diff --git a/src/utils.js b/src/utils.js index 68247d3..4ecfd87 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,38 @@ import enums from './enums.js'; import tvs from './tv.js'; -import { set, has } from 'lodash-es'; +import { set } from 'lodash-es'; +import { XMLParser } from 'fast-xml-parser'; + +export const xmlParser = new XMLParser({ + // We want to make sure collections of length 1 are still parsed as arrays + isArray: (name) => { + const collectionNames = [ + 'mur', + 'plancher_bas', + 'plancher_haut', + 'baie_vitree', + 'porte', + 'pont_thermique', + 'ventilation', + 'installation_ecs', + 'generateur_ecs', + 'climatisation', + 'installation_chauffage', + 'generateur_chauffage', + 'emetteur_chauffage', + 'sortie_par_energie' + ]; + if (collectionNames.includes(name)) return true; + }, + tagValueProcessor: (tagName, val) => { + if (tagName.startsWith('enum_')) { + // Preserve value as string for tags starting with "enum_" + return null; + } + if (Number.isNaN(Number(val))) return val; + return Number(val); + } +}); export let bug_for_bug_compat = false; export function set_bug_for_bug_compat() { @@ -285,44 +317,12 @@ export function removeKeyFromJSON(jsonObj, keyToRemove, skipKeys) { } } -export function useEnumAsString(jsonObj) { - for (const key in jsonObj) { - if (jsonObj.hasOwnProperty(key)) { - if (key.startsWith('enum_')) { - if (jsonObj[key] !== null) jsonObj[key] = jsonObj[key].toString(); - } else if (typeof jsonObj[key] === 'object') { - useEnumAsString(jsonObj[key]); - } - } - } -} - export function clean_dpe(dpe_in) { // skip generateur_[ecs|chauffage] because some input data is contained in donnee_intermediaire (e.g. pn, qp0, ...) removeKeyFromJSON(dpe_in, 'donnee_intermediaire', ['generateur_ecs', 'generateur_chauffage']); set(dpe_in, 'logement.sortie', null); } -export function sanitize_dpe(dpe_in) { - const collection_paths = [ - 'logement.enveloppe.plancher_bas_collection.plancher_bas', - 'logement.enveloppe.plancher_haut_collection.plancher_haut', - 'logement.ventilation_collection.ventilation', - 'logement.climatisation_collection.climatisation', - 'logement.enveloppe.baie_vitree_collection.baie_vitree', - 'logement.enveloppe.porte_collection.porte', - 'logement.enveloppe.pont_thermique_collection.pont_thermique' - ]; - for (const path of collection_paths) { - if (!has(dpe_in, path)) { - set(dpe_in, path, []); - } - } - if (use_enum_as_string) { - useEnumAsString(dpe_in); - } -} - /** * Retrieve a number describing a thickness from the description * @param description string in which to get the number diff --git a/test/test-helpers.js b/test/test-helpers.js index beca3ea..7b7a58c 100644 --- a/test/test-helpers.js +++ b/test/test-helpers.js @@ -1,40 +1,5 @@ -import { XMLParser } from 'fast-xml-parser'; import fs from 'node:fs'; - -const xmlParser = new XMLParser({ - // We want to make sure collections of length 1 are still parsed as arrays - isArray: (name) => { - const collectionNames = [ - 'mur', - 'plancher_bas', - 'plancher_haut', - 'baie_vitree', - 'porte', - 'pont_thermique', - 'ventilation', - 'installation_ecs', - 'generateur_ecs', - 'climatisation', - 'installation_chauffage', - 'generateur_chauffage', - 'emetteur_chauffage', - 'sortie_par_energie' - ]; - if (collectionNames.includes(name)) return true; - }, - tagValueProcessor: (tagName, val) => { - if (tagName.startsWith('enum_')) { - // Preserve value as string for tags starting with "enum_" - return null; - } - if (Number.isNaN(Number(val))) return val; - return Number(val); - } -}); - -export function parseXml(data) { - return xmlParser.parse(data).dpe; -} +import { xmlParser } from '../src/utils.js'; export async function getAdemeFileJsonOrDownload(dpeCode) { if (!process.env.ADEME_CLIENT_ID || !process.env.ADEME_CLIENT_SECRET) {