Skip to content

Commit 83be143

Browse files
authored
Merge pull request #143 from hed-standard/bids-tsv-event
Make TSV validation event-based
2 parents b458473 + 927e12d commit 83be143

File tree

3 files changed

+132
-11
lines changed

3 files changed

+132
-11
lines changed

bids/types/tsv.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ export class BidsTabularFile extends BidsTsvFile {
132132
}
133133
}
134134

135+
/**
136+
* A row in a BIDS TSV file.
137+
*/
135138
export class BidsTsvRow extends ParsedHedString {
136139
/**
137140
* The parsed string representing this row.
@@ -156,6 +159,7 @@ export class BidsTsvRow extends ParsedHedString {
156159

157160
/**
158161
* Constructor.
162+
*
159163
* @param {ParsedHedString} parsedString The parsed string representing this row.
160164
* @param {Map<string, string>} rowCells The column-to-value mapping for this row.
161165
* @param {BidsTsvFile} tsvFile The file this row belongs to.
@@ -178,4 +182,63 @@ export class BidsTsvRow extends ParsedHedString {
178182
toString() {
179183
return super.toString() + ` in TSV file "${this.tsvFile.name}" at line ${this.tsvLine}`
180184
}
185+
186+
/**
187+
* The onset of this row.
188+
*
189+
* @return {number} The onset of this row.
190+
*/
191+
get onset() {
192+
const value = Number(this.rowCells.get('onset'))
193+
if (Number.isNaN(value)) {
194+
throw new Error('Attempting to access the onset of a TSV row without one.')
195+
}
196+
return value
197+
}
198+
}
199+
200+
/**
201+
* An event in a BIDS TSV file.
202+
*/
203+
export class BidsTsvEvent extends ParsedHedString {
204+
/**
205+
* The file this row belongs to.
206+
* @type {BidsTsvFile}
207+
*/
208+
tsvFile
209+
/**
210+
* The TSV rows making up this event.
211+
* @type {BidsTsvRow[]}
212+
*/
213+
tsvRows
214+
215+
/**
216+
* Constructor.
217+
*
218+
* @param {BidsTsvFile} tsvFile The file this row belongs to.
219+
* @param {BidsTsvRow[]} tsvRows The TSV rows making up this event.
220+
*/
221+
constructor(tsvFile, tsvRows) {
222+
super(tsvRows.map((tsvRow) => tsvRow.hedString).join(', '), tsvRows.map((tsvRow) => tsvRow.parseTree).flat())
223+
this.tsvFile = tsvFile
224+
this.tsvRows = tsvRows
225+
}
226+
227+
/**
228+
* The lines in the TSV file corresponding to this event.
229+
*
230+
* @return {string} The lines in the TSV file corresponding to this event.
231+
*/
232+
get tsvLines() {
233+
return this.tsvRows.map((tsvRow) => tsvRow.tsvLine).join(', ')
234+
}
235+
236+
/**
237+
* Override of {@link Object.prototype.toString}.
238+
*
239+
* @returns {string}
240+
*/
241+
toString() {
242+
return super.toString() + ` in TSV file "${this.tsvFile.name}" at line(s) ${this.tsvLines}`
243+
}
181244
}

bids/validator/bidsHedTsvValidator.js

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { BidsHedSidecarValidator } from './bidsHedSidecarValidator'
22
import { BidsHedIssue } from '../types/issues'
3-
import { BidsTsvRow } from '../types/tsv'
3+
import { BidsTsvEvent, BidsTsvRow } from '../types/tsv'
44
import { parseHedString } from '../../parser/main'
55
import ColumnSplicer from '../../parser/columnSplicer'
66
import ParsedHedString from '../../parser/parsedHedString'
77
import { generateIssue } from '../../common/issues/issues'
88
import { validateHedDatasetWithContext } from '../../validator/dataset'
9+
import { groupBy } from '../../utils/map'
910

