Skip to content

Commit d23ad91

Browse files
committed
Implement global is_equal function for deep equals check of Variant types
1 parent 9f03bbf commit d23ad91

File tree

4 files changed

+327
-0
lines changed

4 files changed

+327
-0
lines changed

core/variant/variant_utility.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,6 +1234,108 @@ bool VariantUtilityFunctions::is_same(const Variant &p_a, const Variant &p_b) {
12341234
return p_a.identity_compare(p_b);
12351235
}
12361236

1237+
bool VariantUtilityFunctions::is_equal(const Variant &p_a, const Variant &p_b) {
1238+
const Variant::Type type_a = p_a.get_type();
1239+
const Variant::Type type_b = p_b.get_type();
1240+
1241+
if (type_a != type_b) {
1242+
return false;
1243+
}
1244+
1245+
// Handle non-recursive types.
1246+
if (type_a != Variant::Type::DICTIONARY &&
1247+
type_a != Variant::Type::OBJECT &&
1248+
type_a != Variant::Type::ARRAY) {
1249+
return p_a == p_b;
1250+
}
1251+
1252+
// Handle recursive types, e.g., Array, Dictionary, and Object.
1253+
if (type_a == Variant::Type::ARRAY) {
1254+
const Array arr_a = p_a;
1255+
const Array arr_b = p_b;
1256+
const int64_t size_a = arr_a.size();
1257+
1258+
if (size_a != arr_b.size()) {
1259+
return false;
1260+
}
1261+
1262+
for (int64_t i = 0; i < size_a; ++i) {
1263+
if (!is_equal(arr_a[i], arr_b[i])) {
1264+
return false;
1265+
}
1266+
}
1267+
1268+
return true;
1269+
}
1270+
1271+
if (type_a == Variant::Type::DICTIONARY) {
1272+
const Dictionary dict_a = p_a;
1273+
const Dictionary dict_b = p_b;
1274+
1275+
const Array keys_a = dict_a.keys();
1276+
const Array keys_b = dict_b.keys();
1277+
1278+
if (keys_a.size() != keys_b.size()) {
1279+
return false;
1280+
}
1281+
1282+
for (int64_t i = 0; i < keys_a.size(); ++i) {
1283+
const Variant &key = keys_a[i];
1284+
if (!dict_b.has(key)) {
1285+
return false;
1286+
}
1287+
if (!is_equal(dict_a[key], dict_b[key])) {
1288+
return false;
1289+
}
1290+
}
1291+
1292+
return true;
1293+
}
1294+
1295+
if (type_a == Variant::Type::OBJECT) {
1296+
const Object *obj_a_ptr = p_a;
1297+
const Object *obj_b_ptr = p_b;
1298+
1299+
if (obj_a_ptr == nullptr && obj_b_ptr == nullptr) {
1300+
return true;
1301+
}
1302+
if (obj_a_ptr == nullptr || obj_b_ptr == nullptr) {
1303+
return false;
1304+
}
1305+
1306+
// Optimization: Check for instance equality.
1307+
if (obj_a_ptr == obj_b_ptr) {
1308+
return true;
1309+
}
1310+
1311+
List<PropertyInfo> props_a;
1312+
obj_a_ptr->get_property_list(&props_a);
1313+
List<PropertyInfo> props_b;
1314+
obj_b_ptr->get_property_list(&props_b);
1315+
1316+
if (props_a.size() != props_b.size()) {
1317+
return false;
1318+
}
1319+
1320+
for (const PropertyInfo &prop_info : props_a) {
1321+
if (props_b.find(prop_info) == nullptr) {
1322+
return false;
1323+
}
1324+
}
1325+
1326+
for (const PropertyInfo &prop_info : props_a) {
1327+
if (!is_equal(obj_a_ptr->get(prop_info.name), obj_b_ptr->get(prop_info.name))) {
1328+
return false;
1329+
}
1330+
}
1331+
1332+
return true;
1333+
}
1334+
1335+
ERR_PRINT("Unhandled Variant type in is_equal. This is a bug.");
1336+
return false;
1337+
}
1338+
12371339
#ifdef DEBUG_METHODS_ENABLED
12381340
#define VCALLR *ret = p_func(VariantCasterAndValidate<P>::cast(p_args, Is, r_error)...)
12391341
#define VCALL p_func(VariantCasterAndValidate<P>::cast(p_args, Is, r_error)...)
@@ -1855,6 +1957,7 @@ void Variant::_register_variant_utility_functions() {
18551957
FUNCBINDR(rid_from_int64, sarray("base"), Variant::UTILITY_FUNC_TYPE_GENERAL);
18561958

18571959
FUNCBINDR(is_same, sarray("a", "b"), Variant::UTILITY_FUNC_TYPE_GENERAL);
1960+
FUNCBINDR(is_equal, sarray("a", "b"), Variant::UTILITY_FUNC_TYPE_GENERAL);
18581961
}
18591962

