doing_taskpaper/
section.rs1use std::fmt::{Display, Formatter, Result as FmtResult};
2
3use crate::Entry;
4
5#[derive(Clone, Debug)]
10pub struct Section {
11 entries: Vec<Entry>,
12 title: String,
13 trailing_content: Vec<String>,
14}
15
16impl Section {
17 pub fn new(title: impl Into<String>) -> Self {
19 Self {
20 entries: Vec::new(),
21 title: title.into(),
22 trailing_content: Vec::new(),
23 }
24 }
25
26 pub fn add_entry(&mut self, entry: Entry) {
28 self.entries.push(entry);
29 }
30
31 pub fn entries(&self) -> &[Entry] {
33 &self.entries
34 }
35
36 pub fn entries_mut(&mut self) -> &mut Vec<Entry> {
38 &mut self.entries
39 }
40
41 pub fn into_entries(self) -> Vec<Entry> {
43 self.entries
44 }
45
46 pub fn is_empty(&self) -> bool {
48 self.entries.is_empty()
49 }
50
51 pub fn len(&self) -> usize {
53 self.entries.len()
54 }
55
56 pub fn remove_entry(&mut self, id: &str) -> usize {
58 let before = self.entries.len();
59 self.entries.retain(|e| e.id() != id);
60 before - self.entries.len()
61 }
62
63 pub fn title(&self) -> &str {
65 &self.title
66 }
67
68 pub fn trailing_content(&self) -> &[String] {
70 &self.trailing_content
71 }
72
73 pub fn trailing_content_mut(&mut self) -> &mut Vec<String> {
75 &mut self.trailing_content
76 }
77}
78
79impl Display for Section {
80 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
82 write!(f, "{}:", self.title)?;
83 for entry in &self.entries {
84 write!(f, "\n\t- {} | {}", entry.date().format("%Y-%m-%d %H:%M"), entry)?;
85 if !entry.note().is_empty() {
86 write!(f, "\n{}", entry.note())?;
87 }
88 }
89 for line in &self.trailing_content {
90 write!(f, "\n{line}")?;
91 }
92 Ok(())
93 }
94}
95
96#[cfg(test)]
97mod test {
98 use super::*;
99
100 mod display {
101 use chrono::Local;
102 use pretty_assertions::assert_eq;
103
104 use super::*;
105 use crate::{Note, Tags};
106
107 #[test]
108 fn it_formats_empty_section() {
109 let section = Section::new("Currently");
110
111 assert_eq!(format!("{section}"), "Currently:");
112 }
113
114 #[test]
115 fn it_formats_section_with_entries() {
116 let date = Local::now();
117 let formatted_date = date.format("%Y-%m-%d %H:%M");
118 let entry = Entry::new(
119 date,
120 "Working on feature",
121 Tags::new(),
122 Note::new(),
123 "Currently",
124 None::<String>,
125 );
126 let mut section = Section::new("Currently");
127 section.add_entry(entry.clone());
128
129 let output = format!("{section}");
130
131 assert!(output.starts_with("Currently:"));
132 assert!(output.contains(&format!("\t- {formatted_date} | Working on feature")));
133 }
134
135 #[test]
136 fn it_formats_section_with_notes() {
137 let date = Local::now();
138 let entry = Entry::new(
139 date,
140 "Working on feature",
141 Tags::new(),
142 Note::from_text("A note line"),
143 "Currently",
144 None::<String>,
145 );
146 let mut section = Section::new("Currently");
147 section.add_entry(entry);
148
149 let output = format!("{section}");
150
151 assert!(output.contains("\t\tA note line"));
152 }
153 }
154
155 mod is_empty {
156 use chrono::Local;
157
158 use super::*;
159 use crate::{Note, Tags};
160
161 #[test]
162 fn it_returns_true_when_empty() {
163 let section = Section::new("Currently");
164
165 assert!(section.is_empty());
166 }
167
168 #[test]
169 fn it_returns_false_when_not_empty() {
170 let mut section = Section::new("Currently");
171 section.add_entry(Entry::new(
172 Local::now(),
173 "Test",
174 Tags::new(),
175 Note::new(),
176 "Currently",
177 None::<String>,
178 ));
179
180 assert!(!section.is_empty());
181 }
182 }
183
184 mod len {
185 use chrono::Local;
186 use pretty_assertions::assert_eq;
187
188 use super::*;
189 use crate::{Note, Tags};
190
191 #[test]
192 fn it_returns_entry_count() {
193 let mut section = Section::new("Currently");
194
195 assert_eq!(section.len(), 0);
196
197 section.add_entry(Entry::new(
198 Local::now(),
199 "First",
200 Tags::new(),
201 Note::new(),
202 "Currently",
203 None::<String>,
204 ));
205 section.add_entry(Entry::new(
206 Local::now(),
207 "Second",
208 Tags::new(),
209 Note::new(),
210 "Currently",
211 None::<String>,
212 ));
213
214 assert_eq!(section.len(), 2);
215 }
216 }
217
218 mod remove_entry {
219 use chrono::Local;
220 use pretty_assertions::assert_eq;
221
222 use super::*;
223 use crate::{Note, Tags};
224
225 #[test]
226 fn it_removes_matching_entry() {
227 let entry = Entry::new(
228 Local::now(),
229 "Test",
230 Tags::new(),
231 Note::new(),
232 "Currently",
233 None::<String>,
234 );
235 let id = entry.id().to_string();
236 let mut section = Section::new("Currently");
237 section.add_entry(entry);
238
239 let removed = section.remove_entry(&id);
240
241 assert_eq!(removed, 1);
242 assert_eq!(section.len(), 0);
243 }
244
245 #[test]
246 fn it_returns_zero_when_no_match() {
247 let mut section = Section::new("Currently");
248 section.add_entry(Entry::new(
249 Local::now(),
250 "Test",
251 Tags::new(),
252 Note::new(),
253 "Currently",
254 None::<String>,
255 ));
256
257 let removed = section.remove_entry("nonexistent");
258
259 assert_eq!(removed, 0);
260 assert_eq!(section.len(), 1);
261 }
262 }
263}