freebusy.rs
⎇
Raw
1//! Busy time of calendar objects, for free-busy-query (RFC 4791, 7.10).
2
3use calcard::icalendar::{
4 ICalendar, ICalendarComponentType, ICalendarEntry, ICalendarFreeBusyType,
5 ICalendarParameterName, ICalendarParameterValue, ICalendarPeriod, ICalendarProperty,
6 ICalendarStatus, ICalendarTransparency, ICalendarValue,
7};
8use chrono::{DateTime, Utc};
9
10use crate::expand::{expand, stamp};
11use crate::filter::TimeRange;
12use crate::zone::{Zone, Zones, add};
13
14/// In the order a client ranks them.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
16pub enum Busy {
17 Busy,
18 Tentative,
19 Unavailable,
20}
21
22impl Busy {
23 fn fbtype(self) -> &'static str {
24 match self {
25 Busy::Busy => "BUSY",
26 Busy::Tentative => "BUSY-TENTATIVE",
27 Busy::Unavailable => "BUSY-UNAVAILABLE",
28 }
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Period {
34 pub kind: Busy,
35 pub start: DateTime<Utc>,
36 pub end: DateTime<Utc>,
37}
38
39/// The busy periods of one calendar object, clipped to `range`. Transparent
40/// and cancelled events are free; VFREEBUSY components count as stored.
41pub fn busy(cal: &ICalendar, range: &TimeRange, floating: &Zone) -> Vec<Period> {
42 let zones = Zones::new(cal, floating.clone());
43 let mut out = Vec::new();
44 let mut push = |kind, start: DateTime<Utc>, end: DateTime<Utc>| {
45 let (start, end) = (start.max(range.start), end.min(range.end));
46 if start < end {
47 out.push(Period { kind, start, end });
48 }
49 };
50 for x in expand(cal, range.clone(), floating.clone()).instances {
51 let c = &cal.components[x.component];
52 if c.component_type != ICalendarComponentType::VEvent
53 || c.transparency() == Some(&ICalendarTransparency::Transparent)
54 {
55 continue;
56 }
57 let kind = match c.status() {
58 Some(ICalendarStatus::Cancelled) => continue,
59 Some(ICalendarStatus::Tentative) => Busy::Tentative,
60 _ => Busy::Busy,
61 };
62 push(kind, x.start, x.end);
63 }
64 for c in cal
65 .components
66 .iter()
67 .filter(|c| c.component_type == ICalendarComponentType::VFreebusy)
68 {
69 for e in c.properties(&ICalendarProperty::Freebusy) {
70 let kind = match e.parameter(&ICalendarParameterName::Fbtype) {
71 Some(ICalendarParameterValue::Fbtype(ICalendarFreeBusyType::Free)) => continue,
72 Some(ICalendarParameterValue::Fbtype(ICalendarFreeBusyType::BusyTentative)) => {
73 Busy::Tentative
74 }
75 Some(ICalendarParameterValue::Fbtype(ICalendarFreeBusyType::BusyUnavailable)) => {
76 Busy::Unavailable
77 }
78 _ => Busy::Busy,
79 };
80 for (s, e) in periods(&zones, e) {
81 push(kind, s, e);
82 }
83 }
84 }
85 out
86}
87
88/// Sorts the periods and joins those of one kind that overlap or touch.
89pub fn merge(mut periods: Vec<Period>) -> Vec<Period> {
90 periods.sort_by_key(|p| (p.kind, p.start));
91 let mut out: Vec<Period> = Vec::with_capacity(periods.len());
92 for p in periods {
93 match out.last_mut() {
94 Some(last) if last.kind == p.kind && p.start <= last.end => {
95 last.end = last.end.max(p.end);
96 }
97 _ => out.push(p),
98 }
99 }
100 out
101}
102
103/// The VCALENDAR a free-busy-query answers with.
104pub fn vfreebusy(periods: &[Period], range: &TimeRange, now: DateTime<Utc>) -> String {
105 let t = |d: DateTime<Utc>| d.format("%Y%m%dT%H%M%SZ").to_string();
106 let mut out = format!(
107 "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//filebrowser-ng//pimdav//EN\r\n\
108 BEGIN:VFREEBUSY\r\nDTSTAMP:{}\r\nDTSTART:{}\r\nDTEND:{}\r\n",
109 t(now),
110 t(range.start),
111 t(range.end)
112 );
113 for p in periods {
114 out.push_str(&format!(
115 "FREEBUSY;FBTYPE={}:{}/{}\r\n",
116 p.kind.fbtype(),
117 t(p.start),
118 t(p.end)
119 ));
120 }
121 out.push_str("END:VFREEBUSY\r\nEND:VCALENDAR\r\n");
122 out
123}
124
125/// The PERIOD values of a FREEBUSY property.
126pub(crate) fn periods(zones: &Zones, e: &ICalendarEntry) -> Vec<(DateTime<Utc>, DateTime<Utc>)> {
127 e.values
128 .iter()
129 .filter_map(|v| period(zones, e.tz_id(), v))
130 .collect()
131}
132
133pub(crate) fn period(
134 zones: &Zones,
135 tzid: Option<&str>,
136 v: &ICalendarValue,
137) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
138 match v {
139 ICalendarValue::Period(ICalendarPeriod::Range { start, end }) => Some((
140 stamp(zones, start, tzid)?.utc(),
141 stamp(zones, end, tzid)?.utc(),
142 )),
143 ICalendarValue::Period(ICalendarPeriod::Duration { start, duration }) => {
144 let s = stamp(zones, start, tzid)?.utc();
145 Some((s, add(s, duration.to_time_delta()?)))
146 }
147 _ => None,
148 }
149}
150