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(*first_canonical, first_unit.clone()))
161}
162
163fn value_kind_tag_for_type(spec: &TypeSpecification) -> &'static str {
164 match spec {
165 TypeSpecification::Boolean { .. } => "boolean",
166 TypeSpecification::Quantity { .. } => "quantity",
167 TypeSpecification::Number { .. } => "number",
168 TypeSpecification::NumberRange { .. }
169 | TypeSpecification::QuantityRange { .. }
170 | TypeSpecification::DateRange { .. }
171 | TypeSpecification::TimeRange { .. }
172 | TypeSpecification::RatioRange { .. } => "range",
173 TypeSpecification::Ratio { .. } => "ratio",
174 TypeSpecification::Text { .. } => "text",
175 TypeSpecification::Date { .. } => "date",
176 TypeSpecification::Time { .. } => "time",
177 TypeSpecification::Veto { .. } => "veto",
178 TypeSpecification::Undetermined => "undetermined",
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use crate::computation::rational::{decimal_to_rational, rational_one};
186 use crate::planning::semantics::{
187 primitive_number_arc, QuantityUnit, QuantityUnits, RatioUnit, RatioUnits, TypeExtends,
188 };
189
190 fn dummy_source() -> Source {
191 Source::new(
192 crate::parsing::source::SourceType::Volatile,
193 crate::planning::semantics::Span {
194 start: 0,
195 end: 0,
196 line: 1,
197 col: 1,
198 },
199 )
200 }
201
202 fn mass_quantity_type() -> Arc<LemmaType> {
203 Arc::new(LemmaType::new(
204 "Mass".to_string(),
205 TypeSpecification::Quantity {
206 minimum: None,
207 maximum: None,
208 decimals: None,
209 units: QuantityUnits::from(vec![
210 QuantityUnit {
211 name: "kilogram".to_string(),
212 factor: rational_one(),
213 derived_quantity_factors: Vec::new(),
214 decomposition: crate::literals::BaseQuantityVector::new(),
215 minimum: None,
216 maximum: None,
217 default_magnitude: None,
218 },
219 QuantityUnit {
220 name: "gram".to_string(),
221 factor: decimal_to_rational(Decimal::new(1, 3)).expect("factor"),
222 derived_quantity_factors: Vec::new(),
223 decomposition: crate::literals::BaseQuantityVector::new(),
224 minimum: None,
225 maximum: None,
226 default_magnitude: None,
227 },
228 ]),
229 traits: Vec::new(),
230 decomposition: None,
231 help: String::new(),
232 },
233 TypeExtends::Primitive,
234 ))
235 }
236
237 fn ratio_with_percent_type() -> Arc<LemmaType> {
238 Arc::new(LemmaType::new(
239 "Rate".to_string(),
240 TypeSpecification::Ratio {
241 minimum: None,
242 maximum: None,
243 decimals: None,
244 units: RatioUnits::from(vec![
245 RatioUnit {
246 name: "percent".to_string(),
247 value: decimal_to_rational(Decimal::new(100, 0)).expect("factor"),
248 minimum: None,
249 maximum: None,
250 default_magnitude: None,
251 },
252 RatioUnit {
253 name: "fraction".to_string(),
254 value: rational_one(),
255 minimum: None,
256 maximum: None,
257 default_magnitude: None,
258 },
259 ]),
260 help: String::new(),
261 },
262 TypeExtends::Primitive,
263 ))
264 }
265
266 #[test]
267 fn convenience_string_still_works() {
268 let ty = primitive_number_arc();
269 let lit = parse_data_value(
270 &DataValueInput::Convenience("42".to_string()),
271 ty,
272 &dummy_source(),
273 )
274 .unwrap();
275 assert!(matches!(lit.value, ValueKind::Number(_)));
276 }
277
278 #[test]
279 fn quantity_map_agreeing_units_canonicalize() {
280 let ty = mass_quantity_type();
281 let mut map = BTreeMap::new();
282 map.insert("kilogram".to_string(), "2".to_string());
283 map.insert("gram".to_string(), "2000".to_string());
284 let lit =
285 parse_data_value(&DataValueInput::QuantityMap(map), &ty, &dummy_source()).unwrap();
286 let ValueKind::Quantity(magnitude, signature) = &lit.value else {
287 panic!("expected quantity");
288 };
289 assert_eq!(*magnitude, rational_one() + rational_one());
290 assert_eq!(signature.len(), 1);
291 assert_eq!(signature[0].1, 1);
292 }
293
294 #[test]
295 fn quantity_map_disagreeing_units_rejected() {
296 let ty = mass_quantity_type();
297 let mut map = BTreeMap::new();
298 map.insert("kilogram".to_string(), "2".to_string());
299 map.insert("gram".to_string(), "3000".to_string());
300 let err =
301 parse_data_value(&DataValueInput::QuantityMap(map), &ty, &dummy_source()).unwrap_err();
302 assert!(err.message().contains("disagree"));
303 }
304
305 #[test]
306 fn ratio_map_percent_and_fraction_agree() {
307 let ty = ratio_with_percent_type();
308 let mut map = BTreeMap::new();
309 map.insert("percent".to_string(), "10".to_string());
310 map.insert("fraction".to_string(), "0.1".to_string());
311 let lit = parse_data_value(&DataValueInput::RatioMap(map), &ty, &dummy_source()).unwrap();
312 let ValueKind::Ratio(canonical, unit) = &lit.value else {
313 panic!("expected ratio");
314 };
315 assert_eq!(
316 *canonical,
317 decimal_to_rational(Decimal::new(1, 1)).expect("canonical")
318 );
319 assert!(unit.is_some());
320 }
321}