Skip to main content

doing_taskpaper/
section.rs

1use std::fmt::{Display, Formatter, Result as FmtResult};
2
3use crate::Entry;
4
5/// A named section in a TaskPaper doing file containing an ordered list of entries.
6///
7/// Sections correspond to top-level headings in the doing file format (e.g. `Currently:`,
8/// `Archive:`). Each section holds a title and a sequence of entries that belong to it.
9#[derive(Clone, Debug)]
10pub struct Section {
11  entries: Vec<Entry>,
12  title: String,
13}
14
15impl Section {
16  /// Create a new section with the given title and no entries.
17  pub fn new(title: impl Into<String>) -> Self {
18    Self {
19      entries: Vec::new(),
20      title: title.into(),
21    }
22  }
23
24  /// Add an entry to the end of this section.
25  pub fn add_entry(&mut self, entry: Entry) {
26    self.entries.push(entry);
27  }
28
29  /// Return a slice of all entries in this section.
30  pub fn entries(&self) -> &[Entry] {
31    &self.entries
32  }
33
34  /// Return a mutable slice of all entries in this section.
35  pub fn entries_mut(&mut self) -> &mut Vec<Entry> {
36    &mut self.entries
37  }
38
39  /// Return `true` if this section contains no entries.
40  pub fn is_empty(&self) -> bool {
41    self.entries.is_empty()
42  }
43
44  /// Return the number of entries in this section.
45  pub fn len(&self) -> usize {
46    self.entries.len()
47  }
48
49  /// Remove all entries whose ID matches the given ID, returning the number removed.
50  pub fn remove_entry(&mut self, id: &str) -> usize {
51    let before = self.entries.len();
52    self.entries.retain(|e| e.id() != id);
53    before - self.entries.len()
54  }
55
56  /// Return the section title.
57  pub fn title(&self) -> &str {
58    &self.title
59  }
60}
61
62impl Display for Section {
63  /// Format as a TaskPaper section: title line followed by indented entries with notes.
64  fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
65    write!(f, "{}:", self.title)?;
66    for entry in &self.entries {
67      write!(f, "\n\t- {} | {}", entry.date().format("%Y-%m-%d %H:%M"), entry)?;
68      if !entry.note().is_empty() {
69        write!(f, "\n{}", entry.note())?;
70      }
71    }
72    Ok(())
73  }
74}
75
76#[cfg(test)]
77mod test {
78  use super::*;
79
80  mod display {
81    use chrono::Local;
82    use pretty_assertions::assert_eq;
83
84    use super::*;
85    use crate::{Note, Tags};
86
87    #[test]
88    fn it_formats_empty_section() {
89      let section = Section::new("Currently");
90
91      assert_eq!(format!("{section}"), "Currently:");
92    }
93
94    #[test]
95    fn it_formats_section_with_entries() {
96      let date = Local::now();
97      let formatted_date = date.format("%Y-%m-%d %H:%M");
98      let entry = Entry::new(
99        date,
100        "Working on feature",
101        Tags::new(),
102        Note::new(),
103        "Currently",
104        None::<String>,
105      );
106      let mut section = Section::new("Currently");
107      section.add_entry(entry.clone());
108
109      let output = format!("{section}");
110
111      assert!(output.starts_with("Currently:"));
112      assert!(output.contains(&format!("\t- {formatted_date} | Working on feature")));
113    }
114
115    #[test]
116    fn it_formats_section_with_notes() {
117      let date = Local::now();
118      let entry = Entry::new(
119        date,
120        "Working on feature",
121        Tags::new(),
122        Note::from_str("A note line"),
123        "Currently",
124        None::<String>,
125      );
126      let mut section = Section::new("Currently");
127      section.add_entry(entry);
128
129      let output = format!("{section}");
130
131      assert!(output.contains("\t\tA note line"));
132    }
133  }
134
135  mod is_empty {
136    use chrono::Local;
137    use pretty_assertions::assert_eq;
138
139    use super::*;
140    use crate::{Note, Tags};
141
142    #[test]
143    fn it_returns_true_when_empty() {
144      let section = Section::new("Currently");
145
146      assert_eq!(section.is_empty(), true);
147    }
148
149    #[test]
150    fn it_returns_false_when_not_empty() {
151      let mut section = Section::new("Currently");
152      section.add_entry(Entry::new(
153        Local::now(),
154        "Test",
155        Tags::new(),
156        Note::new(),
157        "Currently",
158        None::<String>,
159      ));
160
161      assert_eq!(section.is_empty(), false);
162    }
163  }
164
165  mod len {
166    use chrono::Local;
167    use pretty_assertions::assert_eq;
168
169    use super::*;
170    use crate::{Note, Tags};
171
172    #[test]
173    fn it_returns_entry_count() {
174      let mut section = Section::new("Currently");
175
176      assert_eq!(section.len(), 0);
177
178      section.add_entry(Entry::new(
179        Local::now(),
180        "First",
181        Tags::new(),
182        Note::new(),
183        "Currently",
184        None::<String>,
185      ));
186      section.add_entry(Entry::new(
187        Local::now(),
188        "Second",
189        Tags::new(),
190        Note::new(),
191        "Currently",
192        None::<String>,
193      ));
194
195      assert_eq!(section.len(), 2);
196    }
197  }
198
199  mod remove_entry {
200    use chrono::Local;
201    use pretty_assertions::assert_eq;
202
203    use super::*;
204    use crate::{Note, Tags};
205
206    #[test]
207    fn it_removes_matching_entry() {
208      let entry = Entry::new(
209        Local::now(),
210        "Test",
211        Tags::new(),
212        Note::new(),
213        "Currently",
214        None::<String>,
215      );
216      let id = entry.id().to_string();
217      let mut section = Section::new("Currently");
218      section.add_entry(entry);
219
220      let removed = section.remove_entry(&id);
221
222      assert_eq!(removed, 1);
223      assert_eq!(section.len(), 0);
224    }
225
226    #[test]
227    fn it_returns_zero_when_no_match() {
228      let mut section = Section::new("Currently");
229      section.add_entry(Entry::new(
230        Local::now(),
231        "Test",
232        Tags::new(),
233        Note::new(),
234        "Currently",
235        None::<String>,
236      ));
237
238      let removed = section.remove_entry("nonexistent");
239
240      assert_eq!(removed, 0);
241      assert_eq!(section.len(), 1);
242    }
243  }
244}