1use crate::error::IoError;
2use crate::traits::{
3 AccessGranularity, BackendCaps, CellData, SaveDestination, SheetData, SpreadsheetReader,
4 SpreadsheetWriter,
5};
6use formualizer_common::LiteralValue;
7use std::collections::BTreeMap;
8use std::fs::File;
9use std::io::{BufReader, Read, Write};
10use std::path::{Path, PathBuf};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
13pub enum CsvEncoding {
14 #[default]
16 Utf8,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
20pub enum CsvTrim {
21 #[default]
22 None,
23 All,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
27pub enum CsvTypeInference {
28 Off,
30 #[default]
32 Basic,
33 BasicWithDates,
35}
36
37#[derive(Clone, Debug)]
38pub struct CsvReadOptions {
39 pub delimiter: u8,
41 pub has_headers: bool,
46 pub trim: CsvTrim,
47 pub encoding: CsvEncoding,
48 pub type_inference: CsvTypeInference,
49}
50
51impl Default for CsvReadOptions {
52 fn default() -> Self {
53 Self {
54 delimiter: b',',
55 has_headers: false,
56 trim: CsvTrim::None,
57 encoding: CsvEncoding::Utf8,
58 type_inference: CsvTypeInference::Basic,
59 }
60 }
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
64pub enum CsvNewline {
65 #[default]
66 Lf,
67 Crlf,
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
71pub enum CsvQuoteStyle {
72 #[default]
73 Necessary,
74 Always,
75 Never,
76 NonNumeric,
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
80pub enum CsvArrayPolicy {
81 #[default]
83 Error,
84 TopLeft,
86 Blank,
88}
89
90#[derive(Clone, Debug)]
91pub struct CsvWriteOptions {
92 pub delimiter: u8,
94 pub newline: CsvNewline,
95 pub quote_style: CsvQuoteStyle,
96
97 pub array_policy: CsvArrayPolicy,
99}
100
101impl Default for CsvWriteOptions {
102 fn default() -> Self {
103 Self {
104 delimiter: b',',
105 newline: CsvNewline::Lf,
106 quote_style: CsvQuoteStyle::Necessary,
107 array_policy: CsvArrayPolicy::Error,
108 }
109 }
110}
111
112#[derive(Clone, Debug, Default)]
113struct CsvSheet {
114 cells: BTreeMap<(u32, u32), LiteralValue>,
116 max_row: u32,
118 max_col: u32,
120}
121
122impl CsvSheet {
123 fn bounds(&self) -> Option<(u32, u32)> {
124 if self.max_row == 0 || self.max_col == 0 {
125 None
126 } else {
127 Some((self.max_row, self.max_col))
128 }
129 }
130
131 fn set_bounds(&mut self, rows: u32, cols: u32) {
132 self.max_row = self.max_row.max(rows);
133 self.max_col = self.max_col.max(cols);
134 }
135}
136
137pub struct CsvAdapter {
144 sheet_name: String,
145 sheet: CsvSheet,
146 path: Option<PathBuf>,
147 read_options: CsvReadOptions,
148 write_options: CsvWriteOptions,
149 caps: BackendCaps,
150}
151
152impl Default for CsvAdapter {
153 fn default() -> Self {
154 Self::new()
155 }
156}
157
158impl CsvAdapter {
159 pub fn new() -> Self {
160 Self::new_with_options(CsvReadOptions::default(), CsvWriteOptions::default())
161 }
162
163 pub fn new_with_options(read_options: CsvReadOptions, write_options: CsvWriteOptions) -> Self {
164 Self {
165 sheet_name: "Sheet1".to_string(),
166 sheet: CsvSheet::default(),
167 path: None,
168 read_options,
169 write_options,
170 caps: BackendCaps {
171 read: true,
172 write: true,
173 streaming: false,
174 tables: false,
175 named_ranges: false,
176 formulas: false,
177 styles: false,
178 lazy_loading: false,
179 random_access: true,
180 bytes_input: true,
181 date_system_1904: false,
182 merged_cells: false,
183 rich_text: false,
184 hyperlinks: false,
185 data_validations: false,
186 shared_formulas: false,
187 },
188 }
189 }
190
191 pub fn read_options(&self) -> &CsvReadOptions {
192 &self.read_options
193 }
194
195 pub fn write_options(&self) -> &CsvWriteOptions {
196 &self.write_options
197 }
198
199 pub fn set_write_options(&mut self, opts: CsvWriteOptions) {
200 self.write_options = opts;
201 }
202
203 pub fn open_path_with_options<P: AsRef<Path>>(
204 path: P,
205 read_options: CsvReadOptions,
206 ) -> Result<Self, IoError> {
207 let mut adapter = Self::new_with_options(read_options, CsvWriteOptions::default());
208 adapter.open_from_path(path.as_ref())?;
209 adapter.path = Some(path.as_ref().to_path_buf());
210 Ok(adapter)
211 }
212
213 pub fn open_reader_with_options(
214 reader: Box<dyn Read + Send + Sync>,
215 read_options: CsvReadOptions,
216 ) -> Result<Self, IoError> {
217 let mut adapter = Self::new_with_options(read_options, CsvWriteOptions::default());
218 adapter.open_from_reader(reader)?;
219 Ok(adapter)
220 }
221
222 pub fn open_bytes_with_options(
223 bytes: Vec<u8>,
224 read_options: CsvReadOptions,
225 ) -> Result<Self, IoError> {
226 let mut adapter = Self::new_with_options(read_options, CsvWriteOptions::default());
227 adapter.open_from_reader(Box::new(std::io::Cursor::new(bytes)))?;
228 Ok(adapter)
229 }
230
231 fn open_from_path(&mut self, path: &Path) -> Result<(), IoError> {
232 let file = File::open(path)?;
233 let reader = BufReader::new(file);
234 self.open_from_reader(Box::new(reader))
235 }
236
237 fn open_from_reader(&mut self, reader: Box<dyn Read + Send + Sync>) -> Result<(), IoError> {
238 if self.read_options.encoding != CsvEncoding::Utf8 {
239 return Err(IoError::Unsupported {
240 feature: "encoding".to_string(),
241 context: "csv: only UTF-8 is supported".to_string(),
242 });
243 }
244
245 let mut rb = csv::ReaderBuilder::new();
246 rb.delimiter(self.read_options.delimiter)
247 .has_headers(self.read_options.has_headers)
248 .flexible(true);
250
251 match self.read_options.trim {
252 CsvTrim::None => rb.trim(csv::Trim::None),
253 CsvTrim::All => rb.trim(csv::Trim::All),
254 };
255
256 let mut rdr = rb.from_reader(reader);
257 self.sheet = CsvSheet::default();
258
259 let mut row: u32 = 1;
260
261 if self.read_options.has_headers {
262 let headers = rdr.headers().map_err(|e| IoError::from_backend("csv", e))?;
263 let cols = headers.len() as u32;
264 self.sheet.set_bounds(1, cols);
265 for (ci, field) in headers.iter().enumerate() {
266 let col = (ci as u32) + 1;
267 if let Some(v) = infer_field(field, CsvTypeInference::Off, true) {
268 self.sheet.cells.insert((row, col), v);
269 }
270 }
271 row += 1;
272 }
273
274 for rec in rdr.records() {
275 let rec = rec.map_err(|e| IoError::from_backend("csv", e))?;
276 let cols = rec.len() as u32;
277 self.sheet.set_bounds(row, cols);
278
279 for (ci, field) in rec.iter().enumerate() {
280 let col = (ci as u32) + 1;
281 if let Some(v) = infer_field(field, self.read_options.type_inference, false) {
282 self.sheet.cells.insert((row, col), v);
283 }
284 }
285 row += 1;
286 }
287
288 Ok(())
289 }
290
291 pub fn write_sheet_to<'a>(
292 &self,
293 sheet: &str,
294 dest: SaveDestination<'a>,
295 opts: CsvWriteOptions,
296 ) -> Result<Option<Vec<u8>>, IoError> {
297 let Some((rows, cols)) = self.sheet_bounds(sheet) else {
298 return match dest {
299 SaveDestination::InPlace => {
300 let Some(path) = self.path.as_ref() else {
301 return Err(IoError::Backend {
302 backend: "csv".to_string(),
303 message: "no known path for in-place save".to_string(),
304 });
305 };
306 let _ = File::create(path)?;
307 Ok(None)
308 }
309 SaveDestination::Path(path) => {
310 let _ = File::create(path)?;
311 Ok(None)
312 }
313 SaveDestination::Writer(_writer) => Ok(None),
314 SaveDestination::Bytes => Ok(Some(Vec::new())),
315 };
316 };
317 self.write_range_to(sheet, (1, 1), (rows, cols), dest, opts)
318 }
319
320 pub fn write_range_to<'a>(
321 &self,
322 sheet: &str,
323 start: (u32, u32),
324 end: (u32, u32),
325 dest: SaveDestination<'a>,
326 opts: CsvWriteOptions,
327 ) -> Result<Option<Vec<u8>>, IoError> {
328 if sheet != self.sheet_name {
329 return Err(IoError::Backend {
330 backend: "csv".to_string(),
331 message: format!("sheet not found: {sheet}"),
332 });
333 }
334 let (sr, sc) = start;
335 let (er, ec) = end;
336 if sr == 0 || sc == 0 || er == 0 || ec == 0 {
337 return Err(IoError::Backend {
338 backend: "csv".to_string(),
339 message: "range coordinates are 1-based".to_string(),
340 });
341 }
342 if er < sr || ec < sc {
343 return Err(IoError::Backend {
344 backend: "csv".to_string(),
345 message: "invalid range (end before start)".to_string(),
346 });
347 }
348
349 match dest {
350 SaveDestination::InPlace => {
351 let Some(path) = self.path.as_ref() else {
352 return Err(IoError::Backend {
353 backend: "csv".to_string(),
354 message: "no known path for in-place save".to_string(),
355 });
356 };
357 let mut file = File::create(path)?;
358 write_rect_csv(&mut file, opts, (sr, sc), (er, ec), |r, c| {
359 self.sheet.cells.get(&(r, c)).cloned()
360 })?;
361 Ok(None)
362 }
363 SaveDestination::Path(path) => {
364 let mut file = File::create(path)?;
365 write_rect_csv(&mut file, opts, (sr, sc), (er, ec), |r, c| {
366 self.sheet.cells.get(&(r, c)).cloned()
367 })?;
368 Ok(None)
369 }
370 SaveDestination::Writer(writer) => {
371 write_rect_csv(writer, opts, (sr, sc), (er, ec), |r, c| {
372 self.sheet.cells.get(&(r, c)).cloned()
373 })?;
374 Ok(None)
375 }
376 SaveDestination::Bytes => {
377 let mut buf: Vec<u8> = Vec::new();
378 write_rect_csv(&mut buf, opts, (sr, sc), (er, ec), |r, c| {
379 self.sheet.cells.get(&(r, c)).cloned()
380 })?;
381 Ok(Some(buf))
382 }
383 }
384 }
385}
386
387impl SpreadsheetReader for CsvAdapter {
388 type Error = IoError;
389
390 fn access_granularity(&self) -> AccessGranularity {
391 AccessGranularity::Workbook
392 }
393
394 fn capabilities(&self) -> BackendCaps {
395 self.caps.clone()
396 }
397
398 fn sheet_names(&self) -> Result<Vec<String>, Self::Error> {
399 Ok(vec![self.sheet_name.clone()])
400 }
401
402 fn open_path<P: AsRef<Path>>(path: P) -> Result<Self, Self::Error>
403 where
404 Self: Sized,
405 {
406 Self::open_path_with_options(path, CsvReadOptions::default())
407 }
408
409 fn open_reader(reader: Box<dyn Read + Send + Sync>) -> Result<Self, Self::Error>
410 where
411 Self: Sized,
412 {
413 Self::open_reader_with_options(reader, CsvReadOptions::default())
414 }
415
416 fn open_bytes(data: Vec<u8>) -> Result<Self, Self::Error>
417 where
418 Self: Sized,
419 {
420 Self::open_bytes_with_options(data, CsvReadOptions::default())
421 }
422
423 fn read_range(
424 &mut self,
425 sheet: &str,
426 start: (u32, u32),
427 end: (u32, u32),
428 ) -> Result<BTreeMap<(u32, u32), CellData>, Self::Error> {
429 if sheet != self.sheet_name {
430 return Ok(BTreeMap::new());
431 }
432 let mut out: BTreeMap<(u32, u32), CellData> = BTreeMap::new();
433 let (sr, sc) = start;
434 let (er, ec) = end;
435 for ((r, c), v) in self.sheet.cells.iter() {
436 if *r >= sr && *r <= er && *c >= sc && *c <= ec {
437 out.insert(
438 (*r, *c),
439 CellData {
440 value: Some(v.clone()),
441 formula: None,
442 style: None,
443 },
444 );
445 }
446 }
447 Ok(out)
448 }
449
450 fn read_sheet(&mut self, sheet: &str) -> Result<SheetData, Self::Error> {
451 if sheet != self.sheet_name {
452 return Ok(SheetData {
453 cells: BTreeMap::new(),
454 dimensions: None,
455 tables: vec![],
456 named_ranges: vec![],
457 date_system_1904: false,
458 merged_cells: vec![],
459 hidden: false,
460 });
461 }
462
463 let mut cells: BTreeMap<(u32, u32), CellData> = BTreeMap::new();
464 for (k, v) in self.sheet.cells.iter() {
465 cells.insert(
466 *k,
467 CellData {
468 value: Some(v.clone()),
469 formula: None,
470 style: None,
471 },
472 );
473 }
474
475 Ok(SheetData {
476 cells,
477 dimensions: self.sheet.bounds(),
478 tables: vec![],
479 named_ranges: vec![],
480 date_system_1904: false,
481 merged_cells: vec![],
482 hidden: false,
483 })
484 }
485
486 fn sheet_bounds(&self, sheet: &str) -> Option<(u32, u32)> {
487 if sheet == self.sheet_name {
488 self.sheet.bounds()
489 } else {
490 None
491 }
492 }
493
494 fn is_loaded(&self, _sheet: &str, _row: Option<u32>, _col: Option<u32>) -> bool {
495 true
496 }
497}
498
499impl SpreadsheetWriter for CsvAdapter {
500 type Error = IoError;
501
502 fn write_cell(
503 &mut self,
504 sheet: &str,
505 row: u32,
506 col: u32,
507 data: CellData,
508 ) -> Result<(), Self::Error> {
509 if sheet != self.sheet_name {
510 return Err(IoError::Backend {
511 backend: "csv".to_string(),
512 message: format!("sheet not found: {sheet}"),
513 });
514 }
515 if data.formula.is_some() {
516 return Err(IoError::Unsupported {
517 feature: "formulas".to_string(),
518 context: "csv".to_string(),
519 });
520 }
521 if data.style.is_some() {
522 return Err(IoError::Unsupported {
523 feature: "styles".to_string(),
524 context: "csv".to_string(),
525 });
526 }
527
528 self.sheet.set_bounds(row, col);
529 match data.value {
530 None => {
531 self.sheet.cells.remove(&(row, col));
532 }
533 Some(LiteralValue::Empty) => {
534 self.sheet.cells.remove(&(row, col));
535 }
536 Some(v) => {
537 self.sheet.cells.insert((row, col), v);
538 }
539 }
540 Ok(())
541 }
542
543 fn write_range(
544 &mut self,
545 sheet: &str,
546 cells: BTreeMap<(u32, u32), CellData>,
547 ) -> Result<(), Self::Error> {
548 for ((r, c), d) in cells {
549 self.write_cell(sheet, r, c, d)?;
550 }
551 Ok(())
552 }
553
554 fn clear_range(
555 &mut self,
556 sheet: &str,
557 start: (u32, u32),
558 end: (u32, u32),
559 ) -> Result<(), Self::Error> {
560 if sheet != self.sheet_name {
561 return Ok(());
562 }
563 let (sr, sc) = start;
564 let (er, ec) = end;
565 let keys: Vec<(u32, u32)> = self
566 .sheet
567 .cells
568 .keys()
569 .copied()
570 .filter(|(r, c)| *r >= sr && *r <= er && *c >= sc && *c <= ec)
571 .collect();
572 for k in keys {
573 self.sheet.cells.remove(&k);
574 }
575 Ok(())
576 }
577
578 fn create_sheet(&mut self, name: &str) -> Result<(), Self::Error> {
579 if name == self.sheet_name {
580 return Ok(());
581 }
582 Err(IoError::Unsupported {
583 feature: "multiple sheets".to_string(),
584 context: "csv".to_string(),
585 })
586 }
587
588 fn delete_sheet(&mut self, name: &str) -> Result<(), Self::Error> {
589 if name == self.sheet_name {
590 self.sheet = CsvSheet::default();
591 }
592 Ok(())
593 }
594
595 fn rename_sheet(&mut self, old: &str, new: &str) -> Result<(), Self::Error> {
596 if old == self.sheet_name {
597 self.sheet_name = new.to_string();
598 }
599 Ok(())
600 }
601
602 fn flush(&mut self) -> Result<(), Self::Error> {
603 Ok(())
604 }
605
606 fn save_to<'a>(&mut self, dest: SaveDestination<'a>) -> Result<Option<Vec<u8>>, Self::Error> {
607 let sheet = self.sheet_name.clone();
608 let opts = self.write_options.clone();
609 self.write_sheet_to(&sheet, dest, opts)
610 }
611}
612
613impl<R> formualizer_eval::engine::ingest::EngineLoadStream<R> for CsvAdapter
615where
616 R: formualizer_eval::traits::EvaluationContext,
617{
618 type Error = IoError;
619
620 fn stream_into_engine(
621 &mut self,
622 engine: &mut formualizer_eval::engine::Engine<R>,
623 ) -> Result<(), Self::Error> {
624 let sheet_name = self.sheet_name.clone();
626 engine
627 .add_sheet(&sheet_name)
628 .map_err(|e| IoError::from_backend("csv", e))?;
629
630 let Some((rows_u32, cols_u32)) = self.sheet.bounds() else {
631 return Ok(());
632 };
633 let rows = rows_u32 as usize;
634 let cols = cols_u32 as usize;
635
636 let chunk_rows: usize = 32 * 1024;
637 let mut aib = formualizer_eval::arrow_store::IngestBuilder::new(
638 &sheet_name,
639 cols,
640 chunk_rows,
641 engine.config.date_system,
642 );
643
644 for r0 in 0..rows {
645 let r = (r0 as u32) + 1;
646 let mut row_vals: Vec<LiteralValue> = vec![LiteralValue::Empty; cols];
647 for (c0, val) in row_vals.iter_mut().enumerate().take(cols) {
648 let c = (c0 as u32) + 1;
649 if let Some(v) = self.sheet.cells.get(&(r, c)) {
650 *val = v.clone();
651 }
652 }
653 aib.append_row(&row_vals)
654 .map_err(|e| IoError::from_backend("csv", e))?;
655 }
656
657 let asheet = aib.finish();
658 let store = engine.sheet_store_mut();
659 if let Some(pos) = store
660 .sheets
661 .iter()
662 .position(|s| s.name.as_ref() == sheet_name)
663 {
664 store.sheets[pos] = asheet;
665 } else {
666 store.sheets.push(asheet);
667 }
668 engine.finalize_sheet_index(&sheet_name);
669 engine.set_first_load_assume_new(false);
670 engine.reset_ensure_touched();
671 Ok(())
672 }
673}
674
675pub fn write_workbook_sheet_to_path(
681 wb: &crate::Workbook,
682 sheet: &str,
683 path: impl AsRef<Path>,
684 opts: CsvWriteOptions,
685) -> Result<(), IoError> {
686 let Some((rows, cols)) = wb.sheet_dimensions(sheet) else {
687 let _ = File::create(path.as_ref())?;
688 return Ok(());
689 };
690 let addr = crate::RangeAddress::new(sheet.to_string(), 1, 1, rows, cols).map_err(|e| {
691 IoError::Backend {
692 backend: "csv".to_string(),
693 message: e.to_string(),
694 }
695 })?;
696 write_workbook_range_to_path(wb, &addr, path, opts)
697}
698
699pub fn write_workbook_range_to_path(
701 wb: &crate::Workbook,
702 addr: &crate::RangeAddress,
703 path: impl AsRef<Path>,
704 opts: CsvWriteOptions,
705) -> Result<(), IoError> {
706 let values = wb.read_range(addr);
707 let mut file = File::create(path.as_ref())?;
708 write_values_csv(&mut file, opts, &values)
709}
710
711pub fn write_workbook_range_to_bytes(
713 wb: &crate::Workbook,
714 addr: &crate::RangeAddress,
715 opts: CsvWriteOptions,
716) -> Result<Vec<u8>, IoError> {
717 let values = wb.read_range(addr);
718 let mut buf: Vec<u8> = Vec::new();
719 write_values_csv(&mut buf, opts, &values)?;
720 Ok(buf)
721}
722
723fn infer_field(field: &str, mode: CsvTypeInference, force_text: bool) -> Option<LiteralValue> {
724 if field.is_empty() {
725 return None;
726 }
727 if force_text || mode == CsvTypeInference::Off {
728 return Some(LiteralValue::Text(field.to_string()));
729 }
730
731 if let Some(b) = parse_bool(field) {
732 return Some(LiteralValue::Boolean(b));
733 }
734 if let Some(i) = parse_unambiguous_i64(field) {
735 return Some(LiteralValue::Int(i));
736 }
737 if let Some(n) = parse_unambiguous_f64(field) {
738 return Some(LiteralValue::Number(n));
739 }
740 if mode == CsvTypeInference::BasicWithDates {
741 if let Some(d) = parse_date(field) {
742 return Some(LiteralValue::Date(d));
743 }
744 if let Some(dt) = parse_datetime(field) {
745 return Some(LiteralValue::DateTime(dt));
746 }
747 }
748 Some(LiteralValue::Text(field.to_string()))
749}
750
751fn parse_bool(s: &str) -> Option<bool> {
752 if s.eq_ignore_ascii_case("true") {
753 Some(true)
754 } else if s.eq_ignore_ascii_case("false") {
755 Some(false)
756 } else {
757 None
758 }
759}
760
761fn parse_unambiguous_i64(s: &str) -> Option<i64> {
762 let bytes = s.as_bytes();
764 if bytes.is_empty() {
765 return None;
766 }
767 let (sign, digits) = match bytes[0] {
768 b'+' => (1i64, &s[1..]),
769 b'-' => (-1i64, &s[1..]),
770 _ => (1i64, s),
771 };
772 if digits.is_empty() {
773 return None;
774 }
775 if digits.len() > 1 && digits.starts_with('0') {
776 return None;
777 }
778 if !digits.as_bytes().iter().all(|b| b.is_ascii_digit()) {
779 return None;
780 }
781 let parsed: i64 = digits.parse().ok()?;
782 Some(sign * parsed)
783}
784
785fn parse_unambiguous_f64(s: &str) -> Option<f64> {
786 if !(s.contains('.') || s.contains('e') || s.contains('E')) {
788 return None;
789 }
790 let s2 = s.strip_prefix('+').unwrap_or(s);
792 let s2 = s2.strip_prefix('-').unwrap_or(s2);
793 if s2.len() > 1 && s2.starts_with('0') && !s2.starts_with("0.") {
794 return None;
795 }
796 let n: f64 = s.parse().ok()?;
797 if !n.is_finite() {
798 return None;
799 }
800 Some(n)
801}
802
803fn parse_date(s: &str) -> Option<chrono::NaiveDate> {
804 chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").ok()
805}
806
807fn parse_datetime(s: &str) -> Option<chrono::NaiveDateTime> {
808 chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
809 .or_else(|_| chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S"))
810 .ok()
811}
812
813fn csv_terminator(nl: CsvNewline) -> csv::Terminator {
814 match nl {
815 CsvNewline::Lf => csv::Terminator::Any(b'\n'),
816 CsvNewline::Crlf => csv::Terminator::CRLF,
817 }
818}
819
820fn csv_quote_style(q: CsvQuoteStyle) -> csv::QuoteStyle {
821 match q {
822 CsvQuoteStyle::Necessary => csv::QuoteStyle::Necessary,
823 CsvQuoteStyle::Always => csv::QuoteStyle::Always,
824 CsvQuoteStyle::Never => csv::QuoteStyle::Never,
825 CsvQuoteStyle::NonNumeric => csv::QuoteStyle::NonNumeric,
826 }
827}
828
829fn write_rect_csv<W: Write + ?Sized>(
830 writer: &mut W,
831 opts: CsvWriteOptions,
832 start: (u32, u32),
833 end: (u32, u32),
834 mut get: impl FnMut(u32, u32) -> Option<LiteralValue>,
835) -> Result<(), IoError> {
836 let mut wb = csv::WriterBuilder::new();
837 wb.delimiter(opts.delimiter)
838 .terminator(csv_terminator(opts.newline))
839 .quote_style(csv_quote_style(opts.quote_style));
840 let mut wtr = wb.from_writer(writer);
841
842 let (sr, sc) = start;
843 let (er, ec) = end;
844 for r in sr..=er {
845 let mut record: Vec<String> = Vec::with_capacity((ec - sc + 1) as usize);
846 for c in sc..=ec {
847 let s = match get(r, c) {
848 Some(v) => literal_to_csv_field(&v, &opts)?,
849 None => String::new(),
850 };
851 record.push(s);
852 }
853 wtr.write_record(record)
854 .map_err(|e| IoError::from_backend("csv", e))?;
855 }
856 wtr.flush().map_err(|e| IoError::from_backend("csv", e))?;
857 Ok(())
858}
859
860fn write_values_csv<W: Write>(
861 writer: &mut W,
862 opts: CsvWriteOptions,
863 values: &[Vec<LiteralValue>],
864) -> Result<(), IoError> {
865 let mut wb = csv::WriterBuilder::new();
866 wb.delimiter(opts.delimiter)
867 .terminator(csv_terminator(opts.newline))
868 .quote_style(csv_quote_style(opts.quote_style));
869 let mut wtr = wb.from_writer(writer);
870 for row in values {
871 let record: Vec<String> = row
872 .iter()
873 .map(|v| literal_to_csv_field(v, &opts))
874 .collect::<Result<Vec<_>, IoError>>()?;
875 wtr.write_record(record)
876 .map_err(|e| IoError::from_backend("csv", e))?;
877 }
878 wtr.flush().map_err(|e| IoError::from_backend("csv", e))?;
879 Ok(())
880}
881
882fn literal_to_csv_field(v: &LiteralValue, opts: &CsvWriteOptions) -> Result<String, IoError> {
883 literal_to_csv_field_inner(v, opts, 0)
884}
885
886fn literal_to_csv_field_inner(
887 v: &LiteralValue,
888 opts: &CsvWriteOptions,
889 depth: u8,
890) -> Result<String, IoError> {
891 if depth > 4 {
892 return Err(IoError::Backend {
893 backend: "csv".to_string(),
894 message: "Array nesting too deep for CSV export".to_string(),
895 });
896 }
897
898 Ok(match v {
899 LiteralValue::Empty => String::new(),
900 LiteralValue::Text(s) => s.clone(),
901 LiteralValue::Int(i) => i.to_string(),
902 LiteralValue::Number(n) => n.to_string(),
903 LiteralValue::Boolean(b) => {
904 if *b {
905 "TRUE".to_string()
906 } else {
907 "FALSE".to_string()
908 }
909 }
910 LiteralValue::Date(d) => d.format("%Y-%m-%d").to_string(),
911 LiteralValue::DateTime(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(),
912 LiteralValue::Time(t) => t.format("%H:%M:%S").to_string(),
913 LiteralValue::Duration(d) => d.num_seconds().to_string(),
914 LiteralValue::Error(e) => e.kind.to_string(),
915 LiteralValue::Pending => "Pending".to_string(),
916 LiteralValue::Array(a) => match opts.array_policy {
917 CsvArrayPolicy::Error => {
918 return Err(IoError::Backend {
919 backend: "csv".to_string(),
920 message: "Cannot export array value to CSV (array_policy=Error)".to_string(),
921 });
922 }
923 CsvArrayPolicy::Blank => String::new(),
924 CsvArrayPolicy::TopLeft => {
925 if let Some(row0) = a.first()
926 && let Some(v0) = row0.first()
927 {
928 return literal_to_csv_field_inner(v0, opts, depth + 1);
929 }
930 String::new()
931 }
932 },
933 })
934}