• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hetalang / heta-compiler / 15041857680

15 May 2025 09:45AM UTC coverage: 67.308% (-0.6%) from 67.859%
15041857680

push

github

metelkin
feat: add useCSwitcher in SBML module

1805 of 2820 branches covered (64.01%)

Branch coverage included in aggregate %.

5 of 31 new or added lines in 2 files covered. (16.13%)

3091 of 4454 relevant lines covered (69.4%)

83.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

51.63
/src/module-system/to-math-expr.js
1

2
/*
3
  Transformation of json formatted <math> element to expression string.
4
  
5
  useParentheses = true is used when operators may require explicit parentheses (...)
6
*/
7

8
const HetaLevelError = require('../heta-level-error');
1✔
9

10
function _toMathExpr(element, useParentheses = false) {
83✔
11
  if (element.name === 'math') {
216✔
12
    return _toMathExpr(element.elements[0]);
40✔
13
  } else if(element.name === 'apply' && element.elements[0].name === 'gt') {
176✔
14
    let one = _toMathExpr(element.elements[1], true);
5✔
15
    let two = _toMathExpr(element.elements[2], true);
5✔
16
    return `${one} > ${two}`;
5✔
17
  } else if(element.name === 'apply' && element.elements[0].name === 'geq') {
171!
18
    let one = _toMathExpr(element.elements[1], true);
×
19
    let two = _toMathExpr(element.elements[2], true);
×
20
    return `${one} >= ${two}`;
×
21
  } else if(element.name === 'apply' && element.elements[0].name === 'eq') {
171!
22
    let one = _toMathExpr(element.elements[1], true);
×
23
    let two = _toMathExpr(element.elements[2], true);
×
24
    return `${one} == ${two}`;
×
25
  } else if(element.name === 'apply' && element.elements[0].name === 'lt') {
171✔
26
    let one = _toMathExpr(element.elements[1], true);
1✔
27
    let two = _toMathExpr(element.elements[2], true);
1✔
28
    return `${one} < ${two}`;
1✔
29
  } else if(element.name === 'apply' && element.elements[0].name === 'leq') {
170!
30
    let one = _toMathExpr(element.elements[1], true);
×
31
    let two = _toMathExpr(element.elements[2], true);
×
32
    return `${one} <= ${two}`;
×
33
  } else if(element.name === 'apply' && element.elements[0].name === 'neq') {
170!
34
    let one = _toMathExpr(element.elements[1], true);
×
35
    let two = _toMathExpr(element.elements[2], true);
×
36
    return `${one} != ${two}`;
×
37
  } else if(element.name === 'apply' && element.elements[0].name === 'and') {
170!
38
    let args = element.elements.slice(1)
×
39
      .map((x) => _toMathExpr(x, true)).join(' and ');
×
40
    return args;
×
41
  } else if(element.name === 'apply' && element.elements[0].name === 'or') {
170!
42
    let args = element.elements.slice(1)
×
43
      .map((x) => _toMathExpr(x, true)).join(' or ');
×
44
    return args;
×
45
  } else if(element.name === 'apply' && element.elements[0].name === 'xor') {
170!
46
    let args = element.elements.slice(1)
×
47
      .map((x) => _toMathExpr(x, true)).join(' xor ');
×
48
    return args;
×
49
  } else if(element.name === 'apply' && element.elements[0].name === 'not') {
170!
50
    let one = _toMathExpr(element.elements[1], true);
×
51
    return `not ${one}`;
×
52
  } else if(element.name === 'apply' && element.elements[0].name === 'times') {
170✔
53
    // A * B * C, <times>
54
    let expr = element.elements.slice(1) // without first element
9✔
55
      .map((x) => _toMathExpr(x, true)).join(' * '); 
20✔
56
    return useParentheses ? `(${expr})` : expr;
9✔
57
  } else if(element.name === 'apply' && element.elements[0].name === 'divide') {
161✔
58
    // A / B, <divide> for two arguments
59
    let args = element.elements.slice(1)
8✔
60
      .map((x) => _toMathExpr(x, true));
17✔
61
    return args[0] + ' / ' + args[1]; 
8✔
62
  } else if(element.name === 'apply' && element.elements[0].name === 'minus' && element.elements.length === 2) {
153✔
63
    // -A, <minus> for one argument
64
    let arg1 = element.elements[1];
6✔
65
    let expr = '-' + _toMathExpr(arg1, true);
6✔
66
    return `(${expr})`; // () cannot be skipped in 2d, 3d,... place of sum
6✔
67
  } else if(element.name === 'apply' && element.elements[0].name === 'minus') {
147✔
68
    // A - B, <minus> for two argumets
69
    let arg0 = _toMathExpr(element.elements[1], false); // skip ()
7✔
70
    let arg1 = _toMathExpr(element.elements[2], true);
7✔
71
    let expr = arg0 + ' - ' + arg1;
7✔
72
    return useParentheses ? `(${expr})` : expr;
7!
73
  } else if(element.name === 'apply' && element.elements[0].name === 'plus') {
140✔
74
    // A + B + C, <plus>
75
    let expr = element.elements.slice(1)
22✔
76
      .map((x) => _toMathExpr(x, false)).join(' + '); // skip ()
52✔
77
    return useParentheses ? `(${expr})` : expr;
22✔
78
  } else if(element.name === 'apply' && element.elements[0].name === 'power') {
118✔
79
    let expr = element.elements.slice(1)
1✔
80
      .map((x) => _toMathExpr(x)).join(', '); // skip ()
2✔
81
    return `pow(${expr})`;
1✔
82
  } else if(element.name === 'apply' && element.elements[0].name === 'ceiling') {
117!
83
    let args = element.elements.slice(1)
×
84
      .map((x) => _toMathExpr(x)); // skip ()
×
85
    return `ceil(${args[0]})`;
×
86
  } else if(element.name === 'apply' && element.elements[0].name === 'root') {
117!
87
    let degree = element.elements
×
88
      .find(y => y.name === 'degree');
×
89
    let args = element.elements.slice(1)
×
90
      .filter((x) => x.name !== 'degree')
×
91
      .map((x) => _toMathExpr(x)); // skip ()
×
92
    if (degree) {
×
93
      let n_element = degree.elements[0];
×
94
      let n = _toMathExpr(n_element, true);
×
95
      return `pow(${args[0]}, 1.0/${n})`;
×
96
    } else {
97
      return `sqrt(${args[0]})`;
×
98
    }
99
  } else if(element.name === 'apply' && element.elements[0].name === 'ln') {
117!
100
    let expr = element.elements.slice(1)
×
101
      .map((x) => _toMathExpr(x));  // skip ()
×
102
    return `ln(${expr[0]})`;
×
103
  } else if(element.name === 'apply' && element.elements[0].name === 'log') {
117!
104
    let logbase = element.elements
×
105
      .find(y => y.name === 'logbase');
×
106
    let expr = element.elements.slice(1)
×
107
      .filter((x) => x.name !== 'logbase')
×
108
      .map((x) => _toMathExpr(x)); // skip ()
×
109
    if (logbase === undefined) {
×
110
      return `log10(${expr[0]})`;
×
111
    } else if (logbase.elements[0]?.elements[0]?.text === '2') {
×
112
      return `log2(${expr[0]})`;
×
113
    } else {
114
      let base = _toMathExpr(logbase.elements[0]); // skip ()
×
115
      return `logbase(${expr[0]}, ${base})`;
×
116
    }
117
  // === trigonometry ===
118
  } else if (element.name === 'apply' && element.elements[0].name === 'arcsin') {
117!
119
    let arg = _toMathExpr(element.elements[1]);
×
120
    return `asin(${arg})`;
×
121
  } else if (element.name === 'apply' && element.elements[0].name === 'arccos') {
117!
122
    let arg = _toMathExpr(element.elements[1]);
×
123
    return `acos(${arg})`;
×
124
  } else if (element.name === 'apply' && element.elements[0].name === 'arctan') {
117!
125
    let arg = _toMathExpr(element.elements[1]);
×
126
    return `atan(${arg})`;
×
127
  } else if (element.name === 'apply' && element.elements[0].name === 'arccot') {
117!
128
    let arg = _toMathExpr(element.elements[1]);
×
129
    return `acot(${arg})`;
×
130
  } else if (element.name === 'apply' && element.elements[0].name === 'arccsc') {
117!
131
    let arg = _toMathExpr(element.elements[1]);
×
132
    return `acsc(${arg})`;
×
133
  } else if (element.name === 'apply' && element.elements[0].name === 'arcsec') {
117!
134
    let arg = _toMathExpr(element.elements[1]);
×
135
    return `asec(${arg})`;
×
136
  // hyperbolic arccsch(x)
137
  } else if (element.name === 'apply' && element.elements[0].name === 'arcsinh') {
117!
138
    let arg = _toMathExpr(element.elements[1]);
×
139
    return `asinh(${arg})`;
×
140
  } else if (element.name === 'apply' && element.elements[0].name === 'arccosh') {
117!
141
    let arg = _toMathExpr(element.elements[1]);
×
142
    return `acosh(${arg})`;
×
143
  } else if (element.name === 'apply' && element.elements[0].name === 'arctanh') {
117!
144
    let arg = _toMathExpr(element.elements[1]);
×
145
    return `atanh(${arg})`;
×
146
  } else if (element.name === 'apply' && element.elements[0].name === 'arccoth') {
117!
147
    let arg = _toMathExpr(element.elements[1]);
×
148
    return `acoth(${arg})`;
×
149
  } else if (element.name === 'apply' && element.elements[0].name === 'arcsech') {
117!
150
    let arg = _toMathExpr(element.elements[1]);
×
151
    return `asech(${arg})`;
×
152
  } else if (element.name === 'apply' && element.elements[0].name === 'arccsch') {
117!
153
    let arg = _toMathExpr(element.elements[1]);
×
154
    return `acsch(${arg})`;
×
155
  } else if (element.name === 'piecewise') { // return ternary if possible, or piecewise
117✔
156
    let args = [];
3✔
157
    // iterate through pieces
158
    element.elements
3✔
159
      .filter((x) => x.name === 'piece')
7✔
160
      .forEach((x) => {
161
        args.push(_toMathExpr(x.elements[0], false));
5✔
162
        args.push(_toMathExpr(x.elements[1], false));
5✔
163
      });
164
    let otherwise = element.elements
3✔
165
      .filter((x) => x.name === 'otherwise');
7✔
166
    if (otherwise.length > 0) {
3✔
167
      let otherwiseExpr = _toMathExpr(otherwise[0].elements[0], false);
2✔
168
      // in case of one piece and one otherwise
169
      if (args.length === 2) {
2✔
170
        return `(${args[1]} ? ${args[0]} : ${otherwiseExpr})`; // BRAKE
1✔
171
      }
172
      // all other cases
173
      args.push(otherwiseExpr);
1✔
174
    }
175

176
    return `piecewise(${args.join(', ')})`;
2✔
177
  } else if (element.name === 'apply' && (element.elements[0].name === 'ci' || element.elements[0].name === 'csymbol')) { // some user defined functions
114!
178
    let funcName = _toMathExpr(element.elements[0]); // first.elements[0]?.text;
×
179
    let args = element.elements.slice(1)
×
180
      .map((x) => _toMathExpr(x)).join(', '); // skip ()
×
181
    return `${funcName}(${args})`;
×
182
  } else if (element.name === 'apply') { // all other internal mathml functions
114✔
183
    let args = element.elements.slice(1)
1✔
184
      .map((x) => _toMathExpr(x)).join(', ');
1✔
185
    return `${element.elements[0].name}(${args})`;
1✔
186
  } else if (element.name === 'ci') {
113✔
187
    return element.elements[0]?.text;
18✔
188
  } else if (element.name === 'csymbol' && element.attributes?.definitionURL === 'http://www.sbml.org/sbml/symbols/time') {
95!
189
    return 't';
×
190
  } else if (element.name === 'csymbol' && element.attributes?.definitionURL === 'http://www.sbml.org/sbml/symbols/delay') {
95!
191
    // return 'delay';
192
    throw new HetaLevelError('"delay" symbol in expression (SBML module) is not supported');
×
193
  } else if (element.name === 'csymbol' && element.attributes?.definitionURL === 'http://www.sbml.org/sbml/symbols/avogadro') {
95!
194
    return '6.02214076e23';
×
195
  } else if (element.name === 'csymbol') {
95!
196
    throw new HetaLevelError(`csymbol with url ${element.attributes?.definitionURL} in expression (SBML module) is not supported`);
×
197
  } else if (element.name === 'cn' && element.attributes?.type === 'rational' && element.elements[1]?.name === 'sep') { // rational numbers: 1/1000
95!
198
    let numerator = element.elements[0]?.text;
×
199
    let denominator = element.elements[2]?.text;
×
200
    let sign = (numerator >= 0 && denominator > 0) || (numerator <= 0 && denominator < 0)
×
201
      ? ''
202
      : '-';
203
    return `(${sign}${Math.abs(numerator)}/${Math.abs(denominator)})`;
×
204
  } else if (element.name === 'cn' && element.attributes?.type === 'e-notation' && element.elements[1]?.name === 'sep') { // rational numbers: 1.1*10^-3
95!
205
    let mantissa = element.elements[0]?.text?.trim();
×
206
    let power = element.elements[2]?.text?.trim();
×
207
    return `(${mantissa}e${power})`;
×
208
  } else if (element.name === 'cn' && element.elements[0]?.text < 0) { // negative number requires (-2)
95✔
209
    return `(${element.elements[0]?.text})`;
9✔
210
  } else if (element.name === 'cn') { // regular positive numbers
86!
211
    return element.elements[0]?.text;
86✔
212
  } else if (element.name === 'true') {
×
213
    return 'true';
×
214
  } else if (element.name === 'false') {
×
215
    return 'false';
×
216
  } else if (element.name === 'exponentiale') {
×
217
    return 'e';
×
218
  } else if (element.name === 'pi') {
×
219
    return 'pi';
×
220
  } else if (element.name === 'infinity') {
×
221
    return 'Infinity';
×
222
  } else if (element.name === 'notanumber') {
×
223
    return 'NaN';
×
224
  } else {
225
    throw new Error('Cannot parse MathML:' + JSON.stringify(element, null, 2));
×
226
  }
227
}
228

