use crate::graph::{Id, Node, Relationship};
use serde_json::Value;
use std::cmp::Ordering;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SortDirection {
Asc,
Desc,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SortCriteria {
pub property: String,
pub direction: SortDirection,
}
impl SortCriteria {
pub fn asc(property: impl Into<String>) -> Self {
SortCriteria {
property: property.into(),
direction: SortDirection::Asc,
}
}
pub fn desc(property: impl Into<String>) -> Self {
SortCriteria {
property: property.into(),
direction: SortDirection::Desc,
}
}
}
pub struct Sorter;
impl Sorter {
pub fn sort_nodes(mut nodes: Vec<Node>, criteria: &[SortCriteria]) -> Vec<Node> {
nodes.sort_by(|a, b| Self::compare_nodes(a, b, criteria));
nodes
}
pub fn sort_relationships(
mut relationships: Vec<Relationship>,
criteria: &[SortCriteria],
) -> Vec<Relationship> {
relationships.sort_by(|a, b| Self::compare_relationships(a, b, criteria));
relationships
}
pub fn sort_node_ids_by_properties(
mut node_ids: Vec<Id>,
nodes: &[Node],
criteria: &[SortCriteria],
) -> Vec<Id> {
let node_map: std::collections::HashMap<Id, &Node> =
nodes.iter().map(|node| (node.id, node)).collect();
node_ids.sort_by(|&a, &b| {
let node_a = node_map.get(&a);
let node_b = node_map.get(&b);
match (node_a, node_b) {
(Some(a), Some(b)) => Self::compare_nodes(a, b, criteria),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
}
});
node_ids
}
fn compare_nodes(a: &Node, b: &Node, criteria: &[SortCriteria]) -> Ordering {
for criterion in criteria {
let value_a = a.get_property(&criterion.property);
let value_b = b.get_property(&criterion.property);
let ordering = Self::compare_values(value_a, value_b);
let final_ordering = match criterion.direction {
SortDirection::Asc => ordering,
SortDirection::Desc => ordering.reverse(),
};
if final_ordering != Ordering::Equal {
return final_ordering;
}
}
Ordering::Equal
}
fn compare_relationships(
a: &Relationship,
b: &Relationship,
criteria: &[SortCriteria],
) -> Ordering {
for criterion in criteria {
let value_a = a.get_property(&criterion.property);
let value_b = b.get_property(&criterion.property);
let ordering = Self::compare_values(value_a, value_b);
let final_ordering = match criterion.direction {
SortDirection::Asc => ordering,
SortDirection::Desc => ordering.reverse(),
};
if final_ordering != Ordering::Equal {
return final_ordering;
}
}
Ordering::Equal
}
fn compare_values(a: Option<&Value>, b: Option<&Value>) -> Ordering {
match (a, b) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Greater, (Some(_), None) => Ordering::Less,
(Some(val_a), Some(val_b)) => Self::compare_json_values(val_a, val_b),
}
}
fn compare_json_values(a: &Value, b: &Value) -> Ordering {
match (a, b) {
(Value::Number(a), Value::Number(b)) => a
.as_f64()
.partial_cmp(&b.as_f64())
.unwrap_or(Ordering::Equal),
(Value::String(a), Value::String(b)) => a.cmp(b),
(Value::Bool(a), Value::Bool(b)) => a.cmp(b),
(Value::Number(_), _) => Ordering::Less,
(_, Value::Number(_)) => Ordering::Greater,
(Value::String(_), Value::Bool(_)) => Ordering::Less,
(Value::Bool(_), Value::String(_)) => Ordering::Greater,
_ => a.to_string().cmp(&b.to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Pagination {
pub offset: usize,
pub limit: usize,
}
impl Pagination {
pub fn new(offset: usize, limit: usize) -> Self {
Pagination { offset, limit }
}
pub fn apply<T>(self, items: Vec<T>) -> Vec<T> {
items
.into_iter()
.skip(self.offset)
.take(self.limit)
.collect()
}
}
#[derive(Debug, Clone)]
pub struct SortedPage<T> {
pub items: Vec<T>,
pub total_count: usize,
pub pagination: Pagination,
}
impl<T> SortedPage<T> {
pub fn has_next_page(&self) -> bool {
self.pagination.offset + self.pagination.limit < self.total_count
}
pub fn has_previous_page(&self) -> bool {
self.pagination.offset > 0
}
pub fn next_page(&self) -> Option<Pagination> {
if self.has_next_page() {
Some(Pagination::new(
self.pagination.offset + self.pagination.limit,
self.pagination.limit,
))
} else {
None
}
}
pub fn previous_page(&self) -> Option<Pagination> {
if self.has_previous_page() {
let offset = self.pagination.offset.saturating_sub(self.pagination.limit);
Some(Pagination::new(offset, self.pagination.limit))
} else {
None
}
}
}
pub struct AdvancedSorter;
impl AdvancedSorter {
pub fn sort_nodes_paginated(
nodes: Vec<Node>,
criteria: &[SortCriteria],
pagination: Pagination,
) -> SortedPage<Node> {
let total_count = nodes.len();
let sorted_nodes = Sorter::sort_nodes(nodes, criteria);
let page_items = pagination.clone().apply(sorted_nodes);
SortedPage {
items: page_items,
total_count,
pagination,
}
}
pub fn sort_and_group_nodes(
nodes: Vec<Node>,
group_by: &str,
sort_criteria: &[SortCriteria],
) -> std::collections::BTreeMap<String, Vec<Node>> {
let mut groups: std::collections::BTreeMap<String, Vec<Node>> =
std::collections::BTreeMap::new();
for node in nodes {
let group_key = if let Some(value) = node.get_property(group_by) {
match value {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
_ => "other".to_string(),
}
} else {
"null".to_string()
};
groups.entry(group_key).or_default().push(node);
}
for group_nodes in groups.values_mut() {
*group_nodes = Sorter::sort_nodes(group_nodes.clone(), sort_criteria);
}
groups
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn create_test_nodes() -> Vec<Node> {
vec![
Node::new(
1,
[
("age".to_string(), json!(30)),
("name".to_string(), json!("Charlie")),
("score".to_string(), json!(85.5)),
]
.into(),
),
Node::new(
2,
[
("age".to_string(), json!(25)),
("name".to_string(), json!("Alice")),
("score".to_string(), json!(92.0)),
]
.into(),
),
Node::new(
3,
[
("age".to_string(), json!(35)),
("name".to_string(), json!("Bob")),
("score".to_string(), json!(78.5)),
]
.into(),
),
]
}
#[test]
fn test_sort_by_age_ascending() {
let nodes = create_test_nodes();
let criteria = vec![SortCriteria::asc("age")];
let sorted = Sorter::sort_nodes(nodes, &criteria);
assert_eq!(sorted[0].id, 2); assert_eq!(sorted[1].id, 1); assert_eq!(sorted[2].id, 3); }
#[test]
fn test_sort_by_name_descending() {
let nodes = create_test_nodes();
let criteria = vec![SortCriteria::desc("name")];
let sorted = Sorter::sort_nodes(nodes, &criteria);
assert_eq!(sorted[0].id, 1); assert_eq!(sorted[1].id, 3); assert_eq!(sorted[2].id, 2); }
#[test]
fn test_multi_criteria_sorting() {
let mut nodes = create_test_nodes();
nodes.push(Node::new(
4,
[
("age".to_string(), json!(25)),
("name".to_string(), json!("David")),
("score".to_string(), json!(88.0)),
]
.into(),
));
let criteria = vec![SortCriteria::asc("age"), SortCriteria::desc("name")];
let sorted = Sorter::sort_nodes(nodes, &criteria);
assert_eq!(sorted[0].id, 4); assert_eq!(sorted[1].id, 2); assert_eq!(sorted[2].id, 1); assert_eq!(sorted[3].id, 3); }
#[test]
fn test_pagination() {
let nodes = create_test_nodes();
let criteria = vec![SortCriteria::asc("age")];
let pagination = Pagination::new(1, 2);
let result = AdvancedSorter::sort_nodes_paginated(nodes, &criteria, pagination);
assert_eq!(result.items.len(), 2);
assert_eq!(result.total_count, 3);
assert_eq!(result.items[0].id, 1); assert_eq!(result.items[1].id, 3); assert!(result.has_previous_page());
assert!(!result.has_next_page());
}
#[test]
fn test_sort_and_group() {
let mut nodes = create_test_nodes();
nodes.push(Node::new(
4,
[
("age".to_string(), json!(25)),
("name".to_string(), json!("Eve")),
]
.into(),
));
let criteria = vec![SortCriteria::asc("name")];
let groups = AdvancedSorter::sort_and_group_nodes(nodes, "age", &criteria);
assert_eq!(groups.len(), 3);
let age_25_group = groups.get("25").unwrap();
assert_eq!(age_25_group.len(), 2);
assert_eq!(
age_25_group[0].get_property("name").unwrap(),
&json!("Alice")
);
assert_eq!(age_25_group[1].get_property("name").unwrap(), &json!("Eve"));
}
}