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