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