///|
/// A compact result for a feature extraction workflow.
pub struct FeatureExtraction {
index : Int
key : String
location : String
sequence : String
sequence_length : Int
translation : String
} derive(Debug, Eq)
///|
/// Extract feature sequences while retaining enough context for audit logs.
pub fn extract_features(
record : Record,
key : String,
) -> Array[FeatureExtraction] {
let result : Array[FeatureExtraction] = []
for index, feature in record.features {
if feature.key == key {
let sequence = record.sequence_for(feature)
result.push({
index,
key: feature.key,
location: feature.location.raw,
sequence,
sequence_length: sequence.length(),
translation: match feature.qualifier("translation") {
Some(value) => value
None => ""
},
})
}
}
result
}
///|
/// Extract all CDS sequences and orient joined/complement locations correctly.
pub fn extract_cds(record : Record) -> Array[FeatureExtraction] {
extract_features(record, "CDS")
}
///|
/// Find features whose qualifier contains a case-insensitive search term.
pub fn features_with_qualifier_text(
record : Record,
name : String,
needle : String,
) -> Array[Feature] {
let wanted = needle.to_lower()
record.features.filter(fn(feature) {
match feature.qualifier(name) {
Some(value) => value.to_lower().contains(wanted)
None => false
}
})
}
///|
/// Return feature rows with a location length in the inclusive range.
pub fn features_in_length_range(
record : Record,
minimum : Int,
maximum : Int,
) -> Array[FeatureTableRow] {
if minimum < 0 || maximum < minimum {
return []
}
feature_table(record).filter(fn(row) {
minimum <= row.length && row.length <= maximum
})
}
///|
/// Return the normalized ORIGIN split into fixed-size chunks for streaming UIs.
pub fn record_origin_chunks(record : Record, chunk_size : Int) -> Array[String] {
sequence_chunks(normalize_sequence(record.origin), chunk_size)
}
///|
/// Count features that cannot be materialized from the local ORIGIN.
pub fn unresolved_feature_count(record : Record) -> Int {
record.features.fold(init=0, fn(total, feature) {
total + (if location_is_remote(feature.location) { 1 } else { 0 })
})
}
///|
/// Produce stable feature labels in source order for list and search widgets.
pub fn record_feature_labels(record : Record) -> Array[String] {
feature_table(record).map(fn(row) {
row.key +
":" +
row.label +
" [" +
row.start.to_string() +
".." +
row.end.to_string() +
"]"
})
}
///|
/// Return an extraction as JSON without requiring a JSON dependency.
pub fn FeatureExtraction::to_json_string(self : FeatureExtraction) -> String {
"{\"index\":" +
self.index.to_string() +
",\"key\":" +
json_string(self.key) +
",\"location\":" +
json_string(self.location) +
",\"sequence\":" +
json_string(self.sequence) +
",\"sequence_length\":" +
self.sequence_length.to_string() +
",\"translation\":" +
json_string(self.translation) +
"}"
}
///|
/// Export extracted features as a line-oriented JSON document.
pub fn feature_extractions_json(record : Record, key : String) -> String {
"[" +
extract_features(record, key)
.map(fn(item) { item.to_json_string() })
.join(",") +
"]"
}
///|
/// Return all features with their local sequence length, preserving order.
pub fn feature_sequence_lengths(record : Record) -> Array[(String, Int)] {
record.features.map(fn(feature) {
(feature.key, record.sequence_for(feature).length())
})
}
///|
/// Check whether every local feature lies inside the declared ORIGIN length.
pub fn local_features_fit_origin(record : Record) -> Bool {
record.features.all(fn(feature) {
if location_is_remote(feature.location) {
true
} else {
location_maximum(feature.location) <= record.origin.length()
}
})
}
///|
fn location_maximum(location : Location) -> Int {
location.ranges.fold(init=0, fn(current, range) {
Int::max(current, range.end)
})
}