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