///|
pub fn parse_record(
line : String,
header~ : VcfHeader,
) -> VcfRecord raise ParseError {
parse_record_at(line, header~, line_number=1)
}
///|
pub fn parse_document(text : String) -> VcfDocument raise ParseError {
let header_lines : Array[String] = []
let record_lines : Array[(Int, String)] = []
let mut saw_columns = false
for index, line_view in text.split("\n") {
let line = line_view.to_owned()
if line == "" {
()
} else if !saw_columns {
header_lines.push(line)
if line.has_prefix("#CHROM") {
saw_columns = true
}
} else {
record_lines.push((index + 1, line))
}
}
let header = parse_header(header_lines.join("\n") + "\n")
let records : Array[VcfRecord] = []
for pair in record_lines {
let (line_number, record_line) = pair
records.push(parse_record_at(record_line, header~, line_number~))
}
{ header, records }
}
///|
fn parse_record_at(
line : String,
header~ : VcfHeader,
line_number~ : Int,
) -> VcfRecord raise ParseError {
let fields = @text.split_tab(line)
guard fields.length() >= 8 else {
raise ParseError::InvalidRecord(
line=line_number,
column=1,
field="record",
message="record must contain at least 8 fields",
)
}
let chrom = fields[0]
let pos = parse_required_int(fields[1], line_number~, column=2, field="POS")
let ids = parse_missing_list(fields[2], ';')
let ref_allele = fields[3]
let alt_alleles = parse_missing_list(fields[4], ',')
let qual = parse_optional_double(
fields[5],
line_number~,
column=6,
field="QUAL",
)
let filters = parse_missing_list(fields[6], ';')
let info = parse_info(fields[7], header~, line_number~)
let format_keys = if fields.length() > 8 && fields[8] != "." {
@text.split_char(fields[8], ':')
} else {
[]
}
let samples = parse_samples(fields, format_keys, header~, line_number~)
{
chrom,
pos,
ids,
ref_allele,
alt_alleles,
qual,
filters,
info,
format_keys,
samples,
}
}
///|
fn parse_missing_list(value : String, sep : Char) -> Array[String] {
if value == "." || value == "" {
[]
} else {
@text.split_char(value, sep)
}
}
///|
fn parse_info(
value : String,
header~ : VcfHeader,
line_number~ : Int,
) -> Array[(String, VcfValue)] raise ParseError {
if value == "." || value == "" {
[]
} else {
let entries : Array[(String, VcfValue)] = []
for part in @text.split_char(value, ';') {
if part != "" {
match @text.split_key_value(part, '=') {
Some((key, raw_value)) =>
entries.push(
(key, coerce_info_value(key, raw_value, header~, line_number~)),
)
None => entries.push((part, VcfValue::Flag))
}
}
}
entries
}
}
///|
fn parse_samples(
fields : Array[String],
format_keys : Array[String],
header~ : VcfHeader,
line_number~ : Int,
) -> Array[SampleCall] raise ParseError {
let calls : Array[SampleCall] = []
if fields.length() <= 9 || format_keys.length() == 0 {
calls
} else {
for sample_index in 0..<(fields.length() - 9) {
let raw_values = @text.split_char(fields[9 + sample_index], ':')
let values : Array[(String, VcfValue)] = []
for key_index, key in format_keys {
let raw_value = if key_index < raw_values.length() {
raw_values[key_index]
} else {
"."
}
values.push(
(key, coerce_format_value(key, raw_value, header~, line_number~)),
)
}
let name = if sample_index < header.samples.length() {
header.samples[sample_index]
} else {
"SAMPLE\{sample_index + 1}"
}
calls.push({ name, values })
}
calls
}
}
///|
fn coerce_info_value(
key : String,
raw_value : String,
header~ : VcfHeader,
line_number~ : Int,
) -> VcfValue raise ParseError {
match find_info_def(header, key) {
Some(def) =>
coerce_typed_value(
raw_value,
def.value_type,
def.number,
line_number~,
field=key,
)
None =>
coerce_typed_value(
raw_value,
ValueType::String,
Cardinality::Variable,
line_number~,
field=key,
)
}
}
///|
fn coerce_format_value(
key : String,
raw_value : String,
header~ : VcfHeader,
line_number~ : Int,
) -> VcfValue raise ParseError {
match find_format_def(header, key) {
Some(def) =>
coerce_typed_value(
raw_value,
def.value_type,
def.number,
line_number~,
field=key,
)
None =>
coerce_typed_value(
raw_value,
ValueType::String,
Cardinality::Fixed(1),
line_number~,
field=key,
)
}
}
///|
fn coerce_typed_value(
raw_value : String,
value_type : ValueType,
number : Cardinality,
line_number~ : Int,
field~ : String,
) -> VcfValue raise ParseError {
if raw_value == "." {
VcfValue::Missing
} else if should_parse_list(number, raw_value) {
let values : Array[VcfValue] = []
for item in @text.split_char(raw_value, ',') {
values.push(coerce_scalar_value(item, value_type, line_number~, field~))
}
VcfValue::List(values)
} else {
coerce_scalar_value(raw_value, value_type, line_number~, field~)
}
}
///|
fn should_parse_list(number : Cardinality, raw_value : String) -> Bool {
if raw_value.contains(",") {
true
} else {
match number {
Cardinality::PerAlt | PerAllele | PerGenotype | Variable => true
_ => false
}
}
}
///|
fn coerce_scalar_value(
raw_value : String,
value_type : ValueType,
line_number~ : Int,
field~ : String,
) -> VcfValue raise ParseError {
match value_type {
ValueType::Integer =>
VcfValue::Integer(
parse_required_int(raw_value, line_number~, column=1, field~),
)
Float =>
VcfValue::Float(
parse_required_double(raw_value, line_number~, column=1, field~),
)
Flag =>
if raw_value == "" {
VcfValue::Flag
} else {
VcfValue::String(raw_value)
}
Character | String | Unknown(_) => VcfValue::String(raw_value)
}
}
///|
fn find_info_def(header : VcfHeader, key : String) -> InfoDef? {
for item in header.meta {
match item {
MetaLine::Info(def) => if def.id == key { return Some(def) }
_ => ()
}
}
None
}
///|
fn find_format_def(header : VcfHeader, key : String) -> FormatDef? {
for item in header.meta {
match item {
MetaLine::Format(def) => if def.id == key { return Some(def) }
_ => ()
}
}
None
}
///|
fn parse_required_int(
value : String,
line_number~ : Int,
column~ : Int,
field~ : String,
) -> Int raise ParseError {
match parse_non_negative_int(value) {
Some(n) => n
None =>
raise ParseError::InvalidValue(
line=line_number,
column~,
field~,
value~,
message="expected non-negative integer",
)
}
}
///|
fn parse_optional_double(
value : String,
line_number~ : Int,
column~ : Int,
field~ : String,
) -> Double? raise ParseError {
if value == "." {
None
} else {
Some(parse_required_double(value, line_number~, column~, field~))
}
}
///|
fn parse_required_double(
value : String,
line_number~ : Int,
column~ : Int,
field~ : String,
) -> Double raise ParseError {
@string.parse_double(value[:]) catch {
_ =>
raise ParseError::InvalidValue(
line=line_number,
column~,
field~,
value~,
message="expected floating point number",
)
}
}