Skip to main content

spreadsheet_to_json/
options.rs

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;
9/// default max number of rows in direct single sheet mode without an override via ->max_row_count(max_row_count)
10pub const DEFAULT_MAX_ROWS: usize = 10_000;
11/// default max number of rows multiple sheet preview mode without an override via ->max_row_count(max_row_count)
12pub const DEFAULT_MAX_ROWS_PREVIEW: usize = 1000;
13
14/// Row parsing options with nested column options
15#[derive(Debug, Clone, Default)]
16pub struct RowOptionSet {
17  pub columns: Vec<Column>,
18  pub decimal_comma: bool, // always parse as euro number format
19  pub date_only: bool,
20}
21
22impl RowOptionSet {
23
24  // simple constructor with column keys only
25  pub fn simple(cols: &[Column]) -> Self {
26    RowOptionSet {
27      decimal_comma: false,
28      date_only: false,
29      columns: cols.to_vec()
30    }
31  }
32
33  // lets you set all options
34  pub fn new(cols: &[Column], decimal_comma: bool, date_only: bool) -> Self {
35    RowOptionSet {
36      decimal_comma,
37      date_only,
38      columns: cols.to_vec()
39    }
40  }
41
42  pub fn column(&self, index: usize) -> Option<&Column> {
43    self.columns.get(index)
44  }
45
46  pub fn date_mode(&self) -> String {
47    if self.date_only {
48      "date only"
49    } else {
50      "date/time"
51    }.to_string()
52  }
53
54  pub fn decimal_separator(&self) -> String {
55    if self.decimal_comma {
56      ","
57    } else {
58      "."
59    }.to_string()
60  }
61}
62
63/// Core options with nested row options
64#[derive(Debug, Clone, Default)]
65pub struct OptionSet {
66  pub selected: Option<Vec<String>>, // Optional sheet name reference. Will default to index value if not matched
67  pub indices: Vec<u32>, // worksheet index
68  pub path: Option<String>, // path argument. If None, do not attempt to parse
69  pub rows: RowOptionSet,
70  pub jsonl: bool,
71  pub max: Option<u32>,
72  pub omit_header: bool,
73  pub header_row: u8,
74  pub read_mode: ReadMode,
75  pub field_mode: FieldNameMode
76}
77
78impl OptionSet {
79  /// Instantiates a new option set with a path string for file operations.
80  pub fn new(path_str: &str) -> Self {
81    OptionSet {
82        selected: None,
83        indices: vec![0],
84        path: Some(path_str.to_string()),
85        rows: RowOptionSet::default(),
86        jsonl: false,
87        max: None,
88        omit_header: false,
89        header_row: 0,
90        read_mode: ReadMode::Sync,
91        field_mode: FieldNameMode::AutoA1,
92    }
93  }
94
95  /// Sets the sheet name for the operation.
96  pub fn sheet_name(mut self, name: &str) -> Self {
97    self.selected = Some(vec![name.to_string()]);
98    self
99  }
100
101  /// Sets the sheet name for the operation.
102  pub fn sheet_names(mut self, names: &[String]) -> Self {
103    self.selected = Some(names.to_vec());
104    self
105  }
106
107  /// Sets the sheet index.
108  pub fn sheet_index(mut self, index: u32) -> Self {
109      self.indices = vec![index];
110      self
111  }
112
113  /// Sets the sheet index.
114  pub fn sheet_indices(mut self, indices: &[u32]) -> Self {
115    self.indices = indices.to_vec();
116    self
117}
118
119  /// Sets JSON Lines mode to true.
120  pub fn json_lines(mut self) -> Self {
121      self.jsonl = true;
122      self
123  }
124
125  /// Sets JSON Lines mode
126  pub fn set_json_lines(mut self, mode: bool) -> Self {
127    self.jsonl = mode;
128    self
129  }
130
131  /// Omits the header when reading.
132  pub fn omit_header(mut self) -> Self {
133      self.omit_header = true;
134      self
135  }
136
137  /// Sets the header row index.
138  pub fn header_row(mut self, row: u8) -> Self {
139      self.header_row = row;
140      self
141  }
142
143  /// Sets the maximum number of rows to read.
144  pub fn max_row_count(mut self, max: u32) -> Self {
145      self.max = Some(max);
146      self
147  }
148
149  /// Sets the read mode to asynchronous, single sheet mode
150  /// This is for reading long files with 10K+ rows in the target sheet
151  pub fn read_mode_async(mut self) -> Self {
152      self.read_mode = ReadMode::Async;
153      self
154  }
155
156   /// Sets the read mode to direct with multiple sheet output
157   /// This serves to fetch quick a overview of a spreadsheet
158   pub fn read_mode_preview(mut self) -> Self {
159    self.read_mode = ReadMode::PreviewMultiple;
160    self
161}
162
163  /// Sets read mode from a range of common key names
164  /// async, preview or sync (default) with synonyms such as `a`, `p` and `s`
165  /// If the key is unmatched, it will always default to Sync
166  pub fn set_read_mode(mut self, key: &str) -> Self {
167    self.read_mode = ReadMode::from_key(key);
168    self
169  }
170
171  pub fn multimode(&self) -> bool {
172    self.read_mode.is_multimode()
173  }
174
175  pub fn file_name(&self) -> Option<String> {
176    if let Some(path_str) = self.path.clone() {
177      Path::new(&path_str).file_name().map(|f| f.to_string_lossy().to_string())
178    } else {
179      None
180    }
181  }
182
183  /// Override matched and unmatched headers with custom headers.
184  pub fn override_headers(mut self, keys: &[&str]) -> Self {
185    let mut columns: Vec<Column> = Vec::with_capacity(keys.len());
186    for ck in keys {
187        columns.push(Column::new(Some(&ck.to_snake_case())));
188    }
189    self.rows = RowOptionSet::simple(&columns);
190    self
191  }
192
193  /// Override matched and unmatched columns with custom keys and/or formatting options
194  pub fn override_columns(mut self, cols: &[Value]) -> Self {
195    let mut columns: Vec<Column> = Vec::with_capacity(cols.len());
196    for json_value in cols {
197        columns.push(Column::from_json(json_value));
198    }
199    self.rows = RowOptionSet::simple(&columns);
200    self
201  }
202
203  /// Sets the column key naming convention.
204  pub fn field_name_mode(mut self, system: &str, override_header: bool) -> Self {
205      self.field_mode = FieldNameMode::from_key(system, override_header);
206      self
207  }
208
209  pub fn row_mode(&self) -> String {
210    if self.jsonl {
211      "JSON lines"
212    } else {
213      "JSON"
214    }.to_string()
215  }
216
217  pub fn header_mode(&self) -> String {
218    if self.omit_header {
219      "ignore"
220    } else {
221      "capture"
222    }.to_string()
223  }
224
225  /// render option output contextually as JSON
226  pub fn to_json(&self) -> Value {
227    
228    let mut output: IndexMap<String, Value> = IndexMap::new();
229    if let Some(selected) =  self.selected.clone() {
230      let selected = if self.multimode() {
231        json!({
232          "sheets": selected,
233          "indices": self.indices.clone()
234        })
235      } else {
236        json!({
237          "sheet": selected.first().unwrap_or(&"".to_string()),
238          "index": self.indices.first().unwrap_or(&0)
239        })
240      };
241      output.insert("selected".to_string(), selected);
242    }
243    if let Some(fname) = self.file_name() {
244      output.insert("file name".to_string(), fname.into());
245    }
246    if let Some(max_val) = self.max {
247      output.insert("max".to_string(), max_val.into());
248    }
249    output.insert("omit_header".to_string(), self.omit_header.into());
250    output.insert("header_row".to_string(), self.header_row.into());
251    output.insert("read_mode".to_string(), self.read_mode.to_string().into());
252    output.insert("jsonl".to_string(), self.jsonl.into());
253    output.insert("decimal_separator".to_string(), self.rows.decimal_separator().into());
254    output.insert("date_only".to_string(), self.rows.date_only.into());
255    if !self.columns().is_empty() {
256      let columns: Vec<Value> = self.rows.columns.clone().into_iter().map(|c| c.to_json()).collect();
257      output.insert("columns".to_string(), columns.into());
258    }
259    json!(output)
260  }
261
262  pub fn index_list(&self) -> String {
263    self.indices.clone().into_iter().map(|s| s.to_string()).collect::<Vec<String>>().join(", ")
264  }
265
266  /// render option output contextually as a list of strings
267  /// for use in a terminal or text output
268  pub fn to_lines(&self) -> Vec<String> {
269    let mut lines = vec![];
270    if let Some(s_names) = self.selected.clone() {
271      let plural = if s_names.len() > 1 {
272        "s"
273      } else {
274        ""
275      };
276      lines.push(format!("sheet name{}: {}", plural, s_names.join(",")));
277    } else if !self.indices.is_empty() {
278      lines.push(format!("sheet indices: {}", self.index_list()));
279    }
280    if let Some(fname) = self.file_name() {
281      lines.push(format!("file name: {}", fname));
282    }
283    if self.max.is_some() {
284      let max_val = self.max.unwrap_or(0);
285      if max_val > 0 {
286        lines.push(format!("max rows: {}", max_val));
287      }
288    }
289    lines.extend(vec![
290      format!("mode: {}", self.row_mode()),
291      format!("headers: {}", self.header_mode()),
292      format!("header row: {}", self.header_row),
293      format!("decimal separator: {}", self.rows.decimal_separator()),
294      format!("date mode: {}", self.rows.date_mode()),
295      format!("column style: {}", self.field_mode.to_string())
296    ]);
297
298    if !self.columns().is_empty() {
299      lines.push("columns:".to_string());
300      for col in self.rows.columns.clone() {
301        lines.push(col.to_line());
302      }
303    }
304    lines
305  }
306
307  /// header row index as usize
308  pub fn header_row_index(&self) -> usize {
309    self.header_row as usize
310  }
311
312  /// get the maximum of rows to be output synchronously
313  pub fn max_rows(&self) -> usize {
314    if let Some(mr) = self.max {
315      mr as usize
316    } else {
317      match self.read_mode {
318        ReadMode::PreviewMultiple => DEFAULT_MAX_ROWS_PREVIEW,
319        _ => DEFAULT_MAX_ROWS
320      }
321    }
322  }
323
324  /// future development with advanced column options
325  #[allow(dead_code)]
326  pub fn columns(&self) -> Vec<Column> {
327    self.rows.columns.clone()
328  }
329
330  /// cloned read mode
331  pub fn read_mode(&self) -> ReadMode {
332    self.read_mode
333  }
334
335  /// Needs full data set to processed later
336  pub fn is_async(&self) -> bool {
337    self.read_mode.is_async()
338  }
339
340  // Should rows be captured synchronously
341  pub fn capture_rows(&self) -> bool {
342    !matches!(self.read_mode, ReadMode::Async)
343  }
344
345}
346
347
348/// Cell format overrides
349#[derive(Debug, Clone)]
350pub enum Format {
351  Auto, // automatic interpretation
352  Text, // text
353  Integer, // integer only
354  Decimal(u8), // decimal to stated precision
355  Float, // f64 
356  Boolean, // Boolean or  cast to boolean from integers
357  Date, // Interpret as date only
358  DateTime, // Interpret as full datetime
359  DateTimeCustom(Arc<str>),
360  Truthy, // interpret common yes/no, y/n, true/false text strings as true/false
361  #[allow(dead_code)]
362  TruthyCustom(TruthyRuleSet) // define custom yes/no values
363}
364
365impl std::fmt::Display for Format {
366  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367    let result = match self {
368      Self::Auto => "auto".to_string(),
369      Self::Text => "text".to_string(),
370      Self::Integer => "integer".to_string(),
371      Self::Decimal(n) => format!("decimal({})", n),
372      Self::Float => "float".to_string(),
373      Self::Boolean => "boolean".to_string(),
374      Self::Date => "date".to_string(),
375      Self::DateTime => "datetime".to_string(),
376      Self::DateTimeCustom(fmt) => format!("datetime({})", fmt),
377      Self::Truthy => "truthy".to_string(),
378      Self::TruthyCustom(rules) => {
379        let true_str: Vec<String> = rules.true_options().iter().map(|o| o.pattern().to_string()).collect();
380        let false_str: Vec<String> = rules.false_options().iter().map(|o| o.pattern().to_string()).collect();
381        format!("truthy({},{})", true_str.join("|"), false_str.join("|"))
382      },
383    };
384    write!(f, "{}", result)
385  }
386}
387
388impl FromStr for Format {
389  type Err = Error;
390  fn from_str(key: &str) -> Result<Self, Self::Err> {
391      let fmt = match key {
392        "s" | "str" | "string" | "t" | "txt" | "text" => Self::Text,
393        "i" | "int" | "integer" => Self::Integer,
394        "d1" | "decimal_1" => Self::Decimal(1),
395        "d2" | "decimal_2" => Self::Decimal(2),
396        "d3" | "decimal_3" => Self::Decimal(3),
397        "d4" | "decimal_4" => Self::Decimal(4),
398        "d5" | "decimal_5" => Self::Decimal(5),
399        "d6" | "decimal_6" => Self::Decimal(6),
400        "d7" | "decimal_7" => Self::Decimal(7),
401        "d8" | "decimal_8" => Self::Decimal(8),
402        "fl" | "f" | "float" => Self::Float,
403        "b" | "bool" | "boolean" => Self::Boolean,
404        "da" | "date" => Self::Date,
405        "dt" | "datetime" => Self::DateTime,
406        "tr" | "truthy" => Self::Truthy,
407        _ => {
408          if let Some(str) = match_custom_dt(key) {
409            Self::DateTimeCustom(Arc::from(str))
410          } else if let Some((yes, no)) = match_custom_truthy(key) {
411            Self::TruthyCustom(TruthyRuleSet::new().add_true(&yes).add_false(&no))
412          } else {
413            Self::Auto
414          }
415        },
416      };
417      Ok(fmt)
418  }
419}
420
421fn match_custom_dt(key: &str) -> Option<String> {
422  let test_str = key.trim();
423  if test_str.starts_with_ci("dt:") {
424    Some(test_str[3..].to_string())
425  } else {
426    None
427  }
428}
429
430fn match_custom_truthy(key: &str) -> Option<(String,String)> {
431  let test_str = key.trim();
432  if let (Some(head), Some(tail)) = test_str.to_head_tail(":") {
433    if tail.len() > 1 && head.len() > 1 && head.starts_with_ci("tr") {
434      if let (Some(yes), Some(no)) = tail.to_head_tail(",") {
435        if !yes.is_empty() && !no.is_empty() {
436          return Some((yes.to_string(), no.to_string()));
437        }
438      }
439    }
440  }
441  None
442}
443
444impl Format {
445  #[allow(dead_code)]
446  pub fn truthy_custom(yes: &str, no: &str) -> Self {
447    Format::TruthyCustom(TruthyRuleSet::new().add_true(yes).add_false(no))
448  }
449}
450
451#[derive(Debug, Clone)]
452pub struct Column {
453  pub key:  Option<Arc<str>>,
454  /// Natural (auto-detected, snake_cased) key to match this override against, regardless
455  /// of the column's actual position. When None, the column applies positionally instead
456  /// (matched by its index within the configured column list), as before.
457  pub source_key: Option<Arc<str>>,
458  pub format: Format,
459  pub default: Option<Value>,
460  pub date_only: bool, // date only in Format::Auto mode with datetime objects
461  pub decimal_comma: bool, // parse as euro number format
462}
463
464impl Column {
465
466  /// build new column with an optional key name only
467  pub fn new(key_opt: Option<&str>) -> Self {
468    Self::from_key_ref_with_format(key_opt, Format::Auto, None, false, false)
469  }
470
471  /// build new column data type override and optional default
472  pub fn new_format(fmt: Format, default: Option<Value>) -> Self {
473    Self::from_key_ref_with_format(None, fmt, default, false, false)
474  }
475
476  /// build a column override matched by its natural (auto-detected) key rather than
477  /// by position, e.g. to rename and/or reformat a single field out of many without
478  /// needing to enumerate every column ahead of it.
479  pub fn from_source_key_with_format(source_key: &str, key_opt: Option<&str>, format: Format, default: Option<Value>, date_only: bool, decimal_comma: bool) -> Self {
480    let mut col = Self::from_key_ref_with_format(key_opt, format, default, date_only, decimal_comma);
481    col.source_key = Some(Arc::from(source_key));
482    col
483  }
484
485  /// build new column data type override and optional default
486  pub fn from_json(json: &Value) -> Self {
487    let key_opt = json.get("key").map(|v| v.as_str().unwrap_or(""));
488    let source_key = json.get("source_key").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
489    let fmt = match json.get("format").and_then(|v| v.as_str()) {
490      Some(fmt_str) => {
491        match Format::from_str(fmt_str) {
492          Ok(fmt) => fmt,
493          Err(_) => Format::Auto
494        }
495      },
496      None => Format::Auto
497    };
498    let default = match json.get("default") {
499      Some(def_val) => {
500        match def_val {
501          Value::String(s) => Some(Value::String(s.clone())),
502          Value::Number(n) => Some(Value::Number(n.clone())),
503          Value::Bool(b) => Some(Value::Bool(*b)),
504          _ => None
505        }
506      },
507      None => None
508    };
509    let date_only = match json.get("date_only") {
510      Some(date_val) => date_val.as_bool().unwrap_or(false),
511      None => false
512    };
513    let dec_commas_keys = ["decimal_comma", "dec_comma"];
514    let mut decimal_comma = false;
515
516    for key in &dec_commas_keys {
517      if let Some(euro_val) = json.get(*key) {
518        decimal_comma = euro_val.as_bool().unwrap_or(false);
519        break;
520      }
521    }
522    if let Some(src) = source_key {
523      Column::from_source_key_with_format(src, key_opt, fmt, default, date_only, decimal_comma)
524    } else {
525      Column::from_key_ref_with_format(key_opt, fmt, default, date_only, decimal_comma)
526    }
527}
528
529
530  // future development with column options
531  #[allow(dead_code)]
532  pub fn set_format(mut self, fmt: Format) -> Self {
533    self.format = fmt;
534    self
535  }
536
537  #[allow(dead_code)]
538  pub fn set_default(mut self, val: Value) -> Self {
539    self.default = Some(val);
540    self
541  }
542
543  #[allow(dead_code)]
544  pub fn set_date_only(mut self, val: bool) -> Self {
545    self.date_only = val;
546    self
547  }
548
549  #[allow(dead_code)]
550  pub fn set_decimal_comma(mut self, val: bool) -> Self {
551    self.decimal_comma = val;
552    self
553  }
554
555  pub fn from_key_ref_with_format(key_opt: Option<&str>, format: Format, default: Option<Value>, date_only: bool, decimal_comma: bool) -> Self {
556    let mut key = None;
557    if let Some(k_str) = key_opt {
558      key = Some(Arc::from(k_str));
559    }
560    Column {
561      key,
562      source_key: None,
563      format,
564      default,
565      date_only,
566      decimal_comma
567    }
568  }
569
570  pub fn key_name(&self) -> String {
571    self.key.clone().unwrap_or(Arc::from("")).to_string()
572  }
573
574  pub fn source_key_name(&self) -> String {
575    self.source_key.clone().unwrap_or(Arc::from("")).to_string()
576  }
577
578  pub fn to_json(&self) -> Value {
579    json!({
580      "key": self.key_name(),
581      "source_key": self.source_key_name(),
582      "format": self.format.to_string(),
583      "default": self.default,
584      "date_only": self.date_only,
585      "decimal_comma": self.decimal_comma
586    })
587  }
588
589  pub fn to_line(&self) -> String {
590    let date_only_str = if self.date_only {
591      ", date only"
592    } else {
593      ""
594    }.to_owned();
595    let def_string = if let Some(def_val) = self.default.clone() {
596      format!("default: {}", def_val)
597    } else {
598      "".to_string()
599    };
600    let comma_str = if self.decimal_comma {
601      ", decimal comma"
602    } else {
603      ""
604    };
605    let source_str = if self.source_key.is_some() {
606      format!(", matched from {}", self.source_key_name())
607    } else {
608      "".to_string()
609    };
610    format!(
611      "\tkey {}, format {}{}{}{}{}",
612      self.key_name(),
613      self.format,
614      def_string,
615      date_only_str,
616      comma_str,
617      source_str)
618  }
619
620}
621
622
623/// Match on permitted file types identified by file extensions
624/// Unmatched means do not process
625#[derive(Debug, Clone, Copy)]
626pub enum Extension {
627  Unmatched,
628  Ods,
629  Xlsx,
630  Xlsm,
631  Xlsb,
632  Xls,
633  Csv,
634  Tsv,
635}
636
637impl Extension {
638  pub fn from_path(path:&Path) -> Extension {
639    if let Some(ext) = path.extension() {
640      if let Some(ext_str) = ext.to_str() {
641        let ext_lc = ext_str.to_lowercase();
642        return match  ext_lc.as_str() {
643          "ods" => Extension::Ods,
644          "xlsx" => Extension::Xlsx,
645          // .xlsm (macro-enabled) is the same OOXML container as .xlsx -- calamine's own
646          // open_workbook_auto already routes both through its Xlsx reader (it does its
647          // own extension check on the same path), so there's nothing macro-specific to
648          // handle here; we just need to stop rejecting the extension before it gets there.
649          "xlsm" => Extension::Xlsm,
650          "xlsb" => Extension::Xlsb,
651          "xls" => Extension::Xls,
652          "csv" => Extension::Csv,
653          "tsv" => Extension::Tsv,
654          _ => Extension::Unmatched
655        }
656      }
657    }
658    Extension::Unmatched
659  }
660
661  /// use the Calamine library
662  pub fn use_calamine(&self) -> bool {
663    matches!(self, Self::Ods | Self::Xlsx | Self::Xlsm | Self::Xlsb | Self::Xls)
664  }
665
666  /// added for future development
667  /// Process a simple CSV or TSV
668  #[allow(dead_code)]
669  pub fn use_csv(&self) -> bool {
670    matches!(self, Self::Csv | Self::Tsv)
671  }
672
673}
674
675impl std::fmt::Display for Extension {
676  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
677    let result = match self {
678      Self::Ods => "ods",
679      Self::Xlsx => "xlsx",
680      Self::Xlsm => "xlsm",
681      Self::Xlsb => "xlsb",
682      Self::Xls => "xls",
683      Self::Csv => "csv",
684      Self::Tsv => "tsv",
685      _ => ""
686    };
687    write!(f, "{}", result)
688  }
689}
690
691pub struct PathData<'a> {
692  path: &'a Path,
693  ext: Extension
694}
695
696impl<'a> PathData<'a> {
697  pub fn new(path: &'a Path) -> Self {
698    PathData {
699      path,
700      ext: Extension::from_path(path)
701    }
702  }
703
704  pub fn mode(&self) -> Extension {
705    self.ext
706  }
707
708  pub fn extension(&self) -> String {
709    self.ext.to_string()
710  }
711
712  pub fn ext(&self) -> Extension {
713    self.ext
714  }
715
716  pub fn path(&self) -> &Path {
717    self.path
718  }
719
720  pub fn is_valid(&self) -> bool {
721    !matches!(self.ext, Extension::Unmatched)
722  }
723
724  pub fn use_calamine(&self) -> bool {
725    self.ext.use_calamine()
726  }
727
728  pub fn filename(&self) -> String {
729    if let Some(file_ref) = self.path.file_name() {
730        file_ref.to_string_lossy().to_string()
731    } else {
732        "".to_owned()
733    }
734  }
735}
736
737
738
739#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
740pub enum ReadMode {
741  #[default]
742  Sync,
743  PreviewMultiple,
744  Async
745}
746
747/// either Preview or Async mode
748impl ReadMode {
749
750  pub fn from_key(key: &str) -> Self {
751    let sample = key.to_lowercase().strip_non_alphanum();
752    match sample.as_str() {
753      "async" | "defer" | "deferred" | "a" => ReadMode::Async,
754      "preview" | "p" | "pre" | "multimode" | "multiple" | "previewmultiple" | "previewmulti" | "m" => ReadMode::PreviewMultiple,
755      _ => ReadMode::Sync
756    }
757  }
758
759  pub fn is_async(&self) -> bool {
760    matches!(self, Self::Async)
761  }
762
763  /// not preview or sync mode
764  pub fn is_multimode(&self) -> bool {
765    matches!(self, Self::PreviewMultiple)
766  }
767}
768
769impl std::fmt::Display for ReadMode {
770  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
771    let result = match self {
772      Self::Async => "deferred",
773      Self::PreviewMultiple => "preview",
774      _ => "direct"
775    };
776    write!(f, "{}", result)
777  }
778}
779
780/// defines the column key naming convention
781#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
782pub enum FieldNameMode {
783  #[default]
784  AutoA1, // will use A1 column keys if headers are unavailable
785  AutoNumPadded, // will use C01 format if column headers are unavailable
786  A1, // Defaults to A1 columns unless custom keys are added
787  NumPadded, // Defaults to C01 format unless custom keys are added
788}
789
790/// either Preview or Async mode
791impl FieldNameMode {
792
793
794  pub fn from_key(system: &str, override_header: bool) -> Self {
795    if system.starts_with_ci("a1") {
796      if override_header {
797        FieldNameMode::A1
798      } else {
799        FieldNameMode::AutoA1
800      }
801    } else if system.starts_with_ci("c") || system.starts_with_ci("n") {
802      if override_header {
803        FieldNameMode::NumPadded
804      } else {
805        FieldNameMode::AutoNumPadded
806      }
807    } else {
808      FieldNameMode::AutoA1
809    }
810  }
811
812
813  /// use AQ column field style
814  pub fn use_a1(&self) -> bool {
815    matches!(self, Self::AutoA1 | Self::A1)
816  }
817
818  /// use c01 column field style
819  pub fn use_c01(&self) -> bool {
820    matches!(self, Self::AutoNumPadded | Self::NumPadded)
821  }
822
823   /// use seqquential a1 or C01 column style unless custom overrides are added
824   pub fn override_headers(&self) -> bool {
825    matches!(self, Self::NumPadded | Self::A1)
826  }
827
828  /// use default headers if available unless override by custom headers
829  pub fn keep_headers(&self) -> bool {
830    !self.override_headers()
831  }
832}
833
834impl std::fmt::Display for FieldNameMode {
835  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
836    let result = match self {
837      Self::AutoNumPadded => "C01 auto",
838      Self::NumPadded => "C01 override",
839      Self::A1 => "A1 override",
840      _ => "A1 auto",
841    };
842    write!(f, "{}", result)
843  }
844}
845
846#[cfg(test)]
847mod tests {
848  use super::*;
849
850  #[test]
851  fn test_format_mode() {
852    let custom_boolean = Format::truthy_custom("si", "no");
853    assert_eq!(custom_boolean.to_string(), "truthy(si,no)");
854  }
855
856  #[test]
857  fn test_match_truthy_custom() {
858    let (true_keys, false_keys) = match_custom_truthy("tr:si,no").unwrap();
859    assert_eq!("si", true_keys);
860    assert_eq!("no", false_keys);
861  }
862
863  #[test]
864  fn test_xlsm_is_recognised_and_routed_through_calamine() {
865    // Regression: .xlsm (macro-enabled) is the same OOXML container as .xlsx -- calamine's
866    // own open_workbook_auto already reads both through its Xlsx reader -- but our own
867    // Extension enum didn't recognise the extension at all, so .xlsm files were rejected
868    // before calamine ever got a chance to open them.
869    let ext = Extension::from_path(Path::new("workbook.xlsm"));
870    assert!(matches!(ext, Extension::Xlsm));
871    assert!(ext.use_calamine());
872    assert_eq!(ext.to_string(), "xlsm");
873  }
874
875}