1011
/**
1112
* Validator for HED data in BIDS TSV files.
@@ -67,18 +68,15 @@ export class BidsHedTsvValidator {
6768
/**
6869
* Combine the BIDS sidecar HED data into a BIDS TSV file's HED data.
6970
*
70-
* @returns {BidsTsvRow[]} The combined HED string collection for this BIDS TSV file.
71+
* @returns {ParsedHedString[]} The combined HED string collection for this BIDS TSV file.
7172
*/
7273
parseHed() {
7374
const tsvHedRows = this._generateHedRows()
74-
const hedStrings = []
75+
const hedStrings = this._parseHedRows(tsvHedRows)
7576

76-
tsvHedRows.forEach((row, index) => {
77-
const hedString = this._parseHedRow(row, index + 2)
78-
if (hedString !== null) {
79-
hedStrings.push(hedString)
80-
}
81-
})
77+
if (this.tsvFile.isTimelineFile) {
78+
return this._mergeEventRows(hedStrings)
79+
}
8280

8381
return hedStrings
8482
}
@@ -91,7 +89,7 @@ export class BidsHedTsvValidator {
9189
*/
9290
_generateHedRows() {
9391
const tsvHedColumns = Array.from(this.tsvFile.parsedTsv.entries()).filter(
94-
([header]) => this.tsvFile.sidecarHedData.has(header) || header === 'HED',
92+
([header]) => this.tsvFile.sidecarHedData.has(header) || header === 'HED' || header === 'onset',
9593
)
9694

9795
const tsvHedRows = []
@@ -104,6 +102,44 @@ export class BidsHedTsvValidator {
104102
return tsvHedRows
105103
}
106104

105+
/**
106+
* Parse the HED rows in the TSV file.
107+
*
108+
* @param {Map<string, string>[]} tsvHedRows A list of single-row column-to-value mappings.
109+
* @return {BidsTsvRow[]} A list of row-based parsed HED strings.
110+
* @private
111+
*/
112+
_parseHedRows(tsvHedRows) {
113+
const hedStrings = []
114+
115+
tsvHedRows.forEach((row, index) => {
116+
const hedString = this._parseHedRow(row, index + 2)
117+
if (hedString !== null) {
118+
hedStrings.push(hedString)
119+
}
120+
})
121+
return hedStrings
122+
}
123+
124+
/**
125+
* Merge rows with the same onset time into a single event string.
126+
*
127+
* @param {BidsTsvRow[]} rowStrings A list of row-based parsed HED strings.
128+
* @return {BidsTsvEvent[]} A list of event-based parsed HED strings.
129+
* @private
130+
*/
131+
_mergeEventRows(rowStrings) {
132+
const groupedTsvRows = groupBy(rowStrings, (rowString) => rowString.onset)
133+
const sortedOnsetTimes = Array.from(groupedTsvRows.keys()).sort((a, b) => a - b)
134+
const eventStrings = []
135+
for (const onset of sortedOnsetTimes) {
136+
const onsetRows = groupedTsvRows.get(onset)
137+
const onsetEventString = new BidsTsvEvent(this.tsvFile, onsetRows)
138+
eventStrings.push(onsetEventString)
139+
}
140+
return eventStrings
141+
}
142+
107143
/**
108144
* Parse a row in a TSV file.
109145
*
@@ -141,7 +177,7 @@ export class BidsHedTsvValidator {
141177
const [parsedString, parsingIssues] = parseHedString(hedString, this.hedSchemas)
142178
const flatParsingIssues = Object.values(parsingIssues).flat()
143179
if (flatParsingIssues.length > 0) {
144-
this.issues.push(...BidsHedIssue.fromHedIssues(...flatParsingIssues, this.tsvFile.file, { tsvLine }))
180+
this.issues.push(...BidsHedIssue.fromHedIssues(flatParsingIssues, this.tsvFile.file, { tsvLine }))
145181
return null
146182
}
147183

utils/map.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import identity from 'lodash/identity'
12
import isEqual from 'lodash/isEqual'
23

34
/**
@@ -27,3 +28,24 @@ export const filterNonEqualDuplicates = function (list, equalityFunction = isEqu
2728
}
2829
return [map, duplicates]
2930
}
31+
32+
/**
33+
* Group a list by a given grouping function.
34+
*
35+
* @template T, U
36+
* @param {T[]} list The list to group.
37+
* @param {function (T): U} groupingFunction A function mapping a list value to the key it is to be grouped under.
38+
* @return {Map<U, T[]>} The grouped map.
39+
*/
40+
export const groupBy = function (list, groupingFunction = identity) {
41+
const groupingMap = new Map()
42+
for (const listEntry of list) {
43+
const groupingValue = groupingFunction(listEntry)
44+
if (groupingMap.has(groupingValue)) {
45+
groupingMap.get(groupingValue).push(listEntry)
46+
} else {
47+
groupingMap.set(groupingValue, [listEntry])
48+
}
49+
}
50+
return groupingMap
51+
}

0 commit comments

Comments
 (0)