Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create new classes for TSV rows and sidecar keys #132

Merged
merged 2 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 116 additions & 32 deletions bids/types/json.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export class BidsJsonFile extends BidsFile {
}

export class BidsSidecar extends BidsJsonFile {
/**
* The extracted keys for this sidecar.
* @type {Map<string, BidsSidecarKey>}
*/
sidecarKeys
/**
* The extracted HED data for this sidecar.
* @type {Map<string, string|Object<string, string>>}
Expand Down Expand Up @@ -70,23 +75,26 @@ export class BidsSidecar extends BidsJsonFile {
const sidecarHedTags = Object.entries(this.jsonData)
.map(([sidecarKey, sidecarValue]) => {
if (sidecarValueHasHed(sidecarValue)) {
return [sidecarKey, sidecarValue.HED]
return [sidecarKey, new BidsSidecarKey(sidecarKey, sidecarValue.HED)]
} else {
return []
return null
}
})
.filter((x) => x.length > 0)
this.hedData = new Map(sidecarHedTags)
.filter((x) => x !== null)
this.sidecarKeys = new Map(sidecarHedTags)
}

_categorizeHedStrings() {
this.hedValueStrings = []
this.hedCategoricalStrings = []
for (const sidecarValue of this.hedData.values()) {
if (typeof sidecarValue === 'string') {
this.hedValueStrings.push(sidecarValue)
this.hedData = new Map()
for (const [key, sidecarValue] of this.sidecarKeys.entries()) {
if (sidecarValue.isValueKey) {
this.hedValueStrings.push(sidecarValue.valueString)
this.hedData.set(key, sidecarValue.valueString)
} else {
this.hedCategoricalStrings.push(...Object.values(sidecarValue))
this.hedCategoricalStrings.push(...Object.values(sidecarValue.categoryMap))
this.hedData.set(key, sidecarValue.categoryMap)
}
}
}
Expand All @@ -97,7 +105,7 @@ export class BidsSidecar extends BidsJsonFile {
* @returns {boolean}
*/
hasHedData() {
return this.hedData.size > 0
return this.sidecarKeys.size > 0
}

/**
Expand All @@ -111,34 +119,18 @@ export class BidsSidecar extends BidsJsonFile {
parseHedStrings(hedSchemas) {
this.parsedHedData = new Map()
const issues = []
for (const [key, value] of this.hedData) {
issues.push(...this._parseSidecarKey(key, value, hedSchemas))
for (const [name, sidecarKey] of this.sidecarKeys.entries()) {
issues.push(...sidecarKey.parseHed(hedSchemas))
if (sidecarKey.isValueKey) {
this.parsedHedData.set(name, sidecarKey.parsedValueString)
} else {
this.parsedHedData.set(name, sidecarKey.parsedCategoryMap)
}
}
this._generateSidecarColumnSpliceMap()
return issues
}

_parseSidecarKey(key, data, hedSchemas) {
if (typeof data === 'string') {
return this._parseHedString(this.parsedHedData, key, data, hedSchemas)
} else if (data !== Object(data)) {
return [generateIssue('illegalSidecarHedType', { key: key, file: this.name })]
}
const issues = []
const keyMap = new Map()
for (const [value, string] of Object.entries(data)) {
issues.push(...this._parseHedString(keyMap, value, string, hedSchemas))
}
this.parsedHedData.set(key, keyMap)
return issues
}

_parseHedString(map, key, string, hedSchemas) {
const [parsedString, parsingIssues] = parseHedString(string, hedSchemas)
map.set(key, parsedString)
return Object.values(parsingIssues).flat()
}

/**
* Generate a mapping of an individual BIDS sidecar's curly brace references.
*
Expand Down Expand Up @@ -206,6 +198,98 @@ export class BidsSidecar extends BidsJsonFile {
}
}

export class BidsSidecarKey {
/**
* The name of this key.
* @type {string}
*/
name
/**
* The unparsed category mapping.
* @type {Object<string, string>}
*/
categoryMap
/**
* The parsed category mapping.
* @type {Map<string, ParsedHedString>}
*/
parsedCategoryMap
/**
* The unparsed value string.
* @type {string}
*/
valueString
/**
* The parsed value string.
* @type {ParsedHedString}
*/
parsedValueString

/**
* Constructor.
*
* @param {string} key The name of this key.
* @param {string|Object<string, string>} data The data for this key.
*/
constructor(key, data) {
this.name = key
if (typeof data === 'string') {
this.valueString = data
} else if (data !== Object(data)) {
throw new Error('Non-object passed as categorical data.')
} else {
this.categoryMap = data
}
}

/**
* Parse the HED data for this key.
*
* @param {Schemas} hedSchemas The HED schema collection.
* @returns {Issue[]} Any issues found.
*/
parseHed(hedSchemas) {
if (this.isValueKey) {
return this._parseValueString(hedSchemas)
}
return this._parseCategory(hedSchemas)
}

_parseValueString(hedSchemas) {
const [parsedString, parsingIssues] = parseHedString(this.valueString, hedSchemas)
const flatIssues = Object.values(parsingIssues).flat()
this.parsedValueString = parsedString
return flatIssues
}

_parseCategory(hedSchemas) {
const issues = []
this.parsedCategoryMap = new Map()
for (const [value, string] of Object.entries(this.categoryMap)) {
const [parsedString, parsingIssues] = parseHedString(string, hedSchemas)
this.parsedCategoryMap.set(value, parsedString)
issues.push(...Object.values(parsingIssues).flat())
}
return issues
}

/**
* Whether this key is a categorical key.
* @returns {boolean}
*/
get isCategoricalKey() {
return Boolean(this.categoryMap)
}

/**
* Whether this key is a value key.
* @returns {boolean}
*/
get isValueKey() {
return Boolean(this.valueString)
}
}

