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

dondi / GRNsight / 22704187776

05 Mar 2026 05:43AM UTC coverage: 80.524% (+0.09%) from 80.435%
22704187776

push

github

web-flow
Merge pull request #1368 from dondi/alex-1192-header-issue

Fixing warnings issue for two column sheets (headers & importing) #1192

444 of 557 branches covered (79.71%)

Branch coverage included in aggregate %.

51 of 52 new or added lines in 2 files covered. (98.08%)

1 existing line in 1 file now uncovered.

1247 of 1543 relevant lines covered (80.82%)

9047.43 hits per line

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

94.39
/server/controllers/additional-sheet-parser.js
1
// Parses "optimization_parameters" and 2-column sheets
2
// from GRNmap input or output workbook
3

4
var constants = require(__dirname + "/workbook-constants");
3✔
5

6
const getSheetHeader = (sheetName, column, row) => {
3✔
7
    if (row === 0) {
1,755✔
8
        if (sheetName === "production_rates" || sheetName === "optimized_production_rates") {
1,755✔
9
            return column === 0 ? "id" : "production_rate";
465✔
10
        } else if (sheetName === "degradation_rates") {
1,290✔
11
            return column === 0 ? "id" : "degradation_rate";
369✔
12
        } else if (sheetName === "threshold_b" || sheetName === "optimized_threshold_b") {
921✔
13
            return column === 0 ? "id" : "threshold_b";
465✔
14
        } else if (sheetName === "optimization_parameters") {
456✔
15
            return column === 0 ? "optimization_parameter" : "value";
378✔
16
        } else if (sheetName === "optimization_diagnostics") {
78✔
17
            return column === 0 ? "Parameter" : "Value";
78✔
18
        }
19
    }
20
};
21

22
const optimizationParametersTypeKey = {
3✔
23
    alpha: "number",
24
    kk_max: "number",
25
    MaxIter: "number",
26
    TolFun: "number",
27
    MaxFunEval: "number",
28
    TolX: "number",
29
    production_function: "string",
30
    L_curve: "number",
31
    estimate_params: "number",
32
    make_graphs: "number",
33
    fix_P: "number",
34
    fix_b: "number",
35
    expression_timepoints: "object",
36
    Strain: "object",
37
    species: "string",
38
    taxon_id: "number",
39
    workbookType: "string",
40
    simulation_timepoints: "object",
41
    b_or_tau: "number",
42
};
43

44
const optimizationDiagnosticsParameters = ["LSE", "Penalty", "min LSE", "iteration count"];
3✔
45

46
const optimizationParametersObjectKey = {
3✔
47
    expression_timepoints: "number",
48
    Strain: "string",
49
    simulation_timepoints: "number",
50
};
51

52
const addWarning = (workbook, message) => {
3✔
53
    let warningsCount;
54
    if (!Object.keys(workbook).includes("warnings")) {
195!
55
        warningsCount = 0;
×
56
        workbook.warnings = [];
×
57
    } else {
58
        warningsCount = workbook.warnings.length;
195✔
59
    }
60
    const MAX_WARNINGS = 75;
195✔
61
    if (warningsCount < MAX_WARNINGS) {
195!
62
        const exists = workbook.warnings.some(w => w.errorDescription === message.errorDescription);
195✔
63
        if (exists) {
195!
NEW
64
            return false;
×
65
        }
66
        workbook.warnings.push(message);
195✔
67
    } else {
68
        workbook.errors.push(constants.errors.warningsCountError);
×
69
        return false;
×
70
    }
71
};
72

73
const addError = (output, message) => {
3✔
74
    const errorsCount = output.errors.length;
69✔
75
    const MAX_ERRORS = 20;
69✔
76
    if (errorsCount < MAX_ERRORS) {
69!
77
        output.errors.push(message);
69✔
78
    } else {
79
        output.errors.push(constants.errors.errorsCountError);
×
80
        return false;
×
81
    }
82
};
83

