UNPKG

@gmod/vcf

Version:

High performance streaming Variant Call Format (VCF) parser in pure JavaScript

150 lines (136 loc) 4.42 kB
import { parseGenotypesOnly } from './parseGenotypesOnly.ts' import { parseInfo } from './parseInfo.ts' import { processGenotypes } from './processGenotypes.ts' import type { InfoValue, MetaMap } from './parseInfo.ts' import type { GenotypeCallback } from './processGenotypes.ts' export type SampleValue = (string | number | undefined)[] | undefined export type SampleData = Record<string, SampleValue> export type Samples = Record<string, SampleData> export class Variant { CHROM: string | undefined POS: number ID: string[] | undefined REF: string | undefined ALT: string[] | undefined QUAL: number | undefined FILTER: string | string[] | undefined INFO: Record<string, InfoValue> FORMAT: string | undefined private formatMeta: MetaMap private rest: string private sampleNames: string[] constructor( line: string, infoMeta: MetaMap, formatMeta: MetaMap, sampleNames: string[], strict: boolean, ) { const lineLen = line.length let currChar = 0 let tabCount = 0 while (currChar < lineLen && tabCount < 9) { if (line.charCodeAt(currChar) === 9) { tabCount += 1 } currChar += 1 } const splitPos = tabCount === 9 ? currChar - 1 : currChar const fields = line.slice(0, splitPos).split('\t') const rest = line.slice(splitPos + 1) const [CHROM, POS, ID, REF, ALT, QUAL, FILTER] = fields const filter = FILTER === '.' ? undefined : FILTER?.split(';') if (strict && !fields[7]) { throw new Error( "no INFO field specified, must contain at least a '.' (turn off strict mode to allow)", ) } this.CHROM = CHROM this.POS = POS !== undefined ? +POS : 0 this.ID = ID === '.' ? undefined : ID?.split(';') this.REF = REF this.ALT = ALT === '.' ? undefined : ALT?.split(',') this.QUAL = QUAL === undefined || QUAL === '.' ? undefined : +QUAL this.FILTER = filter?.length === 1 && filter[0] === 'PASS' ? 'PASS' : filter this.INFO = fields[7] === undefined || fields[7] === '.' ? {} : parseInfo(fields[7], infoMeta) this.FORMAT = fields[8] this.formatMeta = formatMeta this.rest = rest this.sampleNames = sampleNames } SAMPLES(): Samples { const genotypes: Samples = {} const format = this.FORMAT if (format) { const rest = this.rest.split('\t') const formatKeys = format.split(':') const isNumberType = formatKeys.map(k => { const t = this.formatMeta[k]?.Type return t === 'Integer' || t === 'Float' }) const numKeys = formatKeys.length const samplesLen = this.sampleNames.length for (let i = 0; i < samplesLen; i++) { const sample = this.sampleNames[i] ?? '' const sampleData: SampleData = {} const sampleStr = rest[i] ?? '' const sampleStrLen = sampleStr.length let colStart = 0 let colIdx = 0 for (let j = 0; j <= sampleStrLen; j++) { if (j === sampleStrLen || sampleStr[j] === ':') { const key = formatKeys[colIdx] ?? '' const val = sampleStr.slice(colStart, j) if (val === '' || val === '.') { sampleData[key] = undefined } else { const items = val.split(',') const itemsLen = items.length const isNum = isNumberType[colIdx] const result: (string | number | undefined)[] = [] for (let k = 0; k < itemsLen; k++) { const ent = items[k] ?? '' result.push(ent === '.' ? undefined : isNum ? +ent : ent) } sampleData[key] = result } colStart = j + 1 colIdx += 1 if (colIdx >= numKeys) { break } } } genotypes[sample] = sampleData } } return genotypes } GENOTYPES() { return parseGenotypesOnly(this.FORMAT ?? '', this.rest, this.sampleNames) } processGenotypes(callback: GenotypeCallback) { processGenotypes( this.FORMAT ?? '', this.rest, this.sampleNames.length, callback, ) } toJSON() { return { CHROM: this.CHROM, POS: this.POS, ID: this.ID, REF: this.REF, ALT: this.ALT, QUAL: this.QUAL, FILTER: this.FILTER, INFO: this.INFO, FORMAT: this.FORMAT, } } }