1use crate::time_utils::format_task_id_timestamp;
2
3pub const TASK_SLUG_MAX_LENGTH: usize = 60;
4
5pub fn build_task_slug(description: &str) -> String {
6 let slug = slug::slugify(description);
7 let trimmed = slug.chars().take(TASK_SLUG_MAX_LENGTH).collect::<String>();
8
9 if trimmed.is_empty() {
10 "task".to_owned()
11 } else {
12 trimmed
13 }
14}
15
16pub fn build_unique_task_id<F>(
17 timestamp: time::OffsetDateTime,
18 description: &str,
19 mut exists: F,
20) -> String
21where
22 F: FnMut(&str) -> bool,
23{
24 let base_id = format!(
25 "{}-{}",
26 format_task_id_timestamp(timestamp),
27 build_task_slug(description)
28 );
29
30 if !exists(&base_id) {
31 return base_id;
32 }
33
34 let mut suffix = 2;
35 loop {
36 let candidate = format!("{base_id}-{suffix}");
37 if !exists(&candidate) {
38 return candidate;
39 }
40
41 suffix += 1;
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use time::macros::datetime;
48
49 use super::{build_task_slug, build_unique_task_id};
50
51 #[test]
52 fn slug_falls_back_when_description_has_no_letters() {
53 assert_eq!(build_task_slug("!!!"), "task");
54 }
55
56 #[test]
57 fn unique_id_appends_suffix_when_needed() {
58 let id = build_unique_task_id(
59 datetime!(2026-03-16 09:08:07 UTC),
60 "Fix issue",
61 |candidate| candidate == "20260316-090807-fix-issue",
62 );
63
64 assert_eq!(id, "20260316-090807-fix-issue-2");
65 }
66}