1use 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27pub struct CalendarResult {
28 pub value: String,
29 pub unit: String,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39pub struct RangeResult {
40 pub from: RuleResultValue,
41 pub to: RuleResultValue,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
51pub struct RuleResultValue {
52 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum RuleResultValueFailure {
69 DecimalLimit,
70 NumericOverflow,
71 OutOfMemory,
72}
73
74pub(crate) enum UnitExpansion<'a> {
76 Declared,
78 Family(&'a FamilyUnitEntry),
80}
81
82pub 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
136fn 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 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
190pub(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
221pub(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
233pub 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 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}