///|
/// Parse strict signed decimal offsets without integer overflow or coercion.
fn read_offset(text : String) -> Int raise RestrictError {
if text.is_empty() || text.length() > 4 {
raise InvalidInput("OFFSET_DECIMAL")
}
let negative = text.has_prefix("-")
let begin = if negative { 1 } else { 0 }
if begin == text.length() {
raise InvalidInput("OFFSET_DECIMAL")
}
let mut value = 0
for i in begin.. 57 {
raise InvalidInput("OFFSET_DECIMAL")
}
value = value * 10 + c - 48
}
if negative {
-value
} else {
value
}
}
///|
/// Tab-separated header name/motif/top/bottom; blank lines and # comments allowed.
pub fn read_enzymes(text : String) -> Array[Enzyme] raise RestrictError {
if text.length() > 16384 {
raise LimitExceeded("ENZYME_TEXT")
}
let result : Array[Enzyme] = []
let names : Map[String, Bool] = Map([])
let mut header = false
for raw in text.split("\n") {
let line = raw.trim().to_owned()
if line.is_empty() || line.has_prefix("#") {
continue
}
if !header {
if line != "name\tmotif\ttop\tbottom" {
raise InvalidInput("ENZYME_HEADER")
}
header = true
continue
}
let fields = line.split("\t").map(v => v.to_owned()).to_array()
if fields.length() != 4 {
raise InvalidInput("ENZYME_COLUMNS")
}
if result.length() >= 32 {
raise LimitExceeded("ENZYME_MAX_32")
}
let enzyme = Enzyme::new(
fields[0],
fields[1],
read_offset(fields[2]),
read_offset(fields[3]),
)
if names.contains(enzyme.name) {
raise InvalidInput("DUPLICATE_ENZYME_NAME")
}
names[enzyme.name] = true
result.push(enzyme)
}
if !header || result.is_empty() {
raise InvalidInput("ENZYME_EMPTY")
}
result
}
///|
/// Canonical synthetic/user-defined model interchange. No catalogue is bundled.
pub fn write_enzymes(enzymes : Array[Enzyme]) -> String raise RestrictError {
let out = StringBuilder::new()
out.write_string("name\tmotif\ttop\tbottom\n")
for e in enzymes {
out.write_string(
e.name +
"\t" +
e.motif +
"\t" +
e.top_offset.to_string() +
"\t" +
e.bottom_offset.to_string() +
"\n",
)
}
let text = out.to_string()
let _ = read_enzymes(text)
text
}