Skip to main content

doing_ops/
extract_note.rs

1/// Extract a parenthetical note from a title string.
2///
3/// Matches Ruby doing behavior: extracts everything from the **first** `(` to the
4/// **last** `)` as a note, but only when the string ends with `)`.
5///
6/// Empty parentheticals `()` are ignored and do not produce a note.
7///
8/// # Examples
9///
10/// ```
11/// let (title, note) = extract_note("Working on project (some context)");
12/// assert_eq!(title, "Working on project");
13/// assert_eq!(note.unwrap(), "some context");
14/// ```
15pub fn extract_note(title: &str) -> (String, Option<String>) {
16  let trimmed = title.trim();
17
18  if !trimmed.ends_with(')') {
19    return (trimmed.to_string(), None);
20  }
21
22  // Find the first opening paren
23  let open_pos = match trimmed.find('(') {
24    Some(pos) => pos,
25    None => return (trimmed.to_string(), None),
26  };
27
28  let note_content = trimmed[open_pos + 1..trimmed.len() - 1].trim();
29
30  // Ignore empty parentheticals
31  if note_content.is_empty() {
32    return (trimmed.to_string(), None);
33  }
34
35  let title_part = trimmed[..open_pos].trim();
36
37  // Don't extract if the parenthetical is a tag value (e.g. @project(myapp))
38  if title_part.ends_with(|c: char| c.is_alphanumeric() || c == '_')
39    && title_part.contains('@')
40    && title_part
41      .rfind('@')
42      .map(|at| !title_part[at..].contains(' '))
43      .unwrap_or(false)
44  {
45    return (trimmed.to_string(), None);
46  }
47
48  (title_part.to_string(), Some(note_content.to_string()))
49}
50
51#[cfg(test)]
52mod test {
53  use super::*;
54
55  mod extract_note {
56    use pretty_assertions::assert_eq;
57
58    use super::*;
59
60    #[test]
61    fn it_combines_with_existing_note() {
62      let (title, note) = extract_note("Task (extra context)");
63
64      assert_eq!(title, "Task");
65      assert_eq!(note.unwrap(), "extra context");
66    }
67
68    #[test]
69    fn it_extracts_trailing_parenthetical() {
70      let (title, note) = extract_note("Working on project (some context)");
71
72      assert_eq!(title, "Working on project");
73      assert_eq!(note.unwrap(), "some context");
74    }
75
76    #[test]
77    fn it_handles_nested_parens() {
78      let (title, note) = extract_note("Task (note with (nested) parens)");
79
80      assert_eq!(title, "Task");
81      assert_eq!(note.unwrap(), "note with (nested) parens");
82    }
83
84    #[test]
85    fn it_ignores_tag_values() {
86      let (title, note) = extract_note("Working on @project(myapp)");
87
88      assert_eq!(title, "Working on @project(myapp)");
89      assert!(note.is_none());
90    }
91
92    #[test]
93    fn it_ignores_empty_parenthetical() {
94      let (title, note) = extract_note("Task ()");
95
96      assert_eq!(title, "Task ()");
97      assert!(note.is_none());
98    }
99
100    #[test]
101    fn it_ignores_non_trailing_parenthetical() {
102      let (title, note) = extract_note("Foo (bar) baz");
103
104      assert_eq!(title, "Foo (bar) baz");
105      assert!(note.is_none());
106    }
107
108    #[test]
109    fn it_returns_none_for_no_parenthetical() {
110      let (title, note) = extract_note("Just a title");
111
112      assert_eq!(title, "Just a title");
113      assert!(note.is_none());
114    }
115  }
116}