-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #256 from leoafarias/main
- Loading branch information
Showing
2 changed files
with
270 additions
and
0 deletions.
There are no files selected for viewing
78 changes: 78 additions & 0 deletions
78
packages/signals_core/lib/src/utils/deep_collection_equality.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
/// Provides a [DeepCollectionEquality] class that can be used to compare the | ||
/// equality of complex data structures, such as maps, sets, and iterables, | ||
/// by recursively comparing their elements. | ||
/// | ||
/// The [equals] method compares two objects for deep equality, handling maps, | ||
/// sets, and iterables. | ||
/// | ||
/// This class is useful when you need to compare complex data structures for | ||
/// equality or use them as keys in a hash-based data structure, such as a | ||
/// [Map] or [Set]. | ||
class DeepCollectionEquality { | ||
/// Creates a new [DeepCollectionEquality] instance. | ||
const DeepCollectionEquality(); | ||
|
||
/// Compares two maps for deep equality. | ||
/// The maps are equal if they have the same keys and values. | ||
/// The order of keys is not considered. | ||
bool _mapsEqual(Map map1, Map map2) { | ||
if (map1.length != map2.length) return false; | ||
for (var key in map1.keys) { | ||
if (!map2.containsKey(key) || !equals(map1[key], map2[key])) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/// Compares two iterables for deep equality, considering element order. | ||
/// The iterables are equal if they have the same length and contain the same | ||
/// elements in the same order. | ||
bool _iterablesEqual(Iterable iter1, Iterable iter2) { | ||
if (iter1.length != iter2.length) return false; | ||
if (iter1 is List && iter2 is List) { | ||
for (int i = 0; i < iter1.length; i++) { | ||
if (!equals(iter1[i], iter2[i])) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/// Compares two sets for deep equality. | ||
/// The order of elements in the sets is not considered. | ||
/// The sets are equal if they contain the same elements. | ||
bool _setsEqual(Set set1, Set set2) { | ||
if (set1.length != set2.length) return false; | ||
for (var value in set1) { | ||
if (!set2.contains(value)) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/// Compares two objects for deep equality, handling maps, sets, and iterables. | ||
/// The objects are equal if they reference the same object | ||
/// or their types are equal and their fields are equal. | ||
bool equals(Object? obj1, Object? obj2) { | ||
if (identical(obj1, obj2)) return true; | ||
if (obj1.runtimeType != obj2.runtimeType) return false; | ||
|
||
if (obj1 is Map) { | ||
return _mapsEqual(obj1, obj2 as Map); | ||
} else if (obj1 is Set) { | ||
return _setsEqual(obj1, obj2 as Set); | ||
} else if (obj1 is Iterable) { | ||
return _iterablesEqual(obj1, obj2 as Iterable); | ||
} | ||
|
||
return obj1 == obj2; | ||
} | ||
} |
192 changes: 192 additions & 0 deletions
192
packages/signals_core/test/utils/deep_collection_equality_test.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
import 'package:signals_core/src/utils/deep_collection_equality.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
void main() { | ||
group('DeepEqualityChecker hash code', () { | ||
const deepEquality = DeepCollectionEquality(); | ||
test('checks primitive type equality', () { | ||
expect(deepEquality.equals(1, 1), isTrue); | ||
expect(deepEquality.equals('string', 'string'), isTrue); | ||
expect(deepEquality.equals(1.0, 1.0), isTrue); | ||
expect(deepEquality.equals(1, '1'), isFalse); | ||
expect(deepEquality.equals('1', 1), isFalse); | ||
expect(deepEquality.equals(1, 2), isFalse); | ||
}); | ||
|
||
test('checks Map equality with simple types', () { | ||
expect(deepEquality.equals({'key': 'value'}, {'key': 'value'}), isTrue); | ||
expect( | ||
deepEquality.equals({'key1': 'value'}, {'key2': 'value'}), | ||
isFalse, | ||
); | ||
expect( | ||
deepEquality.equals({'key': 'value1'}, {'key': 'value2'}), | ||
isFalse, | ||
); | ||
expect(deepEquality.equals({}, {}), isTrue); | ||
expect( | ||
deepEquality | ||
.equals({'key': 'value'}, {'key': 'value', 'extra': 'value'}), | ||
isFalse, | ||
); | ||
}); | ||
|
||
test('checks Set equality with simple types', () { | ||
expect(deepEquality.equals({1, 2, 3}, {1, 2, 3}), isTrue); | ||
expect(deepEquality.equals({1, 2}, {1, 2, 3}), isFalse); | ||
expect( | ||
deepEquality.equals({3, 2, 1}, {1, 2, 3}), | ||
isTrue, | ||
); // Order should not matter in sets | ||
// ignore: equal_elements_in_set | ||
expect( | ||
// ignore: equal_elements_in_set | ||
deepEquality.equals({1}, {1, 1, 1}), | ||
isTrue, | ||
); // Duplicate elements in a set | ||
}); | ||
|
||
test('checks Iterable equality with simple types', () { | ||
expect(deepEquality.equals([1, 2, 3], [1, 2, 3]), isTrue, reason: '1'); | ||
expect(deepEquality.equals([1, 2, 3], [3, 2, 1]), isFalse, reason: '2'); | ||
expect(deepEquality.equals([], []), isTrue, reason: '3'); | ||
expect(deepEquality.equals([1, 2, 3], [1, 2]), isFalse, reason: '4'); | ||
}); | ||
|
||
test('checks nested collection equality', () { | ||
expect( | ||
deepEquality.equals( | ||
[ | ||
{'key': 'value'}, | ||
{'key2': 'value2'}, | ||
], | ||
[ | ||
{'key': 'value'}, | ||
{'key2': 'value2'}, | ||
], | ||
), | ||
isTrue, | ||
); | ||
expect( | ||
deepEquality.equals( | ||
{ | ||
'outer': {'inner': 'value'}, | ||
}, | ||
{ | ||
'outer': {'inner': 'value'}, | ||
}, | ||
), | ||
isTrue, | ||
); | ||
expect( | ||
deepEquality.equals( | ||
{ | ||
'set': {1, 2, 3}, | ||
}, | ||
{ | ||
'set': {3, 2, 1}, | ||
}, | ||
), | ||
isTrue, | ||
); | ||
expect( | ||
deepEquality.equals( | ||
[ | ||
1, | ||
[ | ||
2, | ||
[3, 4], | ||
], | ||
], | ||
[ | ||
1, | ||
[ | ||
2, | ||
[3, 4], | ||
], | ||
], | ||
), | ||
isTrue, | ||
); | ||
expect( | ||
deepEquality.equals( | ||
{ | ||
'set': {1, 2, 3}, | ||
}, | ||
{ | ||
'set': {1, 2}, | ||
}, | ||
), | ||
isFalse, | ||
); | ||
}); | ||
|
||
test('checks custom object equality', () { | ||
const obj1 = _CustomClass(id: 1, value: 'Test'); | ||
const obj2 = _CustomClass(id: 1, value: 'Test'); | ||
const obj3 = _CustomClass(id: 2, value: 'Test'); | ||
|
||
expect(deepEquality.equals(obj1, obj2), isTrue); | ||
expect(deepEquality.equals(obj1, obj3), isFalse); | ||
}); | ||
|
||
test('checks nested custom object equality', () { | ||
const nestedObj1 = _AnotherClass( | ||
id: 1, | ||
name: 'another class', | ||
); | ||
|
||
const nestedObj2 = _AnotherClass( | ||
id: 1, | ||
name: 'another class', | ||
); | ||
|
||
const nestedObjDifferent = _CustomClass( | ||
id: 1, | ||
value: 'custom class', | ||
); | ||
|
||
expect(deepEquality.equals(nestedObj1, nestedObj2), isTrue); | ||
expect(deepEquality.equals(nestedObj1, nestedObjDifferent), isFalse); | ||
}); | ||
}); | ||
} | ||
|
||
// A dummy custom object class for testing purposes | ||
class _CustomClass { | ||
final int id; | ||
final String value; | ||
|
||
const _CustomClass({required this.id, required this.value}); | ||
|
||
@override | ||
bool operator ==(Object other) { | ||
if (identical(this, other)) return true; | ||
|
||
return other is _CustomClass && other.id == id && other.value == value; | ||
} | ||
|
||
@override | ||
int get hashCode => id.hashCode ^ value.hashCode; | ||
} | ||
|
||
class _AnotherClass { | ||
final int id; | ||
final String name; | ||
|
||
const _AnotherClass({ | ||
required this.id, | ||
required this.name, | ||
}); | ||
|
||
@override | ||
bool operator ==(Object other) => | ||
identical(this, other) || | ||
other is _AnotherClass && | ||
runtimeType == other.runtimeType && | ||
id == other.id && | ||
name == other.name; | ||
|
||
@override | ||
int get hashCode => id.hashCode ^ name.hashCode; | ||
} |