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, 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, 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(""), ); } }