///|
/// An occurrence of an IUPAC motif in a nucleotide sequence.
pub struct MotifHit {
motif : String
start : Int
end : Int
strand : String
matched : String
} derive(Debug, Eq)
///|
/// Summary of a forward and reverse-complement motif scan.
pub struct MotifScanReport {
motif : String
sequence_length : Int
forward_hits : Int
reverse_hits : Int
hits : Array[MotifHit]
} derive(Debug, Eq)
///|
/// Match one IUPAC motif at a zero-based sequence offset.
pub fn iupac_motif_matches_at(
sequence : String,
motif : String,
offset : Int,
) -> Bool {
let subject = sequence.to_upper()
let pattern = motif.to_upper()
if offset < 0 || pattern == "" || offset + pattern.length() > subject.length() {
return false
}
for index in 0.. Bool {
let wanted = symbol.to_string().to_upper()
let actual = base.to_string().to_upper()
iupac_expansion(symbol).any(fn(item) { item.to_string() == actual }) ||
(wanted == actual && is_iupac_base(base))
}
///|
/// Scan overlapping occurrences on one strand using IUPAC semantics.
pub fn scan_iupac_motif(sequence : String, motif : String) -> Array[MotifHit] {
let hits : Array[MotifHit] = []
let subject = sequence.to_upper()
let pattern = motif.to_upper()
if pattern == "" || pattern.length() > subject.length() {
return hits
}
for offset in 0..<=(subject.length() - pattern.length()) {
if iupac_motif_matches_at(subject, pattern, offset) {
hits.push({
motif: pattern,
start: offset + 1,
end: offset + pattern.length(),
strand: "+",
matched: subject.unsafe_substring(
start=offset,
end=offset + pattern.length(),
),
})
}
}
hits
}
///|
/// Scan both strands and retain orientation in every hit.
pub fn scan_iupac_both_strands(
sequence : String,
motif : String,
) -> MotifScanReport {
let forward = scan_iupac_motif(sequence, motif)
let reverse_motif = reverse_complement(motif)
let reverse_hits = scan_iupac_motif(sequence, reverse_motif)
let oriented_reverse = reverse_hits.map(fn(hit) {
{ motif, start: hit.start, end: hit.end, strand: "-", matched: hit.matched }
})
let all = forward.copy()
for hit in oriented_reverse {
all.push(hit)
}
all.sort_by(fn(left, right) {
if left.start == right.start {
if left.strand == right.strand {
0
} else if left.strand == "+" {
-1
} else {
1
}
} else {
left.start - right.start
}
})
{
motif: motif.to_upper(),
sequence_length: sequence.length(),
forward_hits: forward.length(),
reverse_hits: reverse_hits.length(),
hits: all,
}
}
///|
/// Return only one-based coordinates for a strand-specific scan.
pub fn iupac_motif_positions(sequence : String, motif : String) -> Array[Int] {
scan_iupac_motif(sequence, motif).map(fn(hit) { hit.start })
}
///|
/// Count overlapping IUPAC motif occurrences.
pub fn iupac_motif_count(sequence : String, motif : String) -> Int {
scan_iupac_motif(sequence, motif).length()
}
///|
/// Build a consensus IUPAC symbol for a set of observed canonical bases.
pub fn iupac_consensus(observed : Array[Char]) -> Char {
let bases = observed.map(fn(ch) { ch.to_string().to_upper() })
let has_a = bases.contains("A")
let has_c = bases.contains("C")
let has_g = bases.contains("G")
let has_t = bases.contains("T") || bases.contains("U")
match (has_a, has_c, has_g, has_t) {
(true, false, false, false) => 'A'
(false, true, false, false) => 'C'
(false, false, true, false) => 'G'
(false, false, false, true) => 'T'
(true, false, true, false) => 'R'
(false, true, false, true) => 'Y'
(false, true, true, false) => 'S'
(true, false, false, true) => 'W'
(false, false, true, true) => 'K'
(true, true, false, false) => 'M'
(false, true, true, true) => 'B'
(true, false, true, true) => 'D'
(true, true, false, true) => 'H'
(true, true, true, false) => 'V'
(true, true, true, true) => 'N'
_ => 'N'
}
}
///|
/// Derive a position-wise IUPAC consensus from aligned sequences.
pub fn aligned_iupac_consensus(sequences : Array[String]) -> String {
if sequences.length() == 0 {
return ""
}
let width = sequences
.map(fn(item) { item.length() })
.fold(init=0, fn(a, b) { Int::max(a, b) })
let output = StringBuilder()
for position in 0.. String {
"{\"motif\":" +
json_string(self.motif) +
",\"start\":" +
self.start.to_string() +
",\"end\":" +
self.end.to_string() +
",\"strand\":" +
json_string(self.strand) +
",\"matched\":" +
json_string(self.matched) +
"}"
}
///|
pub fn MotifScanReport::to_json_string(self : MotifScanReport) -> String {
"{\"motif\":" +
json_string(self.motif) +
",\"sequence_length\":" +
self.sequence_length.to_string() +
",\"forward_hits\":" +
self.forward_hits.to_string() +
",\"reverse_hits\":" +
self.reverse_hits.to_string() +
",\"hits\":[" +
self.hits.map(fn(item) { item.to_json_string() }).join(",") +
"]}"
}