Skip to content

Commit 294edaf

Browse files
committed
feat: added period_union transform
1 parent f60809c commit 294edaf

File tree

5 files changed

+148
-0
lines changed

5 files changed

+148
-0
lines changed

aw-models/src/event.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use serde_json::Map;
77
use serde_json::Value;
88

99
use crate::duration::DurationSerialization;
10+
use crate::TimeInterval;
1011

1112
#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)]
1213
pub struct Event {
@@ -34,6 +35,9 @@ impl Event {
3435
self.timestamp
3536
+ chrono::Duration::nanoseconds(self.duration.num_nanoseconds().unwrap() as i64)
3637
}
38+
pub fn interval(&self) -> TimeInterval {
39+
TimeInterval::new(self.timestamp, self.calculate_endtime())
40+
}
3741
}
3842

3943
impl PartialEq for Event {

aw-models/src/timeinterval.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::cmp::{max, min};
12
use std::fmt;
23

34
use serde::de::{self, Deserialize, Deserializer, Visitor};
@@ -19,6 +20,7 @@ pub enum TimeIntervalError {
1920
ParseError(),
2021
}
2122

23+
/// Python versions of many of these functions can be found at https://github.com/ErikBjare/timeslot
2224
impl TimeInterval {
2325
pub fn new(start: DateTime<Utc>, end: DateTime<Utc>) -> TimeInterval {
2426
TimeInterval { start, end }
@@ -52,6 +54,28 @@ impl TimeInterval {
5254
pub fn duration(&self) -> Duration {
5355
self.end - self.start
5456
}
57+
58+
/// If intervals are separated by a non-zero gap, return the gap as a new TimeInterval, else None
59+
pub fn gap(&self, other: &TimeInterval) -> Option<TimeInterval> {
60+
if self.end < other.start {
61+
Some(TimeInterval::new(self.end, other.start))
62+
} else if other.end < self.start {
63+
Some(TimeInterval::new(other.end, self.start))
64+
} else {
65+
None
66+
}
67+
}
68+
69+
/// Joins two intervals together if they don't have a gap, else None
70+
pub fn union(&self, other: &TimeInterval) -> Option<TimeInterval> {
71+
match self.gap(other) {
72+
Some(_) => None,
73+
None => Some(TimeInterval::new(
74+
min(self.start, other.start),
75+
max(self.end, other.end),
76+
)),
77+
}
78+
}
5579
}
5680

5781
impl fmt::Display for TimeInterval {

aw-query/src/functions.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ pub fn fill_env(env: &mut VarEnv) {
101101
"tag".to_string(),
102102
DataType::Function("tag".into(), qfunctions::tag),
103103
);
104+
env.insert(
105+
"period_union".to_string(),
106+
DataType::Function("period_union".into(), qfunctions::period_union),
107+
);
104108
}
105109

106110
mod qfunctions {
@@ -503,6 +507,24 @@ mod qfunctions {
503507
}
504508
Ok(DataType::List(event_list))
505509
}
510+
511+
pub fn period_union(
512+
args: Vec<DataType>,
513+
_env: &VarEnv,
514+
_ds: &Datastore,
515+
) -> Result<DataType, QueryError> {
516+
// typecheck
517+
validate::args_length(&args, 2)?;
518+
let events1: Vec<Event> = (&args[0]).try_into()?;
519+
let events2: Vec<Event> = (&args[1]).try_into()?;
520+
521+
let mut result = aw_transform::filter_period_intersect(&events1, &events2);
522+
let mut result_tagged = Vec::new();
523+
for event in result.drain(..) {
524+
result_tagged.push(DataType::Event(event));
525+
}
526+
Ok(DataType::List(result_tagged))
527+
}
506528
}
507529

508530
mod validate {

aw-transform/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,6 @@ pub use filter_period::filter_period_intersect;
4444

4545
mod split_url;
4646
pub use split_url::split_url_event;
47+
48+
mod period_union;
49+
pub use period_union::period_union;

aw-transform/src/period_union.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use super::sort::sort_by_timestamp;
2+
use aw_models::Event;
3+
use chrono::{DateTime, Utc};
4+
use std::collections::VecDeque;
5+
6+
/// Takes a list of two events and returns a new list of events covering the union
7+
/// of the timeperiods contained in the eventlists with no overlapping events.
8+
///
9+
/// aw-core implementation: https://github.com/ActivityWatch/aw-core/blob/b11fbe08a0405dec01380493f7b3261163cc6878/aw_transform/filter_period_intersect.py#L92
10+
///
11+
/// WARNING: This function strips all data from events as it cannot keep it consistent.
12+
///
13+
///
14+
/// # Example
15+
/// ```ignore
16+
/// events1 | ------- --------- |
17+
/// events2 | ------ --- -- ---- |
18+
/// result | ----------- -- --------- |
19+
/// ```
20+
pub fn period_union(events1: &[Event], events2: &[Event]) -> Vec<Event> {
21+
let mut sorted_events: VecDeque<Event> = VecDeque::new();
22+
sorted_events.extend(sort_by_timestamp([events1, events2].concat()));
23+
24+
let mut events_union = Vec::new();
25+
26+
if !events1.is_empty() {
27+
events_union.push(sorted_events.pop_front().unwrap())
28+
}
29+
30+
for e in sorted_events {
31+
let last_event = events_union.last().unwrap();
32+
33+
let e_p = e.interval();
34+
let le_p = last_event.interval();
35+
36+
match e_p.union(&le_p) {
37+
Some(new_period) => {
38+
// If no gap and could be unioned, modify last event
39+
let mut e_mod = events_union.pop().unwrap();
40+
e_mod.duration = new_period.duration();
41+
events_union.push(e_mod);
42+
}
43+
None => {
44+
// If gap and could not be unioned, push event
45+
events_union.push(e);
46+
}
47+
}
48+
}
49+
50+
// for event in merged_events:
51+
// # Clear data
52+
// event.data = {}
53+
54+
events_union
55+
}
56+
57+
#[cfg(test)]
58+
mod tests {
59+
use std::str::FromStr;
60+
61+
use chrono::DateTime;
62+
use chrono::Duration;
63+
use chrono::Utc;
64+
use serde_json::json;
65+
66+
use aw_models::Event;
67+
68+
use super::period_union;
69+
70+
#[test]
71+
fn test_period_union_empty() {
72+
let e_result = period_union(&[], &[]);
73+
assert_eq!(e_result.len(), 0);
74+
}
75+
76+
#[test]
77+
fn test_period_union() {
78+
let e1 = Event {
79+
id: None,
80+
timestamp: DateTime::from_str("2000-01-01T00:00:01Z").unwrap(),
81+
duration: Duration::seconds(1),
82+
data: json_map! {"test": json!(1)},
83+
};
84+
85+
let mut e2 = e1.clone();
86+
e2.timestamp = DateTime::from_str("2000-01-01T00:00:02Z").unwrap();
87+
88+
let e_result = period_union(&[e1], &[e2]);
89+
assert_eq!(e_result.len(), 1);
90+
91+
let dt: DateTime<Utc> = DateTime::from_str("2000-01-01T00:00:01.000Z").unwrap();
92+
assert_eq!(e_result[0].timestamp, dt);
93+
assert_eq!(e_result[0].duration, Duration::milliseconds(2000));
94+
}
95+
}

0 commit comments

Comments
 (0)