1use std::{
2 collections::HashSet,
3 fmt::{Display, Formatter, Result as FmtResult},
4 path::Path,
5};
6
7use doing_error::Result;
8
9use crate::{Entry, Section};
10
11#[derive(Clone, Debug)]
16pub struct Document {
17 other_content_bottom: Vec<String>,
18 other_content_top: Vec<String>,
19 sections: Vec<Section>,
20}
21
22impl Document {
23 pub fn create_file(path: &Path, default_section: &str) -> Result<()> {
28 crate::io::create_file(path, default_section)
29 }
30
31 pub fn new() -> Self {
33 Self {
34 other_content_bottom: Vec::new(),
35 other_content_top: Vec::new(),
36 sections: Vec::new(),
37 }
38 }
39
40 pub fn parse(content: &str) -> Self {
42 crate::parser::parse(content)
43 }
44
45 pub fn add_section(&mut self, section: Section) {
48 if !self.has_section(section.title()) {
49 self.sections.push(section);
50 }
51 }
52
53 pub fn all_entries(&self) -> Vec<&Entry> {
55 self.sections.iter().flat_map(|s| s.entries()).collect()
56 }
57
58 pub fn dedup(&mut self) {
60 let mut seen = HashSet::new();
61 for section in &mut self.sections {
62 section.entries_mut().retain(|e| seen.insert(e.id().to_string()));
63 }
64 }
65
66 pub fn entries_in_section(&self, name: &str) -> Vec<&Entry> {
69 if name.eq_ignore_ascii_case("all") {
70 return self.all_entries();
71 }
72 self
73 .section_by_name(name)
74 .map(|s| s.entries().iter().collect())
75 .unwrap_or_default()
76 }
77
78 pub fn has_section(&self, name: &str) -> bool {
80 self.sections.iter().any(|s| s.title().eq_ignore_ascii_case(name))
81 }
82
83 pub fn is_empty(&self) -> bool {
85 self.sections.is_empty()
86 }
87
88 #[allow(dead_code)]
90 pub fn len(&self) -> usize {
91 self.sections.len()
92 }
93
94 pub fn other_content_bottom(&self) -> &[String] {
96 &self.other_content_bottom
97 }
98
99 pub fn other_content_bottom_mut(&mut self) -> &mut Vec<String> {
101 &mut self.other_content_bottom
102 }
103
104 pub fn other_content_top(&self) -> &[String] {
106 &self.other_content_top
107 }
108
109 pub fn other_content_top_mut(&mut self) -> &mut Vec<String> {
111 &mut self.other_content_top
112 }
113
114 pub fn remove_section(&mut self, name: &str) -> usize {
116 let before = self.sections.len();
117 self.sections.retain(|s| !s.title().eq_ignore_ascii_case(name));
118 before - self.sections.len()
119 }
120
121 pub fn section_by_name(&self, name: &str) -> Option<&Section> {
123 self.sections.iter().find(|s| s.title().eq_ignore_ascii_case(name))
124 }
125
126 pub fn section_by_name_mut(&mut self, name: &str) -> Option<&mut Section> {
128 self.sections.iter_mut().find(|s| s.title().eq_ignore_ascii_case(name))
129 }
130
131 pub fn sections(&self) -> &[Section] {
133 &self.sections
134 }
135
136 pub fn sections_mut(&mut self) -> &mut Vec<Section> {
138 &mut self.sections
139 }
140
141 pub fn sort_entries(&mut self, reverse: bool) {
144 for section in &mut self.sections {
145 section
146 .entries_mut()
147 .sort_by(|a, b| a.date().cmp(&b.date()).then_with(|| a.title().cmp(b.title())));
148 if reverse {
149 section.entries_mut().reverse();
150 }
151 }
152 }
153}
154
155impl Default for Document {
156 fn default() -> Self {
157 Self::new()
158 }
159}
160
161impl Display for Document {
162 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
164 for line in &self.other_content_top {
165 writeln!(f, "{line}")?;
166 }
167 for (i, section) in self.sections.iter().enumerate() {
168 if i > 0 || !self.other_content_top.is_empty() {
169 writeln!(f)?;
170 }
171 write!(f, "{section}")?;
172 }
173 for line in &self.other_content_bottom {
174 write!(f, "\n{line}")?;
175 }
176 Ok(())
177 }
178}
179
180#[cfg(test)]
181mod test {
182 use super::*;
183
184 mod add_section {
185 use pretty_assertions::assert_eq;
186
187 use super::*;
188
189 #[test]
190 fn it_adds_a_section() {
191 let mut doc = Document::new();
192 doc.add_section(Section::new("Currently"));
193
194 assert_eq!(doc.len(), 1);
195 }
196
197 #[test]
198 fn it_ignores_duplicate_section_names() {
199 let mut doc = Document::new();
200 doc.add_section(Section::new("Currently"));
201 doc.add_section(Section::new("currently"));
202
203 assert_eq!(doc.len(), 1);
204 }
205 }
206
207 mod all_entries {
208 use chrono::Local;
209 use pretty_assertions::assert_eq;
210
211 use super::*;
212 use crate::{Note, Tags};
213
214 #[test]
215 fn it_returns_entries_across_all_sections() {
216 let mut doc = Document::new();
217 let mut s1 = Section::new("Currently");
218 s1.add_entry(Entry::new(
219 Local::now(),
220 "Task A",
221 Tags::new(),
222 Note::new(),
223 "Currently",
224 None::<String>,
225 ));
226 let mut s2 = Section::new("Archive");
227 s2.add_entry(Entry::new(
228 Local::now(),
229 "Task B",
230 Tags::new(),
231 Note::new(),
232 "Archive",
233 None::<String>,
234 ));
235 doc.add_section(s1);
236 doc.add_section(s2);
237
238 assert_eq!(doc.all_entries().len(), 2);
239 }
240 }
241
242 mod dedup {
243 use chrono::Local;
244 use pretty_assertions::assert_eq;
245
246 use super::*;
247 use crate::{Note, Tags};
248
249 #[test]
250 fn it_removes_duplicate_entries_by_id() {
251 let entry = Entry::new(
252 Local::now(),
253 "Task A",
254 Tags::new(),
255 Note::new(),
256 "Currently",
257 Some("aaaabbbbccccddddeeeeffffaaaabbbb"),
258 );
259 let mut s1 = Section::new("Currently");
260 s1.add_entry(entry.clone());
261 let mut s2 = Section::new("Archive");
262 s2.add_entry(entry);
263 let mut doc = Document::new();
264 doc.add_section(s1);
265 doc.add_section(s2);
266
267 doc.dedup();
268
269 assert_eq!(doc.all_entries().len(), 1);
270 assert_eq!(doc.sections()[0].len(), 1);
271 assert_eq!(doc.sections()[1].len(), 0);
272 }
273 }
274
275 mod display {
276 use pretty_assertions::assert_eq;
277
278 use super::*;
279
280 #[test]
281 fn it_formats_empty_document() {
282 let doc = Document::new();
283
284 assert_eq!(format!("{doc}"), "");
285 }
286
287 #[test]
288 fn it_formats_sections_in_order() {
289 let mut doc = Document::new();
290 doc.add_section(Section::new("Currently"));
291 doc.add_section(Section::new("Archive"));
292
293 let output = format!("{doc}");
294
295 assert!(output.starts_with("Currently:"));
296 assert!(output.contains("\nArchive:"));
297 }
298
299 #[test]
300 fn it_includes_other_content_top() {
301 let mut doc = Document::new();
302 doc.other_content_top_mut().push("# My Doing File".to_string());
303 doc.add_section(Section::new("Currently"));
304
305 let output = format!("{doc}");
306
307 assert!(output.starts_with("# My Doing File\n"));
308 assert!(output.contains("Currently:"));
309 }
310
311 #[test]
312 fn it_includes_other_content_bottom() {
313 let mut doc = Document::new();
314 doc.add_section(Section::new("Currently"));
315 doc.other_content_bottom_mut().push("# Footer".to_string());
316
317 let output = format!("{doc}");
318
319 assert!(output.contains("Currently:"));
320 assert!(output.ends_with("# Footer"));
321 }
322 }
323
324 mod entries_in_section {
325 use chrono::Local;
326 use pretty_assertions::assert_eq;
327
328 use super::*;
329 use crate::{Note, Tags};
330
331 #[test]
332 fn it_returns_entries_from_named_section() {
333 let mut doc = Document::new();
334 let mut section = Section::new("Currently");
335 section.add_entry(Entry::new(
336 Local::now(),
337 "Task A",
338 Tags::new(),
339 Note::new(),
340 "Currently",
341 None::<String>,
342 ));
343 doc.add_section(section);
344
345 assert_eq!(doc.entries_in_section("currently").len(), 1);
346 }
347
348 #[test]
349 fn it_returns_all_entries_for_all() {
350 let mut doc = Document::new();
351 let mut s1 = Section::new("Currently");
352 s1.add_entry(Entry::new(
353 Local::now(),
354 "Task A",
355 Tags::new(),
356 Note::new(),
357 "Currently",
358 None::<String>,
359 ));
360 let mut s2 = Section::new("Archive");
361 s2.add_entry(Entry::new(
362 Local::now(),
363 "Task B",
364 Tags::new(),
365 Note::new(),
366 "Archive",
367 None::<String>,
368 ));
369 doc.add_section(s1);
370 doc.add_section(s2);
371
372 assert_eq!(doc.entries_in_section("All").len(), 2);
373 }
374
375 #[test]
376 fn it_returns_empty_for_unknown_section() {
377 let doc = Document::new();
378
379 assert_eq!(doc.entries_in_section("Nonexistent").len(), 0);
380 }
381 }
382
383 mod has_section {
384 use pretty_assertions::assert_eq;
385
386 use super::*;
387
388 #[test]
389 fn it_finds_section_case_insensitively() {
390 let mut doc = Document::new();
391 doc.add_section(Section::new("Currently"));
392
393 assert_eq!(doc.has_section("currently"), true);
394 assert_eq!(doc.has_section("CURRENTLY"), true);
395 }
396
397 #[test]
398 fn it_returns_false_for_missing_section() {
399 let doc = Document::new();
400
401 assert_eq!(doc.has_section("Currently"), false);
402 }
403 }
404
405 mod remove_section {
406 use pretty_assertions::assert_eq;
407
408 use super::*;
409
410 #[test]
411 fn it_removes_matching_section() {
412 let mut doc = Document::new();
413 doc.add_section(Section::new("Currently"));
414
415 let removed = doc.remove_section("currently");
416
417 assert_eq!(removed, 1);
418 assert_eq!(doc.len(), 0);
419 }
420
421 #[test]
422 fn it_returns_zero_when_no_match() {
423 let mut doc = Document::new();
424
425 let removed = doc.remove_section("Nonexistent");
426
427 assert_eq!(removed, 0);
428 }
429 }
430
431 mod section_by_name {
432 use pretty_assertions::assert_eq;
433
434 use super::*;
435
436 #[test]
437 fn it_finds_section_case_insensitively() {
438 let mut doc = Document::new();
439 doc.add_section(Section::new("Currently"));
440
441 let section = doc.section_by_name("currently");
442
443 assert!(section.is_some());
444 assert_eq!(section.unwrap().title(), "Currently");
445 }
446
447 #[test]
448 fn it_returns_none_for_missing_section() {
449 let doc = Document::new();
450
451 assert!(doc.section_by_name("Currently").is_none());
452 }
453 }
454
455 mod sections {
456 use pretty_assertions::assert_eq;
457
458 use super::*;
459
460 #[test]
461 fn it_returns_sections_in_order() {
462 let mut doc = Document::new();
463 doc.add_section(Section::new("Currently"));
464 doc.add_section(Section::new("Archive"));
465
466 let names: Vec<&str> = doc.sections().iter().map(|s| s.title()).collect();
467 assert_eq!(names, vec!["Currently", "Archive"]);
468 }
469 }
470}