Skip to content

Commit fe8bb62

Browse files
authored
Merge pull request #568 from bGrass/custom_column_without_description
Data dictionary warning refinement
2 parents 6e6e065 + bc84fba commit fe8bb62

File tree

7 files changed

+141
-29
lines changed

7 files changed

+141
-29
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"channels": [
3+
"description",
4+
"high_cutoff",
5+
"low_cutoff",
6+
"name",
7+
"notch",
8+
"sampling_frequency",
9+
"software_filters",
10+
"status",
11+
"status_description",
12+
"type",
13+
"units"
14+
],
15+
"events": [
16+
"duration",
17+
"HED",
18+
"onset",
19+
"trial_type",
20+
"response_time",
21+
"stim_file"
22+
],
23+
"misc": [],
24+
"participants": ["participant_id"],
25+
"phenotype": ["participant_id"],
26+
"scans": ["acq_time", "filename"],
27+
"sessions": ["acq_time", "session_id"]
28+
}

tests/bids.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,11 @@ var suite = describe('BIDS example datasets ', function() {
194194
})
195195
})
196196

197-
it('checks for tabular files without corresponding data dictionaries', function(isdone) {
197+
it('checks for tabular files with custom columns not described in a data dictionary', function(isdone) {
198198
var options = { ignoreNiftiHeaders: true }
199199
validate.BIDS(
200200
'tests/data/BIDS-examples-' + test_version + '/ds001',
201+
//'tests/data/ds001344-1.0.0',
201202
options,
202203
function(issues) {
203204
assert(issues.warnings.length === 2 && issues.warnings[1].code === '82')

utils/files/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const readFile = require('./readFile')
66
const readNiftiHeader = require('./readNiftiHeader')
77
const readDir = require('./readDir')
88
const potentialLocations = require('./potentialLocations')
9-
const generateMergedSidecarDict = require('./generatedMergedSidecarDict')
9+
const generateMergedSidecarDict = require('./generateMergedSidecarDict')
1010
const getBFileContent = require('./getBFileContent')
1111

1212
// public API ---------------------------------------------------------------------

utils/issues/list.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -446,9 +446,10 @@ module.exports = {
446446
reason: "Third column of the channels file must be named 'notch'",
447447
},
448448
82: {
449-
key: 'TABULARFILE_WITHOUT_DATADICTIONARY',
449+
key: 'CUSTOM_COLUMN_WITHOUT_DESCRIPTION',
450450
severity: 'warning',
451-
reason: 'Tabular file does not have accompanying data dictionary.',
451+
reason:
452+
'Tabular file contains custom columns not described in a data dictionary',
452453
},
453454
83: {
454455
key: 'ECHOTIME1_2_DIFFERENCE_UNREASONABLE',

validators/bids.js

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ BIDS = {
170170
fullTest: function(fileList, callback) {
171171
var self = this
172172

173-
var jsonContentsDict = {},
173+
let jsonContentsDict = {},
174174
bContentsDict = {},
175175
events = [],
176176
stimuli = {
@@ -184,6 +184,8 @@ BIDS = {
184184
participants = null,
185185
phenotypeParticipants = [],
186186
hasSubjectDir = false
187+
188+
let tsvs = []
187189
var hasDatasetDescription = false
188190

189191
var summary = {
@@ -194,7 +196,6 @@ BIDS = {
194196
totalFiles: Object.keys(fileList).length,
195197
size: 0,
196198
}
197-
198199
// collect file directory statistics
199200
async.eachOfLimit(fileList, 200, function(file) {
200201
// collect file stats
@@ -341,32 +342,14 @@ BIDS = {
341342

342343
// validate tsv
343344
else if (file.name && file.name.endsWith('.tsv')) {
344-
// Generate name for corresponding data dictionary file
345-
let dict_path = file.relativePath.replace('.tsv', '.json')
346-
let exists = false
347-
let potentialDicts = utils.files.potentialLocations(dict_path)
348-
// Need to check for .json file at all levels of heirarchy
349-
// Get list of fileList keys
350-
let idxs = Object.keys(fileList)
351-
for (let i of idxs) {
352-
if (potentialDicts.indexOf(fileList[i].relativePath) > -1) {
353-
exists = true
354-
break
355-
}
356-
}
357-
358-
// Check if data dictionary file exists
359-
if (!exists) {
360-
self.issues.push(
361-
new Issue({
362-
code: 82,
363-
file: file,
364-
}),
365-
)
366-
}
367345
utils.files
368346
.readFile(file)
369347
.then(contents => {
348+
// Push TSV to list for custom column verification after all data dictionaries have been read
349+
tsvs.push({
350+
file: file,
351+
contents: contents,
352+
})
370353
if (file.name.endsWith('_events.tsv')) {
371354
events.push({
372355
file: file,
@@ -715,6 +698,11 @@ BIDS = {
715698
self.issues,
716699
)
717700

701+
// validate custom fields in all TSVs and add any issues to the list
702+
self.issues = self.issues.concat(
703+
TSV.validateTsvColumns(tsvs, jsonContentsDict),
704+
)
705+
718706
// validation session files
719707
self.issues = self.issues.concat(session(fileList))
720708

validators/tsv.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var files = require('../utils/files')
44
var utils = require('../utils')
55
var dateIsValid = require('date-fns/isValid')
66
var parseDate = require('date-fns/parse')
7+
const nonCustomColumns = require('../bids_validator/tsv/non_custom_columns.json')
78
/**
89
* TSV
910
*
@@ -344,7 +345,100 @@ const checkAcqTimeFormat = function(rows, file, issues) {
344345
}
345346
}
346347

348+
/**
349+
* @param {Object} file - BIDS file object
350+
* Accepts file object and returns a type based on file path
351+
*/
352+
const getTsvType = function(file) {
353+
let tsvType = 'misc'
354+
if (file.relativePath.includes('phenotype/')) {
355+
tsvType = 'phenotype'
356+
} else if (file.name === 'participants.tsv') {
357+
tsvType = 'participants'
358+
} else if (
359+
file.name.endsWith('_channels.tsv') ||
360+
file.name.endsWith('_events.tsv') ||
361+
file.name.endsWith('_scans.tsv') ||
362+
file.name.endsWith('_sessions.tsv')
363+
) {
364+
const split = file.name.split('_')
365+
tsvType = split[split.length - 1].replace('.tsv', '')
366+
}
367+
return tsvType
368+
}
369+
370+
/**
371+
*
372+
* @param {array} headers -Array of column names
373+
* @param {string} type - Type from getTsvType
374+
* Checks TSV column names to determine if they're core or custom
375+
* Returns array of custom column names
376+
*/
377+
const getCustomColumns = function(headers, type) {
378+
const customCols = []
379+
// Iterate column headers
380+
for (let col of headers) {
381+
// If it's a custom column
382+
if (!nonCustomColumns[type].includes(col)) {
383+
customCols.push(col)
384+
}
385+
}
386+
return customCols
387+
}
388+
389+
/**
390+
*
391+
* @param {array} tsvs - Array of objects containing TSV file objects and contents
392+
* @param {Object} jsonContentsDict
393+
*/
394+
const validateTsvColumns = function(tsvs, jsonContentsDict) {
395+
let tsvIssues = []
396+
tsvs.map(tsv => {
397+
const customColumns = getCustomColumns(
398+
tsv.contents.split('\n')[0].split('\t'),
399+
getTsvType(tsv.file),
400+
)
401+
if (customColumns.length > 0) {
402+
// Get merged data dictionary for this file
403+
const potentialSidecars = utils.files.potentialLocations(
404+
tsv.file.relativePath.replace('.tsv', '.json'),
405+
)
406+
const mergedDict = utils.files.generateMergedSidecarDict(
407+
potentialSidecars,
408+
jsonContentsDict,
409+
)
410+
const keys = Object.keys(mergedDict)
411+
// Gather undefined columns for the file
412+
let undefinedCols = customColumns.filter(col => !keys.includes(col))
413+
// Create an issue for all undefined columns in this file
414+
undefinedCols.length &&
415+
tsvIssues.push(
416+
customColumnIssue(
417+
tsv.file,
418+
undefinedCols.join(', '),
419+
potentialSidecars,
420+
),
421+
)
422+
}
423+
})
424+
// Return array of all instances of undescribed custom columns
425+
return tsvIssues
426+
}
427+
428+
const customColumnIssue = function(file, col, locations) {
429+
return new Issue({
430+
code: 82,
431+
file: file,
432+
evidence:
433+
'Columns: ' +
434+
col +
435+
' not defined, please define in: ' +
436+
locations.toString().replace(',', ', '),
437+
})
438+
}
439+
347440
module.exports = {
348441
TSV: TSV,
349442
checkphenotype: checkphenotype,
443+
validateTsvColumns: validateTsvColumns,
350444
}

0 commit comments

Comments
 (0)