use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogicalOperator {
All,
Any,
Exactly(usize),
AtLeast(usize),
AtMost(usize),
}
impl LogicalOperator {
pub fn evaluate(&self, results: &[bool]) -> bool {
if results.is_empty() {
return match self {
LogicalOperator::All => true, LogicalOperator::Any => false, LogicalOperator::Exactly(n) => *n == 0,
LogicalOperator::AtLeast(n) => *n == 0,
LogicalOperator::AtMost(_) => true, };
}
let true_count = results.iter().filter(|&&x| x).count();
match self {
LogicalOperator::All => true_count == results.len(),
LogicalOperator::Any => true_count > 0,
LogicalOperator::Exactly(n) => true_count == *n,
LogicalOperator::AtLeast(n) => true_count >= *n,
LogicalOperator::AtMost(n) => true_count <= *n,
}
}
pub fn description(&self) -> String {
match self {
LogicalOperator::All => "all".to_string(),
LogicalOperator::Any => "any".to_string(),
LogicalOperator::Exactly(n) => format!("exactly {n}"),
LogicalOperator::AtLeast(n) => format!("at least {n}"),
LogicalOperator::AtMost(n) => format!("at most {n}"),
}
}
}
impl fmt::Display for LogicalOperator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.description())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ColumnSpec {
Single(String),
Multiple(Vec<String>),
}
impl ColumnSpec {
pub fn as_vec(&self) -> Vec<&str> {
match self {
ColumnSpec::Single(col) => vec![col.as_str()],
ColumnSpec::Multiple(cols) => cols.iter().map(|s| s.as_str()).collect(),
}
}
pub fn len(&self) -> usize {
match self {
ColumnSpec::Single(_) => 1,
ColumnSpec::Multiple(cols) => cols.len(),
}
}
pub fn is_empty(&self) -> bool {
match self {
ColumnSpec::Single(_) => false,
ColumnSpec::Multiple(cols) => cols.is_empty(),
}
}
pub fn is_single(&self) -> bool {
matches!(self, ColumnSpec::Single(_))
}
pub fn is_multiple(&self) -> bool {
matches!(self, ColumnSpec::Multiple(_))
}
pub fn to_multiple(self) -> Vec<String> {
match self {
ColumnSpec::Single(col) => vec![col],
ColumnSpec::Multiple(cols) => cols,
}
}
}
impl From<String> for ColumnSpec {
fn from(s: String) -> Self {
ColumnSpec::Single(s)
}
}
impl From<&str> for ColumnSpec {
fn from(s: &str) -> Self {
ColumnSpec::Single(s.to_string())
}
}
impl From<Vec<String>> for ColumnSpec {
fn from(v: Vec<String>) -> Self {
match v.len() {
1 => {
#[allow(clippy::expect_used)]
ColumnSpec::Single(
v.into_iter()
.next()
.expect("Vector with length 1 should have one element"),
)
}
_ => ColumnSpec::Multiple(v),
}
}
}
impl From<Vec<&str>> for ColumnSpec {
fn from(v: Vec<&str>) -> Self {
let strings: Vec<String> = v.into_iter().map(|s| s.to_string()).collect();
strings.into()
}
}
impl<const N: usize> From<[&str; N]> for ColumnSpec {
fn from(arr: [&str; N]) -> Self {
let vec: Vec<String> = arr.iter().map(|s| s.to_string()).collect();
vec.into()
}
}
pub trait ConstraintOptionsBuilder: Sized {
fn with_operator(self, operator: LogicalOperator) -> Self;
fn with_threshold(self, threshold: f64) -> Self;
fn with_option(self, name: &str, value: bool) -> Self;
}
#[derive(Debug, Clone)]
pub struct LogicalResult {
pub result: bool,
pub individual_results: Vec<(String, bool)>,
pub operator: LogicalOperator,
pub message: Option<String>,
}
impl LogicalResult {
pub fn new(
result: bool,
individual_results: Vec<(String, bool)>,
operator: LogicalOperator,
) -> Self {
Self {
result,
individual_results,
operator,
message: None,
}
}
pub fn with_message(mut self, message: String) -> Self {
self.message = Some(message);
self
}
pub fn passed_columns(&self) -> Vec<&str> {
self.individual_results
.iter()
.filter(|(_, passed)| *passed)
.map(|(col, _)| col.as_str())
.collect()
}
pub fn failed_columns(&self) -> Vec<&str> {
self.individual_results
.iter()
.filter(|(_, passed)| !*passed)
.map(|(col, _)| col.as_str())
.collect()
}
pub fn pass_rate(&self) -> f64 {
if self.individual_results.is_empty() {
return 0.0;
}
let passed = self.individual_results.iter().filter(|(_, p)| *p).count();
passed as f64 / self.individual_results.len() as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_logical_operator_all() {
let op = LogicalOperator::All;
assert!(op.evaluate(&[])); assert!(op.evaluate(&[true]));
assert!(!op.evaluate(&[false]));
assert!(op.evaluate(&[true, true, true]));
assert!(!op.evaluate(&[true, false, true]));
}
#[test]
fn test_logical_operator_any() {
let op = LogicalOperator::Any;
assert!(!op.evaluate(&[])); assert!(op.evaluate(&[true]));
assert!(!op.evaluate(&[false]));
assert!(op.evaluate(&[true, false, false]));
assert!(!op.evaluate(&[false, false, false]));
}
#[test]
fn test_logical_operator_exactly() {
let op = LogicalOperator::Exactly(2);
assert!(!op.evaluate(&[]));
assert!(!op.evaluate(&[true]));
assert!(!op.evaluate(&[true, true, true]));
assert!(op.evaluate(&[true, true, false]));
assert!(op.evaluate(&[true, false, true]));
let op_zero = LogicalOperator::Exactly(0);
assert!(op_zero.evaluate(&[]));
assert!(!op_zero.evaluate(&[true]));
assert!(op_zero.evaluate(&[false, false]));
}
#[test]
fn test_logical_operator_at_least() {
let op = LogicalOperator::AtLeast(2);
assert!(!op.evaluate(&[]));
assert!(!op.evaluate(&[true]));
assert!(op.evaluate(&[true, true]));
assert!(op.evaluate(&[true, true, true]));
assert!(op.evaluate(&[true, true, false]));
assert!(!op.evaluate(&[true, false, false]));
}
#[test]
fn test_logical_operator_at_most() {
let op = LogicalOperator::AtMost(2);
assert!(op.evaluate(&[]));
assert!(op.evaluate(&[true]));
assert!(op.evaluate(&[true, true]));
assert!(!op.evaluate(&[true, true, true]));
assert!(op.evaluate(&[true, false, false]));
assert!(op.evaluate(&[false, false, false]));
}
#[test]
fn test_column_spec_single() {
let spec = ColumnSpec::Single("user_id".to_string());
assert_eq!(spec.len(), 1);
assert!(!spec.is_empty());
assert!(spec.is_single());
assert!(!spec.is_multiple());
assert_eq!(spec.as_vec(), vec!["user_id"]);
}
#[test]
fn test_column_spec_multiple() {
let spec = ColumnSpec::Multiple(vec!["email".to_string(), "phone".to_string()]);
assert_eq!(spec.len(), 2);
assert!(!spec.is_empty());
assert!(!spec.is_single());
assert!(spec.is_multiple());
assert_eq!(spec.as_vec(), vec!["email", "phone"]);
}
#[test]
fn test_column_spec_conversions() {
let spec = ColumnSpec::from("test");
assert!(matches!(spec, ColumnSpec::Single(s) if s == "test"));
let spec = ColumnSpec::from("test".to_string());
assert!(matches!(spec, ColumnSpec::Single(s) if s == "test"));
let spec = ColumnSpec::from(vec!["test"]);
assert!(matches!(spec, ColumnSpec::Single(s) if s == "test"));
let spec = ColumnSpec::from(vec!["test1", "test2"]);
assert!(matches!(spec, ColumnSpec::Multiple(v) if v.len() == 2));
let spec = ColumnSpec::from(["a", "b", "c"]);
assert!(matches!(spec, ColumnSpec::Multiple(v) if v.len() == 3));
}
#[test]
fn test_logical_result() {
let individual = vec![
("col1".to_string(), true),
("col2".to_string(), false),
("col3".to_string(), true),
];
let result = LogicalResult::new(true, individual, LogicalOperator::AtLeast(2));
assert_eq!(result.passed_columns(), vec!["col1", "col3"]);
assert_eq!(result.failed_columns(), vec!["col2"]);
assert_eq!(result.pass_rate(), 2.0 / 3.0);
}
}