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};
7
8use is_truthy::TruthyRuleSet;
9pub const DEFAULT_MAX_ROWS: usize = 10_000;
11pub const DEFAULT_MAX_ROWS_PREVIEW: usize = 1000;
13
14#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
19pub enum DateTimeMode {
20 #[default]
21 Full, Simple, DateOnly, TimeOnly, HmOnly, }
31
32impl std::fmt::Display for DateTimeMode {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 let result = match self {
35 Self::Full => "date/time",
36 Self::Simple => "simple date/time",
37 Self::DateOnly => "date only",
38 Self::TimeOnly => "time only",
39 Self::HmOnly => "hours:minutes only",
40 };
41 write!(f, "{}", result)
42 }
43}
44
45#[derive(Debug, Clone, Default)]
47pub struct RowOptionSet {
48 pub columns: Vec<Column>,
49 pub decimal_comma: bool, pub datetime_mode: DateTimeMode,
56}
57
58impl RowOptionSet {
59
60 pub fn simple(cols: &[Column]) -> Self {
62 RowOptionSet {
63 decimal_comma: false,
64 datetime_mode: DateTimeMode::Full,
65 columns: cols.to_vec()
66 }
67 }
68
69 pub fn new(cols: &[Column], decimal_comma: bool, datetime_mode: DateTimeMode) -> Self {
71 RowOptionSet {
72 decimal_comma,
73 datetime_mode,
74 columns: cols.to_vec()
75 }
76 }
77
78 pub fn column(&self, index: usize) -> Option<&Column> {
79 self.columns.get(index)
80 }
81
82 pub fn date_mode(&self) -> String {
83 self.datetime_mode.to_string()
84 }
85
86 pub fn decimal_separator(&self) -> String {
87 if self.decimal_comma {
88 ","
89 } else {
90 "."
91 }.to_string()
92 }
93}
94
95#[derive(Debug, Clone, Default)]
97pub struct OptionSet {
98 pub selected: Option<Vec<String>>, pub indices: Vec<u32>, pub path: Option<String>, pub rows: RowOptionSet,
102 pub jsonl: bool,
103 pub max: Option<u32>,
104 pub omit_header: bool,
105 pub header_row: Option<usize>,
109 pub data_row_index: Option<usize>,
115 pub detect_header: bool,
123 pub read_mode: ReadMode,
124 pub field_mode: FieldNameMode
125}
126
127impl OptionSet {
128 pub fn new(path_str: &str) -> Self {
130 OptionSet {
131 selected: None,
132 indices: vec![0],
133 path: Some(path_str.to_string()),
134 rows: RowOptionSet::default(),
135 jsonl: false,
136 max: None,
137 omit_header: false,
138 header_row: None,
139 data_row_index: None,
140 detect_header: false,
141 read_mode: ReadMode::Sync,
142 field_mode: FieldNameMode::AutoA1,
143 }
144 }
145
146 pub fn sheet_name(mut self, name: &str) -> Self {
148 self.selected = Some(vec![name.to_string()]);
149 self
150 }
151
152 pub fn sheet_names(mut self, names: &[String]) -> Self {
154 self.selected = Some(names.to_vec());
155 self
156 }
157
158 pub fn sheet_index(mut self, index: u32) -> Self {
160 self.indices = vec![index];
161 self
162 }
163
164 pub fn sheet_indices(mut self, indices: &[u32]) -> Self {
166 self.indices = indices.to_vec();
167 self
168}
169
170 pub fn json_lines(mut self) -> Self {
172 self.jsonl = true;
173 self
174 }
175
176 pub fn set_json_lines(mut self, mode: bool) -> Self {
178 self.jsonl = mode;
179 self
180 }
181
182 pub fn omit_header(mut self) -> Self {
184 self.omit_header = true;
185 self
186 }
187
188 pub fn header_row(mut self, row: usize) -> Self {
190 self.header_row = Some(row);
191 self
192 }
193
194 pub fn data_row_index(mut self, row: usize) -> Self {
200 self.data_row_index = Some(row);
201 self
202 }
203
204 pub fn detect_header(mut self) -> Self {
208 self.detect_header = true;
209 self
210 }
211
212 pub fn max_row_count(mut self, max: u32) -> Self {
214 self.max = Some(max);
215 self
216 }
217
218 pub fn read_mode_async(mut self) -> Self {
221 self.read_mode = ReadMode::Async;
222 self
223 }
224
225 pub fn read_mode_preview(mut self) -> Self {
228 self.read_mode = ReadMode::PreviewMultiple;
229 self
230}
231
232 pub fn set_read_mode(mut self, key: &str) -> Self {
236 self.read_mode = ReadMode::from_key(key);
237 self
238 }
239
240 pub fn multimode(&self) -> bool {
241 self.read_mode.is_multimode()
242 }
243
244 pub fn file_name(&self) -> Option<String> {
245 if let Some(path_str) = self.path.clone() {
246 Path::new(&path_str).file_name().map(|f| f.to_string_lossy().to_string())
247 } else {
248 None
249 }
250 }
251
252 pub fn override_headers(mut self, keys: &[&str]) -> Self {
254 let mut columns: Vec<Column> = Vec::with_capacity(keys.len());
255 for ck in keys {
256 columns.push(Column::new(Some(&ck.to_snake_case())));
257 }
258 self.rows = RowOptionSet::simple(&columns);
259 self
260 }
261
262 pub fn override_columns(mut self, cols: &[Value]) -> Self {
264 let mut columns: Vec<Column> = Vec::with_capacity(cols.len());
265 for json_value in cols {
266 columns.push(Column::from_json(json_value));
267 }
268 self.rows = RowOptionSet::simple(&columns);
269 self
270 }
271
272 pub fn field_name_mode(mut self, system: &str, override_header: bool) -> Self {
274 self.field_mode = FieldNameMode::from_key(system, override_header);
275 self
276 }
277
278 pub fn row_mode(&self) -> String {
279 if self.jsonl {
280 "JSON lines"
281 } else {
282 "JSON"
283 }.to_string()
284 }
285
286 pub fn header_mode(&self) -> String {
287 if self.omit_header {
288 "ignore"
289 } else {
290 "capture"
291 }.to_string()
292 }
293
294 pub fn to_json(&self) -> Value {
296
297 let mut output: IndexMap<String, Value> = IndexMap::new();
298 if let Some(selected) = self.selected.clone() {
299 let selected = if self.multimode() {
300 json!({
301 "sheets": selected,
302 "indices": self.indices.clone()
303 })
304 } else {
305 json!({
306 "sheet": selected.first().unwrap_or(&"".to_string()),
307 "index": self.indices.first().unwrap_or(&0)
308 })
309 };
310 output.insert("selected".to_string(), selected);
311 }
312 if let Some(fname) = self.file_name() {
313 output.insert("file name".to_string(), fname.into());
314 }
315 if let Some(max_val) = self.max {
316 output.insert("max".to_string(), max_val.into());
317 }
318 output.insert("omit_header".to_string(), self.omit_header.into());
319 output.insert("header_row".to_string(), self.header_row.into());
320 output.insert("data_row_index".to_string(), self.data_row_index.into());
321 output.insert("detect_header".to_string(), self.detect_header.into());
322 output.insert("read_mode".to_string(), self.read_mode.to_string().into());
323 output.insert("jsonl".to_string(), self.jsonl.into());
324 output.insert("decimal_separator".to_string(), self.rows.decimal_separator().into());
325 output.insert("date_mode".to_string(), self.rows.date_mode().into());
326 if !self.columns().is_empty() {
327 let columns: Vec<Value> = self.rows.columns.clone().into_iter().map(|c| c.to_json()).collect();
328 output.insert("columns".to_string(), columns.into());
329 }
330 json!(output)
331 }
332
333 pub fn index_list(&self) -> String {
334 self.indices.clone().into_iter().map(|s| s.to_string()).collect::<Vec<String>>().join(", ")
335 }
336
337 pub fn to_lines(&self) -> Vec<String> {
340 let mut lines = vec![];
341 if let Some(s_names) = self.selected.clone() {
342 let plural = if s_names.len() > 1 {
343 "s"
344 } else {
345 ""
346 };
347 lines.push(format!("sheet name{}: {}", plural, s_names.join(",")));
348 } else if !self.indices.is_empty() {
349 lines.push(format!("sheet indices: {}", self.index_list()));
350 }
351 if let Some(fname) = self.file_name() {
352 lines.push(format!("file name: {}", fname));
353 }
354 if self.max.is_some() {
355 let max_val = self.max.unwrap_or(0);
356 if max_val > 0 {
357 lines.push(format!("max rows: {}", max_val));
358 }
359 }
360 lines.extend(vec![
361 format!("mode: {}", self.row_mode()),
362 format!("headers: {}", self.header_mode()),
363 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() })),
364 format!("data row index: {}", self.data_row_index.map(|v| v.to_string()).unwrap_or_else(|| "default (immediately after header)".to_string())),
365 format!("decimal separator: {}", self.rows.decimal_separator()),
366 format!("date mode: {}", self.rows.date_mode()),
367 format!("column style: {}", self.field_mode.to_string())
368 ]);
369
370 if !self.columns().is_empty() {
371 lines.push("columns:".to_string());
372 for col in self.rows.columns.clone() {
373 lines.push(col.to_line());
374 }
375 }
376 lines
377 }
378
379 pub fn header_row_index(&self) -> usize {
385 self.header_row.unwrap_or(0)
386 }
387
388 pub fn first_data_row_index(&self) -> Option<usize> {
407 if self.header_row.is_none() && self.data_row_index.is_none() {
408 return None;
409 }
410 let header_row_index = self.header_row_index();
411 let default_start = if self.omit_header { header_row_index } else { header_row_index + 1 };
412 match self.data_row_index {
413 Some(requested) if requested >= header_row_index => Some(requested),
414 _ => Some(default_start),
415 }
416 }
417
418 pub fn max_rows(&self) -> usize {
420 if let Some(mr) = self.max {
421 mr as usize
422 } else {
423 match self.read_mode {
424 ReadMode::PreviewMultiple => DEFAULT_MAX_ROWS_PREVIEW,
425 _ => DEFAULT_MAX_ROWS
426 }
427 }
428 }
429
430 #[allow(dead_code)]
432 pub fn columns(&self) -> Vec<Column> {
433 self.rows.columns.clone()
434 }
435
436 pub fn read_mode(&self) -> ReadMode {
438 self.read_mode
439 }
440
441 pub fn is_async(&self) -> bool {
443 self.read_mode.is_async()
444 }
445
446 pub fn capture_rows(&self) -> bool {
448 !matches!(self.read_mode, ReadMode::Async)
449 }
450
451}
452
453
454#[derive(Debug, Clone)]
456pub enum Format {
457 Auto, Text, Integer, Decimal(u8), Float, Boolean, Date, DateTime, DateTimeSimple, Time, Hm, DateTimeCustom(Arc<str>),
469 Truthy, #[allow(dead_code)]
471 TruthyCustom(TruthyRuleSet) }
473
474impl std::fmt::Display for Format {
475 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476 let result = match self {
477 Self::Auto => "auto".to_string(),
478 Self::Text => "text".to_string(),
479 Self::Integer => "integer".to_string(),
480 Self::Decimal(n) => format!("decimal({})", n),
481 Self::Float => "float".to_string(),
482 Self::Boolean => "boolean".to_string(),
483 Self::Date => "date".to_string(),
484 Self::DateTime => "datetime".to_string(),
485 Self::DateTimeSimple => "simple".to_string(),
486 Self::Time => "time".to_string(),
487 Self::Hm => "hm".to_string(),
488 Self::DateTimeCustom(fmt) => format!("datetime({})", fmt),
489 Self::Truthy => "truthy".to_string(),
490 Self::TruthyCustom(rules) => {
491 let true_str: Vec<String> = rules.true_options().iter().map(|o| o.pattern().to_string()).collect();
492 let false_str: Vec<String> = rules.false_options().iter().map(|o| o.pattern().to_string()).collect();
493 format!("truthy({},{})", true_str.join("|"), false_str.join("|"))
494 },
495 };
496 write!(f, "{}", result)
497 }
498}
499
500impl FromStr for Format {
501 type Err = Error;
502 fn from_str(key: &str) -> Result<Self, Self::Err> {
503 let fmt = match key {
504 "s" | "str" | "string" | "t" | "txt" | "text" => Self::Text,
505 "i" | "int" | "integer" => Self::Integer,
506 "d1" | "decimal_1" => Self::Decimal(1),
507 "d2" | "decimal_2" => Self::Decimal(2),
508 "d3" | "decimal_3" => Self::Decimal(3),
509 "d4" | "decimal_4" => Self::Decimal(4),
510 "d5" | "decimal_5" => Self::Decimal(5),
511 "d6" | "decimal_6" => Self::Decimal(6),
512 "d7" | "decimal_7" => Self::Decimal(7),
513 "d8" | "decimal_8" => Self::Decimal(8),
514 "fl" | "f" | "float" => Self::Float,
515 "b" | "bool" | "boolean" => Self::Boolean,
516 "da" | "date" => Self::Date,
517 "dt" | "datetime" => Self::DateTime,
518 "ds" | "simple" | "datetime_simple" => Self::DateTimeSimple,
519 "ti" | "time" => Self::Time,
520 "hm" | "hoursminutes" => Self::Hm,
521 "tr" | "truthy" => Self::Truthy,
522 _ => {
523 if let Some(str) = match_custom_dt(key) {
524 Self::DateTimeCustom(Arc::from(str))
525 } else if let Some((yes, no)) = match_custom_truthy(key) {
526 Self::TruthyCustom(TruthyRuleSet::new().add_true(&yes).add_false(&no))
527 } else {
528 Self::Auto
529 }
530 },
531 };
532 Ok(fmt)
533 }
534}
535
536fn match_custom_dt(key: &str) -> Option<String> {
537 let test_str = key.trim();
538 if test_str.starts_with_ci("dt:") {
539 Some(test_str[3..].to_string())
540 } else {
541 None
542 }
543}
544
545fn match_custom_truthy(key: &str) -> Option<(String,String)> {
546 let test_str = key.trim();
547 if let (Some(head), Some(tail)) = test_str.to_head_tail(":") {
548 if tail.len() > 1 && head.len() > 1 && head.starts_with_ci("tr") {
549 if let (Some(yes), Some(no)) = tail.to_head_tail(",") {
550 if !yes.is_empty() && !no.is_empty() {
551 return Some((yes.to_string(), no.to_string()));
552 }
553 }
554 }
555 }
556 None
557}
558
559impl Format {
560 #[allow(dead_code)]
561 pub fn truthy_custom(yes: &str, no: &str) -> Self {
562 Format::TruthyCustom(TruthyRuleSet::new().add_true(yes).add_false(no))
563 }
564}
565
566fn datetime_mode_from_json(json: &Value) -> DateTimeMode {
571 if let Some(mode_str) = json.get("datetime_mode").and_then(|v| v.as_str()) {
572 return match mode_str {
573 "date" | "date_only" => DateTimeMode::DateOnly,
574 "time" | "time_only" => DateTimeMode::TimeOnly,
575 "hm" | "hm_only" => DateTimeMode::HmOnly,
576 _ => DateTimeMode::Full,
577 };
578 }
579 if json.get("date_only").and_then(|v| v.as_bool()).unwrap_or(false) {
580 DateTimeMode::DateOnly
581 } else if json.get("time_only").and_then(|v| v.as_bool()).unwrap_or(false) {
582 DateTimeMode::TimeOnly
583 } else if json.get("hm_only").and_then(|v| v.as_bool()).unwrap_or(false) {
584 DateTimeMode::HmOnly
585 } else {
586 DateTimeMode::Full
587 }
588}
589
590#[derive(Debug, Clone)]
591pub struct Column {
592 pub key: Option<Arc<str>>,
593 pub source_key: Option<Arc<str>>,
597 pub format: Format,
598 pub default: Option<Value>,
599 pub datetime_mode: DateTimeMode,
606 pub decimal_comma: bool, }
608
609impl Column {
610
611 pub fn new(key_opt: Option<&str>) -> Self {
613 Self::from_key_ref_with_format(key_opt, Format::Auto, None, DateTimeMode::Full, false)
614 }
615
616 pub fn new_format(fmt: Format, default: Option<Value>) -> Self {
618 Self::from_key_ref_with_format(None, fmt, default, DateTimeMode::Full, false)
619 }
620
621 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 {
625 let mut col = Self::from_key_ref_with_format(key_opt, format, default, datetime_mode, decimal_comma);
626 col.source_key = Some(Arc::from(source_key));
627 col
628 }
629
630 pub fn from_json(json: &Value) -> Self {
632 let key_opt = json.get("key").map(|v| v.as_str().unwrap_or(""));
633 let source_key = json.get("source_key").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
634 let fmt = match json.get("format").and_then(|v| v.as_str()) {
635 Some(fmt_str) => {
636 match Format::from_str(fmt_str) {
637 Ok(fmt) => fmt,
638 Err(_) => Format::Auto
639 }
640 },
641 None => Format::Auto
642 };
643 let default = match json.get("default") {
644 Some(def_val) => {
645 match def_val {
646 Value::String(s) => Some(Value::String(s.clone())),
647 Value::Number(n) => Some(Value::Number(n.clone())),
648 Value::Bool(b) => Some(Value::Bool(*b)),
649 _ => None
650 }
651 },
652 None => None
653 };
654 let datetime_mode = datetime_mode_from_json(json);
655 let dec_commas_keys = ["decimal_comma", "dec_comma"];
656 let mut decimal_comma = false;
657
658 for key in &dec_commas_keys {
659 if let Some(euro_val) = json.get(*key) {
660 decimal_comma = euro_val.as_bool().unwrap_or(false);
661 break;
662 }
663 }
664 if let Some(src) = source_key {
665 Column::from_source_key_with_format(src, key_opt, fmt, default, datetime_mode, decimal_comma)
666 } else {
667 Column::from_key_ref_with_format(key_opt, fmt, default, datetime_mode, decimal_comma)
668 }
669}
670
671
672 #[allow(dead_code)]
674 pub fn set_format(mut self, fmt: Format) -> Self {
675 self.format = fmt;
676 self
677 }
678
679 #[allow(dead_code)]
680 pub fn set_default(mut self, val: Value) -> Self {
681 self.default = Some(val);
682 self
683 }
684
685 #[allow(dead_code)]
686 pub fn set_datetime_mode(mut self, val: DateTimeMode) -> Self {
687 self.datetime_mode = val;
688 self
689 }
690
691 #[allow(dead_code)]
692 pub fn set_decimal_comma(mut self, val: bool) -> Self {
693 self.decimal_comma = val;
694 self
695 }
696
697 pub fn from_key_ref_with_format(key_opt: Option<&str>, format: Format, default: Option<Value>, datetime_mode: DateTimeMode, decimal_comma: bool) -> Self {
698 let mut key = None;
699 if let Some(k_str) = key_opt {
700 key = Some(Arc::from(k_str));
701 }
702 Column {
703 key,
704 source_key: None,
705 format,
706 default,
707 datetime_mode,
708 decimal_comma
709 }
710 }
711
712 pub fn key_name(&self) -> String {
713 self.key.clone().unwrap_or(Arc::from("")).to_string()
714 }
715
716 pub fn source_key_name(&self) -> String {
717 self.source_key.clone().unwrap_or(Arc::from("")).to_string()
718 }
719
720 pub fn to_json(&self) -> Value {
721 json!({
722 "key": self.key_name(),
723 "source_key": self.source_key_name(),
724 "format": self.format.to_string(),
725 "default": self.default,
726 "datetime_mode": self.datetime_mode.to_string(),
727 "decimal_comma": self.decimal_comma
728 })
729 }
730
731 pub fn to_line(&self) -> String {
732 let datetime_mode_str = if self.datetime_mode != DateTimeMode::Full {
733 format!(", {}", self.datetime_mode)
734 } else {
735 "".to_string()
736 };
737 let def_string = if let Some(def_val) = self.default.clone() {
738 format!("default: {}", def_val)
739 } else {
740 "".to_string()
741 };
742 let comma_str = if self.decimal_comma {
743 ", decimal comma"
744 } else {
745 ""
746 };
747 let source_str = if self.source_key.is_some() {
748 format!(", matched from {}", self.source_key_name())
749 } else {
750 "".to_string()
751 };
752 format!(
753 "\tkey {}, format {}{}{}{}{}",
754 self.key_name(),
755 self.format,
756 def_string,
757 datetime_mode_str,
758 comma_str,
759 source_str)
760 }
761
762}
763
764
765#[derive(Debug, Clone, Copy)]
768pub enum Extension {
769 Unmatched,
770 Ods,
771 Xlsx,
772 Xlsm,
773 Xlsb,
774 Xls,
775 Csv,
776 Tsv,
777}
778
779impl Extension {
780 pub fn from_path(path:&Path) -> Extension {
781 if let Some(ext) = path.extension() {
782 if let Some(ext_str) = ext.to_str() {
783 let ext_lc = ext_str.to_lowercase();
784 return match ext_lc.as_str() {
785 "ods" => Extension::Ods,
786 "xlsx" => Extension::Xlsx,
787 "xlsm" => Extension::Xlsm,
792 "xlsb" => Extension::Xlsb,
793 "xls" => Extension::Xls,
794 "csv" => Extension::Csv,
795 "tsv" => Extension::Tsv,
796 _ => Extension::Unmatched
797 }
798 }
799 }
800 Extension::Unmatched
801 }
802
803 pub fn use_calamine(&self) -> bool {
805 matches!(self, Self::Ods | Self::Xlsx | Self::Xlsm | Self::Xlsb | Self::Xls)
806 }
807
808 #[allow(dead_code)]
811 pub fn use_csv(&self) -> bool {
812 matches!(self, Self::Csv | Self::Tsv)
813 }
814
815}
816
817impl std::fmt::Display for Extension {
818 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
819 let result = match self {
820 Self::Ods => "ods",
821 Self::Xlsx => "xlsx",
822 Self::Xlsm => "xlsm",
823 Self::Xlsb => "xlsb",
824 Self::Xls => "xls",
825 Self::Csv => "csv",
826 Self::Tsv => "tsv",
827 _ => ""
828 };
829 write!(f, "{}", result)
830 }
831}
832
833pub struct PathData<'a> {
834 path: &'a Path,
835 ext: Extension
836}
837
838impl<'a> PathData<'a> {
839 pub fn new(path: &'a Path) -> Self {
840 PathData {
841 path,
842 ext: Extension::from_path(path)
843 }
844 }
845
846 pub fn mode(&self) -> Extension {
847 self.ext
848 }
849
850 pub fn extension(&self) -> String {
851 self.ext.to_string()
852 }
853
854 pub fn ext(&self) -> Extension {
855 self.ext
856 }
857
858 pub fn path(&self) -> &Path {
859 self.path
860 }
861
862 pub fn is_valid(&self) -> bool {
863 !matches!(self.ext, Extension::Unmatched)
864 }
865
866 pub fn use_calamine(&self) -> bool {
867 self.ext.use_calamine()
868 }
869
870 pub fn filename(&self) -> String {
871 if let Some(file_ref) = self.path.file_name() {
872 file_ref.to_string_lossy().to_string()
873 } else {
874 "".to_owned()
875 }
876 }
877}
878
879
880
881#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
882pub enum ReadMode {
883 #[default]
884 Sync,
885 PreviewMultiple,
886 Async
887}
888
889impl ReadMode {
891
892 pub fn from_key(key: &str) -> Self {
893 let sample = key.to_lowercase().strip_non_alphanum();
894 match sample.as_str() {
895 "async" | "defer" | "deferred" | "a" => ReadMode::Async,
896 "preview" | "p" | "pre" | "multimode" | "multiple" | "previewmultiple" | "previewmulti" | "m" => ReadMode::PreviewMultiple,
897 _ => ReadMode::Sync
898 }
899 }
900
901 pub fn is_async(&self) -> bool {
902 matches!(self, Self::Async)
903 }
904
905 pub fn is_multimode(&self) -> bool {
907 matches!(self, Self::PreviewMultiple)
908 }
909}
910
911impl std::fmt::Display for ReadMode {
912 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
913 let result = match self {
914 Self::Async => "deferred",
915 Self::PreviewMultiple => "preview",
916 _ => "direct"
917 };
918 write!(f, "{}", result)
919 }
920}
921
922#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
924pub enum FieldNameMode {
925 #[default]
926 AutoA1, AutoNumPadded, A1, NumPadded, }
931
932impl FieldNameMode {
934
935
936 pub fn from_key(system: &str, override_header: bool) -> Self {
937 if system.starts_with_ci("a1") {
938 if override_header {
939 FieldNameMode::A1
940 } else {
941 FieldNameMode::AutoA1
942 }
943 } else if system.starts_with_ci("c") || system.starts_with_ci("n") {
944 if override_header {
945 FieldNameMode::NumPadded
946 } else {
947 FieldNameMode::AutoNumPadded
948 }
949 } else {
950 FieldNameMode::AutoA1
951 }
952 }
953
954
955 pub fn use_a1(&self) -> bool {
957 matches!(self, Self::AutoA1 | Self::A1)
958 }
959
960 pub fn use_c01(&self) -> bool {
962 matches!(self, Self::AutoNumPadded | Self::NumPadded)
963 }
964
965 pub fn override_headers(&self) -> bool {
967 matches!(self, Self::NumPadded | Self::A1)
968 }
969
970 pub fn keep_headers(&self) -> bool {
972 !self.override_headers()
973 }
974
975 pub fn forced_fallback(&self) -> Self {
980 if self.use_c01() {
981 Self::NumPadded
982 } else {
983 Self::A1
984 }
985 }
986}
987
988impl std::fmt::Display for FieldNameMode {
989 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
990 let result = match self {
991 Self::AutoNumPadded => "C01 auto",
992 Self::NumPadded => "C01 override",
993 Self::A1 => "A1 override",
994 _ => "A1 auto",
995 };
996 write!(f, "{}", result)
997 }
998}
999
1000#[cfg(test)]
1001mod tests {
1002 use super::*;
1003
1004 #[test]
1005 fn test_format_mode() {
1006 let custom_boolean = Format::truthy_custom("si", "no");
1007 assert_eq!(custom_boolean.to_string(), "truthy(si,no)");
1008 }
1009
1010 #[test]
1011 fn test_match_truthy_custom() {
1012 let (true_keys, false_keys) = match_custom_truthy("tr:si,no").unwrap();
1013 assert_eq!("si", true_keys);
1014 assert_eq!("no", false_keys);
1015 }
1016
1017 #[test]
1018 fn test_first_data_row_index_defaults_to_none_when_both_unset() {
1019 let opts = OptionSet::new("x.xlsx");
1022 assert_eq!(opts.first_data_row_index(), None);
1023 }
1024
1025 #[test]
1026 fn test_first_data_row_index_defaults_to_right_after_header_row() {
1027 let opts = OptionSet::new("x.xlsx").header_row(2);
1030 assert_eq!(opts.first_data_row_index(), Some(3));
1031 }
1032
1033 #[test]
1034 fn test_first_data_row_index_honors_explicit_gap() {
1035 let opts = OptionSet::new("x.xlsx").header_row(2).data_row_index(4);
1038 assert_eq!(opts.first_data_row_index(), Some(4));
1039 }
1040
1041 #[test]
1042 fn test_first_data_row_index_honors_data_row_equal_to_header() {
1043 let opts = OptionSet::new("x.xlsx").header_row(2).data_row_index(2);
1047 assert_eq!(opts.first_data_row_index(), Some(2));
1048 }
1049
1050 #[test]
1051 fn test_first_data_row_index_ignores_data_row_before_header() {
1052 let opts = OptionSet::new("x.xlsx").header_row(2).data_row_index(0);
1055 assert_eq!(opts.first_data_row_index(), Some(3));
1056 }
1057
1058 #[test]
1059 fn test_first_data_row_index_with_only_data_row_set() {
1060 let opts = OptionSet::new("x.xlsx").data_row_index(1);
1063 assert_eq!(opts.first_data_row_index(), Some(1));
1064 }
1065
1066 #[test]
1067 fn test_xlsm_is_recognised_and_routed_through_calamine() {
1068 let ext = Extension::from_path(Path::new("workbook.xlsm"));
1073 assert!(matches!(ext, Extension::Xlsm));
1074 assert!(ext.use_calamine());
1075 assert_eq!(ext.to_string(), "xlsm");
1076 }
1077
1078}