Skip to main content

mdvault_core/
ids.rs

1//! ID generation utilities for projects and tasks.
2//!
3//! Projects get a 3-letter ID derived from their title.
4//! Tasks get the project ID + a 3-digit counter (e.g., "MCP-001").
5
6/// Generate a project ID from a title.
7///
8/// Takes up to 3 meaningful letters from the title:
9/// - If multiple words, takes first letter of each word (up to 3)
10/// - If single word, takes first 3 letters
11/// - Converts to uppercase
12///
13/// # Examples
14/// ```
15/// use mdvault_core::ids::generate_project_id;
16///
17/// assert_eq!(generate_project_id("My Cool Project"), "MCP");
18/// assert_eq!(generate_project_id("Inventory"), "INV");
19/// assert_eq!(generate_project_id("AI"), "AI");
20/// assert_eq!(generate_project_id("a b c d e"), "ABC");
21/// ```
22pub fn generate_project_id(title: &str) -> String {
23    let words: Vec<&str> = title.split_whitespace().filter(|w| !w.is_empty()).collect();
24
25    let id = if words.len() >= 3 {
26        // Take first letter of first 3 words
27        words.iter().take(3).filter_map(|w| w.chars().next()).collect::<String>()
28    } else if words.len() == 2 {
29        // Two words: first letter of each, plus second letter of longer word
30        let mut chars: Vec<char> =
31            words.iter().filter_map(|w| w.chars().next()).collect();
32        // Add one more char from the longer word
33        let longer = if words[0].len() >= words[1].len() { words[0] } else { words[1] };
34        if let Some(c) = longer.chars().nth(1) {
35            chars.push(c);
36        }
37        chars.into_iter().collect()
38    } else if words.len() == 1 {
39        // Single word: take first 3 letters
40        words[0].chars().take(3).collect()
41    } else {
42        // Empty title, generate placeholder
43        "XXX".to_string()
44    };
45
46    id.to_uppercase()
47}
48
49/// Generate a task ID from a project ID and counter.
50///
51/// Format: `{project_id}-{counter:03}`
52///
53/// # Examples
54/// ```
55/// use mdvault_core::ids::generate_task_id;
56///
57/// assert_eq!(generate_task_id("MCP", 1), "MCP-001");
58/// assert_eq!(generate_task_id("INV", 42), "INV-042");
59/// assert_eq!(generate_task_id("AI", 999), "AI-999");
60/// ```
61pub fn generate_task_id(project_id: &str, counter: u32) -> String {
62    format!("{}-{:03}", project_id, counter)
63}
64
65/// Parse a task ID to extract project ID and counter.
66///
67/// Returns None if the format is invalid.
68///
69/// # Examples
70/// ```
71/// use mdvault_core::ids::parse_task_id;
72///
73/// assert_eq!(parse_task_id("MCP-001"), Some(("MCP".to_string(), 1)));
74/// assert_eq!(parse_task_id("INV-042"), Some(("INV".to_string(), 42)));
75/// assert_eq!(parse_task_id("invalid"), None);
76/// ```
77pub fn parse_task_id(task_id: &str) -> Option<(String, u32)> {
78    let parts: Vec<&str> = task_id.splitn(2, '-').collect();
79    if parts.len() != 2 {
80        return None;
81    }
82
83    let project_id = parts[0].to_string();
84    let counter = parts[1].parse::<u32>().ok()?;
85
86    Some((project_id, counter))
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn test_project_id_multiple_words() {
95        assert_eq!(generate_project_id("My Cool Project"), "MCP");
96        assert_eq!(generate_project_id("Knowledge Base System"), "KBS");
97    }
98
99    #[test]
100    fn test_project_id_two_words() {
101        // Two words: first letter of each + second letter of longer word
102        assert_eq!(generate_project_id("Home Automation"), "HAU");
103        assert_eq!(generate_project_id("AI Research"), "ARE");
104    }
105
106    #[test]
107    fn test_project_id_single_word() {
108        assert_eq!(generate_project_id("Inventory"), "INV");
109        assert_eq!(generate_project_id("AI"), "AI");
110        assert_eq!(generate_project_id("X"), "X");
111    }
112
113    #[test]
114    fn test_project_id_empty() {
115        assert_eq!(generate_project_id(""), "XXX");
116        assert_eq!(generate_project_id("   "), "XXX");
117    }
118
119    #[test]
120    fn test_project_id_lowercase() {
121        assert_eq!(generate_project_id("my cool project"), "MCP");
122    }
123
124    #[test]
125    fn test_task_id_generation() {
126        assert_eq!(generate_task_id("MCP", 1), "MCP-001");
127        assert_eq!(generate_task_id("MCP", 42), "MCP-042");
128        assert_eq!(generate_task_id("MCP", 999), "MCP-999");
129    }
130
131    #[test]
132    fn test_parse_task_id() {
133        assert_eq!(parse_task_id("MCP-001"), Some(("MCP".to_string(), 1)));
134        assert_eq!(parse_task_id("INV-042"), Some(("INV".to_string(), 42)));
135        assert_eq!(parse_task_id("invalid"), None);
136        assert_eq!(parse_task_id("MCP-abc"), None);
137    }
138}