Skip to main content

lemma/
result_value.rs

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