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 = args().skip(1).collect(); let mut ignorables: HashSet = HashSet::new(); for arg in args.iter() { match arg.as_str() { "--ignore" | "-i" => { ignorables = HashSet::from_iter(args.iter().cloned().skip(1).collect::>()); 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 { 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 { 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 = 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 = 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 = ["FreeBSD".to_string()].iter().cloned().collect(); assert_eq!( correct_line("FreeBSD", &ignores), String::from("FreeBSD") ); } #[test] fn test_empty_lines() { let ignores: HashSet = HashSet::new(); assert_eq!(correct_line("", &ignores), String::from("")); } }