1use time::PrimitiveDateTime;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub enum Value {
26 Str(String),
28 Literal(String),
30}
31
32impl Value {
33 #[must_use]
35 pub fn text(&self) -> &str {
36 match self {
37 Value::Str(s) | Value::Literal(s) => s,
38 }
39 }
40}
41
42impl Default for Value {
43 fn default() -> Self {
44 Value::Str(String::new())
45 }
46}
47
48pub trait FromField: Sized {
63 fn from_field(text: &str) -> Option<Self>;
65}
66
67impl FromField for String {
68 fn from_field(text: &str) -> Option<Self> {
69 Some(text.to_owned())
70 }
71}
72
73impl FromField for f64 {
74 fn from_field(text: &str) -> Option<Self> {
75 text.trim().parse().ok()
76 }
77}
78
79impl FromField for i64 {
80 fn from_field(text: &str) -> Option<Self> {
81 lenient_int(text)
82 }
83}
84
85impl FromField for u32 {
86 fn from_field(text: &str) -> Option<Self> {
87 u32::try_from(lenient_int(text)?).ok()
88 }
89}
90
91impl FromField for bool {
92 fn from_field(text: &str) -> Option<Self> {
93 match text.trim() {
94 "T" | "t" | "1" => Some(true),
95 "F" | "f" | "0" => Some(false),
96 s if s.eq_ignore_ascii_case("true") => Some(true),
97 s if s.eq_ignore_ascii_case("false") => Some(false),
98 _ => None,
99 }
100 }
101}
102
103impl FromField for PrimitiveDateTime {
104 fn from_field(text: &str) -> Option<Self> {
105 parse_datetime(text)
106 }
107}
108
109fn lenient_int(text: &str) -> Option<i64> {
111 let t = text.trim();
112 if let Ok(n) = t.parse::<i64>() {
113 return Some(n);
114 }
115 let f = t.parse::<f64>().ok()?;
116 if f.fract() == 0.0 && f.is_finite() && f.abs() < 9.007_199_254_740_992e15 {
117 Some(f as i64)
119 } else {
120 None
121 }
122}
123
124fn parse_datetime(text: &str) -> Option<PrimitiveDateTime> {
130 skymath::parse_date_obs(text).ok()
131}
132
133pub trait IntoValue {
144 fn into_value(self) -> Value;
146}
147
148impl IntoValue for Value {
149 fn into_value(self) -> Value {
150 self
151 }
152}
153
154impl IntoValue for &str {
155 fn into_value(self) -> Value {
156 Value::Str(self.to_owned())
157 }
158}
159
160impl IntoValue for String {
161 fn into_value(self) -> Value {
162 Value::Str(self)
163 }
164}
165
166impl IntoValue for f64 {
167 fn into_value(self) -> Value {
168 Value::Literal(format_f64(self))
169 }
170}
171
172impl IntoValue for i64 {
173 fn into_value(self) -> Value {
174 Value::Literal(self.to_string())
175 }
176}
177
178impl IntoValue for u32 {
179 fn into_value(self) -> Value {
180 Value::Literal(self.to_string())
181 }
182}
183
184impl IntoValue for bool {
185 fn into_value(self) -> Value {
186 Value::Literal(if self { "T" } else { "F" }.to_owned())
187 }
188}
189
190#[derive(Debug, Clone)]
202pub struct Literal(pub String);
203
204impl IntoValue for Literal {
205 fn into_value(self) -> Value {
206 Value::Literal(self.0)
207 }
208}
209
210#[derive(Debug, Clone, Copy)]
221pub struct Fixed(pub f64, pub usize);
222
223impl IntoValue for Fixed {
224 fn into_value(self) -> Value {
225 Value::Literal(format!("{:.*}", self.1, self.0))
226 }
227}
228
229#[derive(Debug, Clone, Copy)]
241pub struct Sci(pub f64, pub usize);
242
243impl IntoValue for Sci {
244 fn into_value(self) -> Value {
245 let digits = self.1.saturating_sub(1);
246 Value::Literal(format!("{:.*e}", digits, self.0).replace('e', "E"))
247 }
248}
249
250fn format_f64(v: f64) -> String {
253 let s = format!("{v}");
254 if s.contains(['.', 'e', 'E']) || !v.is_finite() {
255 s
257 } else {
258 format!("{s}.0")
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn from_field_string_is_verbatim() {
268 assert_eq!(String::from_field(" M31 "), Some(" M31 ".to_owned()));
269 }
270
271 #[test]
272 fn from_field_f64() {
273 assert_eq!(f64::from_field("300"), Some(300.0));
274 assert_eq!(f64::from_field(" 3.5 "), Some(3.5));
275 assert_eq!(f64::from_field("1e3"), Some(1000.0));
276 assert_eq!(f64::from_field("abc"), None);
277 }
278
279 #[test]
280 fn from_field_i64_is_lenient() {
281 assert_eq!(i64::from_field("20"), Some(20));
282 assert_eq!(i64::from_field("20.0"), Some(20));
283 assert_eq!(i64::from_field(" -7 "), Some(-7));
284 assert_eq!(i64::from_field("1e10"), Some(10_000_000_000));
285 assert_eq!(i64::from_field("20.5"), None);
286 assert_eq!(i64::from_field("1e300"), None); assert_eq!(i64::from_field("inf"), None);
288 assert_eq!(i64::from_field("abc"), None);
289 }
290
291 #[test]
292 fn from_field_u32() {
293 assert_eq!(u32::from_field("2"), Some(2));
294 assert_eq!(u32::from_field("2.0"), Some(2));
295 assert_eq!(u32::from_field("-1"), None);
296 assert_eq!(u32::from_field("4294967296"), None); }
298
299 #[test]
300 fn from_field_bool_spellings() {
301 for t in ["T", "t", "1", "true", "TRUE", " T "] {
302 assert_eq!(bool::from_field(t), Some(true), "{t:?}");
303 }
304 for f in ["F", "f", "0", "false", "FALSE"] {
305 assert_eq!(bool::from_field(f), Some(false), "{f:?}");
306 }
307 assert_eq!(bool::from_field("yes"), None);
308 assert_eq!(bool::from_field(""), None);
309 }
310
311 #[test]
312 fn from_field_datetime_forms() {
313 let dt = PrimitiveDateTime::from_field("2026-07-11T22:15:03").unwrap();
314 assert_eq!((dt.year(), dt.hour(), dt.second()), (2026, 22, 3));
315
316 let frac = PrimitiveDateTime::from_field("2026-07-11T22:15:03.25").unwrap();
317 assert_eq!(frac.millisecond(), 250);
318
319 assert!(PrimitiveDateTime::from_field("2026-07-11T22:15:03Z").is_some());
321
322 let date = PrimitiveDateTime::from_field("2026-07-11").unwrap();
324 assert_eq!((date.hour(), date.minute()), (0, 0));
325
326 assert_eq!(PrimitiveDateTime::from_field("2026-13-40T00:00:00"), None);
327 assert_eq!(PrimitiveDateTime::from_field("not a date"), None);
328 }
329
330 #[test]
331 fn into_value_kind_selection() {
332 assert_eq!("s".into_value(), Value::Str("s".to_owned()));
333 assert_eq!(String::from("s").into_value(), Value::Str("s".to_owned()));
334 assert_eq!(100_i64.into_value(), Value::Literal("100".to_owned()));
335 assert_eq!(2_u32.into_value(), Value::Literal("2".to_owned()));
336 assert_eq!(true.into_value(), Value::Literal("T".to_owned()));
337 assert_eq!(false.into_value(), Value::Literal("F".to_owned()));
338 let v = Value::Literal("x".to_owned());
339 assert_eq!(v.clone().into_value(), v);
340 }
341
342 #[test]
343 fn f64_formatting_is_normalized() {
344 assert_eq!(300.0.into_value(), Value::Literal("300.0".to_owned()));
345 assert_eq!(0.5.into_value(), Value::Literal("0.5".to_owned()));
346 let huge = 1e300.into_value();
349 assert_eq!(f64::from_field(huge.text()), Some(1e300));
350 assert!(huge.text().ends_with(".0"));
351 assert_eq!(f64::INFINITY.into_value(), Value::Literal("inf".to_owned()));
352 assert_eq!(f64::NAN.into_value(), Value::Literal("NaN".to_owned()));
353 }
354
355 #[test]
356 fn controlled_formatting_wrappers() {
357 assert_eq!(
358 Fixed(300.0, 2).into_value(),
359 Value::Literal("300.00".to_owned())
360 );
361 assert_eq!(
362 Sci(1234.5, 3).into_value(),
363 Value::Literal("1.23E3".to_owned())
364 );
365 assert_eq!(
366 Sci(0.00012345, 2).into_value(),
367 Value::Literal("1.2E-4".to_owned())
368 );
369 assert_eq!(
370 Literal("0x1F".to_owned()).into_value(),
371 Value::Literal("0x1F".to_owned())
372 );
373 }
374
375 #[test]
376 fn value_text_and_default() {
377 assert_eq!(Value::Str("a".to_owned()).text(), "a");
378 assert_eq!(Value::Literal("1".to_owned()).text(), "1");
379 assert_eq!(Value::default(), Value::Str(String::new()));
380 }
381}