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 raw_values: Vec<String> = row.iter().map(|c| c.to_string()).collect();
198 if is_not_header_row(&raw_values, row_index, &col_keys) {
199 let row_map = workbook_row_to_map(row, &resolved_row_opts, &headers);
200 rows.push(row_map);
201 }
202 }
203 row_index += 1;
204 }
205 }
206 sheets.push(SheetDataSet::new(&sheet_ref, &headers, &rows, total));
207 sheet_index += 1;
208 }
209 Ok(ResultSet::from_multiple(&sheets, &info, opts))
210}
211
212pub async fn read_single_worksheet(
214 mut workbook: Sheets<BufReader<File>>,
215 sheet_ref: &str,
216 opts: &OptionSet,
217 info: &WorkbookInfo,
218 save_opt: Option<
219 Box<dyn Fn(IndexMap<String, Value>) -> Result<(), GenericError> + Send + Sync>,
220 >,
221 out_ref: Option<&str>,
222) -> Result<ResultSet, GenericError> {
223 let range = workbook.worksheet_range(sheet_ref)?;
224 let capture_rows = opts.capture_rows();
225 let columns = opts.rows.columns.clone();
226 let max_rows = opts.max_rows();
227 let mut headers: Vec<String> = vec![];
228 let mut col_keys: Vec<String> = vec![];
229 let mut has_headers = false;
230 let capture_headers = !opts.omit_header;
231 let mut rows: Vec<IndexMap<String, Value>> =
232 Vec::with_capacity(if capture_rows { max_rows } else { 0 });
233 let mut row_index = 0;
234 let header_row_index = opts.header_row_index();
235 let match_header_row_below = capture_headers && header_row_index > 0;
236 let mut resolved_row_opts = opts.rows.clone();
237
238 if let Some(first_row) = range.headers() {
239 let natural_keys = natural_column_keys(&first_row, &opts.field_mode);
240 let resolved_columns = resolve_columns(&columns, &natural_keys);
241 headers = build_header_keys(&first_row, &resolved_columns, &opts.field_mode);
242 resolved_row_opts.columns = resolved_columns;
243 has_headers = !match_header_row_below;
244 col_keys = first_row;
245 }
246 let total = range.get_size().0;
247 if capture_rows || match_header_row_below {
248 let max_row_count = if capture_rows {
249 max_rows
250 } else {
251 header_row_index + 2
252 };
253 let max_take = if total < max_row_count {
254 total
255 } else {
256 max_row_count + 1
257 };
258 for row in range.rows().take(max_take) {
259 if row_index > max_row_count {
260 break;
261 }
262 if match_header_row_below && (row_index + 1) == header_row_index {
263 let h_row = row
264 .into_iter()
265 .map(|c| c.to_string().to_snake_case())
266 .collect::<Vec<String>>();
267 let natural_keys = natural_column_keys(&h_row, &opts.field_mode);
268 let resolved_columns = resolve_columns(&columns, &natural_keys);
269 headers = build_header_keys(&h_row, &resolved_columns, &opts.field_mode);
270 resolved_row_opts.columns = resolved_columns;
271 has_headers = true;
272 } else if (has_headers || !capture_headers) && capture_rows {
273 let raw_values: Vec<String> = row.iter().map(|c| c.to_string()).collect();
275 if is_not_header_row(&raw_values, row_index, &col_keys) {
276 let row_map = workbook_row_to_map(row, &resolved_row_opts, &headers);
277 rows.push(row_map);
278 }
279 }
280 row_index += 1;
281 }
282 }
283 if let Some(save_method) = save_opt {
284 let mut row_iter = range.rows();
285 let mut save_count: usize = 0;
286 if let Some(first_row) = row_iter.next() {
287 let raw_values: Vec<String> = first_row.iter().map(|c| c.to_string()).collect();
288 if is_not_header_row(&raw_values, 0, &col_keys) {
289 let first_row_map = workbook_row_to_map(first_row, &resolved_row_opts, &headers);
290 save_method(first_row_map)?;
291 save_count += 1;
292 }
293 }
294 for row in row_iter {
295 if save_count >= max_rows {
296 break;
297 }
298 let row_map = workbook_row_to_map(row, &resolved_row_opts, &headers);
299 save_method(row_map)?;
300 save_count += 1;
301 }
302 }
303
304 let ds = DataSet::from_count_and_rows(total, rows, opts);
305 Ok(ResultSet::new(info, &headers, ds, opts, out_ref))
306}
307
308pub async fn read_csv_core<'a>(
311 path_data: &PathData<'a>,
312 opts: &OptionSet,
313 save_opt: Option<
314 Box<dyn Fn(IndexMap<String, Value>) -> Result<(), GenericError> + Send + Sync>,
315 >,
316 out_ref: Option<&str>,
317) -> Result<ResultSet, GenericError> {
318 let separator = match path_data.mode() {
319 Extension::Tsv => b't',
320 _ => b',',
321 };
322 if let Ok(mut rdr) = ReaderBuilder::new()
323 .delimiter(separator)
324 .from_path(path_data.path())
325 {
326 let capture_header = opts.omit_header == false;
327 let capture_rows = opts.capture_rows();
328 let max_line_usize = opts.max_rows();
329 let mut rows: Vec<IndexMap<String, Value>> =
330 Vec::with_capacity(if capture_rows { max_line_usize } else { 0 });
331 let mut line_count = 0;
332
333 let mut headers: Vec<String> = vec![];
334 let mut resolved_row_opts = opts.rows.clone();
335 if capture_header {
336 if let Ok(hdrs) = rdr.headers() {
337 headers = hdrs.into_iter().map(|s| s.to_owned()).collect();
338 }
339 let columns = opts.rows.columns.clone();
340 let natural_keys = natural_column_keys(&headers, &opts.field_mode);
341 let resolved_columns = resolve_columns(&columns, &natural_keys);
342 headers = build_header_keys(&headers, &resolved_columns, &opts.field_mode);
343 resolved_row_opts.columns = resolved_columns;
344 }
345
346 let mut total = 0;
347 let opts_rows_arc = Arc::new(&resolved_row_opts);
348 if capture_rows {
349 for result in rdr.records() {
350 if line_count >= max_line_usize {
351 break;
352 }
353 if let Some(row) = csv_row_result_to_values(result, opts_rows_arc.clone()) {
354 rows.push(to_index_map(&row, &headers));
355 line_count += 1;
356 }
357 }
358 total = line_count + rdr.records().count() + 1;
359 } else if let Some(save_method) = save_opt {
360 for result in rdr.records() {
361 if let Some(row) = csv_row_result_to_values(result, opts_rows_arc.clone()) {
362 let row_map = to_index_map(&row, &headers);
363 save_method(row_map)?;
364 total += 1;
365 }
366 }
367 } else {
368 if let Ok(mut count_rdr) = ReaderBuilder::new().from_path(&path_data.path()) {
369 total = count_rdr.records().count();
370 }
371 }
372 let info = WorkbookInfo::simple(path_data);
373 let ds = DataSet::from_count_and_rows(total, rows, opts);
374 Ok(ResultSet::new(&info, &headers, ds, opts, out_ref))
375 } else {
376 let error_msg = match path_data.ext() {
377 Extension::Tsv => "unreadable_tsv_file",
378 _ => "unreadable_csv_file",
379 };
380 Err(GenericError(error_msg))
381 }
382}
383
384fn workbook_row_to_map(
386 row: &[Data],
387 opts: &RowOptionSet,
388 headers: &[String],
389) -> IndexMap<String, Value> {
390 to_index_map(&workbook_row_to_values(row, opts), headers)
391}
392
393fn workbook_row_to_values(row: &[Data], opts: &RowOptionSet) -> Vec<Value> {
395 let opts_arc = Arc::new(opts);
396 let mut c_index = 0;
397 let mut cells: Vec<Value> = vec![];
398 for cell in row {
399 let value = workbook_cell_to_value(cell, opts_arc.clone(), c_index);
400 cells.push(value);
401 c_index += 1;
402 }
403 cells
404}
405
406fn workbook_cell_to_value(cell: &Data, opts: Arc<&RowOptionSet>, c_index: usize) -> Value {
408 let col = opts.column(c_index);
409 let format = col.map_or(Format::Auto, |c| c.format.to_owned());
410 let def_val = col.and_then(|c| c.default.clone());
411
412 let date_only = resolve_date_only(&format, opts.date_only);
413
414 match cell {
415 Data::Int(i) => Value::Number(Number::from_i128(*i as i128).unwrap()),
416 Data::Float(f) => process_float_value(*f, format),
417 Data::DateTimeIso(d) => process_iso_datetime_value(d, def_val, date_only),
418 Data::DateTime(d) => process_excel_datetime_value(d, def_val, date_only),
419 Data::Bool(b) => Value::Bool(*b),
420 Data::String(s) => process_string_value(s, format, def_val),
421 Data::Empty => def_val.unwrap_or(Value::Null),
422 _ => Value::String(cell.to_string()),
423 }
424}
425
426fn resolve_date_only(format: &Format, row_date_only: bool) -> bool {
430 match format {
431 Format::Date => true,
432 Format::DateTime => false,
433 _ => row_date_only,
434 }
435}
436
437fn process_float_value(value: f64, format: Format) -> Value {
438 match format {
439 Format::Integer => Value::Number(Number::from_i128(value as i128).unwrap()),
440 Format::Boolean => Value::Bool(value >= 1.0),
441 Format::Text => Value::String(value.to_string()),
442 _ => Value::Number(Number::from_f64(value).unwrap()),
443 }
444}
445
446fn process_excel_datetime_value(
447 datetime: &calamine::ExcelDateTime,
448 def_val: Option<Value>,
449 date_only: bool,
450) -> Value {
451 let dt_ref = datetime.as_datetime().map_or_else(
452 || def_val.unwrap_or(Value::Null),
453 |dt| {
454 let formatted_date = if date_only {
455 dt.format("%Y-%m-%d").to_string()
456 } else {
457 dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()
458 };
459 Value::String(formatted_date)
460 },
461 );
462 dt_ref
463}
464
465fn process_iso_datetime_value(dt_str: &str, def_val: Option<Value>, date_only: bool) -> Value {
466 if date_only {
467 iso_fuzzy_to_date_string(dt_str)
468 .map_or_else(|| def_val.unwrap_or(Value::Null), |dt| Value::String(dt))
469 } else {
470 iso_fuzzy_to_datetime_string(dt_str)
471 .map_or_else(|| def_val.unwrap_or(Value::Null), |dt| Value::String(dt))
472 }
473}
474
475fn process_string_value(value: &str, format: Format, def_val: Option<Value>) -> Value {
476 match format {
477 Format::Boolean => process_truthy_value(value, def_val, |v, ef| v.is_truthy_core(ef)),
478 Format::Truthy => process_truthy_value(value, def_val, |v, ef| v.is_truthy_standard(ef)),
479 Format::TruthyCustom(opts) => process_truthy_value(value, def_val, |v, _| {
480 v.is_truthy_custom(&opts)
481 }),
482 Format::Decimal(places) => {
483 process_numeric_value(value, def_val, |n| float_value(n.round_decimal(places)))
484 }
485 Format::Float => process_numeric_value(value, def_val, float_value),
486 Format::Date => process_date_value(value, def_val, iso_fuzzy_to_date_string),
487 Format::DateTime => process_date_value(value, def_val, iso_fuzzy_to_datetime_string),
488 _ => Value::String(value.to_owned()),
489 }
490}
491
492fn process_truthy_value<F>(value: &str, def_val: Option<Value>, truthy_fn: F) -> Value
493where
494 F: Fn(&str, bool) -> Option<bool>,
495{
496 if let Some(is_true) = truthy_fn(value, false) {
497 Value::Bool(is_true)
498 } else {
499 def_val.unwrap_or(Value::Null)
500 }
501}
502
503fn process_numeric_value<F>(value: &str, def_val: Option<Value>, numeric_fn: F) -> Value
504where
505 F: Fn(f64) -> Value,
506{
507 if let Some(n) = value.to_first_number::<f64>() {
508 numeric_fn(n)
509 } else {
510 def_val.unwrap_or(Value::Null)
511 }
512}
513
514fn process_date_value<F>(value: &str, def_val: Option<Value>, date_fn: F) -> Value
515where
516 F: Fn(&str) -> Option<String>,
517{
518 if let Some(date_str) = date_fn(value) {
519 string_value(&date_str)
520 } else {
521 def_val.unwrap_or(Value::Null)
522 }
523}
524
525fn csv_row_result_to_values(
527 result: Result<StringRecord, csv::Error>,
528 opts: Arc<&RowOptionSet>,
529) -> Option<Vec<Value>> {
530 if let Ok(record) = result {
531 let mut row: Vec<Value> = vec![];
532 let mut ci: usize = 0;
533 for cell in record.into_iter() {
534 let new_cell = csv_cell_to_json_value(cell, opts.clone(), ci);
535 row.push(new_cell);
536 ci += 1;
537 }
538 return Some(row);
539 }
540 None
541}
542
543fn csv_cell_to_json_value(cell: &str, opts: Arc<&RowOptionSet>, index: usize) -> Value {
545 let has_number = cell.to_first_number::<f64>().is_some();
546 let col = opts.column(index);
548 let (fmt, euro_num_mode) = if let Some(c) = col {
549 (c.format.clone(), c.decimal_comma)
550 } else {
551 (Format::Auto, opts.decimal_comma)
552 };
553 let num_cell = if has_number {
554 let euro_num_mode = uses_decimal_comma(cell, euro_num_mode);
555 if euro_num_mode {
556 cell.replace(",", ".").replace(",", ".")
557 } else {
558 cell.replace(",", "")
559 }
560 } else {
561 cell.to_owned()
562 };
563 let mut new_cell = Value::Null;
564 if num_cell.len() > 0 && num_cell.is_numeric() {
565 if let Ok(float_val) = serde_json::Number::from_str(&num_cell) {
566 match fmt {
567 Format::Integer => {
568 if let Some(f) = float_val.as_f64() {
573 if let Some(int_val) = Number::from_i128(f as i128) {
574 new_cell = Value::Number(int_val);
575 }
576 }
577 }
578 Format::Boolean => {
579 new_cell = Value::Bool(float_val.as_f64().unwrap_or(0f64) >= 1.0);
581 }
582 _ => {
583 new_cell = Value::Number(float_val);
584 }
585 }
586 }
587 } else if let Some(is_true) = cell.is_truthy_core(false) {
588 new_cell = Value::Bool(is_true);
589 } else {
590 new_cell = match fmt {
591 Format::Truthy => {
592 if let Some(is_true) = cell.is_truthy_standard(false) {
593 Value::Bool(is_true)
594 } else {
595 Value::Null
596 }
597 }
598 _ => Value::String(cell.to_string()),
599 };
600 }
601 new_cell
602}
603
604pub async fn read_workbook_sheet_info<'a>(
605 path_data: &PathData<'a>,
606) -> Result<IndexMap<String, usize>, GenericError> {
607 if let Ok(mut workbook) = open_workbook_auto(path_data.path()) {
608 let mut im: IndexMap<String, usize> = IndexMap::new();
609 for name in workbook.sheet_names() {
610 if let Ok(range) = workbook.worksheet_range(&name) {
611 im.insert(name, range.rows().count());
612 }
613 }
614 Ok(im)
615 } else {
616 Err(GenericError("cannot_open_workbook"))
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623 use crate::{helpers::*, Column};
624 use serde_json::json;
625 use std::path;
626
627 #[test]
628 fn test_direct_processing_xlsx() {
629 let sample_path = "data/sample-data-1.xlsx";
630
631 let opts = OptionSet::new(sample_path).max_row_count(1_000);
634
635 let result = process_spreadsheet_direct(&opts);
636
637 assert_eq!(result.unwrap().num_rows, 401);
639 }
640
641 #[test]
642 fn test_source_key_override_renames_and_reformats_without_position() {
643 let sample_path = "data/sample-data-1.csv";
646 let mut opts = OptionSet::new(sample_path).max_row_count(2);
647 opts.rows.columns = vec![
648 Column::from_source_key_with_format("weight", Some("weight_lbs"), Format::Integer, None, false, false),
649 ];
650
651 let result = process_spreadsheet_direct(&opts).unwrap();
652 assert!(result.keys.contains(&"id".to_string()));
654 assert!(result.keys.contains(&"first_name".to_string()));
655 assert!(result.keys.contains(&"weight_lbs".to_string()));
656 assert!(!result.keys.contains(&"weight".to_string()));
657
658 let rows = result.to_vec();
659 let first = rows.first().expect("at least one row");
660 assert!(first.get("weight_lbs").is_some());
661 assert!(first.get("weight").is_none());
662 assert_eq!(first.get("id").unwrap(), 1);
663 }
664
665 #[test]
666 fn test_resolve_date_only_prefers_column_format_over_row_default() {
667 assert_eq!(resolve_date_only(&Format::Date, false), true);
670 assert_eq!(resolve_date_only(&Format::DateTime, true), false);
671 assert_eq!(resolve_date_only(&Format::Auto, false), false);
672 assert_eq!(resolve_date_only(&Format::Auto, true), true);
673 }
674
675 #[test]
676 fn test_source_key_override_casts_native_datetime_cell_to_date_only() {
677 let sample_path = "data/sample-data-1.xlsx";
682 let mut opts = OptionSet::new(sample_path).max_row_count(1);
683 opts.rows.columns = vec![
684 Column::from_source_key_with_format("start_time", None, Format::Date, None, false, false),
685 ];
686
687 let result = process_spreadsheet_direct(&opts).unwrap();
688 let rows = result.to_vec();
689 let first = rows.first().expect("at least one row");
690 let start_time = first.get("start_time").expect("start_time column").as_str().unwrap();
691 assert_eq!(start_time, "2023-06-15");
692 assert!(!start_time.contains('T'), "should be date-only, got: {}", start_time);
693 }
694
695 #[test]
696 fn test_csv_cell_integer_format_truncates_decimal_values() {
697 let cols = vec![Column::new_format(Format::Integer, None)];
701 let row_opts = RowOptionSet::simple(&cols);
702 let opts_arc = Arc::new(&row_opts);
703 assert_eq!(csv_cell_to_json_value("58.2", opts_arc.clone(), 0), Value::Number(Number::from(58)));
704 assert_eq!(csv_cell_to_json_value("82.5", opts_arc.clone(), 0), Value::Number(Number::from(82)));
705 assert_eq!(csv_cell_to_json_value("100", opts_arc.clone(), 0), Value::Number(Number::from(100)));
706 }
707
708 #[test]
709 fn test_csv_cell_does_not_coerce_ids_to_booleans() {
710 let row_opts = RowOptionSet::default();
715 let opts_arc = Arc::new(&row_opts);
716 assert_eq!(csv_cell_to_json_value("SKU001", opts_arc.clone(), 0), Value::String("SKU001".to_string()));
717 assert_eq!(csv_cell_to_json_value("A1", opts_arc.clone(), 0), Value::String("A1".to_string()));
718 assert_eq!(csv_cell_to_json_value("01/06/2024", opts_arc.clone(), 0), Value::String("01/06/2024".to_string()));
719 assert_eq!(csv_cell_to_json_value("true", opts_arc.clone(), 0), Value::Bool(true));
721 assert_eq!(csv_cell_to_json_value("false", opts_arc.clone(), 0), Value::Bool(false));
722 }
723
724 #[test]
725 fn test_direct_processing_csv() {
726 let sample_path = "data/sample-data-1.csv";
727
728 let opts = OptionSet::new(sample_path).max_row_count(1_000);
731
732 let result = process_spreadsheet_direct(&opts);
733
734 assert_eq!(result.unwrap().num_rows, 401);
736 }
737
738 #[test]
739 fn test_multisheet_preview_ods() {
740 let sample_path = "data/sample-data-2.ods";
741
742 let opts = OptionSet::new(sample_path)
747 .max_row_count(10)
748 .read_mode_preview();
749
750 let result = process_spreadsheet_direct(&opts);
751
752 let dataset = result.unwrap();
754 assert_eq!(dataset.sheets.len(), 2);
755 assert_eq!(dataset.num_rows, 118);
757
758 assert_eq!(dataset.data.first_sheet().len(), 10);
760 }
761
762 #[test]
763 fn test_column_override_1() {
764 let sample_json = json!({
765 "sku": "CHAIR16",
766 "height": "112cm",
767 "width": "69cm",
768 "approved": "Y"
769 });
770
771 let rows = json_object_to_calamine_data(sample_json);
772
773 let cols = vec![
774 Column::new_format(Format::Text, Some(string_value(""))),
775 Column::new_format(Format::Float, Some(float_value(95.0))),
776 Column::new_format(Format::Float, Some(float_value(65.0))),
777 Column::new_format(Format::Truthy, Some(bool_value(false))),
778 ];
779
780 let opts = &RowOptionSet::simple(&cols);
782 let result = workbook_row_to_values(&rows, opts);
783 assert_eq!(result.get(1).unwrap(), 112.0);
785 assert_eq!(result.get(2).unwrap(), 69.0);
787 assert_eq!(result.get(3).unwrap(), true);
789 }
790
791 #[test]
792 fn test_column_override_2() {
793 let sample_json = json!({
794 "name": "Sophia",
795 "dob": "2001-9-23",
796 "weight": "62kg",
797 "result": "GOOD"
798 });
799
800 let rows = json_object_to_calamine_data(sample_json);
801
802 let cols = vec![
803 Column::new_format(Format::Text, None),
804 Column::new_format(Format::Date, None),
805 Column::new_format(Format::Float, None),
806 Column::new_format(
808 Format::truthy_custom("good", "bad"),
809 Some(bool_value(false)),
810 ),
811 ];
812
813 let opts = &RowOptionSet::simple(&cols);
815 let result = workbook_row_to_values(&rows, opts);
816 assert_eq!(result.get(1).unwrap(), "2001-09-23");
817 assert_eq!(result.get(2).unwrap(), 62.0);
818 assert_eq!(result.get(3).unwrap(), true);
819 }
820
821 #[tokio::test]
822 async fn test_read_workbook_info() {
823 let sample_path = "data/sample-data-1.xlsx";
824 let path_data = PathData::new(path::Path::new(sample_path));
825 let info = read_workbook_sheet_info(&path_data).await;
826 assert!(info.is_ok());
827 }
828
829 #[tokio::test]
830 async fn test_large_csv_file() {
831 let sample_path = "data/large-datasheet.csv";
832 let max_rows = 100_000;
833 let opts = OptionSet::new(sample_path).max_row_count(max_rows);
834 let result = process_spreadsheet_core(&opts, None, None).await;
835 if let Ok(data) = result.clone() {
836 assert_eq!(data.data.first_sheet().len(), max_rows as usize);
837 } else {
838 panic!("Failed to process large CSV file");
839 }
840 assert!(result.is_ok());
841 }
842
843 #[tokio::test]
844 async fn test_medium_excel_file() {
845 let sample_path = "data/medium-spreadsheet-50_000.xlsx";
846 let max_rows = 5_000;
847 let opts = OptionSet::new(sample_path).max_row_count(max_rows);
848 let result = process_spreadsheet_core(&opts, None, None).await;
849 if let Ok(data) = result.clone() {
850 assert_eq!(data.data.first_sheet().len(), max_rows as usize);
851 } else {
852 panic!("Failed to process large Excel file");
853 }
854 assert!(result.is_ok());
855 }
856}