Skip to main content

zenith_float_num/
csvfmt.rs

1//! Pure-Rust CSV for [`Ieee64Array`] and [`ExactNumArray`].
2//!
3//! Binary64 cells are unsigned integer bit patterns (same as serde IEEE arrays).
4//! Empty / `nan` → [`Ieee64::NAN`]. No hardware IEEE arithmetic.
5
6#[cfg(feature = "std")]
7use crate::defs::DEFAULT_P;
8use crate::ieee_soft::Ieee64Array;
9#[cfg(feature = "std")]
10use crate::Consts;
11use crate::Error;
12#[cfg(feature = "std")]
13use crate::ExactNum;
14#[cfg(feature = "std")]
15use crate::ExactNumArray;
16use crate::Ieee64;
17#[cfg(feature = "std")]
18use crate::Radix;
19#[cfg(feature = "std")]
20use crate::RoundingMode;
21use alloc::string::String;
22use alloc::string::ToString;
23use alloc::vec::Vec;
24
25/// Maximum data rows accepted by [`Ieee64Array::from_csv_str`].
26pub const CSV_MAX_ROWS: usize = 1_048_576;
27
28/// Maximum columns accepted in one CSV row.
29pub const CSV_MAX_COLS: usize = 4_096;
30
31#[cfg(feature = "std")]
32const P_MARK: &str = "@p=";
33
34impl Ieee64Array {
35    /// Encode row-major binary64 bit patterns as unsigned decimals.
36    pub fn to_csv_string(&self) -> Result<String, Error> {
37        if self.shape().0 > CSV_MAX_ROWS {
38            return Err(Error::InvalidArgument);
39        }
40        encode_bits_csv(self.shape(), self.as_bits())
41    }
42
43    /// Parse CSV of bit-pattern cells. Empty / `nan` → [`Ieee64::NAN`].
44    pub fn from_csv_str(text: &str) -> Result<Self, Error> {
45        let (rows, cols, bits) = decode_bits_csv(text)?;
46        Self::from_parts(rows, cols, bits)
47    }
48
49    /// Write [`Self::to_csv_string`] to `path`.
50    #[cfg(feature = "std")]
51    pub fn to_csv<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), Error> {
52        let s = self.to_csv_string()?;
53        std::fs::write(path, s.as_bytes()).map_err(|_| Error::InvalidArgument)
54    }
55
56    /// Read [`Self::from_csv_str`] from `path`.
57    #[cfg(feature = "std")]
58    pub fn from_csv<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
59        let s = std::fs::read_to_string(path).map_err(|_| Error::InvalidArgument)?;
60        Self::from_csv_str(&s)
61    }
62}
63
64#[cfg(feature = "std")]
65impl ExactNumArray {
66    /// Encode each cell as `Display@p=<bits>`.
67    pub fn to_csv_string(&self) -> Result<String, Error> {
68        if self.shape().0 > CSV_MAX_ROWS {
69            return Err(Error::InvalidArgument);
70        }
71        let (rows, cols) = self.shape();
72        let mut out = String::new();
73        for i in 0..rows {
74            if i > 0 {
75                out.push('\n');
76            }
77            for j in 0..cols {
78                if j > 0 {
79                    out.push(',');
80                }
81                let v = self.get2(i, j).ok_or(Error::InvalidArgument)?;
82                out.push_str(&encode_exact(v));
83            }
84        }
85        if rows > 0 {
86            out.push('\n');
87        }
88        Ok(out)
89    }
90
91    /// Parse CSV of `Display` / `Display@p=` cells. Empty → NaN.
92    pub fn from_csv_str(text: &str) -> Result<Self, Error> {
93        let lines = data_lines(text)?;
94        if lines.is_empty() {
95            return Ok(Self::new(DEFAULT_P));
96        }
97        let cols = row_width(lines[0])?;
98        let rows = lines.len();
99        let n = rows.checked_mul(cols).ok_or(Error::InvalidArgument)?;
100        let mut vals = Vec::new();
101        vals.try_reserve_exact(n)?;
102        let mut p = None;
103        for line in &lines {
104            let fields = split_row(line, cols)?;
105            for cell in fields {
106                let v = decode_exact_cell(cell)?;
107                if p.is_none() {
108                    p = Some(v.precision().unwrap_or(DEFAULT_P));
109                }
110                vals.push(v);
111            }
112        }
113        let p = p.unwrap_or(DEFAULT_P);
114        Self::from_parts(p, rows, cols, vals)
115    }
116
117    /// Write [`Self::to_csv_string`] to `path`.
118    pub fn to_csv<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), Error> {
119        let s = self.to_csv_string()?;
120        std::fs::write(path, s.as_bytes()).map_err(|_| Error::InvalidArgument)
121    }
122
123    /// Read [`Self::from_csv_str`] from `path`.
124    pub fn from_csv<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
125        let s = std::fs::read_to_string(path).map_err(|_| Error::InvalidArgument)?;
126        Self::from_csv_str(&s)
127    }
128}
129
130fn encode_bits_csv(shape: (usize, usize), bits: &[u64]) -> Result<String, Error> {
131    let (rows, cols) = shape;
132    let n = rows.checked_mul(cols).ok_or(Error::InvalidArgument)?;
133    if n != bits.len() {
134        return Err(Error::InvalidArgument);
135    }
136    if cols > CSV_MAX_COLS {
137        return Err(Error::InvalidArgument);
138    }
139    let mut out = String::new();
140    for i in 0..rows {
141        if i > 0 {
142            out.push('\n');
143        }
144        for j in 0..cols {
145            if j > 0 {
146                out.push(',');
147            }
148            let idx = i
149                .checked_mul(cols)
150                .and_then(|b| b.checked_add(j))
151                .ok_or(Error::InvalidArgument)?;
152            out.push_str(&bits[idx].to_string());
153        }
154    }
155    if rows > 0 {
156        out.push('\n');
157    }
158    Ok(out)
159}
160
161fn decode_bits_csv(text: &str) -> Result<(usize, usize, Vec<u64>), Error> {
162    let lines = data_lines(text)?;
163    if lines.is_empty() {
164        return Ok((0, 0, Vec::new()));
165    }
166    let cols = row_width(lines[0])?;
167    let rows = lines.len();
168    let n = rows.checked_mul(cols).ok_or(Error::InvalidArgument)?;
169    let mut bits = Vec::new();
170    bits.try_reserve_exact(n)?;
171    for line in &lines {
172        for cell in split_row(line, cols)? {
173            bits.push(parse_ieee64_cell(cell)?);
174        }
175    }
176    Ok((rows, cols, bits))
177}
178
179fn data_lines(text: &str) -> Result<Vec<&str>, Error> {
180    let mut lines: Vec<&str> = text
181        .split('\n')
182        .map(|l| l.strip_suffix('\r').unwrap_or(l))
183        .collect();
184    while lines.last().is_some_and(|l| l.is_empty()) {
185        lines.pop();
186    }
187    if lines.len() > CSV_MAX_ROWS {
188        return Err(Error::InvalidArgument);
189    }
190    Ok(lines)
191}
192
193fn row_width(line: &str) -> Result<usize, Error> {
194    let n = line.split(',').count();
195    if n == 0 || n > CSV_MAX_COLS {
196        return Err(Error::InvalidArgument);
197    }
198    Ok(n)
199}
200
201fn split_row<'a>(line: &'a str, cols: usize) -> Result<Vec<&'a str>, Error> {
202    let mut fields: Vec<&str> = line.split(',').collect();
203    if fields.len() > cols {
204        return Err(Error::InvalidArgument);
205    }
206    if fields.len() > CSV_MAX_COLS {
207        return Err(Error::InvalidArgument);
208    }
209    while fields.len() < cols {
210        fields.push("");
211    }
212    Ok(fields)
213}
214
215fn parse_ieee64_cell(s: &str) -> Result<u64, Error> {
216    let t = s.trim();
217    if t.is_empty() || t.eq_ignore_ascii_case("nan") {
218        return Ok(Ieee64::NAN.to_bits());
219    }
220    if t.eq_ignore_ascii_case("inf") || t.eq_ignore_ascii_case("+inf") {
221        return Ok(Ieee64::INFINITY.to_bits());
222    }
223    if t.eq_ignore_ascii_case("-inf") {
224        return Ok(Ieee64::NEG_INFINITY.to_bits());
225    }
226    t.parse::<u64>().map_err(|_| Error::InvalidArgument)
227}
228
229#[cfg(feature = "std")]
230fn encode_exact(n: &ExactNum) -> String {
231    let p = n.precision().unwrap_or(DEFAULT_P);
232    let mut s = n.to_string();
233    s.push_str(P_MARK);
234    s.push_str(&p.to_string());
235    s
236}
237
238#[cfg(feature = "std")]
239fn decode_exact_cell(s: &str) -> Result<ExactNum, Error> {
240    let t = s.trim();
241    if t.is_empty() {
242        return Ok(crate::NAN.clone());
243    }
244    let (body, p) = match t.rfind(P_MARK) {
245        Some(i) => {
246            let p = t[i + P_MARK.len()..]
247                .parse::<usize>()
248                .map_err(|_| Error::InvalidArgument)?;
249            (&t[..i], p.max(1))
250        }
251        None => (t, DEFAULT_P),
252    };
253    let mut cc = Consts::new()?;
254    let n = ExactNum::parse(body, Radix::Dec, p, RoundingMode::ToEven, &mut cc);
255    if n.is_nan() && !body.eq_ignore_ascii_case("nan") && !body.eq_ignore_ascii_case("err") {
256        return Err(Error::InvalidArgument);
257    }
258    Ok(n)
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn csv_ieee64_100x3_roundtrip() {
267        let mut vals = Vec::new();
268        for i in 0..300 {
269            vals.push(Ieee64::from_i32(i));
270        }
271        let a = Ieee64Array::from_shape(100, 3, &vals).unwrap();
272        let text = a.to_csv_string().unwrap();
273        let b = Ieee64Array::from_csv_str(&text).unwrap();
274        assert_eq!(b.shape(), (100, 3));
275        assert_eq!(a.as_bits(), b.as_bits());
276    }
277
278    #[test]
279    fn csv_missing_cell_is_nan() {
280        let b = Ieee64Array::from_csv_str("1,,3\n").unwrap();
281        assert_eq!(b.shape(), (1, 3));
282        assert_eq!(b.get(0).unwrap().to_bits(), 1);
283        assert_eq!(b.get(1).unwrap().to_bits(), Ieee64::NAN.to_bits());
284        assert_eq!(b.get(2).unwrap().to_bits(), 3);
285        let short = Ieee64Array::from_csv_str("5,6\n7\n").unwrap();
286        assert_eq!(short.shape(), (2, 2));
287        assert_eq!(short.get2(1, 1).unwrap().to_bits(), Ieee64::NAN.to_bits());
288    }
289
290    #[test]
291    fn csv_exact_num_roundtrip_and_too_many_cols() {
292        let p = 64;
293        let n = |k: u8| ExactNum::from_u8(k, p);
294        let a = ExactNumArray::from_shape(p, 2, 2, &[n(1), n(2), n(3), n(4)]).unwrap();
295        let text = a.to_csv_string().unwrap();
296        assert!(text.contains("@p=64"));
297        let b = ExactNumArray::from_csv_str(&text).unwrap();
298        assert_eq!(b.shape(), (2, 2));
299        assert_eq!(b.precision(), p);
300        for i in 0..4 {
301            assert_eq!(a.get(i).unwrap().cmp(b.get(i).unwrap()), Some(0));
302        }
303        assert!(Ieee64Array::from_csv_str("1,2\n3,4,5\n").is_err());
304    }
305
306    #[cfg(feature = "std")]
307    #[test]
308    fn csv_path_roundtrip() {
309        let a = Ieee64Array::from_shape(
310            2,
311            2,
312            &[
313                Ieee64::from_i32(1),
314                Ieee64::from_i32(2),
315                Ieee64::from_i32(3),
316                Ieee64::from_i32(4),
317            ],
318        )
319        .unwrap();
320        let path = std::env::temp_dir().join("zenith_csv_path_gold.csv");
321        a.to_csv(&path).unwrap();
322        let b = Ieee64Array::from_csv(&path).unwrap();
323        let _ = std::fs::remove_file(&path);
324        assert_eq!(a.as_bits(), b.as_bits());
325        assert!(Ieee64Array::from_csv("/no/such/zenith_csv_missing.csv").is_err());
326    }
327}