84
const validGeneName = (output, sheetName, gene, row) => {
3✔
85
    var maxGeneLength = 12;
6,177✔
86
    var regex = /[^a-z0-9\_\-]/gi;
6,177✔
87
    if (typeof gene !== "string") {
6,177✔
88
        addError(output, constants.errors.invalidGeneTypeError(sheetName, gene, row));
30✔
89
        return false;
30✔
90
    } else if (gene.length > maxGeneLength) {
6,147✔
91
        addError(output, constants.errors.invalidGeneLengthError(sheetName, gene, row));
15✔
92
        return false;
15✔
93
    } else if (gene.match(regex) !== null) {
6,132✔
94
        addError(output, constants.errors.specialCharacterError(sheetName, gene, row));
15✔
95
        return false;
15✔
96
    }
97
    return true;
6,117✔
98
};
99
// Optimization Parameters Parser
100
const parseMetaDataSheet = sheet => {
3✔
101
    let meta = {
189✔
102
        data: {},
103
        errors: [],
104
        warnings: [],
105
    };
106
    let paramType;
107
    checkValidHeaderAndAddWarnings(meta, sheet);
189✔
108

109
    const isHeaderMissing = meta.warnings.some(
189✔
110
        w => w.warningCode === `MISSING_COLUMN_HEADER_${sheet.name.toUpperCase()}`
6✔
111
    );
112
    sheet.data.forEach(function (element, index) {
189✔
113
        if (!isHeaderMissing && index === 0) {
3,516✔
114
            return;
186✔
115
        }
116
        const value = element.slice(1);
3,330✔
117
        // Extract element from array if array contains only 1 value
118
        meta.data[element[0]] = value.length > 1 ? value : value[0];
3,330✔
119
    });
120
    for (let key in meta.data) {
189✔
121
        paramType = optimizationParametersTypeKey[key];
3,330✔
122
        if (paramType === "object") {
3,330✔
123
            paramType = `list of ${optimizationParametersObjectKey[key]}s`;
567✔
124
        }
125
        if (meta.data[key] === undefined) {
3,330✔
126
            addWarning(meta, constants.warnings.unknownOptimizationParameter(sheet.name, key));
3✔
127
        } else if (typeof meta.data[key] !== optimizationParametersTypeKey[key]) {
3,327✔
128
            if (
120✔
129
                optimizationParametersTypeKey[key] !== "object" ||
237✔
130
                typeof meta.data[key] !== optimizationParametersObjectKey[key]
131
            ) {
132
                addWarning(
3✔
133
                    meta,
134
                    constants.warnings.invalidOptimizationParameter(sheet.name, key, paramType)
135
                );
136
            }
137
        } else if (optimizationParametersTypeKey[key] === "object") {
3,207✔
138
            for (let val of meta.data[key]) {
4,953✔
139
                if (typeof val !== optimizationParametersObjectKey[key]) {
4,953✔
140
                    // throw error once per object. Makes sure that errors list is not flooded
141
                    addWarning(
3✔
142
                        meta,
143
                        constants.warnings.invalidOptimizationParameter(sheet.name, key, paramType)
144
                    );
145
                    break;
3✔
146
                }
147
            }
450✔
148
        }
149
    }
150
    return meta;
189✔
151
};
152

153
// check header method
154
const checkValidHeaderAndAddWarnings = (output, sheet) => {
3✔
155
    const sheetName = sheet.name;
873✔
156
    const expectedCellA1 = getSheetHeader(sheetName, 0, 0);
873✔
157
    const expectedCellB1 = getSheetHeader(sheetName, 1, 0);
873✔
158

159
    const hasData = sheet.data && sheet.data[0];
873✔
160
    const cellA1 = hasData ? sheet.data[0][0] : undefined;
873!
161
    const cellB1 = hasData && sheet.data[0].length > 1 ? sheet.data[0][1] : undefined;
873✔
162

163
    const isMissing =
164
        cellA1 === null ||
873✔
165
        cellA1 === undefined ||
166
        cellB1 === null ||
167
        cellB1 === undefined ||
168
        String(cellA1).trim() === "" ||
169
        String(cellB1).trim() === "" ||
170
        (typeof cellA1 === "string" && typeof cellB1 === "number");
171

172
    if (isMissing) {
873✔
173
        addWarning(
27✔
174
            output,
175
            constants.warnings.additionalSheetMissingColumnHeaderWarning(
176
                sheetName,
177
                expectedCellA1,
178
                expectedCellB1
179
            )
180
        );
181
        return;
27✔
182
    }
183

184
    if (cellA1 !== expectedCellA1 || cellB1 !== expectedCellB1) {
846✔
185
        addWarning(
27✔
186
            output,
187
            constants.warnings.additionalSheetIncorrectColumnHeaderWarning(
188
                sheetName,
189
                expectedCellA1,
190
                expectedCellB1
191
            )
192
        );
193
    }
194
};
195

