Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: improve equality comparison performance #173

Merged
merged 17 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion lib/src/equatable_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ int mapPropsToHashCode(Iterable<Object?>? props) {
}

/// Determines whether two iterables are equal.
@pragma('vm:prefer-inline')
bool iterableEquals(Iterable<Object?> a, Iterable<Object?> b) {
assert(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on calling setEquals instead of asserting here?

if (a is Set && b is Set) return setEquals(a, b);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't expect anyone to use this function. I'm not sure it's even exported. So I'd prefer to leave it simple, without extra ifs

a is! Set && b is! Set,
"iterableEquals doesn't support Sets. Use setEquals instead.",
);
if (identical(a, b)) return true;
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
Expand All @@ -20,7 +25,9 @@ bool iterableEquals(Iterable<Object?> a, Iterable<Object?> b) {
bool setEquals(Set<Object?> a, Set<Object?> b) {
if (identical(a, b)) return true;
if (a.length != b.length) return false;
if (a.any((e) => !b.contains(e))) return false;
for (final element in a) {
if (!b.any((e) => objectsEquals(element, e))) return false;
}
return true;
}

Expand Down
239 changes: 236 additions & 3 deletions test/equatable_utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,258 @@ class Person with EquatableMixin {
}

void main() {
final bob = Person(name: 'Bob');
final alice = Person(name: 'Alice');
final aliceCopy = Person(name: 'Alice');

group('iterableEquals', () {
test('returns true for identical props', () {
final value = [Object()];
expect(iterableEquals(value, value), isTrue);
});

test('returns true for empty iterables', () {
expect(iterableEquals([], []), isTrue);
});

test('returns false when props differ in length', () {
final object = Object();
expect(iterableEquals([object], [object, object]), isFalse);
});

test('uses == when props are equatable', () {
final bob = Person(name: 'Bob');
final alice = Person(name: 'Alice');
expect(iterableEquals([alice], [alice]), isTrue);
expect(iterableEquals([alice], [aliceCopy]), isTrue);
expect(iterableEquals([bob], [bob]), isTrue);
expect(iterableEquals([alice], [bob]), isFalse);
expect(iterableEquals([bob], [alice]), isFalse);
expect(iterableEquals([alice, null], [alice, -1]), isFalse);
});

test('returns false for iterables with different elements', () {
final iterable1 = [1, 2, 3];
final iterable2 = [1, 2, 4];
expect(iterableEquals(iterable1, iterable2), isFalse);
});

test(
'returns false for iterable with same elements but different order',
() {
final iterable1 = [1, 2, 3];
final iterable2 = [1, 3, 2];
expect(iterableEquals(iterable1, iterable2), isFalse);
},
);

test('returns true for nested identical iterables', () {
final iterable1 = [
[bob, alice],
[alice, bob],
];
final iterable2 = [
[bob, alice],
[alice, bob],
];
expect(iterableEquals(iterable1, iterable2), isTrue);
});

test('returns false for nested iterables with different elements', () {
final iterable1 = [
[bob, 2],
[3, 4],
];
final iterable2 = [
[bob, 2],
[3, 5],
];
expect(iterableEquals(iterable1, iterable2), isFalse);
});
});

group('setEquals', () {
test('returns true for identical sets', () {
final set1 = {1, 2, 3};
final set2 = {1, 2, 3};
expect(setEquals(set1, set2), isTrue);
});

test(
'returns true for identical sets with elements in different order',
() {
final set1 = {1, 3, 2};
final set2 = {1, 2, 3};
expect(setEquals(set1, set2), isTrue);
},
);

test('returns false for sets of different lengths', () {
final set1 = {1, 2, 3};
final set2 = {1, 2};
expect(setEquals(set1, set2), isFalse);
});

test('returns false for sets with different elements', () {
final set1 = {1, 2, 3};
final set2 = {1, 2, 4};
expect(setEquals(set1, set2), isFalse);
});

test('uses == when props are equatable', () {
expect(setEquals({alice}, {aliceCopy}), isTrue);
expect(setEquals({bob}, {bob}), isTrue);
expect(setEquals({alice}, {bob}), isFalse);
expect(setEquals({bob}, {alice}), isFalse);
expect(setEquals({alice, null}, {alice, -1}), isFalse);
});

test('returns true for nested identical sets', () {
final set1 = {
{alice, bob},
{alice, bob},
};
final set2 = {
{alice, bob},
{alice, bob},
};
expect(setEquals(set1, set2), isTrue);
});

test('returns false for nested sets with different elements', () {
final set1 = {
{bob, 2},
{3, 4},
};
final set2 = {
{bob, 2},
{3, 5},
};
expect(setEquals(set1, set2), isFalse);
});

test('returns true for empty sets', () {
expect(setEquals({}, {}), isTrue);
});

test('returns false for sets with different types', () {
final set1 = {1, '2', 3};
final set2 = {1, 2, 3};
expect(setEquals(set1, set2), isFalse);
});
});

group('mapEquals', () {
test('returns true for identical maps', () {
final map1 = {'a': 1, 'b': 2, 'c': 3};
final map2 = {'a': 1, 'b': 2, 'c': 3};
expect(mapEquals(map1, map2), isTrue);
});

test(
'returns true for identical maps with elements in different order',
() {
final map1 = {'a': 1, 'c': 3, 'b': 2};
final map2 = {'a': 1, 'b': 2, 'c': 3};
expect(mapEquals(map1, map2), isTrue);
},
);

test('uses == when props are equatable', () {
expect(mapEquals({'a': alice}, {'a': aliceCopy}), isTrue);
expect(mapEquals({alice: 'a'}, {aliceCopy: 'a'}), isTrue);
expect(mapEquals({'a': bob}, {'a': bob}), isTrue);
});

test('returns false for maps of different lengths', () {
final map1 = {'a': 1, 'b': 2, 'c': 3};
final map2 = {'a': 1, 'b': 2};
expect(mapEquals(map1, map2), isFalse);
});

test('returns false for maps with different keys', () {
final map1 = {'a': 1, 'b': 2, 'c': 3};
final map2 = {'a': 1, 'b': 2, 'd': 3};
expect(mapEquals(map1, map2), isFalse);
});

test('returns false for maps with different values', () {
final map1 = {'a': 1, 'b': 2, 'c': 3};
final map2 = {'a': 1, 'b': 2, 'c': 4};
expect(mapEquals(map1, map2), isFalse);
});

test('returns true for nested identical maps', () {
final map1 = {
'a': {'x': 1, 'y': 2},
'b': {'z': 3},
};
final map2 = {
'a': {'x': 1, 'y': 2},
'b': {'z': 3},
};
expect(mapEquals(map1, map2), isTrue);
});

test('returns false for nested maps with different elements', () {
final map1 = {
'a': {'x': 1, 'y': 2},
'b': {'z': 3},
};
final map2 = {
'a': {'x': 1, 'y': 2},
'b': {'z': 4},
};
expect(mapEquals(map1, map2), isFalse);
});

test('returns true for empty maps', () {
expect(mapEquals({}, {}), isTrue);
});

test('returns false for maps with same keys but different values', () {
final map1 = {'a': 1, 'b': '2', 'c': 3};
final map2 = {'a': 1, 'b': 2, 'c': 3};
expect(mapEquals(map1, map2), isFalse);
});
});

group('objectsEquals', () {
test('returns true for identical objects', () {
final object = Object();
expect(objectsEquals(object, object), isTrue);
});

test('returns true for same objects', () {
const object1 = 'object';
// ignore: prefer_const_declarations
final object2 = 'object';
expect(objectsEquals(object1, object2), isTrue);
});

test('returns true for equatable objects', () {
expect(objectsEquals(alice, aliceCopy), isTrue);
expect(objectsEquals(bob, bob), isTrue);
});

test('returns false for different objects', () {
expect(objectsEquals(alice, bob), isFalse);
expect(objectsEquals(bob, alice), isFalse);
});

test('returns true for same lists', () {
expect(objectsEquals([1, 2, 3], [1, 2, 3]), isTrue);
});

test('returns true for same sets', () {
expect(objectsEquals({1, 2, 3}, {1, 2, 3}), isTrue);
expect(objectsEquals({1, 3, 2}, {1, 2, 3}), isTrue);
});

test('returns true for same maps', () {
expect(objectsEquals({'a': 1, 'b': 2}, {'a': 1, 'b': 2}), isTrue);
expect(objectsEquals({'c': 3, 'b': 2}, {'b': 2, 'c': 3}), isTrue);
});

test('returns false for different types', () {
expect(objectsEquals(1, '1'), isFalse);
});
});
}