1//! A remembered fact as the child hears and reads it. 2//! 3//! The model files a memory as a third-person statement about her ("She watches a show with a blue-haired 4//! character"), and a card that reads that out to her says it as if she were not there. Whiskers is talking to 5//! her, so a fact about her is said to her: "You watch a show with a blue-haired character". 6//! 7//! **The rule.** A clause (a sentence, or a part of one after a `;`) is about the child when it begins with 8//! the child, as `she`, `he`, `they`, her own name or "the child". Such a clause is rewritten in the second person 9//! when that can be done with certainty: 10//! 11//! - the subject becomes `you` and the first verb agrees (`watches` becomes `watch`, `is` becomes `are`, 12//! `doesn't` becomes `don't`; a past tense, a modal or a plural `they` verb already agrees); 13//! - `her`, `his`, `their` in the clause become `your`, `him`, `them` become `you`, and the reflexives 14//! `yourself`; 15//! - a clause that begins with her possessive (`Her`, `His`, `Their`, her name with `'s`) becomes `Your ...` 16//! and nothing else in it needs to change; 17//! - `is`/`are`/`was`/`were` followed by `her`, `his` or `their` is `your` anywhere ("Biscuit is her toy bunny" 18//! becomes "Biscuit is your toy bunny"). 19//! 20//! **When it cannot be done with certainty it is not done.** A second `and`-joined verb that might be a plural 21//! noun ("likes cats and dogs"), a contraction that could be `is` or `has`, a second pronoun that may be someone 22//! else, or another named person beside a pronoun ("plays with Nana and her cat") makes the clause uncertain. 23//! An uncertain clause that begins `she` or `he` and has a name to use says that name instead ("Ada likes cats and 24//! dogs", still correct for a third person); any other uncertain clause is read as it was filed. Saying something 25//! false to a child ("Nana says you love gardening") is worse than saying it a little stiffly. A fact with no 26//! pronoun about her ("Biscuit is a bunny") is not touched. 27//! 28//! The name comes from the profile and no pronoun for the child is chosen here: the words `she` and `he` are only 29//! ever read, as what the model wrote. The child's age does not change the rule; a fixed phrase is easiest to hear 30//! at every age Whiskers is made for. 31 32/// Words that may stand between the subject and its verb. Those that do not end in `s` need no listing (only a 33/// word ending in `s` is ever treated as a present-tense verb). 34const ADVERBS: &[&str] = &["always", "sometimes", "usually", "often", "also", "really", "just", "still", "even", "never", "only"]; 35 36/// Words after `her` that make it the object ("gives her a hug", "lets her go") and not the possessive. 37const OBJECT_ENDERS: &[&str] = &[ 38 "a", "an", "the", "to", "with", "for", "at", "in", "on", "of", "and", "or", "but", "when", "if", "that", "about", "from", "by", "every", "some", "so", "too", 39]; 40 41/// `text` as it is said to the child whose name is `name` (if the profile has one). 42pub(crate) fn to_the_child(text: &str, name: Option<&str>) -> String { 43 let mut out = String::with_capacity(text.len()); 44 let mut clause = String::new(); 45 let chars: Vec<(usize, char)> = text.char_indices().collect(); 46 let mut i = 0; 47 let mut starts = true; 48 while i < chars.len() { 49 let (at, c) = chars[i]; 50 clause.push_str(&text[at..at + c.len_utf8()]); 51 i += 1; 52 if matches!(c, '.' | '!' | '?' | ';') { 53 // A clause ends at a stop followed by a space (or the end), taking any more stops with it. 54 while i < chars.len() && matches!(chars[i].1, '.' | '!' | '?' | ';' | '"' | '\u{201d}') { 55 clause.push(chars[i].1); 56 i += 1; 57 } 58 if i == chars.len() || chars[i].1.is_whitespace() { 59 while i < chars.len() && chars[i].1.is_whitespace() { 60 clause.push(chars[i].1); 61 i += 1; 62 } 63 out.push_str(&clause_to_the_child(&clause, name, starts)); 64 starts = !clause.trim_end().trim_end_matches(['"', '\u{201d}']).ends_with(';'); 65 clause.clear(); 66 } 67 } 68 } 69 out.push_str(&clause_to_the_child(&clause, name, starts)); 70 out 71} 72 73/// One word of a clause with the marks around it: `("(", "show", ".")`. 74struct Word { 75 before: String, 76 core: String, 77 after: String, 78 /// The spacing that followed the word. 79 space: String, 80} 81 82impl Word { 83 /// The word compared: lower case, with a curly apostrophe made straight. 84 fn key(&self) -> String { 85 self.core.to_lowercase().replace('\u{2019}', "'") 86 } 87 88 fn capitalized(&self) -> bool { 89 self.core.chars().next().is_some_and(char::is_uppercase) 90 } 91 92 /// Replaces the word's letters, with a capital where `upper`. 93 fn put(&mut self, new: &str, upper: bool) { 94 self.core = if upper { capitalize(new) } else { new.to_owned() }; 95 } 96} 97 98fn capitalize(s: &str) -> String { 99 let mut c = s.chars(); 100 c.next().map_or_else(String::new, |f| f.to_uppercase().chain(c).collect()) 101} 102 103fn words_of(clause: &str) -> (String, Vec<Word>) { 104 let lead: String = clause.chars().take_while(|c| c.is_whitespace()).collect(); 105 let rest = &clause[lead.len()..]; 106 let mut words = Vec::new(); 107 let mut at = 0; 108 while at < rest.len() { 109 let end = rest[at..].find(char::is_whitespace).map_or(rest.len(), |n| at + n); 110 let gap_end = rest[end..].find(|c: char| !c.is_whitespace()).map_or(rest.len(), |n| end + n); 111 let token = &rest[at..end]; 112 let is_core = |c: char| c.is_alphanumeric() || matches!(c, '\'' | '\u{2019}' | '-'); 113 let start = token.find(is_core).unwrap_or(token.len()); 114 let stop = token.rfind(is_core).map_or(start, |n| n + token[n..].chars().next().map_or(1, char::len_utf8)); 115 words.push(Word { before: token[..start].to_owned(), core: token[start..stop].to_owned(), after: token[stop..].to_owned(), space: rest[end..gap_end].to_owned() }); 116 at = gap_end; 117 } 118 (lead, words) 119} 120 121fn join(lead: &str, words: &[Word]) -> String { 122 let mut s = lead.to_owned(); 123 for w in words { 124 s.push_str(&w.before); 125 s.push_str(&w.core); 126 s.push_str(&w.after); 127 s.push_str(&w.space); 128 } 129 s 130} 131 132fn clause_to_the_child(clause: &str, name: Option<&str>, starts_sentence: bool) -> String { 133 let (lead, mut words) = words_of(clause); 134 if words.is_empty() { 135 return clause.to_owned(); 136 } 137 let name_key = name.map(|n| n.to_lowercase().replace('\u{2019}', "'")); 138 let first = words[0].key(); 139 let is_name = |k: &str| name_key.as_deref() == Some(k); 140 141 // A clause that begins with what she owns needs only its first word changed. 142 let possessive = matches!(first.as_str(), "her" | "his" | "their") || name_key.as_deref().is_some_and(|n| first == format!("{n}'s")); 143 if possessive && words[0].after.is_empty() { 144 words[0].put("your", starts_sentence); 145 be_your(&mut words); 146 return join(&lead, &words); 147 } 148 149 let singular_pronoun = matches!(first.as_str(), "she" | "he" | "she's" | "he's"); 150 let subject = singular_pronoun || first == "they" || is_name(&first); 151 if !subject || !words[0].after.is_empty() || words.len() < 2 { 152 be_your(&mut words); 153 return join(&lead, &words); 154 } 155 156 // `Ada and Omar`, `she, however`: not a plain subject followed by its verb. 157 let next = words[1].key(); 158 if matches!(next.as_str(), "and" | "or" | "but" | "nor") { 159 be_your(&mut words); 160 return join(&lead, &words); 161 } 162 163 match second_person(&words, singular_pronoun || is_name(&first), name_key.as_deref(), starts_sentence) { 164 Some(done) => join(&lead, &done), 165 None => { 166 // Not certain: the name for a `she` or a `he` (the verb already agrees with it), else as filed. 167 if matches!(first.as_str(), "she" | "he") && let Some(n) = name { 168 words[0].core = n.to_owned(); 169 } 170 be_your(&mut words); 171 join(&lead, &words) 172 } 173 } 174} 175 176/// `be` followed by her/his/their: the child's. ("Biscuit is her toy bunny.") 177fn be_your(words: &mut [Word]) { 178 for i in 1..words.len() { 179 if matches!(words[i - 1].key().as_str(), "is" | "are" | "was" | "were") && words[i - 1].after.is_empty() && matches!(words[i].key().as_str(), "her" | "his" | "their") { 180 words[i].put("your", false); 181 } 182 } 183} 184 185/// The clause said to her, or `None` when that cannot be done with certainty. 186fn second_person(words: &[Word], singular: bool, name: Option<&str>, starts_sentence: bool) -> Option<Vec<Word>> { 187 let mut out: Vec<Word> = words.iter().map(|w| Word { before: w.before.clone(), core: w.core.clone(), after: w.after.clone(), space: w.space.clone() }).collect(); 188 let first = words[0].key(); 189 // `she's learning` is `you're learning`; `she's got` could be `has` or `is`, so it is not guessed. 190 if matches!(first.as_str(), "she's" | "he's") { 191 if !words[1].key().ends_with("ing") { 192 return None; 193 } 194 out[0].put("you're", starts_sentence); 195 return rest_of_clause(words, out, 1, singular, name); 196 } 197 out[0].put("you", starts_sentence); 198 199 // The verb: after any one adverb. 200 let mut v = 1; 201 if ADVERBS.contains(&words[v].key().as_str()) { 202 v += 1; 203 } 204 let Some(verb) = words.get(v) else { return Some(out) }; 205 if singular && let Some(agreed) = agree(&verb.key()) { 206 out[v].put(&agreed, false); 207 } 208 rest_of_clause(words, out, v + 1, singular, name) 209} 210 211/// The words of the clause from `from` on: pronouns about her are turned, and anything that might be someone else 212/// makes the whole clause uncertain (`None`). 213fn rest_of_clause(words: &[Word], mut out: Vec<Word>, from: usize, singular: bool, name: Option<&str>) -> Option<Vec<Word>> { 214 let mut turned = false; 215 let mut other_named = false; 216 for i in from..words.len() { 217 let k = words[i].key(); 218 // Her own name again, later in the clause: hers, or (as a bare name) not certain. 219 if let Some(n) = name { 220 if k == n { 221 return None; 222 } 223 if k == format!("{n}'s") { 224 out[i].put("your", false); 225 turned = true; 226 continue; 227 } 228 } 229 match k.as_str() { 230 "she" | "he" | "they" | "she's" | "he's" | "they're" => return None, 231 "her" => { 232 let object = words[i].after.is_empty().then(|| words.get(i + 1).map(|n| OBJECT_ENDERS.contains(&n.key().as_str()))).flatten().unwrap_or(true); 233 out[i].put(if object { "you" } else { "your" }, false); 234 turned = true; 235 } 236 "his" | "their" => { 237 out[i].put("your", false); 238 turned = true; 239 } 240 "him" | "them" => { 241 out[i].put("you", false); 242 turned = true; 243 } 244 "herself" | "himself" | "themselves" | "themself" => { 245 out[i].put("yourself", false); 246 turned = true; 247 } 248 "and" | "or" | "but" => { 249 // A second verb after the conjunction might be a verb that needs agreeing, or a plural noun. 250 if let Some(n) = words.get(i + 1) { 251 let nk = n.key(); 252 if singular && nk.len() > 2 && nk.ends_with('s') && !nk.ends_with("ss") && !matches!(nk.as_str(), "his" | "this" | "its" | "us") { 253 return None; 254 } 255 } 256 } 257 _ => { 258 if words[i].capitalized() && i > 0 { 259 other_named = true; 260 } 261 } 262 } 263 // A comma before a word that might be another verb: the same doubt as after `and`. 264 if words[i].after.starts_with(',') && singular { 265 if let Some(n) = words.get(i + 1) { 266 let nk = n.key(); 267 if nk.len() > 2 && nk.ends_with('s') && !nk.ends_with("ss") && !matches!(nk.as_str(), "his" | "this" | "its" | "us") { 268 return None; 269 } 270 } 271 } 272 } 273 if turned && other_named { 274 return None; 275 } 276 Some(out) 277} 278 279/// The base form of a present-tense verb in the third person singular, or `None` when the word is not one (a past 280/// tense, a modal or a base form needs no change). 281fn agree(verb: &str) -> Option<String> { 282 let irregular = match verb { 283 "is" => "are", 284 "was" => "were", 285 "has" => "have", 286 "does" => "do", 287 "goes" => "go", 288 "doesn't" => "don't", 289 "isn't" => "aren't", 290 "hasn't" => "haven't", 291 "wasn't" => "weren't", 292 _ => "", 293 }; 294 if !irregular.is_empty() { 295 return Some(irregular.to_owned()); 296 } 297 if !verb.ends_with('s') || verb.ends_with("ss") || verb.ends_with("us") || verb.ends_with("is") || verb.len() < 3 { 298 return None; 299 } 300 let stem = &verb[..verb.len() - 1]; 301 if let Some(y) = verb.strip_suffix("ies").filter(|s| s.len() >= 2) { 302 return Some(format!("{y}y")); 303 } 304 for e in ["sses", "shes", "ches", "xes", "zes"] { 305 if verb.ends_with(e) { 306 return Some(verb[..verb.len() - 2].to_owned()); 307 } 308 } 309 Some(stem.to_owned()) 310} 311 312#[cfg(test)] 313mod tests { 314 use super::*; 315 316 fn ada(t: &str) -> String { 317 to_the_child(t, Some("Ada")) 318 } 319 320 fn nobody(t: &str) -> String { 321 to_the_child(t, None) 322 } 323 324 #[test] 325 fn she_becomes_you_and_the_verb_agrees() { 326 assert_eq!( 327 ada("She watches a show with a blue-haired character and colorful cartoon fish"), 328 "You watch a show with a blue-haired character and colorful cartoon fish" 329 ); 330 assert_eq!(nobody("She watches a show with a blue-haired character"), "You watch a show with a blue-haired character"); 331 assert_eq!(ada("She is six."), "You are six."); 332 assert_eq!(ada("She has a cat called Bean."), "You have a cat called Bean."); 333 assert_eq!(ada("She doesn't like peas."), "You don't like peas."); 334 assert_eq!(ada("She always eats toast."), "You always eat toast."); 335 assert_eq!(ada("She studies at home."), "You study at home."); 336 assert_eq!(ada("She kisses the dog."), "You kiss the dog."); 337 assert_eq!(ada("She goes to the park."), "You go to the park."); 338 } 339 340 #[test] 341 fn he_and_they_are_the_same_and_a_plural_verb_is_left_alone() { 342 assert_eq!(ada("He plays with his rocket."), "You play with your rocket."); 343 assert_eq!(ada("They have a dog called Rex."), "You have a dog called Rex."); 344 assert_eq!(ada("They are learning to swim."), "You are learning to swim."); 345 assert_eq!(ada("They give their bunny a hug."), "You give your bunny a hug."); 346 } 347 348 #[test] 349 fn a_past_tense_or_a_modal_already_agrees() { 350 assert_eq!(ada("She went to the park."), "You went to the park."); 351 assert_eq!(ada("She can ride a bicycle."), "You can ride a bicycle."); 352 assert_eq!(ada("She was at Nana's."), "You were at Nana's."); 353 } 354 355 #[test] 356 fn what_she_owns_becomes_yours() { 357 assert_eq!(ada("Her favourite colour is blue."), "Your favourite colour is blue."); 358 assert_eq!(ada("His dog is called Rex."), "Your dog is called Rex."); 359 assert_eq!(ada("Biscuit is her toy bunny, brown with long ears"), "Biscuit is your toy bunny, brown with long ears"); 360 assert_eq!(nobody("Biscuit is her toy bunny"), "Biscuit is your toy bunny"); 361 assert_eq!(ada("She gives her bunny a hug."), "You give your bunny a hug."); 362 assert_eq!(ada("She likes it when we read to her."), "You like it when we read to you."); 363 } 364 365 #[test] 366 fn a_text_that_begins_with_her_own_name_is_said_to_her() { 367 assert_eq!(ada("Ada watches a show."), "You watch a show."); 368 assert_eq!(ada("ada has a brother."), "You have a brother."); 369 assert_eq!(ada("Ada's best friend is Omar."), "Your best friend is Omar."); 370 assert_eq!(ada("Ada\u{2019}s best friend is Omar."), "Your best friend is Omar."); 371 assert_eq!(ada("Ada sings the rainbow song"), "You sing the rainbow song"); 372 assert_eq!(nobody("Ada sings the rainbow song"), "Ada sings the rainbow song", "with no profile there is no name to know"); 373 } 374 375 #[test] 376 fn each_clause_is_judged_alone() { 377 assert_eq!(ada("She has a cat. She names it Bean."), "You have a cat. You name it Bean."); 378 assert_eq!(ada("Ada likes tea; she hates milk."), "You like tea; you hate milk."); 379 assert_eq!(ada("Nana makes pancakes. She likes the blueberries."), "Nana makes pancakes. You like the blueberries."); 380 } 381 382 #[test] 383 fn a_text_with_no_pronoun_about_her_is_not_touched() { 384 for t in ["Biscuit is a bunny", "Nana makes pancakes on Sundays", "Omar likes rockets.", "The children play", "Nana says she loves gardening.", ""] { 385 assert_eq!(ada(t), t, "{t:?}"); 386 assert_eq!(nobody(t), t, "{t:?}"); 387 } 388 } 389 390 #[test] 391 fn what_cannot_be_said_with_certainty_is_the_name_or_as_filed() { 392 // A plural noun or a verb after `and`: not guessed. 393 assert_eq!(ada("She likes cats and dogs."), "Ada likes cats and dogs."); 394 assert_eq!(nobody("She likes cats and dogs."), "She likes cats and dogs."); 395 assert_eq!(ada("She sings and dances."), "Ada sings and dances."); 396 // Another person beside a pronoun may be who the pronoun means. 397 assert_eq!(ada("She plays with Nana and her cat."), "Ada plays with Nana and her cat."); 398 assert_eq!(ada("She studies at home and tries hard."), "Ada studies at home and tries hard."); 399 assert_eq!(ada("She likes it when Nana reads to her."), "Ada likes it when Nana reads to her."); 400 // A contraction could be `is` or `has`: only `-ing` after it is certain. 401 assert_eq!(ada("She's got a bunny."), "She's got a bunny."); 402 assert_eq!(ada("She's learning to ride."), "You're learning to ride."); 403 // Two people are not one child. 404 assert_eq!(ada("She and Nana bake."), "She and Nana bake."); 405 assert_eq!(ada("Ada and Omar play."), "Ada and Omar play."); 406 // A second pronoun may be somebody else. 407 assert_eq!(ada("She says he is tall."), "Ada says he is tall."); 408 // `they` does not take a name: the verb would not agree with it. 409 assert_eq!(ada("They like cats and dogs."), "You like cats and dogs."); 410 } 411 412 #[test] 413 fn punctuation_and_case_survive() { 414 assert_eq!(ada("\"She watches TV!\" Then it rained."), "\"You watch TV!\" Then it rained."); 415 assert_eq!(ada("She watches TV, and eats snacks."), "Ada watches TV, and eats snacks."); 416 assert_eq!(ada("She\u{2019}s learning."), "You're learning."); 417 assert_eq!(ada("She"), "She", "a lone pronoun has no verb to agree"); 418 assert_eq!(ada(" She watches TV."), " You watch TV."); 419 } 420}