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, Default)]
16pub struct RowOptionSet {
17 pub columns: Vec<Column>,
18 pub decimal_comma: bool, pub date_only: bool,
20}
21
22impl RowOptionSet {
23
24 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 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#[derive(Debug, Clone, Default)]
65pub struct OptionSet {
66 pub selected: Option<Vec<String>>, pub indices: Vec<u32>, pub path: Option<String>, 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 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 pub fn sheet_name(mut self, name: &str) -> Self {
97 self.selected = Some(vec![name.to_string()]);
98 self
99 }
100
101 pub fn sheet_names(mut self, names: &[String]) -> Self {
103 self.selected = Some(names.to_vec());
104 self
105 }
106
107 pub fn sheet_index(mut self, index: u32) -> Self {
109 self.indices = vec![index];
110 self
111 }
112
113 pub fn sheet_indices(mut self, indices: &[u32]) -> Self {
115 self.indices = indices.to_vec();
116 self
117}
118
119 pub fn json_lines(mut self) -> Self {
121 self.jsonl = true;
122 self
123 }
124
125 pub fn set_json_lines(mut self, mode: bool) -> Self {
127 self.jsonl = mode;
128 self
129 }
130
131 pub fn omit_header(mut self) -> Self {
133 self.omit_header = true;
134 self
135 }
136
137 pub fn header_row(mut self, row: u8) -> Self {
139 self.header_row = row;
140 self
141 }
142
143 pub fn max_row_count(mut self, max: u32) -> Self {
145 self.max = Some(max);
146 self
147 }
148
149 pub fn read_mode_async(mut self) -> Self {
152 self.read_mode = ReadMode::Async;
153 self
154 }
155
156 pub fn read_mode_preview(mut self) -> Self {
159 self.read_mode = ReadMode::PreviewMultiple;
160 self
161}
162
163 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 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 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 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 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 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 pub fn header_row_index(&self) -> usize {
309 self.header_row as usize
310 }
311
312 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 #[allow(dead_code)]
326 pub fn columns(&self) -> Vec<Column> {
327 self.rows.columns.clone()
328 }
329
330 pub fn read_mode(&self) -> ReadMode {
332 self.read_mode
333 }
334
335 pub fn is_async(&self) -> bool {
337 self.read_mode.is_async()
338 }
339
340 pub fn capture_rows(&self) -> bool {
342 !matches!(self.read_mode, ReadMode::Async)
343 }
344
345}
346
347
348#[derive(Debug, Clone)]
350pub enum Format {
351 Auto, Text, Integer, Decimal(u8), Float, Boolean, Date, DateTime, DateTimeCustom(Arc<str>),
360 Truthy, #[allow(dead_code)]
362 TruthyCustom(TruthyRuleSet) }
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 pub source_key: Option<Arc<str>>,
458 pub format: Format,
459 pub default: Option<Value>,
460 pub date_only: bool, pub decimal_comma: bool, }
463
464impl Column {
465
466 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 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 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 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 #[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#[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" => 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 pub fn use_calamine(&self) -> bool {
663 matches!(self, Self::Ods | Self::Xlsx | Self::Xlsm | Self::Xlsb | Self::Xls)
664 }
665
666 #[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
747impl 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 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
782pub enum FieldNameMode {
783 #[default]
784 AutoA1, AutoNumPadded, A1, NumPadded, }
789
790impl 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 pub fn use_a1(&self) -> bool {
815 matches!(self, Self::AutoA1 | Self::A1)
816 }
817
818 pub fn use_c01(&self) -> bool {
820 matches!(self, Self::AutoNumPadded | Self::NumPadded)
821 }
822
823 pub fn override_headers(&self) -> bool {
825 matches!(self, Self::NumPadded | Self::A1)
826 }
827
828 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 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}