1use crate::planning::semantics::{
2 number_with_unit_to_value_kind, parse_value_from_string, parser_value_to_value_kind, LemmaType,
3 LiteralValue, Source, TypeSpecification, ValueKind,
4};
5use crate::Error;
6use rust_decimal::Decimal;
7use std::collections::BTreeMap;
8use std::str::FromStr;
9use std::sync::Arc;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum DataValueInput {
14 Convenience(String),
15 Boolean(bool),
16 QuantityMap(BTreeMap<String, String>),
17 RatioMap(BTreeMap<String, String>),
18}
19
20impl DataValueInput {
21 pub fn convenience(value: impl Into<String>) -> Self {
22 Self::Convenience(value.into())
23 }
24}
25
26pub fn parse_data_value(
27 input: &DataValueInput,
28 lemma_type: &Arc<LemmaType>,
29 source: &Source,
30) -> Result<LiteralValue, Error> {
31 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
32 let type_spec = &lemma_type.specifications;
33
34 let kind = match (input, type_spec) {
35 (DataValueInput::Convenience(s), _) => {
36 let parsed = parse_value_from_string(s, type_spec, source)?;
37 parser_value_to_value_kind(&parsed, type_spec).map_err(to_err)?
38 }
39 (DataValueInput::Boolean(b), TypeSpecification::Boolean { .. }) => ValueKind::Boolean(*b),
40 (DataValueInput::Boolean(_), _) => {
41 return Err(to_err(format!(
42 "boolean input is only valid for boolean data, not {}",
43 value_kind_tag_for_type(type_spec)
44 )));
45 }
46 (DataValueInput::QuantityMap(map), TypeSpecification::Quantity { .. }) => {
47 quantity_from_unit_map(map, lemma_type.as_ref()).map_err(to_err)?
48 }
49 (
50 DataValueInput::QuantityMap(map) | DataValueInput::RatioMap(map),
51 TypeSpecification::Ratio { .. },
52 ) => ratio_from_unit_map(map, lemma_type.as_ref()).map_err(to_err)?,
53 (DataValueInput::QuantityMap(_), _) => {
54 return Err(to_err(format!(
55 "quantity unit map is only valid for quantity data, not {}",
56 value_kind_tag_for_type(type_spec)
57 )));
58 }
59 (DataValueInput::RatioMap(_), _) => {
60 return Err(to_err(format!(
61 "ratio unit map is only valid for ratio data, not {}",
62 value_kind_tag_for_type(type_spec)
63 )));
64 }
65 };
66
67 Ok(LiteralValue {
68 value: kind,
69 lemma_type: Arc::clone(lemma_type),
70 })
71}
72
73fn quantity_from_unit_map(
74 map: &BTreeMap<String, String>,
75 lemma_type: &LemmaType,
76) -> Result<ValueKind, String> {
77 if map.is_empty() {
78 return Err("quantity input map must contain at least one unit key".to_string());
79 }
80 if lemma_type
81 .quantity_unit_names()
82 .is_none_or(|names| names.is_empty())
83 {
84 unreachable!("BUG: quantity type has no units at data input");
85 }
86
87 let mut kinds: Vec<ValueKind> = Vec::with_capacity(map.len());
88 for (unit_name, mag_str) in map {
89 let magnitude = Decimal::from_str(mag_str.trim())
90 .map_err(|error| format!("invalid decimal '{mag_str}': {error}"))?;
91 kinds.push(number_with_unit_to_value_kind(
92 magnitude, unit_name, lemma_type,
93 )?);
94 }
95
96 let first = kinds.first().expect("BUG: map non-empty");
97 let ValueKind::Quantity(first_magnitude, first_signature) = first else {
98 return Err("expected quantity value".to_string());
99 };
100 if first_signature.len() != 1 || first_signature[0].1 != 1 {
101 return Err(
102 "quantity map produced a compound signature; use a convenience string instead"
103 .to_string(),
104 );
105 }
106 for kind in kinds.iter().skip(1) {
107 let ValueKind::Quantity(magnitude, signature) = kind else {
108 return Err("expected quantity value".to_string());
109 };
110 if signature.len() != 1 || signature[0].1 != 1 {
111 return Err(
112 "quantity map produced a compound signature; use a convenience string instead"
113 .to_string(),
114 );
115 }
116 if magnitude != first_magnitude {
117 return Err(
118 "quantity unit map values disagree when converted to a common basis".to_string(),
119 );
120 }
121 }
122 Ok(first.clone())
123}
124
125fn ratio_from_unit_map(
126 map: &BTreeMap<String, String>,
127 lemma_type: &LemmaType,
128) -> Result<ValueKind, String> {
129 if map.is_empty() {
130 return Err("ratio input map must contain at least one unit key".to_string());
131 }
132 match &lemma_type.specifications {
133 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {}
134 _ => unreachable!("BUG: ratio type has no units at data input"),
135 }
136
137 let mut kinds: Vec<ValueKind> = Vec::with_capacity(map.len());
138 for (unit_name, mag_str) in map {
139 let magnitude = Decimal::from_str(mag_str.trim())
140 .map_err(|error| format!("invalid decimal '{mag_str}': {error}"))?;
141 kinds.push(number_with_unit_to_value_kind(
142 magnitude, unit_name, lemma_type,
143 )?);
144 }
145
146 let first = kinds.first().expect("BUG: map non-empty");
147 let ValueKind::Ratio(first_canonical, first_unit) = first else {
148 return Err("expected ratio value".to_string());
149 };
150 for kind in kinds.iter().skip(1) {
151 let ValueKind::Ratio(canonical, _) = kind else {
152 return Err("expected ratio value".to_string());
153 };
154 if canonical != first_canonical {
155 return Err(
156 "ratio unit map values disagree when converted to a common basis".to_string(),
157 );
158 }
159 }
160 Ok(ValueKind::Ratio(
161 first_canonical.clone(),
162 first_unit.clone(),
163 ))
164}
165
166fn value_kind_tag_for_type(spec: &TypeSpecification) -> &'static str {
167 match spec {
168 TypeSpecification::Boolean { .. } => "boolean",
169 TypeSpecification::Quantity { .. } => "quantity",
170 TypeSpecification::Number { .. } => "number",
171 TypeSpecification::NumberRange { .. }
172 | TypeSpecification::QuantityRange { .. }
173 | TypeSpecification::DateRange { .. }
174 | TypeSpecification::TimeRange { .. }
175 | TypeSpecification::RatioRange { .. } => "range",
176 TypeSpecification::Ratio { .. } => "ratio",
177 TypeSpecification::Text { .. } => "text",
178 TypeSpecification::Date { .. } => "date",
179 TypeSpecification::Time { .. } => "time",
180 TypeSpecification::Veto { .. } => "veto",
181 TypeSpecification::Undetermined => "undetermined",
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use crate::computation::rational::{decimal_to_rational, rational_new, rational_one};
189 use crate::planning::semantics::{
190 primitive_number_arc, QuantityUnit, QuantityUnits, RatioUnit, RatioUnits, TypeExtends,
191 };
192
193 fn dummy_source() -> Source {
194 Source::new(
195 crate::parsing::source::SourceType::Volatile,
196 crate::planning::semantics::Span {
197 start: 0,
198 end: 0,
199 line: 1,
200 col: 1,
201 },
202 )
203 }
204
205 fn mass_quantity_type() -> Arc<LemmaType> {
206 Arc::new(LemmaType::new(
207 "Mass".to_string(),
208 TypeSpecification::Quantity {
209 minimum: None,
210 maximum: None,
211 decimals: None,
212 units: QuantityUnits::from(vec![
213 QuantityUnit {
214 name: "kilogram".to_string(),
215 factor: rational_one(),
216 derived_quantity_factors: Vec::new(),
217 decomposition: crate::literals::BaseQuantityVector::new(),
218 minimum: None,
219 maximum: None,
220 default_magnitude: None,
221 },
222 QuantityUnit {
223 name: "gram".to_string(),
224 factor: decimal_to_rational(Decimal::new(1, 3)).expect("factor"),
225 derived_quantity_factors: Vec::new(),
226 decomposition: crate::literals::BaseQuantityVector::new(),
227 minimum: None,
228 maximum: None,
229 default_magnitude: None,
230 },
231 ]),
232 traits: Vec::new(),
233 decomposition: None,
234 help: String::new(),
235 },
236 TypeExtends::Primitive,
237 ))
238 }
239
240 fn ratio_with_percent_type() -> Arc<LemmaType> {
241 Arc::new(LemmaType::new(
242 "Rate".to_string(),
243 TypeSpecification::Ratio {
244 minimum: None,
245 maximum: None,
246 decimals: None,
247 units: RatioUnits::from(vec![
248 RatioUnit {
249 name: "percent".to_string(),
250 value: decimal_to_rational(Decimal::new(100, 0)).expect("factor"),
251 minimum: None,
252 maximum: None,
253 default_magnitude: None,
254 },
255 RatioUnit {
256 name: "fraction".to_string(),
257 value: rational_one(),
258 minimum: None,
259 maximum: None,
260 default_magnitude: None,
261 },
262 ]),
263 help: String::new(),
264 },
265 TypeExtends::Primitive,
266 ))
267 }
268
269 #[test]
270 fn convenience_string_still_works() {
271 let ty = primitive_number_arc();
272 let lit = parse_data_value(
273 &DataValueInput::Convenience("42".to_string()),
274 ty,
275 &dummy_source(),
276 )
277 .unwrap();
278 assert!(matches!(lit.value, ValueKind::Number(_)));
279 }
280
281 #[test]
282 fn quantity_map_agreeing_units_canonicalize() {
283 let ty = mass_quantity_type();
284 let mut map = BTreeMap::new();
285 map.insert("kilogram".to_string(), "2".to_string());
286 map.insert("gram".to_string(), "2000".to_string());
287 let lit =
288 parse_data_value(&DataValueInput::QuantityMap(map), &ty, &dummy_source()).unwrap();
289 let ValueKind::Quantity(magnitude, signature) = &lit.value else {
290 panic!("expected quantity");
291 };
292 assert_eq!(magnitude, &rational_new(2, 1));
293 assert_eq!(signature.len(), 1);
294 assert_eq!(signature[0].1, 1);
295 }
296
297 #[test]
298 fn quantity_map_disagreeing_units_rejected() {
299 let ty = mass_quantity_type();
300 let mut map = BTreeMap::new();
301 map.insert("kilogram".to_string(), "2".to_string());
302 map.insert("gram".to_string(), "3000".to_string());
303 let err =
304 parse_data_value(&DataValueInput::QuantityMap(map), &ty, &dummy_source()).unwrap_err();
305 assert!(err.message().contains("disagree"));
306 }
307
308 #[test]
309 fn ratio_map_percent_and_fraction_agree() {
310 let ty = ratio_with_percent_type();
311 let mut map = BTreeMap::new();
312 map.insert("percent".to_string(), "10".to_string());
313 map.insert("fraction".to_string(), "0.1".to_string());
314 let lit = parse_data_value(&DataValueInput::RatioMap(map), &ty, &dummy_source()).unwrap();
315 let ValueKind::Ratio(canonical, unit) = &lit.value else {
316 panic!("expected ratio");
317 };
318 assert_eq!(
319 *canonical,
320 decimal_to_rational(Decimal::new(1, 1)).expect("canonical")
321 );
322 assert!(unit.is_some());
323 }
324}