use super::registry::{NamedPredicate, PredicateRegistry};
use serde_json::Value;
use std::sync::Arc;
pub fn register_amount_predicates(registry: &mut PredicateRegistry) {
registry.register(Arc::new(IsNonNegativePredicate));
registry.register(Arc::new(IsPositivePredicate));
registry.register(Arc::new(IsNegativePredicate));
registry.register(Arc::new(IsNonPositivePredicate));
registry.register(Arc::new(IsPercentagePredicate));
registry.register(Arc::new(IsMoneyFormatPredicate));
registry.register(Arc::new(IsMoneyNoSymbolPredicate));
registry.register(Arc::new(IsMultipleOfPredicate));
registry.register(Arc::new(LessThanOrEqualPredicate));
registry.register(Arc::new(GreaterThanOrEqualPredicate));
registry.register(Arc::new(FormatDecimal2Predicate));
registry.register(Arc::new(IsDecimalPlacesPredicate));
}
struct IsNonNegativePredicate;
impl NamedPredicate for IsNonNegativePredicate {
fn name(&self) -> &str {
"is_non_negative"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
match value {
Value::Number(n) => n.as_f64().is_some_and(|f| f >= 0.0),
Value::String(s) => {
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_digit() || *c == '.' || *c == '-')
.collect();
cleaned.parse::<f64>().is_ok_and(|f| f >= 0.0)
}
_ => false,
}
}
}
struct IsPositivePredicate;
impl NamedPredicate for IsPositivePredicate {
fn name(&self) -> &str {
"is_positive"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
match value {
Value::Number(n) => n.as_f64().is_some_and(|f| f > 0.0),
Value::String(s) => {
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_digit() || *c == '.' || *c == '-')
.collect();
cleaned.parse::<f64>().is_ok_and(|f| f > 0.0)
}
_ => false,
}
}
}
struct IsNegativePredicate;
impl NamedPredicate for IsNegativePredicate {
fn name(&self) -> &str {
"is_negative"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
extract_number(value).is_some_and(|f| f < 0.0)
}
}
struct IsNonPositivePredicate;
impl NamedPredicate for IsNonPositivePredicate {
fn name(&self) -> &str {
"is_non_positive"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
extract_number(value).is_some_and(|f| f <= 0.0)
}
}
struct IsPercentagePredicate;
impl NamedPredicate for IsPercentagePredicate {
fn name(&self) -> &str {
"is_percentage"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let n = match value {
Value::Number(n) => n.as_f64(),
Value::String(s) => {
let cleaned: String = s
.trim()
.trim_end_matches('%')
.chars()
.filter(|c| c.is_ascii_digit() || *c == '.' || *c == '-')
.collect();
cleaned.parse::<f64>().ok()
}
_ => return false,
};
let n = match n {
Some(n) => n,
None => return false,
};
let format = args
.get("format")
.and_then(|v| v.as_str())
.unwrap_or("percent");
let allow_over_100 = args
.get("allow_over_100")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let (min, max) = match format {
"decimal" => (0.0, if allow_over_100 { f64::MAX } else { 1.0 }),
_ => (0.0, if allow_over_100 { f64::MAX } else { 100.0 }),
};
n >= min && n <= max
}
}
struct IsMoneyFormatPredicate;
impl NamedPredicate for IsMoneyFormatPredicate {
fn name(&self) -> &str {
"is_money_format"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
if s.is_empty() {
return false;
}
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_digit() || *c == '.' || *c == '-')
.collect();
if cleaned.is_empty() {
return false;
}
let parsed = match cleaned.parse::<f64>() {
Ok(n) => n,
Err(_) => return false,
};
let max_decimals = args
.get("max_decimals")
.and_then(|v| v.as_u64())
.unwrap_or(2) as usize;
if let Some(dot_pos) = cleaned.find('.') {
let decimals = cleaned.len() - dot_pos - 1;
if decimals > max_decimals {
return false;
}
}
let allow_negative = args
.get("allow_negative")
.and_then(|v| v.as_bool())
.unwrap_or(true);
if !allow_negative && parsed < 0.0 {
return false;
}
true
}
}
struct IsMoneyNoSymbolPredicate;
impl NamedPredicate for IsMoneyNoSymbolPredicate {
fn name(&self) -> &str {
"is_money_no_symbol"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return false,
};
let bytes = s.as_bytes();
if bytes.is_empty() {
return false;
}
let max_decimals = args
.get("max_decimals")
.and_then(|v| v.as_u64())
.unwrap_or(2) as usize;
let allow_negative = args
.get("allow_negative")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let mut i = 0;
if bytes[i] == b'-' {
if !allow_negative {
return false;
}
i += 1;
if i >= bytes.len() {
return false;
}
}
if !bytes[i].is_ascii_digit() {
return false;
}
let mut digits_since_comma = 0usize;
let mut has_comma = false;
let mut int_digits = 0usize;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_digit() {
digits_since_comma += 1;
int_digits += 1;
i += 1;
} else if b == b',' {
if has_comma && digits_since_comma != 3 {
return false;
}
if !has_comma && int_digits > 3 {
return false;
}
has_comma = true;
digits_since_comma = 0;
i += 1;
} else if b == b'.' {
break;
} else {
return false;
}
}
if has_comma && digits_since_comma != 3 {
return false;
}
if int_digits == 0 {
return false;
}
if i < bytes.len() && bytes[i] == b'.' {
i += 1;
let dec_start = i;
while i < bytes.len() {
if !bytes[i].is_ascii_digit() {
return false;
}
i += 1;
}
let decimals = i - dec_start;
if decimals == 0 || decimals > max_decimals {
return false;
}
}
i == bytes.len()
}
}
struct IsMultipleOfPredicate;
impl NamedPredicate for IsMultipleOfPredicate {
fn name(&self) -> &str {
"is_multiple_of"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let n = match extract_number(value) {
Some(v) => v,
None => return false,
};
let step = match args.get("value").and_then(extract_number_from_value) {
Some(v) if v != 0.0 => v,
_ => return false,
};
let ratio = n / step;
let nearest = ratio.round();
(ratio - nearest).abs() < 1e-9
}
}
struct LessThanOrEqualPredicate;
impl NamedPredicate for LessThanOrEqualPredicate {
fn name(&self) -> &str {
"less_than_or_equal"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let n = extract_number(value);
let max = args.get("value").and_then(extract_number_from_value);
match (n, max) {
(Some(n), Some(max)) => n <= max,
_ => false,
}
}
}
struct GreaterThanOrEqualPredicate;
impl NamedPredicate for GreaterThanOrEqualPredicate {
fn name(&self) -> &str {
"greater_than_or_equal"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let n = extract_number(value);
let min = args.get("value").and_then(extract_number_from_value);
match (n, min) {
(Some(n), Some(min)) => n >= min,
_ => false,
}
}
}
fn extract_number(value: &Value) -> Option<f64> {
match value {
Value::Number(n) => n.as_f64(),
Value::String(s) => {
let cleaned: String = s
.chars()
.filter(|c| c.is_ascii_digit() || *c == '.' || *c == '-')
.collect();
cleaned.parse::<f64>().ok()
}
_ => None,
}
}
fn extract_number_from_value(value: &Value) -> Option<f64> {
extract_number(value)
}
struct FormatDecimal2Predicate;
impl NamedPredicate for FormatDecimal2Predicate {
fn name(&self) -> &str {
"format:decimal-2"
}
fn evaluate(&self, value: &Value, _args: &Value) -> bool {
let s = match value.as_str() {
Some(s) => s.trim(),
None => return true, };
if s.is_empty() {
return true;
}
if !s.contains('.') {
return s.parse::<i64>().is_ok();
}
if let Some(dot_pos) = s.rfind('.') {
let decimals = s.len() - dot_pos - 1;
decimals == 2 && s.parse::<f64>().is_ok()
} else {
false
}
}
}
struct IsDecimalPlacesPredicate;
impl NamedPredicate for IsDecimalPlacesPredicate {
fn name(&self) -> &str {
"is_decimal_places"
}
fn evaluate(&self, value: &Value, args: &Value) -> bool {
let places = args.get("places").and_then(|v| v.as_u64()).unwrap_or(2) as usize;
let is_max = args.get("max").and_then(|v| v.as_bool()).unwrap_or(false);
let s = match value {
Value::String(s) => s.trim().to_string(),
Value::Number(n) => {
if let Some(f) = n.as_f64() {
format!("{}", f)
} else {
return false;
}
}
_ => return false,
};
if s.is_empty() {
return false;
}
let s = s.trim_start_matches('-');
if s.parse::<f64>().is_err() {
return false;
}
let actual_places = if let Some(dot_pos) = s.rfind('.') {
let decimal_part = &s[dot_pos + 1..];
decimal_part.len()
} else {
0
};
if is_max {
actual_places <= places
} else {
actual_places == places
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_is_non_negative() {
let pred = IsNonNegativePredicate;
assert!(pred.evaluate(&json!(0), &json!(null)));
assert!(pred.evaluate(&json!(100), &json!(null)));
assert!(pred.evaluate(&json!(0.0), &json!(null)));
assert!(pred.evaluate(&json!("100.50"), &json!(null)));
assert!(pred.evaluate(&json!("$1,234.56"), &json!(null)));
assert!(!pred.evaluate(&json!(-1), &json!(null)));
assert!(!pred.evaluate(&json!("-100.50"), &json!(null)));
}
#[test]
fn test_is_positive() {
let pred = IsPositivePredicate;
assert!(pred.evaluate(&json!(1), &json!(null)));
assert!(pred.evaluate(&json!(0.01), &json!(null)));
assert!(!pred.evaluate(&json!(0), &json!(null)));
assert!(!pred.evaluate(&json!(-1), &json!(null)));
}
#[test]
fn test_is_negative() {
let pred = IsNegativePredicate;
assert!(pred.evaluate(&json!(-1), &json!(null)));
assert!(pred.evaluate(&json!("-10.5"), &json!(null)));
assert!(!pred.evaluate(&json!(0), &json!(null)));
assert!(!pred.evaluate(&json!(1), &json!(null)));
}
#[test]
fn test_is_non_positive() {
let pred = IsNonPositivePredicate;
assert!(pred.evaluate(&json!(-1), &json!(null)));
assert!(pred.evaluate(&json!(0), &json!(null)));
assert!(!pred.evaluate(&json!(1), &json!(null)));
}
#[test]
fn test_is_percentage() {
let pred = IsPercentagePredicate;
assert!(pred.evaluate(&json!(50), &json!(null)));
assert!(pred.evaluate(&json!(0), &json!(null)));
assert!(pred.evaluate(&json!(100), &json!(null)));
assert!(pred.evaluate(&json!("75%"), &json!(null)));
assert!(!pred.evaluate(&json!(101), &json!(null)));
assert!(!pred.evaluate(&json!(-1), &json!(null)));
assert!(pred.evaluate(&json!(0.5), &json!({"format": "decimal"})));
assert!(pred.evaluate(&json!(1.0), &json!({"format": "decimal"})));
assert!(!pred.evaluate(&json!(1.5), &json!({"format": "decimal"})));
assert!(pred.evaluate(&json!(150), &json!({"allow_over_100": true})));
}
#[test]
fn test_is_money_format() {
let pred = IsMoneyFormatPredicate;
assert!(pred.evaluate(&json!("1234.56"), &json!(null)));
assert!(pred.evaluate(&json!("$1,234.56"), &json!(null)));
assert!(pred.evaluate(&json!("1234"), &json!(null)));
assert!(pred.evaluate(&json!("-$1,234.56"), &json!(null)));
assert!(!pred.evaluate(&json!("1234.567"), &json!(null)));
assert!(pred.evaluate(&json!("1234.567"), &json!({"max_decimals": 3})));
assert!(!pred.evaluate(&json!("-100"), &json!({"allow_negative": false})));
assert!(!pred.evaluate(&json!(""), &json!(null)));
assert!(!pred.evaluate(&json!("abc"), &json!(null)));
}
#[test]
fn test_is_multiple_of() {
let pred = IsMultipleOfPredicate;
assert!(pred.evaluate(&json!(10), &json!({"value": 5})));
assert!(pred.evaluate(&json!("12.5"), &json!({"value": "2.5"})));
assert!(pred.evaluate(&json!(0.3), &json!({"value": 0.1})));
assert!(!pred.evaluate(&json!(11), &json!({"value": 5})));
assert!(!pred.evaluate(&json!(10), &json!({"value": 0})));
assert!(!pred.evaluate(&json!("abc"), &json!({"value": 5})));
}
#[test]
fn test_less_than_or_equal() {
let pred = LessThanOrEqualPredicate;
assert!(pred.evaluate(&json!(50), &json!({"value": 100})));
assert!(pred.evaluate(&json!(100), &json!({"value": 100})));
assert!(!pred.evaluate(&json!(101), &json!({"value": 100})));
assert!(pred.evaluate(&json!("50.00"), &json!({"value": "100.00"})));
}
#[test]
fn test_greater_than_or_equal() {
let pred = GreaterThanOrEqualPredicate;
assert!(pred.evaluate(&json!(100), &json!({"value": 50})));
assert!(pred.evaluate(&json!(50), &json!({"value": 50})));
assert!(!pred.evaluate(&json!(49), &json!({"value": 50})));
}
#[test]
fn test_is_decimal_places() {
let pred = IsDecimalPlacesPredicate;
assert!(pred.evaluate(&json!("123.45"), &json!({"places": 2})));
assert!(pred.evaluate(&json!("0.99"), &json!({"places": 2})));
assert!(!pred.evaluate(&json!("123.4"), &json!({"places": 2})));
assert!(!pred.evaluate(&json!("123.456"), &json!({"places": 2})));
assert!(pred.evaluate(&json!("123"), &json!({"places": 0})));
assert!(!pred.evaluate(&json!("123.4"), &json!({"places": 0})));
assert!(pred.evaluate(&json!("1.234"), &json!({"places": 3})));
assert!(!pred.evaluate(&json!("1.23"), &json!({"places": 3})));
assert!(pred.evaluate(&json!("123"), &json!({"places": 2, "max": true})));
assert!(pred.evaluate(&json!("123.4"), &json!({"places": 2, "max": true})));
assert!(pred.evaluate(&json!("123.45"), &json!({"places": 2, "max": true})));
assert!(!pred.evaluate(&json!("123.456"), &json!({"places": 2, "max": true})));
assert!(pred.evaluate(&json!("-123.45"), &json!({"places": 2})));
assert!(pred.evaluate(&json!(123.45), &json!({"places": 2})));
}
}