Skip to main content

graphrecords_query/capabilities/
aggregation.rs

1use super::{ValueEquivalence, ValueOrdering, incomparable_with_first};
2use crate::{
3    AttributeName, Failure, IndexDomain, IndexValue, Mask, QueryResult, Scalar, ValueDomain,
4    error::aggregation::InvalidMedianValue,
5};
6use chrono::TimeDelta;
7use graphrecords_core::graphrecord::GraphRecordValue;
8
9const NANOSECONDS_PER_SECOND: i128 = 1_000_000_000;
10
11pub trait ValueMedian: ValueOrdering {
12    fn validate_median(label: &'static str, value: &Self::Value<'_>) -> QueryResult<()>;
13
14    fn find_incomparable_median_values<'a, 'b>(
15        values: impl Iterator<Item = &'a Self::Value<'b>>,
16    ) -> Option<(usize, usize)>
17    where
18        Self::Value<'b>: 'a;
19
20    fn median<'a>(
21        label: &'static str,
22        lower: Self::Value<'a>,
23        upper: Option<Self::Value<'a>>,
24    ) -> QueryResult<Self::Value<'a>>;
25}
26
27pub trait ValueMode: ValueEquivalence {}
28
29pub trait ValueScalar: ValueDomain {
30    fn into_scalar(label: &'static str, value: Self::Value<'_>) -> QueryResult<GraphRecordValue>;
31
32    fn from_scalar<'a>(role: &Self::Value<'_>, value: GraphRecordValue) -> Self::Value<'a>;
33}
34
35fn validate_graphrecord_median_value(
36    label: &'static str,
37    value: &GraphRecordValue,
38) -> QueryResult<()> {
39    if matches!(
40        value,
41        GraphRecordValue::Int(_)
42            | GraphRecordValue::Float(_)
43            | GraphRecordValue::DateTime(_)
44            | GraphRecordValue::Duration(_)
45    ) {
46        Ok(())
47    } else {
48        Err(Failure::new(label, InvalidMedianValue::new(value.clone())))
49    }
50}
51
52fn median_graphrecord_value(
53    lower: GraphRecordValue,
54    upper: Option<GraphRecordValue>,
55) -> GraphRecordValue {
56    match (lower, upper) {
57        (GraphRecordValue::Int(value), None) => GraphRecordValue::Float(value as f64),
58        (GraphRecordValue::Float(value), None) => GraphRecordValue::Float(value),
59        (GraphRecordValue::DateTime(value), None) => GraphRecordValue::DateTime(value),
60        (GraphRecordValue::Duration(value), None) => GraphRecordValue::Duration(value),
61        (GraphRecordValue::Int(lower), Some(GraphRecordValue::Int(upper))) => {
62            GraphRecordValue::Float((lower as f64).midpoint(upper as f64))
63        }
64        (GraphRecordValue::Int(lower), Some(GraphRecordValue::Float(upper))) => {
65            GraphRecordValue::Float((lower as f64).midpoint(upper))
66        }
67        (GraphRecordValue::Float(lower), Some(GraphRecordValue::Int(upper))) => {
68            GraphRecordValue::Float(lower.midpoint(upper as f64))
69        }
70        (GraphRecordValue::Float(lower), Some(GraphRecordValue::Float(upper))) => {
71            GraphRecordValue::Float(lower.midpoint(upper))
72        }
73        (GraphRecordValue::DateTime(lower), Some(GraphRecordValue::DateTime(upper))) => {
74            let difference = upper.signed_duration_since(lower);
75            let half = difference.checked_div(2).expect("two is a nonzero divisor");
76
77            GraphRecordValue::DateTime(
78                lower
79                    .checked_add_signed(half)
80                    .expect("a datetime midpoint lies between its inputs"),
81            )
82        }
83        (GraphRecordValue::Duration(lower), Some(GraphRecordValue::Duration(upper))) => {
84            let lower = i128::from(lower.num_seconds()) * NANOSECONDS_PER_SECOND
85                + i128::from(lower.subsec_nanos());
86            let upper = i128::from(upper.num_seconds()) * NANOSECONDS_PER_SECOND
87                + i128::from(upper.subsec_nanos());
88            let midpoint = lower
89                .checked_add(upper)
90                .expect("two durations fit within i128 nanoseconds")
91                / 2;
92            let seconds = midpoint.div_euclid(NANOSECONDS_PER_SECOND);
93            let nanoseconds = midpoint.rem_euclid(NANOSECONDS_PER_SECOND);
94
95            GraphRecordValue::Duration(
96                TimeDelta::new(
97                    i64::try_from(seconds).expect("a duration midpoint fits in i64 seconds"),
98                    u32::try_from(nanoseconds).expect("subsecond nanoseconds fit in a u32"),
99                )
100                .expect("a duration midpoint lies between its inputs"),
101            )
102        }
103        _ => unreachable!("median values were validated and checked for comparability"),
104    }
105}
106
107impl ValueMedian for Scalar {
108    fn validate_median(label: &'static str, value: &Self::Value<'_>) -> QueryResult<()> {
109        validate_graphrecord_median_value(label, value)
110    }
111
112    fn find_incomparable_median_values<'a, 'b>(
113        values: impl Iterator<Item = &'a Self::Value<'b>>,
114    ) -> Option<(usize, usize)>
115    where
116        Self::Value<'b>: 'a,
117    {
118        incomparable_with_first(values)
119    }
120
121    fn median<'a>(
122        _label: &'static str,
123        lower: Self::Value<'a>,
124        upper: Option<Self::Value<'a>>,
125    ) -> QueryResult<Self::Value<'a>> {
126        Ok(median_graphrecord_value(lower, upper))
127    }
128}
129
130impl ValueScalar for Scalar {
131    fn into_scalar(_label: &'static str, value: Self::Value<'_>) -> QueryResult<GraphRecordValue> {
132        Ok(value)
133    }
134
135    fn from_scalar<'a>(_role: &Self::Value<'_>, value: GraphRecordValue) -> Self::Value<'a> {
136        value
137    }
138}
139
140impl ValueMode for Scalar {}
141
142impl ValueMode for Mask {}
143
144impl ValueMode for AttributeName {}
145
146impl ValueMedian for IndexValue<GraphRecordValue> {
147    fn validate_median(label: &'static str, value: &Self::Value<'_>) -> QueryResult<()> {
148        validate_graphrecord_median_value(label, value)
149    }
150
151    fn find_incomparable_median_values<'a, 'b>(
152        values: impl Iterator<Item = &'a Self::Value<'b>>,
153    ) -> Option<(usize, usize)>
154    where
155        Self::Value<'b>: 'a,
156    {
157        incomparable_with_first(values)
158    }
159
160    fn median<'a>(
161        _label: &'static str,
162        lower: Self::Value<'a>,
163        upper: Option<Self::Value<'a>>,
164    ) -> QueryResult<Self::Value<'a>> {
165        Ok(median_graphrecord_value(lower, upper))
166    }
167}
168
169impl ValueScalar for IndexValue<GraphRecordValue> {
170    fn into_scalar(_label: &'static str, value: Self::Value<'_>) -> QueryResult<GraphRecordValue> {
171        Ok(value)
172    }
173
174    fn from_scalar<'a>(_role: &Self::Value<'_>, value: GraphRecordValue) -> Self::Value<'a> {
175        value
176    }
177}
178
179impl<I: IndexDomain> ValueMode for IndexValue<I> {}