Skip to content

Commit 34b59bc

Browse files
Merge pull request #145 from Open3CL/chore_sanitize_dpe
Chore sanitize dpe
2 parents d97a075 + c40c50b commit 34b59bc

8 files changed

Lines changed: 175 additions & 79 deletions

File tree

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ C'est un bon moyen de détecter un éventuel problème dans le dpe ou la librair
8585
```javascript
8686
import { calcul_3cl } from 'open3cl';
8787

88-
// Exemple d'objet JSON issu d'un fichier XML DPE
88+
// Exemple d'objet JSON (partiel) issu d'un fichier XML DPE
8989
const dpeData = {
9090
numero_dpe: '2113E1018248X',
9191
statut: 'ACTIF',
@@ -108,7 +108,23 @@ const dpeData = {
108108
}
109109
};
110110

111+
// Execution d'un dpe avec la librairie Open3CL avec pré-transformation / nettoyage du dpe (comportement par défaut)
111112
const result = calcul_3cl(dpeData);
113+
const result = calcul_3cl(dpeData, { sanitize: true });
114+
115+
// Execution d'un dpe avec la librairie Open3CL sans pré-transformation / nettoyage du dpe
116+
const result = calcul_3cl(dpeData, { sanitize: false });
117+
118+
// Execution d'un dpe au format xml avec la librairie Open3CL avec pré-transformation / nettoyage du dpe (comportement par défaut)
119+
const result = calcul_3cl_xml('<xml><dpe><numero_dpe>2113E1018248X</numero_dpe></dpe</xml>');
120+
const result = calcul_3cl_xml('<xml><dpe><numero_dpe>2113E1018248X</numero_dpe></dpe</xml>', {
121+
sanitize: true
122+
});
123+
124+
// Execution d'un dpe au format xml avec la librairie Open3CL sans pré-transformation / nettoyage du dpe (comportement par défaut)
125+
const result = calcul_3cl_xml('<xml><dpe><numero_dpe>2113E1018248X</numero_dpe></dpe</xml>', {
126+
sanitize: false
127+
});
112128
```
113129

114130
## Variables d'environnements
@@ -316,7 +332,7 @@ Nous accueillons les contributions avec plaisir ! Si vous souhaitez améliorer O
316332

317333
## Licence
318334

319-
Distribué sous la license `GPL-3.0 license`. Lire le fichier `LICENSE` pour plus d'informations.
335+
Distribué sous la license `MIT`. Lire le fichier `LICENSE` pour plus d'informations.
320336

321337
<p align="right">(<a href="#readme-top">Retour sommaire</a>)</p>
322338

dist/reports/corpus/corpus_list_main.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@
99
"dpe_appartement_individuel_chauffage_individuel_2025.csv",
1010
"dpe_appartement_individuel_chauffage_collectif_2025.csv"
1111
],
12-
"branches": ["main", "fix_issue_132"]
12+
"branches": ["main"]
1313
}

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import {
22
calcul_3cl,
3+
calcul_3cl_xml,
34
get_classe_ges_dpe,
45
get_conso_coeff_1_9_2026,
56
getVersion
67
} from './src/index.js';
7-
export { calcul_3cl, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion };
8+
export { calcul_3cl, calcul_3cl_xml, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion };
89
import { Umur, Uph, Upb, Uporte, Ubv, Upt, calc_deperdition } from './src/3_deperdition.js';
910
export { Umur, Uph, Upb, Uporte, Ubv, Upt, calc_deperdition };

