1use crate::record::{Record, Value};
4
5pub trait FromCard: Sized {
17 fn from_card(record: &Record) -> Option<Self>;
19}
20
21pub trait IntoValue {
32 fn into_value(self) -> Value;
34}
35
36impl FromCard for String {
39 fn from_card(record: &Record) -> Option<Self> {
40 record.value_text().map(str::to_string)
41 }
42}
43
44impl FromCard for bool {
45 fn from_card(record: &Record) -> Option<Self> {
46 match record.value_text()?.trim() {
47 "T" | "1" => Some(true),
48 "F" | "0" => Some(false),
49 _ => None,
50 }
51 }
52}
53
54impl FromCard for f64 {
55 fn from_card(record: &Record) -> Option<Self> {
56 record.value_text().and_then(parse_f64)
57 }
58}
59
60impl FromCard for f32 {
61 fn from_card(record: &Record) -> Option<Self> {
62 record.value_text().and_then(parse_f64).map(|v| v as f32)
63 }
64}
65
66macro_rules! from_card_int {
67 ($($t:ty),* $(,)?) => {$(
68 impl FromCard for $t {
69 fn from_card(record: &Record) -> Option<Self> {
70 record.value_text().and_then(lenient_i128).and_then(|v| <$t>::try_from(v).ok())
71 }
72 }
73 )*};
74}
75from_card_int!(i64, i32, u64, u32, i16, u16, i8, u8);
76
77impl FromCard for time::PrimitiveDateTime {
78 fn from_card(record: &Record) -> Option<Self> {
79 record.value_text().and_then(crate::dates::parse_datetime)
80 }
81}
82
83impl IntoValue for &str {
86 fn into_value(self) -> Value {
87 Value::Str(self.to_string())
88 }
89}
90
91impl IntoValue for String {
92 fn into_value(self) -> Value {
93 Value::Str(self)
94 }
95}
96
97impl IntoValue for &String {
98 fn into_value(self) -> Value {
99 Value::Str(self.clone())
100 }
101}
102
103impl IntoValue for bool {
104 fn into_value(self) -> Value {
105 Value::Literal(if self { "T" } else { "F" }.to_string())
106 }
107}
108
109macro_rules! into_value_int {
110 ($($t:ty),* $(,)?) => {$(
111 impl IntoValue for $t {
112 fn into_value(self) -> Value { Value::Literal(self.to_string()) }
113 }
114 )*};
115}
116into_value_int!(i64, i32, u64, u32, i16, u16, i8, u8, usize, isize);
117
118impl IntoValue for f64 {
119 fn into_value(self) -> Value {
120 Value::Literal(format_f64(self))
121 }
122}
123
124impl IntoValue for f32 {
125 fn into_value(self) -> Value {
126 Value::Literal(format_f64(self as f64))
127 }
128}
129
130pub struct Literal<S: Into<String>>(pub S);
142
143impl<S: Into<String>> IntoValue for Literal<S> {
144 fn into_value(self) -> Value {
145 Value::Literal(self.0.into())
146 }
147}
148
149pub struct Fixed(pub f64, pub u8);
160
161impl IntoValue for Fixed {
162 fn into_value(self) -> Value {
163 Value::Literal(format!("{:.*}", self.1 as usize, self.0))
164 }
165}
166
167pub struct Sci(pub f64, pub u8);
178
179impl IntoValue for Sci {
180 fn into_value(self) -> Value {
181 let prec = self.1.saturating_sub(1) as usize;
182 Value::Literal(format!("{:.*E}", prec, self.0))
183 }
184}
185
186pub fn parse_f64(s: &str) -> Option<f64> {
197 let t = s.trim();
198 if t.contains(['D', 'd']) {
199 t.replace(['D', 'd'], "E").parse().ok()
200 } else {
201 t.parse().ok()
202 }
203}
204
205pub fn parse_i64(s: &str) -> Option<i64> {
214 lenient_i128(s).and_then(|v| i64::try_from(v).ok())
215}
216
217fn lenient_i128(s: &str) -> Option<i128> {
218 let t = s.trim();
219 if let Ok(i) = t.parse::<i128>() {
220 return Some(i);
221 }
222 let f = parse_f64(t)?;
223 (f.fract() == 0.0 && f.is_finite()).then_some(f as i128)
224}
225
226pub(crate) fn format_f64(x: f64) -> String {
228 if !x.is_finite() {
229 return x.to_string();
230 }
231 let mut s = format!("{x}");
232 if s.len() > 20 {
233 s = format!("{x:E}");
236 } else if !s.contains('.') {
237 s.push_str(".0");
238 }
239 s
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use crate::record::Record;
246
247 fn literal(token: &str) -> Record {
248 Record::value("K", Value::Literal(token.to_string()), None)
249 }
250
251 #[test]
252 fn format_f64_normalizes() {
253 assert_eq!(format_f64(120.0), "120.0");
254 assert_eq!(format_f64(0.5), "0.5");
255 assert_eq!(format_f64(-0.0), "-0.0");
256 assert_eq!(format_f64(f64::NAN), "NaN");
257 assert_eq!(format_f64(1e300), "1E300");
259 assert_eq!(format_f64(1e-300), "1E-300");
260 assert_eq!(format_f64(f64::MIN_POSITIVE), "2.2250738585072014E-308");
261 for x in [0.1, 1.0 / 3.0, 6.02214076e23, 1e300, -1e-300, f64::MAX] {
264 let s = format_f64(x);
265 assert!(s.len() <= 24, "token too long: {s}");
266 assert_eq!(s.parse::<f64>().unwrap(), x);
267 }
268 }
269
270 #[test]
271 fn parse_f64_accepts_fortran_exponent() {
272 assert_eq!(parse_f64("1.5D3"), Some(1500.0));
273 assert_eq!(parse_f64("1.5d-2"), Some(0.015));
274 assert_eq!(parse_f64(" 2.0 "), Some(2.0));
275 assert_eq!(parse_f64("abc"), None);
276 }
277
278 #[test]
279 fn parse_i64_is_lenient_but_rejects_fractions() {
280 assert_eq!(parse_i64("20.0"), Some(20));
281 assert_eq!(parse_i64("20.5"), None);
282 assert_eq!(parse_i64("1e3"), Some(1000));
283 assert_eq!(parse_i64("170141183460469231731687303715884105727"), None);
285 assert_eq!(parse_i64("inf"), None);
286 }
287
288 #[test]
289 fn int_narrowing_fails_closed() {
290 assert_eq!(u8::from_card(&literal("300")), None);
291 assert_eq!(u8::from_card(&literal("255")), Some(255));
292 assert_eq!(u32::from_card(&literal("-1")), None);
293 assert_eq!(i16::from_card(&literal("-32768")), Some(-32768));
294 }
295
296 #[test]
297 fn bool_from_card_variants() {
298 assert_eq!(bool::from_card(&literal("T")), Some(true));
299 assert_eq!(bool::from_card(&literal("1")), Some(true));
300 assert_eq!(bool::from_card(&literal("F")), Some(false));
301 assert_eq!(bool::from_card(&literal("0")), Some(false));
302 assert_eq!(bool::from_card(&literal("yes")), None);
303 }
304
305 #[test]
306 fn string_from_card_reads_literal_token() {
307 assert_eq!(
308 String::from_card(&literal("120.0")).as_deref(),
309 Some("120.0")
310 );
311 let empty = Record::value("K", Value::Str(String::new()), None);
313 assert_eq!(String::from_card(&empty), None);
314 }
315
316 #[test]
317 fn into_value_wrappers() {
318 assert_eq!(true.into_value(), Value::Literal("T".to_string()));
319 assert_eq!(false.into_value(), Value::Literal("F".to_string()));
320 assert_eq!(
321 Literal("007").into_value(),
322 Value::Literal("007".to_string())
323 );
324 assert_eq!(
325 Fixed(1.5, 3).into_value(),
326 Value::Literal("1.500".to_string())
327 );
328 assert_eq!(
329 Sci(0.000123, 3).into_value(),
330 Value::Literal("1.23E-4".to_string())
331 );
332 assert_eq!(
333 Sci(1234.0, 1).into_value(),
334 Value::Literal("1E3".to_string())
335 );
336 assert_eq!("s".into_value(), Value::Str("s".to_string()));
337 assert_eq!((&"s".to_string()).into_value(), Value::Str("s".to_string()));
338 assert_eq!(42u8.into_value(), Value::Literal("42".to_string()));
339 assert_eq!((-3isize).into_value(), Value::Literal("-3".to_string()));
340 assert_eq!(2.5f32.into_value(), Value::Literal("2.5".to_string()));
341 }
342
343 #[test]
344 fn datetime_from_card() {
345 let r = Record::value("K", Value::Str("2026-07-11T22:15:03".to_string()), None);
346 let dt = time::PrimitiveDateTime::from_card(&r).unwrap();
347 assert_eq!(crate::dates::format_datetime(&dt), "2026-07-11T22:15:03");
348 assert_eq!(time::PrimitiveDateTime::from_card(&literal("nope")), None);
349 }
350}