Skip to main content

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