src/dpe-sanitizer.service.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { ObjectUtil } from './core/util/infrastructure/object-util.js';
2+
3+
const nodesToMap = [
4+
'mur',
5+
'plancher_bas',
6+
'plancher_haut',
7+
'baie_vitree',
8+
'porte',
9+
'pont_thermique',
10+
'ventilation',
11+
'installation_ecs',
12+
'generateur_ecs',
13+
'climatisation',
14+
'installation_chauffage',
15+
'generateur_chauffage',
16+
'emetteur_chauffage',
17+
'sortie_par_energie'
18+
];
19+
20+
/**
21+
* Transform single nodes in {@link nodesToMap} into array of nodes.
22+
* Transform string number into digits
23+
* These transformations should be done inside the open3cl library
24+
*
25+
* @example
26+
* // Will transform
27+
* "plancher_haut_collection": {
28+
* "plancher_haut": {"id": 1}
29+
* }
30+
* // Into
31+
* "plancher_haut_collection": {
32+
* "plancher_haut": [{"id": 1}]
33+
* }
34+
*
35+
* @example
36+
* // Will transform
37+
* "surface_paroi_opaque": "40.94"
38+
* // Into
39+
* "surface_paroi_opaque": 40.94
40+
*/
41+
export default class DpeSanitizerService {
42+
/**
43+
* @param dpe {FullDpe}
44+
* @return {FullDpe}
45+
*/
46+
execute(dpe) {
47+
return ObjectUtil.deepObjectTransform(
48+
dpe,
49+
(key) => key,
50+
(val, key) => {
51+
if (this.#needTransform(key, val)) {
52+
return [val];
53+
}
54+
55+
if (this.#isEnum(key)) {
56+
return val;
57+
}
58+
59+
if (this.#isUndefinedVal(val)) {
60+
return '';
61+
}
62+
63+
if (this.#isEmptyArray(val)) {
64+
return val;
65+
}
66+
67+
if (Number.isNaN(Number(val))) {
68+
return val;
69+
}
70+
return Number(val);
71+
}
72+
);
73+
}
74+
75+
#isEnum(key) {
76+
return key.startsWith('enum_') || key.startsWith('original_enum');
77+
}
78+
79+
#needTransform(key, val) {
80+
return typeof val === 'object' && !Array.isArray(val) && nodesToMap.includes(key);
81+
}
82+
83+
#isUndefinedVal(val) {
84+
return val === '' || val === null;
85+
}
86+
87+
#isEmptyArray(val) {
88+
return Array.isArray(val) && val.length === 0;
89+
}
90+
}

src/engine.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@ import {
1919
collectionCanBeEmpty,
2020
containsAnySubstring,
2121
isEffetJoule,
22-
sanitize_dpe
22+
xmlParser
2323
} from './utils.js';
2424
import { Inertie } from './7_inertie.js';
2525
import getFicheTechnique from './ficheTechnique.js';
2626
import { ProductionENR } from './16.2_production_enr.js';
27+
import DpeSanitizerService from './dpe-sanitizer.service.js';
2728

2829
const LIB_VERSION = 'OPEN3CL_VERSION';
2930