196
const parseOptimizationDiagnosticsSheet = sheet => {
3✔
197
    let output = {
39✔
198
        data: {
199
            Parameters: {},
200
            MSE: {
201
                "column-headers": [],
202
                Genes: {},
203
            },
204
        },
205
        errors: [],
206
        warnings: [],
207
    };
208
    let currentParameter;
209
    let currentValue;
210
    let currentGene;
211
    let currentMSE = [];
39✔
212
    // Check Headers
213
    checkValidHeaderAndAddWarnings(output, sheet);
39✔
214
    // Check Parameter Section
215
    let row = 1;
39✔
216
    // a missing row is the indicator to move onto the MSE
217
    while (sheet.data[row].length > 0) {
39✔
218
        currentParameter = sheet.data[row][0];
162✔
219
        currentValue = sheet.data[row][1];
162✔
220
        if (currentParameter === undefined || currentParameter.replace(/\s+/g, "") === "") {
162✔
221
            if (currentValue === undefined || currentValue.replace(/\s+/g, "") === "") {
3!
222
                // if there is no parameter or value assume that its time to move on
223
                row++;
3✔
224
                break;
3✔
225
            }
226
        }
227
        if (sheet.data[row].length > 2) {
159✔
228
            addWarning(output, constants.warnings.extraneousDataWarning(sheet.name, row + 1));
3✔
229
        }
230
        if (!optimizationDiagnosticsParameters.includes(currentParameter)) {
159✔
231
            if (currentParameter === "Gene") {
3!
232
                row--;
×
233
                break;
×
234
            }
235
            addWarning(
3✔
236
                output,
237
                constants.warnings.unknownOptimizationDiagnosticsParameter(
238
                    sheet.name,
239
                    currentParameter
240
                )
241
            );
242
        } else if (typeof currentValue !== "number") {
156✔
243
            addWarning(
3✔
244
                output,
245
                constants.warnings.invalidOptimizationDiagnosticsValue(sheet.name, currentParameter)
246
            );
247
        } else {
248
            output.data.Parameters[currentParameter] = currentValue;
153✔
249
        }
250
        row++;
159✔
251
    }
252
    // Skip until Gene section
253
    while (sheet.data[row] !== undefined && sheet.data[row].length < 1) {
39✔
254
        row++;
36✔
255
    }
256
    // Check Gene section MSE's
257
    if (sheet.data[row].length > 1) {
39✔
258
        if (sheet.data[row][0] !== "Gene") {
39✔
259
            addWarning(
6✔
260
                output,
261
                constants.warnings.incorrectMSEGeneHeaderWarning(sheet.name, row + 1)
262
            );
263
        }
264
        for (let col = 1; col < sheet.data[row].length; col++) {
39✔
265
            if (!sheet.data[row][col].includes("MSE")) {
234✔
266
                addWarning(
9✔
267
                    output,
268
                    constants.warnings.incorrectMSEHeaderWarning(
269
                        sheet.name,
270
                        sheet.data[row][col],
271
                        row + 1,
272
                        constants.numbersToLetters[col]
273
                    )
274
                );
275
            }
276
            // we still push the header (even tho it's sus) because the gene MSE's are
277
            // dependent on the order of the column headers
278
            output.data.MSE["column-headers"].push(sheet.data[row][col]);
234✔
279
        }
280
        row++;
39✔
281
        // on to the actual genes
282
        while (row < sheet.data.length) {
39✔
283
            if (sheet.data[row].length > output.data.MSE["column-headers"].length + 1) {
585✔
284
                addWarning(output, constants.warnings.extraneousDataWarning(sheet.name, row + 1));
6✔
285
            }
286
            currentGene = sheet.data[row][0];
585✔
287
            // if it's a valid gene set the key = MSE value
288
            if (validGeneName(output, sheet.name, currentGene, row)) {
585✔
289
                for (let col = 1; col <= output.data.MSE["column-headers"].length; col++) {
585✔
290
                    if (typeof sheet.data[row][col] === "number") {
3,510✔
291
                        currentMSE.push(sheet.data[row][col]);
3,468✔
292
                    } else if (sheet.data[row][col] === undefined) {
42✔
293
                        addWarning(
30✔
294
                            output,
295
                            constants.warnings.missingMSEDataWarning(
296
                                sheet.name,
297
                                row + 1,
298
                                constants.numbersToLetters[col]
299
                            )
300
                        );
301
                    } else {
302
                        addWarning(
12✔
303
                            output,
304
                            constants.warnings.invalidMSEDataWarning(
305
                                sheet.name,
306
                                row + 1,
307
                                constants.numbersToLetters[col]
308
                            )
309
                        );
310
                    }
311
                }
312
                output.data.MSE.Genes[currentGene] = currentMSE;
585✔
313
                currentMSE = [];
585✔
314
            }
315
            row++;
585✔
316
        }
317
    }
318
    return output;
39✔
319
};
320

