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

dondi / GRNsight / 22110302200

17 Feb 2026 06:16PM UTC coverage: 80.155% (+0.03%) from 80.129%
22110302200

Pull #1325

github

ntran18
Merge branch 'beta' into maika-1192
Pull Request #1325: Fixing warnings issue for two column sheets #1192

425 of 536 branches covered (79.29%)

Branch coverage included in aggregate %.

34 of 39 new or added lines in 3 files covered. (87.18%)

1 existing line in 1 file now uncovered.

1231 of 1530 relevant lines covered (80.46%)

9140.12 hits per line

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

92.86
/server/controllers/additional-sheet-parser.js
1
// Parses "optimization_paramters" 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) {
993✔
8
        if (sheetName === "production_rates" || sheetName === "optimized_production_rates") {
993✔
9
            return "production_rate";
222✔
10
        } else if (sheetName === "degradation_rates") {
771✔
11
            return "degradation_rate";
153✔
12
        } else if (sheetName === "threshold_b" || sheetName === "optimized_threshold_b") {
618✔
13
            return "threshold_b";
222✔
14
        } else if (sheetName === "optimization_parameters") {
396✔
15
            return column === 0 ? "optimization_parameter" : "value";
306✔
16
        } else if (sheetName === "optimization_diagnostics") {
90✔
17
            return column === 0 ? "Parameter" : "Value";
90✔
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 optimizationParametersObectKey = {
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")) {
141!
55
        warningsCount = 0;
×
56
        workbook.warnings = [];
×
57
    } else {
58
        warningsCount = workbook.warnings.length;
141✔
59
    }
60
    const MAX_WARNINGS = 75;
141✔
61
    if (warningsCount < MAX_WARNINGS) {
141!
62
        workbook.warnings.push(message);
141✔
63
    } else {
64
        workbook.errors.push(constants.errors.warningsCountError);
×
65
        return false;
×
66
    }
67
};
68

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

80
const TWO_COL_SHEET_NAMES = [
3✔
81
    "production_rates",
82
    "degradation_rates",
83
    "threshold_b",
84
    "optimized_production_rates",
85
    "optimized_threshold_b",
86
];
87

88
const validGeneName = (output, sheetName, gene, row) => {
3✔
89
    var maxGeneLength = 12;
7,221✔
90
    var regex = /[^a-z0-9_\-]/gi;
7,221✔
91
    if (typeof gene !== "string") {
7,221✔
92
        addError(output, constants.errors.invalidGeneTypeError(sheetName, gene, row));
30✔
93
        return false;
30✔
94
    } else if (gene.length > maxGeneLength) {
7,191✔
95
        addError(output, constants.errors.invalidGeneLengthError(sheetName, gene, row));
15✔
96
        return false;
15✔
97
    } else if (gene.match(regex) !== null) {
7,176✔
98
        addError(output, constants.errors.specialCharacterError(sheetName, gene, row));
15✔
99
        return false;
15✔
100
    }
101
    return true;
7,161✔
102
};
103
// Optimization Parameters Parser
104
const parseMetaDataSheet = sheet => {
3✔
105
    let meta = {
150✔
106
        data: {},
107
        errors: [],
108
        warnings: [],
109
    };
110
    let paramType;
111
    if (sheet.data[0][0] === undefined) {
150✔
112
        addError(
3✔
113
            meta,
114
            constants.errors.missingColumnHeaderError(
115
                sheet.name,
116
                constants.numbersToLetters[0],
117
                getSheetHeader(sheet.name, 0, 0)
118
            )
119
        );
120
    } else if (sheet.data[0][0] !== getSheetHeader(sheet.name, 0, 0)) {
147✔
121
        addError(
3✔
122
            meta,
123
            constants.errors.incorrectColumnHeaderError(
124
                sheet.name,
125
                constants.numbersToLetters[0],
126
                getSheetHeader(sheet.name, 0, 0)
127
            )
128
        );
129
    }
130
    if (sheet.data[0][1] === undefined) {
150✔
131
        addError(
3✔
132
            meta,
133
            constants.errors.missingColumnHeaderError(
134
                sheet.name,
135
                constants.numbersToLetters[1],
136
                getSheetHeader(sheet.name, 1, 0)
137
            )
138
        );
139
    } else if (sheet.data[0][1] !== getSheetHeader(sheet.name, 1, 0)) {
147✔
140
        addError(
3✔
141
            meta,
142
            constants.errors.incorrectColumnHeaderError(
143
                sheet.name,
144
                constants.numbersToLetters[1],
145
                getSheetHeader(sheet.name, 1, 0)
146
            )
147
        );
148
    }
149

150
    sheet.data.forEach(function (element, index) {
150✔
151
        if (index !== 0) {
2,766✔
152
            const value = element.slice(1);
2,616✔
153
            // Extract element from array if array contains only 1 value
154
            meta.data[element[0]] = value.length > 1 ? value : value[0];
2,616✔
155
        }
156
    });
157
    for (let key in meta.data) {
150✔
158
        paramType = optimizationParametersTypeKey[key];
2,616✔
159
        if (paramType === "object") {
2,616✔
160
            paramType = `list of ${optimizationParametersObectKey[key]}s`;
450✔
161
        }
162
        if (meta.data[key] === undefined) {
2,616✔
163
            addWarning(meta, constants.warnings.unknownOptimizationParameter(sheet.name, key));
3✔
164
        } else if (typeof meta.data[key] !== optimizationParametersTypeKey[key]) {
2,613✔
165
            if (
66✔
166
                optimizationParametersTypeKey[key] !== "object" ||
129✔
167
                typeof meta.data[key] !== optimizationParametersObectKey[key]
168
            ) {
169
                addWarning(
3✔
170
                    meta,
171
                    constants.warnings.invalidOptimizationParameter(sheet.name, key, paramType)
172
                );
173
            }
174
        } else if (optimizationParametersTypeKey[key] === "object") {
2,547✔
175
            for (let val of meta.data[key]) {
3,663✔
176
                if (typeof val !== optimizationParametersObectKey[key]) {
3,663✔
177
                    // throw error once per object. Makes sure that errors list is not flooded
178
                    addWarning(
3✔
179
                        meta,
180
                        constants.warnings.invalidOptimizationParameter(sheet.name, key, paramType)
181
                    );
182
                    break;
3✔
183
                }
184
            }
387✔
185
        }
186
    }
187
    return meta;
150✔
188
};
189

