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;
10use std::sync::Arc;
11
12use crate::data_set::*;
13use crate::error::GenericError;
14use alphanumeric::*;
15use crate::headers::*;
16use crate::helpers::float_value;
17use crate::helpers::string_value;
18use is_truthy::*;
19use crate::round_decimal::RoundDecimal;
20use crate::Extension;
21use crate::Format;
22use crate::OptionSet;
23use crate::PathData;
24use crate::RowOptionSet;
25use fuzzy_datetime::{iso_fuzzy_to_date_string, iso_fuzzy_to_datetime_string};
26
27pub fn process_spreadsheet_direct(opts: &OptionSet) -> Result<ResultSet, GenericError> {
30 let rt = tokio::runtime::Runtime::new().unwrap();
31 rt.block_on(process_spreadsheet_core(opts, None, None))
32}
33
34pub async fn process_spreadsheet_immediate(opts: &OptionSet) -> Result<ResultSet, GenericError> {
38 process_spreadsheet_core(opts, None, None).await
39}
40
41#[deprecated(
42 since = "1.0.6",
43 note = "This function is a wrapper for the renamed function `process_spreadsheet_inline`"
44)]
45pub async fn render_spreadsheet_direct(opts: &OptionSet) -> Result<ResultSet, GenericError> {
46 process_spreadsheet_core(opts, None, None).await
47}
48
49pub async fn process_spreadsheet_async(
51 opts: &OptionSet,
52 save_func: Box<dyn Fn(IndexMap<String, Value>) -> Result<(), GenericError> + Send + Sync>,
53 out_ref: Option<&str>,
54) -> Result<ResultSet, GenericError> {
55 process_spreadsheet_core(opts, Some(save_func), out_ref).await
56}
57
58pub async fn process_spreadsheet_core(
61 opts: &OptionSet,
62 save_opt: Option<
63 Box<dyn Fn(IndexMap<String, Value>) -> Result<(), GenericError> + Send + Sync>,
64 >,
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<
95 Box<dyn Fn(IndexMap<String, Value>) -> Result<(), GenericError> + Send + Sync>,
96 >,
97 out_ref: Option<&str>,
98) -> Result<ResultSet, GenericError> {
99 process_spreadsheet_core(opts, save_opt, out_ref).await
100}
101
102pub async fn read_workbook_core<'a>(
105 path_data: &PathData<'a>,
106 opts: &OptionSet,
107 save_opt: Option<
108 Box<dyn Fn(IndexMap<String, Value>) -> Result<(), GenericError> + Send + Sync>,
109 >,
110 out_ref: Option<&str>,
111) -> Result<ResultSet, GenericError> {
112 if let Ok(mut workbook) = open_workbook_auto(path_data.path()) {
113 let max_rows = opts.max_rows();
114 let (selected_names, sheet_names, _sheet_indices) =
115 match_sheet_name_and_index(&mut workbook, opts);
116
117 if selected_names.len() > 0 {
118 let info = WorkbookInfo::new(path_data, &selected_names, &sheet_names);
119
120 if opts.multimode() {
121 read_multiple_worksheets(&mut workbook, &sheet_names, opts, &info, max_rows).await
122 } else {
123 let sheet_ref = &selected_names[0];
124 read_single_worksheet(workbook, sheet_ref, opts, &info, save_opt, out_ref).await
125 }
126 } else {
127 Err(GenericError("workbook_with_no_sheets"))
128 }
129 } else {
130 Err(GenericError("cannot_open_workbook"))
131 }
132}
133
134async fn read_multiple_worksheets(
136 workbook: &mut Sheets<BufReader<File>>,
137 sheet_names: &[String],
138 opts: &OptionSet,
139 info: &WorkbookInfo,
140 max_rows: usize,
141) -> Result<ResultSet, GenericError> {
142 let mut sheets: Vec<SheetDataSet> = vec![];
143 let mut sheet_index: usize = 0;
144 let capture_rows = opts.capture_rows();
145 for sheet_ref in sheet_names {
146 let range = workbook.worksheet_range(&sheet_ref.clone())?;
147 let mut headers: Vec<String> = vec![];
148 let mut has_headers = false;
149 let capture_headers = !opts.omit_header;
150 let mut rows: Vec<IndexMap<String, Value>> =
151 Vec::with_capacity(if capture_rows { max_rows } else { 0 });
152 let mut row_index = 0;
153 let header_row_index = opts.header_row_index();
154 let mut col_keys: Vec<String> = vec![];
155 let columns = if sheet_index == 0 {
156 opts.rows.columns.clone()
157 } else {
158 vec![]
159 };
160 let mut resolved_row_opts = opts.rows.clone();
161 let match_header_row_below = capture_headers && header_row_index > 0;
162 if let Some(first_row) = range.headers() {
163 let natural_keys = natural_column_keys(&first_row, &opts.field_mode);
164 let resolved_columns = resolve_columns(&columns, &natural_keys);
165 headers = build_header_keys(&first_row, &resolved_columns, &opts.field_mode);
166 resolved_row_opts.columns = resolved_columns;
167 has_headers = !match_header_row_below;
168 col_keys = first_row;
169 }
170 let total = range.get_size().0;
171 if capture_rows || match_header_row_below {
172 let max_row_count = if capture_rows {
173 max_rows
174 } else {
175 header_row_index + 2
176 };
177 let max_take = if total < max_row_count {
178 total
179 } else {
180 max_row_count + 1
181 };
182 for row in range.rows().take(max_take) {
183 if row_index > max_row_count {
184 break;
185 }
186 if match_header_row_below && (row_index + 1) == header_row_index {
187 let h_row = row
188 .into_iter()
189 .map(|c| c.to_string().to_snake_case())
190 .collect::<Vec<String>>();
191 let natural_keys = natural_column_keys(&h_row, &opts.field_mode);
192 let resolved_columns = resolve_columns(&columns, &natural_keys);
193 headers = build_header_keys(&h_row, &resolved_columns, &opts.field_mode);
194 resolved_row_opts.columns = resolved_columns;
195 has_headers = true;
196 } else if (has_headers || !capture_headers) && capture_rows {
197 let row_map = workbook_row_to_map(row, &resolved_row_opts, &headers);
198 if is_not_header_row(&row_map, row_index, &col_keys) {
199 rows.push(row_map);
200 }
201 }
202 row_index += 1;
203 }
204 }
205 sheets.push(SheetDataSet::new(&sheet_ref, &headers, &rows, total));
206 sheet_index += 1;
207 }
208 Ok(ResultSet::from_multiple(&sheets, &info, opts))
209}
210
211pub async fn read_single_worksheet(
213 mut workbook: Sheets<BufReader<File>>,
214 sheet_ref: &str,
215 opts: &OptionSet,
216 info: &WorkbookInfo,
217 save_opt: Option<
218 Box<dyn Fn(IndexMap<String, Value>) -> Result<(), GenericError> + Send + Sync>,
219 >,
220 out_ref: Option<&str>,
221) -> Result<ResultSet, GenericError> {
222 let range = workbook.worksheet_range(sheet_ref)?;
223 let capture_rows = opts.capture_rows();
224 let columns = opts.rows.columns.clone();
225 let max_rows = opts.max_rows();
226 let mut headers: Vec<String> = vec![];
227 let mut col_keys: Vec<String> = vec![];
228 let mut has_headers = false;
229 let capture_headers = !opts.omit_header;
230 let mut rows: Vec<IndexMap<String, Value>> =
231 Vec::with_capacity(if capture_rows { max_rows } else { 0 });
232 let mut row_index = 0;
233 let header_row_index = opts.header_row_index();
234 let match_header_row_below = capture_headers && header_row_index > 0;
235 let mut resolved_row_opts = opts.rows.clone();
236
237 if let Some(first_row) = range.headers() {
238 let natural_keys = natural_column_keys(&first_row, &opts.field_mode);
239 let resolved_columns = resolve_columns(&columns, &natural_keys);
240 headers = build_header_keys(&first_row, &resolved_columns, &opts.field_mode);
241 resolved_row_opts.columns = resolved_columns;
242 has_headers = !match_header_row_below;
243 col_keys = first_row;
244 }
245 let total = range.get_size().0;
246 if capture_rows || match_header_row_below {
247 let max_row_count = if capture_rows {
248 max_rows
249 } else {
250 header_row_index + 2
251 };
252 let max_take = if total < max_row_count {
253 total
254 } else {
255 max_row_count + 1
256 };
257 for row in range.rows().take(max_take) {
258 if row_index > max_row_count {
259 break;
260 }
261 if match_header_row_below && (row_index + 1) == header_row_index {
262 let h_row = row
263 .into_iter()
264 .map(|c| c.to_string().to_snake_case())
265 .collect::<Vec<String>>();
266 let natural_keys = natural_column_keys(&h_row, &opts.field_mode);
267 let resolved_columns = resolve_columns(&columns, &natural_keys);
268 headers = build_header_keys(&h_row, &resolved_columns, &opts.field_mode);
269 resolved_row_opts.columns = resolved_columns;
270 has_headers = true;
271 } else if (has_headers || !capture_headers) && capture_rows {
272 let row_map = workbook_row_to_map(row, &resolved_row_opts, &headers);
274 if is_not_header_row(&row_map, row_index, &col_keys) {
275 rows.push(row_map);
276 }
277 }
278 row_index += 1;
279 }
280 }
281 if let Some(save_method) = save_opt {
282 let mut row_iter = range.rows();
283 let mut save_count: usize = 0;
284 if let Some(first_row) = row_iter.next() {
285 let first_row_map = workbook_row_to_map(first_row, &resolved_row_opts, &headers);
286 if is_not_header_row(&first_row_map, 0, &col_keys) {
287 save_method(first_row_map)?;
288 save_count += 1;
289 }
290 }
291 for row in row_iter {
292 if save_count >= max_rows {
293 break;
294 }
295 let row_map = workbook_row_to_map(row, &resolved_row_opts, &headers);
296 save_method(row_map)?;
297 save_count += 1;
298 }
299 }
300
301 let ds = DataSet::from_count_and_rows(total, rows, opts);
302 Ok(ResultSet::new(info, &headers, ds, opts, out_ref))
303}
304
305pub async fn read_csv_core<'a>(
308 path_data: &PathData<'a>,
309 opts: &OptionSet,
310 save_opt: Option<
311 Box<dyn Fn(IndexMap<String, Value>) -> Result<(), GenericError> + Send + Sync>,
312 >,
313 out_ref: Option<&str>,
314) -> Result<ResultSet, GenericError> {
315 let separator = match path_data.mode() {
316 Extension::Tsv => b't',
317 _ => b',',
318 };
319 if let Ok(mut rdr) = ReaderBuilder::new()
320 .delimiter(separator)
321 .from_path(path_data.path())
322 {
323 let capture_header = opts.omit_header == false;
324 let capture_rows = opts.capture_rows();
325 let max_line_usize = opts.max_rows();
326 let mut rows: Vec<IndexMap<String, Value>> =
327 Vec::with_capacity(if capture_rows { max_line_usize } else { 0 });
328 let mut line_count = 0;
329
330 let mut headers: Vec<String> = vec![];
331 let mut resolved_row_opts = opts.rows.clone();
332 if capture_header {
333 if let Ok(hdrs) = rdr.headers() {
334 headers = hdrs.into_iter().map(|s| s.to_owned()).collect();
335 }
336 let columns = opts.rows.columns.clone();
337 let natural_keys = natural_column_keys(&headers, &opts.field_mode);
338 let resolved_columns = resolve_columns(&columns, &natural_keys);
339 headers = build_header_keys(&headers, &resolved_columns, &opts.field_mode);
340 resolved_row_opts.columns = resolved_columns;
341 }
342
343 let mut total = 0;
344 let opts_rows_arc = Arc::new(&resolved_row_opts);
345 if capture_rows {
346 for result in rdr.records() {
347 if line_count >= max_line_usize {
348 break;
349 }
350 if let Some(row) = csv_row_result_to_values(result, opts_rows_arc.clone()) {
351 rows.push(to_index_map(&row, &headers));
352 line_count += 1;
353 }
354 }
355 total = line_count + rdr.records().count() + 1;
356 } else if let Some(save_method) = save_opt {
357 for result in rdr.records() {
358 if let Some(row) = csv_row_result_to_values(result, opts_rows_arc.clone()) {
359 let row_map = to_index_map(&row, &headers);
360 save_method(row_map)?;
361 total += 1;
362 }
363 }
364 } else {
365 if let Ok(mut count_rdr) = ReaderBuilder::new().from_path(&path_data.path()) {
366 total = count_rdr.records().count();
367 }
368 }
369 let info = WorkbookInfo::simple(path_data);
370 let ds = DataSet::from_count_and_rows(total, rows, opts);
371 Ok(ResultSet::new(&info, &headers, ds, opts, out_ref))
372 } else {
373 let error_msg = match path_data.ext() {
374 Extension::Tsv => "unreadable_tsv_file",
375 _ => "unreadable_csv_file",
376 };
377 Err(GenericError(error_msg))
378 }
379}
380
381fn workbook_row_to_map(
383 row: &[Data],
384 opts: &RowOptionSet,
385 headers: &[String],
386) -> IndexMap<String, Value> {
387 to_index_map(&workbook_row_to_values(row, opts), headers)
388}
389
390fn workbook_row_to_values(row: &[Data], opts: &RowOptionSet) -> Vec<Value> {
392 let opts_arc = Arc::new(opts);
393 let mut c_index = 0;
394 let mut cells: Vec<Value> = vec![];
395 for cell in row {
396 let value = workbook_cell_to_value(cell, opts_arc.clone(), c_index);
397 cells.push(value);
398 c_index += 1;
399 }
400 cells
401}
402
403fn workbook_cell_to_value(cell: &Data, opts: Arc<&RowOptionSet>, c_index: usize) -> Value {
405 let col = opts.column(c_index);
406 let format = col.map_or(Format::Auto, |c| c.format.to_owned());
407 let def_val = col.and_then(|c| c.default.clone());
408
409 match cell {
410 Data::Int(i) => Value::Number(Number::from_i128(*i as i128).unwrap()),
411 Data::Float(f) => process_float_value(*f, format),
412 Data::DateTimeIso(d) => process_iso_datetime_value(d, def_val, opts.date_only),
413 Data::DateTime(d) => process_excel_datetime_value(d, def_val, opts.date_only),
414 Data::Bool(b) => Value::Bool(*b),
415 Data::String(s) => process_string_value(s, format, def_val),
416 Data::Empty => def_val.unwrap_or(Value::Null),
417 _ => Value::String(cell.to_string()),
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), |dt| Value::String(dt))
453 } else {
454 iso_fuzzy_to_datetime_string(dt_str)
455 .map_or_else(|| def_val.unwrap_or(Value::Null), |dt| Value::String(dt))
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: Arc<&RowOptionSet>,
513) -> Option<Vec<Value>> {
514 if let Ok(record) = result {
515 let mut row: Vec<Value> = vec![];
516 let mut ci: usize = 0;
517 for cell in record.into_iter() {
518 let new_cell = csv_cell_to_json_value(cell, opts.clone(), ci);
519 row.push(new_cell);
520 ci += 1;
521 }
522 return Some(row);
523 }
524 None
525}
526
527fn csv_cell_to_json_value(cell: &str, opts: Arc<&RowOptionSet>, index: usize) -> Value {
529 let has_number = cell.to_first_number::<f64>().is_some();
530 let col = opts.column(index);
532 let (fmt, euro_num_mode) = if let Some(c) = col {
533 (c.format.clone(), c.decimal_comma)
534 } else {
535 (Format::Auto, opts.decimal_comma)
536 };
537 let num_cell = if has_number {
538 let euro_num_mode = uses_decimal_comma(cell, euro_num_mode);
539 if euro_num_mode {
540 cell.replace(",", ".").replace(",", ".")
541 } else {
542 cell.replace(",", "")
543 }
544 } else {
545 cell.to_owned()
546 };
547 let mut new_cell = Value::Null;
548 if num_cell.len() > 0 && num_cell.is_numeric() {
549 if let Ok(float_val) = serde_json::Number::from_str(&num_cell) {
550 match fmt {
551 Format::Integer => {
552 if let Some(f) = float_val.as_f64() {
557 if let Some(int_val) = Number::from_i128(f as i128) {
558 new_cell = Value::Number(int_val);
559 }
560 }
561 }
562 Format::Boolean => {
563 new_cell = Value::Bool(float_val.as_f64().unwrap_or(0f64) >= 1.0);
565 }
566 _ => {
567 new_cell = Value::Number(float_val);
568 }
569 }
570 }
571 } else if let Some(is_true) = cell.is_truthy_core(false) {
572 new_cell = Value::Bool(is_true);
573 } else {
574 new_cell = match fmt {
575 Format::Truthy => {
576 if let Some(is_true) = cell.is_truthy_standard(false) {
577 Value::Bool(is_true)
578 } else {
579 Value::Null
580 }
581 }
582 _ => Value::String(cell.to_string()),
583 };
584 }
585 new_cell
586}
587
588pub async fn read_workbook_sheet_info<'a>(
589 path_data: &PathData<'a>,
590) -> Result<IndexMap<String, usize>, GenericError> {
591 if let Ok(mut workbook) = open_workbook_auto(path_data.path()) {
592 let mut im: IndexMap<String, usize> = IndexMap::new();
593 for name in workbook.sheet_names() {
594 if let Ok(range) = workbook.worksheet_range(&name) {
595 im.insert(name, range.rows().count());
596 }
597 }
598 Ok(im)
599 } else {
600 Err(GenericError("cannot_open_workbook"))
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607 use crate::{helpers::*, Column};
608 use serde_json::json;
609 use std::path;
610
611 #[test]
612 fn test_direct_processing_xlsx() {
613 let sample_path = "data/sample-data-1.xlsx";
614
615 let opts = OptionSet::new(sample_path).max_row_count(1_000);
618
619 let result = process_spreadsheet_direct(&opts);
620
621 assert_eq!(result.unwrap().num_rows, 401);
623 }
624
625 #[test]
626 fn test_source_key_override_renames_and_reformats_without_position() {
627 let sample_path = "data/sample-data-1.csv";
630 let mut opts = OptionSet::new(sample_path).max_row_count(2);
631 opts.rows.columns = vec![
632 Column::from_source_key_with_format("weight", Some("weight_lbs"), Format::Integer, None, false, false),
633 ];
634
635 let result = process_spreadsheet_direct(&opts).unwrap();
636 assert!(result.keys.contains(&"id".to_string()));
638 assert!(result.keys.contains(&"first_name".to_string()));
639 assert!(result.keys.contains(&"weight_lbs".to_string()));
640 assert!(!result.keys.contains(&"weight".to_string()));
641
642 let rows = result.to_vec();
643 let first = rows.first().expect("at least one row");
644 assert!(first.get("weight_lbs").is_some());
645 assert!(first.get("weight").is_none());
646 assert_eq!(first.get("id").unwrap(), 1);
647 }
648
649 #[test]
650 fn test_csv_cell_integer_format_truncates_decimal_values() {
651 let cols = vec![Column::new_format(Format::Integer, None)];
655 let row_opts = RowOptionSet::simple(&cols);
656 let opts_arc = Arc::new(&row_opts);
657 assert_eq!(csv_cell_to_json_value("58.2", opts_arc.clone(), 0), Value::Number(Number::from(58)));
658 assert_eq!(csv_cell_to_json_value("82.5", opts_arc.clone(), 0), Value::Number(Number::from(82)));
659 assert_eq!(csv_cell_to_json_value("100", opts_arc.clone(), 0), Value::Number(Number::from(100)));
660 }
661
662 #[test]
663 fn test_csv_cell_does_not_coerce_ids_to_booleans() {
664 let row_opts = RowOptionSet::default();
669 let opts_arc = Arc::new(&row_opts);
670 assert_eq!(csv_cell_to_json_value("SKU001", opts_arc.clone(), 0), Value::String("SKU001".to_string()));
671 assert_eq!(csv_cell_to_json_value("A1", opts_arc.clone(), 0), Value::String("A1".to_string()));
672 assert_eq!(csv_cell_to_json_value("01/06/2024", opts_arc.clone(), 0), Value::String("01/06/2024".to_string()));
673 assert_eq!(csv_cell_to_json_value("true", opts_arc.clone(), 0), Value::Bool(true));
675 assert_eq!(csv_cell_to_json_value("false", opts_arc.clone(), 0), Value::Bool(false));
676 }
677
678 #[test]
679 fn test_direct_processing_csv() {
680 let sample_path = "data/sample-data-1.csv";
681
682 let opts = OptionSet::new(sample_path).max_row_count(1_000);
685
686 let result = process_spreadsheet_direct(&opts);
687
688 assert_eq!(result.unwrap().num_rows, 401);
690 }
691
692 #[test]
693 fn test_multisheet_preview_ods() {
694 let sample_path = "data/sample-data-2.ods";
695
696 let opts = OptionSet::new(sample_path)
701 .max_row_count(10)
702 .read_mode_preview();
703
704 let result = process_spreadsheet_direct(&opts);
705
706 let dataset = result.unwrap();
708 assert_eq!(dataset.sheets.len(), 2);
709 assert_eq!(dataset.num_rows, 118);
711
712 assert_eq!(dataset.data.first_sheet().len(), 10);
714 }
715
716 #[test]
717 fn test_column_override_1() {
718 let sample_json = json!({
719 "sku": "CHAIR16",
720 "height": "112cm",
721 "width": "69cm",
722 "approved": "Y"
723 });
724
725 let rows = json_object_to_calamine_data(sample_json);
726
727 let cols = vec![
728 Column::new_format(Format::Text, Some(string_value(""))),
729 Column::new_format(Format::Float, Some(float_value(95.0))),
730 Column::new_format(Format::Float, Some(float_value(65.0))),
731 Column::new_format(Format::Truthy, Some(bool_value(false))),
732 ];
733
734 let opts = &RowOptionSet::simple(&cols);
736 let result = workbook_row_to_values(&rows, opts);
737 assert_eq!(result.get(1).unwrap(), 112.0);
739 assert_eq!(result.get(2).unwrap(), 69.0);
741 assert_eq!(result.get(3).unwrap(), true);
743 }
744
745 #[test]
746 fn test_column_override_2() {
747 let sample_json = json!({
748 "name": "Sophia",
749 "dob": "2001-9-23",
750 "weight": "62kg",
751 "result": "GOOD"
752 });
753
754 let rows = json_object_to_calamine_data(sample_json);
755
756 let cols = vec![
757 Column::new_format(Format::Text, None),
758 Column::new_format(Format::Date, None),
759 Column::new_format(Format::Float, None),
760 Column::new_format(
762 Format::truthy_custom("good", "bad"),
763 Some(bool_value(false)),
764 ),
765 ];
766
767 let opts = &RowOptionSet::simple(&cols);
769 let result = workbook_row_to_values(&rows, opts);
770 assert_eq!(result.get(1).unwrap(), "2001-09-23");
771 assert_eq!(result.get(2).unwrap(), 62.0);
772 assert_eq!(result.get(3).unwrap(), true);
773 }
774
775 #[tokio::test]
776 async fn test_read_workbook_info() {
777 let sample_path = "data/sample-data-1.xlsx";
778 let path_data = PathData::new(path::Path::new(sample_path));
779 let info = read_workbook_sheet_info(&path_data).await;
780 assert!(info.is_ok());
781 }
782
783 #[tokio::test]
784 async fn test_large_csv_file() {
785 let sample_path = "data/large-datasheet.csv";
786 let max_rows = 100_000;
787 let opts = OptionSet::new(sample_path).max_row_count(max_rows);
788 let result = process_spreadsheet_core(&opts, None, None).await;
789 if let Ok(data) = result.clone() {
790 assert_eq!(data.data.first_sheet().len(), max_rows as usize);
791 } else {
792 panic!("Failed to process large CSV file");
793 }
794 assert!(result.is_ok());
795 }
796
797 #[tokio::test]
798 async fn test_medium_excel_file() {
799 let sample_path = "data/medium-spreadsheet-50_000.xlsx";
800 let max_rows = 5_000;
801 let opts = OptionSet::new(sample_path).max_row_count(max_rows);
802 let result = process_spreadsheet_core(&opts, None, None).await;
803 if let Ok(data) = result.clone() {
804 assert_eq!(data.data.first_sheet().len(), max_rows as usize);
805 } else {
806 panic!("Failed to process large Excel file");
807 }
808 assert!(result.is_ok());
809 }
810}