1use heck::ToSnakeCase;
2use indexmap::IndexMap;
3use serde_json::{json, Error, Value};
4use simple_string_patterns::{SimpleMatch, StripCharacters};
5use to_segments::ToSegments;
6use std::{path::Path, str::FromStr, sync::Arc};
7use enclose_strings::SimpleExtract;
8
9use is_truthy::TruthyRuleSet;
10use crate::key_segment::KeySegment;
11pub const DEFAULT_MAX_ROWS: usize = 10_000;
13pub const DEFAULT_MAX_ROWS_PREVIEW: usize = 1000;
15
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
21pub enum DateTimeMode {
22 #[default]
23 Full, Simple, DateOnly, TimeOnly, HmOnly, }
33
34impl std::fmt::Display for DateTimeMode {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 let result = match self {
37 Self::Full => "date/time",
38 Self::Simple => "simple date/time",
39 Self::DateOnly => "date only",
40 Self::TimeOnly => "time only",
41 Self::HmOnly => "hours:minutes only",
42 };
43 write!(f, "{}", result)
44 }
45}
46
47#[derive(Debug, Clone, Default)]
49pub struct RowOptionSet {
50 pub columns: Vec<Column>,
51 pub decimal_comma: bool, pub datetime_mode: DateTimeMode,
58 pub omit_null_values: bool,
64}
65
66impl RowOptionSet {
67
68 pub fn simple(cols: &[Column]) -> Self {
70 RowOptionSet {
71 decimal_comma: false,
72 datetime_mode: DateTimeMode::Full,
73 omit_null_values: false,
74 columns: cols.to_vec()
75 }
76 }
77
78 pub fn new(cols: &[Column], decimal_comma: bool, datetime_mode: DateTimeMode) -> Self {
80 RowOptionSet {
81 decimal_comma,
82 datetime_mode,
83 omit_null_values: false,
84 columns: cols.to_vec()
85 }
86 }
87
88 pub fn column(&self, index: usize) -> Option<&Column> {
89 self.columns.get(index)
90 }
91
92 pub fn date_mode(&self) -> String {
93 self.datetime_mode.to_string()
94 }
95
96 pub fn decimal_separator(&self) -> String {
97 if self.decimal_comma {
98 ","
99 } else {
100 "."
101 }.to_string()
102 }
103}
104
105#[derive(Debug, Clone, Default)]
107pub struct OptionSet {
108 pub selected: Option<Vec<String>>, pub indices: Vec<u32>, pub path: Option<String>, pub rows: RowOptionSet,
112 pub jsonl: bool,
113 pub max: Option<u32>,
114 pub omit_header: bool,
115 pub header_row: Option<usize>,
119 pub data_row_index: Option<usize>,
125 pub header_row_span: usize,
136 pub detect_header: bool,
144 pub read_mode: ReadMode,
145 pub field_mode: FieldNameMode
146}
147
148impl OptionSet {
149 pub fn new(path_str: &str) -> Self {
151 OptionSet {
152 selected: None,
153 indices: vec![0],
154 path: Some(path_str.to_string()),
155 rows: RowOptionSet::default(),
156 jsonl: false,
157 max: None,
158 omit_header: false,
159 header_row: None,
160 data_row_index: None,
161 header_row_span: 1,
162 detect_header: false,
163 read_mode: ReadMode::Sync,
164 field_mode: FieldNameMode::AutoA1,
165 }
166 }
167
168 pub fn sheet_name(mut self, name: &str) -> Self {
170 self.selected = Some(vec![name.to_string()]);
171 self
172 }
173
174 pub fn sheet_names(mut self, names: &[String]) -> Self {
176 self.selected = Some(names.to_vec());
177 self
178 }
179
180 pub fn sheet_index(mut self, index: u32) -> Self {
182 self.indices = vec![index];
183 self
184 }
185
186 pub fn sheet_indices(mut self, indices: &[u32]) -> Self {
188 self.indices = indices.to_vec();
189 self
190}
191
192 pub fn json_lines(mut self) -> Self {
194 self.jsonl = true;
195 self
196 }
197
198 pub fn set_json_lines(mut self, mode: bool) -> Self {
200 self.jsonl = mode;
201 self
202 }
203
204 pub fn omit_header(mut self) -> Self {
206 self.omit_header = true;
207 self
208 }
209
210 pub fn header_row(mut self, row: usize) -> Self {
212 self.header_row = Some(row);
213 self
214 }
215
216 pub fn data_row_index(mut self, row: usize) -> Self {
222 self.data_row_index = Some(row);
223 self
224 }
225
226 pub fn header_row_span(mut self, span: usize) -> Self {
230 self.header_row_span = span;
231 self
232 }
233
234 pub fn effective_header_row_span(&self) -> usize {
238 self.header_row_span.max(1)
239 }
240
241 pub fn detect_header(mut self) -> Self {
245 self.detect_header = true;
246 self
247 }
248
249 pub fn max_row_count(mut self, max: u32) -> Self {
251 self.max = Some(max);
252 self
253 }
254
255 pub fn read_mode_async(mut self) -> Self {
258 self.read_mode = ReadMode::Async;
259 self
260 }
261
262 pub fn read_mode_preview(mut self) -> Self {
265 self.read_mode = ReadMode::PreviewMultiple;
266 self
267}
268
269 pub fn set_read_mode(mut self, key: &str) -> Self {
273 self.read_mode = ReadMode::from_key(key);
274 self
275 }
276
277 pub fn multimode(&self) -> bool {
278 self.read_mode.is_multimode()
279 }
280
281 pub fn file_name(&self) -> Option<String> {
282 if let Some(path_str) = self.path.clone() {
283 Path::new(&path_str).file_name().map(|f| f.to_string_lossy().to_string())
284 } else {
285 None
286 }
287 }
288
289 pub fn override_headers(mut self, keys: &[&str]) -> Self {
291 let mut columns: Vec<Column> = Vec::with_capacity(keys.len());
292 for ck in keys {
293 columns.push(Column::new(Some(&ck.to_snake_case())));
294 }
295 self.rows = RowOptionSet::simple(&columns);
296 self
297 }
298
299 pub fn override_columns(mut self, cols: &[Value]) -> Self {
301 let mut columns: Vec<Column> = Vec::with_capacity(cols.len());
302 for json_value in cols {
303 columns.push(Column::from_json(json_value));
304 }
305 self.rows = RowOptionSet::simple(&columns);
306 self
307 }
308
309 pub fn field_name_mode(mut self, system: &str, override_header: bool) -> Self {
311 self.field_mode = FieldNameMode::from_key(system, override_header);
312 self
313 }
314
315 pub fn row_mode(&self) -> String {
316 if self.jsonl {
317 "JSON lines"
318 } else {
319 "JSON"
320 }.to_string()
321 }
322
323 pub fn header_mode(&self) -> String {
324 if self.omit_header {
325 "ignore"
326 } else {
327 "capture"
328 }.to_string()
329 }
330
331 pub fn to_json(&self) -> Value {
333
334 let mut output: IndexMap<String, Value> = IndexMap::new();
335 if let Some(selected) = self.selected.clone() {
336 let selected = if self.multimode() {
337 json!({
338 "sheets": selected,
339 "indices": self.indices.clone()
340 })
341 } else {
342 json!({
343 "sheet": selected.first().unwrap_or(&"".to_string()),
344 "index": self.indices.first().unwrap_or(&0)
345 })
346 };
347 output.insert("selected".to_string(), selected);
348 }
349 if let Some(fname) = self.file_name() {
350 output.insert("file name".to_string(), fname.into());
351 }
352 if let Some(max_val) = self.max {
353 output.insert("max".to_string(), max_val.into());
354 }
355 output.insert("omit_header".to_string(), self.omit_header.into());
356 output.insert("header_row".to_string(), self.header_row.into());
357 output.insert("data_row_index".to_string(), self.data_row_index.into());
358 output.insert("detect_header".to_string(), self.detect_header.into());
359 output.insert("read_mode".to_string(), self.read_mode.to_string().into());
360 output.insert("jsonl".to_string(), self.jsonl.into());
361 output.insert("decimal_separator".to_string(), self.rows.decimal_separator().into());
362 output.insert("date_mode".to_string(), self.rows.date_mode().into());
363 if !self.columns().is_empty() {
364 let columns: Vec<Value> = self.rows.columns.clone().into_iter().map(|c| c.to_json()).collect();
365 output.insert("columns".to_string(), columns.into());
366 }
367 json!(output)
368 }
369
370 pub fn index_list(&self) -> String {
371 self.indices.clone().into_iter().map(|s| s.to_string()).collect::<Vec<String>>().join(", ")
372 }
373
374 pub fn to_lines(&self) -> Vec<String> {
377 let mut lines = vec![];
378 if let Some(s_names) = self.selected.clone() {
379 let plural = if s_names.len() > 1 {
380 "s"
381 } else {
382 ""
383 };
384 lines.push(format!("sheet name{}: {}", plural, s_names.join(",")));
385 } else if !self.indices.is_empty() {
386 lines.push(format!("sheet indices: {}", self.index_list()));
387 }
388 if let Some(fname) = self.file_name() {
389 lines.push(format!("file name: {}", fname));
390 }
391 if self.max.is_some() {
392 let max_val = self.max.unwrap_or(0);
393 if max_val > 0 {
394 lines.push(format!("max rows: {}", max_val));
395 }
396 }
397 lines.extend(vec![
398 format!("mode: {}", self.row_mode()),
399 format!("headers: {}", self.header_mode()),
400 format!("header row: {}", self.header_row.map(|v| v.to_string()).unwrap_or_else(|| if self.detect_header { "auto-detect".to_string() } else { "0 (default)".to_string() })),
401 format!("data row index: {}", self.data_row_index.map(|v| v.to_string()).unwrap_or_else(|| "default (immediately after header)".to_string())),
402 format!("decimal separator: {}", self.rows.decimal_separator()),
403 format!("date mode: {}", self.rows.date_mode()),
404 format!("column style: {}", self.field_mode.to_string())
405 ]);
406
407 if !self.columns().is_empty() {
408 lines.push("columns:".to_string());
409 for col in self.rows.columns.clone() {
410 lines.push(col.to_line());
411 }
412 }
413 lines
414 }
415
416 pub fn header_row_index(&self) -> usize {
422 self.header_row.unwrap_or(0)
423 }
424
425 pub fn first_data_row_index(&self) -> Option<usize> {
444 if self.header_row.is_none() && self.data_row_index.is_none() && self.effective_header_row_span() <= 1 {
445 return None;
446 }
447 let header_row_index = self.header_row_index();
448 let default_start = if self.omit_header { header_row_index } else { header_row_index + self.effective_header_row_span() };
449 match self.data_row_index {
450 Some(requested) if requested >= header_row_index => Some(requested),
451 _ => Some(default_start),
452 }
453 }
454
455 pub fn max_rows(&self) -> usize {
457 if let Some(mr) = self.max {
458 mr as usize
459 } else {
460 match self.read_mode {
461 ReadMode::PreviewMultiple => DEFAULT_MAX_ROWS_PREVIEW,
462 _ => DEFAULT_MAX_ROWS
463 }
464 }
465 }
466
467 #[allow(dead_code)]
469 pub fn columns(&self) -> Vec<Column> {
470 self.rows.columns.clone()
471 }
472
473 pub fn read_mode(&self) -> ReadMode {
475 self.read_mode
476 }
477
478 pub fn is_async(&self) -> bool {
480 self.read_mode.is_async()
481 }
482
483 pub fn capture_rows(&self) -> bool {
485 !matches!(self.read_mode, ReadMode::Async)
486 }
487
488}
489
490
491#[derive(Debug, Clone)]
493pub enum Format {
494 Auto, Text, Integer, Decimal(u8), Float, Boolean, Date, DateTime, DateTimeSimple, Time, Hm, DateTimeCustom(Arc<str>),
506 Truthy, #[allow(dead_code)]
508 TruthyCustom(TruthyRuleSet), Array(Arc<Format>, Arc<str>),
516}
517
518impl std::fmt::Display for Format {
519 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520 let result = match self {
521 Self::Auto => "auto".to_string(),
522 Self::Text => "text".to_string(),
523 Self::Integer => "integer".to_string(),
524 Self::Decimal(n) => format!("decimal({})", n),
525 Self::Float => "float".to_string(),
526 Self::Boolean => "boolean".to_string(),
527 Self::Date => "date".to_string(),
528 Self::DateTime => "datetime".to_string(),
529 Self::DateTimeSimple => "datetime_simple".to_string(),
530 Self::Time => "time".to_string(),
531 Self::Hm => "hm".to_string(),
532 Self::DateTimeCustom(fmt) => format!("datetime({})", fmt),
533 Self::Truthy => "truthy".to_string(),
534 Self::TruthyCustom(rules) => {
535 let true_str: Vec<String> = rules.true_options().iter().map(|o| o.pattern().to_string()).collect();
536 let false_str: Vec<String> = rules.false_options().iter().map(|o| o.pattern().to_string()).collect();
537 format!("truthy({},{})", true_str.join("|"), false_str.join("|"))
538 },
539 Self::Array(fmt, sep) => format!("array({},{})", fmt, sep),
540 };
541 write!(f, "{}", result)
542 }
543}
544
545impl FromStr for Format {
546 type Err = Error;
547 fn from_str(key: &str) -> Result<Self, Self::Err> {
548 let is_array = str::contains(key, "[]");
549 let base_key = if is_array {
550 key.to_head("[")
551 } else {
552 key.to_string()
553 };
554 let clean_key = base_key.trim().to_lowercase().strip_non_alphanum();
555
556 let array_splitter = if is_array {
557 let raw = key.to_tail("]").extract_from_parentheses().unwrap_or(",".to_string());
558 let unquoted = raw.extract_from_single_quotes()
562 .or_else(|| raw.extract_from_double_quotes())
563 .unwrap_or(raw);
564 Some(unquoted)
565 } else {
566 None
567 };
568 let fmt = match clean_key.as_str() {
569 "s" | "str" | "string" | "t" | "txt" | "text" => Self::Text,
570 "i" | "int" | "integer" => Self::Integer,
571 "d1" | "dec1" | "decimal1" => Self::Decimal(1),
572 "d2" | "dec2" | "decimal2" => Self::Decimal(2),
573 "d3" | "dec3" | "decimal3" => Self::Decimal(3),
574 "d4" | "dec4" | "decimal4" => Self::Decimal(4),
575 "d5" | "dec5" | "decimal5" => Self::Decimal(5),
576 "d6" | "dec6" | "decimal6" => Self::Decimal(6),
577 "d7" | "dec7" | "decimal7" => Self::Decimal(7),
578 "d8" | "dec8" | "decimal8" => Self::Decimal(8),
579 "fl" | "f" | "float" => Self::Float,
580 "b" | "bool" | "boolean" => Self::Boolean,
581 "da" | "date" => Self::Date,
582 "dt" | "datetime" => Self::DateTime,
583 "ds" | "datetimesimple" => Self::DateTimeSimple,
584 "ti" | "time" => Self::Time,
585 "hm" | "hoursminutes" | "hourmin"=> Self::Hm,
586 "tr" | "truthy" | "true" => Self::Truthy,
587 _ => {
588 if let Some(str) = match_custom_dt(key) {
589 Self::DateTimeCustom(Arc::from(str))
590 } else if let Some((yes, no)) = match_custom_truthy(key) {
591 Self::TruthyCustom(TruthyRuleSet::new().add_true(&yes).add_false(&no))
592 } else {
593 Self::Auto
594 }
595 },
596 };
597 if is_array {
598 let splitter = array_splitter.unwrap_or(",".to_string());
599 Ok(Self::Array(Arc::new(fmt), Arc::from(splitter.as_str())))
600 } else {
601 Ok(fmt)
602 }
603 }
604}
605
606fn match_custom_dt(key: &str) -> Option<String> {
607 let test_str = key.trim();
608 if test_str.starts_with_ci("dt:") {
609 Some(test_str[3..].to_string())
610 } else {
611 None
612 }
613}
614
615fn match_custom_truthy(key: &str) -> Option<(String,String)> {
616 let test_str = key.trim();
617 if let (Some(head), Some(tail)) = test_str.to_head_tail(":") {
618 if tail.len() > 1 && head.len() > 1 && head.starts_with_ci("tr") {
619 if let (Some(yes), Some(no)) = tail.to_head_tail(",") {
620 if !yes.is_empty() && !no.is_empty() {
621 return Some((yes.to_string(), no.to_string()));
622 }
623 }
624 }
625 }
626 if test_str.to_head("(").starts_with_ci("tr") {
630 if let Some(inner) = test_str.extract_from_parentheses() {
631 if let (Some(yes), Some(no)) = inner.to_head_tail(",") {
632 if !yes.is_empty() && !no.is_empty() {
633 return Some((yes.to_string(), no.to_string()));
634 }
635 }
636 }
637 }
638 None
639}
640
641impl Format {
642 #[allow(dead_code)]
643 pub fn truthy_custom(yes: &str, no: &str) -> Self {
644 Format::TruthyCustom(TruthyRuleSet::new().add_true(yes).add_false(no))
645 }
646}
647
648fn datetime_mode_from_json(json: &Value) -> DateTimeMode {
653 if let Some(mode_str) = json.get("datetime_mode").and_then(|v| v.as_str()) {
654 return match mode_str {
655 "date" | "date_only" => DateTimeMode::DateOnly,
656 "time" | "time_only" => DateTimeMode::TimeOnly,
657 "hm" | "hm_only" => DateTimeMode::HmOnly,
658 _ => DateTimeMode::Full,
659 };
660 }
661 if json.get("date_only").and_then(|v| v.as_bool()).unwrap_or(false) {
662 DateTimeMode::DateOnly
663 } else if json.get("time_only").and_then(|v| v.as_bool()).unwrap_or(false) {
664 DateTimeMode::TimeOnly
665 } else if json.get("hm_only").and_then(|v| v.as_bool()).unwrap_or(false) {
666 DateTimeMode::HmOnly
667 } else {
668 DateTimeMode::Full
669 }
670}
671
672#[derive(Debug, Clone)]
673pub struct Column {
674 pub key: Option<KeySegment>,
679 pub source_key: Option<Arc<str>>,
683 pub format: Format,
684 pub default: Option<Value>,
685 pub datetime_mode: DateTimeMode,
692 pub decimal_comma: bool, }
694
695impl Column {
696
697 pub fn new(key_opt: Option<&str>) -> Self {
699 Self::from_key_ref_with_format(key_opt, Format::Auto, None, DateTimeMode::Full, false)
700 }
701
702 pub fn new_format(fmt: Format, default: Option<Value>) -> Self {
704 Self::from_key_ref_with_format(None, fmt, default, DateTimeMode::Full, false)
705 }
706
707 pub fn from_source_key_with_format(source_key: &str, key_opt: Option<&str>, format: Format, default: Option<Value>, datetime_mode: DateTimeMode, decimal_comma: bool) -> Self {
711 let mut col = Self::from_key_ref_with_format(key_opt, format, default, datetime_mode, decimal_comma);
712 col.source_key = Some(Arc::from(source_key));
713 col
714 }
715
716 pub fn from_json(json: &Value) -> Self {
718 let key_opt = json.get("key").map(|v| v.as_str().unwrap_or(""));
719 let source_key = json.get("source_key").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
720 let fmt = match json.get("format").and_then(|v| v.as_str()) {
721 Some(fmt_str) => {
722 match Format::from_str(fmt_str) {
723 Ok(fmt) => fmt,
724 Err(_) => Format::Auto
725 }
726 },
727 None => Format::Auto
728 };
729 let default = match json.get("default") {
730 Some(def_val) => {
731 match def_val {
732 Value::String(s) => Some(Value::String(s.clone())),
733 Value::Number(n) => Some(Value::Number(n.clone())),
734 Value::Bool(b) => Some(Value::Bool(*b)),
735 _ => None
736 }
737 },
738 None => None
739 };
740 let datetime_mode = datetime_mode_from_json(json);
741 let dec_commas_keys = ["decimal_comma", "dec_comma"];
742 let mut decimal_comma = false;
743
744 for key in &dec_commas_keys {
745 if let Some(euro_val) = json.get(*key) {
746 decimal_comma = euro_val.as_bool().unwrap_or(false);
747 break;
748 }
749 }
750 let mut col = if let Some(src) = source_key {
751 Column::from_source_key_with_format(src, key_opt, fmt, default, datetime_mode, decimal_comma)
752 } else {
753 Column::from_key_ref_with_format(key_opt, fmt, default, datetime_mode, decimal_comma)
754 };
755 if let Some(key_json) = json.get("key") {
763 if let Some(segment) = KeySegment::from_json(key_json) {
764 col.key = Some(segment);
765 }
766 }
767 col
768}
769
770
771 #[allow(dead_code)]
773 pub fn set_format(mut self, fmt: Format) -> Self {
774 self.format = fmt;
775 self
776 }
777
778 #[allow(dead_code)]
779 pub fn set_default(mut self, val: Value) -> Self {
780 self.default = Some(val);
781 self
782 }
783
784 #[allow(dead_code)]
785 pub fn set_datetime_mode(mut self, val: DateTimeMode) -> Self {
786 self.datetime_mode = val;
787 self
788 }
789
790 #[allow(dead_code)]
791 pub fn set_decimal_comma(mut self, val: bool) -> Self {
792 self.decimal_comma = val;
793 self
794 }
795
796 pub fn from_key_ref_with_format(key_opt: Option<&str>, format: Format, default: Option<Value>, datetime_mode: DateTimeMode, decimal_comma: bool) -> Self {
797 let key = key_opt.map(|k_str| KeySegment::Simple(Arc::from(k_str)));
798 Column {
799 key,
800 source_key: None,
801 format,
802 default,
803 datetime_mode,
804 decimal_comma
805 }
806 }
807
808 pub fn key_name(&self) -> String {
813 self.key.as_ref().map(|k| k.to_string()).unwrap_or_default()
814 }
815
816 pub fn source_key_name(&self) -> String {
817 self.source_key.clone().unwrap_or(Arc::from("")).to_string()
818 }
819
820 pub fn to_json(&self) -> Value {
821 json!({
822 "key": self.key_name(),
823 "source_key": self.source_key_name(),
824 "format": self.format.to_string(),
825 "default": self.default,
826 "datetime_mode": self.datetime_mode.to_string(),
827 "decimal_comma": self.decimal_comma
828 })
829 }
830
831 pub fn to_line(&self) -> String {
832 let datetime_mode_str = if self.datetime_mode != DateTimeMode::Full {
833 format!(", {}", self.datetime_mode)
834 } else {
835 "".to_string()
836 };
837 let def_string = if let Some(def_val) = self.default.clone() {
838 format!("default: {}", def_val)
839 } else {
840 "".to_string()
841 };
842 let comma_str = if self.decimal_comma {
843 ", decimal comma"
844 } else {
845 ""
846 };
847 let source_str = if self.source_key.is_some() {
848 format!(", matched from {}", self.source_key_name())
849 } else {
850 "".to_string()
851 };
852 format!(
853 "\tkey {}, format {}{}{}{}{}",
854 self.key_name(),
855 self.format,
856 def_string,
857 datetime_mode_str,
858 comma_str,
859 source_str)
860 }
861
862}
863
864
865#[derive(Debug, Clone, Copy)]
868pub enum Extension {
869 Unmatched,
870 Ods,
871 Xlsx,
872 Xlsm,
873 Xlsb,
874 Xls,
875 Csv,
876 Tsv,
877}
878
879impl Extension {
880 pub fn from_path(path:&Path) -> Extension {
881 if let Some(ext) = path.extension() {
882 if let Some(ext_str) = ext.to_str() {
883 let ext_lc = ext_str.to_lowercase();
884 return match ext_lc.as_str() {
885 "ods" => Extension::Ods,
886 "xlsx" => Extension::Xlsx,
887 "xlsm" => Extension::Xlsm,
892 "xlsb" => Extension::Xlsb,
893 "xls" => Extension::Xls,
894 "csv" => Extension::Csv,
895 "tsv" => Extension::Tsv,
896 _ => Extension::Unmatched
897 }
898 }
899 }
900 Extension::Unmatched
901 }
902
903 pub fn use_calamine(&self) -> bool {
905 matches!(self, Self::Ods | Self::Xlsx | Self::Xlsm | Self::Xlsb | Self::Xls)
906 }
907
908 #[allow(dead_code)]
911 pub fn use_csv(&self) -> bool {
912 matches!(self, Self::Csv | Self::Tsv)
913 }
914
915}
916
917impl std::fmt::Display for Extension {
918 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
919 let result = match self {
920 Self::Ods => "ods",
921 Self::Xlsx => "xlsx",
922 Self::Xlsm => "xlsm",
923 Self::Xlsb => "xlsb",
924 Self::Xls => "xls",
925 Self::Csv => "csv",
926 Self::Tsv => "tsv",
927 _ => ""
928 };
929 write!(f, "{}", result)
930 }
931}
932
933pub struct PathData<'a> {
934 path: &'a Path,
935 ext: Extension
936}
937
938impl<'a> PathData<'a> {
939 pub fn new(path: &'a Path) -> Self {
940 PathData {
941 path,
942 ext: Extension::from_path(path)
943 }
944 }
945
946 pub fn mode(&self) -> Extension {
947 self.ext
948 }
949
950 pub fn extension(&self) -> String {
951 self.ext.to_string()
952 }
953
954 pub fn ext(&self) -> Extension {
955 self.ext
956 }
957
958 pub fn path(&self) -> &Path {
959 self.path
960 }
961
962 pub fn is_valid(&self) -> bool {
963 !matches!(self.ext, Extension::Unmatched)
964 }
965
966 pub fn use_calamine(&self) -> bool {
967 self.ext.use_calamine()
968 }
969
970 pub fn filename(&self) -> String {
971 if let Some(file_ref) = self.path.file_name() {
972 file_ref.to_string_lossy().to_string()
973 } else {
974 "".to_owned()
975 }
976 }
977}
978
979
980
981#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
982pub enum ReadMode {
983 #[default]
984 Sync,
985 PreviewMultiple,
986 Async
987}
988
989impl ReadMode {
991
992 pub fn from_key(key: &str) -> Self {
993 let sample = key.to_lowercase().strip_non_alphanum();
994 match sample.as_str() {
995 "async" | "defer" | "deferred" | "a" => ReadMode::Async,
996 "preview" | "p" | "pre" | "multimode" | "multiple" | "previewmultiple" | "previewmulti" | "m" => ReadMode::PreviewMultiple,
997 _ => ReadMode::Sync
998 }
999 }
1000
1001 pub fn is_async(&self) -> bool {
1002 matches!(self, Self::Async)
1003 }
1004
1005 pub fn is_multimode(&self) -> bool {
1007 matches!(self, Self::PreviewMultiple)
1008 }
1009}
1010
1011impl std::fmt::Display for ReadMode {
1012 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1013 let result = match self {
1014 Self::Async => "deferred",
1015 Self::PreviewMultiple => "preview",
1016 _ => "direct"
1017 };
1018 write!(f, "{}", result)
1019 }
1020}
1021
1022#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1024pub enum FieldNameMode {
1025 #[default]
1026 AutoA1, AutoNumPadded, A1, NumPadded, }
1031
1032impl FieldNameMode {
1034
1035
1036 pub fn from_key(system: &str, override_header: bool) -> Self {
1037 if system.starts_with_ci("a1") {
1038 if override_header {
1039 FieldNameMode::A1
1040 } else {
1041 FieldNameMode::AutoA1
1042 }
1043 } else if system.starts_with_ci("c") || system.starts_with_ci("n") {
1044 if override_header {
1045 FieldNameMode::NumPadded
1046 } else {
1047 FieldNameMode::AutoNumPadded
1048 }
1049 } else {
1050 FieldNameMode::AutoA1
1051 }
1052 }
1053
1054
1055 pub fn use_a1(&self) -> bool {
1057 matches!(self, Self::AutoA1 | Self::A1)
1058 }
1059
1060 pub fn use_c01(&self) -> bool {
1062 matches!(self, Self::AutoNumPadded | Self::NumPadded)
1063 }
1064
1065 pub fn override_headers(&self) -> bool {
1067 matches!(self, Self::NumPadded | Self::A1)
1068 }
1069
1070 pub fn keep_headers(&self) -> bool {
1072 !self.override_headers()
1073 }
1074
1075 pub fn forced_fallback(&self) -> Self {
1080 if self.use_c01() {
1081 Self::NumPadded
1082 } else {
1083 Self::A1
1084 }
1085 }
1086}
1087
1088impl std::fmt::Display for FieldNameMode {
1089 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1090 let result = match self {
1091 Self::AutoNumPadded => "C01 auto",
1092 Self::NumPadded => "C01 override",
1093 Self::A1 => "A1 override",
1094 _ => "A1 auto",
1095 };
1096 write!(f, "{}", result)
1097 }
1098}
1099
1100#[cfg(test)]
1101mod tests {
1102 use super::*;
1103
1104 #[test]
1105 fn test_format_mode() {
1106 let custom_boolean = Format::truthy_custom("si", "no");
1107 assert_eq!(custom_boolean.to_string(), "truthy(si,no)");
1108 }
1109
1110 #[test]
1111 fn test_match_truthy_custom() {
1112 let (true_keys, false_keys) = match_custom_truthy("tr:si,no").unwrap();
1113 assert_eq!("si", true_keys);
1114 assert_eq!("no", false_keys);
1115
1116 let (true_keys, false_keys) = match_custom_truthy("true(vrai,faux)").unwrap();
1117 assert_eq!("vrai", true_keys);
1118 assert_eq!("faux", false_keys);
1119 }
1120
1121 fn assert_array_format(parsed: Format, expected_element: &str, expected_separator: &str) {
1122 match parsed {
1123 Format::Array(element_fmt, separator) => {
1124 assert_eq!(element_fmt.to_string(), expected_element);
1125 assert_eq!(separator.as_ref(), expected_separator);
1126 }
1127 other => panic!("expected Format::Array, got {:?}", other),
1128 }
1129 }
1130
1131 #[test]
1132 fn test_format_from_str_parses_the_documented_array_syntax() {
1133 assert_array_format(Format::from_str("string[](|)").unwrap(), "text", "|");
1134 assert_array_format(Format::from_str("int[](|)").unwrap(), "integer", "|");
1135 }
1136
1137 #[test]
1138 fn test_format_from_str_array_accepts_arbitrary_text_between_bracket_and_paren() {
1139 assert_array_format(Format::from_str("text[].split(,)").unwrap(), "text", ",");
1143 assert_array_format(Format::from_str("int[].split(|)").unwrap(), "integer", "|");
1144 }
1145
1146 #[test]
1147 fn test_format_from_str_array_strips_a_js_like_quoted_separator() {
1148 assert_array_format(Format::from_str("text[].split(',')").unwrap(), "text", ",");
1149 assert_array_format(Format::from_str(r#"text[].split(",")"#).unwrap(), "text", ",");
1150 assert_array_format(Format::from_str("text[](,)").unwrap(), "text", ",");
1153 }
1154
1155 #[test]
1156 fn test_format_from_str_array_defaults_to_comma_with_no_explicit_separator() {
1157 assert_array_format(Format::from_str("int[]").unwrap(), "integer", ",");
1158 assert_array_format(Format::from_str("string[]").unwrap(), "text", ",");
1159 }
1160
1161 #[test]
1162 fn test_format_from_str_array_accepts_the_same_loose_type_synonyms_as_scalars() {
1163 for (key, expected) in [
1166 ("s[](|)", "text"),
1167 ("str[](|)", "text"),
1168 ("STRING[](|)", "text"),
1169 ("i[](|)", "integer"),
1170 ("INTEGER[](|)", "integer"),
1171 ("fl[](|)", "float"),
1172 ("float[](|)", "float"),
1173 ("d2[](|)", "decimal(2)"),
1174 ("b[](|)", "boolean"),
1175 ("da[](|)", "date"),
1176 ] {
1177 assert_array_format(Format::from_str(key).unwrap(), expected, "|");
1178 }
1179 }
1180
1181 #[test]
1182 fn test_format_from_str_array_accepts_a_multi_character_separator() {
1183 assert_array_format(Format::from_str("string[](::)").unwrap(), "text", "::");
1184 }
1185
1186 #[test]
1187 fn test_format_from_str_array_with_an_unrecognised_element_type_falls_back_to_auto() {
1188 assert_array_format(Format::from_str("bogus[](|)").unwrap(), "auto", "|");
1191 }
1192
1193 #[test]
1194 fn test_format_from_str_non_array_syntax_is_unaffected() {
1195 assert_eq!(Format::from_str("int").unwrap().to_string(), "integer");
1198 assert_eq!(Format::from_str("string").unwrap().to_string(), "text");
1199 assert_eq!(Format::from_str("d3").unwrap().to_string(), "decimal(3)");
1200 }
1201
1202
1203 #[test]
1204 fn test_first_data_row_index_defaults_to_none_when_both_unset() {
1205 let opts = OptionSet::new("x.xlsx");
1208 assert_eq!(opts.first_data_row_index(), None);
1209 }
1210
1211 #[test]
1212 fn test_first_data_row_index_defaults_to_right_after_header_row() {
1213 let opts = OptionSet::new("x.xlsx").header_row(2);
1216 assert_eq!(opts.first_data_row_index(), Some(3));
1217 }
1218
1219 #[test]
1220 fn test_first_data_row_index_honors_explicit_gap() {
1221 let opts = OptionSet::new("x.xlsx").header_row(2).data_row_index(4);
1224 assert_eq!(opts.first_data_row_index(), Some(4));
1225 }
1226
1227 #[test]
1228 fn test_first_data_row_index_honors_data_row_equal_to_header() {
1229 let opts = OptionSet::new("x.xlsx").header_row(2).data_row_index(2);
1233 assert_eq!(opts.first_data_row_index(), Some(2));
1234 }
1235
1236 #[test]
1237 fn test_first_data_row_index_ignores_data_row_before_header() {
1238 let opts = OptionSet::new("x.xlsx").header_row(2).data_row_index(0);
1241 assert_eq!(opts.first_data_row_index(), Some(3));
1242 }
1243
1244 #[test]
1245 fn test_first_data_row_index_with_only_data_row_set() {
1246 let opts = OptionSet::new("x.xlsx").data_row_index(1);
1249 assert_eq!(opts.first_data_row_index(), Some(1));
1250 }
1251
1252 #[test]
1253 fn test_xlsm_is_recognised_and_routed_through_calamine() {
1254 let ext = Extension::from_path(Path::new("workbook.xlsm"));
1259 assert!(matches!(ext, Extension::Xlsm));
1260 assert!(ext.use_calamine());
1261 assert_eq!(ext.to_string(), "xlsm");
1262 }
1263
1264}