@gmod/bed
Version:
A BED file format parser with autoSql support
107 lines (98 loc) • 3.52 kB
text/typescript
import parser from './autoSql.ts'
import types from './defaultTypes.ts'
import { detectTypes, AutoSqlSchema, AutoSqlPreSchema } from './util.ts'
const strandMap = { '.': 0, '-': -1, '+': 1 }
// heuristic that a BED file is BED12 like...the number in col 10 is
// blockCount-like
function isBed12Like(fields: string[]) {
return (
fields.length >= 12 &&
!Number.isNaN(Number.parseInt(fields[9], 10)) &&
fields[10]?.split(',').filter(f => !!f).length ===
Number.parseInt(fields[9], 10)
)
}
export default class BED {
public autoSql: AutoSqlSchema
private attemptDefaultBed?: boolean
constructor(arguments_: { autoSql?: string; type?: string } = {}) {
if (arguments_.autoSql) {
this.autoSql = detectTypes(
// @ts-expect-error
parser.parse(arguments_.autoSql) as AutoSqlPreSchema,
)
} else if (arguments_.type) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!types[arguments_.type]) {
throw new Error('Type not found')
}
this.autoSql = detectTypes(types[arguments_.type])
} else {
this.autoSql = detectTypes(types.defaultBedSchema)
this.attemptDefaultBed = true
}
}
/*
* parses a line of text as a BED line with the loaded autoSql schema
*
* @param line - a BED line as tab delimited text or array
* @param opts - supply opts.uniqueId
* @return a object representing a feature
*/
parseLine(line: string | string[], options: { uniqueId?: string } = {}) {
const { autoSql } = this
const { uniqueId } = options
const fields = Array.isArray(line) ? line : line.split('\t')
let feature = {} as Record<string, any>
if (
!this.attemptDefaultBed ||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
(this.attemptDefaultBed && isBed12Like(fields))
) {
for (let index = 0; index < autoSql.fields.length; index++) {
const autoField = autoSql.fields[index]
let columnValue: any = fields[index]
const { isNumeric, isArray, arrayIsNumeric, name } = autoField
if (columnValue === null || columnValue === undefined) {
break
}
if (columnValue !== '.') {
if (isNumeric) {
const number_ = Number(columnValue)
columnValue = Number.isNaN(number_) ? columnValue : number_
} else if (isArray) {
columnValue = columnValue.split(',')
if (columnValue.at(-1) === '') {
columnValue.pop()
}
if (arrayIsNumeric) {
columnValue = columnValue.map(Number)
}
}
feature[name] = columnValue
}
}
} else {
const fieldNames = ['chrom', 'chromStart', 'chromEnd', 'name']
feature = Object.fromEntries(
fields.map((f, index) => [fieldNames[index] || 'field' + index, f]),
)
feature.chromStart = +feature.chromStart
feature.chromEnd = +feature.chromEnd
if (!Number.isNaN(Number.parseFloat(feature.field4))) {
feature.score = +feature.field4
delete feature.field4
}
if (feature.field5 === '+' || feature.field5 === '-') {
feature.strand = feature.field5
delete feature.field5
}
}
if (uniqueId) {
feature.uniqueId = uniqueId
}
feature.strand = strandMap[feature.strand as keyof typeof strandMap] || 0
feature.chrom = decodeURIComponent(feature.chrom)
return feature
}
}