lemma/computation/
operation_result.rs1use std::fmt;
4
5use crate::planning::semantics::{
6 BoundValueKind, DataPath, LemmaType, LiteralValue, SemanticDateTime, SemanticTime,
7 TypeSpecification,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub enum VetoType {
13 MissingData {
15 data: DataPath,
16 suggestion: Option<String>,
17 },
18 MissingRowData { key: String },
21 UserDefined { message: Option<String> },
23 Computation { message: String },
25}
26
27impl fmt::Display for VetoType {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 VetoType::MissingData {
31 data,
32 suggestion: Some(suggestion),
33 } => write!(
34 f,
35 "Required data '{data}' was not provided. Did you mean '{suggestion}'?"
36 ),
37 VetoType::MissingData {
38 data,
39 suggestion: None,
40 } => write!(f, "Required data '{data}' was not provided."),
41 VetoType::MissingRowData { key } => match split_numbered_row_field_key(key) {
42 Some((list, number, field)) => write!(
43 f,
44 "Required field '{field}' was not provided on row {number} of list '{list}'."
45 ),
46 None => write!(f, "Required data '{key}' was not provided."),
47 },
48 VetoType::UserDefined { message: Some(msg) } => write!(f, "{msg}"),
49 VetoType::UserDefined { message: None } => write!(f, "This rule was vetoed."),
50 VetoType::Computation { message } => write!(f, "{message}"),
51 }
52 }
53}
54
55fn split_numbered_row_field_key(key: &str) -> Option<(&str, &str, &str)> {
57 let (list_and_number, field) = key.rsplit_once('.')?;
58 let (list, number) = list_and_number.rsplit_once('.')?;
59 if number.is_empty() || !number.bytes().all(|b| b.is_ascii_digit()) {
60 return None;
61 }
62 if list.is_empty() || field.is_empty() {
63 return None;
64 }
65 Some((list, number, field))
66}
67
68impl VetoType {
69 #[must_use]
70 pub fn computation(message: impl Into<String>) -> Self {
71 VetoType::Computation {
72 message: message.into(),
73 }
74 }
75
76 #[must_use]
77 pub fn missing_data(data: DataPath, suggestion: Option<String>) -> Self {
78 VetoType::MissingData { data, suggestion }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Hash)]
84pub enum OperationResult {
85 Value(BoundValueKind),
87 Veto(VetoType),
89}
90
91impl OperationResult {
92 pub fn from_literal(value: LiteralValue) -> Self {
94 Self::Value(BoundValueKind::unbound(value.value))
95 }
96
97 pub fn from_bound(bound: BoundValueKind) -> Self {
98 Self::Value(bound)
99 }
100
101 pub fn vetoed(&self) -> bool {
102 matches!(self, OperationResult::Veto(_))
103 }
104
105 #[must_use]
107 pub fn is_missing_data(&self) -> bool {
108 matches!(
109 self,
110 OperationResult::Veto(VetoType::MissingData { .. } | VetoType::MissingRowData { .. })
111 )
112 }
113
114 #[must_use]
115 pub fn value(&self) -> Option<&BoundValueKind> {
116 match self {
117 OperationResult::Value(value) => Some(value),
118 OperationResult::Veto(_) => None,
119 }
120 }
121
122 #[must_use]
124 pub fn literal_value(&self) -> Option<LiteralValue> {
125 self.value().map(BoundValueKind::to_literal)
126 }
127
128 pub fn number(number: rust_decimal::Decimal) -> Self {
129 Self::from_literal(LiteralValue::number_from_decimal(number))
130 }
131
132 pub fn measure(
133 value: rust_decimal::Decimal,
134 unit: impl Into<String>,
135 lemma_type: Option<LemmaType>,
136 ) -> Self {
137 use crate::computation::rational::checked_mul;
138 let lemma_type = std::sync::Arc::new(
139 lemma_type.unwrap_or_else(|| LemmaType::primitive(TypeSpecification::measure())),
140 );
141 let unit_name = unit.into();
142 let rational = crate::literals::rational_from_parsed_decimal(value)
143 .expect("BUG: operation result measure must lift at boundary");
144 let factor = if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
145 units
146 .get(&unit_name)
147 .map(|u| u.require_literal_factor().clone())
148 .unwrap_or_else(|_| {
149 panic!(
150 "BUG: OperationResult::measure unit '{}' not declared on type",
151 unit_name
152 )
153 })
154 } else {
155 crate::computation::rational::rational_one()
156 };
157 let canonical = checked_mul(&rational, &factor)
158 .expect("BUG: measure canonicalization overflow in OperationResult::measure");
159 Self::from_bound(BoundValueKind {
160 value: crate::planning::semantics::ValueKind::Measure(canonical),
161 unit: Some(std::sync::Arc::from(unit_name)),
162 instance_factors: None,
163 })
164 }
165
166 pub fn text(text: impl Into<String>) -> Self {
167 Self::from_literal(LiteralValue::text(text.into()))
168 }
169
170 pub fn date(date: impl Into<SemanticDateTime>) -> Self {
171 Self::from_literal(LiteralValue::date(date.into()))
172 }
173
174 pub fn time(time: impl Into<SemanticTime>) -> Self {
175 Self::from_literal(LiteralValue::time(time.into()))
176 }
177
178 pub fn boolean(boolean: bool) -> Self {
179 Self::from_literal(LiteralValue::from_bool(boolean))
180 }
181
182 pub fn ratio(rational: rust_decimal::Decimal) -> Self {
183 Self::from_literal(LiteralValue::ratio_from_decimal(rational))
184 }
185
186 pub fn veto(veto: impl Into<String>) -> Self {
187 Self::Veto(VetoType::UserDefined {
188 message: Some(veto.into()),
189 })
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::VetoType;
196
197 #[test]
198 fn missing_row_data_display_names_list_row_and_field() {
199 let v = VetoType::MissingRowData {
200 key: "menu.2.price".to_string(),
201 };
202 assert_eq!(
203 v.to_string(),
204 "Required field 'price' was not provided on row 2 of list 'menu'."
205 );
206 }
207}