Skip to main content

turnframe_understand/
values.rs

1//! From an extraction to the operation's values: slicing words, evaluating dates and
2//! amounts, resolving handles. Every failure is structural and worded for a repair.
3
4use std::collections::BTreeMap;
5
6use turnframe_core::operation::{Money, ValueShape};
7use turnframe_core::understanding::{
8    ArgumentValue, Excerpt, MessageRef, RecordValue, UnderstoodArgument,
9};
10use turnframe_tasks::StructuralError;
11
12use crate::input::UnderstandingInput;
13use crate::tasks::extract::{BY_NAME, CURRENT, ExtractInput, Extraction, Given};
14use crate::tasks::{check_one_of, not_one_of, out_of_range};
15use crate::words::{Span, Words};
16
17/// An extraction turned into values.
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
19pub struct Extracted {
20    /// The arguments given, by name.
21    pub arguments: BTreeMap<String, UnderstoodArgument>,
22    /// The arguments not given, in declaration order.
23    pub not_given: Vec<String>,
24    /// Of those, the ones pointed at in words another part of the message holds, with
25    /// those words: that part's value, which this act may still find elsewhere.
26    pub elsewhere: Vec<(String, Span)>,
27}
28
29/// Turns `output` into values, or says what is structurally wrong with it.
30///
31/// # Errors
32///
33/// A [`StructuralError`] for a missing or unknown argument, a value of the wrong kind,
34/// a pointer outside its message, a date that does not exist or an amount that is not
35/// one.
36pub fn convert(
37    turn: &UnderstandingInput,
38    input: &ExtractInput<'_>,
39    output: &Extraction,
40) -> Result<Extracted, StructuralError> {
41    let extracted = converted(turn, input, output)?;
42    shares_no_words(input, &extracted)?;
43    Ok(extracted)
44}
45
46/// Whether `given` chooses one of the records offered: a listed one, or one another act of
47/// this message creates.
48fn chosen(input: &ExtractInput<'_>, given: &Given) -> bool {
49    let Given::Record { record, .. } = given else {
50        return false;
51    };
52    input
53        .record_choices
54        .values()
55        .flat_map(|choices| choices.iter())
56        .any(|choice| &choice.handle == record)
57}
58
59/// Whether `span` belongs to another part of the message: outside the request's own words
60/// and the words it continues, and not reached from them through words no part holds.
61fn elsewhere(input: &ExtractInput<'_>, span: Span) -> bool {
62    let within = |outer: Span| outer.from <= span.from && span.to <= outer.to;
63    if within(input.words) || input.continues.is_some_and(within) {
64        return false;
65    }
66    // A value of this part running on into a neighbour asking for the same operation is one
67    // value the segmentation cut in two.
68    let mut whole = input.words;
69    for kin in &input.kin {
70        if kin.to.saturating_add(1) == whole.from {
71            whole = Span::new(kin.from, whole.to);
72        } else if whole.to.saturating_add(1) == kin.from {
73            whole = Span::new(whole.from, kin.to);
74        }
75    }
76    if within(whole) && span.from <= input.words.to && input.words.from <= span.to {
77        return false;
78    }
79    let reach = Span::new(span.from.min(input.words.from), span.to.max(input.words.to));
80    input
81        .others
82        .iter()
83        .any(|other| other.from <= reach.to && reach.from <= other.to)
84}
85
86/// One word is one value: two arguments pointing at the same words of a message are two
87/// readings of one value, and one of them is wrong. A value that may be deduced points at
88/// the words that imply it, which may be those of the value they state.
89fn shares_no_words(input: &ExtractInput<'_>, extracted: &Extracted) -> Result<(), StructuralError> {
90    let deduced = |name: &str| {
91        input.arguments.iter().any(|argument| {
92            argument.name == name
93                && matches!(
94                    argument.source,
95                    turnframe_core::operation::ArgumentSource::Inferred
96                )
97        })
98    };
99    let pointed: Vec<(&String, &Excerpt)> = extracted
100        .arguments
101        .iter()
102        .filter_map(|(name, argument)| argument.excerpt.as_ref().map(|excerpt| (name, excerpt)))
103        .collect();
104    for (at, (name, excerpt)) in pointed.iter().enumerate() {
105        for (other, theirs) in &pointed[at + 1..] {
106            if deduced(name) != deduced(other) {
107                continue;
108            }
109            let shared = excerpt.message == theirs.message
110                && excerpt.words.first <= theirs.words.last
111                && theirs.words.first <= excerpt.words.last;
112            if shared {
113                return Err(StructuralError::new(
114                    "shared_words",
115                    format!(
116                        "`{name}` and `{other}` point at the same words: each value takes words \
117                         of its own, and a value the message only implies points at the words \
118                         that imply it"
119                    ),
120                ));
121            }
122        }
123    }
124    Ok(())
125}
126
127fn converted(
128    turn: &UnderstandingInput,
129    input: &ExtractInput<'_>,
130    output: &Extraction,
131) -> Result<Extracted, StructuralError> {
132    let asked: Vec<String> = input.arguments.iter().map(|a| a.name.clone()).collect();
133    if let Some(unknown) = output.arguments.keys().find(|name| !asked.contains(name)) {
134        return Err(not_one_of("argument", unknown, &asked));
135    }
136    let messages = input.messages(turn);
137    let mut extracted = Extracted::default();
138    for argument in &input.arguments {
139        let name = &argument.name;
140        let given = output.arguments.get(name).ok_or_else(|| {
141            StructuralError::new("missing_argument", format!("`arguments.{name}` is missing"))
142        })?;
143        let Some((message, span)) = given.pointer() else {
144            extracted.not_given.push(name.clone());
145            continue;
146        };
147        check_one_of(&format!("{name}.message"), message, &messages)?;
148        // Words this part of the message repeats are the value, wherever the reading pointed:
149        // what the user says now outranks an earlier copy that matching reads as the same.
150        let (message, span) = match given {
151            Given::Words { text, .. } if message != CURRENT && !text.trim().is_empty() => turn
152                .message
153                .narrow(input.words, text)
154                .map_or((message, span), |here| (CURRENT, here)),
155            _ => (message, span),
156        };
157        let (reference, words) = message_words(turn, message)
158            .ok_or_else(|| not_one_of(&format!("{name}.message"), message, &messages))?;
159        let span = match given {
160            Given::Words { text, .. } if !text.trim().is_empty() => {
161                narrow(name, words, span, text)?
162            }
163            _ => span,
164        };
165        let range = words
166            .range(span)
167            .map_err(|_| out_of_range(name, span, words))?;
168        // A value in another part of this message is that part's act's, not this one's; a
169        // record offered is chosen by its handle, and one named by its name, wherever its
170        // words are: a record is looked up, never copied.
171        let by_name = matches!(given, Given::Record { record, .. } if record == BY_NAME);
172        if message == CURRENT && elsewhere(input, span) && !chosen(input, given) && !by_name {
173            extracted.not_given.push(name.clone());
174            extracted.elsewhere.push((name.clone(), span));
175            continue;
176        }
177        let value = value_of(turn, input, name, &argument.shape, given, words, span)?;
178        if !matches!(
179            argument.source,
180            turnframe_core::operation::ArgumentSource::Inferred
181        ) {
182            says_its_number(name, given, words, span)?;
183        }
184        extracted.arguments.insert(
185            name.clone(),
186            UnderstoodArgument {
187                value,
188                excerpt: Some(Excerpt {
189                    message: reference,
190                    words: range,
191                }),
192            },
193        );
194    }
195    Ok(extracted)
196}
197
198/// A stated number is one its words say, when they hold any: «450» pointed at «500 euros» is
199/// a pointer at another number. Words that say it in letters are not read here.
200fn says_its_number(
201    name: &str,
202    given: &Given,
203    words: &Words,
204    span: Span,
205) -> Result<(), StructuralError> {
206    let stated = match given {
207        Given::Money { amount, .. } => amount.replace(',', ".").trim().parse::<f64>().ok(),
208        Given::Value { value, .. } => value.as_f64(),
209        _ => None,
210    };
211    let (Some(stated), Ok(said)) = (stated, words.slice(span)) else {
212        return Ok(());
213    };
214    let numbers = numbers_in(said);
215    if numbers.is_empty() || numbers.iter().any(|n| (n - stated).abs() < 1e-9) {
216        return Ok(());
217    }
218    let (from, to) = span.shown();
219    Err(StructuralError::new(
220        "number_not_in_words",
221        format!(
222            "`arguments.{name}` is {stated}, which words {from} to {to} («{said}») do not say; \
223             point at the words that say it"
224        ),
225    ))
226}
227
228/// Every value the numbers written in digits in `text` may mean, reading a lone comma or
229/// full stop as decimal or thousands mark, and the last of two kinds as the decimal one.
230fn numbers_in(text: &str) -> Vec<f64> {
231    let mut values = Vec::new();
232    let tokens = text
233        .split(|c: char| !(c.is_ascii_digit() || c == ',' || c == '.'))
234        .map(|token| token.trim_matches(|c: char| c == ',' || c == '.'))
235        .filter(|token| token.starts_with(|c: char| c.is_ascii_digit()));
236    for token in tokens {
237        let commas = token.contains(',');
238        let stops = token.contains('.');
239        let readings: Vec<String> = match (commas, stops) {
240            (false, false) => vec![token.to_owned()],
241            (true, false) => vec![token.replace(',', "."), token.replace(',', "")],
242            (false, true) => vec![token.to_owned(), token.replace('.', "")],
243            (true, true) => {
244                let last = token.rfind([',', '.']).unwrap_or(0);
245                let (whole, part) = token.split_at(last);
246                vec![format!("{}.{}", whole.replace([',', '.'], ""), &part[1..])]
247            }
248        };
249        values.extend(
250            readings
251                .iter()
252                .filter_map(|reading| reading.parse::<f64>().ok()),
253        );
254    }
255    values
256}
257
258/// The words of `span` that `copied` repeats: the value alone, without the words that
259/// name its field or join it to the rest. A copy the words pointed at do not hold is where
260/// the message holds it, when it holds it once.
261fn narrow(name: &str, words: &Words, span: Span, copied: &str) -> Result<Span, StructuralError> {
262    let pointed = words
263        .slice(span)
264        .map_err(|_| out_of_range(name, span, words))?;
265    words
266        .narrow(span, copied)
267        .or_else(|| words.only_place(copied))
268        .ok_or_else(|| {
269        let (from, to) = span.shown();
270        StructuralError::new(
271            "text_not_pointed_at",
272            format!(
273                "`arguments.{name}.text` is «{copied}», which is not among words {from} to {to} \
274                 («{pointed}»); copy the value's words exactly, and point at the words that hold them"
275            ),
276        )
277    })
278}
279
280fn message_words<'a>(turn: &'a UnderstandingInput, name: &str) -> Option<(MessageRef, &'a Words)> {
281    if name == CURRENT {
282        return Some((MessageRef::Current, &turn.message));
283    }
284    let index = name
285        .strip_prefix('m')?
286        .parse::<usize>()
287        .ok()?
288        .checked_sub(1)?;
289    let message = turn.transcript.get(index)?;
290    Some((MessageRef::Earlier { index }, &message.words))
291}
292
293fn wrong_kind(name: &str, shape: &ValueShape) -> StructuralError {
294    let expected = match shape {
295        ValueShape::Text { written: false } => "words",
296        ValueShape::Text { written: true } => "written",
297        ValueShape::Date { .. } => "date",
298        ValueShape::Money => "money",
299        ValueShape::Record { .. } => "record",
300        _ => "value",
301    };
302    StructuralError::new(
303        "wrong_kind",
304        format!("`arguments.{name}` takes kind {expected} or not_given"),
305    )
306}
307
308/// `text` without one pair of quotes around the whole of it.
309fn unquoted(text: &str) -> &str {
310    const PAIRS: [(char, char); 5] = [('"', '"'), ('\'', '\''), ('«', '»'), ('“', '”'), ('‘', '’')];
311    PAIRS
312        .iter()
313        .find_map(|(open, close)| {
314            let inner = text.strip_prefix(*open)?.strip_suffix(*close)?;
315            (!inner.trim().is_empty()).then(|| inner.trim())
316        })
317        .unwrap_or(text)
318}
319
320fn value_of(
321    turn: &UnderstandingInput,
322    input: &ExtractInput<'_>,
323    name: &str,
324    shape: &ValueShape,
325    given: &Given,
326    words: &Words,
327    span: Span,
328) -> Result<ArgumentValue, StructuralError> {
329    let json = ArgumentValue::Json;
330    match (shape, given) {
331        (ValueShape::Text { written: false }, Given::Words { text: copied, .. }) => {
332            let text = words
333                .slice(span)
334                .map_err(|_| out_of_range(name, span, words))?;
335            // The punctuation that joins a value to the next words is not the value, nor
336            // are the quotes that mark it off, nor the mark ending a sentence the copy drops;
337            // a mark the joining punctuation follows ends no sentence.
338            let joined = text.trim_end_matches([',', ';', ':']);
339            let ends_sentence = joined.len() == text.len();
340            let mut text = joined;
341            let ends = ['.', '!', '?'];
342            if ends_sentence && !copied.trim().is_empty() && !copied.trim_end().ends_with(ends) {
343                text = text.trim_end_matches(ends);
344            }
345            let text = unquoted(text);
346            Ok(json(serde_json::Value::from(text)))
347        }
348        (ValueShape::Text { written: true }, Given::Written { text, .. }) => {
349            if text.trim().is_empty() {
350                return Err(StructuralError::new(
351                    "empty_text",
352                    format!("`arguments.{name}.text` is empty; give not_given instead"),
353                ));
354            }
355            Ok(json(serde_json::Value::from(text.trim())))
356        }
357        (ValueShape::Enum { values }, Given::Value { value, .. }) => {
358            let text = value.as_str().unwrap_or_default();
359            check_one_of(&format!("{name}.value"), text, values)?;
360            Ok(json(value.clone()))
361        }
362        (ValueShape::Integer, Given::Value { value, .. }) if value.is_i64() || value.is_u64() => {
363            Ok(json(value.clone()))
364        }
365        (ValueShape::Number, Given::Value { value, .. }) if value.is_number() => {
366            Ok(json(value.clone()))
367        }
368        (ValueShape::Bool, Given::Value { value, .. }) if value.is_boolean() => {
369            Ok(json(value.clone()))
370        }
371        (ValueShape::Structured, Given::Value { value, .. }) => Ok(json(value.clone())),
372        (ValueShape::Date { direction }, Given::Date { date, .. }) => {
373            let day = date.evaluate(turn.today, *direction).map_err(|error| {
374                StructuralError::new(
375                    "no_such_date",
376                    format!("`arguments.{name}.date`: {error}; give the date the user said"),
377                )
378            })?;
379            Ok(json(serde_json::Value::from(day.to_string())))
380        }
381        (
382            ValueShape::Money,
383            Given::Money {
384                amount, currency, ..
385            },
386        ) => {
387            let money = Money::parse(amount, currency).map_err(|error| {
388                StructuralError::new("not_money", format!("`arguments.{name}`: {error}"))
389            })?;
390            Ok(json(serde_json::to_value(money).unwrap_or_default()))
391        }
392        (
393            ValueShape::Record { workflow },
394            Given::Record {
395                record,
396                name: named,
397                ..
398            },
399        ) if record == BY_NAME => {
400            if named.trim().is_empty() {
401                return Err(StructuralError::new(
402                    "missing_name",
403                    format!("`arguments.{name}.name` must be the record's name when it is by_name"),
404                ));
405            }
406            // A name that is the whole name of one record in view is that record.
407            let words = |text: &str| {
408                text.split_whitespace()
409                    .map(str::to_lowercase)
410                    .collect::<Vec<_>>()
411            };
412            let listed: Vec<&RecordValue> = input
413                .record_choices
414                .get(name)
415                .into_iter()
416                .flatten()
417                .map(|choice| &choice.value)
418                .filter(|value| match value {
419                    RecordValue::Record { token } => turn
420                        .record(token)
421                        .is_some_and(|(_, record)| words(&record.label) == words(named)),
422                    _ => false,
423                })
424                .collect();
425            if let [record] = listed.as_slice() {
426                return Ok(ArgumentValue::Record((*record).clone()));
427            }
428            Ok(ArgumentValue::Record(RecordValue::Named {
429                workflow: workflow.clone(),
430                named: named.trim().to_owned(),
431            }))
432        }
433        (ValueShape::Record { .. }, Given::Record { record, .. }) => {
434            let choices = input
435                .record_choices
436                .get(name)
437                .map(Vec::as_slice)
438                .unwrap_or(&[]);
439            let handles: Vec<String> = choices.iter().map(|c| c.handle.clone()).collect();
440            let choice = choices
441                .iter()
442                .find(|choice| &choice.handle == record)
443                .ok_or_else(|| not_one_of(&format!("{name}.record"), record, &handles))?;
444            Ok(ArgumentValue::Record(choice.value.clone()))
445        }
446        _ => Err(wrong_kind(name, shape)),
447    }
448}