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")]
78 pub unit: Option<String>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub range: Option<Box<RangeResult>>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub spec: Option<Box<SpecResult>>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
89pub struct SpecResult {
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub repository: Option<String>,
92 pub spec: String,
93 pub effective: String,
95 pub instance: String,
97 pub data: BTreeMap<String, SpecRuleResult>,
101 pub rules: BTreeMap<String, SpecRuleResult>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
109pub struct SpecRuleResult {
110 pub vetoed: bool,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub veto_reason: Option<String>,
113 pub rule_type: String,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub result: Option<RuleResultValue>,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum RuleResultValueFailure {
121 DecimalLimit,
122 NumericOverflow,
123 OutOfMemory,
124}
125
126pub(crate) enum UnitExpansion<'a> {
128 Declared,
130 Family(&'a FamilyUnitEntry),
132}
133
134pub fn rule_result_value_failure_message(failure: RuleResultValueFailure) -> &'static str {
136 match failure {
137 RuleResultValueFailure::DecimalLimit => "Calculated result exceeds decimal value limit",
138 RuleResultValueFailure::NumericOverflow => "numeric overflow",
139 RuleResultValueFailure::OutOfMemory => "out of memory",
140 }
141}
142
143fn map_numeric_to_rule_result_value_failure(failure: NumericFailure) -> RuleResultValueFailure {
144 match failure {
145 NumericFailure::Overflow => RuleResultValueFailure::DecimalLimit,
146 NumericFailure::OutOfMemory => RuleResultValueFailure::OutOfMemory,
147 NumericFailure::DivisionByZero => {
148 panic!(
149 "BUG: decimal commit encountered division by zero while building RuleResultValue"
150 )
151 }
152 NumericFailure::Irrational => {
153 panic!(
154 "BUG: decimal commit encountered irrational result while building RuleResultValue"
155 )
156 }
157 }
158}
159
160fn map_unit_conversion_failure(failure: NumericFailure) -> RuleResultValueFailure {
161 match failure {
162 NumericFailure::Overflow => RuleResultValueFailure::NumericOverflow,
163 NumericFailure::OutOfMemory => RuleResultValueFailure::OutOfMemory,
164 NumericFailure::DivisionByZero => {
165 panic!("BUG: unit conversion division by zero while building RuleResultValue")
166 }
167 NumericFailure::Irrational => {
168 panic!("BUG: unit conversion irrational while building RuleResultValue")
169 }
170 }
171}
172
173fn map_literal_unit_map_failure(failure: LiteralUnitMapFailure) -> RuleResultValueFailure {
174 match failure {
175 LiteralUnitMapFailure::Commit(failure) => map_numeric_to_rule_result_value_failure(failure),
176 LiteralUnitMapFailure::UnitConversion(failure) => map_unit_conversion_failure(failure),
177 }
178}
179
180fn expansion_for_type<'a>(
181 rule_type: &LemmaType,
182 catalog: Option<&'a FamilyUnitCatalog>,
183) -> UnitExpansion<'a> {
184 let Some(catalog) = catalog else {
185 return UnitExpansion::Declared;
186 };
187 match catalog.entry_for_type(rule_type) {
188 Some(entry) => UnitExpansion::Family(entry),
189 None => UnitExpansion::Declared,
190 }
191}
192
193fn unit_names_for_expansion(
194 result_type: &LemmaType,
195 expansion: &UnitExpansion<'_>,
196 catalog: Option<&FamilyUnitCatalog>,
197) -> Arc<[String]> {
198 match expansion {
199 UnitExpansion::Declared => Arc::from(declared_bare_names_only(result_type)),
200 UnitExpansion::Family(_) => catalog
201 .expect("BUG: family expansion requires FamilyUnitCatalog")
202 .ordered_bare_names_for_type(result_type),
203 }
204}
205
206fn measure_factor_source<'a>(
207 result_type: &'a LemmaType,
208 expansion: &'a UnitExpansion<'a>,
209) -> UnitFactorSource<'a> {
210 match expansion {
211 UnitExpansion::Declared => UnitFactorSource::DeclaredOn(result_type),
212 UnitExpansion::Family(entry) => UnitFactorSource::Merged {
213 measure: entry.merged_measure_units.as_ref(),
214 ratio: entry.merged_ratio_units.as_ref(),
215 },
216 }
217}
218
219fn ratio_factor_source<'a>(
220 result_type: &'a LemmaType,
221 expansion: &'a UnitExpansion<'a>,
222) -> UnitFactorSource<'a> {
223 measure_factor_source(result_type, expansion)
224}
225
226fn result_value_from_literal(
227 literal: &LiteralValue,
228 lemma_type: &LemmaType,
229 expansion: &UnitExpansion<'_>,
230 catalog: Option<&FamilyUnitCatalog>,
231) -> Result<RuleResultValue, RuleResultValueFailure> {
232 match &literal.value {
233 ValueKind::Range(from, to) => {
234 let from_value = result_value_from_range_endpoint(
235 &from.to_literal(),
236 from.lemma_type.as_ref(),
237 expansion,
238 catalog,
239 )?;
240 let to_value = result_value_from_range_endpoint(
241 &to.to_literal(),
242 to.lemma_type.as_ref(),
243 expansion,
244 catalog,
245 )?;
246 Ok(RuleResultValue {
247 result: Some(literal.display_value_with_type(lemma_type)),
248 range: Some(Box::new(RangeResult {
249 from: from_value,
250 to: to_value,
251 })),
252 ..RuleResultValue::default()
253 })
254 }
255 _ => result_value_from_non_range_literal(literal, lemma_type, expansion, catalog),
256 }
257}
258
259pub(crate) fn rule_result_value_from_literal(
263 literal: &LiteralValue,
264 rule_type: &LemmaType,
265 catalog: &FamilyUnitCatalog,
266) -> Result<RuleResultValue, RuleResultValueFailure> {
267 let expansion = expansion_for_type(rule_type, Some(catalog));
268 result_value_from_literal(literal, rule_type, &expansion, Some(catalog))
269}
270
271pub fn type_scoped_result_value_from_literal(
273 literal: &LiteralValue,
274 lemma_type: &LemmaType,
275) -> Result<RuleResultValue, RuleResultValueFailure> {
276 let expansion = UnitExpansion::Declared;
277 result_value_from_literal(literal, lemma_type, &expansion, None)
278}
279
280fn result_value_from_range_endpoint(
281 endpoint: &LiteralValue,
282 endpoint_type: &LemmaType,
283 expansion: &UnitExpansion<'_>,
284 catalog: Option<&FamilyUnitCatalog>,
285) -> Result<RuleResultValue, RuleResultValueFailure> {
286 if matches!(&endpoint.value, ValueKind::Range(_, _)) {
287 panic!("BUG: range endpoint must not itself be a range");
288 }
289 result_value_from_non_range_literal(endpoint, endpoint_type, expansion, catalog)
290}
291
292fn result_value_from_non_range_literal(
293 literal: &LiteralValue,
294 result_type: &LemmaType,
295 expansion: &UnitExpansion<'_>,
296 catalog: Option<&FamilyUnitCatalog>,
297) -> Result<RuleResultValue, RuleResultValueFailure> {
298 match &literal.value {
299 ValueKind::Measure(rational) if result_type.is_calendar_like() => {
300 let unit = semantic_calendar_unit_from_measure_type(result_type);
301 let value = result_type
302 .try_rational_as_decimal(rational)
303 .map_err(map_numeric_to_rule_result_value_failure)?;
304 let result = Some(literal.display_value_with_type(result_type));
305 Ok(RuleResultValue {
306 result,
307 calendar: Some(CalendarResult {
308 value,
309 unit: unit.to_string(),
310 }),
311 ..RuleResultValue::default()
312 })
313 }
314 ValueKind::Measure(_) => {
315 let result = Some(literal.display_value_with_type(result_type));
316 let unit_names = unit_names_for_expansion(result_type, expansion, catalog);
317 let unit_name_refs: Vec<&str> = unit_names.iter().map(String::as_str).collect();
318 Ok(RuleResultValue {
319 result,
320 measure: Some(
321 result_type
322 .measure_literal_unit_map(
323 literal,
324 &unit_name_refs,
325 measure_factor_source(result_type, expansion),
326 )
327 .map_err(map_literal_unit_map_failure)?,
328 ),
329 unit: result_type.unit().map(str::to_string),
330 ..RuleResultValue::default()
331 })
332 }
333 ValueKind::Ratio(_) => {
334 let result = Some(literal.display_value_with_type(result_type));
335 let unit_names = unit_names_for_expansion(result_type, expansion, catalog);
336 let unit_name_refs: Vec<&str> = unit_names.iter().map(String::as_str).collect();
337 Ok(RuleResultValue {
338 result,
339 ratio: Some(
340 result_type
341 .ratio_literal_unit_map(
342 literal,
343 &unit_name_refs,
344 ratio_factor_source(result_type, expansion),
345 )
346 .map_err(map_literal_unit_map_failure)?,
347 ),
348 unit: result_type.unit().map(str::to_string),
349 ..RuleResultValue::default()
350 })
351 }
352 other => scalar_result_value(result_type, other),
353 }
354}
355
356fn scalar_result_value(
357 result_type: &LemmaType,
358 value: &ValueKind,
359) -> Result<RuleResultValue, RuleResultValueFailure> {
360 match value {
361 ValueKind::Number(rational) => {
362 let decimal = rational
363 .try_to_decimal()
364 .map_err(map_numeric_to_rule_result_value_failure)?;
365 let api_decimal = format_decimal_for_api(decimal, result_type.decimal_places());
366 let display_string = decimal_to_display_str(&decimal);
367 Ok(RuleResultValue {
368 result: Some(display_string),
369 number: Some(api_decimal),
370 ..RuleResultValue::default()
371 })
372 }
373 ValueKind::Boolean(b) => {
374 let result = Some(value.to_string());
375 Ok(RuleResultValue {
376 result,
377 boolean: Some(*b),
378 ..RuleResultValue::default()
379 })
380 }
381 ValueKind::Text(_) | ValueKind::Date(_) | ValueKind::Time(_) => {
382 let result = Some(value.to_string());
383 Ok(scalar_result_value_non_numeric(result_type, result, value))
384 }
385 ValueKind::Measure(_) | ValueKind::Ratio(_) => {
386 unreachable!("BUG: measure and ratio must be handled by caller")
387 }
388 ValueKind::Range(_, _) => {
389 unreachable!("BUG: range must be handled by result_value_from_literal")
390 }
391 ValueKind::Spec(instance) => {
392 let identity = result_type.spec_identity().unwrap_or_else(|| {
393 panic!(
394 "BUG: spec value whose type is not a spec ({})",
395 result_type.name()
396 )
397 });
398 Ok(RuleResultValue {
399 result: Some(instance.to_string()),
400 spec: Some(Box::new(SpecResult {
401 repository: identity.repository.clone(),
402 spec: identity.spec.clone(),
403 effective: identity.effective.to_string(),
404 instance: instance.to_string(),
405 data: BTreeMap::new(),
406 rules: BTreeMap::new(),
407 })),
408 ..RuleResultValue::default()
409 })
410 }
411 }
412}
413
414fn scalar_result_value_non_numeric(
415 _result_type: &LemmaType,
416 result: Option<String>,
417 value: &ValueKind,
418) -> RuleResultValue {
419 match value {
420 ValueKind::Text(s) => RuleResultValue {
421 result,
422 text: Some(s.clone()),
423 ..RuleResultValue::default()
424 },
425 ValueKind::Date(d) => RuleResultValue {
426 result,
427 date: Some(d.clone()),
428 ..RuleResultValue::default()
429 },
430 ValueKind::Time(t) => RuleResultValue {
431 result,
432 time: Some(t.clone()),
433 ..RuleResultValue::default()
434 },
435 _ => unreachable!("BUG: scalar_result_value_non_numeric called with non-scalar type"),
436 }
437}
438
439fn literal_from_measure_map(
440 measure: &BTreeMap<String, Decimal>,
441 rule_type: &LemmaType,
442) -> LiteralValue {
443 let unit_names = rule_type
444 .measure_unit_names()
445 .expect("BUG: measure rule result must have declared units");
446 let unit_name = rule_type
447 .unit()
448 .map(str::to_string)
449 .or_else(|| unit_names.first().map(|name| (*name).to_string()))
450 .expect("BUG: measure rule result type must declare at least one unit");
451 let magnitude = measure
452 .get(&unit_name)
453 .unwrap_or_else(|| panic!("BUG: measure map missing unit '{unit_name}'"));
454 let rational = rational_from_parsed_decimal(*magnitude)
455 .expect("BUG: measure rule result value must lift to rational");
456 let factor = rule_type.measure_unit_factor(&unit_name);
457 let canonical = checked_mul(&rational, factor).unwrap_or_else(|failure| {
458 panic!("BUG: measure canonicalization from RuleResultValue fields failed: {failure}")
459 });
460 let typed = if rule_type.unit() == Some(unit_name.as_str()) {
461 Arc::new(rule_type.clone())
462 } else {
463 Arc::new(rule_type.clone().with_unit(unit_name))
464 };
465 LiteralValue::measure_with_type(canonical, typed)
466}
467
468fn literal_from_ratio_map(
469 ratio: &BTreeMap<String, Decimal>,
470 rule_type: &LemmaType,
471) -> LiteralValue {
472 let ratio_type = ratio_element_type_for_api(rule_type);
473 let units = match &ratio_type.specifications {
474 TypeSpecification::Ratio { units, .. } => units,
475 _ => panic!(
476 "BUG: ratio rule result type must be Ratio, got {}",
477 rule_type.name()
478 ),
479 };
480 let unit = rule_type
481 .unit()
482 .and_then(|name| units.get(name).ok())
483 .or_else(|| units.iter().next())
484 .expect("BUG: ratio rule result type must declare at least one unit");
485 let magnitude = ratio
486 .get(&unit.name)
487 .unwrap_or_else(|| panic!("BUG: ratio map missing unit '{}'", unit.name));
488 let display_rational = rational_from_parsed_decimal(*magnitude)
489 .expect("BUG: ratio rule result value must lift to rational");
490 let canonical = checked_div(&display_rational, &unit.value).unwrap_or_else(|failure| {
491 panic!("BUG: ratio canonicalization from RuleResultValue fields failed: {failure}")
492 });
493 let typed = if rule_type.unit() == Some(unit.name.as_str()) {
494 Arc::new(rule_type.clone())
495 } else {
496 Arc::new(rule_type.clone().with_unit(unit.name.clone()))
497 };
498 LiteralValue::ratio_with_type(canonical, typed)
499}
500
501impl RuleResultValue {
502 pub fn to_literal(&self, rule_type: &LemmaType) -> LiteralValue {
508 if let Some(spec) = &self.spec {
509 let hops: Vec<&str> = spec
510 .instance
511 .split('.')
512 .filter(|hop| !hop.is_empty())
513 .collect();
514 if hops.len() != 1 {
515 todo!(
516 "rebuilding spec instance '{}' needs each uses hop's spec",
517 spec.instance
518 );
519 }
520 let identity = rule_type
521 .spec_identity()
522 .unwrap_or_else(|| panic!("BUG: spec result whose rule type is not a spec"));
523 return LiteralValue::new(crate::planning::semantics::ValueKind::Spec(
524 crate::planning::semantics::SpecInstance {
525 prefix: vec![crate::planning::semantics::PathSegment {
526 uses: hops[0].to_string(),
527 repository: identity.repository.clone(),
528 spec: identity.spec.clone(),
529 }],
530 },
531 ));
532 }
533 if let Some(range) = &self.range {
534 if range.from.range.is_some() || range.to.range.is_some() {
535 panic!("BUG: range endpoint must not itself be a range");
536 }
537 let endpoint_schema =
538 element_type_from_range_rule(rule_type).unwrap_or_else(|| rule_type.clone());
539 let left_type =
540 endpoint_type_with_written_unit(&endpoint_schema, range.from.unit.as_deref());
541 let right_type =
542 endpoint_type_with_written_unit(&endpoint_schema, range.to.unit.as_deref());
543 let left = range.from.to_literal(&left_type);
544 let right = range.to.to_literal(&right_type);
545 return LiteralValue::range(
546 crate::planning::semantics::TypedLiteral {
547 value: left.value,
548 lemma_type: Arc::new(left_type),
549 },
550 crate::planning::semantics::TypedLiteral {
551 value: right.value,
552 lemma_type: Arc::new(right_type),
553 },
554 );
555 }
556
557 if let Some(b) = self.boolean {
558 return LiteralValue::from_bool(b);
559 }
560 let owned_rule_type = match self.unit.as_deref() {
561 Some(unit)
562 if matches!(
563 &rule_type.specifications,
564 TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. }
565 ) && rule_type.unit() != Some(unit) =>
566 {
567 Arc::new(rule_type.clone().with_unit(unit))
568 }
569 _ => Arc::new(rule_type.clone()),
570 };
571 if let Some(number) = self.number {
572 return LiteralValue::number_with_type_from_decimal(number, owned_rule_type);
573 }
574 if let Some(calendar) = &self.calendar {
575 let rational = rational_from_parsed_decimal(calendar.value)
576 .expect("BUG: calendar rule result value must lift to rational");
577 return LiteralValue::measure_with_type(rational, owned_rule_type);
578 }
579 if let Some(measure) = &self.measure {
580 return literal_from_measure_map(measure, owned_rule_type.as_ref());
581 }
582 if let Some(ratio) = &self.ratio {
583 return literal_from_ratio_map(ratio, owned_rule_type.as_ref());
584 }
585 if let Some(date) = &self.date {
586 return LiteralValue::date_with_type(date.clone(), owned_rule_type);
587 }
588 if let Some(time) = &self.time {
589 return LiteralValue::time_with_type(time.clone(), owned_rule_type);
590 }
591 if let Some(text) = &self.text {
592 return LiteralValue::text_with_type(text.clone(), owned_rule_type);
593 }
594 panic!("BUG: rule result value fields cannot reconstruct literal");
595 }
596}
597
598fn endpoint_type_with_written_unit(endpoint_schema: &LemmaType, unit: Option<&str>) -> LemmaType {
599 match unit {
600 Some(unit)
601 if matches!(
602 &endpoint_schema.specifications,
603 TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. }
604 ) && endpoint_schema.unit() != Some(unit) =>
605 {
606 endpoint_schema.clone().with_unit(unit)
607 }
608 _ => endpoint_schema.clone(),
609 }
610}
611
612fn element_type_from_range_rule(rule_type: &LemmaType) -> Option<LemmaType> {
613 range_element_type_specification(&rule_type.specifications).map(LemmaType::primitive)
614}
615
616fn format_unit_map(map: &BTreeMap<String, Decimal>) -> String {
617 map.iter()
618 .map(|(unit, value)| format!("{value} {unit}"))
619 .collect::<Vec<_>>()
620 .join(", ")
621}
622
623impl fmt::Display for RuleResultValue {
624 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
625 if let Some(range) = &self.range {
626 return write!(f, "{}...{}", range.from, range.to);
627 }
628 if let Some(measure) = &self.measure {
629 return write!(f, "{}", format_unit_map(measure));
630 }
631 if let Some(ratio) = &self.ratio {
632 return write!(f, "{}", format_unit_map(ratio));
633 }
634 if let Some(number) = &self.number {
635 return write!(f, "{number}");
636 }
637 if let Some(b) = self.boolean {
638 return write!(f, "{b}");
639 }
640 if let Some(text) = &self.text {
641 return write!(f, "{text}");
642 }
643 if let Some(date) = &self.date {
644 return write!(f, "{date}");
645 }
646 if let Some(time) = &self.time {
647 return write!(f, "{time}");
648 }
649 if let Some(calendar) = &self.calendar {
650 return write!(f, "{} {}", calendar.value, calendar.unit);
651 }
652 if let Some(display) = &self.result {
653 return write!(f, "{display}");
654 }
655 write!(f, "")
656 }
657}