321
const checkValidGenesAndValuesInTwoColumnSheet = (output, sheet, row, genesMissingValue) => {
3✔
322
    if (!sheet.data[row]) return;
5,592!
323

324
    const currentGene = sheet.data[row][0];
5,592✔
325
    const currentValue = sheet.data[row][1];
5,592✔
326

327
    if (validGeneName(output, sheet.name, currentGene, row + 1)) {
5,592✔
328
        const isEmpty =
329
            currentValue === null ||
5,532✔
330
            currentValue === undefined ||
331
            (typeof currentValue === "string" && currentValue.trim() === "");
332
        if (isEmpty) {
5,532✔
333
            genesMissingValue.push(currentGene);
27✔
334
            output.data[currentGene] = undefined;
27✔
335
        } else {
336
            if (typeof currentValue === "number") {
5,505✔
337
                output.data[currentGene] = currentValue;
5,496✔
338
            } else {
339
                addError(
9✔
340
                    output,
341
                    constants.errors.invalidValueError(
342
                        sheet.name,
343
                        currentValue,
344
                        row + 1,
345
                        getSheetHeader(sheet.name, 1, 0)
346
                    )
347
                );
348
            }
349
        }
350
    }
351
};
352

353
const parseTwoColumnSheet = (sheet, genesInNetwork) => {
3✔
354
    let output = {
654✔
355
        data: {},
356
        errors: [],
357
        warnings: [],
358
    };
359

360
    if (sheet.data.length === 0) {
654✔
361
        return output;
9✔
362
    }
363

364
    const genesMissingValue = [];
645✔
365

366
    // check to see if the genes are strings and the values are numbers
367

368
    for (let row = 0; row < sheet.data.length; row++) {
645✔
369
        if (sheet.data[row].length > 2) {
6,216✔
370
            addWarning(output, constants.warnings.extraneousDataWarning(sheet.name, row + 1));
15✔
371
        }
372
        if (row === 0) {
6,216✔
373
            checkValidHeaderAndAddWarnings(output, sheet);
645✔
374

375
            const isHeaderMissing = output.warnings.some(
645✔
376
                w => w.warningCode === `MISSING_COLUMN_HEADER_${sheet.name.toUpperCase()}`
45✔
377
            );
378

379
            if (!isHeaderMissing) {
645✔
380
                continue;
624✔
381
            }
382
        }
383

384
        checkValidGenesAndValuesInTwoColumnSheet(output, sheet, row, genesMissingValue);
5,592✔
385
    }
386

387
    // Check whether all genes are missing values
388
    const isAllGenesMissingValues =
389
        genesInNetwork && genesInNetwork.every(gene => genesMissingValue.includes(gene));
645✔
390
    if (isAllGenesMissingValues) {
645✔
391
        addWarning(
9✔
392
            output,
393
            constants.warnings.missingAllGenesAndValuesInTwoColumnSheet(
394
                sheet.name,
395
                /*isAllGenesMissing=*/ false
396
            )
397
        );
398
    }
399

400
    // Check for missing genes in sheet
401
    if (genesInNetwork) {
645✔
402
        //  Check if the output data keys (genes in sheet) include all genes in the network
403
        const missingGenes = genesInNetwork.filter(g => !Object.keys(output.data).includes(g));
1,872✔
404
        if (missingGenes.length > 0) {
276✔
405
            if (missingGenes.length === genesInNetwork.length) {
36✔
406
                addWarning(
9✔
407
                    output,
408
                    constants.warnings.missingAllGenesAndValuesInTwoColumnSheet(
409
                        sheet.name,
410
                        /*isAllGenesMissing=*/ true
411
                    )
412
                );
413
            } else {
414
                addWarning(
27✔
415
                    output,
416
                    constants.warnings.missingGenesAndValuesInTwoColumnSheetWarningWhenImporting(
417
                        sheet.name,
418
                        missingGenes.join(", ")
419
                    )
420
                );
421
            }
422
        }
423
    }
424

425
    return output;
645✔
426
};
427

428
module.exports = function (workbookFile, genesInNetwork) {
3✔
429
    let output = {
207✔
430
        meta: {
431
            data: {},
432
            errors: [],
433
            warnings: [],
434
        }, // optimization_parameters only
435
        twoColumnSheets: {}, // 2-column data
436
        meta2: {}, // optimation_diagnostics only //temporary until where it goes is decided
437
    };
438
    workbookFile.forEach(function (sheet) {
207✔
439
        if (sheet.name === "optimization_parameters") {
2,127✔
440
            output.meta = parseMetaDataSheet(sheet);
189✔
441
            // above line creates an object from the optimization parameters sheet
442
            // these are part of the "meta" property
443
        } else if (constants.TWO_COL_SHEET_NAMES.includes(sheet.name)) {
1,938✔
444
            output.twoColumnSheets[sheet.name] = parseTwoColumnSheet(sheet, genesInNetwork);
654✔
445
        } else if (sheet.name === "optimization_diagnostics") {
1,284✔
446
            output.meta2 = parseOptimizationDiagnosticsSheet(sheet);
39✔
447
        }
448
    });
449
    return output;
207✔
450
};
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