///|
/// Render a record as a canonical GenBank flat-file document.
pub fn Record::to_genbank_string(
self : Record,
line_width? : Int = 80,
) -> String {
let width = if line_width < 20 { 80 } else { line_width }
let out = StringBuilder()
out.write_string(self.locus.to_genbank_line())
out.write_char('\n')
write_wrapped_field(out, "DEFINITION", self.definition, width)
if self.accession != "" {
write_wrapped_field(out, "ACCESSION", self.accession, width)
}
if self.version != "" {
write_wrapped_field(out, "VERSION", self.version, width)
}
if self.keywords != "" {
write_wrapped_field(out, "KEYWORDS", self.keywords, width)
}
if self.source != "" {
write_wrapped_field(out, "SOURCE", self.source, width)
}
out.write_string("FEATURES Location/Qualifiers\n")
for feature in self.features {
out.write_string(feature.to_genbank_head())
out.write_char('\n')
for qualifier in feature.qualifiers {
for line in qualifier.to_genbank_lines(line_width=width) {
out.write_string(line)
out.write_char('\n')
}
}
}
out.write_string("ORIGIN\n")
out.write_string(format_origin(self.origin, width))
out.write_string("//\n")
out.to_string()
}
///|
pub fn Locus::to_genbank_line(self : Locus) -> String {
let fields = [
self.name,
self.length.to_string(),
"bp",
self.molecule_type,
self.topology,
self.division,
self.date,
]
"LOCUS " + fields.filter(fn(item) { item != "" }).join(" ")
}
///|
pub fn Feature::to_genbank_head(self : Feature) -> String {
" " + pad_right(self.key, 16) + self.location.raw
}
///|
pub fn Qualifier::to_genbank_lines(
self : Qualifier,
line_width? : Int = 80,
) -> Array[String] {
let lines : Array[String] = []
let prefix = " /" + self.name
match self.value {
None => lines.push(prefix)
Some(value) => {
let encoded = if value.contains("\"") {
"\"" + value.replace_all(old="\"", new="\\\"") + "\""
} else {
"\"" + value + "\""
}
let first = prefix + "="
let available = Int::max(8, line_width - first.length())
let chunks = wrap_text(encoded, available)
if chunks.length() == 0 {
lines.push(first)
} else {
lines.push(first + chunks[0])
for chunk in chunks.view(start=1) {
lines.push(" " + chunk)
}
}
}
}
lines
}
///|
fn write_wrapped_field(
out : StringBuilder,
name : String,
value : String,
line_width : Int,
) -> Unit {
let prefix = pad_right(name, 12)
let available = Int::max(8, line_width - prefix.length())
let chunks = wrap_text(value, available)
if chunks.length() == 0 {
out.write_string(prefix)
out.write_char('\n')
} else {
out.write_string(prefix + chunks[0])
out.write_char('\n')
for chunk in chunks.view(start=1) {
out.write_string(" " + chunk)
out.write_char('\n')
}
}
}
///|
fn format_origin(sequence : String, line_width : Int) -> String {
let out = StringBuilder()
let normalized = normalize_sequence(sequence)
let bases_per_line = Int::max(10, (line_width - 10) / 11 * 10)
let mut offset = 0
while offset < normalized.length() {
let end = Int::min(offset + bases_per_line, normalized.length())
let piece = normalized.unsafe_substring(start=offset, end~)
out.write_string(pad_left((offset + 1).to_string(), 9))
let mut group_start = 0
while group_start < piece.length() {
let group_end = Int::min(group_start + 10, piece.length())
if group_start > 0 {
out.write_char(' ')
}
out.write_string(piece.unsafe_substring(start=group_start, end=group_end))
group_start = group_end
}
out.write_char('\n')
offset = end
}
out.to_string()
}
///|
fn pad_right(text : String, width : Int) -> String {
let out = StringBuilder()
out.write_string(text)
let mut count = text.length()
while count < width {
out.write_char(' ')
count += 1
}
out.to_string()
}
///|
fn pad_left(text : String, width : Int) -> String {
let out = StringBuilder()
let mut count = text.length()
while count < width {
out.write_char(' ')
count += 1
}
out.write_string(text)
out.to_string()
}
///|
fn wrap_text(text : String, width : Int) -> Array[String] {
let chunks : Array[String] = []
if text == "" {
return chunks
}
let safe_width = Int::max(1, width)
let mut start = 0
while start < text.length() {
let end = Int::min(start + safe_width, text.length())
chunks.push(text.unsafe_substring(start~, end~))
start = end
}
chunks
}
///|
/// Normalize line endings and ensure a final GenBank terminator.
pub fn normalize_genbank_text(text : String) -> String {
let normalized = text.replace_all(old="\r\n", new="\n")
if normalized.trim().to_owned().has_suffix("//") {
if normalized.has_suffix("\n") {
normalized
} else {
normalized + "\n"
}
} else {
normalized + "//\n"
}
}
///|
pub fn genbank_round_trip(text : String) -> String raise GenBankError {
parse_record(normalize_genbank_text(text)).to_genbank_string()
}
///|
pub fn record_text_lines(record : Record) -> Array[String] {
record
.to_genbank_string()
.split("\n")
.map(fn(line) { line.to_owned() })
.to_array()
}
///|
pub fn record_text_length(record : Record) -> Int {
record.to_genbank_string().length()
}
///|
pub fn record_has_terminator(record : Record) -> Bool {
record.to_genbank_string().has_suffix("//\n")
}