1use calamine::{open_workbook_auto, Data, Reader, Sheets};
2use csv::{ReaderBuilder, StringRecord};
3use heck::ToSnakeCase;
4use indexmap::IndexMap;
5use serde_json::{Number, Value};
6use std::fs::File;
7use std::io::BufReader;
8use std::path::Path;
9use std::str::FromStr;
10
11use crate::data_set::*;
12use crate::error::GenericError;
13use alphanumeric::*;
14use crate::headers::*;
15use crate::helpers::float_value;
16use crate::helpers::string_value;
17use is_truthy::*;
18use crate::round_decimal::RoundDecimal;
19use crate::Extension;
20use crate::Format;
21use crate::OptionSet;
22use crate::PathData;
23use crate::RowOptionSet;
24use fuzzy_datetime::{iso_fuzzy_to_date_string, iso_fuzzy_to_datetime_string};
25
26pub type SaveRowFn = Box<dyn Fn(IndexMap<String, Value>) -> Result<(), GenericError> + Send + Sync>;
28
29pub fn process_spreadsheet_direct(opts: &OptionSet) -> Result<ResultSet, GenericError> {
32 let rt = tokio::runtime::Runtime::new().unwrap();
33 rt.block_on(process_spreadsheet_core(opts, None, None))
34}
35
36pub async fn process_spreadsheet_immediate(opts: &OptionSet) -> Result<ResultSet, GenericError> {
40 process_spreadsheet_core(opts, None, None).await
41}
42
43#[deprecated(
44 since = "1.0.6",
45 note = "This function is a wrapper for the renamed function `process_spreadsheet_inline`"
46)]
47pub async fn render_spreadsheet_direct(opts: &OptionSet) -> Result<ResultSet, GenericError> {
48 process_spreadsheet_core(opts, None, None).await
49}
50
51pub async fn process_spreadsheet_async(
53 opts: &OptionSet,
54 save_func: SaveRowFn,
55 out_ref: Option<&str>,
56) -> Result<ResultSet, GenericError> {
57 process_spreadsheet_core(opts, Some(save_func), out_ref).await
58}
59
60pub async fn process_spreadsheet_core(
63 opts: &OptionSet,
64 save_opt: Option<SaveRowFn>,
65 out_ref: Option<&str>,
66) -> Result<ResultSet, GenericError> {
67 if let Some(filepath) = opts.path.clone() {
68 let path = Path::new(&filepath);
69 if !path.exists() {
70 #[allow(dead_code)]
71 return Err(GenericError("file_unavailable"));
72 }
73 let path_data = PathData::new(path);
74 if path_data.is_valid() {
75 if path_data.use_calamine() {
76 read_workbook_core(&path_data, opts, save_opt, out_ref).await
77 } else {
78 read_csv_core(&path_data, opts, save_opt, out_ref).await
79 }
80 } else {
81 Err(GenericError("unsupported_format"))
82 }
83 } else {
84 Err(GenericError("no_filepath_specified"))
85 }
86}
87
88#[deprecated(
89 since = "1.0.6",
90 note = "This function is a wrapper for the renamed function `process_spreadsheet_core`"
91)]
92pub async fn render_spreadsheet_core(
93 opts: &OptionSet,
94 save_opt: Option<SaveRowFn>,
95 out_ref: Option<&str>,
96) -> Result<ResultSet, GenericError> {
97 process_spreadsheet_core(opts, save_opt, out_ref).await
98}
99
100pub async fn read_workbook_core<'a>(
103 path_data: &PathData<'a>,
104 opts: &OptionSet,
105 save_opt: Option<SaveRowFn>,
106 out_ref: Option<&str>,
107) -> Result<ResultSet, GenericError> {
108 if let Ok(mut workbook) = open_workbook_auto(path_data.path()) {
109 let max_rows = opts.max_rows();
110 let (selected_names, sheet_names, _sheet_indices) =
111 match_sheet_name_and_index(&mut workbook, opts);
112
113 if !selected_names.is_empty() {
114 let info = WorkbookInfo::new(path_data, &selected_names, &sheet_names);
115
116 if opts.multimode() {
117 read_multiple_worksheets(&mut workbook, &sheet_names, opts, &info, max_rows).await
118 } else {
119 let sheet_ref = &selected_names[0];
120 read_single_worksheet(workbook, sheet_ref, opts, &info, save_opt, out_ref).await
121 }
122 } else {
123 Err(GenericError("workbook_with_no_sheets"))
124 }
125 } else {
126 Err(GenericError("cannot_open_workbook"))
127 }
128}
129
130async fn read_multiple_worksheets(
132 workbook: &mut Sheets<BufReader<File>>,
133 sheet_names: &[String],
134 opts: &OptionSet,
135 info: &WorkbookInfo,
136 max_rows: usize,
137) -> Result<ResultSet, GenericError> {
138 let mut sheets: Vec<SheetDataSet> = vec![];
139 let capture_rows = opts.capture_rows();
140 for (sheet_index, sheet_ref) in sheet_names.iter().enumerate() {
141 let range = workbook.worksheet_range(&sheet_ref.clone())?;
142 let mut headers: Vec<String> = vec![];
143 let mut has_headers = false;
144 let capture_headers = !opts.omit_header;
145 let mut rows: Vec<IndexMap<String, Value>> =
146 Vec::with_capacity(if capture_rows { max_rows } else { 0 });
147 let mut row_index = 0;
148 let header_row_index = opts.header_row_index();
149 let mut col_keys: Vec<String> = vec![];
150 let columns = if sheet_index == 0 {
151 opts.rows.columns.clone()
152 } else {
153 vec![]
154 };
155 let mut resolved_row_opts = opts.rows.clone();
156 let match_header_row_below = capture_headers && header_row_index > 0;
157 if let Some(first_row) = range.headers() {
158 let natural_keys = natural_column_keys(&first_row, &opts.field_mode);
159 let resolved_columns = resolve_columns(&columns, &natural_keys);
160 headers = build_header_keys(&first_row, &resolved_columns, &opts.field_mode);
161 resolved_row_opts.columns = resolved_columns;
162 has_headers = !match_header_row_below;
163 col_keys = first_row;
164 }
165 let total = range.get_size().0;
166 if capture_rows || match_header_row_below {
167 let max_row_count = if capture_rows {
168 max_rows
169 } else {
170 header_row_index + 2
171 };
172 let max_take = if total < max_row_count {
173 total
174 } else {
175 max_row_count + 1
176 };
177 for row in range.rows().take(max_take) {
178 if row_index > max_row_count {
179 break;
180 }
181 if match_header_row_below && (row_index + 1) == header_row_index {
182 let h_row = row
183 .iter()
184 .map(|c| c.to_string().to_snake_case())
185 .collect::<Vec<String>>();
186 let natural_keys = natural_column_keys(&h_row, &opts.field_mode);
187 let resolved_columns = resolve_columns(&columns, &natural_keys);
188 headers = build_header_keys(&h_row, &resolved_columns, &opts.field_mode);
189 resolved_row_opts.columns = resolved_columns;
190 has_headers = true;
191 } else if (has_headers || !capture_headers) && capture_rows {
192 let raw_values: Vec<String> = row.iter().map(|c| c.to_string()).collect();
193 if is_not_header_row(&raw_values, row_index, &col_keys) {
194 let row_map = workbook_row_to_map(row, &resolved_row_opts, &headers);
195 rows.push(row_map);
196 }
197 }
198 row_index += 1;
199 }
200 }
201 sheets.push(SheetDataSet::new(sheet_ref, &headers, &rows, total));
202 }
203 Ok(ResultSet::from_multiple(&sheets, info, opts))
204}
205
206pub async fn read_single_worksheet(
208 mut workbook: Sheets<BufReader<File>>,
209 sheet_ref: &str,
210 opts: &OptionSet,
211 info: &WorkbookInfo,
212 save_opt: Option<SaveRowFn>,
213 out_ref: Option<&str>,
214) -> Result<ResultSet, GenericError> {
215 let range = workbook.worksheet_range(sheet_ref)?;
216 let capture_rows = opts.capture_rows();
217 let columns = opts.rows.columns.clone();
218 let max_rows = opts.max_rows();
219 let mut headers: Vec<String> = vec![];
220 let mut col_keys: Vec<String> = vec![];
221 let mut has_headers = false;
222 let capture_headers = !opts.omit_header;
223 let mut rows: Vec<IndexMap<String, Value>> =
224 Vec::with_capacity(if capture_rows { max_rows } else { 0 });
225 let mut row_index = 0;
226 let header_row_index = opts.header_row_index();
227 let match_header_row_below = capture_headers && header_row_index > 0;
228 let mut resolved_row_opts = opts.rows.clone();
229
230 if let Some(first_row) = range.headers() {
231 let natural_keys = natural_column_keys(&first_row, &opts.field_mode);
232 let resolved_columns = resolve_columns(&columns, &natural_keys);
233 headers = build_header_keys(&first_row, &resolved_columns, &opts.field_mode);
234 resolved_row_opts.columns = resolved_columns;
235 has_headers = !match_header_row_below;
236 col_keys = first_row;
237 }
238 let total = range.get_size().0;
239 if capture_rows || match_header_row_below {
240 let max_row_count = if capture_rows {
241 max_rows
242 } else {
243 header_row_index + 2
244 };
245 let max_take = if total < max_row_count {
246 total
247 } else {
248 max_row_count + 1
249 };
250 for row in range.rows().take(max_take) {
251 if row_index > max_row_count {
252 break;
253 }
254 if match_header_row_below && (row_index + 1) == header_row_index {
255 let h_row = row
256 .iter()
257 .map(|c| c.to_string().to_snake_case())
258 .collect::<Vec<String>>();
259 let natural_keys = natural_column_keys(&h_row, &opts.field_mode);
260 let resolved_columns = resolve_columns(&columns, &natural_keys);
261 headers = build_header_keys(&h_row, &resolved_columns, &opts.field_mode);
262 resolved_row_opts.columns = resolved_columns;
263 has_headers = true;
264 } else if (has_headers || !capture_headers) && capture_rows {
265 let raw_values: Vec<String> = row.iter().map(|c| c.to_string()).collect();
267 if is_not_header_row(&raw_values, row_index, &col_keys) {
268 let row_map = workbook_row_to_map(row, &resolved_row_opts, &headers);
269 rows.push(row_map);
270 }
271 }
272 row_index += 1;
273 }
274 }
275 if let Some(save_method) = save_opt {
276 let mut row_iter = range.rows();
277 let mut save_count: usize = 0;
278 if let Some(first_row) = row_iter.next() {
279 let raw_values: Vec<String> = first_row.iter().map(|c| c.to_string()).collect();
280 if is_not_header_row(&raw_values, 0, &col_keys) {
281 let first_row_map = workbook_row_to_map(first_row, &resolved_row_opts, &headers);
282 save_method(first_row_map)?;
283 save_count += 1;
284 }
285 }
286 for row in row_iter {
287 if save_count >= max_rows {
288 break;
289 }
290 let row_map = workbook_row_to_map(row, &resolved_row_opts, &headers);
291 save_method(row_map)?;
292 save_count += 1;
293 }
294 }
295
296 let ds = DataSet::from_count_and_rows(total, rows, opts);
297 Ok(ResultSet::new(info, &headers, ds, opts, out_ref))
298}
299
300pub async fn read_csv_core<'a>(
303 path_data: &PathData<'a>,
304 opts: &OptionSet,
305 save_opt: Option<SaveRowFn>,
306 out_ref: Option<&str>,
307) -> Result<ResultSet, GenericError> {
308 let separator = match path_data.mode() {
309 Extension::Tsv => b't',
310 _ => b',',
311 };
312 if let Ok(mut rdr) = ReaderBuilder::new()
313 .delimiter(separator)
314 .from_path(path_data.path())
315 {
316 let capture_header = !opts.omit_header;
317 let capture_rows = opts.capture_rows();
318 let max_line_usize = opts.max_rows();
319 let mut rows: Vec<IndexMap<String, Value>> =
320 Vec::with_capacity(if capture_rows { max_line_usize } else { 0 });
321 let mut line_count = 0;
322
323 let mut headers: Vec<String> = vec![];
324 let mut resolved_row_opts = opts.rows.clone();
325 if capture_header {
326 if let Ok(hdrs) = rdr.headers() {
327 headers = hdrs.into_iter().map(|s| s.to_owned()).collect();
328 }
329 let columns = opts.rows.columns.clone();
330 let natural_keys = natural_column_keys(&headers, &opts.field_mode);
331 let resolved_columns = resolve_columns(&columns, &natural_keys);
332 headers = build_header_keys(&headers, &resolved_columns, &opts.field_mode);
333 resolved_row_opts.columns = resolved_columns;
334 }
335
336 let mut total = 0;
337 if capture_rows {
338 for result in rdr.records() {
339 if line_count >= max_line_usize {
340 break;
341 }
342 if let Some(row) = csv_row_result_to_values(result, &resolved_row_opts) {
343 rows.push(to_index_map(&row, &headers));
344 line_count += 1;
345 }
346 }
347 total = line_count + rdr.records().count() + 1;
348 } else if let Some(save_method) = save_opt {
349 for result in rdr.records() {
350 if let Some(row) = csv_row_result_to_values(result, &resolved_row_opts) {
351 let row_map = to_index_map(&row, &headers);
352 save_method(row_map)?;
353 total += 1;
354 }
355 }
356 } else {
357 if let Ok(mut count_rdr) = ReaderBuilder::new().from_path(path_data.path()) {
358 total = count_rdr.records().count();
359 }
360 }
361 let info = WorkbookInfo::simple(path_data);
362 let ds = DataSet::from_count_and_rows(total, rows, opts);
363 Ok(ResultSet::new(&info, &headers, ds, opts, out_ref))
364 } else {
365 let error_msg = match path_data.ext() {
366 Extension::Tsv => "unreadable_tsv_file",
367 _ => "unreadable_csv_file",
368 };
369 Err(GenericError(error_msg))
370 }
371}
372
373fn workbook_row_to_map(
375 row: &[Data],
376 opts: &RowOptionSet,
377 headers: &[String],
378) -> IndexMap<String, Value> {
379 to_index_map(&workbook_row_to_values(row, opts), headers)
380}
381
382fn workbook_row_to_values(row: &[Data], opts: &RowOptionSet) -> Vec<Value> {
384 row.iter()
385 .enumerate()
386 .map(|(c_index, cell)| workbook_cell_to_value(cell, opts, c_index))
387 .collect()
388}
389
390fn workbook_cell_to_value(cell: &Data, opts: &RowOptionSet, c_index: usize) -> Value {
392 let col = opts.column(c_index);
393 let format = col.map_or(Format::Auto, |c| c.format.to_owned());
394 let def_val = col.and_then(|c| c.default.clone());
395
396 let date_only = resolve_date_only(&format, opts.date_only);
397
398 match cell {
399 Data::Int(i) => Value::Number(Number::from_i128(*i as i128).unwrap()),
400 Data::Float(f) => process_float_value(*f, format),
401 Data::DateTimeIso(d) => process_iso_datetime_value(d, def_val, date_only),
402 Data::DateTime(d) => process_excel_datetime_value(d, def_val, date_only),
403 Data::Bool(b) => Value::Bool(*b),
404 Data::String(s) => process_string_value(s, format, def_val),
405 Data::Empty => def_val.unwrap_or(Value::Null),
406 _ => Value::String(cell.to_string()),
407 }
408}
409
410fn resolve_date_only(format: &Format, row_date_only: bool) -> bool {
414 match format {
415 Format::Date => true,
416 Format::DateTime => false,
417 _ => row_date_only,
418 }
419}
420
421fn process_float_value(value: f64, format: Format) -> Value {
422 match format {
423 Format::Integer => Value::Number(Number::from_i128(value as i128).unwrap()),
424 Format::Boolean => Value::Bool(value >= 1.0),
425 Format::Text => Value::String(value.to_string()),
426 _ => Value::Number(Number::from_f64(value).unwrap()),
427 }
428}
429
430fn process_excel_datetime_value(
431 datetime: &calamine::ExcelDateTime,
432 def_val: Option<Value>,
433 date_only: bool,
434) -> Value {
435 let dt_ref = datetime.as_datetime().map_or_else(
436 || def_val.unwrap_or(Value::Null),
437 |dt| {
438 let formatted_date = if date_only {
439 dt.format("%Y-%m-%d").to_string()
440 } else {
441 dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()
442 };
443 Value::String(formatted_date)
444 },
445 );
446 dt_ref
447}
448
449fn process_iso_datetime_value(dt_str: &str, def_val: Option<Value>, date_only: bool) -> Value {
450 if date_only {
451 iso_fuzzy_to_date_string(dt_str)
452 .map_or_else(|| def_val.unwrap_or(Value::Null), Value::String)
453 } else {
454 iso_fuzzy_to_datetime_string(dt_str)
455 .map_or_else(|| def_val.unwrap_or(Value::Null), Value::String)
456 }
457}
458
459fn process_string_value(value: &str, format: Format, def_val: Option<Value>) -> Value {
460 match format {
461 Format::Boolean => process_truthy_value(value, def_val, |v, ef| v.is_truthy_core(ef)),
462 Format::Truthy => process_truthy_value(value, def_val, |v, ef| v.is_truthy_standard(ef)),
463 Format::TruthyCustom(opts) => process_truthy_value(value, def_val, |v, _| {
464 v.is_truthy_custom(&opts)
465 }),
466 Format::Decimal(places) => {
467 process_numeric_value(value, def_val, |n| float_value(n.round_decimal(places)))
468 }
469 Format::Float => process_numeric_value(value, def_val, float_value),
470 Format::Date => process_date_value(value, def_val, iso_fuzzy_to_date_string),
471 Format::DateTime => process_date_value(value, def_val, iso_fuzzy_to_datetime_string),
472 _ => Value::String(value.to_owned()),
473 }
474}
475
476fn process_truthy_value<F>(value: &str, def_val: Option<Value>, truthy_fn: F) -> Value
477where
478 F: Fn(&str, bool) -> Option<bool>,
479{
480 if let Some(is_true) = truthy_fn(value, false) {
481 Value::Bool(is_true)
482 } else {
483 def_val.unwrap_or(Value::Null)
484 }
485}
486
487fn process_numeric_value<F>(value: &str, def_val: Option<Value>, numeric_fn: F) -> Value
488where
489 F: Fn(f64) -> Value,
490{
491 if let Some(n) = value.to_first_number::<f64>() {
492 numeric_fn(n)
493 } else {
494 def_val.unwrap_or(Value::Null)
495 }
496}
497
498fn process_date_value<F>(value: &str, def_val: Option<Value>, date_fn: F) -> Value
499where
500 F: Fn(&str) -> Option<String>,
501{
502 if let Some(date_str) = date_fn(value) {
503 string_value(&date_str)
504 } else {
505 def_val.unwrap_or(Value::Null)
506 }
507}
508
509fn csv_row_result_to_values(
511 result: Result<StringRecord, csv::Error>,
512 opts: &RowOptionSet,
513) -> Option<Vec<Value>> {
514 if let Ok(record) = result {
515 let row = record
516 .into_iter()
517 .enumerate()
518 .map(|(ci, cell)| csv_cell_to_json_value(cell, opts, ci))
519 .collect();
520 return Some(row);
521 }
522 None
523}
524
525fn csv_cell_to_json_value(cell: &str, opts: &RowOptionSet, index: usize) -> Value {
527 let has_number = cell.to_first_number::<f64>().is_some();
528 let col = opts.column(index);
530 let (fmt, euro_num_mode) = if let Some(c) = col {
531 (c.format.clone(), c.decimal_comma)
532 } else {
533 (Format::Auto, opts.decimal_comma)
534 };
535 let num_cell = if has_number {
536 let euro_num_mode = uses_decimal_comma(cell, euro_num_mode);
537 if euro_num_mode {
538 cell.replace(",", ".").replace(",", ".")
539 } else {
540 cell.replace(",", "")
541 }
542 } else {
543 cell.to_owned()
544 };
545 let mut new_cell = Value::Null;
546 if !num_cell.is_empty() && num_cell.is_numeric() {
547 if let Ok(float_val) = serde_json::Number::from_str(&num_cell) {
548 match fmt {
549 Format::Integer => {
550 if let Some(f) = float_val.as_f64() {
555 if let Some(int_val) = Number::from_i128(f as i128) {
556 new_cell = Value::Number(int_val);
557 }
558 }
559 }
560 Format::Boolean => {
561 new_cell = Value::Bool(float_val.as_f64().unwrap_or(0f64) >= 1.0);
563 }
564 _ => {
565 new_cell = Value::Number(float_val);
566 }
567 }
568 }
569 } else if let Some(is_true) = cell.is_truthy_core(false) {
570 new_cell = Value::Bool(is_true);
571 } else {
572 new_cell = match fmt {
573 Format::Truthy => {
574 if let Some(is_true) = cell.is_truthy_standard(false) {
575 Value::Bool(is_true)
576 } else {
577 Value::Null
578 }
579 }
580 _ => Value::String(cell.to_string()),
581 };
582 }
583 new_cell
584}
585
586pub async fn read_workbook_sheet_info<'a>(
587 path_data: &PathData<'a>,
588) -> Result<IndexMap<String, usize>, GenericError> {
589 if let Ok(mut workbook) = open_workbook_auto(path_data.path()) {
590 let mut im: IndexMap<String, usize> = IndexMap::new();
591 for name in workbook.sheet_names() {
592 if let Ok(range) = workbook.worksheet_range(&name) {
593 im.insert(name, range.rows().count());
594 }
595 }
596 Ok(im)
597 } else {
598 Err(GenericError("cannot_open_workbook"))
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605 use crate::{helpers::*, Column};
606 use serde_json::json;
607 use std::path;
608
609 #[test]
610 fn test_direct_processing_xlsx() {
611 let sample_path = "data/sample-data-1.xlsx";
612
613 let opts = OptionSet::new(sample_path).max_row_count(1_000);
616
617 let result = process_spreadsheet_direct(&opts);
618
619 assert_eq!(result.unwrap().num_rows, 401);
621 }
622
623 #[test]
624 fn test_source_key_override_renames_and_reformats_without_position() {
625 let sample_path = "data/sample-data-1.csv";
628 let mut opts = OptionSet::new(sample_path).max_row_count(2);
629 opts.rows.columns = vec![
630 Column::from_source_key_with_format("weight", Some("weight_lbs"), Format::Integer, None, false, false),
631 ];
632
633 let result = process_spreadsheet_direct(&opts).unwrap();
634 assert!(result.keys.contains(&"id".to_string()));
636 assert!(result.keys.contains(&"first_name".to_string()));
637 assert!(result.keys.contains(&"weight_lbs".to_string()));
638 assert!(!result.keys.contains(&"weight".to_string()));
639
640 let rows = result.to_vec();
641 let first = rows.first().expect("at least one row");
642 assert!(first.get("weight_lbs").is_some());
643 assert!(first.get("weight").is_none());
644 assert_eq!(first.get("id").unwrap(), 1);
645 }
646
647 #[test]
648 fn test_resolve_date_only_prefers_column_format_over_row_default() {
649 assert!(resolve_date_only(&Format::Date, false));
652 assert!(!resolve_date_only(&Format::DateTime, true));
653 assert!(!resolve_date_only(&Format::Auto, false));
654 assert!(resolve_date_only(&Format::Auto, true));
655 }
656
657 #[test]
658 fn test_source_key_override_casts_native_datetime_cell_to_date_only() {
659 let sample_path = "data/sample-data-1.xlsx";
664 let mut opts = OptionSet::new(sample_path).max_row_count(1);
665 opts.rows.columns = vec![
666 Column::from_source_key_with_format("start_time", None, Format::Date, None, false, false),
667 ];
668
669 let result = process_spreadsheet_direct(&opts).unwrap();
670 let rows = result.to_vec();
671 let first = rows.first().expect("at least one row");
672 let start_time = first.get("start_time").expect("start_time column").as_str().unwrap();
673 assert_eq!(start_time, "2023-06-15");
674 assert!(!start_time.contains('T'), "should be date-only, got: {}", start_time);
675 }
676
677 #[test]
678 fn test_csv_cell_integer_format_truncates_decimal_values() {
679 let cols = vec![Column::new_format(Format::Integer, None)];
683 let row_opts = RowOptionSet::simple(&cols);
684 assert_eq!(csv_cell_to_json_value("58.2", &row_opts, 0), Value::Number(Number::from(58)));
685 assert_eq!(csv_cell_to_json_value("82.5", &row_opts, 0), Value::Number(Number::from(82)));
686 assert_eq!(csv_cell_to_json_value("100", &row_opts, 0), Value::Number(Number::from(100)));
687 }
688
689 #[test]
690 fn test_csv_cell_does_not_coerce_ids_to_booleans() {
691 let row_opts = RowOptionSet::default();
696 assert_eq!(csv_cell_to_json_value("SKU001", &row_opts, 0), Value::String("SKU001".to_string()));
697 assert_eq!(csv_cell_to_json_value("A1", &row_opts, 0), Value::String("A1".to_string()));
698 assert_eq!(csv_cell_to_json_value("01/06/2024", &row_opts, 0), Value::String("01/06/2024".to_string()));
699 assert_eq!(csv_cell_to_json_value("true", &row_opts, 0), Value::Bool(true));
701 assert_eq!(csv_cell_to_json_value("false", &row_opts, 0), Value::Bool(false));
702 }
703
704 #[test]
705 fn test_direct_processing_csv() {
706 let sample_path = "data/sample-data-1.csv";
707
708 let opts = OptionSet::new(sample_path).max_row_count(1_000);
711
712 let result = process_spreadsheet_direct(&opts);
713
714 assert_eq!(result.unwrap().num_rows, 401);
716 }
717
718 #[test]
719 fn test_multisheet_preview_ods() {
720 let sample_path = "data/sample-data-2.ods";
721
722 let opts = OptionSet::new(sample_path)
727 .max_row_count(10)
728 .read_mode_preview();
729
730 let result = process_spreadsheet_direct(&opts);
731
732 let dataset = result.unwrap();
734 assert_eq!(dataset.sheets.len(), 2);
735 assert_eq!(dataset.num_rows, 118);
737
738 assert_eq!(dataset.data.first_sheet().len(), 10);
740 }
741
742 #[test]
743 fn test_column_override_1() {
744 let sample_json = json!({
745 "sku": "CHAIR16",
746 "height": "112cm",
747 "width": "69cm",
748 "approved": "Y"
749 });
750
751 let rows = json_object_to_calamine_data(sample_json);
752
753 let cols = vec![
754 Column::new_format(Format::Text, Some(string_value(""))),
755 Column::new_format(Format::Float, Some(float_value(95.0))),
756 Column::new_format(Format::Float, Some(float_value(65.0))),
757 Column::new_format(Format::Truthy, Some(bool_value(false))),
758 ];
759
760 let opts = &RowOptionSet::simple(&cols);
762 let result = workbook_row_to_values(&rows, opts);
763 assert_eq!(result.get(1).unwrap(), 112.0);
765 assert_eq!(result.get(2).unwrap(), 69.0);
767 assert_eq!(result.get(3).unwrap(), true);
769 }
770
771 #[test]
772 fn test_column_override_2() {
773 let sample_json = json!({
774 "name": "Sophia",
775 "dob": "2001-9-23",
776 "weight": "62kg",
777 "result": "GOOD"
778 });
779
780 let rows = json_object_to_calamine_data(sample_json);
781
782 let cols = vec![
783 Column::new_format(Format::Text, None),
784 Column::new_format(Format::Date, None),
785 Column::new_format(Format::Float, None),
786 Column::new_format(
788 Format::truthy_custom("good", "bad"),
789 Some(bool_value(false)),
790 ),
791 ];
792
793 let opts = &RowOptionSet::simple(&cols);
795 let result = workbook_row_to_values(&rows, opts);
796 assert_eq!(result.get(1).unwrap(), "2001-09-23");
797 assert_eq!(result.get(2).unwrap(), 62.0);
798 assert_eq!(result.get(3).unwrap(), true);
799 }
800
801 #[tokio::test]
802 async fn test_read_workbook_info() {
803 let sample_path = "data/sample-data-1.xlsx";
804 let path_data = PathData::new(path::Path::new(sample_path));
805 let info = read_workbook_sheet_info(&path_data).await;
806 assert!(info.is_ok());
807 }
808
809 #[tokio::test]
810 async fn test_large_csv_file() {
811 let sample_path = "data/large-datasheet.csv";
812 let max_rows = 100_000;
813 let opts = OptionSet::new(sample_path).max_row_count(max_rows);
814 let result = process_spreadsheet_core(&opts, None, None).await;
815 if let Ok(data) = result.clone() {
816 assert_eq!(data.data.first_sheet().len(), max_rows as usize);
817 } else {
818 panic!("Failed to process large CSV file");
819 }
820 assert!(result.is_ok());
821 }
822
823 #[tokio::test]
824 async fn test_medium_excel_file() {
825 let sample_path = "data/medium-spreadsheet-50_000.xlsx";
826 let max_rows = 5_000;
827 let opts = OptionSet::new(sample_path).max_row_count(max_rows);
828 let result = process_spreadsheet_core(&opts, None, None).await;
829 if let Ok(data) = result.clone() {
830 assert_eq!(data.data.first_sheet().len(), max_rows as usize);
831 } else {
832 panic!("Failed to process large Excel file");
833 }
834 assert!(result.is_ok());
835 }
836}