///|
/// Parse one GenBank/INSDC flat-file record.
pub fn parse_record(text : String) -> Record raise GenBankError {
let lines = text.split("\n").map(fn(line) { line.to_owned() }).to_array()
let locus = parse_locus(require_prefixed(lines, "LOCUS"))
let accession = parse_accession(lines)
let version = collect_field(lines, "VERSION")
let definition = collect_field(lines, "DEFINITION")
let keywords = collect_field(lines, "KEYWORDS")
let source = collect_field(lines, "SOURCE")
let features = parse_features(lines)
let origin = parse_origin(lines)
{ accession, version, locus, definition, keywords, source, features, origin }
}
///|
/// Parse all records in a GenBank flat-file stream separated by `//`.
pub fn parse_records(text : String) -> Array[Record] raise GenBankError {
let records : Array[Record] = []
let buffer = StringBuilder()
for line in text.split("\n") {
let owned = line.to_owned()
if owned.trim().to_owned() == "//" {
buffer.write_string("//\n")
let chunk = buffer.to_string()
if chunk.trim().to_owned() != "" {
records.push(parse_record(chunk))
}
buffer.reset()
} else {
buffer.write_string(owned)
buffer.write_char('\n')
}
}
let tail = buffer.to_string()
if tail.trim().to_owned() != "" {
records.push(parse_record(tail))
}
records
}
///|
fn require_prefixed(
lines : Array[String],
prefix : String,
) -> String raise GenBankError {
match lines.search_by(fn(line) { line.has_prefix(prefix) }) {
Some(index) => lines[index]
None => raise GenBankError::MissingSection(prefix)
}
}
///|
fn parse_accession(lines : Array[String]) -> String {
match lines.search_by(fn(line) { line.has_prefix("ACCESSION") }) {
Some(index) =>
match field_value(lines[index], "ACCESSION") {
Some(value) => value
None => ""
}
None => ""
}
}
///|
fn collect_field(lines : Array[String], name : String) -> String {
match lines.search_by(fn(line) { line.has_prefix(name) }) {
Some(index) => {
let first = match field_value(lines[index], name) {
Some(value) => value
None => ""
}
let parts : Array[String] = [first]
for i = index + 1; i < lines.length(); i = i + 1 {
let line = lines[i]
if line.has_prefix(" ") {
parts.push(line.trim().to_owned())
} else {
break
}
}
parts.join(" ")
}
None => ""
}
}
///|
fn parse_origin(lines : Array[String]) -> String raise GenBankError {
match lines.search_by(fn(line) { line.has_prefix("ORIGIN") }) {
Some(index) => {
let pieces : Array[String] = []
for i = index + 1; i < lines.length(); i = i + 1 {
let line = lines[i]
if line.has_prefix("//") {
break
}
if is_origin_sequence_line(line) {
for ch in line {
if is_base(ch) {
pieces.push(ch.to_string())
}
}
}
}
pieces.join("").to_upper()
}
None => raise GenBankError::MissingSection("ORIGIN")
}
}
///|
fn is_base(ch : Char) -> Bool {
ch == 'a' ||
ch == 'c' ||
ch == 'g' ||
ch == 't' ||
ch == 'u' ||
ch == 'n' ||
ch == 'A' ||
ch == 'C' ||
ch == 'G' ||
ch == 'T' ||
ch == 'U' ||
ch == 'N' ||
ch == 'r' ||
ch == 'y' ||
ch == 's' ||
ch == 'w' ||
ch == 'k' ||
ch == 'm' ||
ch == 'b' ||
ch == 'd' ||
ch == 'h' ||
ch == 'v' ||
ch == 'R' ||
ch == 'Y' ||
ch == 'S' ||
ch == 'W' ||
ch == 'K' ||
ch == 'M' ||
ch == 'B' ||
ch == 'D' ||
ch == 'H' ||
ch == 'V'
}
///|
fn is_origin_sequence_line(line : String) -> Bool {
let trimmed = line.trim().to_owned()
for ch in trimmed {
return ch >= '0' && ch <= '9'
}
false
}
///|
fn parse_features(lines : Array[String]) -> Array[Feature] raise GenBankError {
match lines.search_by(fn(line) { line.has_prefix("FEATURES") }) {
Some(start) => {
let features : Array[Feature] = []
let mut current_key = ""
let mut current_location = ""
let mut qualifiers : Array[Qualifier] = []
for i = start + 1; i < lines.length(); i = i + 1 {
let line = lines[i]
if line.has_prefix("ORIGIN") || line.has_prefix("//") {
break
}
if is_feature_start(line) {
if current_key != "" {
features.push(
make_feature(current_key, current_location, qualifiers),
)
}
let parsed = parse_feature_head(line)
current_key = parsed.0
current_location = parsed.1
qualifiers = []
} else if is_qualifier_line(line) {
qualifiers.push(parse_qualifier(line))
} else if current_key != "" {
let extra = line.trim().to_owned()
if extra != "" &&
!extra.has_prefix("/") &&
append_qualifier_continuation(qualifiers, extra) {
()
} else if extra != "" && !extra.has_prefix("/") {
current_location = current_location + extra
}
}
}
if current_key != "" {
features.push(make_feature(current_key, current_location, qualifiers))
}
features
}
None => raise GenBankError::MissingSection("FEATURES")
}
}
///|
fn make_feature(
key : String,
location_text : String,
qualifiers : Array[Qualifier],
) -> Feature raise GenBankError {
let location = parse_location(location_text)
{ key, location, qualifiers }
}
///|
fn is_feature_start(line : String) -> Bool {
line.length() > 21 &&
line.has_prefix(" ") &&
line[5] != ' ' &&
!line.trim().to_owned().has_prefix("/")
}
///|
fn is_qualifier_line(line : String) -> Bool {
line.trim().to_owned().has_prefix("/")
}
///|
fn parse_feature_head(line : String) -> (String, String) {
let rest = line.unsafe_substring(start=5, end=line.length())
let parts = split_words(rest)
if parts.length() == 0 {
("", "")
} else {
let key = parts[0]
let location = if parts.length() == 1 {
""
} else {
parts.view(start=1).join("")
}
(key, location)
}
}
///|
fn parse_qualifier(line : String) -> Qualifier {
let trimmed = line.trim().to_owned()
match trimmed.split_once("=") {
Some((left, right)) => {
let left_text = left.to_owned()
let name = left_text.unsafe_substring(start=1, end=left_text.length())
let value = strip_quotes(right.to_owned())
{ name, value: Some(value) }
}
None => {
let name = trimmed.unsafe_substring(start=1, end=trimmed.length())
{ name, value: None }
}
}
}
///|
fn append_qualifier_continuation(
qualifiers : Array[Qualifier],
extra : String,
) -> Bool {
match qualifiers.last() {
Some(last) =>
match last.value {
Some(value) => {
let index = qualifiers.length() - 1
let joined = value + strip_quotes(extra)
qualifiers[index] = { name: last.name, value: Some(joined) }
true
}
None => false
}
None => false
}
}
///|
fn strip_quotes(text : String) -> String {
let trimmed = text.trim().to_owned()
let without_left = if trimmed.has_prefix("\"") {
trimmed.unsafe_substring(start=1, end=trimmed.length())
} else {
trimmed
}
if without_left.has_suffix("\"") {
without_left.unsafe_substring(start=0, end=without_left.length() - 1)
} else {
without_left
}
}