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 use pretty_assertions::assert_eq;
158
159 use super::*;
160 use crate::{Note, Tags};
161
162 #[test]
163 fn it_returns_true_when_empty() {
164 let section = Section::new("Currently");
165
166 assert_eq!(section.is_empty(), true);
167 }
168
169 #[test]
170 fn it_returns_false_when_not_empty() {
171 let mut section = Section::new("Currently");
172 section.add_entry(Entry::new(
173 Local::now(),
174 "Test",
175 Tags::new(),
176 Note::new(),
177 "Currently",
178 None::<String>,
179 ));
180
181 assert_eq!(section.is_empty(), false);
182 }
183 }
184
185 mod len {
186 use chrono::Local;
187 use pretty_assertions::assert_eq;
188
189 use super::*;
190 use crate::{Note, Tags};
191
192 #[test]
193 fn it_returns_entry_count() {
194 let mut section = Section::new("Currently");
195
196 assert_eq!(section.len(), 0);
197
198 section.add_entry(Entry::new(
199 Local::now(),
200 "First",
201 Tags::new(),
202 Note::new(),
203 "Currently",
204 None::<String>,
205 ));
206 section.add_entry(Entry::new(
207 Local::now(),
208 "Second",
209 Tags::new(),
210 Note::new(),
211 "Currently",
212 None::<String>,
213 ));
214
215 assert_eq!(section.len(), 2);
216 }
217 }
218
219 mod remove_entry {
220 use chrono::Local;
221 use pretty_assertions::assert_eq;
222
223 use super::*;
224 use crate::{Note, Tags};
225
226 #[test]
227 fn it_removes_matching_entry() {
228 let entry = Entry::new(
229 Local::now(),
230 "Test",
231 Tags::new(),
232 Note::new(),
233 "Currently",
234 None::<String>,
235 );
236 let id = entry.id().to_string();
237 let mut section = Section::new("Currently");
238 section.add_entry(entry);
239
240 let removed = section.remove_entry(&id);
241
242 assert_eq!(removed, 1);
243 assert_eq!(section.len(), 0);
244 }
245
246 #[test]
247 fn it_returns_zero_when_no_match() {
248 let mut section = Section::new("Currently");
249 section.add_entry(Entry::new(
250 Local::now(),
251 "Test",
252 Tags::new(),
253 Note::new(),
254 "Currently",
255 None::<String>,
256 ));
257
258 let removed = section.remove_entry("nonexistent");
259
260 assert_eq!(removed, 0);
261 assert_eq!(section.len(), 1);
262 }
263 }
264}