flix_model/
text.rs

1//! This module contains helper functions for normalizing media titles
2
3use core::iter::Peekable;
4
5use itertools::Itertools;
6
7/// # Panics
8///
9/// Panics if `input` is not ASCII.
10fn split_normalized_words(input: &str) -> impl Iterator<Item = String> {
11	if !input.is_ascii() {
12		panic!("Input is not ASCII: {input}");
13	}
14
15	input.split_ascii_whitespace().map(|s| {
16		let chars = s
17			.chars()
18			.filter(|c| c.is_ascii_alphanumeric() || *c == '-')
19			.map(|c| c.to_ascii_lowercase());
20
21		if s.len() > 4
22			&& s.len().is_multiple_of(2)
23			&& chars.clone().tuples().all(|(l, r)| l != '.' && r == '.')
24		{
25			// Collapse acronym
26			chars.tuples().map(|(l, _)| l).collect()
27		} else {
28			chars.collect()
29		}
30	})
31}
32
33fn split_leading_article<I: Iterator<Item = String>>(iter: I) -> (Option<String>, Peekable<I>) {
34	let mut iter = iter.peekable();
35	match iter.peek().map(String::as_str) {
36		Some("a" | "an" | "the") => (iter.next(), iter),
37		_ => (None, iter),
38	}
39}
40
41/// Convert a media title to be sortable and searchable
42///
43///     use flix_model::text::make_sortable_title;
44///
45///     assert_eq!(make_sortable_title("The Matrix"), "matrix, the");
46///     assert_eq!(make_sortable_title("Marvel's Agents of S.H.I.E.L.D."), "marvels agents of shield");
47///     assert_eq!(make_sortable_title("Avatar: The Last Airbender"), "avatar the last airbender");
48///
49/// # Panics
50///
51/// Panics if `input` is not ASCII.
52pub fn make_sortable_title(title: &str) -> String {
53	let words = split_normalized_words(title);
54	let (article, words) = split_leading_article(words);
55
56	let output = Itertools::intersperse(words, " ".to_string());
57	if let Some(article) = article {
58		output.chain([", ".to_string(), article]).collect()
59	} else {
60		output.collect()
61	}
62}
63
64/// Convert a media title to a folder name representable on filesystems
65///
66///     use flix_model::text::make_fs_slug;
67///
68///     assert_eq!(make_fs_slug("The Matrix"), "matrix");
69///     assert_eq!(make_fs_slug("Marvel's Agents of S.H.I.E.L.D."), "marvels agents of shield");
70///     assert_eq!(make_fs_slug("Avatar: The Last Airbender"), "avatar the last airbender");
71///
72/// # Panics
73///
74/// Panics if `input` is not ASCII.
75pub fn make_fs_slug(title: &str) -> String {
76	let words = split_normalized_words(title);
77	let (_, words) = split_leading_article(words);
78
79	Itertools::intersperse(words, " ".to_string()).collect()
80}
81
82/// Convert a media title and year to a folder name representable on filesystems
83///
84///     use flix_model::text::make_fs_slug_year;
85///
86///     assert_eq!(make_fs_slug_year("The Matrix", 1999), "matrix (1999)");
87///     assert_eq!(make_fs_slug_year("Marvel's Agents of S.H.I.E.L.D.", 2013), "marvels agents of shield (2013)");
88///     assert_eq!(make_fs_slug_year("Avatar: The Last Airbender", 2005), "avatar the last airbender (2005)");
89///
90/// # Panics
91///
92/// Panics if `input` is not ASCII.
93pub fn make_fs_slug_year(title: &str, year: i32) -> String {
94	let words = split_normalized_words(title);
95	let (_, words) = split_leading_article(words);
96
97	Itertools::intersperse(words, " ".to_string())
98		.chain([format!(" ({year})")])
99		.collect()
100}
101
102/// Normalize a filesystem name
103///
104///     use flix_model::text::normalize_fs_name;
105///
106///     assert_eq!(normalize_fs_name("Matrix (1999)"), "matrix (1999)");
107///     assert_eq!(normalize_fs_name("Marvel's Agents of SHIELD (2013)"), "marvels agents of shield (2013)");
108///     assert_eq!(normalize_fs_name("Avatar The Last Airbender (2005)"), "avatar the last airbender (2005)");
109pub fn normalize_fs_name(input: &str) -> String {
110	let chars = input.split_ascii_whitespace().map(|s| {
111		let chars = s
112			.chars()
113			.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '(' || *c == ')')
114			.map(|c| c.to_ascii_lowercase());
115
116		if s.len() > 4
117			&& s.len().is_multiple_of(2)
118			&& chars.clone().tuples().all(|(l, r)| l != '.' && r == '.')
119		{
120			// Collapse acronym
121			chars.tuples().map(|(l, _)| l).collect()
122		} else {
123			chars.collect()
124		}
125	});
126	Itertools::intersperse(chars, " ".to_string()).collect()
127}
128
129/// Convert a media title to a url compatible string
130///
131///     use flix_model::text::make_web_slug;
132///
133///     assert_eq!(make_web_slug("The Matrix"), "matrix");
134///     assert_eq!(make_web_slug("Marvel's Agents of S.H.I.E.L.D."), "marvels-agents-of-shield");
135///     assert_eq!(make_web_slug("Avatar: The Last Airbender"), "avatar-the-last-airbender");
136///
137/// # Panics
138///
139/// Panics if `input` is not ASCII.
140pub fn make_web_slug(title: &str) -> String {
141	let words = split_normalized_words(title);
142	let (_, words) = split_leading_article(words);
143
144	Itertools::intersperse(words, "-".to_string()).collect()
145}
146
147/// Convert a media title and year to a url compatible string
148///
149///     use flix_model::text::make_web_slug_year;
150///
151///     assert_eq!(make_web_slug_year("The Matrix", 1999), "matrix-1999");
152///     assert_eq!(make_web_slug_year("Marvel's Agents of S.H.I.E.L.D.", 2013), "marvels-agents-of-shield-2013");
153///     assert_eq!(make_web_slug_year("Avatar: The Last Airbender", 2005), "avatar-the-last-airbender-2005");
154///
155/// # Panics
156///
157/// Panics if `input` is not ASCII.
158pub fn make_web_slug_year(title: &str, year: i32) -> String {
159	let words = split_normalized_words(title);
160	let (_, words) = split_leading_article(words);
161
162	Itertools::intersperse(words, "-".to_string())
163		.chain([format!("-{year}")])
164		.collect()
165}