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