devjournal 0.1.0

A dev first cli journaling tool
Documentation
use anyhow::{Result, bail};
use chrono::{Datelike, Duration, Local, NaiveDateTime, NaiveTime, Weekday};
use sha2::{Digest, Sha256};
use uuid::Uuid;

#[derive(Debug, Clone)]
pub enum ParsedDate {
    Today,
    Yesterday,
    LastWeek,
    LastWeekday(Weekday),
    Concrete(NaiveDateTime),
}

impl ParsedDate {
    /// Parse prospective date into a datetime
    pub fn to_naive_datetime(&self) -> NaiveDateTime {
        let today = to_midnight(&now());

        match self {
            ParsedDate::Today => today,
            ParsedDate::Yesterday => today - Duration::days(1),
            ParsedDate::LastWeek => today - Duration::days(7),
            ParsedDate::LastWeekday(target_weekday) => {
                // Start at yesterday so that last monday on a monday works
                let mut day = today - Duration::days(1);
                while day.weekday() != *target_weekday {
                    day -= Duration::days(1);
                }
                to_midnight(&day)
            }
            ParsedDate::Concrete(date) => *date,
        }
    }
}

/// Get current date time
pub fn now() -> NaiveDateTime {
    Local::now().naive_local()
}

fn to_midnight(datetime: &NaiveDateTime) -> NaiveDateTime {
    let midnight = NaiveTime::from_hms_opt(0, 0, 0).unwrap();
    return NaiveDateTime::new(datetime.date(), midnight);
}

fn weekday_from_str(day: &str) -> Option<Weekday> {
    match day {
        "monday" => Some(Weekday::Mon),
        "tuesday" => Some(Weekday::Tue),
        "wednesday" => Some(Weekday::Wed),
        "thursday" => Some(Weekday::Thu),
        "friday" => Some(Weekday::Fri),
        "saturday" => Some(Weekday::Sat),
        "sunday" => Some(Weekday::Sun),
        _ => None,
    }
}

pub fn parse_date_arg(input: &str) -> Result<NaiveDateTime> {
    let input = input.trim().to_lowercase();

    let parsed_date = match input.as_str() {
        "today" => ParsedDate::Today,
        "yesterday" => ParsedDate::Yesterday,
        "last-week" => ParsedDate::LastWeek,
        _ if input.starts_with("last-") => {
            let day_str = &input[5..]; // after "last-"
            if let Some(weekday) = weekday_from_str(day_str) {
                ParsedDate::LastWeekday(weekday)
            } else {
                bail!("Invalid last- weekday: {}", day_str);
            }
        }
        _ => {
            // Parse concrete date in dd/mm/yy format
            // e.g. "14/04/25" -> 14 April 2025
            if let Ok(date) = NaiveDateTime::parse_from_str(&input, "%d/%m/%y") {
                ParsedDate::Concrete(date)
            } else {
                bail!("Invalid date format: expected dd/mm/yy, got '{}'", input);
            }
        }
    };

    Ok(parsed_date.to_naive_datetime())
}

pub fn generate_reproducible_id_from_datetime(date_time: NaiveDateTime) -> Uuid {
    // Get year-month-day as string
    let date_str = format!(
        "{:04}-{:02}-{:02}",
        date_time.year(),
        date_time.month(),
        date_time.day()
    );

    // Hash it with SHA-256
    let hash = Sha256::digest(date_str.as_bytes());

    // Take first 16 bytes for UUID
    let mut bytes = [0u8; 16];
    bytes.copy_from_slice(&hash[..16]);

    // Manually set UUIDv4 version and variant bits
    // version (4 bits) = 4 for UUIDv4 at bits 12..15 (bytes[6])
    bytes[6] = (bytes[6] & 0x0F) | 0x40;

    // variant (2 bits) = 10 for RFC 4122 variant at bits 6..7 (bytes[8])
    bytes[8] = (bytes[8] & 0x3F) | 0x80;

    Uuid::from_bytes(bytes)
}