summaryrefslogtreecommitdiff
path: root/src/main.rs
blob: 2b670e65006e2cdd5b31046c35d92cd470200a1c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
use std::io::stdin;
use std::io::BufRead;
use std::io::BufReader;

/// Words that will not be capitalized unless they are at the start or end of a title.
const EXCEPTIONS: &[&str] = &[
    "vs", "above", "across", "against", "at", "between", "by", "along", "among", "down", "in",
    "once", "around", "of", "off", "on", "to", "with", "before", "behind", "below", "beneath",
    "down", "from", "near", "toward", "upon", "within", "a", "an", "the", "and", "but", "for",
    "or", "nor", "so", "till", "when", "yet", "that", "than", "in", "into", "like", "onto", "over",
    "past", "as", "if",
];

/// Here are the rules:
///
/// - Every line is its own title. No multi-line titles.
/// - All words are capitalized, except...
/// - Words are not capitalized if they are EXCEPTIONS, unless...
/// - Words are either the first or the last word.
fn main() {
    for line in BufReader::new(stdin()).lines() {
        println!("{}", capitalize_line(&line.expect("IO error")));
    }
}

/// Capitalizes a single line (title) based on placement and exceptions.
fn capitalize_line(line: &str) -> String {
    let mut line = line.split_whitespace().map(str::to_lowercase).peekable();
    let mut result: Vec<String> = Vec::new();

    // handle first word
    let first = line.next().expect("No words on line");
    result.push(capitalize_word(&first));

    while let Some(word) = line.next() {
        let word = if line.peek().is_none() || !EXCEPTIONS.contains(&word.as_str()) {
            // capitalize words that are last or aren't in exceptions
            capitalize_word(&word)
        } else {
            // ignore the rest
            word
        };

        result.push(word);
    }

    // join words into title
    result.join(" ")
}

/// Capitalizes a single word.
fn capitalize_word(word: &str) -> String {
    let mut chars = word.chars();
    let first = chars.next().unwrap();
    format!("{}{}", first.to_uppercase(), chars.collect::<String>())
}

#[cfg(test)]
mod tests {
    // Note this useful idiom: importing names from outer (for mod tests) scope.
    use super::*;

    #[test]
    fn test_capitalize_word() {
        assert_eq!(capitalize_word("word"), String::from("Word"));
        assert_eq!(capitalize_word("as"), String::from("As"));
    }

    #[test]
    fn test_capitalize_line() {
        assert_eq!(
            capitalize_line("this is a test"),
            String::from("This Is a Test")
        );
        assert_eq!(
            capitalize_line("similEs: lIke Or As"),
            String::from("Similes: Like or As")
        );
    }
}