1use crate::config::FILENAME_COPY_COUNTER_MAX;
3use crate::config::FILENAME_DOTFILE_MARKER;
4use crate::config::FILENAME_EXTENSION_SEPARATOR_DOT;
5use crate::config::FILENAME_LEN_MAX;
6use crate::config::LIB_CFG;
7use crate::error::FileError;
8use crate::markup_language::MarkupLanguage;
9use crate::settings::SETTINGS;
10use std::mem::swap;
11use std::path::Path;
12use std::path::PathBuf;
13use std::time::SystemTime;
14
15pub trait NotePathBuf {
17 fn from_disassembled(
22 sort_tag: &str,
23 stem: &str,
24 copy_counter: Option<usize>,
25 extension: &str,
26 ) -> Self;
27 fn set_next_unused(&mut self) -> Result<(), FileError>;
65
66 fn shorten_filename(&mut self);
100}
101
102impl NotePathBuf for PathBuf {
103 #[inline]
104 fn from_disassembled(
105 sort_tag: &str,
106 stem: &str,
107 copy_counter: Option<usize>,
108 extension: &str,
109 ) -> Self {
110 let mut filename = String::new();
112
113 let scheme = &LIB_CFG.read_recursive().scheme[SETTINGS.read_recursive().current_scheme];
115
116 if !sort_tag.is_empty() {
117 filename.push_str(sort_tag);
118 filename.push_str(&scheme.filename.sort_tag.separator);
119 }
120 let mut test_path = String::from(stem);
124 test_path.push_str(&scheme.filename.sort_tag.separator);
125 if stem.is_empty() || !&test_path.split_sort_tag(false).0.is_empty() {
127 filename.push(scheme.filename.sort_tag.extra_separator);
128 }
129
130 filename.push_str(stem);
131
132 if let Some(cc) = copy_counter {
133 if stem.split_copy_counter().1.is_some() {
136 filename.push_str(&scheme.filename.copy_counter.extra_separator);
138 };
139
140 filename.push_str(&scheme.filename.copy_counter.opening_brackets);
141 filename.push_str(&cc.to_string());
142 filename.push_str(&scheme.filename.copy_counter.closing_brackets);
143 }
144
145 if !extension.is_empty() {
146 filename.push(FILENAME_EXTENSION_SEPARATOR_DOT);
147 filename.push_str(extension);
148 };
149 PathBuf::from(filename)
150 }
151
152 fn set_next_unused(&mut self) -> Result<(), FileError> {
153 if !&self.exists() {
154 return Ok(());
155 };
156
157 let (sort_tag, _, stem, _copy_counter, ext) = self.disassemble();
158
159 let mut new_path = self.clone();
160
161 for copy_counter in 1..FILENAME_COPY_COUNTER_MAX {
163 let filename = Self::from_disassembled(sort_tag, stem, Some(copy_counter), ext);
164 new_path.set_file_name(filename);
165
166 if !new_path.exists() {
167 break;
168 }
169 }
170
171 if new_path.exists() {
173 return Err(FileError::NoFreeFileName {
174 directory: self.parent().unwrap_or_else(|| Path::new("")).to_path_buf(),
175 });
176 }
177 swap(self, &mut new_path);
178 Ok(())
179 }
180
181 fn shorten_filename(&mut self) {
182 let stem = self
184 .file_stem()
185 .unwrap_or_default()
186 .to_str()
187 .unwrap_or_default();
188 let ext = self
189 .extension()
190 .unwrap_or_default()
191 .to_str()
192 .unwrap_or_default();
193 let ext_len = ext.len();
194
195 let mut stem_short = String::new();
197 for i in (0..FILENAME_LEN_MAX - (ext_len + 2)).rev() {
201 if let Some(s) = stem.get(..=i) {
202 stem_short = s.to_string();
203 break;
204 }
205 }
206
207 if stem_short.split_copy_counter().1.is_some() {
209 let scheme = &LIB_CFG.read_recursive().scheme[SETTINGS.read_recursive().current_scheme];
210
211 stem_short.push_str(&scheme.filename.copy_counter.extra_separator);
212 }
213
214 let mut note_filename = stem_short;
216 if !ext.is_empty() {
217 note_filename.push(FILENAME_DOTFILE_MARKER);
218 note_filename.push_str(ext);
219 }
220 self.set_file_name(note_filename);
222 }
223}
224
225pub trait NotePath {
227 fn disassemble(&self) -> (&str, &str, &str, Option<usize>, &str);
231
232 fn exclude_copy_counter_eq(&self, p2: &Path) -> bool;
235
236 fn has_tpnote_ext(&self) -> bool;
238
239 fn has_wellformed_filename(&self) -> bool;
241
242 fn find_last_created_file(&self) -> Option<String>;
248
249 fn has_file_with_sort_tag(&self, sort_tag: &str) -> Option<String>;
252
253 fn find_file_with_sort_tag(&self, sort_tag: &str) -> Option<PathBuf>;
256}
257
258impl NotePath for Path {
259 fn disassemble(&self) -> (&str, &str, &str, Option<usize>, &str) {
260 let sort_tag_stem_copy_counter_ext = self
261 .file_name()
262 .unwrap_or_default()
263 .to_str()
264 .unwrap_or_default();
265
266 let (sort_tag, stem_copy_counter_ext, _) =
267 sort_tag_stem_copy_counter_ext.split_sort_tag(false);
268
269 let ext = Path::new(stem_copy_counter_ext)
270 .extension()
271 .unwrap_or_default()
272 .to_str()
273 .unwrap_or_default(); let (stem_copy_counter, ext) = if !ext.is_empty()
276 && ext.chars().all(|c| c.is_alphanumeric())
277 {
278 (
279 &stem_copy_counter_ext[..stem_copy_counter_ext.len().saturating_sub(ext.len() + 1)],
281 ext,
283 )
284 } else {
285 (stem_copy_counter_ext, "")
286 };
287
288 let (stem, copy_counter) = stem_copy_counter.split_copy_counter();
289
290 (sort_tag, stem_copy_counter_ext, stem, copy_counter, ext)
291 }
292
293 fn exclude_copy_counter_eq(&self, p2: &Path) -> bool {
296 let (sort_tag1, _, stem1, _, ext1) = self.disassemble();
297 let (sort_tag2, _, stem2, _, ext2) = p2.disassemble();
298 sort_tag1 == sort_tag2 && stem1 == stem2 && ext1 == ext2
299 }
300
301 fn has_tpnote_ext(&self) -> bool {
306 MarkupLanguage::from(self).is_some()
307 }
308
309 fn has_wellformed_filename(&self) -> bool {
332 let filename = &self.file_name().unwrap_or_default();
333 let ext = self
334 .extension()
335 .unwrap_or_default()
336 .to_str()
337 .unwrap_or_default();
338
339 let is_filename = !filename.is_empty();
340
341 let filename = filename.to_str().unwrap_or_default();
342 let is_dot_file = filename.starts_with(FILENAME_DOTFILE_MARKER)
343 && (filename == filename.trim())
345 && filename.split_whitespace().count() == 1;
346
347 let has_extension = !ext.is_empty()
348 && ext.chars().all(|c| c.is_ascii_alphanumeric());
350
351 is_filename && (is_dot_file || has_extension)
352 }
353
354 fn find_last_created_file(&self) -> Option<String> {
355 if let Ok(files) = self.read_dir() {
356 let mut filename_max = String::new();
359 let mut ctime_max = SystemTime::UNIX_EPOCH;
360 for file in files.flatten() {
361 match file.file_type() {
362 Ok(ft) if ft.is_file() => {}
363 _ => continue,
364 }
365 let ctime = file
366 .metadata()
367 .ok()
368 .and_then(|md| md.created().ok())
369 .unwrap_or(SystemTime::UNIX_EPOCH);
370 let filename = file.file_name();
371 let filename = filename.to_str().unwrap();
372 if filename.is_empty() || !filename.has_tpnote_ext() {
373 continue;
374 }
375
376 if ctime > ctime_max
377 || (ctime == ctime_max
378 && filename.split_sort_tag(false).0 > filename_max.split_sort_tag(false).0)
379 {
380 filename_max = filename.to_string();
381 ctime_max = ctime;
382 }
383 } if !filename_max.is_empty() {
386 Some(filename_max.to_string())
387 } else {
388 None
389 }
390 } else {
391 None
392 }
393 }
394
395 fn has_file_with_sort_tag(&self, sort_tag: &str) -> Option<String> {
396 if let Ok(files) = self.read_dir() {
397 for file in files.flatten() {
398 match file.file_type() {
399 Ok(ft) if ft.is_file() => {}
400 _ => continue,
401 }
402 let filename = file.file_name();
403 let filename = filename.to_str().unwrap();
404
405 if filename.starts_with(sort_tag)
407 && filename.has_tpnote_ext()
408 && filename.split_sort_tag(false).0 == sort_tag
409 {
410 let filename = filename.to_string();
411 return Some(filename);
412 }
413 }
414 }
415 None
416 }
417
418 fn find_file_with_sort_tag(&self, sort_tag: &str) -> Option<PathBuf> {
419 let mut found = None;
420
421 if let Ok(files) = self.read_dir() {
422 let mut minimum = PathBuf::new();
425 'file_loop: for file in files.flatten() {
426 match file.file_type() {
427 Ok(ft) if ft.is_file() => {}
428 _ => continue,
429 }
430 let file = file.path();
431 if !(*file).has_tpnote_ext() {
432 continue 'file_loop;
433 }
434 if file.disassemble().0 == sort_tag {
437 if minimum == Path::new("") || minimum > file {
440 minimum = file;
441 }
442 }
443 } if minimum != Path::new("") {
445 log::debug!(
446 "File `{}` referenced by sort-tag match `{}`.",
447 minimum.to_str().unwrap_or_default(),
448 sort_tag,
449 );
450 found = Some(minimum)
452 }
453 }
454 found
455 }
456}
457
458pub(crate) trait NotePathStr {
460 fn has_tpnote_ext(&self) -> bool;
464
465 fn split_copy_counter(&self) -> (&str, Option<usize>);
471
472 fn split_sort_tag(&self, ignore_sort_tag_separator: bool) -> (&str, &str, bool);
485
486 fn is_valid_sort_tag(&self) -> Option<&str>;
493}
494
495impl NotePathStr for str {
496 fn has_tpnote_ext(&self) -> bool {
497 MarkupLanguage::from(Path::new(self)).is_some()
498 }
499
500 #[inline]
501 fn split_copy_counter(&self) -> (&str, Option<usize>) {
502 let scheme = &LIB_CFG.read_recursive().scheme[SETTINGS.read_recursive().current_scheme];
503 let tag1 =
505 if let Some(t) = self.strip_suffix(&scheme.filename.copy_counter.closing_brackets) {
506 t
507 } else {
508 return (self, None);
509 };
510 let tag2 = tag1.trim_end_matches(|c: char| c.is_numeric());
512 let copy_counter: Option<usize> = if tag2.len() < tag1.len() {
513 tag1[tag2.len()..].parse().ok()
514 } else {
515 return (self, None);
516 };
517 let tag3 =
519 if let Some(t) = tag2.strip_suffix(&scheme.filename.copy_counter.opening_brackets) {
520 t
521 } else {
522 return (self, None);
523 };
524 if let Some(t) = tag3.strip_suffix(&scheme.filename.copy_counter.extra_separator) {
526 (t, copy_counter)
527 } else {
528 (tag3, copy_counter)
529 }
530 }
531
532 fn split_sort_tag(&self, ignore_sort_tag_separator: bool) -> (&str, &str, bool) {
533 let scheme = &LIB_CFG.read_recursive().scheme[SETTINGS.read_recursive().current_scheme];
534
535 let mut is_sequential_sort_tag = true;
536
537 let mut digits: u8 = 0;
538 let mut letters: u8 = 0;
539 let mut sort_tag = &self[..self
540 .chars()
541 .take_while(|&c| {
542 if c.is_ascii_digit() {
543 digits += 1;
544 if digits > scheme.filename.sort_tag.sequential.digits_in_succession_max {
545 is_sequential_sort_tag = false;
546 }
547 } else {
548 digits = 0;
549 }
550
551 if c.is_ascii_lowercase() {
552 letters += 1;
553 } else {
554 letters = 0;
555 }
556
557 letters <= scheme.filename.sort_tag.letters_in_succession_max
558 && (c.is_ascii_digit()
559 || c.is_ascii_lowercase()
560 || scheme.filename.sort_tag.extra_chars.contains([c]))
561 })
562 .count()];
563
564 let mut stem_copy_counter_ext;
565 if scheme.filename.sort_tag.separator.is_empty() || ignore_sort_tag_separator {
566 stem_copy_counter_ext = &self[sort_tag.len()..];
568 } else {
569 if let Some(i) = sort_tag.rfind(&scheme.filename.sort_tag.separator) {
571 sort_tag = &sort_tag[..i];
572 stem_copy_counter_ext = &self[i + scheme.filename.sort_tag.separator.len()..];
573 } else {
574 sort_tag = "";
575 stem_copy_counter_ext = self;
576 }
577 }
578
579 let mut chars = stem_copy_counter_ext.chars();
582 if chars
583 .next()
584 .is_some_and(|c| c == scheme.filename.sort_tag.extra_separator)
585 && chars.next().is_some_and(|c| {
586 c.is_ascii_digit()
587 || c.is_ascii_lowercase()
588 || scheme.filename.sort_tag.extra_chars.contains(c)
589 })
590 {
591 stem_copy_counter_ext = stem_copy_counter_ext
592 .strip_prefix(scheme.filename.sort_tag.extra_separator)
593 .unwrap();
594 }
595
596 (sort_tag, stem_copy_counter_ext, is_sequential_sort_tag)
597 }
598
599 fn is_valid_sort_tag(&self) -> Option<&str> {
600 let filename = if let Some((_, filename)) = self.rsplit_once(['\\', '/']) {
601 filename
602 } else {
603 self
604 };
605 if filename.is_empty() {
606 return None;
607 }
608
609 if filename.split_sort_tag(true).1.is_empty() {
611 Some(filename)
612 } else {
613 None
614 }
615 }
616}
617
618pub(crate) trait Extension {
620 fn is_tpnote_ext(&self) -> bool;
623}
624
625impl Extension for str {
626 fn is_tpnote_ext(&self) -> bool {
627 MarkupLanguage::from(self).is_some()
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use std::ffi::OsString;
634 use std::path::Path;
635 use std::path::PathBuf;
636
637 #[test]
638 fn test_from_disassembled() {
639 use crate::filename::NotePathBuf;
640
641 let expected = PathBuf::from("My_file.md");
642 let result = PathBuf::from_disassembled("", "My_file", None, "md");
643 assert_eq!(expected, result);
644
645 let expected = PathBuf::from("1_2_3-My_file(1).md");
646 let result = PathBuf::from_disassembled("1_2_3", "My_file", Some(1), "md");
647 assert_eq!(expected, result);
648
649 let expected = PathBuf::from("1_2_3-123 my_file(1).md");
650 let result = PathBuf::from_disassembled("1_2_3", "123 my_file", Some(1), "md");
651 assert_eq!(expected, result);
652
653 let expected = PathBuf::from("1_2_3-'123-My_file(1).md");
654 let result = PathBuf::from_disassembled("1_2_3", "123-My_file", Some(1), "md");
655 assert_eq!(expected, result);
656
657 let expected = PathBuf::from("'123-My_file(1).md");
658 let result = PathBuf::from_disassembled("", "123-My_file", Some(1), "md");
659 assert_eq!(expected, result);
660
661 let res = PathBuf::from_disassembled("1234", "title--subtitle", Some(9), "md");
662 assert_eq!(res, Path::new("1234-title--subtitle(9).md"));
663
664 let res = PathBuf::from_disassembled("1234ab", "title--subtitle", Some(9), "md");
665 assert_eq!(res, Path::new("1234ab-title--subtitle(9).md"));
666
667 let res = PathBuf::from_disassembled("1234", "5678", Some(9), "md");
668 assert_eq!(res, Path::new("1234-'5678(9).md"));
669
670 let res = PathBuf::from_disassembled("1234", "5678--subtitle", Some(9), "md");
671 assert_eq!(res, Path::new("1234-'5678--subtitle(9).md"));
672
673 let res = PathBuf::from_disassembled("1234", "", None, "md");
674 assert_eq!(res, Path::new("1234-'.md"));
675
676 let res = PathBuf::from_disassembled("1234", "'5678--subtitle", Some(9), "md");
678 assert_eq!(res, Path::new("1234-'5678--subtitle(9).md"));
679
680 let res = PathBuf::from_disassembled("", "-", Some(9), "md");
681 assert_eq!(res, Path::new("'-(9).md"));
682
683 let res = PathBuf::from_disassembled("", "(1)", Some(9), "md");
684 assert_eq!(res, Path::new("(1)-(9).md"));
685
686 let res = PathBuf::from_disassembled("", "(1)-", Some(9), "md");
688 assert_eq!(res, Path::new("(1)-(9).md"));
689 }
690
691 #[test]
692 fn test_set_next_unused() {
693 use crate::filename::NotePathBuf;
694
695 use std::env::temp_dir;
696 use std::fs;
697
698 let raw = "This simulates a non tp-note file";
699 let mut notefile = temp_dir().join("20221030-some.pdf--Note.md");
700 fs::write(¬efile, raw.as_bytes()).unwrap();
701
702 notefile.set_next_unused().unwrap();
703 let expected = temp_dir().join("20221030-some.pdf--Note(1).md");
704 assert_eq!(notefile, expected);
705 let _ = fs::remove_file(notefile);
706 }
707
708 #[test]
709 fn test_shorten_filename() {
710 use crate::config::FILENAME_LEN_MAX;
711 use crate::filename::NotePathBuf;
712
713 let mut input = PathBuf::from("fn(1).md");
716 let expected = PathBuf::from("fn(1)-.md");
717 input.shorten_filename();
720 let output = input;
721 assert_eq!(OsString::from(expected), output);
722
723 let mut input = PathBuf::from("20221030-some.pdf--Note.md");
726 let expected = input.clone();
727 input.shorten_filename();
728 let output = input;
729 assert_eq!(OsString::from(expected), output);
730
731 let mut input = "X".repeat(FILENAME_LEN_MAX + 10);
734 input.push_str(".ext");
735
736 let mut expected = "X".repeat(FILENAME_LEN_MAX - ".ext".len() - 1);
737 expected.push_str(".ext");
738
739 let mut input = PathBuf::from(input);
740 input.shorten_filename();
741 let output = input;
742 assert_eq!(OsString::from(expected), output);
743 }
744
745 #[test]
746 fn test_disassemble_filename() {
747 use crate::filename::NotePath;
748
749 let expected = (
750 "1_2_3",
751 "my_title--my_subtitle(1).md",
752 "my_title--my_subtitle",
753 Some(1),
754 "md",
755 );
756 let p = Path::new("/my/dir/1_2_3-my_title--my_subtitle(1).md");
757 let result = p.disassemble();
758 assert_eq!(result, expected);
759
760 let expected = (
761 "1_2_3",
762 "my_title--my_subtitle(1)-(9).md",
763 "my_title--my_subtitle(1)",
764 Some(9),
765 "md",
766 );
767 let p = Path::new("/my/dir/1_2_3-my_title--my_subtitle(1)-(9).md");
768 let result = p.disassemble();
769 assert_eq!(result, expected);
770
771 let expected = (
772 "2021.04.12",
773 "my_title--my_subtitle(1).md",
774 "my_title--my_subtitle",
775 Some(1),
776 "md",
777 );
778 let p = Path::new("/my/dir/2021.04.12-my_title--my_subtitle(1).md");
779 let result = p.disassemble();
780 assert_eq!(result, expected);
781
782 let expected = (
783 "",
784 "2021 04 12 my_title--my_subtitle(1).md",
785 "2021 04 12 my_title--my_subtitle",
786 Some(1),
787 "md",
788 );
789 let p = Path::new("/my/dir/2021 04 12 my_title--my_subtitle(1).md");
790 let result = p.disassemble();
791 assert_eq!(result, expected);
792
793 let expected = ("2021-04-12", "", "", None, "");
794 let p = Path::new("/my/dir/2021-04-12-");
795 let result = p.disassemble();
796 assert_eq!(result, expected);
797
798 let expected = ("2021-04-12", ".dotfile", ".dotfile", None, "");
800 let p = Path::new("/my/dir/2021-04-12-'.dotfile");
801 let result = p.disassemble();
802 assert_eq!(result, expected);
803
804 let expected = ("2021-04-12", "(9).md", "", Some(9), "md");
805 let p = Path::new("/my/dir/2021-04-12-(9).md");
806 let result = p.disassemble();
807 assert_eq!(result, expected);
808
809 let expected = (
810 "20221030",
811 "Some.pdf--Note.md",
812 "Some.pdf--Note",
813 None,
814 "md",
815 );
816 let p = Path::new("/my/dir/20221030-Some.pdf--Note.md");
817 let result = p.disassemble();
818 assert_eq!(result, expected);
819
820 let expected = (
821 "1_2_3",
822 "my_title--my_subtitle(1).md",
823 "my_title--my_subtitle",
824 Some(1),
825 "md",
826 );
827 let p = Path::new("/my/dir/1_2_3-my_title--my_subtitle(1).md");
828 let result = p.disassemble();
829 assert_eq!(result, expected);
830
831 let expected = (
832 "1_2_3",
833 "123 my_title--my_subtitle(1).md",
834 "123 my_title--my_subtitle",
835 Some(1),
836 "md",
837 );
838 let p = Path::new("/my/dir/1_2_3-123 my_title--my_subtitle(1).md");
839 let result = p.disassemble();
840 assert_eq!(result, expected);
841
842 let expected = (
843 "1_2_3-123",
844 "My_title--my_subtitle(1).md",
845 "My_title--my_subtitle",
846 Some(1),
847 "md",
848 );
849 let p = Path::new("/my/dir/1_2_3-123-My_title--my_subtitle(1).md");
850 let result = p.disassemble();
851 assert_eq!(result, expected);
852
853 let expected = (
854 "1_2_3",
855 "123-my_title--my_subtitle(1).md",
856 "123-my_title--my_subtitle",
857 Some(1),
858 "md",
859 );
860 let p = Path::new("/my/dir/1_2_3-'123-my_title--my_subtitle(1).md");
861 let result = p.disassemble();
862 assert_eq!(result, expected);
863
864 let expected = (
865 "1_2_3",
866 "123 my_title--my_subtitle(1).md",
867 "123 my_title--my_subtitle",
868 Some(1),
869 "md",
870 );
871 let p = Path::new("/my/dir/1_2_3-123 my_title--my_subtitle(1).md");
872 let result = p.disassemble();
873 assert_eq!(result, expected);
874
875 let expected = (
876 "1_2_3",
877 "my_title--my_subtitle(1).md",
878 "my_title--my_subtitle",
879 Some(1),
880 "md",
881 );
882 let p = Path::new("/my/dir/1_2_3-my_title--my_subtitle(1).md");
883 let result = p.disassemble();
884 assert_eq!(expected, result);
885
886 let expected = (
887 "1a2b3ab",
888 "my_title--my_subtitle(1).md",
889 "my_title--my_subtitle",
890 Some(1),
891 "md",
892 );
893 let p = Path::new("/my/dir/1a2b3ab-my_title--my_subtitle(1).md");
894 let result = p.disassemble();
895 assert_eq!(expected, result);
896
897 let expected = (
898 "",
899 "1a2b3abc-my_title--my_subtitle(1).md",
900 "1a2b3abc-my_title--my_subtitle",
901 Some(1),
902 "md",
903 );
904 let p = Path::new("/my/dir/1a2b3abc-my_title--my_subtitle(1).md");
905 let result = p.disassemble();
906 assert_eq!(result, expected);
907
908 let expected = (
909 "1_2_3",
910 "my_title--my_subtitle(1).m d",
911 "my_title--my_subtitle(1).m d",
912 None,
913 "",
914 );
915 let p = Path::new("/my/dir/1_2_3-my_title--my_subtitle(1).m d");
916 let result = p.disassemble();
917 assert_eq!(result, expected);
918
919 let expected = (
920 "1_2_3",
921 "my_title--my_subtitle(1)",
922 "my_title--my_subtitle",
923 Some(1),
924 "",
925 );
926 let p = Path::new("/my/dir/1_2_3-my_title--my_subtitle(1)");
927 let result = p.disassemble();
928 assert_eq!(result, expected);
929 }
930
931 #[test]
932 fn test_exclude_copy_counter_eq() {
933 use crate::filename::NotePath;
934
935 let p1 = PathBuf::from("/mypath/123-title(1).md");
936 let p2 = PathBuf::from("/mypath/123-title(3).md");
937 let expected = true;
938 let result = Path::exclude_copy_counter_eq(&p1, &p2);
939 assert_eq!(expected, result);
940
941 let p1 = PathBuf::from("/mypath/123-title(1).md");
942 let p2 = PathBuf::from("/mypath/123-titlX(3).md");
943 let expected = false;
944 let result = Path::exclude_copy_counter_eq(&p1, &p2);
945 assert_eq!(expected, result);
946 }
947
948 #[test]
949 fn test_note_path_has_tpnote_ext() {
950 use crate::filename::NotePath;
951
952 let path = Path::new("/dir/file.md");
954 assert!(path.has_tpnote_ext());
955
956 let path = Path::new("/dir/file.abc");
958 assert!(!path.has_tpnote_ext());
959
960 let path = Path::new("md");
963 assert!(!path.has_tpnote_ext());
964 }
965
966 #[test]
967 fn test_has_wellformed_filename() {
968 use crate::filename::NotePath;
969 use std::path::Path;
970
971 assert!(&Path::new("long filename.ext").has_wellformed_filename());
973
974 assert!(&Path::new("long directory name/long filename.ext").has_wellformed_filename());
976
977 assert!(&Path::new(".dotfile").has_wellformed_filename());
979
980 assert!(&Path::new(".dotfile.ext").has_wellformed_filename());
982
983 assert!(!&Path::new(".dot file").has_wellformed_filename());
985
986 assert!(!&Path::new("filename.e xt").has_wellformed_filename());
988
989 assert!(!&Path::new("filename. ext").has_wellformed_filename());
991
992 assert!(!&Path::new("filename.ext ").has_wellformed_filename());
994
995 assert!(&Path::new("/path/to/filename.ext").has_wellformed_filename());
997 }
998
999 #[test]
1000 fn test_trim_copy_counter() {
1001 use crate::filename::NotePathStr;
1002
1003 let expected = ("my_stem", Some(78));
1005 let result = "my_stem(78)".split_copy_counter();
1006 assert_eq!(expected, result);
1007
1008 let expected = ("my_stem", Some(78));
1010 let result = "my_stem-(78)".split_copy_counter();
1011 assert_eq!(expected, result);
1012
1013 let expected = ("my_stem_", Some(78));
1015 let result = "my_stem_(78)".split_copy_counter();
1016 assert_eq!(expected, result);
1017
1018 assert_eq!(expected, result);
1020 let expected = ("my_stem_(78))", None);
1021 let result = "my_stem_(78))".split_copy_counter();
1022 assert_eq!(expected, result);
1023
1024 let expected = ("my_stem_)78)", None);
1026 let result = "my_stem_)78)".split_copy_counter();
1027 assert_eq!(expected, result);
1028 }
1029
1030 #[test]
1031 fn test_split_sort_tag() {
1032 use crate::filename::NotePathStr;
1033
1034 let expected = ("123", "", true);
1035 let result = "123".split_sort_tag(true);
1036 assert_eq!(expected, result);
1037
1038 let expected = ("123", "Rest", true);
1039 let result = "123-Rest".split_sort_tag(false);
1040 assert_eq!(expected, result);
1041
1042 let expected = ("2023-10-30", "Rest", false);
1043 let result = "2023-10-30-Rest".split_sort_tag(false);
1044 assert_eq!(expected, result);
1045 }
1046
1047 #[test]
1048 fn test_note_path_str_has_tpnote() {
1049 use crate::filename::NotePathStr;
1050
1051 let path_str = "/dir/file.md";
1053 assert!(path_str.has_tpnote_ext());
1054
1055 let path_str = "/dir/file.abc";
1057 assert!(!path_str.has_tpnote_ext());
1058 }
1059
1060 #[test]
1061 fn test_is_tpnote_ext() {
1062 use crate::filename::Extension;
1063 let ext = "md";
1065 assert!(ext.is_tpnote_ext());
1066
1067 let ext = "/dir/file.md";
1069 assert!(!ext.is_tpnote_ext());
1070 }
1071
1072 #[test]
1073 fn test_filename_is_valid_sort_tag() {
1074 use super::NotePathStr;
1075 let f = "20230821";
1076 assert_eq!(f.is_valid_sort_tag(), Some("20230821"));
1077
1078 let f = "dir/20230821";
1079 assert_eq!(f.is_valid_sort_tag(), Some("20230821"));
1080
1081 let f = "dir\\20230821";
1082 assert_eq!(f.is_valid_sort_tag(), Some("20230821"));
1083
1084 let f = "1_3_2";
1085 assert_eq!(f.is_valid_sort_tag(), Some("1_3_2"));
1086
1087 let f = "1c2";
1088 assert_eq!(f.is_valid_sort_tag(), Some("1c2"));
1089
1090 let f = "2023ab";
1091 assert_eq!(f.is_valid_sort_tag(), Some("2023ab"));
1092
1093 let f = "2023abc";
1094 assert_eq!(f.is_valid_sort_tag(), None);
1095
1096 let f = "dir/2023abc";
1097 assert_eq!(f.is_valid_sort_tag(), None);
1098
1099 let f = "2023A";
1100 assert_eq!(f.is_valid_sort_tag(), None);
1101
1102 let f = "20230821";
1103 assert_eq!(f.is_valid_sort_tag(), Some("20230821"));
1104
1105 let f = "2023-08-21";
1106 assert_eq!(f.is_valid_sort_tag(), Some("2023-08-21"));
1107
1108 let f = "20-08-21";
1109 assert_eq!(f.is_valid_sort_tag(), Some("20-08-21"));
1110
1111 let f = "2023ab";
1112 assert_eq!(f.is_valid_sort_tag(), Some("2023ab"));
1113
1114 let f = "202ab";
1115 assert_eq!(f.is_valid_sort_tag(), Some("202ab"));
1116 }
1117}