Skip to main content

lemma/
result_value.rs

1//! API value for a rule result, `ShowData.prefilled`, or `ShowData.suggestion`.
2//!
3//! This is the single API-facing value representation shared by all three sites, expanded
4//! into every declared unit for measure/ratio. It sits at the crate root (not under
5//! `evaluation/`) because `planning::execution_plan::ShowData` needs it too, and planning
6//! must not import evaluation. The plan/eval-internal representation is the canonical
7//! `planning::semantics::LiteralValue`; this module is the boundary between the two.
8
9use crate::computation::rational::{checked_div, checked_mul, NumericFailure};
10use crate::literals::rational_from_parsed_decimal;
11use crate::planning::semantics::{
12    range_element_type_specification, semantic_calendar_unit_from_measure_signature, LemmaType,
13    LiteralUnitMapFailure, LiteralValue, SemanticDateTime, SemanticTime, TypeSpecification,
14    ValueKind,
15};
16use serde::{Deserialize, Serialize};
17use std::collections::BTreeMap;
18use std::fmt;
19use std::sync::Arc;
20
21/// Calendar value (a measure whose unit is a calendar unit, e.g. `3 months`) on a result.
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23pub struct CalendarResult {
24    pub value: String,
25    pub unit: String,
26}
27
28/// Both endpoints of a range result.
29///
30/// Each endpoint is itself a [`RuleResultValue`], but an endpoint's own `range` field is
31/// always `None` — a range endpoint must never itself be a range. Building a
32/// [`RuleResultValue`] and reconstructing a literal both panic if this invariant is
33/// violated.
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35pub struct RangeResult {
36    pub from: RuleResultValue,
37    pub to: RuleResultValue,
38}
39
40/// API value shared by flattened [`crate::evaluation::response::RuleResult`],
41/// `ShowData.prefilled`, and `ShowData.suggestion`.
42///
43/// When present: always `display` (from [`LiteralValue::display_value`]), plus exactly
44/// one typed field for a non-range value; `range` is set instead for a range value, and
45/// every other typed field stays `None`.
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
47pub struct RuleResultValue {
48    /// Engine-rendered string for UI (`LiteralValue::display_value`). Present whenever
49    /// this value is present, including range endpoints.
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub display: Option<String>,
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub measure: Option<BTreeMap<String, String>>,
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub ratio: Option<BTreeMap<String, String>>,
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub number: Option<String>,
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub boolean: Option<bool>,
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub text: Option<String>,
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub date: Option<SemanticDateTime>,
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub time: Option<SemanticTime>,
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub calendar: Option<CalendarResult>,
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub range: Option<Box<RangeResult>>,
70}
71
72/// Why building a [`RuleResultValue`] from a canonical [`LiteralValue`] failed.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum RuleResultValueFailure {
75    DecimalLimit,
76    NumericOverflow,
77    OutOfMemory,
78}
79
80/// Human-readable veto message for a [`RuleResultValueFailure`].
81pub fn rule_result_value_failure_message(failure: RuleResultValueFailure) -> &'static str {
82    match failure {
83        RuleResultValueFailure::DecimalLimit => "Calculated result exceeds decimal value limit",
84        RuleResultValueFailure::NumericOverflow => "numeric overflow",
85        RuleResultValueFailure::OutOfMemory => "out of memory",
86    }
87}
88
89fn map_numeric_to_rule_result_value_failure(failure: NumericFailure) -> RuleResultValueFailure {
90    match failure {
91        NumericFailure::Overflow => RuleResultValueFailure::DecimalLimit,
92        NumericFailure::OutOfMemory => RuleResultValueFailure::OutOfMemory,
93        NumericFailure::DivisionByZero => {
94            panic!(
95                "BUG: decimal commit encountered division by zero while building RuleResultValue"
96            )
97        }
98        NumericFailure::Irrational => {
99            panic!(
100                "BUG: decimal commit encountered irrational result while building RuleResultValue"
101            )
102        }
103    }
104}
105
106fn map_unit_conversion_failure(failure: NumericFailure) -> RuleResultValueFailure {
107    match failure {
108        NumericFailure::Overflow => RuleResultValueFailure::NumericOverflow,
109        NumericFailure::OutOfMemory => RuleResultValueFailure::OutOfMemory,
110        NumericFailure::DivisionByZero => {
111            panic!(
112                "BUG: unit conversion encountered division by zero while building RuleResultValue"
113            )
114        }
115        NumericFailure::Irrational => {
116            panic!(
117                "BUG: unit conversion encountered irrational result while building RuleResultValue"
118            )
119        }
120    }
121}
122
123fn map_literal_unit_map_failure(failure: LiteralUnitMapFailure) -> RuleResultValueFailure {
124    match failure {
125        LiteralUnitMapFailure::Commit(nf) => map_numeric_to_rule_result_value_failure(nf),
126        LiteralUnitMapFailure::UnitConversion(nf) => map_unit_conversion_failure(nf),
127    }
128}
129
130fn measure_to_unit_map(
131    literal: &LiteralValue,
132    result_type: &LemmaType,
133) -> Result<BTreeMap<String, String>, RuleResultValueFailure> {
134    result_type
135        .measure_literal_in_all_units(literal)
136        .map_err(map_literal_unit_map_failure)
137}
138
139fn ratio_to_unit_map(
140    literal: &LiteralValue,
141    result_type: &LemmaType,
142) -> Result<BTreeMap<String, String>, RuleResultValueFailure> {
143    result_type
144        .ratio_literal_in_all_units(literal)
145        .map_err(map_literal_unit_map_failure)
146}
147
148fn element_type_from_range_rule(rule_type: &LemmaType) -> Option<LemmaType> {
149    range_element_type_specification(&rule_type.specifications).map(LemmaType::primitive)
150}
151
152/// A range endpoint uses its own declared type when it carries one
153/// (unit-scoped range endpoints), falling back to the range's element type otherwise.
154fn range_endpoint_type(endpoint: &LiteralValue, range_element_type: &LemmaType) -> LemmaType {
155    if endpoint.lemma_type.measure_unit_names().is_some() {
156        endpoint.lemma_type.as_ref().clone()
157    } else {
158        range_element_type.clone()
159    }
160}
161
162/// Build a [`RuleResultValue`] from a canonical [`LiteralValue`].
163///
164/// Measure and ratio expand into every unit declared on `rule_type`. A range value
165/// builds both endpoints; an endpoint that is itself a range is a planning bug
166/// (ranges do not nest) and panics rather than being silently flattened.
167pub fn rule_result_value_from_literal(
168    literal: &LiteralValue,
169    rule_type: &LemmaType,
170) -> Result<RuleResultValue, RuleResultValueFailure> {
171    match &literal.value {
172        ValueKind::Range(from, to) => {
173            let endpoint_type =
174                element_type_from_range_rule(rule_type).unwrap_or_else(|| rule_type.clone());
175            let from_type = range_endpoint_type(from, &endpoint_type);
176            let to_type = range_endpoint_type(to, &endpoint_type);
177            let from_value = rule_result_value_from_range_endpoint(from, &from_type)?;
178            let to_value = rule_result_value_from_range_endpoint(to, &to_type)?;
179            Ok(RuleResultValue {
180                display: Some(literal.display_value()),
181                range: Some(Box::new(RangeResult {
182                    from: from_value,
183                    to: to_value,
184                })),
185                ..RuleResultValue::default()
186            })
187        }
188        _ => rule_result_value_from_non_range_literal(literal, rule_type),
189    }
190}
191
192fn rule_result_value_from_range_endpoint(
193    endpoint: &LiteralValue,
194    endpoint_type: &LemmaType,
195) -> Result<RuleResultValue, RuleResultValueFailure> {
196    if matches!(&endpoint.value, ValueKind::Range(_, _)) {
197        panic!("BUG: range endpoint must not itself be a range");
198    }
199    rule_result_value_from_non_range_literal(endpoint, endpoint_type)
200}
201
202fn rule_result_value_from_non_range_literal(
203    literal: &LiteralValue,
204    result_type: &LemmaType,
205) -> Result<RuleResultValue, RuleResultValueFailure> {
206    let display = Some(literal.display_value());
207    match &literal.value {
208        ValueKind::Measure(rational, sig) if literal.lemma_type.is_calendar_like() => {
209            let unit = semantic_calendar_unit_from_measure_signature(sig);
210            let value = literal
211                .lemma_type
212                .try_rational_as_decimal_string(rational)
213                .map_err(map_numeric_to_rule_result_value_failure)?;
214            Ok(RuleResultValue {
215                display,
216                calendar: Some(CalendarResult {
217                    value,
218                    unit: unit.to_string(),
219                }),
220                ..RuleResultValue::default()
221            })
222        }
223        ValueKind::Measure(_, _) => Ok(RuleResultValue {
224            display,
225            measure: Some(measure_to_unit_map(literal, result_type)?),
226            ..RuleResultValue::default()
227        }),
228        ValueKind::Ratio(_, _) => Ok(RuleResultValue {
229            display,
230            ratio: Some(ratio_to_unit_map(literal, result_type)?),
231            ..RuleResultValue::default()
232        }),
233        ValueKind::Number(rational) => {
234            let number = result_type
235                .try_rational_as_decimal_string(rational)
236                .map_err(map_numeric_to_rule_result_value_failure)?;
237            Ok(RuleResultValue {
238                display,
239                number: Some(number),
240                ..RuleResultValue::default()
241            })
242        }
243        ValueKind::Boolean(b) => Ok(RuleResultValue {
244            display,
245            boolean: Some(*b),
246            ..RuleResultValue::default()
247        }),
248        ValueKind::Text(s) => Ok(RuleResultValue {
249            display,
250            text: Some(s.clone()),
251            ..RuleResultValue::default()
252        }),
253        ValueKind::Date(d) => Ok(RuleResultValue {
254            display,
255            date: Some(d.clone()),
256            ..RuleResultValue::default()
257        }),
258        ValueKind::Time(t) => Ok(RuleResultValue {
259            display,
260            time: Some(t.clone()),
261            ..RuleResultValue::default()
262        }),
263        ValueKind::Range(_, _) => {
264            unreachable!("BUG: range must be handled by rule_result_value_from_literal")
265        }
266    }
267}
268
269fn decimal_from_api_string(value: &str) -> rust_decimal::Decimal {
270    use std::str::FromStr;
271    rust_decimal::Decimal::from_str(value)
272        .unwrap_or_else(|_| panic!("BUG: rule result API decimal string must parse as decimal"))
273}
274
275fn literal_from_measure_map(
276    measure: &BTreeMap<String, String>,
277    rule_type: &LemmaType,
278) -> LiteralValue {
279    let unit_names = rule_type
280        .measure_unit_names()
281        .expect("BUG: measure rule result must have declared units");
282    let unit_name = unit_names
283        .first()
284        .expect("BUG: measure rule result type must declare at least one unit");
285    let display = measure
286        .get(*unit_name)
287        .unwrap_or_else(|| panic!("BUG: measure map missing unit '{unit_name}'"));
288    let rational = rational_from_parsed_decimal(decimal_from_api_string(display))
289        .expect("BUG: measure rule result value must lift to rational");
290    let factor = rule_type.measure_unit_factor(unit_name);
291    let canonical = checked_mul(&rational, factor).unwrap_or_else(|failure| {
292        panic!("BUG: measure canonicalization from RuleResultValue fields failed: {failure}")
293    });
294    LiteralValue::measure_with_type(
295        canonical,
296        (*unit_name).to_string(),
297        Arc::new(rule_type.clone()),
298    )
299}
300
301fn literal_from_ratio_map(ratio: &BTreeMap<String, String>, rule_type: &LemmaType) -> LiteralValue {
302    let units = match &rule_type.specifications {
303        TypeSpecification::Ratio { units, .. } => units,
304        TypeSpecification::RatioRange { .. } => {
305            let element = range_element_type_specification(&rule_type.specifications)
306                .expect("BUG: ratio range rule type must have ratio element specification");
307            let TypeSpecification::Ratio { units, .. } = element else {
308                panic!("BUG: ratio range element spec must be Ratio");
309            };
310            return literal_from_ratio_map(
311                ratio,
312                &LemmaType::primitive(TypeSpecification::Ratio {
313                    minimum: None,
314                    maximum: None,
315                    decimals: None,
316                    units,
317                    help: String::new(),
318                }),
319            );
320        }
321        _ => panic!(
322            "BUG: ratio rule result type must be Ratio, got {}",
323            rule_type.name()
324        ),
325    };
326    let unit = units
327        .iter()
328        .next()
329        .expect("BUG: ratio rule result type must declare at least one unit");
330    let display = ratio
331        .get(&unit.name)
332        .unwrap_or_else(|| panic!("BUG: ratio map missing unit '{}'", unit.name));
333    let display_rational = rational_from_parsed_decimal(decimal_from_api_string(display))
334        .expect("BUG: ratio rule result value must lift to rational");
335    let canonical = checked_div(&display_rational, &unit.value).unwrap_or_else(|failure| {
336        panic!("BUG: ratio canonicalization from RuleResultValue fields failed: {failure}")
337    });
338    LiteralValue::ratio_with_type(canonical, None, Arc::new(rule_type.clone()))
339}
340
341impl RuleResultValue {
342    /// Reconstruct the [`LiteralValue`] from this API value's fields.
343    ///
344    /// Panics if the fields cannot reconstruct a literal, or if a range endpoint is
345    /// itself a range (ranges do not nest — enforced here, not by convention).
346    pub fn to_literal(&self, rule_type: &LemmaType) -> LiteralValue {
347        if let Some(range) = &self.range {
348            if range.from.range.is_some() || range.to.range.is_some() {
349                panic!("BUG: range endpoint must not itself be a range");
350            }
351            let endpoint_type =
352                element_type_from_range_rule(rule_type).unwrap_or_else(|| rule_type.clone());
353            let left = range.from.to_literal(&endpoint_type);
354            let right = range.to.to_literal(&endpoint_type);
355            return LiteralValue::range(left, right);
356        }
357
358        let owned_rule_type = Arc::new(rule_type.clone());
359        if let Some(b) = self.boolean {
360            return LiteralValue {
361                value: ValueKind::Boolean(b),
362                lemma_type: owned_rule_type,
363            };
364        }
365        if let Some(number) = &self.number {
366            return LiteralValue::number_with_type_from_decimal(
367                decimal_from_api_string(number),
368                owned_rule_type,
369            );
370        }
371        if let Some(calendar) = &self.calendar {
372            let rational = rational_from_parsed_decimal(decimal_from_api_string(&calendar.value))
373                .expect("BUG: calendar rule result value must lift to rational");
374            return LiteralValue::measure_with_type(
375                rational,
376                calendar.unit.clone(),
377                owned_rule_type,
378            );
379        }
380        if let Some(measure) = &self.measure {
381            return literal_from_measure_map(measure, rule_type);
382        }
383        if let Some(ratio) = &self.ratio {
384            return literal_from_ratio_map(ratio, rule_type);
385        }
386        if let Some(date) = &self.date {
387            return LiteralValue {
388                value: ValueKind::Date(date.clone()),
389                lemma_type: owned_rule_type,
390            };
391        }
392        if let Some(time) = &self.time {
393            return LiteralValue {
394                value: ValueKind::Time(time.clone()),
395                lemma_type: owned_rule_type,
396            };
397        }
398        if let Some(text) = &self.text {
399            return LiteralValue {
400                value: ValueKind::Text(text.clone()),
401                lemma_type: owned_rule_type,
402            };
403        }
404        panic!("BUG: rule result value fields cannot reconstruct literal");
405    }
406}
407
408fn format_unit_map(map: &BTreeMap<String, String>) -> String {
409    map.iter()
410        .map(|(unit, value)| format!("{value} {unit}"))
411        .collect::<Vec<_>>()
412        .join(", ")
413}
414
415impl fmt::Display for RuleResultValue {
416    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
417        if let Some(range) = &self.range {
418            return write!(f, "{}...{}", range.from, range.to);
419        }
420        if let Some(measure) = &self.measure {
421            return write!(f, "{}", format_unit_map(measure));
422        }
423        if let Some(ratio) = &self.ratio {
424            return write!(f, "{}", format_unit_map(ratio));
425        }
426        if let Some(number) = &self.number {
427            return write!(f, "{number}");
428        }
429        if let Some(b) = self.boolean {
430            return write!(f, "{b}");
431        }
432        if let Some(text) = &self.text {
433            return write!(f, "{text}");
434        }
435        if let Some(date) = &self.date {
436            return write!(f, "{date}");
437        }
438        if let Some(time) = &self.time {
439            return write!(f, "{time}");
440        }
441        if let Some(calendar) = &self.calendar {
442            return write!(f, "{} {}", calendar.value, calendar.unit);
443        }
444        panic!("BUG: rule result value has no field set to display");
445    }
446}