use crate::slice::{SortedDedupedSliceComparer, partition_dedup};
use itertools::Itertools;
use std::borrow::Borrow;
#[derive(Clone, Copy, Debug)]
pub struct OrFilter<L> {
inner: L,
}
impl<L: FilterStorage> OrFilter<L> {
pub const fn all() -> Self {
Self { inner: L::EMPTY }
}
pub fn new_unchecked(sorted_deduped: L) -> Self {
debug_assert!(
!sorted_deduped.as_ref().is_empty(),
"`sorted` cannot be empty"
);
debug_assert!(
sorted_deduped.as_ref().is_sorted(),
"`sorted` must be sorted"
);
debug_assert!(
!sorted_deduped
.as_ref()
.iter()
.tuple_windows()
.any(|(a, b)| a == b),
"`sorted` must not contain duplicates",
);
Self {
inner: sorted_deduped,
}
}
pub fn as_slice_filter(&self) -> OrFilter<&[L::Item]> {
OrFilter {
inner: self.inner.as_ref(),
}
}
pub fn to_vec_filter(&self) -> OrFilter<Vec<L::Item>>
where
L::Item: Clone,
{
OrFilter {
inner: self.inner.as_ref().to_vec(),
}
}
pub fn is_all(&self) -> bool {
self.inner.as_ref().is_empty()
}
pub fn items(&self) -> Items<&'_ [L::Item]> {
if self.is_all() {
Items::All
} else {
Items::Some(self.inner.as_ref())
}
}
pub fn contains<U>(&self, item: &U) -> bool
where
U: Ord + ?Sized,
L::Item: Borrow<U>,
{
if self.is_all() {
return true;
}
self.inner
.as_ref()
.binary_search_by_key(&item, |item| item.borrow())
.is_ok()
}
pub fn into_list(self) -> Items<L> {
if self.inner.as_ref().is_empty() {
Items::All
} else {
Items::Some(self.inner)
}
}
pub fn compare<'a, I, B>(&'a self, iter: Option<I>) -> Comparer<'a, L::Item, I>
where
I: IntoIterator<Item = &'a B>,
B: Ord + ?Sized + 'a,
L::Item: Borrow<B>,
{
Comparer(match (iter, self.is_all()) {
(Some(iter), false) => ComparerInner::SliceComparer(SortedDedupedSliceComparer::new(
self.inner.as_ref(),
iter,
)),
(Some(_), true) => ComparerInner::Immediate(false),
(None, is_all) => ComparerInner::Immediate(is_all),
})
}
}
impl<L: FilterStorageMut> OrFilter<L> {
pub fn from_list<M>(list: M) -> Option<Self>
where
M: Into<Option<L>>,
{
let Some(mut list) = list.into() else {
return Some(Self::all());
};
if list.as_ref().is_empty() {
return None;
}
list.as_mut().sort_unstable();
Some(Self::new_unchecked(list.dedup()))
}
}
impl<L: FilterStorage> Default for OrFilter<L> {
fn default() -> Self {
Self::all()
}
}
impl<L, M> PartialEq<OrFilter<M>> for OrFilter<L>
where
L: FilterStorage,
M: FilterStorage,
L::Item: PartialEq<M::Item>,
{
fn eq(&self, other: &OrFilter<M>) -> bool {
self.inner.as_ref() == other.inner.as_ref()
}
}
impl<L> Eq for OrFilter<L>
where
L: FilterStorage,
L::Item: Eq,
{
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Items<L> {
All,
Some(L),
}
pub trait FilterStorage: Ord + AsRef<[Self::Item]> + sealed::Sealed {
const EMPTY: Self;
type Item: Ord;
}
pub trait FilterStorageMut: FilterStorage + AsMut<[Self::Item]> {
fn dedup(self) -> Self;
}
impl<T: Ord> FilterStorage for Vec<T> {
const EMPTY: Self = Self::new();
type Item = T;
}
impl<T: Ord> FilterStorageMut for Vec<T> {
fn dedup(mut self) -> Self {
Vec::dedup(&mut self);
self
}
}
impl<T: Ord> FilterStorage for &'_ [T] {
const EMPTY: Self = &[];
type Item = T;
}
impl<T: Ord> FilterStorage for &'_ mut [T] {
const EMPTY: Self = empty_mut_slice();
type Item = T;
}
const fn empty_mut_slice<'a, T>() -> &'a mut [T] {
&mut []
}
impl<T: Ord> FilterStorageMut for &'_ mut [T] {
fn dedup(self) -> Self {
partition_dedup(self).0
}
}
pub struct Comparer<'a, T, I>(ComparerInner<'a, T, I>);
enum ComparerInner<'a, T, I> {
Immediate(bool),
SliceComparer(SortedDedupedSliceComparer<'a, T, I>),
}
impl<'a, T, I, B> Comparer<'a, T, I>
where
I: IntoIterator<Item = &'a B>,
B: Ord + ?Sized + 'a,
T: Borrow<B>,
{
pub fn eq<const STACK_SIZE: usize>(self) -> bool {
match self.0 {
ComparerInner::Immediate(b) => b,
ComparerInner::SliceComparer(cmp) => cmp.eq::<STACK_SIZE>(),
}
}
}
mod sealed {
pub trait Sealed {}
impl<T> Sealed for Vec<T> {}
impl<T> Sealed for &[T] {}
impl<T> Sealed for &mut [T] {}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use rstest::rstest;
use std::collections::BTreeSet;
fn filter(items: &[&str]) -> OrFilter<Vec<String>> {
OrFilter::from_list(items.iter().copied().map(str::to_owned).collect::<Vec<_>>())
.expect("`items` must not be empty")
}
#[test]
fn all_filter_has_no_items() {
let all = OrFilter::<Vec<String>>::all();
assert!(all.is_all());
assert_eq!(all.items(), Items::All);
assert_eq!(all.into_list(), Items::All);
}
#[test]
fn all_filter_contains_everything() {
let all = OrFilter::<Vec<String>>::all();
assert!(all.contains("bash"));
assert!(all.contains(""));
assert!(all.contains("anything at all"));
}
#[rstest]
#[case(&["bash"], "bash", true)]
#[case(&["bash"], "zsh", false)]
#[case(&["", "bash"], "", true)]
#[case(&["", "bash"], "zsh", false)]
#[case(&["bash", "fish", "zsh"], "fish", true)]
#[case(&["bash", "fish", "zsh"], "nu", false)]
fn contains_only_listed_items(
#[case] items: &[&str],
#[case] probe: &str,
#[case] expected: bool,
) {
assert_eq!(filter(items).contains(probe), expected);
}
#[test]
fn from_list_sorts_and_dedupes() {
let expected = ["", "bash", "zsh"].map(str::to_owned);
assert_eq!(
filter(&["zsh", "bash", "zsh", ""]).items(),
Items::Some(expected.as_slice())
);
}
#[rstest]
#[case(&["zsh", "bash", "zsh", ""], &["", "bash", "zsh"])]
#[case(&["a", "a", "a"], &["a"])]
#[case(&["b", "a", "a"], &["a", "b"])]
#[case(&["a", "b", "b"], &["a", "b"])]
#[case(&["a"], &["a"])]
fn from_list_of_a_mut_slice_sorts_and_dedupes(
#[case] items: &[&str],
#[case] expected: &[&str],
) {
let mut items = items.to_vec();
let filter = OrFilter::from_list(items.as_mut_slice()).expect("`items` must not be empty");
assert_eq!(filter.items(), Items::Some(expected));
}
#[test]
fn from_list_rejects_an_empty_list() {
assert_eq!(OrFilter::<Vec<String>>::from_list(Vec::new()), None);
}
#[test]
fn from_list_of_none_is_an_all_filter() {
let filter = OrFilter::<Vec<String>>::from_list(None::<Vec<String>>)
.expect("`None` yields an \"all\" filter");
assert!(filter.is_all());
}
#[test]
fn into_list_returns_the_sorted_items() {
assert_eq!(
filter(&["zsh", "bash"]).into_list(),
Items::Some(vec!["bash".to_owned(), "zsh".to_owned()]),
);
}
#[rstest]
#[case(&["bash"], Some(&["bash"][..]), true)]
#[case(&["bash"], Some(&["bash", "bash"][..]), true)]
#[case(&["bash", "zsh"], Some(&["zsh", "bash"][..]), true)]
#[case(&["bash", "zsh"], Some(&["bash"][..]), false)]
#[case(&["bash"], Some(&["bash", "zsh"][..]), false)]
#[case(&["bash"], Some(&[][..]), false)]
#[case(&["bash"], None, false)]
fn compare_with_a_non_all_filter(
#[case] items: &[&str],
#[case] other: Option<&[&str]>,
#[case] expected: bool,
) {
let f = filter(items);
let comparer = f.compare(other.map(|o| o.iter().copied()));
assert_eq!(comparer.eq::<4>(), expected);
}
#[rstest]
#[case(None, true)]
#[case(Some(&[][..]), false)]
#[case(Some(&["bash"][..]), false)]
fn compare_with_an_all_filter(#[case] other: Option<&[&str]>, #[case] expected: bool) {
let all = OrFilter::<Vec<String>>::all();
let comparer = all.compare(other.map(|o| o.iter().copied()));
assert_eq!(comparer.eq::<4>(), expected);
}
fn any_item() -> impl Strategy<Value = String> {
"[a-c]{0,2}"
}
fn any_items() -> impl Strategy<Value = Vec<String>> {
prop::collection::vec(any_item(), 0..8)
}
fn any_nonempty_items() -> impl Strategy<Value = Vec<String>> {
prop::collection::vec(any_item(), 1..8)
}
fn sort_dedup(items: &[String]) -> Vec<String> {
items
.iter()
.cloned()
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
proptest! {
#[test]
fn from_list_sorts_dedupes_and_rejects_empty_lists(items in any_items()) {
let expected = sort_dedup(&items);
match OrFilter::from_list(items.clone()) {
None => prop_assert!(items.is_empty()),
Some(filter) => {
prop_assert!(!items.is_empty());
prop_assert!(!filter.is_all());
prop_assert_eq!(filter.items(), Items::Some(expected.as_slice()));
}
}
}
#[test]
fn mut_slice_storage_agrees_with_vec_storage(items in any_nonempty_items()) {
let mut slice_items = items.clone();
let from_slice = OrFilter::from_list(slice_items.as_mut_slice())
.expect("`items` is nonempty");
let from_vec = OrFilter::from_list(items).expect("`items` is nonempty");
prop_assert_eq!(from_slice.items(), from_vec.items());
}
#[test]
fn contains_agrees_with_the_item_set(items in any_nonempty_items(), probe in any_item()) {
let expected = items.contains(&probe);
let filter = OrFilter::from_list(items).expect("`items` is nonempty");
prop_assert_eq!(filter.contains(probe.as_str()), expected);
}
#[test]
fn an_all_filter_contains_everything(probe in any_item()) {
prop_assert!(OrFilter::<Vec<String>>::all().contains(probe.as_str()));
}
#[test]
fn slice_and_vec_views_preserve_the_items(items in any_nonempty_items()) {
let filter = OrFilter::from_list(items).expect("`items` is nonempty");
let vec_filter = filter.to_vec_filter();
let slice_filter = filter.as_slice_filter();
prop_assert_eq!(slice_filter.items(), filter.items());
prop_assert_eq!(vec_filter.items(), filter.items());
prop_assert!(vec_filter == filter);
}
#[test]
fn into_list_returns_the_items(items in any_nonempty_items()) {
let expected = sort_dedup(&items);
let filter = OrFilter::from_list(items).expect("`items` is nonempty");
prop_assert_eq!(filter.into_list(), Items::Some(expected));
}
#[test]
fn equality_is_set_equality(a in any_nonempty_items(), b in any_nonempty_items()) {
let expected = sort_dedup(&a) == sort_dedup(&b);
let a = OrFilter::from_list(a).expect("`a` is nonempty");
let b = OrFilter::from_list(b).expect("`b` is nonempty");
prop_assert_eq!(a == b, expected);
}
#[test]
fn compare_matches_set_equality(items in any_nonempty_items(), other in any_items()) {
let expected = sort_dedup(&items) == sort_dedup(&other);
let filter = OrFilter::from_list(items).expect("`items` is nonempty");
prop_assert_eq!(filter.compare(Some(other.iter().map(String::as_str))).eq::<4>(), expected);
prop_assert_eq!(filter.compare(Some(other.iter().map(String::as_str))).eq::<0>(), expected);
prop_assert_eq!(filter.compare(Some(other.iter().map(String::as_str))).eq::<64>(), expected);
let reversed = other.iter().rev().map(String::as_str);
prop_assert_eq!(filter.compare(Some(reversed)).eq::<4>(), expected);
let duplicated = other.iter().chain(other.iter()).map(String::as_str);
prop_assert_eq!(filter.compare(Some(duplicated)).eq::<4>(), expected);
prop_assert!(!filter.compare(None::<std::iter::Empty<&str>>).eq::<4>());
}
#[test]
fn an_all_filter_compares_equal_only_to_none(other in any_items()) {
let all = OrFilter::<Vec<String>>::all();
prop_assert!(all.compare(None::<std::iter::Empty<&str>>).eq::<4>());
prop_assert!(!all.compare(Some(other.iter().map(String::as_str))).eq::<4>());
prop_assert!(!all.compare(Some(other.iter().map(String::as_str))).eq::<0>());
prop_assert!(!all.compare(Some(other.iter().map(String::as_str))).eq::<64>());
}
}
}