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

                                               
                                                    
                                                                                 
 

                                                    
                                                                                      

                                                                                              



                                                                                                   



                       
                                  



                                                                 
                  

                                   
                                        

                                       





                                            
                                         
                       
             



                                            





                                                                         


                                                                                 

                                              
                                                 
                                           





                                                         
     
                                                    

 
                                                                     




                                       
                                                      
                                   
 

                                        
                                                                            
            

                           
     
 
                              
                                        






                                      

     
          

 
                                                              


                        
                                 

                                       


                                             
                                                                                    







                                                                      
                                                  

                                          







                                                        



            








                                                                  
                            
                   





                                                     


                                





                                                     


                                





                                                     


                                





                                                     


                                
                                                                               






                                     




                                                     


                                          

                                      
                                

                                                     
                                               

          


                                     

                                                   
                   
                                                                  





                                   



                                                               
     
 
use std::collections::HashSet;
use std::env::args;
use std::io::{prelude::*, stdin, stdout, BufReader, BufWriter};

const HELP_MSG: &str = "Title-Case Tool\n\
        -h | --help\t\t\tPrints this message\n\
        -v | --version\t\t\tPrints version number\n\
        -i | --ignore [word...]\t\tA list of words that should not be corrected";

const VERSION_MSG: &str = env!("CARGO_PKG_VERSION");

/// 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() {
    // gather args
    let mut args = args();
    args.next(); // ignore self arg
    let mut ignorables = HashSet::new();

    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--ignore" | "-i" => {
                ignorables = args.collect();
                break;
            }
            "--help" | "-h" => {
                println!("{}", HELP_MSG);
                return;
            }
            "--version" | "-v" => {
                println!("{}", VERSION_MSG);
                return;
            }
            unrecognized => {
                panic!(format!("Unrecognized option: {}", unrecognized));
            }
        }
    }

    // gather exceptions
    let exceptions: HashSet<&'static str> = EXCEPTIONS.iter().cloned().collect();

    // read input, correct lines, write output
    let mut writer = BufWriter::new(stdout());
    for line in BufReader::new(stdin()).lines() {
        let line = line.expect("IO error");
        writeln!(
            &mut writer,
            "{}",
            correct_line(&line, &ignorables, &exceptions)
        )
        .expect("Failed to write output");
    }
    writer.flush().expect("Failed to write output");
}

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

    if let Some(first) = line.next() {
        // always correct the first word
        result.push_str(&correct_word(first, true, ignorables, exceptions));
    } else {
        // skip empty lines
        return result;
    }

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

    result
}

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

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

    if let Some(first) = chars.next() {
        // uppercase first character and append the rest
        first.to_uppercase().chain(chars).collect()
    } else {
        // handle empty words
        String::new()
    }
}

#[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() {
        assert_eq!(
            correct_word(
                "Like",
                false,
                &HashSet::new(),
                &EXCEPTIONS.iter().cloned().collect()
            ),
            String::from("like")
        );
        assert_eq!(
            correct_word(
                "like",
                false,
                &HashSet::new(),
                &EXCEPTIONS.iter().cloned().collect()
            ),
            String::from("like")
        );
        assert_eq!(
            correct_word(
                "like",
                true,
                &HashSet::new(),
                &EXCEPTIONS.iter().cloned().collect()
            ),
            String::from("Like")
        );
        assert_eq!(
            correct_word(
                "liKe",
                false,
                &HashSet::new(),
                &EXCEPTIONS.iter().cloned().collect()
            ),
            String::from("like")
        );
        assert_eq!(
            correct_word("computers", false, &HashSet::new(), &HashSet::new()),
            String::from("Computers")
        );
    }

    #[test]
    fn test_correct_line() {
        assert_eq!(
            correct_line(
                "this is a test",
                &HashSet::new(),
                &EXCEPTIONS.iter().cloned().collect()
            ),
            String::from("This Is a Test")
        );
        assert_eq!(
            correct_line(
                "similEs: lIke Or As",
                &HashSet::new(),
                &EXCEPTIONS.iter().cloned().collect()
            ),
            String::from("Similes: like or As")
        );
    }

    #[test]
    fn test_proper_nouns_acronyms() {
        let mut ignorables = HashSet::new();
        ignorables.insert(String::from("FreeBSD"));
        assert_eq!(
            correct_line("FreeBSD", &ignorables, &HashSet::new()),
            String::from("FreeBSD")
        );
    }

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