1use crate::computation::rational::{checked_div, checked_mul, NumericFailure};
10use crate::literals::rational_from_parsed_decimal;
11use crate::planning::semantics::{
12 range_element_type_specification, semantic_calendar_unit_from_measure_signature, LemmaType,
13 LiteralUnitMapFailure, LiteralValue, SemanticDateTime, SemanticTime, TypeSpecification,
14 ValueKind,
15};
16use serde::{Deserialize, Serialize};
17use std::collections::BTreeMap;
18use std::fmt;
19use std::sync::Arc;
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23pub struct CalendarResult {
24 pub value: String,
25 pub unit: String,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35pub struct RangeResult {
36 pub from: RuleResultValue,
37 pub to: RuleResultValue,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
47pub struct RuleResultValue {
48 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub display: Option<String>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub measure: Option<BTreeMap<String, String>>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub ratio: Option<BTreeMap<String, String>>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub number: Option<String>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub boolean: Option<bool>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub text: Option<String>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub date: Option<SemanticDateTime>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub time: Option<SemanticTime>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub calendar: Option<CalendarResult>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub range: Option<Box<RangeResult>>,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum RuleResultValueFailure {
75 DecimalLimit,
76 NumericOverflow,
77 OutOfMemory,
78}
79
80pub fn rule_result_value_failure_message(failure: RuleResultValueFailure) -> &'static str {
82 match failure {
83 RuleResultValueFailure::DecimalLimit => "Calculated result exceeds decimal value limit",
84 RuleResultValueFailure::NumericOverflow => "numeric overflow",
85 RuleResultValueFailure::OutOfMemory => "out of memory",
86 }
87}
88
89fn map_numeric_to_rule_result_value_failure(failure: NumericFailure) -> RuleResultValueFailure {
90 match failure {
91 NumericFailure::Overflow => RuleResultValueFailure::DecimalLimit,
92 NumericFailure::OutOfMemory => RuleResultValueFailure::OutOfMemory,
93 NumericFailure::DivisionByZero => {
94 panic!(
95 "BUG: decimal commit encountered division by zero while building RuleResultValue"
96 )
97 }
98 NumericFailure::Irrational => {
99 panic!(
100 "BUG: decimal commit encountered irrational result while building RuleResultValue"
101 )
102 }
103 }
104}
105
106fn map_unit_conversion_failure(failure: NumericFailure) -> RuleResultValueFailure {
107 match failure {
108 NumericFailure::Overflow => RuleResultValueFailure::NumericOverflow,
109 NumericFailure::OutOfMemory => RuleResultValueFailure::OutOfMemory,
110 NumericFailure::DivisionByZero => {
111 panic!(
112 "BUG: unit conversion encountered division by zero while building RuleResultValue"
113 )
114 }
115 NumericFailure::Irrational => {
116 panic!(
117 "BUG: unit conversion encountered irrational result while building RuleResultValue"
118 )
119 }
120 }
121}
122
123fn map_literal_unit_map_failure(failure: LiteralUnitMapFailure) -> RuleResultValueFailure {
124 match failure {
125 LiteralUnitMapFailure::Commit(nf) => map_numeric_to_rule_result_value_failure(nf),
126 LiteralUnitMapFailure::UnitConversion(nf) => map_unit_conversion_failure(nf),
127 }
128}
129
130fn measure_to_unit_map(
131 literal: &LiteralValue,
132 result_type: &LemmaType,
133) -> Result<BTreeMap<String, String>, RuleResultValueFailure> {
134 result_type
135 .measure_literal_in_all_units(literal)
136 .map_err(map_literal_unit_map_failure)
137}
138
139fn ratio_to_unit_map(
140 literal: &LiteralValue,
141 result_type: &LemmaType,
142) -> Result<BTreeMap<String, String>, RuleResultValueFailure> {
143 result_type
144 .ratio_literal_in_all_units(literal)
145 .map_err(map_literal_unit_map_failure)
146}
147
148fn element_type_from_range_rule(rule_type: &LemmaType) -> Option<LemmaType> {
149 range_element_type_specification(&rule_type.specifications).map(LemmaType::primitive)
150}
151
152fn range_endpoint_type(endpoint: &LiteralValue, range_element_type: &LemmaType) -> LemmaType {
155 if endpoint.lemma_type.measure_unit_names().is_some() {
156 endpoint.lemma_type.as_ref().clone()
157 } else {
158 range_element_type.clone()
159 }
160}
161
162pub fn rule_result_value_from_literal(
168 literal: &LiteralValue,
169 rule_type: &LemmaType,
170) -> Result<RuleResultValue, RuleResultValueFailure> {
171 match &literal.value {
172 ValueKind::Range(from, to) => {
173 let endpoint_type =
174 element_type_from_range_rule(rule_type).unwrap_or_else(|| rule_type.clone());
175 let from_type = range_endpoint_type(from, &endpoint_type);
176 let to_type = range_endpoint_type(to, &endpoint_type);
177 let from_value = rule_result_value_from_range_endpoint(from, &from_type)?;
178 let to_value = rule_result_value_from_range_endpoint(to, &to_type)?;
179 Ok(RuleResultValue {
180 display: Some(literal.display_value()),
181 range: Some(Box::new(RangeResult {
182 from: from_value,
183 to: to_value,
184 })),
185 ..RuleResultValue::default()
186 })
187 }
188 _ => rule_result_value_from_non_range_literal(literal, rule_type),
189 }
190}
191
192fn rule_result_value_from_range_endpoint(
193 endpoint: &LiteralValue,
194 endpoint_type: &LemmaType,
195) -> Result<RuleResultValue, RuleResultValueFailure> {
196 if matches!(&endpoint.value, ValueKind::Range(_, _)) {
197 panic!("BUG: range endpoint must not itself be a range");
198 }
199 rule_result_value_from_non_range_literal(endpoint, endpoint_type)
200}
201
202fn rule_result_value_from_non_range_literal(
203 literal: &LiteralValue,
204 result_type: &LemmaType,
205) -> Result<RuleResultValue, RuleResultValueFailure> {
206 let display = Some(literal.display_value());
207 match &literal.value {
208 ValueKind::Measure(rational, sig) if literal.lemma_type.is_calendar_like() => {
209 let unit = semantic_calendar_unit_from_measure_signature(sig);
210 let value = literal
211 .lemma_type
212 .try_rational_as_decimal_string(rational)
213 .map_err(map_numeric_to_rule_result_value_failure)?;
214 Ok(RuleResultValue {
215 display,
216 calendar: Some(CalendarResult {
217 value,
218 unit: unit.to_string(),
219 }),
220 ..RuleResultValue::default()
221 })
222 }
223 ValueKind::Measure(_, _) => Ok(RuleResultValue {
224 display,
225 measure: Some(measure_to_unit_map(literal, result_type)?),
226 ..RuleResultValue::default()
227 }),
228 ValueKind::Ratio(_, _) => Ok(RuleResultValue {
229 display,
230 ratio: Some(ratio_to_unit_map(literal, result_type)?),
231 ..RuleResultValue::default()
232 }),
233 ValueKind::Number(rational) => {
234 let number = result_type
235 .try_rational_as_decimal_string(rational)
236 .map_err(map_numeric_to_rule_result_value_failure)?;
237 Ok(RuleResultValue {
238 display,
239 number: Some(number),
240 ..RuleResultValue::default()
241 })
242 }
243 ValueKind::Boolean(b) => Ok(RuleResultValue {
244 display,
245 boolean: Some(*b),
246 ..RuleResultValue::default()
247 }),
248 ValueKind::Text(s) => Ok(RuleResultValue {
249 display,
250 text: Some(s.clone()),
251 ..RuleResultValue::default()
252 }),
253 ValueKind::Date(d) => Ok(RuleResultValue {
254 display,
255 date: Some(d.clone()),
256 ..RuleResultValue::default()
257 }),
258 ValueKind::Time(t) => Ok(RuleResultValue {
259 display,
260 time: Some(t.clone()),
261 ..RuleResultValue::default()
262 }),
263 ValueKind::Range(_, _) => {
264 unreachable!("BUG: range must be handled by rule_result_value_from_literal")
265 }
266 }
267}
268
269fn decimal_from_api_string(value: &str) -> rust_decimal::Decimal {
270 use std::str::FromStr;
271 rust_decimal::Decimal::from_str(value)
272 .unwrap_or_else(|_| panic!("BUG: rule result API decimal string must parse as decimal"))
273}
274
275fn literal_from_measure_map(
276 measure: &BTreeMap<String, String>,
277 rule_type: &LemmaType,
278) -> LiteralValue {
279 let unit_names = rule_type
280 .measure_unit_names()
281 .expect("BUG: measure rule result must have declared units");
282 let unit_name = unit_names
283 .first()
284 .expect("BUG: measure rule result type must declare at least one unit");
285 let display = measure
286 .get(*unit_name)
287 .unwrap_or_else(|| panic!("BUG: measure map missing unit '{unit_name}'"));
288 let rational = rational_from_parsed_decimal(decimal_from_api_string(display))
289 .expect("BUG: measure rule result value must lift to rational");
290 let factor = rule_type.measure_unit_factor(unit_name);
291 let canonical = checked_mul(&rational, factor).unwrap_or_else(|failure| {
292 panic!("BUG: measure canonicalization from RuleResultValue fields failed: {failure}")
293 });
294 LiteralValue::measure_with_type(
295 canonical,
296 (*unit_name).to_string(),
297 Arc::new(rule_type.clone()),
298 )
299}
300
301fn literal_from_ratio_map(ratio: &BTreeMap<String, String>, rule_type: &LemmaType) -> LiteralValue {
302 let units = match &rule_type.specifications {
303 TypeSpecification::Ratio { units, .. } => units,
304 TypeSpecification::RatioRange { .. } => {
305 let element = range_element_type_specification(&rule_type.specifications)
306 .expect("BUG: ratio range rule type must have ratio element specification");
307 let TypeSpecification::Ratio { units, .. } = element else {
308 panic!("BUG: ratio range element spec must be Ratio");
309 };
310 return literal_from_ratio_map(
311 ratio,
312 &LemmaType::primitive(TypeSpecification::Ratio {
313 minimum: None,
314 maximum: None,
315 decimals: None,
316 units,
317 help: String::new(),
318 }),
319 );
320 }
321 _ => panic!(
322 "BUG: ratio rule result type must be Ratio, got {}",
323 rule_type.name()
324 ),
325 };
326 let unit = units
327 .iter()
328 .next()
329 .expect("BUG: ratio rule result type must declare at least one unit");
330 let display = ratio
331 .get(&unit.name)
332 .unwrap_or_else(|| panic!("BUG: ratio map missing unit '{}'", unit.name));
333 let display_rational = rational_from_parsed_decimal(decimal_from_api_string(display))
334 .expect("BUG: ratio rule result value must lift to rational");
335 let canonical = checked_div(&display_rational, &unit.value).unwrap_or_else(|failure| {
336 panic!("BUG: ratio canonicalization from RuleResultValue fields failed: {failure}")
337 });
338 LiteralValue::ratio_with_type(canonical, None, Arc::new(rule_type.clone()))
339}
340
341impl RuleResultValue {
342 pub fn to_literal(&self, rule_type: &LemmaType) -> LiteralValue {
347 if let Some(range) = &self.range {
348 if range.from.range.is_some() || range.to.range.is_some() {
349 panic!("BUG: range endpoint must not itself be a range");
350 }
351 let endpoint_type =
352 element_type_from_range_rule(rule_type).unwrap_or_else(|| rule_type.clone());
353 let left = range.from.to_literal(&endpoint_type);
354 let right = range.to.to_literal(&endpoint_type);
355 return LiteralValue::range(left, right);
356 }
357
358 let owned_rule_type = Arc::new(rule_type.clone());
359 if let Some(b) = self.boolean {
360 return LiteralValue {
361 value: ValueKind::Boolean(b),
362 lemma_type: owned_rule_type,
363 };
364 }
365 if let Some(number) = &self.number {
366 return LiteralValue::number_with_type_from_decimal(
367 decimal_from_api_string(number),
368 owned_rule_type,
369 );
370 }
371 if let Some(calendar) = &self.calendar {
372 let rational = rational_from_parsed_decimal(decimal_from_api_string(&calendar.value))
373 .expect("BUG: calendar rule result value must lift to rational");
374 return LiteralValue::measure_with_type(
375 rational,
376 calendar.unit.clone(),
377 owned_rule_type,
378 );
379 }
380 if let Some(measure) = &self.measure {
381 return literal_from_measure_map(measure, rule_type);
382 }
383 if let Some(ratio) = &self.ratio {
384 return literal_from_ratio_map(ratio, rule_type);
385 }
386 if let Some(date) = &self.date {
387 return LiteralValue {
388 value: ValueKind::Date(date.clone()),
389 lemma_type: owned_rule_type,
390 };
391 }
392 if let Some(time) = &self.time {
393 return LiteralValue {
394 value: ValueKind::Time(time.clone()),
395 lemma_type: owned_rule_type,
396 };
397 }
398 if let Some(text) = &self.text {
399 return LiteralValue {
400 value: ValueKind::Text(text.clone()),
401 lemma_type: owned_rule_type,
402 };
403 }
404 panic!("BUG: rule result value fields cannot reconstruct literal");
405 }
406}
407
408fn format_unit_map(map: &BTreeMap<String, String>) -> String {
409 map.iter()
410 .map(|(unit, value)| format!("{value} {unit}"))
411 .collect::<Vec<_>>()
412 .join(", ")
413}
414
415impl fmt::Display for RuleResultValue {
416 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
417 if let Some(range) = &self.range {
418 return write!(f, "{}...{}", range.from, range.to);
419 }
420 if let Some(measure) = &self.measure {
421 return write!(f, "{}", format_unit_map(measure));
422 }
423 if let Some(ratio) = &self.ratio {
424 return write!(f, "{}", format_unit_map(ratio));
425 }
426 if let Some(number) = &self.number {
427 return write!(f, "{number}");
428 }
429 if let Some(b) = self.boolean {
430 return write!(f, "{b}");
431 }
432 if let Some(text) = &self.text {
433 return write!(f, "{text}");
434 }
435 if let Some(date) = &self.date {
436 return write!(f, "{date}");
437 }
438 if let Some(time) = &self.time {
439 return write!(f, "{time}");
440 }
441 if let Some(calendar) = &self.calendar {
442 return write!(f, "{} {}", calendar.value, calendar.unit);
443 }
444 panic!("BUG: rule result value has no field set to display");
445 }
446}