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