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