aloud.rsannotatedaloud.rssource420 lines · 18.7 KB · raw
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}