use crate::pattern::PatternSet;
use crate::region::Region;
use crate::revcomp::revcomp;
pub struct MatchOpts {
pub by_name: bool,
pub by_seq: bool,
pub only_positive: bool,
pub ignore_case: bool,
pub region: Option<Region>,
}
impl MatchOpts {
#[must_use]
pub fn effective_by_seq(explicit_by_seq: bool, degenerate: bool, region: bool) -> bool {
explicit_by_seq || degenerate || region
}
}
#[must_use]
pub fn is_hit(id: &[u8], name: &[u8], seq: &[u8], opts: &MatchOpts, patterns: &PatternSet) -> bool {
if !opts.by_seq {
let target = if opts.by_name { name } else { id };
return patterns.is_match(target, opts.ignore_case);
}
let strands: &[bool] = if opts.only_positive {
&[false]
} else {
&[false, true]
};
for &minus in strands {
let owned;
let base: &[u8] = if minus {
owned = revcomp(seq);
&owned
} else {
seq
};
let target: &[u8] = match &opts.region {
Some(region) => {
let (s, e) = region.resolve(base.len());
if s == e {
continue;
}
&base[s..e]
}
None => base,
};
if patterns.is_match(target, opts.ignore_case) {
return true;
}
}
false
}
#[must_use]
pub fn split_id(name: &[u8]) -> &[u8] {
match name.iter().position(|&b| b == b' ' || b == b'\t') {
Some(i) => &name[..i],
None => name,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pattern::{self, BuildOpts};
fn exact(pats: &[&str]) -> PatternSet {
let owned: Vec<String> = pats.iter().map(|s| (*s).to_string()).collect();
pattern::build(
&owned,
&BuildOpts {
use_regexp: false,
degenerate: false,
by_seq: false,
ignore_case: false,
},
)
.unwrap()
}
fn substr(pats: &[&str]) -> PatternSet {
let owned: Vec<String> = pats.iter().map(|s| (*s).to_string()).collect();
pattern::build(
&owned,
&BuildOpts {
use_regexp: false,
degenerate: false,
by_seq: true,
ignore_case: false,
},
)
.unwrap()
}
#[test]
fn split_id_stops_at_first_space() {
assert_eq!(split_id(b"seq1 description one"), b"seq1");
assert_eq!(split_id(b"seq1\tdescription"), b"seq1");
assert_eq!(split_id(b"seq1"), b"seq1");
}
#[test]
fn id_mode_never_checks_strand() {
let p = exact(&["seq1"]);
let opts = MatchOpts {
by_name: false,
by_seq: false,
only_positive: false,
ignore_case: false,
region: None,
};
assert!(is_hit(b"seq1", b"seq1 desc", b"ACGT", &opts, &p));
}
#[test]
fn seq_mode_checks_both_strands_by_default() {
let p = substr(&["GGGGTTTT"]);
let opts = MatchOpts {
by_name: false,
by_seq: true,
only_positive: false,
ignore_case: false,
region: None,
};
assert!(is_hit(b"id", b"id", b"TTTTAAAACCCCGGGG", &opts, &p));
}
#[test]
fn only_positive_strand_excludes_minus_hits() {
let p = substr(&["GGGGTTTT"]);
let opts = MatchOpts {
by_name: false,
by_seq: true,
only_positive: true,
ignore_case: false,
region: None,
};
assert!(!is_hit(b"id", b"id", b"TTTTAAAACCCCGGGG", &opts, &p));
}
#[test]
fn region_restricts_search_window() {
let p = substr(&["TTTT"]);
let region = Region::parse("1:4").unwrap();
let opts = MatchOpts {
by_name: false,
by_seq: true,
only_positive: true,
ignore_case: false,
region: Some(region),
};
assert!(is_hit(b"id", b"id", b"TTTTCCCC", &opts, &p));
assert!(!is_hit(b"id", b"id", b"CCCCTTTT", &opts, &p));
}
}