import differenceWith from 'lodash/differenceWith'
import { IssueError } from '../issues/issues'
import ParsedHedSubstring from './parsedHedSubstring'
import ParsedHedTag from './parsedHedTag'
import ParsedHedColumnSplice from './parsedHedColumnSplice'
import { ReservedChecker } from './reservedChecker'
import { filterByClass, categorizeTagsByName, getDuplicates, filterByTagName } from './parseUtils'
/**
* A parsed HED tag group.
*/
export default class ParsedHedGroup extends ParsedHedSubstring {
/**
* The parsed HED tags, groups, or splices in the HED tag group at the top level.
* @type {ParsedHedSubstring[]}
*/
tags
/**
* The top-level parsed HED tags in this string.
* @type {ParsedHedTag[]}
*/
topTags
/**
* The top-level parsed HED groups in this string.
* @type {ParsedHedGroup[]}
*/
topGroups
/**
* The top-level column splices in this string
* @type {ParsedHedColumnSplice[]}
*/
topSplices
/**
* All the parsed HED tags in this string.
* @type {ParsedHedTag[]}
*/
allTags
/**
* Reserved HED group tags. This only covers top group tags in the group.
* @type {Map<string, ParsedHedTag[]>}
*/
reservedTags
/**
* The top-level child subgroups containing Def-expand tags.
* @type {ParsedHedGroup[]}
*/
defExpandChildren
/**
* The top-level Def tags
* @type {ParsedHedTag[]}
*/
defTags
/**
* The top-level Def-expand tags
* @type {ParsedHedTag[]}
*/
defExpandTags
/**
* True if this group has a Definition tag at the top level.
* @type {boolean}
*/
isDefinitionGroup
/**
* The total number of top-level Def tags and top-level Def-expand groups.
* @type {Number}
*/
defCount
/**
* The unique top-level tag requiring a Def or Def-expand group, if any.
* @type {ParsedHedTag[] | null}
*/
requiresDefTag
/**
* Constructor.
* @param {ParsedHedSubstring[]} parsedHedTags The parsed HED tags, groups or column splices in the HED tag group.
* @param {string} hedString The original HED string.
* @param {number[]} originalBounds The bounds of the HED tag in the original HED string.
*/
constructor(parsedHedTags, hedString, originalBounds) {
const originalTag = hedString.substring(originalBounds[0], originalBounds[1])
super(originalTag, originalBounds)
this.tags = parsedHedTags
this.topGroups = filterByClass(parsedHedTags, ParsedHedGroup)
this.topTags = filterByClass(parsedHedTags, ParsedHedTag)
this.topSplices = filterByClass(parsedHedTags, ParsedHedColumnSplice)
this.allTags = this._getAllTags()
this._normalized = undefined
this._initializeGroups()
}
/**
* Recursively create a list of all the tags in this group.
* @returns {ParsedHedTag[]}
* @private
*/
_getAllTags() {
const subgroupTags = this.topGroups.flatMap((tagGroup) => tagGroup.allTags)
return this.topTags.concat(subgroupTags)
}
/**
* Sets information about the reserved tags, particularly definition-related tags in this group.
* @private
*/
_initializeGroups() {
const reserved = ReservedChecker.getInstance()
this.reservedTags = categorizeTagsByName(this.topTags, reserved.reservedNames)
this.defExpandTags = this._filterTopTagsByTagName('Def-expand')
this.definitionTags = this._filterTopTagsByTagName('Definition')
this.defExpandChildren = this._filterSubgroupsByTagName('Def-expand')
this.defTags = this._filterTopTagsByTagName('Def')
this.defCount = this.defTags.length + this.defExpandChildren.length
this.isDefinitionGroup = this.definitionTags.length > 0
this.requiresDefTag = [...this.reservedTags.entries()]
.filter((pair) => reserved.requiresDefTags.has(pair[0]))
.flatMap((pair) => pair[1]) // Flatten the values into a single list
}
/**
* Filter top tags by tag name.
*
* @param {string} tagName - The schemaTag name to filter by.
* @returns {Array} - An array of
* @private
*
*/
_filterTopTagsByTagName(tagName) {
return this.topTags.filter((tag) => tag.schemaTag._name === tagName)
}
/**
* Filter top subgroups that include a tag at the top-level of the group.
*
* @param {string} tagName - The schemaTag name to filter by.
* @returns {Array} - Array of subgroups containing the specified tag.
* @private
*/
_filterSubgroupsByTagName(tagName) {
return Array.from(this.topLevelGroupIterator()).filter((subgroup) =>
subgroup.topTags.some((tag) => tag.schemaTag.name === tagName),
)
}
/**
* Nicely format this tag group.
*
* @param {boolean} long Whether the tags should be in long form.
* @returns {string}
*/
format(long = true) {
return '(' + this.tags.map((substring) => substring.format(long)).join(', ') + ')'
}
equivalent(other) {
if (!(other instanceof ParsedHedGroup)) {
return false
}
const equivalence = (ours, theirs) => ours.equivalent(theirs)
return (
differenceWith(this.tags, other.tags, equivalence).length === 0 &&
differenceWith(other.tags, this.tags, equivalence).length === 0
)
}
/**
* Return a normalized string representation
* @returns {string}
*/
get normalized() {
if (this._normalized) {
return this._normalized
}
// Recursively normalize each item in the group
const normalizedItems = this.tags.map((item) => item.normalized)
// Sort normalized items to ensure order independence
const sortedNormalizedItems = normalizedItems.sort()
const duplicates = getDuplicates(sortedNormalizedItems)
if (duplicates.length > 0) {
IssueError.generateAndThrow('duplicateTag', {
tags: '[' + duplicates.join('],[') + ']',
string: this.originalTag,
})
}
this._normalized = '(' + sortedNormalizedItems.join(',') + ')'
// Return the normalized group as a string
return `(${sortedNormalizedItems.join(',')})` // Using curly braces to indicate unordered group
}
/**
* Iterator over the ParsedHedGroup objects in this HED tag group.
* @param {string | null} tagName - The name of the tag whose groups are to be iterated over or null if all tags.
* @yields {ParsedHedGroup} - This object and the ParsedHedGroup objects belonging to this tag group.
*/
*subParsedGroupIterator(tagName = null) {
if (!tagName || filterByTagName(this.topTags, tagName)) {
yield this
}
for (const innerTag of this.tags) {
if (innerTag instanceof ParsedHedGroup) {
yield* innerTag.subParsedGroupIterator(tagName)
}
}
}
/**
* Iterator over the parsed HED tags in this HED tag group.
*
* @yields {ParsedHedTag} This tag group's HED tags.
*/
*tagIterator() {
for (const innerTag of this.tags) {
if (innerTag instanceof ParsedHedTag) {
yield innerTag
} else if (innerTag instanceof ParsedHedGroup) {
yield* innerTag.tagIterator()
}
}
}
/**
* Iterator over the parsed HED column splices in this HED tag group.
*
* @yields {ParsedHedColumnSplice} This tag group's HED column splices.
*/
*columnSpliceIterator() {
for (const innerTag of this.tags) {
if (innerTag instanceof ParsedHedColumnSplice) {
yield innerTag
} else if (innerTag instanceof ParsedHedGroup) {
yield* innerTag.columnSpliceIterator()
}
}
}
/**
* Iterator over the top-level parsed HED groups in this HED tag group.
*
* @yields {ParsedHedTag} This tag group's top-level HED groups.
*/
*topLevelGroupIterator() {
for (const innerTag of this.tags) {
if (innerTag instanceof ParsedHedGroup) {
yield innerTag
}
}
}
}