229
// same as _toMathExpr but converts boolean operations to numeric
230
// which are sutable for CSwitcher triggers
231
// a > b -> a - b
232
// a < b -> b - a
233
// a == b -> throws
234
// a != b -> throws
235
// not(a>b) -> b - a
236
// a>b and c>d -> min(a - b, c - d)
237
// a>b or c>d -> max(a - b, c - d)
238
function _toNumericExpr(element, useParentheses = false) {
×
239
  //console.log(element);
240
  // transform all boolean, other expressions are the same
NEW
241
  if (element.name === 'math') {
×
NEW
242
    return _toNumericExpr(element.elements[0]);
×
243
  }
NEW
244
  if (element.name === 'apply' && element.elements[0].name in ['gt', 'geq']) {
×
NEW
245
    let one = _toMathExpr(element.elements[1], true);
×
NEW
246
    let two = _toMathExpr(element.elements[2], true);
×
NEW
247
    return `${one} - ${two}`;
×
248
  }
NEW
249
  if (element.name === 'apply' && (element.elements[0].name === 'lt' || element.elements[0].name === 'leq')) {
×
NEW
250
    let one = _toMathExpr(element.elements[1], true);
×
NEW
251
    let two = _toMathExpr(element.elements[2], true);
×
NEW
252
    return `${two} - ${one}`;
×
253
  }
NEW
254
  if (element.name === 'apply' && element.elements[0].name === 'neq') {
×
NEW
255
    throw new HetaLevelError('!= operator in expression (SBML module) is not supported');
×
256
  }
NEW
257
  if (element.name === 'apply' && element.elements[0].name === 'eq') {
×
NEW
258
    throw new HetaLevelError('== operator in expression (SBML module) is not supported');
×
259
  }
NEW
260
  if (element.name === 'apply' && element.elements[0].name === 'and') {
×
NEW
261
    let args = element.elements.slice(1)
×
NEW
262
      .map((x) => _toNumericExpr(x)).join(', ');
×
NEW
263
    return `min(${args})`;
×
264
  }
NEW
265
  if (element.name === 'apply' && element.elements[0].name === 'or') {
×
NEW
266
    let args = element.elements.slice(1)
×
NEW
267
      .map((x) => _toNumericExpr(x)).join(', ');
×
NEW
268
    return `max(${args})`;
×
269
  }
NEW
270
  if (element.name === 'apply' && element.elements[0].name === 'not') {
×
NEW
271
    let one = _toNumericExpr(element.elements[1], true);
×
NEW
272
    return `-(${one})`;
×
273
  }
274
  
275
  // all other cases
NEW
276
  return _toMathExpr(element);
×
277
}
278

279
module.exports = { _toMathExpr, _toNumericExpr };
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc