use super::{BRACKETS, SEPS};
use std::sync::LazyLock;
pub(super) fn clean_title(raw: &str) -> String {
let s = strip_leading_brackets(raw);
let s = strip_paren_year(&s);
let s = strip_paren_groups(&s);
let s = normalize_separators(&s, DashPolicy::WordDashOnly);
let s = trim_trailing_punct(&s);
strip_trailing_keywords(&s)
}
pub(super) fn clean_episode_title(raw: &str) -> String {
let trimmed = raw.trim_start_matches(['.', '_', ' ', '-']);
let s = strip_leading_brackets(trimmed);
let s = strip_paren_year(&s);
let s = strip_paren_groups(&s);
let s = normalize_separators(&s, DashPolicy::WordDashOnly);
trim_trailing_punct(&s)
}
pub(super) fn clean_title_preserve_dashes(raw: &str) -> String {
let s = strip_leading_brackets(raw);
let s = strip_paren_year(&s);
let s = strip_paren_groups(&s);
let s = normalize_separators(&s, DashPolicy::PreserveStructuralDash);
trim_trailing_punct(&s)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum DashPolicy {
WordDashOnly,
PreserveStructuralDash,
}
pub(super) fn strip_leading_brackets(raw: &str) -> String {
let mut s = raw.to_string();
while s.starts_with('[') {
if let Some(end) = s.find(']') {
s = s[end + 1..].to_string();
s = s.trim_start_matches(SEPS).to_string();
} else {
break;
}
}
s
}
static RE_PAREN_YEAR: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"\s*\((?:19|20)\d{2}\)\s*$").expect("RE_PAREN_YEAR regex is valid")
});
pub(super) fn strip_paren_year(s: &str) -> String {
if let Some(m) = RE_PAREN_YEAR.find(s) {
s[..m.start()].to_string()
} else {
s.to_string()
}
}
static RE_PAREN_GROUP: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"\s*\([^)]*\)\s*").expect("RE_PAREN_GROUP regex is valid"));
pub(super) fn strip_paren_groups(s: &str) -> String {
let stripped = RE_PAREN_GROUP.replace_all(s, " ").into_owned();
if stripped.trim().is_empty() {
s.to_string()
} else {
stripped
}
}
static RE_DOT_ACRONYM: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"(?:^|[\s._])([A-Za-z0-9](?:\.[A-Za-z0-9]){2,}\.?)")
.expect("RE_DOT_ACRONYM regex is valid")
});
fn acronym_match_starts_at_separator(input: &str, match_start: usize) -> bool {
match_start > 0 && matches!(input.as_bytes()[match_start], b' ' | b'\t' | b'.' | b'_')
}
fn position_in_any_range(pos: usize, ranges: &[(usize, usize)]) -> bool {
ranges.iter().any(|(s, e)| pos >= *s && pos < *e)
}
pub(super) fn normalize_separators(s: &str, dash: DashPolicy) -> String {
let protected_ranges: Vec<(usize, usize)> = RE_DOT_ACRONYM
.find_iter(s)
.map(|m| {
let actual_start = if acronym_match_starts_at_separator(s, m.start()) {
m.start() + 1
} else {
m.start()
};
(actual_start, m.end())
})
.collect();
let in_protected = |pos: usize| -> bool { position_in_any_range(pos, &protected_ranges) };
let chars: Vec<char> = s.chars().collect();
let mut byte_positions: Vec<usize> = Vec::with_capacity(chars.len());
let mut byte_pos = 0;
for &c in &chars {
byte_positions.push(byte_pos);
byte_pos += c.len_utf8();
}
let mut out = String::with_capacity(s.len());
for (i, &c) in chars.iter().enumerate() {
match c {
'-' => {
let kind = classify_dash(&chars, i);
match (kind, dash) {
(DashKind::WordDash, _) => out.push('-'),
(DashKind::SeparatorFlanked, DashPolicy::PreserveStructuralDash) => {
if out.ends_with(' ') {
out.pop();
}
out.push_str(" - ");
}
_ => out.push(' '),
}
}
'.' if in_protected(byte_positions[i]) => out.push('.'),
ch if SEPS.contains(&ch) || BRACKETS.contains(&ch) || ch == '*' => out.push(' '),
ch => out.push(ch),
}
}
collapse_spaces(&out)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DashKind {
WordDash,
SeparatorFlanked,
Other,
}
fn classify_dash(chars: &[char], i: usize) -> DashKind {
let prev = if i > 0 { Some(chars[i - 1]) } else { None };
let next = chars.get(i + 1).copied();
let is_alnum = |c: Option<char>| c.is_some_and(|c| c.is_alphanumeric());
let is_sep = |c: Option<char>| c.is_some_and(|c| SEPS.contains(&c));
if is_alnum(prev) && is_alnum(next) {
DashKind::WordDash
} else if is_sep(prev) && is_sep(next) {
DashKind::SeparatorFlanked
} else {
DashKind::Other
}
}
pub(super) fn trim_trailing_punct(s: &str) -> String {
s.trim_end_matches([':', '-', ',', ';']).trim().to_string()
}
static RE_TRAILING_PART: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"(?i)\s+Part\s*(?:I{1,4}|IV|VI{0,3}|IX|X{0,3}|[0-9]+)?\s*$")
.expect("RE_TRAILING_PART regex is valid")
});
static RE_TRAILING_SEASON: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(
r"(?i)\s+(?:S\d{1,3}|(?:Saison|Temporada|Stagione|Tem\.?|Season|Seasons?)\s*(?:I{1,4}|IV|VI{0,3}|IX|X{0,3}|[0-9]+)?(?:\s*(?:&|and)\s*(?:I{1,4}|IV|VI{0,3}|IX|X{0,3}|[0-9]+))?)\s*$"
).expect("RE_TRAILING_SEASON regex is valid")
});
static RE_TRAILING_EP: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"(?i)\s+(?:Episodes?|Ep\.?)\s*$").expect("RE_TRAILING_EP regex is valid")
});
static RE_TRAILING_BONUS: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"(?i)[-]x\d{1,3}\s*$").expect("RE_TRAILING_BONUS regex is valid")
});
pub(super) fn strip_trailing_keywords(input: &str) -> String {
let mut s = input.to_string();
s = strip_if_nonempty(&s, &RE_TRAILING_PART);
s = strip_if_nonempty(&s, &RE_TRAILING_SEASON);
s = strip_if_nonempty(&s, &RE_TRAILING_EP);
s = strip_if_nonempty(&s, &RE_TRAILING_BONUS);
s
}
fn strip_if_nonempty(s: &str, re: ®ex::Regex) -> String {
if let Some(m) = re.find(s) {
let stripped = &s[..m.start()];
if !stripped.trim().is_empty() {
return stripped.to_string();
}
}
s.to_string()
}
pub(super) fn collapse_spaces(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut prev_space = true;
for c in s.chars() {
if c == ' ' {
if !prev_space {
result.push(' ');
}
prev_space = true;
} else {
result.push(c);
prev_space = false;
}
}
result.trim().to_string()
}
pub(super) fn strip_extension(s: &str) -> &str {
if let Some(dot) = s.rfind('.') {
let ext = &s[dot + 1..];
let ext_lower = ext.to_lowercase();
if ext.len() <= 5 && is_likely_extension(&ext_lower) {
return &s[..dot];
}
}
s
}
pub(super) fn is_likely_extension(ext: &str) -> bool {
matches!(
ext,
"mkv"
| "mp4"
| "avi"
| "wmv"
| "flv"
| "mov"
| "webm"
| "ogm"
| "ogv"
| "ts"
| "m2ts"
| "m4v"
| "mpg"
| "mpeg"
| "vob"
| "divx"
| "3gp"
| "srt"
| "sub"
| "ssa"
| "ass"
| "idx"
| "sup"
| "vtt"
| "nfo"
| "txt"
| "jpg"
| "jpeg"
| "png"
| "nzb"
| "par"
| "par2"
| "iso"
| "img"
| "rar"
| "zip"
| "7z"
)
}
pub(super) fn is_abbreviated(title: &str) -> bool {
let segments: Vec<&str> = title
.split(|c: char| c.is_whitespace() || c == '-')
.collect();
segments.iter().all(|w| {
w.len() <= 6
&& w.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
}) && title.len() <= 20
}
#[inline]
pub(super) fn casing_score(s: &str) -> i32 {
if s.chars()
.filter(|c| c.is_alphabetic())
.all(|c| c.is_uppercase())
{
return -10;
}
if s.chars()
.filter(|c| c.is_alphabetic())
.all(|c| c.is_lowercase())
{
return -5;
}
s.split_whitespace()
.filter(|w| w.starts_with(|c: char| c.is_uppercase()))
.count() as i32
}
pub(super) fn pick_better_casing<'a>(a: &'a str, b: &'a str) -> &'a str {
if casing_score(a) >= casing_score(b) {
a
} else {
b
}
}
pub(crate) fn is_generic_dir(name: &str) -> bool {
let lower = name.to_lowercase();
if matches!(
lower.as_str(),
"movies"
| "movie"
| "films"
| "film"
| "series"
| "tv shows"
| "tvshows"
| "tv"
| "media"
| "video"
| "videos"
| "anime"
| "donghua"
| "kids"
| "cartoons"
| "shows"
| "documentary"
| "documentaries"
| "music"
| "concert"
| "concerts"
| "chinese"
| "english"
| "japanese"
| "korean"
| "french"
| "german"
| "spanish"
| "italian"
| "portuguese"
| "russian"
| "thai"
| "hindi"
| "arabic"
| "downloads"
| "download"
| "completed"
| "mnt"
| "nas"
| "share"
| "shares"
| "data"
| "public"
| "home"
| "tmp"
| "temp"
| "extras"
| "extra"
| "specials"
| "special"
| "bonus"
| "featurettes"
| "featurette"
| "behind the scenes"
| "deleted scenes"
| "interviews"
| "interview"
| "trailers"
| "trailer"
| "samples"
| "sample"
| "特典映像" | "特典" | "映像特典" | "sp"
| "pv"
| "op"
| "ed"
| "ncop"
| "nced"
| "ncop&nced"
| "nced&ncop"
| "menu"
| "menus"
| "ova"
| "oad"
| "ona"
| "cm"
| "tokuten"
| "subs"
| "subtitles"
| "subtitle"
| "ost"
| "soundtrack"
| "soundtracks"
) {
return true;
}
if lower.starts_with("season")
|| lower.starts_with("saison")
|| lower.starts_with("temporada")
|| lower.starts_with("stagione")
|| lower.starts_with("disc")
|| lower.starts_with("disk")
|| lower.starts_with("dvd")
{
return true;
}
if lower.starts_with("cd") && lower[2..].chars().all(|c| c.is_ascii_digit()) && lower.len() <= 4
{
return true;
}
if lower.ends_with('p') && lower[..lower.len() - 1].chars().all(|c| c.is_ascii_digit()) {
return true;
}
if lower == "4k" {
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clean_title_dots() {
assert_eq!(clean_title("The.Matrix"), "The Matrix");
}
#[test]
fn test_clean_title_underscores() {
assert_eq!(clean_title("The_Matrix_Reloaded"), "The Matrix Reloaded");
}
#[test]
fn test_strip_leading_bracket() {
assert_eq!(clean_title("[XCT].Le.Prestige"), "Le Prestige");
}
#[test]
fn test_strip_paren_year() {
assert_eq!(clean_title("Movie Name (2005)"), "Movie Name");
}
#[test]
fn step_strip_leading_brackets_drops_multiple() {
assert_eq!(strip_leading_brackets("[A][B] Show"), "Show");
assert_eq!(strip_leading_brackets("[XCT].Le.Prestige"), "Le.Prestige");
}
#[test]
fn step_strip_leading_brackets_unclosed_passes_through() {
assert_eq!(strip_leading_brackets("[unclosed Show"), "[unclosed Show");
}
#[test]
fn step_strip_paren_year_only_at_end() {
assert_eq!(strip_paren_year("Movie (2005)"), "Movie");
assert_eq!(strip_paren_year("(2005) Movie"), "(2005) Movie");
}
#[test]
fn step_strip_paren_groups_empty_fallback() {
assert_eq!(strip_paren_groups("(only paren)"), "(only paren)");
assert_eq!(strip_paren_groups("Movie (alt)"), "Movie ");
}
#[test]
fn step_normalize_preserves_word_dash() {
let out = normalize_separators("Spider-Man.2002", DashPolicy::WordDashOnly);
assert_eq!(out, "Spider-Man 2002");
}
#[test]
fn step_normalize_drops_separator_flanked_dash() {
let out = normalize_separators("Show - Sub", DashPolicy::WordDashOnly);
assert_eq!(out, "Show Sub");
}
#[test]
fn step_normalize_preserves_separator_flanked_dash_in_preserve_mode() {
assert_eq!(
normalize_separators("Show - Sub", DashPolicy::PreserveStructuralDash),
"Show - Sub"
);
assert_eq!(
normalize_separators("Show_-_Sub_-_Final", DashPolicy::PreserveStructuralDash),
"Show - Sub - Final"
);
assert_eq!(
normalize_separators("Show.-.Sub", DashPolicy::PreserveStructuralDash),
"Show - Sub"
);
assert_eq!(
normalize_separators("Spider-Man", DashPolicy::PreserveStructuralDash),
"Spider-Man"
);
}
#[test]
fn preserve_dashes_keeps_inner_separator() {
assert_eq!(clean_title("Show - Subtitle"), "Show Subtitle");
assert_eq!(
clean_title_preserve_dashes("Show - Subtitle"),
"Show - Subtitle"
);
assert_eq!(
clean_title_preserve_dashes("San no Shou Part 2"),
"San no Shou Part 2"
);
assert_eq!(
clean_title_preserve_dashes("Show_-_Sub_-_Final"),
"Show - Sub - Final"
);
}
#[test]
fn preserve_dashes_kitchen_sink_composition() {
let input =
"[Group].Show_-_Sub.-.Detail.(Director's.Cut).Part.2.S.H.I.E.L.D._-_End.(2014).";
let expected = "Show - Sub - Detail Part 2.S.H.I.E.L.D. - End";
assert_eq!(
clean_title_preserve_dashes(input),
expected,
"composition order regression: each step must run on the \
output of the previous one in the documented sequence"
);
}
#[test]
fn step_normalize_preserves_dot_acronyms() {
let out = normalize_separators("Agents.of.S.H.I.E.L.D.S01", DashPolicy::WordDashOnly);
assert!(out.contains("S.H.I.E.L.D"), "got: {out}");
}
#[test]
fn step_trim_trailing_punct() {
assert_eq!(trim_trailing_punct("Title -"), "Title");
assert_eq!(trim_trailing_punct("Title:,;"), "Title");
assert_eq!(trim_trailing_punct("Title"), "Title");
}
#[test]
fn step_strip_trailing_keywords() {
assert_eq!(strip_trailing_keywords("Show Part 2"), "Show");
assert_eq!(strip_trailing_keywords("Show Season 3"), "Show");
assert_eq!(strip_trailing_keywords("Show Episode"), "Show");
assert_eq!(strip_trailing_keywords("Show -x05"), "Show ");
assert_eq!(strip_trailing_keywords("Part 2"), "Part 2");
}
#[test]
fn episode_title_keeps_part_n() {
assert_eq!(
clean_episode_title("The Battle Part 2"),
"The Battle Part 2"
);
}
#[test]
fn episode_title_trims_leading_seps() {
assert_eq!(clean_episode_title(" - The Battle"), "The Battle");
assert_eq!(clean_episode_title(".._The Battle"), "The Battle");
}
#[test]
fn generic_dir_originals() {
assert!(is_generic_dir("Movies"));
assert!(is_generic_dir("tv"));
assert!(is_generic_dir("Season 1"));
assert!(is_generic_dir("Saison 03"));
}
#[test]
fn generic_dir_extras_and_bonus() {
assert!(is_generic_dir("Extras"));
assert!(is_generic_dir("Specials"));
assert!(is_generic_dir("Bonus"));
assert!(is_generic_dir("Featurettes"));
assert!(is_generic_dir("Behind The Scenes"));
assert!(is_generic_dir("Deleted Scenes"));
assert!(is_generic_dir("Trailers"));
assert!(is_generic_dir("Sample"));
}
#[test]
fn generic_dir_disc_and_cd() {
assert!(is_generic_dir("Disc 1"));
assert!(is_generic_dir("Disc2"));
assert!(is_generic_dir("Disk 3"));
assert!(is_generic_dir("DVD1"));
assert!(is_generic_dir("CD1"));
assert!(is_generic_dir("CD2"));
assert!(!is_generic_dir("CD123")); }
#[test]
fn generic_dir_quality() {
assert!(is_generic_dir("1080p"));
assert!(is_generic_dir("720p"));
assert!(is_generic_dir("2160p"));
assert!(is_generic_dir("4K"));
}
#[test]
fn generic_dir_recognizes_all_season_prefix_synonyms() {
assert!(
is_generic_dir("Temporada 2"),
"Portuguese 'Temporada' (Season) prefix"
);
assert!(
is_generic_dir("Stagione 1"),
"Italian 'Stagione' (Season) prefix"
);
assert!(is_generic_dir("DVD9"), "DVD prefix (e.g., DVD5, DVD9)");
assert!(is_generic_dir("dvdrip"), "DVD prefix is case-insensitive");
}
#[test]
fn classify_dash_word_dash_when_both_flanks_alphanumeric() {
let chars: Vec<char> = "Spider-Man".chars().collect();
let dash_idx = chars.iter().position(|&c| c == '-').unwrap();
assert_eq!(classify_dash(&chars, dash_idx), DashKind::WordDash);
}
#[test]
fn classify_dash_separator_flanked_when_both_sides_are_separators() {
let chars: Vec<char> = "Show - Title".chars().collect();
let dash_idx = chars.iter().position(|&c| c == '-').unwrap();
assert_eq!(classify_dash(&chars, dash_idx), DashKind::SeparatorFlanked);
}
#[test]
fn classify_dash_other_when_only_one_flank_is_separator() {
let chars: Vec<char> = "Foo -[Bar".chars().collect();
let dash_idx = chars.iter().position(|&c| c == '-').unwrap();
assert_eq!(classify_dash(&chars, dash_idx), DashKind::Other);
let chars: Vec<char> = "[- Bar".chars().collect();
let dash_idx = chars.iter().position(|&c| c == '-').unwrap();
assert_eq!(classify_dash(&chars, dash_idx), DashKind::Other);
}
#[test]
fn classify_dash_at_boundaries() {
let chars: Vec<char> = "-Foo".chars().collect();
assert_eq!(classify_dash(&chars, 0), DashKind::Other);
let chars: Vec<char> = "Foo-".chars().collect();
let last = chars.len() - 1;
assert_eq!(classify_dash(&chars, last), DashKind::Other);
}
#[test]
fn generic_dir_subtitles_and_audio() {
assert!(is_generic_dir("Subs"));
assert!(is_generic_dir("Subtitles"));
assert!(is_generic_dir("OST"));
assert!(is_generic_dir("Soundtrack"));
}
#[test]
fn generic_dir_structural() {
assert!(is_generic_dir("Anime"));
assert!(is_generic_dir("Kids"));
assert!(is_generic_dir("Cartoons"));
assert!(is_generic_dir("Shows"));
assert!(is_generic_dir("Documentary"));
assert!(is_generic_dir("Documentaries"));
assert!(is_generic_dir("Music"));
assert!(is_generic_dir("Concert"));
assert!(is_generic_dir("Concerts"));
}
#[test]
fn generic_dir_language_categories() {
assert!(is_generic_dir("Chinese"));
assert!(is_generic_dir("English"));
assert!(is_generic_dir("Japanese"));
assert!(is_generic_dir("Korean"));
assert!(is_generic_dir("French"));
assert!(is_generic_dir("German"));
assert!(is_generic_dir("Spanish"));
assert!(is_generic_dir("Italian"));
assert!(is_generic_dir("Portuguese"));
assert!(is_generic_dir("Russian"));
assert!(is_generic_dir("Thai"));
assert!(is_generic_dir("Hindi"));
assert!(is_generic_dir("Arabic"));
}
#[test]
fn generic_dir_cjk_bonus() {
assert!(is_generic_dir("特典映像"));
assert!(is_generic_dir("特典"));
assert!(is_generic_dir("映像特典"));
assert!(is_generic_dir("SP"));
}
#[test]
fn non_generic_dirs() {
assert!(!is_generic_dir("Paw Patrol"));
assert!(!is_generic_dir("Transformers 1984"));
assert!(!is_generic_dir("Breaking Bad"));
assert!(!is_generic_dir("十二国記"));
}
#[test]
fn casing_score_all_uppercase_returns_minus_ten() {
assert_eq!(casing_score("THE MATRIX"), -10);
assert_eq!(casing_score("AVENGERS"), -10);
assert_eq!(casing_score("X"), -10);
}
#[test]
fn casing_score_all_lowercase_returns_minus_five() {
assert_eq!(casing_score("the matrix"), -5);
assert_eq!(casing_score("avengers"), -5);
assert_eq!(casing_score("x"), -5);
}
#[test]
fn casing_score_title_case_counts_capitalized_words() {
assert_eq!(casing_score("The Matrix"), 2);
assert_eq!(casing_score("Star Wars Episode IV"), 4);
assert_eq!(casing_score("Matrix"), 1);
}
#[test]
fn casing_score_mixed_case_only_caps_starts_count() {
assert_eq!(casing_score("the Matrix"), 1);
assert_eq!(casing_score("The matrix Reloaded"), 2);
}
#[test]
fn casing_score_no_alphabetic_treats_as_all_upper() {
assert_eq!(casing_score(""), -10);
assert_eq!(casing_score("123"), -10);
assert_eq!(casing_score(" "), -10);
}
#[test]
fn pick_better_casing_picks_higher_score() {
assert_eq!(pick_better_casing("The Matrix", "the matrix"), "The Matrix");
assert_eq!(pick_better_casing("the matrix", "The Matrix"), "The Matrix");
assert_eq!(pick_better_casing("THE MATRIX", "The Matrix"), "The Matrix");
assert_eq!(pick_better_casing("THE MATRIX", "the matrix"), "the matrix");
}
#[test]
fn pick_better_casing_tie_returns_left() {
assert_eq!(pick_better_casing("The Matrix", "Star Wars"), "The Matrix");
assert_eq!(pick_better_casing("Star Wars", "The Matrix"), "Star Wars");
assert_eq!(pick_better_casing("the matrix", "star wars"), "the matrix");
assert_eq!(pick_better_casing("THE MATRIX", "STAR WARS"), "THE MATRIX");
assert_eq!(pick_better_casing("Same", "Same"), "Same");
}
#[test]
fn strip_extension_no_dot_returns_input_unchanged() {
assert_eq!(strip_extension("NoExtensionHere"), "NoExtensionHere");
}
#[test]
fn strip_extension_known_extension_strips_dot_and_ext() {
assert_eq!(strip_extension("The.Matrix.mkv"), "The.Matrix");
assert_eq!(strip_extension("movie.mp4"), "movie");
}
#[test]
fn strip_extension_dot_plus_one_indexing_is_correct() {
assert_eq!(strip_extension("foo.mkv"), "foo");
}
#[test]
fn strip_extension_unknown_short_extension_keeps_input() {
assert_eq!(strip_extension("foo.xyzab"), "foo.xyzab");
}
#[test]
fn strip_extension_long_known_lookalike_keeps_input() {
assert_eq!(
strip_extension("sample.notarealextension"),
"sample.notarealextension"
);
}
#[test]
fn strip_extension_known_three_char_extension_pins_le_boundary() {
assert_eq!(strip_extension("clip.mkv"), "clip");
assert_eq!(strip_extension("video.webm"), "video");
}
#[test]
fn strip_extension_case_insensitive_match() {
assert_eq!(strip_extension("Movie.MKV"), "Movie");
assert_eq!(strip_extension("Trailer.Mp4"), "Trailer");
}
#[test]
fn acronym_match_starts_at_separator_at_position_zero_returns_false() {
assert!(!acronym_match_starts_at_separator("S.H.I.E.L.D", 0));
}
#[test]
fn acronym_match_starts_at_separator_position_zero_with_separator_byte() {
assert!(!acronym_match_starts_at_separator(" foo", 0));
assert!(!acronym_match_starts_at_separator(".foo", 0));
assert!(!acronym_match_starts_at_separator("_foo", 0));
}
#[test]
fn acronym_match_starts_at_separator_with_each_separator_byte() {
assert!(!acronym_match_starts_at_separator("abcd", 2));
assert!(acronym_match_starts_at_separator("a foo", 1)); assert!(acronym_match_starts_at_separator("a\tfoo", 1)); assert!(acronym_match_starts_at_separator("a.foo", 1)); assert!(acronym_match_starts_at_separator("a_foo", 1)); }
#[test]
fn position_in_any_range_empty_list_returns_false() {
assert!(!position_in_any_range(0, &[]));
assert!(!position_in_any_range(100, &[]));
}
#[test]
fn position_in_any_range_pins_lower_bound_inclusive() {
assert!(position_in_any_range(10, &[(10, 20)]));
}
#[test]
fn position_in_any_range_pins_upper_bound_exclusive() {
assert!(!position_in_any_range(20, &[(10, 20)]));
assert!(position_in_any_range(19, &[(10, 20)]));
}
#[test]
fn position_in_any_range_pins_and_to_or_with_disjoint_pos() {
assert!(!position_in_any_range(5, &[(10, 20)]));
assert!(!position_in_any_range(25, &[(10, 20)]));
}
#[test]
fn position_in_any_range_finds_match_in_second_range() {
let ranges = [(0, 5), (10, 20), (30, 40)];
assert!(position_in_any_range(15, &ranges));
assert!(position_in_any_range(35, &ranges));
assert!(!position_in_any_range(7, &ranges));
assert!(!position_in_any_range(25, &ranges));
}
}