///|
/// Parse a GenBank location expression into ordered, strand-aware segments.
pub fn parse_location(text : String) -> Location raise GenBankError {
let raw = text.trim().to_owned()
if raw == "" {
raise GenBankError::InvalidLocation(raw)
}
let segments = parse_location_expr(raw)
if segments.is_empty() {
raise GenBankError::InvalidLocation(raw)
}
let ranges = segments.map(fn(segment) { segment.range })
{
raw,
ranges,
complement: raw.has_prefix("complement(") && raw.has_suffix(")"),
joined: raw.has_prefix("join(") && raw.has_suffix(")"),
ordered: raw.has_prefix("order(") && raw.has_suffix(")"),
segments,
}
}
///|
fn parse_location_expr(
text : String,
) -> Array[LocationSegment] raise GenBankError {
let s = text.trim().to_owned()
if has_wrapper(s, "complement(") {
let inner = unwrap_wrapper(s, "complement(")
let original = parse_location_expr(inner)
let segments : Array[LocationSegment] = []
for i = original.length() - 1; i >= 0; i = i - 1 {
let segment = original[i]
segments.push({
range: segment.range,
reverse: !segment.reverse,
remote: segment.remote,
})
}
return segments
}
if has_wrapper(s, "join(") || has_wrapper(s, "order(") {
let prefix = if s.has_prefix("join(") { "join(" } else { "order(" }
let inner = unwrap_wrapper(s, prefix)
let segments : Array[LocationSegment] = []
for part in split_top_level(inner, 44) {
if part.trim().to_owned() == "" {
raise GenBankError::InvalidLocation(s)
}
for segment in parse_location_expr(part) {
segments.push(segment)
}
}
return segments
}
if s.has_prefix("(") || s.has_suffix(")") {
raise GenBankError::InvalidLocation(s)
}
let remote_and_body = split_remote_accession(s)
let range = parse_range(remote_and_body.1)
[{ range, reverse: false, remote: remote_and_body.0 }]
}
///|
fn has_wrapper(text : String, prefix : String) -> Bool {
text.has_prefix(prefix) &&
text.has_suffix(")") &&
matching_close(text, prefix.length() - 1)
}
///|
fn unwrap_wrapper(text : String, prefix : String) -> String {
text.unsafe_substring(start=prefix.length(), end=text.length() - 1)
}
///|
fn matching_close(text : String, open_index : Int) -> Bool {
let mut depth = 0
for i = open_index; i < text.length(); i = i + 1 {
match text[i] {
'(' => depth = depth + 1
')' => {
depth = depth - 1
if depth == 0 && i != text.length() - 1 {
return false
}
}
_ => ()
}
}
depth == 0
}
///|
fn split_top_level(text : String, separator : Int) -> Array[String] {
let parts : Array[String] = []
let mut depth = 0
let mut start = 0
for i = 0; i < text.length(); i = i + 1 {
match text[i] {
'(' => depth = depth + 1
')' => depth = depth - 1
ch if ch.to_int() == separator && depth == 0 => {
parts.push(text.unsafe_substring(start~, end=i))
start = i + 1
}
_ => ()
}
}
parts.push(text.unsafe_substring(start~, end=text.length()))
parts
}
///|
fn split_remote_accession(text : String) -> (String?, String) {
let parts = split_top_level(text, 58)
if parts.length() == 2 && !parts[0].contains("(") {
(Some(parts[0].trim().to_owned()), parts[1].trim().to_owned())
} else {
(None, text.trim().to_owned())
}
}
///|
fn parse_range(text : String) -> Range raise GenBankError {
let s = text.trim().to_owned()
let has_partial_start = s.has_prefix("<") || s.has_prefix(">")
let has_partial_end = s.has_suffix(">") || s.has_suffix("<")
let is_between = s.contains("^")
match s.split_once("..") {
Some((left, right)) => {
let start = parse_endpoint(left.to_owned())
let end = parse_endpoint(right.to_owned())
make_range(
start,
end,
has_partial_start || left.contains("<"),
has_partial_end || right.contains(">"),
false,
s,
)
}
None =>
match s.split_once("^") {
Some((left, right)) => {
let start = parse_endpoint(left.to_owned())
let end = parse_endpoint(right.to_owned())
make_range(
start,
end,
has_partial_start || left.contains("<"),
has_partial_end || right.contains(">"),
true,
s,
)
}
None => {
let point = parse_endpoint(s)
make_range(
point, point, has_partial_start, has_partial_end, is_between, text,
)
}
}
}
}
///|
fn make_range(
start : Int,
end : Int,
partial_start : Bool,
partial_end : Bool,
between : Bool,
original : String,
) -> Range raise GenBankError {
if start < 1 || end < 1 || start > end {
raise GenBankError::InvalidLocation(original)
}
{ start, end, partial_start, partial_end, between }
}
///|
fn parse_endpoint(text : String) -> Int raise GenBankError {
let s = text.trim().to_owned()
let without_marker = if s.has_prefix("<") || s.has_prefix(">") {
s.unsafe_substring(start=1, end=s.length())
} else {
s
}
if without_marker.has_prefix("one-of(") && without_marker.has_suffix(")") {
let inner = unwrap_wrapper(without_marker, "one-of(")
match split_top_level(inner, 44).get(0) {
Some(first) => parse_endpoint(first)
None => raise GenBankError::InvalidLocation(text)
}
} else {
parse_int(without_marker)
}
}