///|
/// A deterministic comparison between two parsed records.
pub struct RecordDiff {
accession_changed : Bool
definition_changed : Bool
sequence_changed : Bool
length_delta : Int
feature_count_left : Int
feature_count_right : Int
added_features : Int
removed_features : Int
changed_features : Int
qualifier_changes : Int
identical : Bool
} derive(Debug, Eq)
///|
/// Compare records using normalized sequence and feature signatures.
pub fn compare_records(left : Record, right : Record) -> RecordDiff {
let left_signatures = feature_signatures(left)
let right_signatures = feature_signatures(right)
let added = count_not_in(right_signatures, left_signatures)
let removed = count_not_in(left_signatures, right_signatures)
let common = Int::min(left_signatures.length(), right_signatures.length()) -
added
let changed = Int::max(
0,
common - (left_signatures.length() - removed - added),
)
let qualifier_changes = qualifier_difference(left, right)
let sequence_changed = normalize_sequence(left.origin) !=
normalize_sequence(right.origin)
let accession_changed = left.accession != right.accession
let definition_changed = left.definition != right.definition
let identical = !accession_changed &&
!definition_changed &&
!sequence_changed &&
added == 0 &&
removed == 0 &&
qualifier_changes == 0
{
accession_changed,
definition_changed,
sequence_changed,
length_delta: right.origin.length() - left.origin.length(),
feature_count_left: left.features.length(),
feature_count_right: right.features.length(),
added_features: added,
removed_features: removed,
changed_features: Int::max(0, changed),
qualifier_changes,
identical,
}
}
///|
/// Compare a record against itself after canonical sequence normalization.
pub fn records_equivalent(left : Record, right : Record) -> Bool {
compare_records(left, right).identical
}
///|
/// Build a signature that includes key, location, and all qualifier values.
pub fn feature_signatures(record : Record) -> Array[String] {
record.features.map(fn(feature) {
let qualifiers = feature.qualifiers.map(fn(item) {
let value = match item.value {
Some(text) => text
None => ""
}
item.name + "=" + value
})
feature.key + "|" + feature.location.raw + "|" + qualifiers.join(";")
})
}
///|
/// Count signatures that occur in the left array but not the right array.
pub fn count_not_in(left : Array[String], right : Array[String]) -> Int {
left.fold(init=0, fn(total, item) {
total + (if right.contains(item) { 0 } else { 1 })
})
}
///|
/// Count qualifier values that changed for features occupying the same slot.
pub fn qualifier_difference(left : Record, right : Record) -> Int {
let limit = Int::min(left.features.length(), right.features.length())
let mut changes = 0
for index in 0.. String {
feature.qualifiers
.map(fn(item) {
let value = match item.value {
Some(value) => value
None => ""
}
item.name + ":" + value
})
.join("|")
}
///|
/// Produce an actionable human-readable comparison.
pub fn RecordDiff::to_text(self : RecordDiff) -> String {
if self.identical {
return "records are equivalent"
}
let out = StringBuilder()
if self.accession_changed {
out.write_string("accession changed\n")
}
if self.definition_changed {
out.write_string("definition changed\n")
}
if self.sequence_changed {
out.write_string("sequence changed\n")
}
if self.length_delta != 0 {
out.write_string("length delta: " + self.length_delta.to_string() + "\n")
}
if self.added_features != 0 {
out.write_string(
"added features: " + self.added_features.to_string() + "\n",
)
}
if self.removed_features != 0 {
out.write_string(
"removed features: " + self.removed_features.to_string() + "\n",
)
}
if self.changed_features != 0 {
out.write_string(
"changed feature signatures: " + self.changed_features.to_string() + "\n",
)
}
if self.qualifier_changes != 0 {
out.write_string(
"changed qualifier sets: " + self.qualifier_changes.to_string() + "\n",
)
}
out.to_string()
}
///|
pub fn RecordDiff::to_json_string(self : RecordDiff) -> String {
"{\"accession_changed\":" +
self.accession_changed.to_string() +
",\"definition_changed\":" +
self.definition_changed.to_string() +
",\"sequence_changed\":" +
self.sequence_changed.to_string() +
",\"length_delta\":" +
self.length_delta.to_string() +
",\"feature_count_left\":" +
self.feature_count_left.to_string() +
",\"feature_count_right\":" +
self.feature_count_right.to_string() +
",\"added_features\":" +
self.added_features.to_string() +
",\"removed_features\":" +
self.removed_features.to_string() +
",\"changed_features\":" +
self.changed_features.to_string() +
",\"qualifier_changes\":" +
self.qualifier_changes.to_string() +
",\"identical\":" +
self.identical.to_string() +
"}"
}