Skip to main content

tramlines_core/
text.rs

1//! Checks of a scalar value that every format shares, and the English
2//! that a finding is written in: the article before a word and a count.
3
4/// Whether `s` is a slug: lower-case letters and digits joined by single
5/// hyphens, `^[a-z0-9]+(-[a-z0-9]+)*$`.
6pub fn is_slug(s: &str) -> bool {
7    !s.is_empty()
8        && s.split('-').all(|w| {
9            !w.is_empty()
10                && w.bytes()
11                    .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
12        })
13}
14
15/// Whether `s` has a URL scheme.
16pub fn is_url(s: &str) -> bool {
17    let Some(i) = s.find(':') else {
18        return false;
19    };
20    let scheme = &s[..i];
21    let mut chars = scheme.chars();
22    chars.next().is_some_and(|c| c.is_ascii_alphabetic())
23        && chars.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.')
24        && (s[i + 1..].starts_with("//") || scheme.eq_ignore_ascii_case("mailto"))
25}
26
27/// Each of `items` in code font, joined by `sep`: "`a`, `b`".
28pub fn code_list<T: std::fmt::Display>(items: impl IntoIterator<Item = T>, sep: &str) -> String {
29    items
30        .into_iter()
31        .map(|i| format!("`{i}`"))
32        .collect::<Vec<_>>()
33        .join(sep)
34}
35
36/// The indefinite article before `word`, as English says the word: `an`
37/// before a vowel sound, judged from its spelling, which reads a `u` that
38/// sounds as "you" and an `h` that is silent.
39pub fn article(word: &str) -> &'static str {
40    let w = word
41        .trim_start_matches(|c: char| !c.is_ascii_alphanumeric())
42        .to_ascii_lowercase();
43    const YOU: [&str; 13] = [
44        "uni", "uin", "uid", "uli", "use", "usu", "uti", "utf", "uri", "url", "uk", "uu", "eu",
45    ];
46    const SILENT: [&str; 5] = ["hour", "honest", "honor", "heir", "html"];
47    let vowel = w.starts_with(['a', 'e', 'i', 'o', 'u'])
48        && !YOU.iter().any(|p| w.starts_with(p))
49        && !w.starts_with("one");
50    // A number reads as its first word: eight, eleven, eighteen.
51    let eight = w.starts_with('8') || w.starts_with("11") || w.starts_with("18");
52    if vowel || eight || SILENT.iter().any(|p| w.starts_with(p)) {
53        "an"
54    } else {
55        "a"
56    }
57}
58
59/// `n` with `one` or `many`, as its number takes: `1 item`, `2 items`.
60pub fn count(n: usize, one: &str, many: &str) -> String {
61    format!("{n} {}", if n == 1 { one } else { many })
62}
63
64/// `is` for one, `are` for any other number.
65pub fn is_are(n: usize) -> &'static str {
66    if n == 1 { "is" } else { "are" }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn a_count_takes_its_number() {
75        assert_eq!(count(1, "item", "items"), "1 item");
76        assert_eq!(count(0, "item", "items"), "0 items");
77        assert_eq!((is_are(1), is_are(2)), ("is", "are"));
78    }
79
80    #[test]
81    fn the_article_follows_the_sound() {
82        for (word, said) in [
83            ("image", "an"),
84            ("int", "an"),
85            ("`amount`", "an"),
86            ("item", "an"),
87            ("html", "an"),
88            ("integer", "an"),
89            ("unit", "a"),
90            ("url", "a"),
91            ("slug", "a"),
92            ("paragraph", "a"),
93            ("row", "a"),
94            ("bullet-list", "a"),
95            ("uint", "a"),
96            ("ulid", "a"),
97            ("utf8", "a"),
98            ("8bit", "an"),
99            ("one-of", "a"),
100        ] {
101            assert_eq!(article(word), said, "{word}");
102        }
103    }
104
105    #[test]
106    fn a_slug_is_lower_case_words_joined_by_hyphens() {
107        assert!(is_slug("issue-1"));
108        assert!(!is_slug("Issue"));
109        assert!(!is_slug("a--b"));
110        assert!(!is_slug(""));
111    }
112
113    #[test]
114    fn a_url_has_a_scheme() {
115        assert!(is_url("https://a"));
116        assert!(is_url("mailto:a@b"));
117        assert!(!is_url("a:b"));
118        assert!(!is_url("1http://x"));
119    }
120}