summaryrefslogblamecommitdiff
path: root/src/main.rs
blob: 8bdcca4df264a25d8428910d3e9572bff7a8f534 (plain) (tree)
1
2
3
4
5
6
7
8

                              

                                         

                                                                                      

                                                                                              



                                                                                                   



                       
                                  



                                                                 

                                                         
 
                            

                                  
                                                                                                       










                                                                                                                           
                                                 

                                                         


     
                                                                     
                                                                     

                                                      
 






                                                           
 
                              
                                        
                                                                           





                            
                                                              

                                                                                          














                                                                                       

                                          

                                                 



            








                                                                  
                            




                                                                                
                   
                                                       





                                     
                                                      
                   
                                                     


                                          
                                                          
                                               

          


                                     

                                                                                         
                   
                                              





                                   

                                                                 
     
 
use std::iter::FromIterator;
use std::collections::HashSet;
use std::env::args;
use std::io::{stdin, BufRead, 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.
/// - 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() {
    let args: Vec<String> = args().skip(1).collect();
    let mut ignorables: HashSet<String> = HashSet::new();

    for arg in args.iter() {
        match arg.as_str() {
            "--ignore" | "-i" => {
                ignorables = HashSet::from_iter(args.iter().cloned().skip(1).collect::<Vec<String>>());
                break;
            }
            "--help" | "-h" => {
                println!("Title-Case Tool.\n-i | --ignore [word...]\tIgnores a list of words that should not be altered.");
            }
            unrecognized => {
                panic!(format!("Unrecognized option: {}", unrecognized));
            }
        }
    }

    for line in BufReader::new(stdin()).lines() {
        let line = line.expect("IO error");
        println!("{}", correct_line(&line, &ignorables));
    }
}

/// Corrects a single line (title) based on placement and exceptions.
fn correct_line(line: &str, ignorables: &HashSet<String>) -> String {
    let mut line = line.split_whitespace().peekable();
    let mut result = Vec::new();

    if let Some(first) = line.next() {
        // always correct the first word
        result.push(correct_word(first, true, ignorables));
    } else {
        // handle empty lines
        return String::new();
    }

    // handle every other word
    while let Some(word) = line.next() {
        result.push(correct_word(word, line.peek().is_none(), ignorables));
    }

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

/// Corrects a single word based on exceptions and ignorables.
fn correct_word(word: &str, first_or_last: bool, ignorables: &HashSet<String>) -> String {
    if ignorables.contains(word) {
        // leave ignorables untouched
        word.to_owned()
    } else if first_or_last || !EXCEPTIONS.iter().any(|s| **s == word.to_lowercase()) {
        // capitalize words that are first, last, or aren't exceptions
        capitalize_word(&word.to_lowercase())
    } else {
        // lowercase all other words
        word.to_lowercase()
    }
}

// correct with just ignorables
// correct with ignorables and exceptions

/// Capitalizes the first letter of a single word.
fn capitalize_word(word: &str) -> String {
    let mut chars = word.chars();
    let first = chars.next().unwrap_or_default();
    first.to_uppercase().chain(chars).collect()
}

#[cfg(test)]
mod tests {
    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_correct_word() {
        let ignores: HashSet<String> = HashSet::new();
        assert_eq!(correct_word("Like", false, &ignores), String::from("like"));
        assert_eq!(correct_word("like", false, &ignores), String::from("like"));
        assert_eq!(correct_word("like", true, &ignores), String::from("Like"));
        assert_eq!(correct_word("liKe", false, &ignores), String::from("like"));
        assert_eq!(
            correct_word("computers", false, &ignores),
            String::from("Computers")
        );
    }

    #[test]
    fn test_correct_line() {
        let ignores: HashSet<String> = HashSet::new();
        assert_eq!(
            correct_line("this is a test", &ignores),
            String::from("This Is a Test")
        );
        assert_eq!(
            correct_line("similEs: lIke Or As", &ignores),
            String::from("Similes: like or As")
        );
    }

    #[test]
    fn test_proper_nouns_acronyms() {
        let ignores: HashSet<String> = ["FreeBSD".to_string()].iter().cloned().collect();

        assert_eq!(
            correct_line("FreeBSD", &ignores),
            String::from("FreeBSD")
        );
    }

    #[test]
    fn test_empty_lines() {
        let ignores: HashSet<String> = HashSet::new();
        assert_eq!(correct_line("", &ignores), String::from(""));
    }
}