Skip to content
Open
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
3 changes: 1 addition & 2 deletions spec_tests/jsonTests.spec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import * as fs from 'node:fs'
import path from 'node:path'

import chai from 'chai'
const assert = chai.assert
import { assert } from 'chai'

import { beforeAll, describe, afterAll } from '@jest/globals'
import { BidsHedIssue } from '../src/bids/types/issues'
Expand Down
8 changes: 4 additions & 4 deletions src/bids/datasetParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
import fsp from 'node:fs/promises'
import path from 'node:path'

import { organizePaths } from '../utils/paths'
import { IssueError } from '../issues/issues'
import { buildBidsSchemas } from './schema'
import { type BidsJsonFile } from './types/json'
import { IssueError } from '../issues/issues'
import { type HedSchemas } from '../schema/containers'
import { organizePaths } from '../utils/paths'

type SchemaBuilder = (datasetDescription: BidsJsonFile) => Promise<HedSchemas>

Expand Down Expand Up @@ -46,7 +46,7 @@ export abstract class BidsFileAccessor<FileType> {
/**
* The HED schema builder function.
*/
protected readonly schemaBuilder: SchemaBuilder
readonly schemaBuilder: SchemaBuilder

/**
* BIDS suffixes.
Expand All @@ -64,7 +64,7 @@ export abstract class BidsFileAccessor<FileType> {
/**
* BIDS special directories.
*/
private static readonly SPECIAL_DIRS: string[] = ['phenotype', 'stimuli']
static readonly SPECIAL_DIRS: string[] = ['phenotype', 'stimuli']

/**
* Constructs a BidsFileAccessor.
Expand Down
35 changes: 17 additions & 18 deletions src/bids/types/dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
import path from 'node:path'

import { BidsFileAccessor } from '../datasetParser'
import { BidsSidecar } from './json'
import { BidsHedIssue } from './issues'
import { BidsJsonFile, BidsSidecar } from './json'
import { BidsTsvFile } from './tsv'
import { generateIssue, IssueError } from '../../issues/issues'
import { getMergedSidecarData, organizedPathsGenerator } from '../../utils/paths'
import { BidsHedIssue } from './issues'
import { type HedSchemas } from '../../schema/containers'
import { getMergedSidecarData, organizedPathsGenerator } from '../../utils/paths'

type BidsFileAccessorConstructor = {
create(datasetRootDirectory: string | object): Promise<BidsFileAccessor>
type BidsFileAccessorConstructor<FileType> = {
create(datasetRootDirectory: string | object): Promise<BidsFileAccessor<FileType>>
}

/**
Expand Down Expand Up @@ -52,7 +52,7 @@ type BidsFileAccessorConstructor = {
* @property {HedSchemas} hedSchemas The HED schemas used to validate this dataset.
* @property {BidsFileAccessor} fileAccessor The BIDS file accessor.
*/
export class BidsDataset {
export class BidsDataset<FileType> {
/**
* Map of BIDS sidecar files that contain HED annotations.
* The keys are relative paths and the values are BidsSidecar objects.
Expand All @@ -72,7 +72,7 @@ export class BidsDataset {
/**
* The BIDS file accessor.
*/
public fileAccessor: BidsFileAccessor
public fileAccessor: BidsFileAccessor<FileType>

/**
* Constructor for a BIDS dataset.
Expand All @@ -81,7 +81,7 @@ export class BidsDataset {
* @throws {IssueError} If accessor is not an instance of BidsFileAccessor.
* @see BidsDataset.create
*/
private constructor(accessor: BidsFileAccessor) {
private constructor(accessor: BidsFileAccessor<FileType>) {
if (!(accessor instanceof BidsFileAccessor)) {
IssueError.generateAndThrowInternalError('BidsDataset constructor requires an instance of BidsFileAccessor')
}
Expand All @@ -102,15 +102,15 @@ export class BidsDataset {
* @param fileAccessorClass The BidsFileAccessor class to use for accessing files.
* @returns A Promise that resolves to a two-element array containing the BidsDataset instance (or null if creation failed) and an array of issues.
*/
static async create(
static async create<FileType>(
rootOrFiles: string | object,
fileAccessorClass: BidsFileAccessorConstructor,
): Promise<[BidsDataset | null, BidsHedIssue[]]> {
fileAccessorClass: BidsFileAccessorConstructor<FileType>,
): Promise<[BidsDataset<FileType> | null, BidsHedIssue[]]> {
let dataset = null
const issues: BidsHedIssue[] = []
try {
const accessor = await fileAccessorClass.create(rootOrFiles)
dataset = new BidsDataset(accessor)
dataset = new BidsDataset<FileType>(accessor)
const schemaIssues = await dataset.setHedSchemas()
issues.push(...schemaIssues)
const sidecarIssues = await dataset.setSidecars()
Expand All @@ -134,21 +134,20 @@ export class BidsDataset {
* @throws {IssueError} If `dataset_description.json` is missing or contains an invalid HED specification.
*/
async setHedSchemas(): Promise<BidsHedIssue[]> {
const datasetDescriptionFileName = 'dataset_description.json'
let description

try {
const descriptionContentString = await this.fileAccessor.getFileContent('dataset_description.json')
const descriptionContentString = await this.fileAccessor.getFileContent(datasetDescriptionFileName)
if (descriptionContentString === null || typeof descriptionContentString === 'undefined') {
IssueError.generateAndThrow('missingSchemaSpecification', { file: 'dataset_description.json' })
}
description = {
jsonData: JSON.parse(descriptionContentString),
IssueError.generateAndThrow('missingSchemaSpecification', { file: datasetDescriptionFileName })
}
description = new BidsJsonFile(datasetDescriptionFileName, {}, JSON.parse(descriptionContentString))
} catch (e) {
if (e instanceof IssueError) {
throw e
}
IssueError.generateAndThrow('missingSchemaSpecification', { file: 'dataset_description.json' })
IssueError.generateAndThrow('missingSchemaSpecification', { file: datasetDescriptionFileName })
}

try {
Expand Down
2 changes: 1 addition & 1 deletion src/bids/types/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import BidsHedSidecarValidator from '../validator/sidecarValidator'
import { IssueError, addIssueParameters, type Issue } from '../../issues/issues'
import { DefinitionManager, Definition } from '../../parser/definitionManager'
import { type HedSchemas } from '../../schema/containers'
import { type ParsedHedColumnSplice } from '../../parser/parsedHedColumnSplice'
import type ParsedHedColumnSplice from '../../parser/parsedHedColumnSplice'

const ILLEGAL_SIDECAR_KEYS = new Set(['hed', 'n/a'])

Expand Down
94 changes: 53 additions & 41 deletions src/parser/parseUtils.js → src/parser/parseUtils.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,45 @@
/** This module holds utilities for parsing HED strings.
* @module parser/parseUtils
*/

import ParsedHedTag from './parsedHedTag'
import { type Constructor } from '../utils/types'

/**
* Extract the items of a specified subtype from a list of ParsedHedSubstring
* @param {ParsedHedSubstring[]} items - Objects to be filtered by class type.
* @param {Class} classType - The class type to filter by.
* @returns {ParsedHedSubstring[]} - A list of objects of the specified subclass of ParsedHedSubstring
* Extract the items of a specified subtype from a list of ParsedHedSubstring.
*
* @param items - Objects to be filtered by class type.
* @param classType - The class type to filter by.
* @returns A list of objects of the specified subclass of ParsedHedSubstring.
*/
export function filterByClass(items, classType) {
return items && items.length ? items.filter((item) => item instanceof classType) : []
export function filterByClass<C>(items: any[], classType: Constructor<C>): C[] {
return items?.filter((item) => item instanceof classType) ?? []
}

/**
* Extract the ParsedHedTag tags with a specified tag name
* @param {ParsedHedTag[]} tags - to be filtered by name
* @param {string} tagName - name of the tag to filter by
* @returns {ParsedHedTag[]}
*
* @param tags - to be filtered by name
* @param tagName - name of the tag to filter by
* @returns A list of tags with the name {}
*/
export function filterByTagName(tags, tagName) {
if (!tags) {
return []
}
return tags.filter((tag) => tag instanceof ParsedHedTag && tag.schemaTag?.name === tagName)
export function filterByTagName(tags: ParsedHedTag[], tagName: string): ParsedHedTag[] {
return tags?.filter((tag) => tag instanceof ParsedHedTag && tag.schemaTag?.name === tagName) ?? []
}

/**
* Extract the ParsedHedTag tags with a specified tag name.
* @param {Map<string, ParsedHedTag[]>} tagMap - The Map of parsed HED tags for extraction (must be defined).
* @param {string[]} tagNames - The names to use as keys for the filter.
* @returns {ParsedHedTag[]} - A list of temporal tags.
*
* @param tagMap - The Map of parsed HED tags for extraction (must be defined).
* @param tagNames - The names to use as keys for the filter.
* @returns A list of temporal tags.
*/
export function filterTagMapByNames(tagMap, tagNames) {
export function filterTagMapByNames(tagMap: Map<string, ParsedHedTag[]>, tagNames: string[]): ParsedHedTag[] {
if (!tagNames || tagMap.size === 0) {
return []
}

const keys = [...tagNames].filter((name) => tagMap.has(name))
const keys = tagNames.filter((name) => tagMap.has(name))
if (keys.length === 0) {
return []
}
Expand All @@ -47,59 +49,68 @@ export function filterTagMapByNames(tagMap, tagNames) {

/**
* Convert a list of ParsedHedTag objects into a comma-separated string of their string representations.
* @param {ParsedHedTag[]} tagList - The HED tags whose string representations should be put in a comma-separated list.
* @returns {string} A comma separated list of original tag names for tags in tagList.
*
* @param tagList - The HED tags whose string representations should be put in a comma-separated list.
* @returns A comma separated list of original tag names for tags in tagList.
*/
export function getTagListString(tagList) {
export function getTagListString(tagList: ParsedHedTag[]): string {
return tagList.map((tag) => tag.toString()).join(', ')
}

/**
* Create a map of the ParsedHedTags by type.
* @param {ParsedHedTag[]} tagList - The HED tags to be categorized.
* @param {Set} tagNames - The tag names to use as categories.
* @returns {Map} - A map (string --> ParsedHedTag) of tag name to a list of tags with that name.
*
* @param tagList - The HED tags to be categorized.
* @param tagNames - The tag names to use as categories.
* @returns A map of tag name to a list of tags with that name.
*/
export function categorizeTagsByName(tagList, tagNames = null) {
export function categorizeTagsByName(
tagList: ParsedHedTag[],
tagNames: Set<string> | null = null,
): Map<string, ParsedHedTag[]> {
// Initialize the map with keys from tagNames and an "other" key
const resultMap = new Map()
const resultMap = new Map<string, ParsedHedTag[]>()

// Iterate through A and categorize
tagList.forEach((tag) => {
for (const tag of tagList) {
if (!tagNames || tagNames.has(tag.schemaTag.name)) {
const tagList = resultMap.get(tag.schemaTag.name) || []
const tagList = resultMap.get(tag.schemaTag.name) ?? []
tagList.push(tag)
resultMap.set(tag.schemaTag.name, tagList) // Add to matching key list
}
})
}

return resultMap
}

/**
* Return a list of duplicate strings.
* @param { string[] } itemList - A list of strings to look for duplicates in.
* @returns {string[]} - A list of unique duplicate strings (multiple copies not repeated).
* Return a list of duplicates.
*
* @param itemList - A list of items in which to look for duplicates.
* @returns A list of unique duplicates (multiple copies not repeated).
*/
export function getDuplicates(itemList) {
const checkSet = new Set()
const dupSet = new Set()
export function getDuplicates<T>(itemList: T[]): T[] {
const checkSet = new Set<T>()
const dupSet = new Set<T>()

for (const item of itemList) {
if (!checkSet.has(item)) {
checkSet.add(item)
} else {
dupSet.add(item)
}
}

return [...dupSet]
}

/**
* lean up a string and remove redundant commas and parentheses.
* @param {string} stringIn - The input string to be cleaned up.
* @return {string} - The cleaned-up string with redundant commas and parentheses removed.
* Clean up a string and remove redundant commas and parentheses.
*
* @param stringIn - The input string to be cleaned up.
* @return The cleaned-up string with redundant commas and parentheses removed.
*/
export function cleanupEmpties(stringIn) {
export function cleanupEmpties(stringIn: string): string {
const leadingCommaRegEx = /^\s*,+/g // Remove leading commas
const trailingCommaRegEx = /,\s*$/g // Remove trailing commas
const innerCommaRegEx = /,\s*,+/g // Collapse multiple commas inside
Expand All @@ -108,7 +119,7 @@ export function cleanupEmpties(stringIn) {
const trailingInnerCommaRegEx = /[\s,]+\)/g // Remove trailing commas and spaces inside parentheses

let result = stringIn
let previousResult
let previousResult: string

do {
previousResult = result
Expand All @@ -131,6 +142,7 @@ export function cleanupEmpties(stringIn) {
// Step 5: Remove trailing commas inside parentheses
result = result.replace(trailingInnerCommaRegEx, ')')
} while (result !== previousResult) // Keep looping until no more changes

result = result.replace(/\(\s*,+/g, '(')
return result.trim()
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,55 @@
*/

import ParsedHedSubstring from './parsedHedSubstring'
import ParsedHedGroup from './parsedHedGroup'
import ParsedHedString from './parsedHedString'

/**
* A template for an inline column splice in a HED string.
*
* @see {@link ParsedHedString}
* @see {@link ParsedHedGroup}
*/
export class ParsedHedColumnSplice extends ParsedHedSubstring {
export default class ParsedHedColumnSplice extends ParsedHedSubstring {
/**
* The normalized string representation of this column splice.
* @type {string}
* @private
*/
_normalized
private readonly _normalized: string

/**
* Constructor.
*
* @param {string} columnName The token for this tag.
* @param {number[]} bounds The collection of HED schemas.
* @param columnName The name of the referenced column.
* @param bounds The bounds of the column splice.
*/
constructor(columnName, bounds) {
public constructor(columnName: string, bounds: [number, number]) {
super(columnName, bounds) // Sets originalTag and originalBounds
this._normalized = this.format(false) // Sets various forms of the tag.
}

/**
* Get the normalized version of the object.
*
* @returns {string}
*/
get normalized() {
public get normalized(): string {
return this._normalized
}

/**
* Nicely format this column splice template.
*
* @param {boolean} long Whether the tags should be in long form.
* @returns {string} The formatted column splice template.
* @param long Whether the tags should be in long form.
* @returns The formatted column splice template.
*/
// eslint-disable-next-line no-unused-vars
format(long = true) {
public format(long: boolean = true): string {
return '{' + this.originalTag + '}'
}

/**
* Determine if this column splice is equivalent to another.
*
* @param {ParsedHedColumnSplice} other The other column splice.
* @returns {boolean} Whether the two column splices are equivalent.
* @param other The other column splice.
* @returns Whether the two column splices are equivalent.
*/
equivalent(other) {
public equivalent(other: unknown): boolean {
return other instanceof ParsedHedColumnSplice && this.originalTag === other.originalTag
}
}

export default ParsedHedColumnSplice
Loading