Skip to main content

doing_plugins/
helpers.rs

1use std::hash::Hash;
2
3use doing_config::Config;
4use doing_taskpaper::Entry;
5use doing_time::{DurationFormat, FormattedDuration};
6use indexmap::IndexMap;
7
8/// Format an entry's interval duration as a string, returning `None` if zero or absent.
9pub fn format_interval(entry: &Entry, config: &Config) -> Option<String> {
10  entry.interval().and_then(|iv| {
11    let fmt = DurationFormat::from_config(&config.interval_format);
12    let formatted = FormattedDuration::new(iv, fmt).to_string();
13    if formatted == "00:00:00" { None } else { Some(formatted) }
14  })
15}
16
17/// Group entries by an arbitrary key, preserving the order keys are first seen.
18pub fn group_entries_by<'a, K, F>(entries: &'a [Entry], key_fn: F) -> Vec<(K, Vec<&'a Entry>)>
19where
20  K: Eq + Hash,
21  F: Fn(&'a Entry) -> K,
22{
23  let mut map: IndexMap<K, Vec<&'a Entry>> = IndexMap::new();
24  for entry in entries {
25    map.entry(key_fn(entry)).or_default().push(entry);
26  }
27  map.into_iter().collect()
28}
29
30/// Group entries by section name, preserving the order sections are first seen.
31pub fn group_by_section(entries: &[Entry]) -> Vec<(&str, Vec<&Entry>)> {
32  group_entries_by(entries, |entry| entry.section())
33}
34
35/// Convert an entry's note lines into an HTML unordered list.
36///
37/// Returns an empty string if the note is empty.
38pub fn note_to_html_list(entry: &Entry, css_class: &str, escape: fn(&str) -> String) -> String {
39  if entry.note().is_empty() {
40    return String::new();
41  }
42
43  let items: Vec<String> = entry
44    .note()
45    .lines()
46    .iter()
47    .map(|line| format!("<li>{}</li>", escape(line.trim())))
48    .collect();
49
50  format!(r#"<ul class="{css_class}">{}</ul>"#, items.join(""))
51}
52
53#[cfg(test)]
54mod test {
55  use doing_taskpaper::{Entry, Note, Tags};
56
57  use super::*;
58  use crate::test_helpers::sample_date;
59
60  mod group_by_section {
61    use pretty_assertions::assert_eq;
62
63    use super::*;
64
65    #[test]
66    fn it_groups_entries_by_section() {
67      let entries = vec![
68        Entry::new(
69          sample_date(17, 14, 0),
70          "A",
71          Tags::new(),
72          Note::new(),
73          "Currently",
74          None::<String>,
75        ),
76        Entry::new(
77          sample_date(17, 15, 0),
78          "B",
79          Tags::new(),
80          Note::new(),
81          "Archive",
82          None::<String>,
83        ),
84        Entry::new(
85          sample_date(17, 16, 0),
86          "C",
87          Tags::new(),
88          Note::new(),
89          "Currently",
90          None::<String>,
91        ),
92      ];
93
94      let groups = group_by_section(&entries);
95
96      assert_eq!(groups.len(), 2);
97      assert_eq!(groups[0].0, "Currently");
98      assert_eq!(groups[0].1.len(), 2);
99      assert_eq!(groups[1].0, "Archive");
100      assert_eq!(groups[1].1.len(), 1);
101    }
102
103    #[test]
104    fn it_preserves_first_seen_order() {
105      let entries = vec![
106        Entry::new(
107          sample_date(17, 14, 0),
108          "A",
109          Tags::new(),
110          Note::new(),
111          "Archive",
112          None::<String>,
113        ),
114        Entry::new(
115          sample_date(17, 15, 0),
116          "B",
117          Tags::new(),
118          Note::new(),
119          "Currently",
120          None::<String>,
121        ),
122      ];
123
124      let groups = group_by_section(&entries);
125
126      assert_eq!(groups[0].0, "Archive");
127      assert_eq!(groups[1].0, "Currently");
128    }
129  }
130}