use std::collections::HashSet;
use tishlang_core::{ObjectData, Value, VmRef};
fn same_value_zero_num(a: f64, b: f64) -> bool {
if a.is_nan() && b.is_nan() {
return true;
}
if a == 0.0 && b == 0.0 {
return true;
}
a == b
}
fn ptr_key_obj(o: &VmRef<ObjectData>) -> usize {
o.as_ptr() as usize
}
fn ptr_key_arr(a: &VmRef<Vec<Value>>) -> usize {
a.as_ptr() as usize
}
fn type_name_of(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "Boolean",
Value::Number(_) => "Number",
Value::String(_) => "String",
Value::Symbol(_) => "Symbol",
Value::Array(_) | Value::NumberArray(_) => "Array",
Value::Object(_) => "Object",
Value::Function(_) => "Function",
#[cfg(feature = "regex")]
Value::RegExp(_) => "RegExp",
Value::Promise(_) => "Promise",
Value::Opaque(_) => "Object",
}
}
fn asymmetric_match(
actual: &Value,
expected: &Value,
seen: &mut HashSet<(usize, usize)>,
) -> Option<bool> {
let Value::Object(e) = expected else {
return None;
};
let eb = e.borrow();
let Some(Value::String(kind)) = eb.strings.get(crate::expect::ASYMMETRIC_KEY) else {
return None;
};
Some(match kind.as_str() {
"anything" => !matches!(actual, Value::Null),
"any" => {
let want = eb
.strings
.get("typeName")
.map(|v| v.to_display_string())
.unwrap_or_else(|| "Object".into());
let got = type_name_of(actual);
want.eq_ignore_ascii_case(got)
|| (want.eq_ignore_ascii_case("Object") && matches!(actual, Value::Object(_)))
}
"objectContaining" => {
let sample = eb.strings.get("sample").cloned().unwrap_or(Value::Null);
partial_deep_strict_equal(actual, &sample)
}
"arrayContaining" => {
let sample = eb
.strings
.get("sample")
.cloned()
.unwrap_or(Value::Null)
.coerce_number_array();
let Value::Array(want) = sample else {
return Some(false);
};
let actual_coerced = actual.clone().coerce_number_array();
let Value::Array(haystack) = actual_coerced else {
return Some(false);
};
let needles: Vec<Value> = want.borrow().clone();
let hay: Vec<Value> = haystack.borrow().clone();
needles.iter().all(|needle| {
hay.iter()
.any(|item| deep_strict_equal_inner(item, needle, seen))
})
}
"stringMatching" => {
let sample = eb.strings.get("sample").cloned().unwrap_or(Value::Null);
let Value::String(s) = actual else {
return Some(false);
};
match sample {
Value::String(pat) => s.contains(pat.as_str()),
#[cfg(feature = "regex")]
Value::RegExp(re) => re.borrow_mut().test(s),
_ => false,
}
}
_ => false,
})
}
pub fn deep_strict_equal(a: &Value, b: &Value) -> bool {
let mut seen = HashSet::new();
deep_strict_equal_inner(a, b, &mut seen)
}
fn deep_strict_equal_inner(a: &Value, b: &Value, seen: &mut HashSet<(usize, usize)>) -> bool {
let a = a.clone().coerce_number_array();
let b = b.clone().coerce_number_array();
if let Some(ok) = asymmetric_match(&a, &b, seen) {
return ok;
}
if let Some(ok) = asymmetric_match(&b, &a, seen) {
return ok;
}
match (&a, &b) {
(Value::Null, Value::Null) => true,
(Value::Bool(x), Value::Bool(y)) => x == y,
(Value::Number(x), Value::Number(y)) => same_value_zero_num(*x, *y),
(Value::String(x), Value::String(y)) => x.as_str() == y.as_str(),
(Value::Symbol(x), Value::Symbol(y)) => std::sync::Arc::ptr_eq(x, y),
(Value::Array(x), Value::Array(y)) => {
if VmRef::ptr_eq(x, y) {
return true;
}
let kx = ptr_key_arr(x);
let ky = ptr_key_arr(y);
let pair = (kx.min(ky), kx.max(ky));
if !seen.insert(pair) {
return true;
}
let xb = x.borrow();
let yb = y.borrow();
if xb.len() != yb.len() {
return false;
}
xb.iter()
.zip(yb.iter())
.all(|(u, v)| deep_strict_equal_inner(u, v, seen))
}
(Value::Object(x), Value::Object(y)) => {
if VmRef::ptr_eq(x, y) {
return true;
}
let kx = ptr_key_obj(x);
let ky = ptr_key_obj(y);
let pair = (kx.min(ky), kx.max(ky));
if !seen.insert(pair) {
return true;
}
let xb = x.borrow();
let yb = y.borrow();
if xb.strings.len() != yb.strings.len() {
return false;
}
for (k, v) in xb.strings.iter() {
match yb.strings.get(k) {
Some(ov) => {
if !deep_strict_equal_inner(v, ov, seen) {
return false;
}
}
None => return false,
}
}
true
}
#[cfg(feature = "regex")]
(Value::RegExp(x), Value::RegExp(y)) => {
let xb = x.borrow();
let yb = y.borrow();
xb.source == yb.source && format!("{}", xb.flags) == format!("{}", yb.flags)
}
(Value::Function(x), Value::Function(y)) => std::sync::Arc::ptr_eq(x, y),
(Value::Opaque(x), Value::Opaque(y)) => std::sync::Arc::ptr_eq(x, y),
_ => false,
}
}
pub fn partial_deep_strict_equal(actual: &Value, expected: &Value) -> bool {
match (actual, expected) {
(Value::Object(a), Value::Object(e)) => {
let ab = a.borrow();
let eb = e.borrow();
for (k, ev) in eb.strings.iter() {
match ab.strings.get(k) {
Some(av) => {
if matches!(ev, Value::Object(_) | Value::Array(_)) {
if !partial_deep_strict_equal(av, ev) {
return false;
}
} else if !deep_strict_equal(av, ev) {
return false;
}
}
None => return false,
}
}
true
}
(Value::Array(a), Value::Array(e)) => {
let ab = a.borrow();
let eb = e.borrow();
if eb.len() > ab.len() {
return false;
}
for (ev, av) in eb.iter().zip(ab.iter()) {
if matches!(ev, Value::Object(_) | Value::Array(_)) {
if !partial_deep_strict_equal(av, ev) {
return false;
}
} else if !deep_strict_equal(av, ev) {
return false;
}
}
true
}
_ => deep_strict_equal(actual, expected),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tishlang_core::{Arc, ObjectMap};
#[test]
fn primitives() {
assert!(deep_strict_equal(&Value::Null, &Value::Null));
assert!(deep_strict_equal(&Value::Number(1.0), &Value::Number(1.0)));
assert!(deep_strict_equal(
&Value::Number(f64::NAN),
&Value::Number(f64::NAN)
));
assert!(!deep_strict_equal(&Value::Number(1.0), &Value::Number(2.0)));
}
#[test]
fn objects() {
let mut m1 = ObjectMap::default();
m1.insert(Arc::from("a"), Value::Number(1.0));
let mut m2 = ObjectMap::default();
m2.insert(Arc::from("a"), Value::Number(1.0));
assert!(deep_strict_equal(&Value::object(m1), &Value::object(m2)));
}
}