///|
/// A flat row suitable for TSV export, spreadsheets, and lightweight UIs.
pub struct FeatureTableRow {
index : Int
key : String
start : Int
end : Int
length : Int
strand : String
remote : Bool
fuzzy : Bool
qualifier_count : Int
label : String
} derive(Debug, Eq)
///|
/// A key-level annotation summary used by dashboards and batch reports.
pub struct FeatureKeySummary {
key : String
count : Int
total_bases : Int
remote_count : Int
fuzzy_count : Int
qualifier_count : Int
} derive(Debug, Eq)
///|
/// Convert a parsed feature to a stable tabular record.
pub fn feature_table_row(index : Int, feature : Feature) -> FeatureTableRow {
let location = summarize_location(feature.location)
let strand = if feature.location.complement { "reverse" } else { "forward" }
let label = match feature.qualifier("label") {
Some(value) => value
None =>
match feature.qualifier("gene") {
Some(value) => value
None =>
match feature.qualifier("locus_tag") {
Some(value) => value
None => feature.key
}
}
}
{
index,
key: feature.key,
start: location.minimum,
end: location.maximum,
length: location.length,
strand,
remote: location.remote_segments > 0,
fuzzy: location.fuzzy,
qualifier_count: feature.qualifiers.length(),
label,
}
}
///|
/// Build rows in source order; source order is preserved for reproducible CSV.
pub fn feature_table(record : Record) -> Array[FeatureTableRow] {
let rows : Array[FeatureTableRow] = []
for index, feature in record.features {
rows.push(feature_table_row(index, feature))
}
rows
}
///|
/// Select rows by key without rebuilding the complete feature index.
pub fn feature_table_by_key(
record : Record,
key : String,
) -> Array[FeatureTableRow] {
feature_table(record).filter(fn(row) { row.key == key })
}
///|
/// Summarize all features by key in first-seen order.
pub fn feature_key_summary(record : Record) -> Array[FeatureKeySummary] {
let result : Array[FeatureKeySummary] = []
for feature in record.features {
let location = summarize_location(feature.location)
let qualifier_count = feature.qualifiers.length()
match result.search_by(fn(item) { item.key == feature.key }) {
Some(index) => {
let item = result[index]
result[index] = {
key: item.key,
count: item.count + 1,
total_bases: item.total_bases + location.length,
remote_count: item.remote_count +
(if location.remote_segments > 0 { 1 } else { 0 }),
fuzzy_count: item.fuzzy_count + (if location.fuzzy { 1 } else { 0 }),
qualifier_count: item.qualifier_count + qualifier_count,
}
}
None =>
result.push({
key: feature.key,
count: 1,
total_bases: location.length,
remote_count: if location.remote_segments > 0 {
1
} else {
0
},
fuzzy_count: if location.fuzzy {
1
} else {
0
},
qualifier_count,
})
}
}
result
}
///|
/// Return the percentage of a record covered by features of one key.
pub fn feature_key_coverage(record : Record, key : String) -> Double {
let coordinates : Array[Int] = []
for feature in record.features {
if feature.key == key {
for coordinate in location_coordinates(feature.location) {
if !coordinates.contains(coordinate) {
coordinates.push(coordinate)
}
}
}
}
if record.origin.length() == 0 {
0.0
} else {
coordinates.length().to_double() *
100.0 /
record.origin.length().to_double()
}
}
///|
/// Locate the first feature covering a one-based sequence coordinate.
pub fn feature_at_coordinate(
record : Record,
coordinate : Int,
) -> Array[FeatureTableRow] {
if coordinate < 1 {
return []
}
feature_table(record).filter(fn(row) {
row.start <= coordinate && coordinate <= row.end
})
}
///|
/// Export feature rows as RFC-style tab-separated text with a header.
pub fn feature_table_tsv(record : Record) -> String {
let out = StringBuilder()
out.write_string(
"index\tkey\tstart\tend\tlength\tstrand\tremote\tfuzzy\tqualifiers\tlabel\n",
)
for row in feature_table(record) {
out.write_string(row.index.to_string())
out.write_char('\t')
out.write_string(row.key)
out.write_char('\t')
out.write_string(row.start.to_string())
out.write_char('\t')
out.write_string(row.end.to_string())
out.write_char('\t')
out.write_string(row.length.to_string())
out.write_char('\t')
out.write_string(row.strand)
out.write_char('\t')
out.write_string(row.remote.to_string())
out.write_char('\t')
out.write_string(row.fuzzy.to_string())
out.write_char('\t')
out.write_string(row.qualifier_count.to_string())
out.write_char('\t')
out.write_string(
row.label.replace_all(old="\t", new=" ").replace_all(old="\n", new=" "),
)
out.write_char('\n')
}
out.to_string()
}
///|
/// Export key summaries as stable TSV for command-line data processing.
pub fn feature_key_summary_tsv(record : Record) -> String {
let out = StringBuilder()
out.write_string("key\tcount\ttotal_bases\tremote\tfuzzy\tqualifiers\n")
for item in feature_key_summary(record) {
out.write_string(item.key)
out.write_char('\t')
out.write_string(item.count.to_string())
out.write_char('\t')
out.write_string(item.total_bases.to_string())
out.write_char('\t')
out.write_string(item.remote_count.to_string())
out.write_char('\t')
out.write_string(item.fuzzy_count.to_string())
out.write_char('\t')
out.write_string(item.qualifier_count.to_string())
out.write_char('\n')
}
out.to_string()
}
///|
pub fn FeatureTableRow::to_json_string(self : FeatureTableRow) -> String {
"{\"index\":" +
self.index.to_string() +
",\"key\":" +
json_string(self.key) +
",\"start\":" +
self.start.to_string() +
",\"end\":" +
self.end.to_string() +
",\"length\":" +
self.length.to_string() +
",\"strand\":" +
json_string(self.strand) +
",\"remote\":" +
self.remote.to_string() +
",\"fuzzy\":" +
self.fuzzy.to_string() +
",\"qualifier_count\":" +
self.qualifier_count.to_string() +
",\"label\":" +
json_string(self.label) +
"}"
}
///|
pub fn FeatureKeySummary::to_json_string(self : FeatureKeySummary) -> String {
"{\"key\":" +
json_string(self.key) +
",\"count\":" +
self.count.to_string() +
",\"total_bases\":" +
self.total_bases.to_string() +
",\"remote_count\":" +
self.remote_count.to_string() +
",\"fuzzy_count\":" +
self.fuzzy_count.to_string() +
",\"qualifier_count\":" +
self.qualifier_count.to_string() +
"}"
}