aloud.rsannotatedaloud.rssource420 lines · 18.7 KB · raw

A remembered fact as the child hears and reads it.

The model files a memory as a third-person statement about her ("She watches a show with a blue-haired character"), and a card that reads that out to her says it as if she were not there. Whiskers is talking to her, so a fact about her is said to her: "You watch a show with a blue-haired character".

The rule. A clause (a sentence, or a part of one after a ;) is about the child when it begins with the child, as she, he, they, her own name or "the child". Such a clause is rewritten in the second person when that can be done with certainty:

  • the subject becomes you and the first verb agrees (watches becomes watch, is becomes are, doesn't becomes don't; a past tense, a modal or a plural they verb already agrees);
  • her, his, their in the clause become your, him, them become you, and the reflexives yourself;
  • a clause that begins with her possessive (Her, His, Their, her name with 's) becomes Your ... and nothing else in it needs to change;
  • is/are/was/were followed by her, his or their is your anywhere ("Biscuit is her toy bunny" becomes "Biscuit is your toy bunny").

When it cannot be done with certainty it is not done. A second and-joined verb that might be a plural noun ("likes cats and dogs"), a contraction that could be is or has, a second pronoun that may be someone else, or another named person beside a pronoun ("plays with Nana and her cat") makes the clause uncertain. An uncertain clause that begins she or he and has a name to use says that name instead ("Ada likes cats and dogs", still correct for a third person); any other uncertain clause is read as it was filed. Saying something false to a child ("Nana says you love gardening") is worse than saying it a little stiffly. A fact with no pronoun about her ("Biscuit is a bunny") is not touched.

The name comes from the profile and no pronoun for the child is chosen here: the words she and he are only ever read, as what the model wrote. The child's age does not change the rule; a fixed phrase is easiest to hear at every age Whiskers is made for.

Words that may stand between the subject and its verb. Those that do not end in s need no listing (only a 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"];

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];

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}

One word of a clause with the marks around it: ("(", "show", ".").

74struct Word {
75    before: String,
76    core: String,
77    after: String,

The spacing that followed the word.

79    space: String,
80}
82impl Word {

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    }
88    fn capitalized(&self) -> bool {
89        self.core.chars().next().is_some_and(char::is_uppercase)
90    }

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}
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}

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}

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}

The words of the clause from from on: pronouns about her are turned, and anything that might be someone else 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}

The base form of a present-tense verb in the third person singular, or None when the word is not one (a past 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}
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}