31+
const dpeSanitizerService = new DpeSanitizerService();
32+
3033
function calc_th(map_id) {
3134
const map = enums.methode_application_dpe_log[map_id];
3235
if (map.includes('maison')) return 'maison';
@@ -44,11 +47,26 @@ export function getVersion() {
4447
}
4548

4649
/**
47-
* @param dpe {FullDpe}
50+
* Run the engine with a full dpe xml content
51+
* @param dpeXmlContent {string} A full dpe xml content
52+
* @param options {{sanitize: boolean}?}
53+
* @return {FullDpe}
54+
*/
55+
export function calcul_3cl_xml(dpeXmlContent, options) {
56+
/** @type {{dpe: FullDpe}} **/
57+
const xmlDpe = xmlParser.parse(dpeXmlContent);
58+
return calcul_3cl(xmlDpe.dpe, options);
59+
}
60+
61+
/**
62+
* Run the engine with a javascript plain object dpe
63+
* @param inputDpe {FullDpe} A full plain object dpe
64+
* @param options {{sanitize: boolean}?}
4865
* @return {FullDpe}
4966
*/
50-
export function calcul_3cl(dpe) {
51-
sanitize_dpe(dpe);
67+
export function calcul_3cl(inputDpe, options) {
68+
if (!options) options = { sanitize: true };
69+
const dpe = options.sanitize ? dpeSanitizerService.execute(inputDpe) : inputDpe;
5270
const modele = enums.modele_dpe[dpe.administratif.enum_modele_dpe_id];
5371
const dateDpe = dpe.administratif.date_etablissement_dpe;
5472
if (modele !== 'dpe 3cl 2021 méthode logement') {

src/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { calcul_3cl, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion } from './engine.js';
2-
export { calcul_3cl, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion };
1+
import {
2+
calcul_3cl,
3+
calcul_3cl_xml,
4+
get_classe_ges_dpe,
5+
get_conso_coeff_1_9_2026,
6+
getVersion
7+
} from './engine.js';
8+
export { calcul_3cl, calcul_3cl_xml, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion };
39
import { Umur, Uph, Upb, Uporte, Ubv, Upt } from './3_deperdition.js';
410
export { Umur, Uph, Upb, Uporte, Ubv, Upt };

src/utils.js

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,38 @@
11
import enums from './enums.js';
22
import tvs from './tv.js';
3-
import { set, has } from 'lodash-es';
3+
import { set } from 'lodash-es';
4+
import { XMLParser } from 'fast-xml-parser';
5+
6+
export const xmlParser = new XMLParser({
7+
// We want to make sure collections of length 1 are still parsed as arrays
8+
isArray: (name) => {
9+
const collectionNames = [
10+
'mur',
11+
'plancher_bas',
12+
'plancher_haut',
13+
'baie_vitree',
14+
'porte',
15+
'pont_thermique',
16+
'ventilation',
17+
'installation_ecs',
18+
'generateur_ecs',
19+
'climatisation',
20+
'installation_chauffage',
21+
'generateur_chauffage',
22+
'emetteur_chauffage',
23+
'sortie_par_energie'
24+
];
25+
if (collectionNames.includes(name)) return true;
26+
},
27+
tagValueProcessor: (tagName, val) => {
28+
if (tagName.startsWith('enum_')) {
29+
// Preserve value as string for tags starting with "enum_"
30+
return null;
31+
}
32+
if (Number.isNaN(Number(val))) return val;
33+
return Number(val);
34+
}
35+
});
436

537
export let bug_for_bug_compat = false;
638
export function set_bug_for_bug_compat() {
@@ -285,44 +317,12 @@ export function removeKeyFromJSON(jsonObj, keyToRemove, skipKeys) {
285317
}
286318
}
287319

288-
export function useEnumAsString(jsonObj) {
289-
for (const key in jsonObj) {
290-
if (jsonObj.hasOwnProperty(key)) {
291-
if (key.startsWith('enum_')) {
292-
if (jsonObj[key] !== null) jsonObj[key] = jsonObj[key].toString();
293-
} else if (typeof jsonObj[key] === 'object') {
294-
useEnumAsString(jsonObj[key]);
295-
}
296-
}
297-
}
298-
}
299-
300320
export function clean_dpe(dpe_in) {
301321
// skip generateur_[ecs|chauffage] because some input data is contained in donnee_intermediaire (e.g. pn, qp0, ...)
302322
removeKeyFromJSON(dpe_in, 'donnee_intermediaire', ['generateur_ecs', 'generateur_chauffage']);
303323
set(dpe_in, 'logement.sortie', null);
304324
}
305325

306-
export function sanitize_dpe(dpe_in) {
307-
const collection_paths = [
308-
'logement.enveloppe.plancher_bas_collection.plancher_bas',
309-
'logement.enveloppe.plancher_haut_collection.plancher_haut',
310-
'logement.ventilation_collection.ventilation',
311-
'logement.climatisation_collection.climatisation',
312-
'logement.enveloppe.baie_vitree_collection.baie_vitree',
313-
'logement.enveloppe.porte_collection.porte',
314-
'logement.enveloppe.pont_thermique_collection.pont_thermique'
315-
];
316-
for (const path of collection_paths) {
317-
if (!has(dpe_in, path)) {
318-
set(dpe_in, path, []);
319-
}
320-
}
321-
if (use_enum_as_string) {
322-
useEnumAsString(dpe_in);
323-
}
324-
}
325-
326326
/**
327327
* Retrieve a number describing a thickness from the description
328328
* @param description string in which to get the number

test/test-helpers.js

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,5 @@
1-
import { XMLParser } from 'fast-xml-parser';
21
import fs from 'node:fs';
3-
4-
const xmlParser = new XMLParser({
5-
// We want to make sure collections of length 1 are still parsed as arrays
6-
isArray: (name) => {
7-
const collectionNames = [
8-
'mur',
9-
'plancher_bas',
10-
'plancher_haut',
11-
'baie_vitree',
12-
'porte',
13-
'pont_thermique',
14-
'ventilation',
15-
'installation_ecs',
16-
'generateur_ecs',
17-
'climatisation',
18-
'installation_chauffage',
19-
'generateur_chauffage',
20-
'emetteur_chauffage',
21-
'sortie_par_energie'
22-
];
23-
if (collectionNames.includes(name)) return true;
24-
},
25-
tagValueProcessor: (tagName, val) => {
26-
if (tagName.startsWith('enum_')) {
27-
// Preserve value as string for tags starting with "enum_"
28-
return null;
29-
}
30-
if (Number.isNaN(Number(val))) return val;
31-
return Number(val);
32-
}
33-
});
34-
35-
export function parseXml(data) {
36-
return xmlParser.parse(data).dpe;
37-
}
2+
import { xmlParser } from '../src/utils.js';
383

394
export async function getAdemeFileJsonOrDownload(dpeCode) {
405
if (!process.env.ADEME_CLIENT_ID || !process.env.ADEME_CLIENT_SECRET) {

0 commit comments

Comments
 (0)