@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
package/formula.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Évaluation des formules tableur présentes dans les tables de valeurs.
|
|
3
|
+
*
|
|
4
|
+
* Remplace `mathjs` — dont seul `evaluate()` était utilisé, pour une grammaire très réduite —
|
|
5
|
+
* par un analyseur dédié. mathjs tirait 113 modules (mathjs, decimal.js, fraction.js,
|
|
6
|
+
* typed-function, seedrandom, @babel/runtime) et ~500 ms d'initialisation, et re-parsait la
|
|
7
|
+
* chaîne de la formule à chaque appel.
|
|
8
|
+
*
|
|
9
|
+
* Grammaire supportée, alignée sur ce que mathjs acceptait pour ces formules :
|
|
10
|
+
* - nombres décimaux, séparateur `.` ou `,` (`0,085` ≡ `0.085`)
|
|
11
|
+
* - pourcentages : `0.6%` vaut 0.006
|
|
12
|
+
* - variables : `Pn`, `logPn`, `E`, `F`
|
|
13
|
+
* - fonction : `log10(x)`
|
|
14
|
+
* - opérateurs : `+` `-` `*` `/` `^` et parenthèses
|
|
15
|
+
* - multiplication implicite : `2 logPn` ≡ `2 * logPn`
|
|
16
|
+
*
|
|
17
|
+
* `^` est associatif à droite et lie plus fort que le moins unaire (`-2^2` vaut -4,
|
|
18
|
+
* `2^-1` vaut 0.5), comme dans mathjs.
|
|
19
|
+
*
|
|
20
|
+
* Chaque formule n'est analysée qu'une fois : le résultat est une fermeture mise en cache.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const VARIABLES = ['Pn', 'logPn', 'E', 'F'];
|
|
24
|
+
const FUNCTIONS = { log10: Math.log10 };
|
|
25
|
+
|
|
26
|
+
/* --------------------------------------------------------------------------------------- */
|
|
27
|
+
/* Analyse lexicale */
|
|
28
|
+
/* --------------------------------------------------------------------------------------- */
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param source {string}
|
|
32
|
+
* @returns {{type: string, value: *}[]}
|
|
33
|
+
*/
|
|
34
|
+
function tokenize(source) {
|
|
35
|
+
const tokens = [];
|
|
36
|
+
let i = 0;
|
|
37
|
+
|
|
38
|
+
while (i < source.length) {
|
|
39
|
+
const c = source[i];
|
|
40
|
+
|
|
41
|
+
if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
|
|
42
|
+
i += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (c >= '0' && c <= '9') {
|
|
47
|
+
let j = i;
|
|
48
|
+
while (j < source.length && source[j] >= '0' && source[j] <= '9') j += 1;
|
|
49
|
+
if (source[j] === '.' || source[j] === ',') {
|
|
50
|
+
j += 1;
|
|
51
|
+
while (j < source.length && source[j] >= '0' && source[j] <= '9') j += 1;
|
|
52
|
+
}
|
|
53
|
+
let value = Number(source.slice(i, j).replace(',', '.'));
|
|
54
|
+
i = j;
|
|
55
|
+
// Un `%` suffixant un nombre en fait une fraction : `0.6%` → 0.006.
|
|
56
|
+
while (i < source.length && (source[i] === ' ' || source[i] === '\t')) i += 1;
|
|
57
|
+
if (source[i] === '%') {
|
|
58
|
+
value /= 100;
|
|
59
|
+
i += 1;
|
|
60
|
+
}
|
|
61
|
+
tokens.push({ type: 'number', value });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (/[A-Za-z_]/.test(c)) {
|
|
66
|
+
let j = i;
|
|
67
|
+
while (j < source.length && /[A-Za-z0-9_]/.test(source[j])) j += 1;
|
|
68
|
+
tokens.push({ type: 'name', value: source.slice(i, j) });
|
|
69
|
+
i = j;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if ('+-*/^()'.includes(c)) {
|
|
74
|
+
tokens.push({ type: c, value: c });
|
|
75
|
+
i += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new SyntaxError(`caractère inattendu \`${c}\` dans la formule \`${source}\``);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return tokens;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* --------------------------------------------------------------------------------------- */
|
|
86
|
+
/* Analyse syntaxique — descente récursive, produit une fermeture (scope) => number */
|
|
87
|
+
/* --------------------------------------------------------------------------------------- */
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param source {string}
|
|
91
|
+
* @returns {(scope: {Pn: number, logPn: number, E: number, F: number}) => number}
|
|
92
|
+
*/
|
|
93
|
+
function parse(source) {
|
|
94
|
+
const tokens = tokenize(source);
|
|
95
|
+
let pos = 0;
|
|
96
|
+
|
|
97
|
+
const peek = () => tokens[pos];
|
|
98
|
+
const eat = (type) => {
|
|
99
|
+
if (!tokens[pos] || tokens[pos].type !== type) {
|
|
100
|
+
throw new SyntaxError(`\`${type}\` attendu dans la formule \`${source}\``);
|
|
101
|
+
}
|
|
102
|
+
return tokens[pos++];
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** Un jeton qui peut démarrer un facteur — sert à détecter la multiplication implicite. */
|
|
106
|
+
const startsFactor = () => {
|
|
107
|
+
const t = peek();
|
|
108
|
+
return !!t && (t.type === 'number' || t.type === 'name' || t.type === '(');
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function parseExpression() {
|
|
112
|
+
let left = parseTerm();
|
|
113
|
+
while (peek() && (peek().type === '+' || peek().type === '-')) {
|
|
114
|
+
const op = tokens[pos++].type;
|
|
115
|
+
const right = parseTerm();
|
|
116
|
+
const l = left;
|
|
117
|
+
left = op === '+' ? (s) => l(s) + right(s) : (s) => l(s) - right(s);
|
|
118
|
+
}
|
|
119
|
+
return left;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseTerm() {
|
|
123
|
+
let left = parseUnary();
|
|
124
|
+
for (;;) {
|
|
125
|
+
const t = peek();
|
|
126
|
+
if (t && (t.type === '*' || t.type === '/')) {
|
|
127
|
+
const op = tokens[pos++].type;
|
|
128
|
+
const right = parseUnary();
|
|
129
|
+
const l = left;
|
|
130
|
+
left = op === '*' ? (s) => l(s) * right(s) : (s) => l(s) / right(s);
|
|
131
|
+
} else if (startsFactor()) {
|
|
132
|
+
// multiplication implicite : `2 logPn`
|
|
133
|
+
const right = parseUnary();
|
|
134
|
+
const l = left;
|
|
135
|
+
left = (s) => l(s) * right(s);
|
|
136
|
+
} else {
|
|
137
|
+
return left;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseUnary() {
|
|
143
|
+
const t = peek();
|
|
144
|
+
if (t && (t.type === '+' || t.type === '-')) {
|
|
145
|
+
pos += 1;
|
|
146
|
+
const operand = parseUnary();
|
|
147
|
+
return t.type === '-' ? (s) => -operand(s) : operand;
|
|
148
|
+
}
|
|
149
|
+
return parsePower();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parsePower() {
|
|
153
|
+
const base = parsePrimary();
|
|
154
|
+
if (peek() && peek().type === '^') {
|
|
155
|
+
pos += 1;
|
|
156
|
+
// associatif à droite, et l'exposant peut être signé : `(Pn)^-0.4`
|
|
157
|
+
const exponent = parseUnary();
|
|
158
|
+
return (s) => base(s) ** exponent(s);
|
|
159
|
+
}
|
|
160
|
+
return base;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parsePrimary() {
|
|
164
|
+
const t = peek();
|
|
165
|
+
if (!t) throw new SyntaxError(`formule incomplète : \`${source}\``);
|
|
166
|
+
|
|
167
|
+
if (t.type === 'number') {
|
|
168
|
+
pos += 1;
|
|
169
|
+
const { value } = t;
|
|
170
|
+
return () => value;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (t.type === '(') {
|
|
174
|
+
pos += 1;
|
|
175
|
+
const inner = parseExpression();
|
|
176
|
+
eat(')');
|
|
177
|
+
return inner;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (t.type === 'name') {
|
|
181
|
+
pos += 1;
|
|
182
|
+
const name = t.value;
|
|
183
|
+
|
|
184
|
+
if (peek() && peek().type === '(') {
|
|
185
|
+
const fn = FUNCTIONS[name];
|
|
186
|
+
if (!fn) throw new SyntaxError(`fonction inconnue \`${name}\` dans \`${source}\``);
|
|
187
|
+
pos += 1;
|
|
188
|
+
const arg = parseExpression();
|
|
189
|
+
eat(')');
|
|
190
|
+
return (s) => fn(arg(s));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!VARIABLES.includes(name)) {
|
|
194
|
+
throw new SyntaxError(`variable inconnue \`${name}\` dans \`${source}\``);
|
|
195
|
+
}
|
|
196
|
+
return (s) => s[name];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
throw new SyntaxError(`jeton inattendu \`${t.value}\` dans \`${source}\``);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const compiled = parseExpression();
|
|
203
|
+
if (pos !== tokens.length) {
|
|
204
|
+
throw new SyntaxError(`fin de formule inattendue dans \`${source}\``);
|
|
205
|
+
}
|
|
206
|
+
return compiled;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* --------------------------------------------------------------------------------------- */
|
|
210
|
+
/* API */
|
|
211
|
+
/* --------------------------------------------------------------------------------------- */
|
|
212
|
+
|
|
213
|
+
const compiledFormulas = new Map();
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Compile une formule (ou la récupère depuis le cache).
|
|
217
|
+
*
|
|
218
|
+
* @param formula {string}
|
|
219
|
+
* @returns {(scope: {Pn: number, logPn: number, E: number, F: number}) => number}
|
|
220
|
+
*/
|
|
221
|
+
export function compileFormula(formula) {
|
|
222
|
+
let compiled = compiledFormulas.get(formula);
|
|
223
|
+
if (compiled === undefined) {
|
|
224
|
+
compiled = parse(formula);
|
|
225
|
+
compiledFormulas.set(formula, compiled);
|
|
226
|
+
}
|
|
227
|
+
return compiled;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Évalue une formule tableur, ou retourne la valeur telle quelle si c'en est déjà une.
|
|
232
|
+
*
|
|
233
|
+
* @param formulaOrValue {string|number} Formule tableur ou valeur numérique.
|
|
234
|
+
* @param pn {number} Puissance nominale en W (exposée à la formule sous `Pn`, en kW).
|
|
235
|
+
* @param E {number} [Coefficient forfaitaire E]
|
|
236
|
+
* @param F {number} [Coefficient forfaitaire F]
|
|
237
|
+
* @returns {number}
|
|
238
|
+
*/
|
|
239
|
+
export function evaluateFormula(formulaOrValue, pn, E, F) {
|
|
240
|
+
const formula =
|
|
241
|
+
typeof formulaOrValue === 'string'
|
|
242
|
+
? formulaOrValue.replace(/(\d),(\d)/g, '$1.$2')
|
|
243
|
+
: formulaOrValue;
|
|
244
|
+
|
|
245
|
+
if (!isNaN(formula)) {
|
|
246
|
+
return Number(formula);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const Pn = pn / 1000;
|
|
250
|
+
return compileFormula(formula)({ Pn, logPn: Math.log10(Pn), E, F });
|
|
251
|
+
}
|
package/formula.spec.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { compileFormula, evaluateFormula } from './formula.js';
|
|
3
|
+
|
|
4
|
+
describe('formula', () => {
|
|
5
|
+
describe('evaluateFormula', () => {
|
|
6
|
+
test.each([
|
|
7
|
+
[42, 42],
|
|
8
|
+
['42', 42],
|
|
9
|
+
['0.5', 0.5],
|
|
10
|
+
['12,5', 12.5]
|
|
11
|
+
])('retourne la valeur numérique telle quelle : %s → %s', (input, expected) => {
|
|
12
|
+
expect(evaluateFormula(input, 24000)).toBe(expected);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('expose Pn en kW à partir de pn en W', () => {
|
|
16
|
+
expect(evaluateFormula('Pn', 24000)).toBe(24);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('expose logPn = log10(Pn)', () => {
|
|
20
|
+
expect(evaluateFormula('logPn', 100000)).toBeCloseTo(2, 10);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('expose les coefficients E et F', () => {
|
|
24
|
+
expect(evaluateFormula('E + F * logPn', 100000, 3, 1.5)).toBeCloseTo(6, 10);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('accepte la virgule comme séparateur décimal', () => {
|
|
28
|
+
expect(evaluateFormula('0,085 * Pn', 10000)).toBeCloseTo(0.85, 10);
|
|
29
|
+
expect(evaluateFormula('84,5 + 2 logPn', 100000)).toBeCloseTo(88.5, 10);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('évalue des formules représentatives des tables de valeurs', () => {
|
|
33
|
+
// rendement à pleine charge d'une chaudière : 84 + 2 logPn
|
|
34
|
+
expect(evaluateFormula('84 + 2 logPn', 100000)).toBeCloseTo(88, 10);
|
|
35
|
+
// pertes à l'arrêt : exposant négatif signé
|
|
36
|
+
expect(evaluateFormula('2.5 * (Pn)^-0.4', 32000)).toBeCloseTo(2.5 * 32 ** -0.4, 10);
|
|
37
|
+
// pourcentage
|
|
38
|
+
expect(evaluateFormula('0.6% * Pn', 50000)).toBeCloseTo(0.3, 10);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('compileFormula', () => {
|
|
43
|
+
const evaluate = (formula, scope = {}) => compileFormula(formula)(scope);
|
|
44
|
+
|
|
45
|
+
test.each([
|
|
46
|
+
['2 + 3 * 4', 14],
|
|
47
|
+
['(2 + 3) * 4', 20],
|
|
48
|
+
['10 / 4', 2.5],
|
|
49
|
+
['8 - 3 - 2', 3],
|
|
50
|
+
['16 / 4 / 2', 2],
|
|
51
|
+
['2^3^2', 512],
|
|
52
|
+
['-2^2', -4],
|
|
53
|
+
['2^-1', 0.5],
|
|
54
|
+
['-(3 - 5)', 2],
|
|
55
|
+
['+3', 3],
|
|
56
|
+
['--3', 3],
|
|
57
|
+
['log10(1000)', 3],
|
|
58
|
+
['log10(10 * 10)', 2],
|
|
59
|
+
['2(3 + 1)', 8],
|
|
60
|
+
['2 log10(100)', 4],
|
|
61
|
+
['0.6%', 0.006],
|
|
62
|
+
['50 %', 0.5],
|
|
63
|
+
['0,5 + 1', 1.5],
|
|
64
|
+
[' 1 +\t2\n', 3]
|
|
65
|
+
])('%s → %s', (formula, expected) => {
|
|
66
|
+
expect(evaluate(formula)).toBeCloseTo(expected, 10);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('lit les variables dans le scope', () => {
|
|
70
|
+
const scope = { Pn: 20, logPn: Math.log10(20), E: 2, F: 3 };
|
|
71
|
+
expect(evaluate('Pn', scope)).toBe(20);
|
|
72
|
+
expect(evaluate('E F', scope)).toBe(6);
|
|
73
|
+
expect(evaluate('E + F Pn', scope)).toBe(62);
|
|
74
|
+
expect(evaluate('logPn', scope)).toBeCloseTo(Math.log10(20), 10);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('met en cache la formule compilée', () => {
|
|
78
|
+
const first = compileFormula('Pn * 2 + 1');
|
|
79
|
+
expect(compileFormula('Pn * 2 + 1')).toBe(first);
|
|
80
|
+
expect(first({ Pn: 3 })).toBe(7);
|
|
81
|
+
expect(first({ Pn: 5 })).toBe(11);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test.each([
|
|
85
|
+
['2 $ 3', /caractère inattendu `\$`/],
|
|
86
|
+
['foo + 1', /variable inconnue `foo`/],
|
|
87
|
+
['sqrt(4)', /fonction inconnue `sqrt`/],
|
|
88
|
+
['(2 + 3', /`\)` attendu/],
|
|
89
|
+
['log10(2', /`\)` attendu/],
|
|
90
|
+
['2 +', /formule incomplète/],
|
|
91
|
+
['', /formule incomplète/],
|
|
92
|
+
['2 )', /fin de formule inattendue/],
|
|
93
|
+
['* 2', /jeton inattendu `\*`/]
|
|
94
|
+
])('rejette la formule invalide `%s`', (formula, message) => {
|
|
95
|
+
expect(() => compileFormula(formula)).toThrow(SyntaxError);
|
|
96
|
+
expect(() => compileFormula(formula)).toThrow(message);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open3cl/engine",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "Open Source 3CL-DPE engine",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|
|
@@ -42,11 +42,8 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"dioma": "^0.4.6",
|
|
44
44
|
"fast-xml-parser": "^5.11.1",
|
|
45
|
-
"http-server": "^14.1.1",
|
|
46
45
|
"lodash-es": "^4.18.1",
|
|
47
|
-
"
|
|
48
|
-
"winston": "^3.19.0",
|
|
49
|
-
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
|
46
|
+
"winston": "^3.19.0"
|
|
50
47
|
},
|
|
51
48
|
"devDependencies": {
|
|
52
49
|
"@aws-sdk/client-s3": "^3.1013.0",
|
|
@@ -71,13 +68,15 @@
|
|
|
71
68
|
"eslint-plugin-n": "^16.6.2",
|
|
72
69
|
"eslint-plugin-promise": "^6.1.1",
|
|
73
70
|
"fast-csv": "^5.0.2",
|
|
71
|
+
"http-server": "^14.1.1",
|
|
74
72
|
"husky": "^9.0.11",
|
|
75
73
|
"jscpd": "^3.5.10",
|
|
76
74
|
"piscina": "^5.1.2",
|
|
77
75
|
"prettier": "^3.2.5",
|
|
78
76
|
"pretty-quick": "^4.0.0",
|
|
79
77
|
"semantic-release": "^25.0.2",
|
|
80
|
-
"vitest": "3.2.7"
|
|
78
|
+
"vitest": "3.2.7",
|
|
79
|
+
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
|
81
80
|
},
|
|
82
81
|
"author": "Open3Cl",
|
|
83
82
|
"license": "GPL-3.0",
|