/**
* Fallback default dataset_description.json file.
* @deprecated Will be removed in v4.0.0.
Expand Down
49 changes: 49 additions & 0 deletions bids/types/tsv.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BidsFile } from './basic'
import { convertParsedTSVData, parseTSV } from '../tsvParser'
import { BidsSidecar } from './json'
import ParsedHedString from '../../parser/parsedHedString'

/**
* A BIDS TSV file.
Expand Down Expand Up @@ -130,3 +131,51 @@ export class BidsTabularFile extends BidsTsvFile {
super(name, tsvData, file, potentialSidecars, mergedDictionary)
}
}

export class BidsTsvRow extends ParsedHedString {
/**
* The parsed string representing this row.
* @type {ParsedHedString}
*/
parsedString
/**
* The column-to-value mapping for this row.
* @type {Map<string, string>}
*/
rowCells
/**
* The file this row belongs to.
* @type {BidsTsvFile}
*/
tsvFile
/**
* The line number in {@link BidsTsvRow.tsvFile} this line is located at.
* @type {number}
*/
tsvLine

/**
* Constructor.
* @param {ParsedHedString} parsedString The parsed string representing this row.
* @param {Map<string, string>} rowCells The column-to-value mapping for this row.
* @param {BidsTsvFile} tsvFile The file this row belongs to.
* @param {number} tsvLine The line number in {@link tsvFile} this line is located at.
*/
constructor(parsedString, rowCells, tsvFile, tsvLine) {
super(parsedString.hedString, parsedString.parseTree)
this.parsedString = parsedString
this.context = parsedString.context
this.rowCells = rowCells
this.tsvFile = tsvFile
this.tsvLine = tsvLine
}

