use crate::core::eval::eval;
use crate::core::Expr;
use crate::error::{Result, TemplateError};
use crate::template::TModel;
use std::cmp::Ordering;
use unicode_normalization::UnicodeNormalization;
#[derive(Clone, Copy)]
pub enum CmpOp {
Eq,
NotEq,
Gt,
Gte,
Lt,
Lte,
}
pub struct ComparisonExpression {
pub left: Expr,
pub right: Expr,
pub op: CmpOp,
}
impl ComparisonExpression {
pub fn new(left: Expr, right: Expr, op: CmpOp) -> Self {
ComparisonExpression { left, right, op }
}
pub(crate) fn eval(&self, env: &mut crate::core::Environment) -> Result<TModel> {
eval_compare(env, &self.left, &self.right, self.op)
}
}
fn eval_compare(
env: &mut crate::core::Environment,
a: &Expr,
b: &Expr,
op: CmpOp,
) -> Result<TModel> {
let l = eval(env, a)?;
let r = eval(env, b)?;
let ord = compare_models(env, &l, &r, op)?;
Ok(TModel::from_boolean(ord))
}
pub fn compare_models(
_env: &mut crate::core::Environment,
l: &TModel,
r: &TModel,
op: CmpOp,
) -> Result<bool> {
let order = if l.is_number() && r.is_number() {
compare_numbers(&l.get_number()?, &r.get_number()?)
} else if l.is_date() && r.is_date() {
let ld = l.get_date()?;
let rd = r.get_date()?;
if ld.kind != rd.kind {
return Err(TemplateError::misc(format!(
"Can't compare dates of different types. Left date type is {}, right date type is {}.",
ld.kind.name(),
rd.kind.name()
)));
}
ld.dt.cmp(&rd.dt)
} else if l.is_scalar() && r.is_scalar() {
if !matches!(op, CmpOp::Eq | CmpOp::NotEq) {
return Err(TemplateError::misc(format!(
"Can't use operator \"{}\" on string values.",
cmp_op_str(op)
)));
}
let ls = l.get_scalar()?;
let rs = r.get_scalar()?;
let ln: String = ls.chars().nfkc().collect();
let rn: String = rs.chars().nfkc().collect();
utf16_cmp(&ln, &rn)
} else if l.is_boolean() && r.is_boolean() {
if !matches!(op, CmpOp::Eq | CmpOp::NotEq) {
return Err(TemplateError::misc(format!(
"Can't use operator \"{}\" on boolean values.",
cmp_op_str(op)
)));
}
let lb = l.get_boolean()?;
let rb = r.get_boolean()?;
lb.cmp(&rb)
} else {
return Err(TemplateError::misc(
"Can't compare values of these types. Allowed comparisons are between two numbers, two strings, two dates, or two booleans.",
));
};
Ok(match op {
CmpOp::Eq => order == Ordering::Equal,
CmpOp::NotEq => order != Ordering::Equal,
CmpOp::Gt => order == Ordering::Greater,
CmpOp::Gte => order != Ordering::Less,
CmpOp::Lt => order == Ordering::Less,
CmpOp::Lte => order != Ordering::Greater,
})
}
pub(crate) fn compare_numbers(a: &crate::value::TNumber, b: &crate::value::TNumber) -> Ordering {
use crate::value::TNumber as N;
let sa = number_signum(a);
let sb = number_signum(b);
if sa != sb {
return sa.cmp(&sb);
}
if sa == 0 && sb == 0 {
return Ordering::Equal;
}
match (a, b) {
(N::Float(x), N::Float(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
(N::Double(x), N::Double(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
(N::Int(x), N::Int(y)) => x.cmp(y),
(N::Long(x), N::Long(y)) => x.cmp(y),
(N::BigInt(x), N::BigInt(y)) => x.cmp(y),
(N::Float(x), N::Double(y)) => (*x as f64).partial_cmp(y).unwrap_or(Ordering::Equal),
(N::Double(x), N::Float(y)) => x.partial_cmp(&(*y as f64)).unwrap_or(Ordering::Equal),
_ => a.as_big_decimal().cmp(&b.as_big_decimal()),
}
}
fn number_signum(n: &crate::value::TNumber) -> i32 {
use crate::value::TNumber as N;
match n {
N::Int(v) => v.signum(),
N::Long(v) => v.signum() as i32,
N::BigInt(v) => match v.sign() {
num_bigint::Sign::Minus => -1,
num_bigint::Sign::NoSign => 0,
num_bigint::Sign::Plus => 1,
},
N::Decimal(d) => match d.sign() {
num_bigint::Sign::Minus => -1,
num_bigint::Sign::NoSign => 0,
num_bigint::Sign::Plus => 1,
},
N::Float(v) => {
if *v > 0.0 {
1
} else if *v < 0.0 {
-1
} else {
0
}
}
N::Double(v) => {
if *v > 0.0 {
1
} else if *v < 0.0 {
-1
} else {
0
}
}
}
}
fn cmp_op_str(op: CmpOp) -> &'static str {
match op {
CmpOp::Eq => "==",
CmpOp::NotEq => "!=",
CmpOp::Gt => ">",
CmpOp::Gte => ">=",
CmpOp::Lt => "<",
CmpOp::Lte => "<=",
}
}
fn utf16_cmp(a: &str, b: &str) -> Ordering {
let au: Vec<u16> = a.encode_utf16().collect();
let bu: Vec<u16> = b.encode_utf16().collect();
for (x, y) in au.iter().zip(bu.iter()) {
match x.cmp(y) {
Ordering::Equal => {}
o => return o,
}
}
au.len().cmp(&bu.len())
}