Skip to content

Commit

Permalink
Merge pull request #256 from leoafarias/main
Browse files Browse the repository at this point in the history
  • Loading branch information
rodydavis committed Apr 26, 2024
2 parents ad824c1 + 36a2be0 commit cd1711f
Show file tree
Hide file tree
Showing 2 changed files with 270 additions and 0 deletions.
78 changes: 78 additions & 0 deletions packages/signals_core/lib/src/utils/deep_collection_equality.dart
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 packages/signals_core/test/utils/deep_collection_equality_test.dart
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;
}

0 comments on commit cd1711f

Please sign in to comment.