1#[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
25pub const CSV_MAX_ROWS: usize = 1_048_576;
27
28pub const CSV_MAX_COLS: usize = 4_096;
30
31#[cfg(feature = "std")]
32const P_MARK: &str = "@p=";
33
34impl Ieee64Array {
35 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 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 #[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 #[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 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 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 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 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}