criterium 3.1.4

Lightweigt dynamic database queries for rusqlite.
Documentation
// SPDX-FileCopyrightText: 2026 Slatian
//
// SPDX-License-Identifier: LGPL-3.0-only

//! Implements DirectMatch for the TagCriterium, doesn’t export any functions

use crate::direct_match::DirectMatchResult;
use crate::direct_match::DirectMatchResultKind;
use crate::tag::TagCriterium;
use crate::DirectMatch;

impl<T, D> DirectMatch<[D]> for TagCriterium<T>
where
	T: DirectMatch<D>,
{
	type Output = T::Output;

	fn criterium_match(&self, data: &[D]) -> Self::Output {
		match self {
			Self::Has(chain) => {
				// Implement a logical OR as a match with any of the elements is okay
				let mut output = Self::Output::new(false);

				for d in data {
					output = output.or(chain.criterium_match(d));
					match output.kind() {
						DirectMatchResultKind::True => {
							return output;
						}
						DirectMatchResultKind::Error => {
							return output;
						}
						_ => { /* do nothing*/ }
					}
				}
				return output;
			}
			Self::HasNot(chain) => {
				// Implement a logical NOR as a match on any element means this has failed
				let mut output = Self::Output::new(true);

				for d in data {
					output = output.and(chain.criterium_match(d).invert());
					match output.kind() {
						DirectMatchResultKind::False => {
							return output;
						}
						DirectMatchResultKind::Error => {
							return output;
						}
						_ => { /* do nothing*/ }
					}
				}
				return output;
			}
			Self::HasNOf { n, chain } => {
				let mut count: i64 = 0;
				let mut count_unknown: i64 = 0;
				let mut unknown_output: Option<Self::Output> = None;
				for d in data {
					let out = chain.criterium_match(d);
					match out.kind() {
						DirectMatchResultKind::True => count += 1,
						DirectMatchResultKind::False => { /* noop */ }
						DirectMatchResultKind::Unknown => {
							if unknown_output.is_none() {
								unknown_output = Some(out)
							};
							count_unknown += 1;
						}
						DirectMatchResultKind::Error => return out,
					}
				}
				if let Some(unknown_output) = unknown_output {
					// Use a range map if unkown output is possible
					return n
						.criterium_match(&(count..=count + count_unknown))
						.map(Self::Output::new)
						.unwrap_or(unknown_output);
				} else {
					// Directly match if no unknowns were involved
					return Self::Output::new(n.criterium_match(&count));
				}
			}
		}
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	use crate::CriteriumChain;
	use crate::NumberCriterium;
	use crate::StringCriterium;

	#[test]
	fn test_tag_criterium_has() {
		let test_vec = vec!["foo", "bar", "baz"];

		assert!(
			TagCriterium::Has(CriteriumChain::<StringCriterium>::MatchAlways)
				.criterium_match(&test_vec)
		);
		assert!(
			!TagCriterium::Has(CriteriumChain::<StringCriterium>::MatchNever)
				.criterium_match(&test_vec)
		);
		assert!(
			TagCriterium::Has(CriteriumChain::Match(StringCriterium::Equals(
				"foo".to_string()
			)))
			.criterium_match(&test_vec)
		);
		assert!(
			!TagCriterium::Has(CriteriumChain::Match(StringCriterium::Equals(
				"blubb".to_string()
			)))
			.criterium_match(&test_vec)
		);

		// An empty vec should not match a has as it doesn't have any entries
		assert!(
			!TagCriterium::Has(CriteriumChain::<StringCriterium>::MatchAlways)
				.criterium_match(&Vec::<String>::new())
		);
	}

	#[test]
	fn test_tag_criterium_has_not() {
		let test_vec = vec!["foo", "bar", "baz"];

		assert!(
			!TagCriterium::HasNot(CriteriumChain::<StringCriterium>::MatchAlways)
				.criterium_match(&test_vec)
		);
		assert!(
			TagCriterium::HasNot(CriteriumChain::<StringCriterium>::MatchNever)
				.criterium_match(&test_vec)
		);
		assert!(
			!TagCriterium::HasNot(CriteriumChain::Match(StringCriterium::Equals(
				"foo".to_string()
			)))
			.criterium_match(&test_vec)
		);
		assert!(
			TagCriterium::HasNot(CriteriumChain::Match(StringCriterium::Equals(
				"blubb".to_string()
			)))
			.criterium_match(&test_vec)
		);

		// An empty vec should not match a has as it doesn't have any entries
		assert!(
			TagCriterium::HasNot(CriteriumChain::<StringCriterium>::MatchAlways)
				.criterium_match(&Vec::<String>::new())
		);
	}

	#[test]
	fn test_tag_criterium_has_n_of() {
		let test_vec = vec!["foo", "bar", "baz"];

		assert!(TagCriterium::HasNOf {
			chain: CriteriumChain::<StringCriterium>::MatchAlways,
			n: NumberCriterium::Equals(3)
		}
		.criterium_match(&test_vec));
		assert!(!TagCriterium::HasNOf {
			chain: CriteriumChain::<StringCriterium>::MatchAlways,
			n: NumberCriterium::Equals(1)
		}
		.criterium_match(&test_vec));
		assert!(TagCriterium::HasNOf {
			chain: CriteriumChain::<StringCriterium>::MatchNever,
			n: NumberCriterium::Equals(0)
		}
		.criterium_match(&test_vec));
		assert!(
			!TagCriterium::HasNot(CriteriumChain::Match(StringCriterium::Equals(
				"foo".to_string()
			)))
			.criterium_match(&test_vec)
		);
		assert!(TagCriterium::HasNOf {
			chain: CriteriumChain::<StringCriterium>::Match("foo".into()),
			n: NumberCriterium::Equals(1)
		}
		.criterium_match(&test_vec));
		assert!(TagCriterium::HasNOf {
			chain: CriteriumChain::Match(StringCriterium::HasPrefix("ba".to_string())),
			n: NumberCriterium::Equals(2)
		}
		.criterium_match(&test_vec));

		// An empty vec should not match a has as it doesn't have any entries
		assert!(TagCriterium::HasNOf {
			chain: CriteriumChain::<StringCriterium>::MatchAlways,
			n: NumberCriterium::Equals(0)
		}
		.criterium_match(&Vec::<String>::new()));
	}
}