1use crate::agenda::Occurrence;
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeSet;
13use std::path::{Path, PathBuf};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
17pub struct Overlap {
18 pub key: String,
20 pub first: Occurrence,
21 pub second: Occurrence,
22}
23
24fn held(event: &Occurrence) -> (i64, i64) {
26 (event.start - i64::from(event.margins.before) * 60, event.end + i64::from(event.margins.after) * 60)
27}
28
29pub fn overlaps(events: &[Occurrence]) -> Vec<Overlap> {
32 let mut timed: Vec<&Occurrence> = events.iter().filter(|e| !e.cancelled && !e.all_day && e.end > e.start).collect();
33 timed.sort_by(|a, b| (a.start, &a.uid).cmp(&(b.start, &b.uid)));
34 let mut out = Vec::new();
35 for (i, first) in timed.iter().enumerate() {
36 let (from, to) = held(first);
37 for second in &timed[i + 1..] {
38 if second.uid == first.uid && second.start == first.start {
40 continue;
41 }
42 let (other_from, other_to) = held(second);
43 if other_from < to && from < other_to {
44 out.push(Overlap { key: format!("{}@{}|{}@{}", first.uid, first.start, second.uid, second.start), first: (*first).clone(), second: (*second).clone() });
45 }
46 }
47 }
48 out
49}
50
51#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
54pub struct SetAside {
55 #[serde(default)]
56 pub keys: BTreeSet<String>,
57}
58
59impl SetAside {
60 pub fn default_path() -> PathBuf {
61 crate::config::state_dir().join("overlaps-set-aside.toml")
62 }
63
64 pub fn load(path: &Path) -> SetAside {
65 std::fs::read_to_string(path).ok().and_then(|t| toml::from_str(&t).ok()).unwrap_or_default()
66 }
67
68 pub fn save(&self, path: &Path) -> Result<(), String> {
69 let fail = |e: std::io::Error| format!("{}: {e}", path.display());
70 if let Some(parent) = path.parent() {
71 std::fs::create_dir_all(parent).map_err(fail)?;
72 }
73 std::fs::write(path, toml::to_string(self).map_err(|e| e.to_string())?).map_err(fail)
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use crate::demands::Margins;
81
82 fn event(uid: &str, start: i64, end: i64, margins: Margins) -> Occurrence {
83 Occurrence { uid: uid.into(), key: format!("{uid}.ics"), summary: uid.into(), start, end, margins, ..Occurrence::default() }
84 }
85
86 #[test]
87 fn two_events_at_once() {
88 let none = Margins::default();
89 let events = [event("a", 32_400, 36_000, none), event("b", 34_200, 37_800, none), event("c", 37_800, 39_600, none)];
91 let found = overlaps(&events);
92 assert_eq!(found.iter().map(|o| o.key.as_str()).collect::<Vec<_>>(), ["a@32400|b@34200"]);
93 let events = [event("b", 34_200, 37_800, none), event("c", 37_800, 39_600, Margins { before: 30, after: 0 })];
95 assert_eq!(overlaps(&events).len(), 1);
96 let cancelled = Occurrence { cancelled: true, ..event("d", 32_400, 36_000, none) };
98 let moment = event("e", 34_200, 34_200, none);
99 let twice = [event("a", 32_400, 36_000, none), event("a", 32_400, 36_000, none)];
100 assert!(overlaps(&[event("a", 32_400, 36_000, none), cancelled, moment]).is_empty());
101 assert!(overlaps(&twice).is_empty());
102 let dir = std::env::temp_dir().join(format!("sioul-overlaps-{}", std::process::id()));
104 let path = dir.join("set-aside.toml");
105 let mut aside = SetAside::load(&path);
106 aside.keys.insert(found[0].key.clone());
107 aside.save(&path).unwrap();
108 assert!(SetAside::load(&path).keys.contains("a@32400|b@34200"));
109 let _ = std::fs::remove_dir_all(dir);
110 }
111}