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