use super::numeric::resolve_number;
use crate::graph::GraphStorage;
use crate::*;
#[derive(Default, Clone)]
struct StepMedian {}
impl fmt::Display for StepMedian {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "median()")
}
}
impl<'g, S: GraphStorage + 'g> GraphTraversalSource<'g, S> {
pub fn median(self) -> Self {
self.push_step(GraphStep::Barrier(
"median",
Box::new(StepMedian::default()),
))
}
}
impl __ {
pub fn median<'g, S: GraphStorage + 'g>() -> AnonymousTraversal<'g, S> {
AnonymousTraversal::new().median()
}
}
impl<'g, S: GraphStorage + 'g> AnonymousTraversal<'g, S> {
pub fn median(self) -> Self {
let step = GraphStep::Barrier("median", Box::new(StepMedian::default()));
self.push_step(step)
}
}
impl<'g, S: GraphStorage + 'g> GraphStepBarrier<'g, S> for StepMedian {
fn materializes(&self) -> bool {
true
}
#[inline]
fn process(
&self,
context: &mut TraverserContext<'_, S>,
source: Vec<Traverser<S>>,
) -> TraverserResult<Option<Traverser<S>>> {
let mut values: Vec<(f64, u64)> = Vec::with_capacity(source.len());
let mut total: u64 = 0;
for traverser in source.into_iter() {
let bulk = traverser.bulk;
if let Some((number, _)) = resolve_number(context, traverser)? {
total = total.checked_add(bulk).ok_or_else(|| {
Box::new(TraverserError::BulkOverflow {
step: "median".to_string(),
})
})?;
values.push((number.as_f64(), bulk));
}
}
if values.is_empty() {
return Ok(None);
}
values.sort_by(|a, b| a.0.total_cmp(&b.0));
let at = |index: u64| -> f64 {
let mut seen: u64 = 0;
for (value, bulk) in &values {
seen += bulk;
if index < seen {
return *value;
}
}
values.last().map_or(0.0, |(value, _)| *value)
};
let median = if total.is_multiple_of(2) {
(at(total / 2 - 1) + at(total / 2)) / 2_f64
} else {
at((total - 1) / 2)
};
Ok(Some(Traverser::start(
context,
TraverserValue::Computed(ElementProperty::Number(median.into()), None),
)))
}
fn clone_box(&self) -> Box<dyn GraphStepBarrier<'g, S> + 'g> {
Box::new(self.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_input_emits_nothing() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v(None).has_label("nope").values("age").median().to_list(),
rhai: [
r#"g.V().hasLabel("nope").values("age").median()"#,
r#"g.v().has_label("nope").values("age").median()"#,
],
assert: |res| assert!(res.is_empty()),
);
}
#[test]
fn v() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v(None).values("age").median().to_list(),
rhai: [
r#"g.V().values("age").median()"#,
r#"g.v().values("age").median()"#,
],
assert: |res| assert_eq!(res[0].as_f64(), Some(30.5_f64)),
);
}
#[test]
fn where_() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v(None).values("age").where_t(__::median()).count().to_list(),
rhai: [
r#"g.V().values("age").where(__.median()).count()"#,
r#"g.v().values("age").where(__.median()).count()"#,
],
assert: |res| assert_eq!(res[0].as_i64(), Some(4)),
);
}
#[test]
fn where_inner() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.v(None).where_t(__::values("age").median()).count().to_list(),
rhai: [
r#"g.V().where(__.values("age").median()).count()"#,
r#"g.v().where(__.values("age").median()).count()"#,
],
assert: |res| assert_eq!(res[0].as_i64(), Some(4)),
);
}
#[test]
fn e() {
test_dual!(
graph: GraphSource::tinkerpop_modern(),
rust: |g| g.e(None).values("weight").median().to_list(),
rhai: [
r#"g.E().values("weight").median()"#,
r#"g.e().values("weight").median()"#,
],
assert: |res| assert_eq!(res[0].as_f64(), Some(0.45_f64)),
);
}
#[test]
fn profile() {
let graph = GraphSource::tinkerpop_modern();
let g_lock = graph.read();
let g = g_lock.traversal();
let result = g.e(None).values("weight").median().profile().unwrap();
assert!(result.to_string().contains("median("));
}
}