///|
pub fn parse_header(text : String) -> VcfHeader raise ParseError {
let meta : Array[MetaLine] = []
let mut fileformat = ""
let mut columns : Array[String] = []
let mut samples : Array[String] = []
let mut saw_columns = false
for index, line_view in text.split("\n") {
let line = line_view.to_owned()
if line == "" {
()
} else if line.has_prefix("##") {
let item = parse_meta_line(line, line_number=index + 1)
if item is MetaLine::FileFormat(value) {
fileformat = value
}
meta.push(item)
} else if line.has_prefix("#CHROM") {
let fields = @text.split_tab(line)
guard fields.length() >= 8 else {
raise ParseError::InvalidHeader(
line=index + 1,
column=1,
message="column header must contain at least 8 fields",
)
}
columns = fields
samples = if fields.length() > 9 { fields[9:].to_owned() } else { [] }
saw_columns = true
} else {
raise ParseError::InvalidHeader(
line=index + 1,
column=1,
message="header contains a record before #CHROM",
)
}
}
guard saw_columns else {
raise ParseError::InvalidHeader(
line=0,
column=1,
message="missing #CHROM column header",
)
}
{ fileformat, meta, columns, samples }
}
///|
pub fn parse_meta_line(
line : String,
line_number~ : Int,
) -> MetaLine raise ParseError {
guard line.has_prefix("##") else {
raise ParseError::InvalidHeader(
line=line_number,
column=1,
message="metadata line must start with ##",
)
}
let body = line.unsafe_substring(start=2, end=line.length())
guard @text.split_key_value(body, '=') is Some((key, value)) else {
raise ParseError::InvalidHeader(
line=line_number,
column=3,
message="metadata line must contain key=value",
)
}
match key {
"fileformat" => MetaLine::FileFormat(value)
"INFO" => MetaLine::Info(parse_info_def(value, line_number~))
"FORMAT" => MetaLine::Format(parse_format_def(value, line_number~))
"FILTER" => MetaLine::Filter(parse_filter_def(value, line_number~))
"contig" => MetaLine::Contig(parse_contig_def(value, line_number~))
_ => MetaLine::Generic(key~, value~)
}
}
///|
fn parse_info_def(
value : String,
line_number~ : Int,
) -> InfoDef raise ParseError {
let attrs = parse_angle_attributes(value, line_number~)
{
id: require_attr(attrs, "ID", line_number~),
number: parse_cardinality(require_attr(attrs, "Number", line_number~)),
value_type: parse_value_type(require_attr(attrs, "Type", line_number~)),
description: optional_attr(attrs, "Description").unwrap_or(""),
attributes: attrs,
}
}
///|
fn parse_format_def(
value : String,
line_number~ : Int,
) -> FormatDef raise ParseError {
let attrs = parse_angle_attributes(value, line_number~)
{
id: require_attr(attrs, "ID", line_number~),
number: parse_cardinality(require_attr(attrs, "Number", line_number~)),
value_type: parse_value_type(require_attr(attrs, "Type", line_number~)),
description: optional_attr(attrs, "Description").unwrap_or(""),
attributes: attrs,
}
}
///|
fn parse_filter_def(
value : String,
line_number~ : Int,
) -> FilterDef raise ParseError {
let attrs = parse_angle_attributes(value, line_number~)
{
id: require_attr(attrs, "ID", line_number~),
description: optional_attr(attrs, "Description").unwrap_or(""),
attributes: attrs,
}
}
///|
fn parse_contig_def(
value : String,
line_number~ : Int,
) -> ContigDef raise ParseError {
let attrs = parse_angle_attributes(value, line_number~)
{
id: require_attr(attrs, "ID", line_number~),
length: match optional_attr(attrs, "length") {
Some(raw_length) => parse_non_negative_int(raw_length)
None => None
},
attributes: attrs,
}
}
///|
fn parse_angle_attributes(
value : String,
line_number~ : Int,
) -> Array[(String, String)] raise ParseError {
guard @text.strip_angle_brackets(value) is Some(inner) else {
raise ParseError::InvalidHeader(
line=line_number,
column=1,
message="structured metadata must be wrapped in angle brackets",
)
}
let attrs : Array[(String, String)] = []
for part in @text.split_char(inner, ',') {
guard @text.split_key_value(part, '=') is Some((key, raw_value)) else {
raise ParseError::InvalidHeader(
line=line_number,
column=1,
message="structured metadata attribute must be key=value",
)
}
attrs.push((key, unquote(raw_value)))
}
attrs
}
///|
fn require_attr(
attrs : Array[(String, String)],
key : String,
line_number~ : Int,
) -> String raise ParseError {
match optional_attr(attrs, key) {
Some(value) => value
None =>
raise ParseError::InvalidHeader(
line=line_number,
column=1,
message="missing required metadata attribute \{key}",
)
}
}
///|
fn optional_attr(attrs : Array[(String, String)], key : String) -> String? {
for pair in attrs {
let (attr_key, attr_value) = pair
if attr_key == key {
return Some(attr_value)
}
}
None
}
///|
fn parse_cardinality(value : String) -> Cardinality {
match value {
"A" => Cardinality::PerAlt
"R" => Cardinality::PerAllele
"G" => Cardinality::PerGenotype
"." => Cardinality::Variable
_ =>
match parse_non_negative_int(value) {
Some(n) => Cardinality::Fixed(n)
None => Cardinality::Unknown(value)
}
}
}
///|
fn parse_value_type(value : String) -> ValueType {
match value {
"Integer" => ValueType::Integer
"Float" => ValueType::Float
"Flag" => ValueType::Flag
"Character" => ValueType::Character
"String" => ValueType::String
_ => ValueType::Unknown(value)
}
}
///|
fn parse_non_negative_int(value : String) -> Int? {
if value == "" {
None
} else {
let mut total = 0
for ch in value {
if ch >= '0' && ch <= '9' {
total = total * 10 + ch.to_int() - '0'.to_int()
} else {
return None
}
}
Some(total)
}
}
///|
fn unquote(value : String) -> String {
if value.length() >= 2 && value.has_prefix("\"") && value.has_suffix("\"") {
value.unsafe_substring(start=1, end=value.length() - 1)
} else {
value
}
}