@open3cl/engine 1.7.1 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/13.2_generateur_combustion.js +2 -10
- package/3.1_b.js +45 -33
- package/3.3_baie_vitree.js +32 -10
- package/3.4_pont_thermique.js +21 -13
- package/3.4_pont_thermique.spec.js +1 -1
- package/README.md +31 -1
- package/engine.js +1 -1
- package/ficheTechnique.js +133 -53
- package/ficheTechnique.spec.js +46 -14
- package/formula.js +251 -0
- package/formula.spec.js +99 -0
- package/package.json +5 -6
- package/utils.js +195 -44
- package/utils.spec.js +69 -0
|
@@ -4,7 +4,7 @@ import { updateGenerateurBouilleurs } from './13.2_generateur_combustion_bouille
|
|
|
4
4
|
import { updateGenerateurChaudieres } from './13.2_generateur_combustion_chaudiere.js';
|
|
5
5
|
import { updateGenerateurPacs } from './13.2_generateur_pac.js';
|
|
6
6
|
import getFicheTechnique from './ficheTechnique.js';
|
|
7
|
-
import {
|
|
7
|
+
import { evaluateFormula as evaluateTvFormula } from './formula.js';
|
|
8
8
|
|
|
9
9
|
function criterePn(Pn, matcher) {
|
|
10
10
|
let critere_list = tvColumnLines('generateur_combustion', 'critere_pn', matcher);
|
|
@@ -38,15 +38,7 @@ const F_tab = {
|
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
function evaluateFormula(formulaOrValue, pn, E, F) {
|
|
41
|
-
|
|
42
|
-
if (!isNaN(formulaOrValue)) {
|
|
43
|
-
return Number(formulaOrValue);
|
|
44
|
-
}
|
|
45
|
-
const variables = new Map();
|
|
46
|
-
variables.set('Pn', pnVariable);
|
|
47
|
-
variables.set('E', E);
|
|
48
|
-
variables.set('F', F);
|
|
49
|
-
return evaluate(formulaOrValue, variables);
|
|
41
|
+
return evaluateTvFormula(formulaOrValue, pn, E, F);
|
|
50
42
|
}
|
|
51
43
|
|
|
52
44
|
/**
|
package/3.1_b.js
CHANGED
|
@@ -1,8 +1,47 @@
|
|
|
1
1
|
import enums from './enums.js';
|
|
2
2
|
import { tv, requestInput, requestInputID, bug_for_bug_compat } from './utils.js';
|
|
3
3
|
|
|
4
|
+
const RANGES = [0.25, 0.5, 0.75, 1, 1.25, 2, 2.5, 3, 3.5, 4, 6, 8, 10, 25, 50];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Identifiants de type d'adjacence pour lesquels le ratio aiu/aue est pris en compte.
|
|
8
|
+
*/
|
|
9
|
+
const ADJACENCE_AIU_AUE_IDS = new Set([
|
|
10
|
+
'8',
|
|
11
|
+
'9',
|
|
12
|
+
'11',
|
|
13
|
+
'12',
|
|
14
|
+
'13',
|
|
15
|
+
'14',
|
|
16
|
+
'15',
|
|
17
|
+
'16',
|
|
18
|
+
'17',
|
|
19
|
+
'18',
|
|
20
|
+
'19',
|
|
21
|
+
'21'
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const TYPE_ADJACENCE_IDS = Object.keys(enums.type_adjacence);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Formate une borne du ratio aiu/aue au format des tables de valeurs (`0,25`, `1,00`…).
|
|
28
|
+
*
|
|
29
|
+
* `toLocaleString(undefined, { minimumFractionDigits: 2, … })` construisait un
|
|
30
|
+
* `Intl.NumberFormat` à chaque appel (~29 µs), ce qui représentait l'essentiel du
|
|
31
|
+
* temps CPU de `b()`. `toFixed(2)` produit exactement les mêmes chaînes pour toutes
|
|
32
|
+
* les valeurs de `RANGES`, en ~0,2 µs, et sans dépendre de la locale de la machine —
|
|
33
|
+
* le `.replace('.', ',')` qui suivait était d'ailleurs un no-op sur une machine en
|
|
34
|
+
* locale française et une vraie conversion ailleurs.
|
|
35
|
+
*
|
|
36
|
+
* @param borne {number}
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function formatBorneAiuAue(borne) {
|
|
40
|
+
return borne.toFixed(2).replace('.', ',');
|
|
41
|
+
}
|
|
42
|
+
|
|
4
43
|
export function findRanges(inputNumber) {
|
|
5
|
-
const ranges =
|
|
44
|
+
const ranges = RANGES;
|
|
6
45
|
const result = [];
|
|
7
46
|
|
|
8
47
|
if (inputNumber < ranges[0]) {
|
|
@@ -28,7 +67,7 @@ export function findRanges(inputNumber) {
|
|
|
28
67
|
export default function b(di, de, du, zc_id) {
|
|
29
68
|
const zc = enums.zone_climatique[zc_id].slice(0, 2);
|
|
30
69
|
|
|
31
|
-
du.enum_type_adjacence_id =
|
|
70
|
+
du.enum_type_adjacence_id = TYPE_ADJACENCE_IDS;
|
|
32
71
|
const matcher = {
|
|
33
72
|
enum_type_adjacence_id: requestInputID(de, du, 'type_adjacence')
|
|
34
73
|
};
|
|
@@ -37,11 +76,7 @@ export default function b(di, de, du, zc_id) {
|
|
|
37
76
|
matcher.zone_climatique = zc;
|
|
38
77
|
matcher.enum_cfg_isolation_lnc_id = requestInputID(de, du, 'cfg_isolation_lnc');
|
|
39
78
|
/* du.enum_cfg_isolation_lnc_id = ['6', '7', '8', '9', '10', '11'] */
|
|
40
|
-
} else if (
|
|
41
|
-
['8', '9', '11', '12', '13', '14', '15', '16', '17', '18', '19', '21'].includes(
|
|
42
|
-
de.enum_type_adjacence_id.toString()
|
|
43
|
-
)
|
|
44
|
-
) {
|
|
79
|
+
} else if (ADJACENCE_AIU_AUE_IDS.has(de.enum_type_adjacence_id.toString())) {
|
|
45
80
|
if (de.surface_aue === 0) {
|
|
46
81
|
// cf page 10
|
|
47
82
|
// NOTE: bizarre de regarder aue pour un local chauffé non accessible
|
|
@@ -96,36 +131,13 @@ export default function b(di, de, du, zc_id) {
|
|
|
96
131
|
// Si le rapport des surfaces est égale à la borne inférieure, utilisation de la borne supérieure
|
|
97
132
|
// et prise en compte de l'égalité
|
|
98
133
|
if (ranges[0] === de.surface_aiu / de.surface_aue) {
|
|
99
|
-
matcher.aiu_aue_max =
|
|
100
|
-
'≤ ' +
|
|
101
|
-
ranges[0]
|
|
102
|
-
.toLocaleString(undefined, {
|
|
103
|
-
minimumFractionDigits: 2,
|
|
104
|
-
maximumFractionDigits: 2,
|
|
105
|
-
useGrouping: false
|
|
106
|
-
})
|
|
107
|
-
.replace('.', ',');
|
|
134
|
+
matcher.aiu_aue_max = '≤ ' + formatBorneAiuAue(ranges[0]);
|
|
108
135
|
} else {
|
|
109
|
-
matcher.aiu_aue_min =
|
|
110
|
-
ranges[0]
|
|
111
|
-
.toLocaleString(undefined, {
|
|
112
|
-
minimumFractionDigits: 2,
|
|
113
|
-
maximumFractionDigits: 2,
|
|
114
|
-
useGrouping: false
|
|
115
|
-
})
|
|
116
|
-
.replace('.', ',') + ' <';
|
|
136
|
+
matcher.aiu_aue_min = formatBorneAiuAue(ranges[0]) + ' <';
|
|
117
137
|
}
|
|
118
138
|
}
|
|
119
139
|
if (ranges[1]) {
|
|
120
|
-
matcher.aiu_aue_max =
|
|
121
|
-
'≤ ' +
|
|
122
|
-
ranges[1]
|
|
123
|
-
.toLocaleString(undefined, {
|
|
124
|
-
minimumFractionDigits: 2,
|
|
125
|
-
maximumFractionDigits: 2,
|
|
126
|
-
useGrouping: false
|
|
127
|
-
})
|
|
128
|
-
.replace('.', ',');
|
|
140
|
+
matcher.aiu_aue_max = '≤ ' + formatBorneAiuAue(ranges[1]);
|
|
129
141
|
}
|
|
130
142
|
}
|
|
131
143
|
}
|
package/3.3_baie_vitree.js
CHANGED
|
@@ -23,6 +23,37 @@ function tv_ug(di, de, du) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Valeurs de Ug tabulées dans `uw`, indexées par (type de baie, matériau de menuiserie).
|
|
28
|
+
*
|
|
29
|
+
* Le scan des 723 lignes de `tvs.uw` — avec un `split('|')` par ligne — était refait
|
|
30
|
+
* pour chaque baie vitrée de chaque DPE, alors que la table est statique.
|
|
31
|
+
*
|
|
32
|
+
* Le tableau est partagé et `getRange()` le trie sur place : le tri par défaut de
|
|
33
|
+
* `Array#sort` étant déterministe, la première mise en cache le laisse trié et les
|
|
34
|
+
* appels suivants produisent exactement le même résultat qu'avec un tableau reconstruit.
|
|
35
|
+
*/
|
|
36
|
+
const uwUgValuesCache = new Map();
|
|
37
|
+
|
|
38
|
+
function uwUgValues(enum_type_baie_id, enum_type_materiaux_menuiserie_id) {
|
|
39
|
+
const cacheKey = `${enum_type_baie_id}${enum_type_materiaux_menuiserie_id}`;
|
|
40
|
+
let values = uwUgValuesCache.get(cacheKey);
|
|
41
|
+
if (values === undefined) {
|
|
42
|
+
values = tvs.uw
|
|
43
|
+
.filter((row) => {
|
|
44
|
+
return (
|
|
45
|
+
row.enum_type_baie_id.split('|').includes(enum_type_baie_id) &&
|
|
46
|
+
row.enum_type_materiaux_menuiserie_id
|
|
47
|
+
.split('|')
|
|
48
|
+
.includes(enum_type_materiaux_menuiserie_id)
|
|
49
|
+
);
|
|
50
|
+
})
|
|
51
|
+
.map((row) => parseFloat(row.ug));
|
|
52
|
+
uwUgValuesCache.set(cacheKey, values);
|
|
53
|
+
}
|
|
54
|
+
return values;
|
|
55
|
+
}
|
|
56
|
+
|
|
26
57
|
function tv_uw(di, de) {
|
|
27
58
|
const enum_type_baie_id = de.enum_type_baie_id;
|
|
28
59
|
const matcher = { enum_type_baie_id };
|
|
@@ -46,16 +77,7 @@ function tv_uw(di, de) {
|
|
|
46
77
|
matcher.enum_type_materiaux_menuiserie_id = enum_type_materiaux_menuiserie_id;
|
|
47
78
|
|
|
48
79
|
// Récupération de toutes les valeurs de Ug présentes dans les tables Uw pour le type de baie et matériaux de la baie vitrée
|
|
49
|
-
const ugValues =
|
|
50
|
-
.filter((row) => {
|
|
51
|
-
return (
|
|
52
|
-
row.enum_type_baie_id.split('|').includes(enum_type_baie_id) &&
|
|
53
|
-
row.enum_type_materiaux_menuiserie_id
|
|
54
|
-
.split('|')
|
|
55
|
-
.includes(enum_type_materiaux_menuiserie_id)
|
|
56
|
-
);
|
|
57
|
-
})
|
|
58
|
-
.map((row) => parseFloat(row.ug));
|
|
80
|
+
const ugValues = uwUgValues(enum_type_baie_id, enum_type_materiaux_menuiserie_id);
|
|
59
81
|
|
|
60
82
|
/**
|
|
61
83
|
* 3.3.2 Coefficients Uw des fenêtres / portes-fenêtres
|
package/3.4_pont_thermique.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import enums from './enums.js';
|
|
2
2
|
import { isNil } from 'lodash-es';
|
|
3
|
-
import { tv, requestInput,
|
|
3
|
+
import { tv, requestInput, cleanReference, bug_for_bug_compat } from './utils.js';
|
|
4
4
|
|
|
5
5
|
function defaultValue(type_liaison, pt_di, de) {
|
|
6
6
|
if (pt_di.k === 0) {
|
|
@@ -115,10 +115,20 @@ function tv_k(pt_di, di, de, du, pc_id, logement) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Références du pont thermique nettoyées une seule fois : elles sont confrontées à toutes les
|
|
120
|
+
* parois de plusieurs listes ci-dessous, et `compareReferences` les renettoyait à chaque
|
|
121
|
+
* comparaison. Les affectations de `de.reference_1` / `de.reference_2` ont toutes eu lieu
|
|
122
|
+
* au-dessus, dans le bloc de récupération par description.
|
|
123
|
+
*/
|
|
124
|
+
const cleaned_reference_1 = cleanReference(de.reference_1);
|
|
125
|
+
const cleaned_reference_2 = cleanReference(de.reference_2);
|
|
126
|
+
|
|
118
127
|
let mur = mur_list.find((mur, murIndex) => {
|
|
128
|
+
const cleaned_mur_reference = cleanReference(mur.donnee_entree.reference);
|
|
119
129
|
if (
|
|
120
|
-
|
|
121
|
-
|
|
130
|
+
cleaned_mur_reference === cleaned_reference_1 ||
|
|
131
|
+
cleaned_mur_reference === cleaned_reference_2
|
|
122
132
|
) {
|
|
123
133
|
return true;
|
|
124
134
|
}
|
|
@@ -190,11 +200,10 @@ function tv_k(pt_di, di, de, du, pc_id, logement) {
|
|
|
190
200
|
case 'plancher bas / mur':
|
|
191
201
|
case 'plancher haut lourd / mur': {
|
|
192
202
|
const plancher_list = ph_list.concat(pb_list);
|
|
193
|
-
const plancher = plancher_list.find(
|
|
194
|
-
(plancher)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
);
|
|
203
|
+
const plancher = plancher_list.find((plancher) => {
|
|
204
|
+
const cleaned = cleanReference(plancher.donnee_entree.reference);
|
|
205
|
+
return cleaned === cleaned_reference_1 || cleaned === cleaned_reference_2;
|
|
206
|
+
});
|
|
198
207
|
if (!plancher) {
|
|
199
208
|
di.k = defaultValue(type_liaison, pt_di, de);
|
|
200
209
|
console.error(
|
|
@@ -331,11 +340,10 @@ function tv_k(pt_di, di, de, du, pc_id, logement) {
|
|
|
331
340
|
}
|
|
332
341
|
|
|
333
342
|
const menuiserie_list = bv_list.concat(porte_list);
|
|
334
|
-
const menuiserie = menuiserie_list.find(
|
|
335
|
-
(men)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
);
|
|
343
|
+
const menuiserie = menuiserie_list.find((men) => {
|
|
344
|
+
const cleaned = cleanReference(men.donnee_entree.reference);
|
|
345
|
+
return cleaned === cleaned_reference_1 || cleaned === cleaned_reference_2;
|
|
346
|
+
});
|
|
339
347
|
if (!menuiserie) {
|
|
340
348
|
di.k = defaultValue(type_liaison, pt_di, de);
|
|
341
349
|
console.error(
|
|
@@ -30,7 +30,7 @@ vi.mock('./utils.js', () => ({
|
|
|
30
30
|
set_bug_for_bug_compat: vi.fn(),
|
|
31
31
|
tv: vi.fn(),
|
|
32
32
|
requestInput: (de, du, field) => de[field],
|
|
33
|
-
|
|
33
|
+
cleanReference: (reference) => reference,
|
|
34
34
|
get bug_for_bug_compat() {
|
|
35
35
|
return state.bug;
|
|
36
36
|
}
|
package/README.md
CHANGED
|
@@ -385,7 +385,7 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
|
|
|
385
385
|
|
|
386
386
|
<!-- CORPUS:START -->
|
|
387
387
|
|
|
388
|
-
> **Version `1.6.2`** · branche `main` · généré le 2026-09-
|
|
388
|
+
> **Version `1.6.2`** · branche `main` · généré le 2026-09-18
|
|
389
389
|
> Seuil de tolérance **5%**
|
|
390
390
|
|
|
391
391
|
<table>
|
|
@@ -411,6 +411,36 @@ Ces résultats sont **générés automatiquement** à la fin de `npm run test:co
|
|
|
411
411
|
|
|
412
412
|
<sub>🟢 ≥ 85 % · 🟡 ≥ 60 % · 🔴 < 60 %</sub>
|
|
413
413
|
|
|
414
|
+
#### Temps d’exécution
|
|
415
|
+
|
|
416
|
+
> Durée de l’appel à `calcul_3cl` par DPE, sur 89 956 calculs.
|
|
417
|
+
> La copie défensive de l’entrée et la lecture du fichier sont exclues de la mesure.
|
|
418
|
+
|
|
419
|
+
<table>
|
|
420
|
+
<tr>
|
|
421
|
+
<td align="center"><strong>5,61 ms</strong><br/><sub>moyenne</sub></td>
|
|
422
|
+
<td align="center"><strong>5,03 ms</strong><br/><sub>médiane</sub></td>
|
|
423
|
+
<td align="center"><strong>0,11 ms</strong><br/><sub>min</sub></td>
|
|
424
|
+
<td align="center"><strong>705,8 ms</strong><br/><sub>max</sub></td>
|
|
425
|
+
</tr>
|
|
426
|
+
</table>
|
|
427
|
+
|
|
428
|
+
| Corpus | Moyenne | Médiane | Min | Max | p95 | p99 |
|
|
429
|
+
| :--------------------------------------------- | ------: | ------: | ------: | -------: | ------: | ------: |
|
|
430
|
+
| **Généraliste** | 4,50 ms | 4,09 ms | 0,20 ms | 34,8 ms | 8,12 ms | 11,3 ms |
|
|
431
|
+
| **Appartement · chauffage individuel (2025)** | 3,71 ms | 3,46 ms | 1,32 ms | 26,1 ms | 6,22 ms | 7,99 ms |
|
|
432
|
+
| **Logement individuel (2025)** | 5,30 ms | 4,84 ms | 1,52 ms | 42,5 ms | 9,73 ms | 13,2 ms |
|
|
433
|
+
| **Maison individuelle (2025)** | 6,54 ms | 6,02 ms | 1,71 ms | 54,5 ms | 11,6 ms | 16,4 ms |
|
|
434
|
+
| **Immeuble · chauffage individuel** | 7,17 ms | 6,22 ms | 0,11 ms | 137,7 ms | 14,3 ms | 22,3 ms |
|
|
435
|
+
| **Appartement · chauffage collectif (2025)** | 3,98 ms | 3,76 ms | 1,22 ms | 37,6 ms | 6,35 ms | 8,00 ms |
|
|
436
|
+
| **Immeuble · chauffage collectif** | 6,40 ms | 5,58 ms | 0,15 ms | 101,0 ms | 12,1 ms | 21,2 ms |
|
|
437
|
+
| **Immeuble · chauffage mixte** | 6,01 ms | 5,01 ms | 0,22 ms | 705,8 ms | 12,4 ms | 19,8 ms |
|
|
438
|
+
| **Individuel généré depuis l'immeuble (2026)** | 6,89 ms | 6,27 ms | 1,52 ms | 65,1 ms | 13,0 ms | 19,7 ms |
|
|
439
|
+
|
|
440
|
+
<sub>La moyenne est tirée vers le haut par les DPE collectifs, dont le coût atteint plusieurs
|
|
441
|
+
dizaines de fois la médiane : c’est la médiane qui décrit le cas courant, et p95/p99 le cas
|
|
442
|
+
défavorable réel.</sub>
|
|
443
|
+
|
|
414
444
|
<!-- CORPUS:END -->
|
|
415
445
|
|
|
416
446
|
📈 Historique complet des versions : [docs/CORPUS-HISTORY.md](docs/CORPUS-HISTORY.md)
|
package/engine.js
CHANGED
|
@@ -33,7 +33,7 @@ import getFicheTechnique from './ficheTechnique.js';
|
|
|
33
33
|
import { ProductionENR } from './16.2_production_enr.js';
|
|
34
34
|
import DpeSanitizerService from './dpe-sanitizer.service.js';
|
|
35
35
|
|
|
36
|
-
const LIB_VERSION = '1.7.
|
|
36
|
+
const LIB_VERSION = '1.7.2';
|
|
37
37
|
|
|
38
38
|
const dpeSanitizerService = new DpeSanitizerService();
|
|
39
39
|
|
package/ficheTechnique.js
CHANGED
|
@@ -1,3 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recherche dans les fiches techniques d'un DPE.
|
|
3
|
+
*
|
|
4
|
+
* L'implémentation précédente rebalayait la totalité des fiches à chaque appel, concaténait les
|
|
5
|
+
* sous-fiches dans un `reduce` (`acc.concat(...)` recopie l'accumulateur à chaque itération) et
|
|
6
|
+
* remettait en minuscules des valeurs qui ne changent jamais. Le coût par appel croissait en
|
|
7
|
+
* `fiches^1,3`.
|
|
8
|
+
*
|
|
9
|
+
* Sur un DPE de logement individuel, une poignée de fiches : invisible. Sur un DPE collectif,
|
|
10
|
+
* les fiches suivent les installations — un DPE mesuré comptait 235 fiches et 1597 sous-fiches
|
|
11
|
+
* pour 73 générateurs — et la fonction représentait alors **54 % du temps de `calcul_3cl`**,
|
|
12
|
+
* le ramasse-miettes des concaténations comptant pour 10 % de plus.
|
|
13
|
+
*
|
|
14
|
+
* Les fiches techniques ne sont jamais modifiées pendant un calcul : elles sont donc indexées
|
|
15
|
+
* une fois par DPE, par catégorie, avec les minuscules pré-calculées. L'index est mémorisé dans
|
|
16
|
+
* une `WeakMap` sur le DPE lui-même, donc libéré avec lui — rien à vider, aucune fuite possible
|
|
17
|
+
* sur un process qui enchaîne les calculs.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** @type {WeakMap<object, Map<string, {sousFiches: any[], valeursMinuscules: (string|null)[], champsMinuscules: Map<string, (string|null)[]>}[]>>} */
|
|
21
|
+
const indexParDpe = new WeakMap();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Regroupe les sous-fiches par catégorie, en conservant l'ordre du document — l'ordre détermine
|
|
25
|
+
* quelle correspondance est retournée en premier.
|
|
26
|
+
*
|
|
27
|
+
* @param dpe {FullDpe}
|
|
28
|
+
*/
|
|
29
|
+
function indexerFichesTechniques(dpe) {
|
|
30
|
+
const parCategorie = new Map();
|
|
31
|
+
|
|
32
|
+
/** @type {FicheTechniqueItem[]} */
|
|
33
|
+
let fichesTechniques = dpe.fiche_technique_collection?.fiche_technique || [];
|
|
34
|
+
if (!Array.isArray(fichesTechniques)) {
|
|
35
|
+
fichesTechniques = [fichesTechniques];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const ficheTechnique of fichesTechniques) {
|
|
39
|
+
if (!ficheTechnique) continue;
|
|
40
|
+
|
|
41
|
+
const categorie = ficheTechnique.enum_categorie_fiche_technique_id;
|
|
42
|
+
let groupes = parCategorie.get(categorie);
|
|
43
|
+
if (!groupes) {
|
|
44
|
+
groupes = [];
|
|
45
|
+
parCategorie.set(categorie, groupes);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const brut = ficheTechnique.sous_fiche_technique_collection?.sous_fiche_technique;
|
|
49
|
+
const sousFiches = brut === undefined ? [] : Array.isArray(brut) ? brut : [brut];
|
|
50
|
+
|
|
51
|
+
groupes.push({
|
|
52
|
+
sousFiches,
|
|
53
|
+
// `valeur` en minuscules, calculée une fois ; null quand ce n'est pas une chaîne, auquel
|
|
54
|
+
// cas la comparaison se fait par identité stricte comme avant.
|
|
55
|
+
valeursMinuscules: sousFiches.map((sousFiche) =>
|
|
56
|
+
typeof sousFiche?.valeur === 'string' ? sousFiche.valeur.toLowerCase() : null
|
|
57
|
+
),
|
|
58
|
+
// Les autres champs sont mis en minuscules à la demande : seuls `description` et `valeur`
|
|
59
|
+
// sont interrogés en pratique, inutile de préparer le reste.
|
|
60
|
+
champsMinuscules: new Map()
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return parCategorie;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param groupe {{sousFiches: any[], champsMinuscules: Map<string, (string|null)[]>}}
|
|
69
|
+
* @param field {string}
|
|
70
|
+
* @returns {(string|null)[]}
|
|
71
|
+
*/
|
|
72
|
+
function champEnMinuscules(groupe, field) {
|
|
73
|
+
let valeurs = groupe.champsMinuscules.get(field);
|
|
74
|
+
if (valeurs === undefined) {
|
|
75
|
+
valeurs = groupe.sousFiches.map((sousFiche) =>
|
|
76
|
+
sousFiche && sousFiche[field] ? sousFiche[field].toString().toLowerCase() : null
|
|
77
|
+
);
|
|
78
|
+
groupe.champsMinuscules.set(field, valeurs);
|
|
79
|
+
}
|
|
80
|
+
return valeurs;
|
|
81
|
+
}
|
|
82
|
+
|
|
1
83
|
/**
|
|
2
84
|
* Retourne si elle existe la fiche technique contenant le texte description et
|
|
3
85
|
* présente dans la catégorie de fiches techniques ayant pour catégorie categoryFicheTechiqueId
|
|
@@ -15,72 +97,70 @@ export default function getFicheTechnique(
|
|
|
15
97
|
classifications = [],
|
|
16
98
|
field = 'description'
|
|
17
99
|
) {
|
|
18
|
-
|
|
19
|
-
|
|
100
|
+
let index = indexParDpe.get(dpe);
|
|
101
|
+
if (index === undefined) {
|
|
102
|
+
index = indexerFichesTechniques(dpe);
|
|
103
|
+
indexParDpe.set(dpe, index);
|
|
104
|
+
}
|
|
20
105
|
|
|
21
|
-
|
|
22
|
-
|
|
106
|
+
const groupes = index.get(categoryFicheTechiqueId);
|
|
107
|
+
if (!groupes) {
|
|
108
|
+
return null;
|
|
23
109
|
}
|
|
24
110
|
|
|
25
111
|
classifications = classifications.filter((classification) => classification);
|
|
112
|
+
const aiguilles = classifications.map((classification) =>
|
|
113
|
+
classification.toString().toLowerCase()
|
|
114
|
+
);
|
|
115
|
+
const descriptionMinuscule = description.toLowerCase();
|
|
26
116
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (classifications.length) {
|
|
36
|
-
/** @type {SousFicheTechniqueItem[]} */
|
|
37
|
-
let sousFichesTechniques =
|
|
38
|
-
ficheTechnique.sous_fiche_technique_collection.sous_fiche_technique;
|
|
39
|
-
|
|
40
|
-
if (!Array.isArray(sousFichesTechniques)) {
|
|
41
|
-
sousFichesTechniques = [sousFichesTechniques];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const allContained = classifications.every((classification) =>
|
|
45
|
-
sousFichesTechniques.some((ficheTechnique) => {
|
|
46
|
-
const valeur = ficheTechnique?.valeur;
|
|
47
|
-
|
|
48
|
-
if (typeof valeur === 'string') {
|
|
49
|
-
return (
|
|
50
|
-
ficheTechnique.valeur
|
|
51
|
-
.toLowerCase()
|
|
52
|
-
.indexOf(classification.toString().toLowerCase()) !== -1
|
|
53
|
-
);
|
|
54
|
-
}
|
|
117
|
+
for (const groupe of groupes) {
|
|
118
|
+
/**
|
|
119
|
+
* Plusieurs collections de fiches techniques peuvent exister pour la même catégorie (pour 2
|
|
120
|
+
* systèmes ECS par exemple). Le champ classification permet de trouver la collection qui
|
|
121
|
+
* convient en filtrant sur une seconde donnée.
|
|
122
|
+
*/
|
|
123
|
+
if (classifications.length) {
|
|
124
|
+
let toutesPresentes = true;
|
|
55
125
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
126
|
+
for (let i = 0; i < classifications.length; i++) {
|
|
127
|
+
const aiguille = aiguilles[i];
|
|
128
|
+
let presente = false;
|
|
59
129
|
|
|
60
|
-
|
|
61
|
-
|
|
130
|
+
for (let j = 0; j < groupe.sousFiches.length; j++) {
|
|
131
|
+
const valeurMinuscule = groupe.valeursMinuscules[j];
|
|
132
|
+
if (valeurMinuscule !== null) {
|
|
133
|
+
if (valeurMinuscule.indexOf(aiguille) !== -1) {
|
|
134
|
+
presente = true;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
} else if (groupe.sousFiches[j].valeur === classifications[i]) {
|
|
138
|
+
presente = true;
|
|
139
|
+
break;
|
|
62
140
|
}
|
|
63
141
|
}
|
|
64
142
|
|
|
65
|
-
|
|
143
|
+
if (!presente) {
|
|
144
|
+
toutesPresentes = false;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
66
147
|
}
|
|
67
|
-
return acc;
|
|
68
|
-
}, []);
|
|
69
|
-
|
|
70
|
-
if (!fichesTechniques.length) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
148
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
ficheTechnique &&
|
|
77
|
-
ficheTechnique[field] &&
|
|
78
|
-
ficheTechnique[field].toString().toLowerCase().indexOf(description.toLowerCase()) !== -1
|
|
79
|
-
);
|
|
149
|
+
if (!toutesPresentes) continue;
|
|
150
|
+
}
|
|
80
151
|
|
|
81
|
-
|
|
82
|
-
|
|
152
|
+
// L'ancienne implémentation aplatissait toutes les catégories correspondantes avant de
|
|
153
|
+
// filtrer : la première correspondance du tableau aplati est la première correspondance du
|
|
154
|
+
// premier groupe qui en contient une. Parcourir les groupes dans l'ordre donne le même
|
|
155
|
+
// résultat, sans construire le tableau intermédiaire.
|
|
156
|
+
const champs = champEnMinuscules(groupe, field);
|
|
157
|
+
for (let j = 0; j < groupe.sousFiches.length; j++) {
|
|
158
|
+
const champ = champs[j];
|
|
159
|
+
if (champ !== null && champ.indexOf(descriptionMinuscule) !== -1) {
|
|
160
|
+
return groupe.sousFiches[j];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
83
163
|
}
|
|
84
164
|
|
|
85
|
-
return
|
|
165
|
+
return null;
|
|
86
166
|
}
|
package/ficheTechnique.spec.js
CHANGED
|
@@ -188,21 +188,53 @@ describe('FicheTechnique service tests', () => {
|
|
|
188
188
|
|
|
189
189
|
fiche = getFicheTechnique(dpe, '10', 'non existing');
|
|
190
190
|
expect(fiche).toBeNull();
|
|
191
|
+
});
|
|
191
192
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Les fiches techniques sont indexées une fois par objet DPE : chaque cas utilise donc un DPE
|
|
195
|
+
* neuf, muter un DPE déjà interrogé n'aurait aucun effet.
|
|
196
|
+
*/
|
|
197
|
+
describe('fiches techniques absentes ou incomplètes', () => {
|
|
198
|
+
const buildDpe = (ficheTechnique) => ({
|
|
199
|
+
fiche_technique_collection: { fiche_technique: ficheTechnique }
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test.each([
|
|
203
|
+
['sans fiche_technique_collection', {}],
|
|
204
|
+
['avec fiche_technique null', buildDpe(null)],
|
|
205
|
+
['avec une fiche technique null', buildDpe([null])],
|
|
206
|
+
[
|
|
207
|
+
'sans sous_fiche_technique_collection',
|
|
208
|
+
buildDpe([
|
|
209
|
+
{ enum_categorie_fiche_technique_id: '10', sous_fiche_technique_collection: null }
|
|
210
|
+
])
|
|
211
|
+
],
|
|
212
|
+
[
|
|
213
|
+
'avec des sous-fiches null ou sans description',
|
|
214
|
+
buildDpe([
|
|
215
|
+
{
|
|
216
|
+
enum_categorie_fiche_technique_id: '10',
|
|
217
|
+
sous_fiche_technique_collection: {
|
|
218
|
+
sous_fiche_technique: [null, { valeur: 'plusieurs' }]
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
])
|
|
222
|
+
]
|
|
223
|
+
])('retourne null %s', (_, dpeIncomplet) => {
|
|
224
|
+
expect(getFicheTechnique(dpeIncomplet, '10', 'exposées')).toBeNull();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('ignore les fiches techniques null au milieu des autres', () => {
|
|
228
|
+
const fiche = { description: 'Façades exposées: plusieurs', valeur: 'plusieurs' };
|
|
229
|
+
const dpeAvecTrou = buildDpe([
|
|
230
|
+
null,
|
|
231
|
+
{
|
|
232
|
+
enum_categorie_fiche_technique_id: '10',
|
|
233
|
+
sous_fiche_technique_collection: { sous_fiche_technique: [fiche] }
|
|
234
|
+
}
|
|
235
|
+
]);
|
|
204
236
|
|
|
205
|
-
|
|
206
|
-
|
|
237
|
+
expect(getFicheTechnique(dpeAvecTrou, '10', 'exposées')).toBe(fiche);
|
|
238
|
+
});
|
|
207
239
|
});
|
|
208
240
|
});
|