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. 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() {
let mut args = args();
args.next(); // ignore self arg
let mut ignorables = Vec::new();
while let Some(arg) = args.next() {
dbg!(&arg);
match arg.as_str() {
"--ignore" | "-i" => {
ignorables = args.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));
}
}
/// Capitalizes a single line (title) based on placement and exceptions.
fn correct_line(line: &str, ignorables: &[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: &[String]) -> String {
if ignorables.iter().any(|s| s == 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() {
assert_eq!(correct_word("Like", false, &[]), String::from("like"));
assert_eq!(correct_word("like", false, &[]), String::from("like"));
assert_eq!(correct_word("like", true, &[]), String::from("Like"));
assert_eq!(correct_word("liKe", false, &[]), String::from("like"));
assert_eq!(
correct_word("computers", false, &[]),
String::from("Computers")
);
}
#[test]
fn test_correct_line() {
assert_eq!(
correct_line("this is a test", &[]),
String::from("This Is a Test")
);
assert_eq!(
correct_line("similEs: lIke Or As", &[]),
String::from("Similes: like or As")
);
}
#[test]
fn test_proper_nouns_acronyms() {
assert_eq!(
correct_line("FreeBSD", &[String::from("FreeBSD")]),
String::from("FreeBSD")
);
}
#[test]
fn test_empty_lines() {
assert_eq!(correct_line("", &[]), String::from(""));
}
}