/**
* Override of {@link Object.prototype.toString}.
*
* @returns {string}
*/
toString() {
return super.toString() + ` in TSV file "${this.tsvFile.name}" at line ${this.tsvLine}`
}
}
22 changes: 6 additions & 16 deletions bids/validator/bidsHedTsvValidator.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BidsHedSidecarValidator } from './bidsHedSidecarValidator'
import { BidsHedIssue } from '../types/issues'
import { BidsEventFile } from '../types/tsv'
import { BidsTsvRow } from '../types/tsv'
import { parseHedString } from '../../parser/main'
import ColumnSplicer from '../../parser/columnSplicer'
import ParsedHedString from '../../parser/parsedHedString'
Expand Down Expand Up @@ -67,15 +67,10 @@ export class BidsHedTsvValidator {
/**
* Combine the BIDS sidecar HED data into a BIDS TSV file's HED data.
*
* @returns {ParsedHedString[]} The combined HED string collection for this BIDS TSV file.
* @returns {BidsTsvRow[]} The combined HED string collection for this BIDS TSV file.
*/
parseHed() {
const tsvHedRows = this._generateHedRows()
if (tsvHedRows === null) {
// There is no HED data.
return []
}

const hedStrings = []

tsvHedRows.forEach((row, index) => {
Expand All @@ -98,16 +93,11 @@ export class BidsHedTsvValidator {
const tsvHedColumns = Array.from(this.tsvFile.parsedTsv.entries()).filter(
([header]) => this.tsvFile.sidecarHedData.has(header) || header === 'HED',
)
if (tsvHedColumns.length === 0) {
return null
}

const tsvHedRows = []
for (const [header, data] of tsvHedColumns) {
data.forEach((value, index) => {
if (tsvHedRows[index] === undefined) {
tsvHedRows[index] = new Map()
}
tsvHedRows[index] ??= new Map()
tsvHedRows[index].set(header, value)
})
}
Expand All @@ -119,7 +109,7 @@ export class BidsHedTsvValidator {
*
* @param {Map<string, string>} rowCells The column-to-value mapping for a single row.
* @param {number} tsvLine The index of this row in the TSV file.
* @return {ParsedHedString} A parsed HED string.
* @return {BidsTsvRow} A parsed HED string.
* @private
*/
_parseHedRow(rowCells, tsvLine) {
Expand All @@ -142,7 +132,7 @@ export class BidsHedTsvValidator {
* @param {Map<string, string>} rowCells The column-to-value mapping for a single row.
* @param {number} tsvLine The index of this row in the TSV file.
* @param {string} hedString The unparsed HED string for this row.
* @return {ParsedHedString} A parsed HED string.
* @return {BidsTsvRow} A parsed HED string.
* @private
*/
_parseHedRowString(rowCells, tsvLine, hedString) {
Expand All @@ -164,7 +154,7 @@ export class BidsHedTsvValidator {
}
splicedParsedString.context.set('tsvLine', tsvLine)

return splicedParsedString
return new BidsTsvRow(splicedParsedString, rowCells, this.tsvFile, tsvLine)
}

/**
Expand Down
3 changes: 3 additions & 0 deletions common/issues/issues.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ export class Issue {
if (this.parameters.tsvLine) {
message += ` TSV line: ${this.parameters.tsvLine}.`
}
if (this.parameters.hedString) {
message += ` HED string: "${this.parameters.hedString}".`
}
const hedCodeAnchor = this.hedCode.toLowerCase().replace(/_/g, '-')
const hedSpecLink = `For more information on this HED ${this.level}, see https://hed-specification.readthedocs.io/en/latest/Appendix_B.html#${hedCodeAnchor}`
this.message = `${this.level.toUpperCase()}: [${this.hedCode}] ${message} (${hedSpecLink}.)`
Expand Down
9 changes: 9 additions & 0 deletions parser/parsedHedString.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ export class ParsedHedString {
return [group.definitionName, group]
})
}

/**
* Override of {@link Object.prototype.toString}.
*
* @returns {string}
*/
toString() {
return this.hedString
}
}

export default ParsedHedString
Loading