use key_paths_core::KeyPaths;
use std::marker::PhantomData;
use std::time::SystemTime;
#[cfg(feature = "datetime")]
use chrono::{DateTime, TimeZone};
pub struct LazyQuery<'a, T: 'static, I>
where
I: Iterator<Item = &'a T>,
{
iter: I,
filter_groups: Vec<FilterGroup<'a, T>>,
_phantom: PhantomData<&'a T>,
}
enum FilterGroup<'a, T: 'static> {
And(Vec<Box<dyn Fn(&T) -> bool + 'a>>),
Or(Vec<Box<dyn Fn(&T) -> bool + 'a>>),
}
impl<'a, T: 'static> FilterGroup<'a, T> {
fn evaluate(&self, item: &T) -> bool {
match self {
FilterGroup::And(filters) => filters.iter().all(|f| f(item)),
FilterGroup::Or(filters) => filters.iter().any(|f| f(item)),
}
}
}
impl<'a, T: 'static> LazyQuery<'a, T, std::slice::Iter<'a, T>> {
pub fn new(data: &'a [T]) -> Self {
Self {
iter: data.iter(),
filter_groups: Vec::new(),
_phantom: PhantomData,
}
}
}
impl<'a, T: 'static, I> LazyQuery<'a, T, I>
where
I: Iterator<Item = &'a T>,
{
pub fn from_iter(iter: I) -> Self {
Self {
iter,
filter_groups: Vec::new(),
_phantom: PhantomData,
}
}
fn apply_filters(self) -> impl Iterator<Item = &'a T> + 'a
where
I: 'a,
{
let filter_groups = self.filter_groups;
let (and_groups, or_groups): (Vec<_>, Vec<_>) = filter_groups
.into_iter()
.partition(|group| matches!(group, FilterGroup::And(_)));
self.iter.filter(move |item| {
match (and_groups.is_empty(), or_groups.is_empty()) {
(false, true) => and_groups.iter().all(|group| group.evaluate(item)),
(true, false) => or_groups.iter().any(|group| group.evaluate(item)),
(false, false) => {
let all_and_pass = and_groups.iter().all(|group| group.evaluate(item));
let any_or_pass = or_groups.iter().any(|group| group.evaluate(item));
all_and_pass || any_or_pass
}
(true, true) => true,
}
})
}
}
impl<'a, T: 'static, I> LazyQuery<'a, T, I>
where
I: Iterator<Item = &'a T> + 'a,
{
pub fn where_<F, P>(mut self, path: KeyPaths<T, F>, predicate: P) -> Self
where
F: 'static,
P: Fn(&F) -> bool + 'a,
{
let filter = Box::new(move |item: &T| {
path.get(item).map_or(false, |val| predicate(val))
});
match self.filter_groups.last_mut() {
Some(FilterGroup::Or(filters)) => {
filters.push(filter);
}
Some(FilterGroup::And(filters)) => {
filters.push(filter);
}
None => {
self.filter_groups.push(FilterGroup::And(vec![filter]));
}
}
self
}
pub fn and<F, P>(mut self, path: KeyPaths<T, F>, predicate: P) -> Self
where
F: 'static,
P: Fn(&F) -> bool + 'a,
{
let filter = Box::new(move |item: &T| {
path.get(item).map_or(false, |val| predicate(val))
});
match self.filter_groups.last_mut() {
Some(FilterGroup::And(filters)) => {
filters.push(filter);
}
_ => {
self.filter_groups.push(FilterGroup::And(vec![filter]));
}
}
self
}
pub fn or<F, P>(mut self, path: KeyPaths<T, F>, predicate: P) -> Self
where
F: 'static,
P: Fn(&F) -> bool + 'a,
{
let filter = Box::new(move |item: &T| {
path.get(item).map_or(false, |val| predicate(val))
});
match self.filter_groups.last_mut() {
Some(FilterGroup::Or(filters)) => {
filters.push(filter);
}
_ => {
self.filter_groups.push(FilterGroup::Or(vec![filter]));
}
}
self
}
pub fn map_items<F, O>(self, f: F) -> impl Iterator<Item = O> + 'a
where
F: Fn(&'a T) -> O + 'a,
I: 'a,
{
self.apply_filters().map(f)
}
pub fn select_lazy<F>(self, path: KeyPaths<T, F>) -> impl Iterator<Item = F> + 'a
where
F: Clone + 'static,
I: 'a,
{
self.apply_filters().filter_map(move |item| path.get(item).cloned())
}
pub fn take_lazy(self, n: usize) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a>
where
I: 'a,
{
LazyQuery {
iter: self.iter.take(n),
filter_groups: self.filter_groups,
_phantom: PhantomData,
}
}
pub fn skip_lazy(self, n: usize) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a>
where
I: 'a,
{
LazyQuery {
iter: self.iter.skip(n),
filter_groups: self.filter_groups,
_phantom: PhantomData,
}
}
pub fn collect(self) -> Vec<&'a T>
where
I: 'a,
{
self.apply_filters().collect()
}
pub fn first(self) -> Option<&'a T>
where
I: 'a,
{
self.apply_filters().next()
}
pub fn count(self) -> usize
where
I: 'a,
{
self.apply_filters().count()
}
pub fn any(self) -> bool
where
I: 'a,
{
self.apply_filters().next().is_some()
}
pub fn for_each<F>(self, f: F)
where
F: FnMut(&'a T),
{
self.apply_filters().for_each(f)
}
pub fn fold<B, F>(self, init: B, f: F) -> B
where
F: FnMut(B, &'a T) -> B,
{
self.apply_filters().fold(init, f)
}
pub fn find<P>(self, predicate: P) -> Option<&'a T>
where
P: FnMut(&&'a T) -> bool,
I: 'a,
{
self.apply_filters().find(predicate)
}
pub fn all_match<P>(self, mut predicate: P) -> bool
where
P: FnMut(&'a T) -> bool,
I: 'a,
{
self.apply_filters().all(move |item| predicate(item))
}
pub fn all(self) -> Vec<&'a T>
where
I: 'a,
{
self.apply_filters().collect()
}
pub fn into_iter(self) -> I {
self.iter
}
}
impl<'a, T: 'static, I> LazyQuery<'a, T, I>
where
I: Iterator<Item = &'a T> + 'a,
{
pub fn sum_by<F>(self, path: KeyPaths<T, F>) -> F
where
F: Clone + std::ops::Add<Output = F> + Default + 'static,
I: 'a,
{
self.apply_filters()
.filter_map(move |item| path.get(item).cloned())
.fold(F::default(), |acc, val| acc + val)
}
pub fn avg_by(self, path: KeyPaths<T, f64>) -> Option<f64>
where
I: 'a,
{
let items: Vec<f64> = self
.apply_filters()
.filter_map(move |item| path.get(item).cloned())
.collect();
if items.is_empty() {
None
} else {
Some(items.iter().sum::<f64>() / items.len() as f64)
}
}
pub fn min_by<F>(self, path: KeyPaths<T, F>) -> Option<F>
where
F: Ord + Clone + 'static,
I: 'a,
{
self.apply_filters().filter_map(move |item| path.get(item).cloned()).min()
}
pub fn max_by<F>(self, path: KeyPaths<T, F>) -> Option<F>
where
F: Ord + Clone + 'static,
I: 'a,
{
self.apply_filters().filter_map(move |item| path.get(item).cloned()).max()
}
pub fn min_by_float(self, path: KeyPaths<T, f64>) -> Option<f64>
where
I: 'a,
{
self.apply_filters()
.filter_map(move |item| path.get(item).cloned())
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
}
pub fn max_by_float(self, path: KeyPaths<T, f64>) -> Option<f64>
where
I: 'a,
{
self.apply_filters()
.filter_map(move |item| path.get(item).cloned())
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
}
pub fn where_after_systemtime(self, path: KeyPaths<T, SystemTime>, reference: SystemTime) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a> {
self.where_(path, move |time| time > &reference)
}
pub fn where_before_systemtime(self, path: KeyPaths<T, SystemTime>, reference: SystemTime) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a> {
self.where_(path, move |time| time < &reference)
}
pub fn where_between_systemtime(
self,
path: KeyPaths<T, SystemTime>,
start: SystemTime,
end: SystemTime,
) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a> {
self.where_(path, move |time| time >= &start && time <= &end)
}
}
#[cfg(feature = "datetime")]
impl<'a, T: 'static, I> LazyQuery<'a, T, I>
where
I: Iterator<Item = &'a T> + 'a,
{
pub fn where_after<Tz>(self, path: KeyPaths<T, DateTime<Tz>>, reference: DateTime<Tz>) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a>
where
Tz: TimeZone + 'static,
Tz::Offset: std::fmt::Display,
{
self.where_(path, move |time| time > &reference)
}
pub fn where_before<Tz>(self, path: KeyPaths<T, DateTime<Tz>>, reference: DateTime<Tz>) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a>
where
Tz: TimeZone + 'static,
Tz::Offset: std::fmt::Display,
{
self.where_(path, move |time| time < &reference)
}
pub fn where_between<Tz>(
self,
path: KeyPaths<T, DateTime<Tz>>,
start: DateTime<Tz>,
end: DateTime<Tz>,
) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a>
where
Tz: TimeZone + 'static,
Tz::Offset: std::fmt::Display,
{
self.where_(path, move |time| time >= &start && time <= &end)
}
pub fn where_today<Tz>(self, path: KeyPaths<T, DateTime<Tz>>, now: DateTime<Tz>) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a>
where
Tz: TimeZone + 'static,
Tz::Offset: std::fmt::Display,
{
self.where_(path, move |time| {
time.date_naive() == now.date_naive()
})
}
pub fn where_year<Tz>(self, path: KeyPaths<T, DateTime<Tz>>, year: i32) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a>
where
Tz: TimeZone + 'static,
Tz::Offset: std::fmt::Display,
{
use chrono::Datelike;
self.where_(path, move |time| time.year() == year)
}
pub fn where_month<Tz>(self, path: KeyPaths<T, DateTime<Tz>>, month: u32) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a>
where
Tz: TimeZone + 'static,
Tz::Offset: std::fmt::Display,
{
use chrono::Datelike;
self.where_(path, move |time| time.month() == month)
}
pub fn where_day<Tz>(self, path: KeyPaths<T, DateTime<Tz>>, day: u32) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a>
where
Tz: TimeZone + 'static,
Tz::Offset: std::fmt::Display,
{
use chrono::Datelike;
self.where_(path, move |time| time.day() == day)
}
pub fn where_weekend<Tz>(self, path: KeyPaths<T, DateTime<Tz>>) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a>
where
Tz: TimeZone + 'static,
Tz::Offset: std::fmt::Display,
{
use chrono::Datelike;
self.where_(path, |time| {
let weekday = time.weekday().num_days_from_monday();
weekday >= 5
})
}
pub fn where_weekday<Tz>(self, path: KeyPaths<T, DateTime<Tz>>) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a>
where
Tz: TimeZone + 'static,
Tz::Offset: std::fmt::Display,
{
use chrono::Datelike;
self.where_(path, |time| {
let weekday = time.weekday().num_days_from_monday();
weekday < 5
})
}
pub fn where_business_hours<Tz>(self, path: KeyPaths<T, DateTime<Tz>>) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a>
where
Tz: TimeZone + 'static,
Tz::Offset: std::fmt::Display,
{
use chrono::Timelike;
self.where_(path, |time| {
let hour = time.hour();
hour >= 9 && hour < 17
})
}
}
impl<'a, T: 'static, I> LazyQuery<'a, T, I>
where
I: Iterator<Item = &'a T> + 'a,
{
pub fn min_timestamp(self, path: KeyPaths<T, i64>) -> Option<i64>
where
I: 'a,
{
self.apply_filters()
.filter_map(move |item| path.get(item).cloned())
.min()
}
pub fn max_timestamp(self, path: KeyPaths<T, i64>) -> Option<i64>
where
I: 'a,
{
self.apply_filters()
.filter_map(move |item| path.get(item).cloned())
.max()
}
pub fn avg_timestamp(self, path: KeyPaths<T, i64>) -> Option<i64>
where
I: 'a,
{
let items: Vec<i64> = self
.apply_filters()
.filter_map(move |item| path.get(item).cloned())
.collect();
if items.is_empty() {
None
} else {
Some(items.iter().sum::<i64>() / items.len() as i64)
}
}
pub fn sum_timestamp(self, path: KeyPaths<T, i64>) -> i64
where
I: 'a,
{
self.apply_filters()
.filter_map(move |item| path.get(item).cloned())
.sum()
}
pub fn count_timestamp(self, path: KeyPaths<T, i64>) -> usize
where
I: 'a,
{
self.apply_filters()
.filter(move |item| path.get(item).is_some())
.count()
}
pub fn where_after_timestamp(self, path: KeyPaths<T, i64>, reference: i64) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a> {
self.where_(path, move |timestamp| timestamp > &reference)
}
pub fn where_before_timestamp(self, path: KeyPaths<T, i64>, reference: i64) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a> {
self.where_(path, move |timestamp| timestamp < &reference)
}
pub fn where_between_timestamp(
self,
path: KeyPaths<T, i64>,
start: i64,
end: i64,
) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a> {
self.where_(path, move |timestamp| timestamp >= &start && timestamp <= &end)
}
pub fn where_last_days_timestamp(self, path: KeyPaths<T, i64>, days: i64) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a> {
let now = chrono::Utc::now().timestamp_millis();
let cutoff = now - (days * 24 * 60 * 60 * 1000); self.where_after_timestamp(path, cutoff)
}
pub fn where_next_days_timestamp(self, path: KeyPaths<T, i64>, days: i64) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a> {
let now = chrono::Utc::now().timestamp_millis();
let cutoff = now + (days * 24 * 60 * 60 * 1000); self.where_before_timestamp(path, cutoff)
}
pub fn where_last_hours_timestamp(self, path: KeyPaths<T, i64>, hours: i64) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a> {
let now = chrono::Utc::now().timestamp_millis();
let cutoff = now - (hours * 60 * 60 * 1000); self.where_after_timestamp(path, cutoff)
}
pub fn where_next_hours_timestamp(self, path: KeyPaths<T, i64>, hours: i64) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a> {
let now = chrono::Utc::now().timestamp_millis();
let cutoff = now + (hours * 60 * 60 * 1000); self.where_before_timestamp(path, cutoff)
}
pub fn where_last_minutes_timestamp(self, path: KeyPaths<T, i64>, minutes: i64) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a> {
let now = chrono::Utc::now().timestamp_millis();
let cutoff = now - (minutes * 60 * 1000); self.where_after_timestamp(path, cutoff)
}
pub fn where_next_minutes_timestamp(self, path: KeyPaths<T, i64>, minutes: i64) -> LazyQuery<'a, T, impl Iterator<Item = &'a T> + 'a> {
let now = chrono::Utc::now().timestamp_millis();
let cutoff = now + (minutes * 60 * 1000); self.where_before_timestamp(path, cutoff)
}
}
impl<'a, T: 'static, I> IntoIterator for LazyQuery<'a, T, I>
where
I: Iterator<Item = &'a T> + 'a,
{
type Item = &'a T;
type IntoIter = Box<dyn Iterator<Item = &'a T> + 'a>;
fn into_iter(self) -> Self::IntoIter {
Box::new(self.apply_filters())
}
}
#[cfg(test)]
mod tests {
use crate::ext::QueryableExt;
use crate::lazy::LazyQuery;
use key_paths_derive::Keypath;
#[derive(Debug, Clone, PartialEq, Keypath)]
struct Product {
id: u32,
name: String,
price: f64,
category: String,
stock: u32,
rating: f64,
}
fn create_test_products() -> Vec<Product> {
vec![
Product {
id: 1,
name: "Laptop".to_string(),
price: 999.99,
category: "Electronics".to_string(),
stock: 5,
rating: 4.5,
},
Product {
id: 2,
name: "Mouse".to_string(),
price: 29.99,
category: "Electronics".to_string(),
stock: 50,
rating: 4.0,
},
Product {
id: 3,
name: "Keyboard".to_string(),
price: 79.99,
category: "Electronics".to_string(),
stock: 30,
rating: 4.8,
},
Product {
id: 4,
name: "Monitor".to_string(),
price: 299.99,
category: "Electronics".to_string(),
stock: 12,
rating: 4.2,
},
Product {
id: 5,
name: "Desk Chair".to_string(),
price: 199.99,
category: "Furniture".to_string(),
stock: 8,
rating: 4.7,
},
Product {
id: 6,
name: "Premium Laptop".to_string(),
price: 1999.99,
category: "Electronics".to_string(),
stock: 3,
rating: 4.9,
},
]
}
#[test]
fn test_where_implicit_and() {
let products = create_test_products();
let results: Vec<_> = products
.lazy_query()
.where_(Product::price(), |&p| p < 100.0)
.where_(Product::stock(), |&s| s > 10)
.collect();
assert_eq!(results.len(), 2);
assert!(results.iter().any(|p| p.name == "Mouse"));
assert!(results.iter().any(|p| p.name == "Keyboard"));
}
#[test]
fn test_explicit_and() {
let products = create_test_products();
let results: Vec<_> = products
.lazy_query()
.where_(Product::price(), |&p| p < 100.0)
.and(Product::stock(), |&s| s > 10)
.collect();
assert_eq!(results.len(), 2);
assert!(results.iter().any(|p| p.name == "Mouse"));
assert!(results.iter().any(|p| p.name == "Keyboard"));
}
#[test]
fn test_or_operator() {
let products = create_test_products();
let results: Vec<_> = products
.lazy_query()
.where_(Product::price(), |&p| p < 50.0)
.or(Product::category(), |c| c == "Furniture")
.collect();
assert_eq!(results.len(), 2);
assert!(results.iter().any(|p| p.name == "Mouse"));
assert!(results.iter().any(|p| p.name == "Desk Chair"));
}
#[test]
fn test_complex_and_or_composition() {
let products = create_test_products();
let results: Vec<_> = products
.lazy_query()
.where_(Product::price(), |&p| p < 100.0)
.and(Product::stock(), |&s| s > 10)
.or(Product::category(), |c| c == "Furniture")
.collect();
assert_eq!(results.len(), 3);
assert!(results.iter().any(|p| p.name == "Mouse"));
assert!(results.iter().any(|p| p.name == "Keyboard"));
assert!(results.iter().any(|p| p.name == "Desk Chair"));
}
#[test]
fn test_multiple_and_conditions() {
let products = create_test_products();
let results: Vec<_> = products
.lazy_query()
.where_(Product::price(), |&p| p < 200.0)
.and(Product::stock(), |&s| s > 5)
.and(Product::rating(), |&r| r > 4.5)
.collect();
assert_eq!(results.len(), 2);
assert!(results.iter().any(|p| p.name == "Keyboard"));
assert!(results.iter().any(|p| p.name == "Desk Chair"));
}
#[test]
fn test_multiple_or_conditions() {
let products = create_test_products();
let results: Vec<_> = products
.lazy_query()
.where_(Product::price(), |&p| p > 500.0)
.or(Product::category(), |c| c == "Furniture")
.or(Product::rating(), |&r| r > 4.8)
.collect();
assert_eq!(results.len(), 3);
assert!(results.iter().any(|p| p.name == "Laptop"));
assert!(results.iter().any(|p| p.name == "Desk Chair"));
assert!(results.iter().any(|p| p.name == "Premium Laptop"));
}
#[test]
fn test_and_then_or_then_where() {
let products = create_test_products();
let results: Vec<_> = products
.lazy_query()
.where_(Product::price(), |&p| p < 100.0)
.and(Product::stock(), |&s| s > 10)
.or(Product::category(), |c| c == "Furniture")
.where_(Product::rating(), |&r| r > 4.0) .collect();
assert!(results.len() >= 3);
}
#[test]
fn test_empty_results() {
let products = create_test_products();
let results: Vec<_> = products
.lazy_query()
.where_(Product::price(), |&p| p < 0.0)
.collect();
assert_eq!(results.len(), 0);
}
#[test]
fn test_all_results() {
let products = create_test_products();
let results: Vec<_> = products
.lazy_query()
.where_(Product::price(), |&p| p > 0.0)
.collect();
assert_eq!(results.len(), products.len());
}
#[test]
fn test_or_with_no_previous_and() {
let products = create_test_products();
let results: Vec<_> = products
.lazy_query()
.or(Product::category(), |c| c == "Furniture")
.collect();
assert_eq!(results.len(), 1);
assert!(results[0].name == "Desk Chair");
}
#[test]
fn test_count_with_and_or() {
let products = create_test_products();
let count = products
.lazy_query()
.where_(Product::price(), |&p| p < 100.0)
.and(Product::stock(), |&s| s > 10)
.count();
assert_eq!(count, 2);
}
#[test]
fn test_first_with_and_or() {
let products = create_test_products();
let first = products
.lazy_query()
.where_(Product::price(), |&p| p < 100.0)
.and(Product::stock(), |&s| s > 10)
.first();
assert!(first.is_some());
assert!(first.unwrap().price < 100.0);
assert!(first.unwrap().stock > 10);
}
#[test]
fn test_any_with_and_or() {
let products = create_test_products();
let has_match = products
.lazy_query()
.where_(Product::price(), |&p| p < 50.0)
.or(Product::category(), |c| c == "Furniture")
.any();
assert!(has_match);
}
}