18601963
void Variant::_unregister_variant_utility_functions() {

core/variant/variant_utility.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,5 @@ struct VariantUtilityFunctions {
152152
static uint64_t rid_allocate_id();
153153
static RID rid_from_int64(uint64_t p_base);
154154
static bool is_same(const Variant &p_a, const Variant &p_b);
155+
static bool is_equal(const Variant &a, const Variant &b);
155156
};

doc/classes/@GlobalScope.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,20 @@
537537
See also [method lerp], which performs the reverse of this operation, and [method remap] to map a continuous series of values to another.
538538
</description>
539539
</method>
540+
<method name="is_equal">
541+
<return type="bool" />
542+
<param index="0" name="a" type="Variant" />
543+
<param index="1" name="b" type="Variant" />
544+
<description>
545+
Performs a deep recursive equality check between two Variant values, [param a] and [param b].
546+
547+
Returns [code]true[/code] if the Variants are considered equal based on their type and content, [code]false[/code] otherwise.
548+
- Basic types (non-Array/Dictionary/Object) use standard [code]==[/code] comparison.
549+
- Arrays are equal if sizes match and all elements are recursively equal.
550+
- Dictionaries are equal if keys match and all corresponding values are recursively equal (order doesn't matter).
551+
- Objects are equal if both are [code]nullptr[/code], or if both are valid Objects with matching property lists and recursively equal property values (order doesn't matter).
552+
</description>
553+
</method>
540554
<method name="is_equal_approx" keywords="roughly">
541555
<return type="bool" />
542556
<param index="0" name="a" type="float" />

tests/core/variant/test_variant_utility.h

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@
3030

3131
#pragma once
3232

33+
#include "core/os/memory.h"
3334
#include "core/variant/variant_utility.h"
3435

36+
#include "scene/main/node.h"
3537
#include "tests/test_macros.h"
3638

3739
namespace TestVariantUtility {
@@ -128,4 +130,211 @@ TEST_CASE("[VariantUtility] Type conversion") {
128130
}
129131
}
130132

133+
TEST_CASE("[VariantUtility] is_equal deep equality checks") {
134+
SUBCASE("Different Types") {
135+
Variant v_int = 5;
136+
Variant v_str = "hello";
137+
Variant v_arr = Array();
138+
Variant v_dict = Dictionary();
139+
Variant v_null_obj = Variant();
140+
Node *node = memnew(Node);
141+
142+
CHECK_FALSE(VariantUtilityFunctions::is_equal(v_int, v_str));
143+
CHECK_FALSE(VariantUtilityFunctions::is_equal(v_str, v_int));
144+
CHECK_FALSE(VariantUtilityFunctions::is_equal(v_int, v_arr));
145+
CHECK_FALSE(VariantUtilityFunctions::is_equal(v_arr, v_dict));
146+
CHECK_FALSE(VariantUtilityFunctions::is_equal(v_dict, node));
147+
CHECK_FALSE(VariantUtilityFunctions::is_equal(node, v_null_obj));
148+
CHECK_FALSE(VariantUtilityFunctions::is_equal(v_int, v_null_obj));
149+
150+
memdelete(node);
151+
}
152+
153+
SUBCASE("Basic Types") {
154+
CHECK(VariantUtilityFunctions::is_equal(Variant(10), Variant(10)));
155+
CHECK(VariantUtilityFunctions::is_equal(Variant(3.14), Variant(3.14)));
156+
CHECK(VariantUtilityFunctions::is_equal(Variant(true), Variant(true)));
157+
CHECK(VariantUtilityFunctions::is_equal(Variant("test"), Variant("test")));
158+
CHECK(VariantUtilityFunctions::is_equal(Variant(Vector2(1, 2)), Variant(Vector2(1, 2))));
159+
CHECK(VariantUtilityFunctions::is_equal(Variant(), Variant())); // Nil == Nil
160+
161+
CHECK_FALSE(VariantUtilityFunctions::is_equal(Variant(10), Variant(11)));
162+
CHECK_FALSE(VariantUtilityFunctions::is_equal(Variant(3.14), Variant(3.141)));
163+
CHECK_FALSE(VariantUtilityFunctions::is_equal(Variant(true), Variant(false)));
164+
CHECK_FALSE(VariantUtilityFunctions::is_equal(Variant("test"), Variant("Test")));
165+
CHECK_FALSE(VariantUtilityFunctions::is_equal(Variant("test"), Variant("")));
166+
CHECK_FALSE(VariantUtilityFunctions::is_equal(Variant(Vector2(1, 2)), Variant(Vector2(1, 3))));
167+
CHECK_FALSE(VariantUtilityFunctions::is_equal(Variant(5), Variant()));
168+
}
169+
170+
SUBCASE("Arrays") {
171+
Array a1, a2;
172+
CHECK_MESSAGE(VariantUtilityFunctions::is_equal(a1, a2), "Empty arrays should be equal.");
173+
174+
a1.append(1);
175+
a1.append("hello");
176+
a2.append(1);
177+
a2.append("hello");
178+
CHECK_MESSAGE(VariantUtilityFunctions::is_equal(a1, a2), "Arrays with identical basic elements should be equal.");
179+
180+
Array a3;
181+
a3.append(1);
182+
a3.append("world");
183+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(a1, a3), "Arrays with different element values should not be equal.");
184+
185+
Array a4;
186+
a4.append(1);
187+
a4.append(2);
188+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(a1, a4), "Arrays with different element types at the same index should not be equal.");
189+
190+
Array a5;
191+
a5.append(1);
192+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(a1, a5), "Arrays with different sizes should not be equal.");
193+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(a5, a1), "Arrays with different sizes should not be equal.");
194+
195+
Array nested1, nested2, inner1, inner2;
196+
inner1.append(true);
197+
inner1.append(2.5);
198+
inner2.append(true);
199+
inner2.append(2.5);
200+
nested1.append("outer");
201+
nested1.append(inner1);
202+
nested2.append("outer");
203+
nested2.append(inner2);
204+
CHECK_MESSAGE(VariantUtilityFunctions::is_equal(nested1, nested2), "Arrays with equal nested arrays should be equal.");
205+
206+
Array nested3, inner3;
207+
inner3.append(true);
208+
inner3.append(3.0);
209+
nested3.append("outer");
210+
nested3.append(inner3);
211+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(nested1, nested3), "Arrays with unequal nested arrays should not be equal.");
212+
}
213+
214+
SUBCASE("Dictionaries") {
215+
Dictionary d1, d2;
216+
CHECK_MESSAGE(VariantUtilityFunctions::is_equal(d1, d2), "Empty dictionaries should be equal.");
217+
218+
d1["key1"] = 100;
219+
d1["key2"] = "val";
220+
d2["key1"] = 100;
221+
d2["key2"] = "val";
222+
CHECK_MESSAGE(VariantUtilityFunctions::is_equal(d1, d2), "Dictionaries with identical key-value pairs should be equal.");
223+
224+
Dictionary d3;
225+
d3["key2"] = "val";
226+
d3["key1"] = 100;
227+
CHECK_MESSAGE(VariantUtilityFunctions::is_equal(d1, d3), "Dictionaries with identical key-value pairs in different order should be equal.");
228+
229+
Dictionary d4;
230+
d4["key1"] = 100;
231+
d4["key2"] = "VAL";
232+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(d1, d4), "Dictionaries with different values for the same key should not be equal.");
233+
234+
Dictionary d5;
235+
d5["key1"] = 100;
236+
d5["key_other"] = "val";
237+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(d1, d5), "Dictionaries with different keys should not be equal.");
238+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(d5, d1), "Dictionaries with different keys should not be equal.");
239+
240+
Dictionary d6;
241+
d6["key1"] = 100;
242+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(d1, d6), "Dictionaries with different sizes should not be equal.");
243+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(d6, d1), "Dictionaries with different sizes should not be equal.");
244+
245+
Dictionary nested_d1, nested_d2, inner_d1, inner_d2;
246+
inner_d1["sub_key"] = 9.9;
247+
inner_d2["sub_key"] = 9.9;
248+
nested_d1["level1"] = inner_d1;
249+
nested_d1["another"] = false;
250+
nested_d2["level1"] = inner_d2;
251+
nested_d2["another"] = false;
252+
CHECK_MESSAGE(VariantUtilityFunctions::is_equal(nested_d1, nested_d2), "Dictionaries with equal nested dictionaries should be equal.");
253+
254+
Dictionary nested_d3, inner_d3;
255+
inner_d3["sub_key"] = 10.0;
256+
nested_d3["level1"] = inner_d3;
257+
nested_d3["another"] = false;
258+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(nested_d1, nested_d3), "Dictionaries with unequal nested dictionaries should not be equal.");
259+
}
260+
261+
SUBCASE("Objects") {
262+
Variant null_obj1;
263+
Variant null_obj2;
264+
CHECK_MESSAGE(VariantUtilityFunctions::is_equal(null_obj1, null_obj2), "Two null object Variants should be equal.");
265+
266+
Node *node1 = memnew(Node);
267+
Node *node2 = memnew(Node);
268+
Node *node3 = memnew(Node);
269+
270+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(null_obj1, node1), "A null object Variant and a non-null object Variant should not be equal.");
271+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(node1, null_obj1), "A non-null object Variant and a null object Variant should not be equal.");
272+
273+
CHECK_MESSAGE(VariantUtilityFunctions::is_equal(node1, node2), "Two distinct Node instances with default properties should be equal.");
274+
275+
node1->set_name("TestNode");
276+
node2->set_name("TestNode");
277+
CHECK_MESSAGE(VariantUtilityFunctions::is_equal(node1, node2), "Two Node instances with the same property value should be equal.");
278+
279+
node3->set_name("AnotherNode");
280+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(node1, node3), "Two Node instances with different property values should not be equal.");
281+
282+
Object *base_obj1_ptr = memnew(Object);
283+
Object *base_obj2_ptr = memnew(Object);
284+
Variant v_base_obj1 = base_obj1_ptr;
285+
Variant v_base_obj2 = base_obj2_ptr;
286+
287+
CHECK_MESSAGE(VariantUtilityFunctions::is_equal(v_base_obj1, v_base_obj2), "Two distinct base Object instances should be equal if properties match.");
288+
289+
base_obj1_ptr->set_meta("info", "data1");
290+
base_obj2_ptr->set_meta("info", "data1");
291+
CHECK_MESSAGE(VariantUtilityFunctions::is_equal(v_base_obj1, v_base_obj2), "Two objects with the same meta values should be equal.");
292+
293+
base_obj2_ptr->set_meta("info", "data2");
294+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(v_base_obj1, v_base_obj2), "Two objects with different meta values should not be equal.");
295+
296+
memdelete(base_obj1_ptr);
297+
memdelete(base_obj2_ptr);
298+
memdelete(node1);
299+
memdelete(node2);
300+
memdelete(node3);
301+
}
302+
303+
SUBCASE("Mixed Recursive Types") {
304+
Array mixed_a1, mixed_a2;
305+
Dictionary mixed_d1a, mixed_d1b;
306+
mixed_d1a["value"] = 50;
307+
mixed_d1b["value"] = 50;
308+
mixed_a1.append(mixed_d1a);
309+
mixed_a1.append("common");
310+
mixed_a2.append(mixed_d1b);
311+
mixed_a2.append("common");
312+
CHECK_MESSAGE(VariantUtilityFunctions::is_equal(mixed_a1, mixed_a2), "Arrays containing equal dictionaries should be equal.");
313+
314+
Array mixed_a3;
315+
Dictionary mixed_d1c;
316+
mixed_d1c["value"] = 51;
317+
mixed_a3.append(mixed_d1c);
318+
mixed_a3.append("common");
319+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(mixed_a1, mixed_a3), "Arrays containing unequal dictionaries should not be equal.");
320+
321+
Dictionary mixed_d2a, mixed_d2b;
322+
Array mixed_a1a, mixed_a1b;
323+
mixed_a1a.append(true);
324+
mixed_a1b.append(true);
325+
mixed_d2a["list"] = mixed_a1a;
326+
mixed_d2a["id"] = 1;
327+
mixed_d2b["list"] = mixed_a1b;
328+
mixed_d2b["id"] = 1;
329+
CHECK_MESSAGE(VariantUtilityFunctions::is_equal(mixed_d2a, mixed_d2b), "Dictionaries containing equal arrays should be equal.");
330+
331+
Dictionary mixed_d2c;
332+
Array mixed_a1c;
333+
mixed_a1c.append(false);
334+
mixed_d2c["list"] = mixed_a1c;
335+
mixed_d2c["id"] = 1;
336+
CHECK_FALSE_MESSAGE(VariantUtilityFunctions::is_equal(mixed_d2a, mixed_d2c), "Dictionaries containing unequal arrays should not be equal.");
337+
}
338+
}
339+
131340
} // namespace TestVariantUtility

0 commit comments

Comments
 (0)