1use time::PrimitiveDateTime;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub enum Value {
22 Str(String),
24 Literal(String),
26}
27
28impl Value {
29 #[must_use]
31 pub fn text(&self) -> &str {
32 match self {
33 Value::Str(s) | Value::Literal(s) => s,
34 }
35 }
36}
37
38impl Default for Value {
39 fn default() -> Self {
40 Value::Str(String::new())
41 }
42}
43
44pub trait FromField: Sized {
59 fn from_field(text: &str) -> Option<Self>;
61}
62
63impl FromField for String {
64 fn from_field(text: &str) -> Option<Self> {
65 Some(text.to_owned())
66 }
67}
68
69impl FromField for f64 {
70 fn from_field(text: &str) -> Option<Self> {
71 text.trim().parse().ok()
72 }
73}
74
75impl FromField for i64 {
76 fn from_field(text: &str) -> Option<Self> {
77 lenient_int(text)
78 }
79}
80
81impl FromField for u32 {
82 fn from_field(text: &str) -> Option<Self> {
83 u32::try_from(lenient_int(text)?).ok()
84 }
85}
86
87impl FromField for bool {
88 fn from_field(text: &str) -> Option<Self> {
89 match text.trim() {
90 "T" | "t" | "1" => Some(true),
91 "F" | "f" | "0" => Some(false),
92 s if s.eq_ignore_ascii_case("true") => Some(true),
93 s if s.eq_ignore_ascii_case("false") => Some(false),
94 _ => None,
95 }
96 }
97}
98
99impl FromField for PrimitiveDateTime {
100 fn from_field(text: &str) -> Option<Self> {
101 parse_datetime(text)
102 }
103}
104
105fn lenient_int(text: &str) -> Option<i64> {
107 let t = text.trim();
108 if let Ok(n) = t.parse::<i64>() {
109 return Some(n);
110 }
111 let f = t.parse::<f64>().ok()?;
112 if f.fract() == 0.0 && f.is_finite() && f.abs() < 9.007_199_254_740_992e15 {
113 Some(f as i64)
115 } else {
116 None
117 }
118}
119
120fn parse_datetime(text: &str) -> Option<PrimitiveDateTime> {
123 let s = text.trim().trim_end_matches('Z');
124 for pat in [
125 "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond]",
126 "[year]-[month]-[day]T[hour]:[minute]:[second]",
127 ] {
128 if let Ok(fmt) = time::format_description::parse_borrowed::<2>(pat) {
129 if let Ok(dt) = PrimitiveDateTime::parse(s, &fmt) {
130 return Some(dt);
131 }
132 }
133 }
134 if let Ok(fmt) = time::format_description::parse_borrowed::<2>("[year]-[month]-[day]") {
135 if let Ok(date) = time::Date::parse(s, &fmt) {
136 return Some(PrimitiveDateTime::new(date, time::Time::MIDNIGHT));
137 }
138 }
139 None
140}
141
142pub trait IntoValue {
153 fn into_value(self) -> Value;
155}
156
157impl IntoValue for Value {
158 fn into_value(self) -> Value {
159 self
160 }
161}
162
163impl IntoValue for &str {
164 fn into_value(self) -> Value {
165 Value::Str(self.to_owned())
166 }
167}
168
169impl IntoValue for String {
170 fn into_value(self) -> Value {
171 Value::Str(self)
172 }
173}
174
175impl IntoValue for f64 {
176 fn into_value(self) -> Value {
177 Value::Literal(format_f64(self))
178 }
179}
180
181impl IntoValue for i64 {
182 fn into_value(self) -> Value {
183 Value::Literal(self.to_string())
184 }
185}
186
187impl IntoValue for u32 {
188 fn into_value(self) -> Value {
189 Value::Literal(self.to_string())
190 }
191}
192
193impl IntoValue for bool {
194 fn into_value(self) -> Value {
195 Value::Literal(if self { "T" } else { "F" }.to_owned())
196 }
197}
198
199#[derive(Debug, Clone)]
211pub struct Literal(pub String);
212
213impl IntoValue for Literal {
214 fn into_value(self) -> Value {
215 Value::Literal(self.0)
216 }
217}
218
219#[derive(Debug, Clone, Copy)]
230pub struct Fixed(pub f64, pub usize);
231
232impl IntoValue for Fixed {
233 fn into_value(self) -> Value {
234 Value::Literal(format!("{:.*}", self.1, self.0))
235 }
236}
237
238#[derive(Debug, Clone, Copy)]
250pub struct Sci(pub f64, pub usize);
251
252impl IntoValue for Sci {
253 fn into_value(self) -> Value {
254 let digits = self.1.saturating_sub(1);
255 Value::Literal(format!("{:.*e}", digits, self.0).replace('e', "E"))
256 }
257}
258
259fn format_f64(v: f64) -> String {
262 let s = format!("{v}");
263 if s.contains(['.', 'e', 'E']) || !v.is_finite() {
264 s
266 } else {
267 format!("{s}.0")
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn from_field_string_is_verbatim() {
277 assert_eq!(String::from_field(" M31 "), Some(" M31 ".to_owned()));
278 }
279
280 #[test]
281 fn from_field_f64() {
282 assert_eq!(f64::from_field("300"), Some(300.0));
283 assert_eq!(f64::from_field(" 3.5 "), Some(3.5));
284 assert_eq!(f64::from_field("1e3"), Some(1000.0));
285 assert_eq!(f64::from_field("abc"), None);
286 }
287
288 #[test]
289 fn from_field_i64_is_lenient() {
290 assert_eq!(i64::from_field("20"), Some(20));
291 assert_eq!(i64::from_field("20.0"), Some(20));
292 assert_eq!(i64::from_field(" -7 "), Some(-7));
293 assert_eq!(i64::from_field("1e10"), Some(10_000_000_000));
294 assert_eq!(i64::from_field("20.5"), None);
295 assert_eq!(i64::from_field("1e300"), None); assert_eq!(i64::from_field("inf"), None);
297 assert_eq!(i64::from_field("abc"), None);
298 }
299
300 #[test]
301 fn from_field_u32() {
302 assert_eq!(u32::from_field("2"), Some(2));
303 assert_eq!(u32::from_field("2.0"), Some(2));
304 assert_eq!(u32::from_field("-1"), None);
305 assert_eq!(u32::from_field("4294967296"), None); }
307
308 #[test]
309 fn from_field_bool_spellings() {
310 for t in ["T", "t", "1", "true", "TRUE", " T "] {
311 assert_eq!(bool::from_field(t), Some(true), "{t:?}");
312 }
313 for f in ["F", "f", "0", "false", "FALSE"] {
314 assert_eq!(bool::from_field(f), Some(false), "{f:?}");
315 }
316 assert_eq!(bool::from_field("yes"), None);
317 assert_eq!(bool::from_field(""), None);
318 }
319
320 #[test]
321 fn from_field_datetime_forms() {
322 let dt = PrimitiveDateTime::from_field("2026-07-11T22:15:03").unwrap();
323 assert_eq!((dt.year(), dt.hour(), dt.second()), (2026, 22, 3));
324
325 let frac = PrimitiveDateTime::from_field("2026-07-11T22:15:03.25").unwrap();
326 assert_eq!(frac.millisecond(), 250);
327
328 assert!(PrimitiveDateTime::from_field("2026-07-11T22:15:03Z").is_some());
330
331 let date = PrimitiveDateTime::from_field("2026-07-11").unwrap();
333 assert_eq!((date.hour(), date.minute()), (0, 0));
334
335 assert_eq!(PrimitiveDateTime::from_field("2026-13-40T00:00:00"), None);
336 assert_eq!(PrimitiveDateTime::from_field("not a date"), None);
337 }
338
339 #[test]
340 fn into_value_kind_selection() {
341 assert_eq!("s".into_value(), Value::Str("s".to_owned()));
342 assert_eq!(String::from("s").into_value(), Value::Str("s".to_owned()));
343 assert_eq!(100_i64.into_value(), Value::Literal("100".to_owned()));
344 assert_eq!(2_u32.into_value(), Value::Literal("2".to_owned()));
345 assert_eq!(true.into_value(), Value::Literal("T".to_owned()));
346 assert_eq!(false.into_value(), Value::Literal("F".to_owned()));
347 let v = Value::Literal("x".to_owned());
348 assert_eq!(v.clone().into_value(), v);
349 }
350
351 #[test]
352 fn f64_formatting_is_normalized() {
353 assert_eq!(300.0.into_value(), Value::Literal("300.0".to_owned()));
354 assert_eq!(0.5.into_value(), Value::Literal("0.5".to_owned()));
355 let huge = 1e300.into_value();
358 assert_eq!(f64::from_field(huge.text()), Some(1e300));
359 assert!(huge.text().ends_with(".0"));
360 assert_eq!(f64::INFINITY.into_value(), Value::Literal("inf".to_owned()));
361 assert_eq!(f64::NAN.into_value(), Value::Literal("NaN".to_owned()));
362 }
363
364 #[test]
365 fn controlled_formatting_wrappers() {
366 assert_eq!(
367 Fixed(300.0, 2).into_value(),
368 Value::Literal("300.00".to_owned())
369 );
370 assert_eq!(
371 Sci(1234.5, 3).into_value(),
372 Value::Literal("1.23E3".to_owned())
373 );
374 assert_eq!(
375 Sci(0.00012345, 2).into_value(),
376 Value::Literal("1.2E-4".to_owned())
377 );
378 assert_eq!(
379 Literal("0x1F".to_owned()).into_value(),
380 Value::Literal("0x1F".to_owned())
381 );
382 }
383
384 #[test]
385 fn value_text_and_default() {
386 assert_eq!(Value::Str("a".to_owned()).text(), "a");
387 assert_eq!(Value::Literal("1".to_owned()).text(), "1");
388 assert_eq!(Value::default(), Value::Str(String::new()));
389 }
390}