1use 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31pub struct CalendarResult {
32 pub value: Decimal,
33 pub unit: String,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
43pub struct RangeResult {
44 pub from: RuleResultValue,
45 pub to: RuleResultValue,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
55pub struct RuleResultValue {
56 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum RuleResultValueFailure {
83 DecimalLimit,
84 NumericOverflow,
85 OutOfMemory,
86}
87
88pub(crate) enum UnitExpansion<'a> {
90 Declared,
92 Family(&'a FamilyUnitEntry),
94}
95
96pub 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
215pub(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
227pub 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 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}