190
const parseOptimizationDiagnosticsSheet = sheet => {
3✔
191
    let output = {
42✔
192
        data: {
193
            Parameters: {},
194
            MSE: {
195
                "column-headers": [],
196
                Genes: {},
197
            },
198
        },
199
        errors: [],
200
        warnings: [],
201
    };
202
    let currentParameter;
203
    let currentValue;
204
    let currentGene;
205
    let currentMSE = [];
42✔
206
    // Check Headers
207
    if (sheet.data[0].length > 1) {
42✔
208
        if (sheet.data[0][0] !== getSheetHeader(sheet.name, 0, 0)) {
39✔
209
            addError(
3✔
210
                output,
211
                constants.errors.incorrectColumnHeaderError(
212
                    sheet.name,
213
                    constants.numbersToLetters[0],
214
                    getSheetHeader(sheet.name, 0, 0)
215
                )
216
            );
217
        }
218
        if (sheet.data[0][1] !== getSheetHeader(sheet.name, 1, 0)) {
39✔
219
            addError(
3✔
220
                output,
221
                constants.errors.incorrectColumnHeaderError(
222
                    sheet.name,
223
                    constants.numbersToLetters[1],
224
                    getSheetHeader(sheet.name, 1, 0)
225
                )
226
            );
227
        }
228
    } else {
229
        // seems a bit sus, but we'll see if this works properly during testing :\
230
        for (let col = 1; col >= sheet.data[0].length; col--) {
3✔
231
            addError(
6✔
232
                output,
233
                constants.errors.missingColumnHeaderError(
234
                    sheet.name,
235
                    constants.numbersToLetters[col],
236
                    getSheetHeader(sheet.name, col, 0)
237
                )
238
            );
239
        }
240
    }
241
    // Check Parameter Section
242
    let row = 1;
42✔
243
    // a missing row is the indicator to move onto the MSE
244
    while (sheet.data[row].length > 0) {
42✔
245
        currentParameter = sheet.data[row][0];
174✔
246
        currentValue = sheet.data[row][1];
174✔
247
        if (currentParameter === undefined || currentParameter.replace(/\s+/g, "") === "") {
174✔
248
            if (currentValue === undefined || currentValue.replace(/\s+/g, "") === "") {
3!
249
                // if there is no parameter or value assume that its time to move on
250
                row++;
3✔
251
                break;
3✔
252
            }
253
        }
254
        if (sheet.data[row].length > 2) {
171✔
255
            addWarning(output, constants.warnings.extraneousDataWarning(sheet.name, row + 1));
3✔
256
        }
257
        if (!optimizationDiagnosticsParameters.includes(currentParameter)) {
171✔
258
            if (currentParameter === "Gene") {
3!
259
                row--;
×
260
                break;
×
261
            }
262
            addWarning(
3✔
263
                output,
264
                constants.warnings.unknownOptimizationDiagnosticsParameter(
265
                    sheet.name,
266
                    currentParameter
267
                )
268
            );
269
        } else if (typeof currentValue !== "number") {
168✔
270
            addWarning(
3✔
271
                output,
272
                constants.warnings.invalidOptimizationDiagnosticsValue(sheet.name, currentParameter)
273
            );
274
        } else {
275
            output.data.Parameters[currentParameter] = currentValue;
165✔
276
        }
277
        row++;
171✔
278
    }
279
    // Skip until Gene section
280
    while (sheet.data[row] !== undefined && sheet.data[row].length < 1) {
42✔
281
        row++;
39✔
282
    }
283
    // Check Gene section MSE's
284
    if (sheet.data[row].length > 1) {
42✔
285
        if (sheet.data[row][0] !== "Gene") {
42✔
286
            addWarning(
6✔
287
                output,
288
                constants.warnings.incorrectMSEGeneHeaderWarning(sheet.name, row + 1)
289
            );
290
        }
291
        for (let col = 1; col < sheet.data[row].length; col++) {
42✔
292
            if (!sheet.data[row][col].includes("MSE")) {
252✔
293
                addWarning(
9✔
294
                    output,
295
                    constants.warnings.incorrectMSEHeaderWarning(
296
                        sheet.name,
297
                        sheet.data[row][col],
298
                        row + 1,
299
                        constants.numbersToLetters[col]
300
                    )
301
                );
302
            }
303
            // we still push the header (even tho it's sus) because the gene MSE's are
304
            // dependent on the order of the column headers
305
            output.data.MSE["column-headers"].push(sheet.data[row][col]);
252✔
306
        }
307
        row++;
42✔
308
        // on to the actual genes
309
        while (row < sheet.data.length) {
42✔
310
            if (sheet.data[row].length > output.data.MSE["column-headers"].length + 1) {
630✔
311
                addWarning(output, constants.warnings.extraneousDataWarning(sheet.name, row + 1));
6✔
312
            }
313
            currentGene = sheet.data[row][0];
630✔
314
            // if it's a valid gene set the key = MSE value
315
            if (validGeneName(output, sheet.name, currentGene, row)) {
630✔
316
                for (let col = 1; col <= output.data.MSE["column-headers"].length; col++) {
630✔
317
                    if (typeof sheet.data[row][col] === "number") {
3,780✔
318
                        currentMSE.push(sheet.data[row][col]);
3,738✔
319
                    } else if (sheet.data[row][col] === undefined) {
42✔
320
                        addWarning(
30✔
321
                            output,
322
                            constants.warnings.missingMSEDataWarning(
323
                                sheet.name,
324
                                row + 1,
325
                                constants.numbersToLetters[col]
326
                            )
327
                        );
328
                    } else {
329
                        addWarning(
12✔
330
                            output,
331
                            constants.warnings.invalidMSEDataWarning(
332
                                sheet.name,
333
                                row + 1,
334
                                constants.numbersToLetters[col]
335
                            )
336
                        );
337
                    }
338
                }
339
                output.data.MSE.Genes[currentGene] = currentMSE;
630✔
340
                currentMSE = [];
630✔
341
            }
342
            row++;
630✔
343
        }
344
    }
345
    return output;
42✔
346
};
347

