yz-diary-date 0.1.1

parser for my personal diary directory structure, and a simple formatter
Documentation
// Copyright (c) 2021 Alain Zscheile
// SPDX-License-Identifier: MIT OR Apache-2.0

use chrono::NaiveDate;
use std::path::Path;

fn fti_digits(s: &str) -> bool {
    s.len() >= 2 && s.chars().take(2).all(|i| i.is_ascii_digit())
}

/// usually, `parse_from_path` should be used instead of this low-level function
pub fn parse(par: &str, fin: &str) -> Option<NaiveDate> {
    if !fti_digits(fin) || par.len() < 4 {
        return None;
    }

    let y = par.parse::<i32>().ok()?;
    let m = fin[..2].parse::<u32>().unwrap();

    // verify the day info
    let mut dayinf = &fin[2..];
    if dayinf.starts_with(|i| matches!(i, '-' | '_')) {
        dayinf = &dayinf[1..];
    }
    if !fti_digits(dayinf) {
        return None;
    }
    let d = dayinf[..2].parse::<u32>().unwrap();

    chrono::NaiveDate::from_ymd_opt(y, m, d)
}

/// tries to parse a diary entry path to extract the reference date
/// should be usually given a path containing at least 2 components
pub fn parse_from_path(x: &Path) -> Option<NaiveDate> {
    let mut fin = x;
    let mut fins = fin.file_name()?.to_str()?;

    // we allow 1 additional path component after the date part
    if !fti_digits(fins) {
        fin = x.parent()?;
        fins = fin.file_name()?.to_str()?;
        if !fti_digits(fins) {
            return None;
        }
    }

    let par = fin.parent()?.file_name()?.to_str()?;
    parse(par, fins)
}

/// tries to parse a diary entry path to extract the reference date
/// should be usually given a path containing at least 2 components
/// but additionally requires that the whole path must be UTF-8
#[cfg(feature = "camino")]
pub fn parse_from_utf8path(x: &camino::Utf8Path) -> Option<NaiveDate> {
    let mut fin = x;
    let mut fins = fin.file_name()?;

    // we allow 1 additional path component after the date part
    if !fti_digits(fins) {
        fin = x.parent()?;
        fins = fin.file_name()?;
        if !fti_digits(fins) {
            return None;
        }
    }

    let par = fin.parent()?.file_name()?;
    parse(par, fins)
}

pub fn fmt(date: &NaiveDate, daysep: Option<char>) -> String {
    let tmp;
    date.format(if let Some(daysep) = daysep {
        assert!(matches!(daysep, '-' | '_'));
        tmp = format!("%Y/%m{}%d", daysep);
        &tmp
    } else {
        "%Y/%m%d"
    })
    .to_string()
}

#[cfg(test)]
mod tests {
    use crate::{fmt, NaiveDate};

    #[test]
    fn standard() {
        use crate::parse_from_path;
        use std::path::Path;
        assert_eq!(parse_from_path(Path::new("201/01_01")), None);
        assert_eq!(parse_from_path(Path::new("2016/0")), None);
        assert_eq!(
            parse_from_path(Path::new("2016/08_28")),
            Some(NaiveDate::from_ymd(2016, 08, 28))
        );
        assert_eq!(
            parse_from_path(Path::new("2016/08-28")),
            Some(NaiveDate::from_ymd(2016, 08, 28))
        );
        assert_eq!(
            parse_from_path(Path::new("2016/08-28ä")),
            Some(NaiveDate::from_ymd(2016, 08, 28))
        );
        assert_eq!(
            parse_from_path(Path::new("teller/2016/08_28")),
            Some(NaiveDate::from_ymd(2016, 08, 28))
        );
        assert_eq!(
            parse_from_path(Path::new("teller/2016/08_28nox/fluppig.jpg")),
            Some(NaiveDate::from_ymd(2016, 08, 28))
        );
        assert_eq!(
            parse_from_path(Path::new("/blog/2017/1124y_vf.html")),
            Some(NaiveDate::from_ymd(2017, 11, 24))
        );
    }

    #[cfg(feature = "camino")]
    #[test]
    fn utf8path() {
        use crate::parse_from_utf8path as parse_from_path;
        use camino::Utf8Path as Path;
        assert_eq!(parse_from_path(Path::new("201/01_01")), None);
        assert_eq!(parse_from_path(Path::new("2016/0")), None);
        assert_eq!(
            parse_from_path(Path::new("2016/08_28")),
            Some(NaiveDate::from_ymd(2016, 08, 28))
        );
        assert_eq!(
            parse_from_path(Path::new("2016/08-28")),
            Some(NaiveDate::from_ymd(2016, 08, 28))
        );
        assert_eq!(
            parse_from_path(Path::new("2016/08-28ä")),
            Some(NaiveDate::from_ymd(2016, 08, 28))
        );
        assert_eq!(
            parse_from_path(Path::new("teller/2016/08_28")),
            Some(NaiveDate::from_ymd(2016, 08, 28))
        );
        assert_eq!(
            parse_from_path(Path::new("teller/2016/08_28nox/fluppig.jpg")),
            Some(NaiveDate::from_ymd(2016, 08, 28))
        );
        assert_eq!(
            parse_from_path(Path::new("/blog/2017/1124y_vf.html")),
            Some(NaiveDate::from_ymd(2017, 11, 24))
        );
    }

    #[test]
    fn tfmt() {
        assert_eq!(fmt(&NaiveDate::from_ymd(2016, 08, 28), None), "2016/0828");
        assert_eq!(
            fmt(&NaiveDate::from_ymd(2016, 08, 28), Some('_')),
            "2016/08_28"
        );
        assert_eq!(
            fmt(&NaiveDate::from_ymd(2016, 08, 28), Some('-')),
            "2016/08-28"
        );
        assert_eq!(fmt(&NaiveDate::from_ymd(2017, 11, 24), None), "2017/1124");
        assert_eq!(
            fmt(&NaiveDate::from_ymd(2017, 11, 24), Some('_')),
            "2017/11_24"
        );
        assert_eq!(
            fmt(&NaiveDate::from_ymd(2017, 11, 24), Some('-')),
            "2017/11-24"
        );
    }

    #[test]
    #[should_panic]
    fn tfmt_fail() {
        fmt(&NaiveDate::from_ymd(2016, 08, 28), Some('*'));
    }
}