graphrecords_query/capabilities/
aggregation.rs1use 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> {}