348
const parseTwoColumnSheet = (sheet, genesInNetwork) => {
3✔
349
    let output = {
591✔
350
        data: {},
351
        errors: [],
352
        warnings: [],
353
    };
354

355
    if (sheet.data.length === 0) {
591✔
356
        return output;
9✔
357
    }
358

359
    let currentGene;
360
    let currentValue;
361

362
    const genesMissingValue = [];
582✔
363

364
    // check to see if the genes are strings and the values are numbers
365

366
    for (let row = 0; row < sheet.data.length; row++) {
582✔
367
        if (sheet.data[row].length > 2) {
7,173✔
368
            addWarning(output, constants.warnings.extraneousDataWarning(sheet.name, row + 1));
15✔
369
        }
370
        if (row === 0) {
7,173✔
371
            if (sheet.data[row].length > 0) {
582✔
372
                if (sheet.data[row][0] !== "id") {
582✔
373
                    addError(output, constants.errors.idLabelError(sheet.name));
15✔
374
                }
375
            }
376
            if (sheet.data[row].length > 1) {
582✔
377
                if (sheet.data[row][1] !== getSheetHeader(sheet.name, 1, row)) {
552✔
378
                    addError(
15✔
379
                        output,
380
                        constants.errors.incorrectColumnHeaderError(
381
                            sheet.name,
382
                            constants.numbersToLetters[1],
383
                            getSheetHeader(sheet.name, 1, row)
384
                        )
385
                    );
386
                }
387
            } else {
388
                addError(
30✔
389
                    output,
390
                    constants.errors.missingColumnHeaderError(
391
                        sheet.name,
392
                        constants.numbersToLetters[1],
393
                        getSheetHeader(sheet.name, 1, row)
394
                    )
395
                );
396
            }
397
        } else {
398
            currentGene = sheet.data[row][0];
6,591✔
399
            currentValue = sheet.data[row][1];
6,591✔
400

401
            if (validGeneName(output, sheet.name, currentGene, row + 1)) {
6,591✔
402
                if (currentValue === null || currentValue === undefined) {
6,531✔
403
                    genesMissingValue.push(currentGene);
27✔
404
                    output.data[currentGene] = undefined;
27✔
405
                } else {
406
                    if (typeof currentValue === "number") {
6,504!
407
                        output.data[currentGene] = currentValue;
6,504✔
408
                    } else {
NEW
409
                        if (typeof currentValue === "number") {
×
NEW
410
                            output.data[currentGene] = currentValue;
×
NEW
411
                            genesInSheet.push(currentGene);
×
412
                        } else {
NEW
413
                            addError(
×
414
                                output,
415
                                constants.errors.invalidValueError(
416
                                    sheet.name,
417
                                    currentValue,
418
                                    row + 1,
419
                                    getSheetHeader(sheet.name, 1, row)
420
                                )
421
                            );
422
                        }
423
                    }
424
                }
425
            }
426
        }
427
    }
428

429
    const allMissing =
430
        genesInNetwork && genesInNetwork.every(gene => genesMissingValue.includes(gene));
582✔
431
    if (allMissing) {
582✔
432
        addWarning(
9✔
433
            output,
434
            constants.warnings.missingAllValuesForGenes(sheet.name, genesMissingValue)
435
        );
436
    }
437

438
    // Check for missing genes in sheet
439
    if (genesInNetwork) {
582✔
440
        //  Check if the output data keys (genes in sheet) include all genes in the network
441
        const missingGenes = genesInNetwork.filter(g => !Object.keys(output.data).includes(g));
2,268✔
442
        if (missingGenes.length > 0) {
288✔
443
            addWarning(
36✔
444
                output,
445
                constants.warnings.missingGenesInTwoColumnSheetWarningWhenImporting(
446
                    sheet.name,
447
                    missingGenes.join(", ")
448
                )
449
            );
450
        }
451
    }
452

453
    return output;
582✔
454
};
455

456
module.exports = function (workbookFile, genesInNetwork) {
3✔
457
    let output = {
174✔
458
        meta: {
459
            data: {},
460
            errors: [],
461
            warnings: [],
462
        }, // optimization_parameters only
463
        twoColumnSheets: {}, // 2-column data
464
        meta2: {}, // optimation_diagnostics only //temporary until where it goes is decided
465
    };
466
    workbookFile.forEach(function (sheet) {
174✔
467
        if (sheet.name === "optimization_parameters") {
2,034✔
468
            output.meta = parseMetaDataSheet(sheet);
150✔
469
            // above line creates an object from the optimization paramerters sheet
470
            // these are part of the "meta" property
471
        } else if (constants.TWO_COL_SHEET_NAMES.includes(sheet.name)) {
1,884✔
472
            output.twoColumnSheets[sheet.name] = parseTwoColumnSheet(sheet, genesInNetwork);
591✔
473
        } else if (sheet.name === "optimization_diagnostics") {
1,293✔
474
            output.meta2 = parseOptimizationDiagnosticsSheet(sheet);
42✔
475
        }
476
    });
477
    return output;
174✔
478
};
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