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}