- Added List.equals extension
This commit is contained in:
Hans Kokx
2025-04-15 15:26:43 +02:00
parent c8c30d3838
commit 5b402763d1
5 changed files with 304 additions and 6 deletions
+5
View File
@@ -1,3 +1,8 @@
## 1.4.1
- Added a `List` equality extension, `equals`.
- Fixed an issue with the `List` extension `unique` that may have caused null-safety issues.
## 1.4.0
- [BREAKING] JWT-related extensions have been reworked.
+40 -2
View File
@@ -17,7 +17,10 @@ providing utility functions and extensions that simplify common tasks.
common transformations and checks.
- **JWT Utilities**: Provides getters to parse a JWT token from a `String`, then
get common properties from it.
- **List Extensions**: Adds a new `unique` operator for filtering `List` items.
- **List Extensions**: Adds a new `unique` operator for filtering `List` items,
as well as getters for `isNullOrEmpty`, `isEmptyOrNull`, `isNotNullOrEmpty`,
and `isNotEmptyOrNull`. Furthermore, an `equals` extension has been introduced
which can be used to compare two lists.
## Getting Started
@@ -80,7 +83,7 @@ converted from a `String?` to an `int?`:
```dart
@freezed
class MyFreezedClass with _$MyFreezedClass {
abstract class MyFreezedClass with _$MyFreezedClass {
const factory MyFreezedClass({
@DecimalConverter() double? valueIsMaybeNull,
@DecimalConverter() double? valueIsDouble,
@@ -411,6 +414,41 @@ The following extensions have been added to the `List` object:
print(nullList.isNullOrEmpty); // Output: true
```
- `equals`: Compares two lists to see if they are equal.
```dart
List<int?>? list1 = [1, 2, null, 4];
List<int?>? list2 = [1, 2, null, 4];
List<int?>? list3 = [1, 2, 3, 4];
List<int?>? list4 = null;
List<int?>? list5 = [1, 2, 3, null];
print(list1.equals(list2)); // Output: true
print(list1.equals(list3)); // Output: false
print(list1.equals(list4)); // Output: false
print(list4.equals(null)); // Output: true
print(list5.equals([1,2,3,null])); // Output: true
// Example with ignoreSorting:
List<int>? list6 = [1, 2, 3];
List<int>? list7 = [3, 1, 2];
// Output: true (order doesn't matter)
print(list6.equals(list7, ignoreSorting: true));
// Output: false (order matters)
print(list6.equals(list7, ignoreSorting: false));
List<String>? list8 = ["apple", "banana", "cherry"];
List<String>? list9 = ["cherry", "apple", "banana"];
// Output: true
print(list8.equals(list9, ignoreSorting: true));
// Output: false
print(list8.equals(list9, ignoreSorting: false));
```
## Contributing
Contributions are welcome! Feel free to fork the repository and submit pull
+112 -3
View File
@@ -37,10 +37,10 @@ extension Unique<E, Id> on List<E> {
/// print(uniquePeople.map((p) => p.name)); // Output: ['Alice', 'Bob']
/// ```
///
List<E> unique([Id Function(E element)? id, bool inplace = true]) {
final Set ids = {};
List<E> unique([Object? Function(E element)? id, bool inplace = true]) {
final Set<Object?> ids = {};
final List<E> list = inplace ? this : List<E>.from(this);
list.retainWhere((x) => ids.add(id != null ? id(x) : x as Id));
list.retainWhere((x) => ids.add(id != null ? id(x) : x));
return list;
}
}
@@ -122,3 +122,112 @@ extension ListNullability on List? {
/// This is identical to [isEmptyOrNull].
bool get isEmptyOrNull => isNullOrEmpty;
}
/// Extension on nullable lists of nullable elements to provide a custom equality check.
extension ListEquality<T> on List<T?>? {
/// Checks if this list is equal to another list.
///
/// Two lists are considered equal if:
/// - Both are null, or
/// - Both have the same length, and
/// - Elements at the same index are equal.
///
/// Nullable list elements are handled as follows:
/// - If both elements at a given index are null, they are considered equal.
/// - If one element is null and the other is not, they are considered unequal.
///
/// The type parameter `T` represents the type of elements in the list.
/// The elements can be nullable (`T?`).
///
/// Example:
///
/// ```dart
/// List<int?>? list1 = [1, 2, null, 4];
/// List<int?>? list2 = [1, 2, null, 4];
/// List<int?>? list3 = [1, 2, 3, 4];
/// List<int?>? list4 = null;
/// List<int?>? list5 = [1, 2, 3, null];
///
/// print(list1.equals(list2)); // Output: true
/// print(list1.equals(list3)); // Output: false
/// print(list1.equals(list4)); // Output: false
/// print(list4.equals(null)); // Output: true
/// print(list5.equals([1,2,3,null])); //Output: true
///
/// // Example with ignoreSorting:
/// List<int>? list6 = [1, 2, 3];
/// List<int>? list7 = [3, 1, 2];
/// print(list6.equals(list7, ignoreSorting: true)); // Output: true (order doesn't matter)
/// print(list6.equals(list7, ignoreSorting: false)); // Output: false (order matters)
///
/// List<String>? list8 = ["apple", "banana", "cherry"];
/// List<String>? list9 = ["cherry", "apple", "banana"];
/// print(list8.equals(list9, ignoreSorting: true)); // Output: true
/// print(list8.equals(list9, ignoreSorting: false)); // Output: false
/// ```
///
/// Returns `true` if the lists are equal, `false` otherwise.
///
/// The [ignoreSorting] parameter, if set to true, will compare the lists
/// after sorting them. This defaults to false.
bool equals(
List<T?>? a, {
bool ignoreSorting = false,
}) {
if (this == null) return a == null;
if (a == null || this?.length != a.length) return false;
if (this.runtimeType != a.runtimeType) return false;
if (ignoreSorting) {
// Create copies to avoid modifying the original lists.
final List<T?> sortedThis = this?.toList() ?? [];
final List<T?> sortedA = a.toList();
// If the lists contain non-comparable elements, we can't rely on sorting to determine equality.
if (T is! Comparable) {
// Instead, we check if the lists contain the same elements, regardless of order.
// This is an O(n^2) operation, but it's the only reliable way to compare non-comparable lists.
for (final itemThis in sortedThis) {
final int indexA = sortedA.indexOf(itemThis);
if (indexA == -1) {
// itemThis is not in sortedA, so the lists are not equal.
return false;
}
// Remove the item from sortedA to avoid double-counting.
sortedA.removeAt(indexA);
}
// If sortedA is empty, all elements in sortedThis were found in sortedA.
return sortedA.isEmpty;
}
// Sort if comparable
sortedThis.sort((a, b) {
if (a == null && b == null) return 0;
if (a == null) return -1;
if (b == null) return 1;
return (a as Comparable).compareTo(b as Comparable);
});
sortedA.sort((a, b) {
if (a == null && b == null) return 0;
if (a == null) return -1;
if (b == null) return 1;
return (a as Comparable).compareTo(b as Comparable);
});
// Compare sorted lists
for (int i = 0; i < sortedThis.length; i++) {
if (sortedThis[i] != sortedA[i]) {
return false;
}
}
return true;
} else {
// Original comparison
for (int i = 0; i < (this?.length ?? 0); i++) {
if (this?[i] != a[i]) {
return false;
}
}
return true;
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
name: arcane_helper_utils
description: Provides a variety of helpful utilities and extensions for Flutter
and Dart.
version: 1.4.0
version: 1.4.1
repository: https://github.com/hanskokx/arcane_helper_utils
issue_tracker: https://github.com/hanskokx/arcane_helper_utils/issues
+146
View File
@@ -63,4 +63,150 @@ void main() {
expect(nonEmptyList.isNullOrEmpty, !nonEmptyList.isNotNullOrEmpty);
});
});
group("ListEquality", () {
// Helper function to make tests more concise
void testEquality<T>(
List<T?>? list1,
List<T?>? list2,
bool expected, {
bool ignoreSorting = false,
}) {
expect(
list1.equals(list2, ignoreSorting: ignoreSorting),
expected,
reason:
'Expected ${list1?.toString()} and ${list2?.toString()} to be ${expected ? "equal" : "unequal"} when ignoreSorting is $ignoreSorting',
);
}
test("Both lists are null", () {
testEquality(null, null, true);
});
test("One list is null, the other is not", () {
testEquality([1, 2, 3], null, false);
testEquality(null, [1, 2, 3], false);
});
test("Lists have different lengths", () {
testEquality([1, 2, 3], [1, 2], false);
testEquality([1, 2], [1, 2, 3], false);
});
test("Lists have the same elements in the same order", () {
testEquality([1, 2, 3], [1, 2, 3], true);
testEquality(["a", "b", "c"], ["a", "b", "c"], true);
testEquality([true, false, true], [true, false, true], true);
});
test(
"Lists have the same elements in a different order - without ignoreSorting",
() {
testEquality([1, 2, 3], [3, 2, 1], false);
});
test(
"Lists have the same elements in a different order - with ignoreSorting",
() {
testEquality([1, 2, 3], [3, 2, 1], true, ignoreSorting: true);
});
test("Lists have different elements", () {
testEquality([1, 2, 3], [1, 2, 4], false);
testEquality(["a", "b", "c"], ["a", "b", "d"], false);
});
test("Lists with null elements - both null at same index", () {
testEquality([1, null, 3], [1, null, 3], true);
});
test("Lists with null elements - one null, one not null at same index", () {
testEquality([1, null, 3], [1, 2, 3], false);
testEquality([1, 2, 3], [1, null, 3], false);
});
test("Lists with multiple null elements", () {
testEquality([null, null, null], [null, null, null], true);
testEquality([null, 1, null], [null, 1, null], true);
testEquality([null, 1, null], [1, null, 1], false);
});
test("Empty lists", () {
testEquality([], [], true);
});
test("List of different types", () {
testEquality(<int?>[], <String?>[], false);
});
test("Lists of comparable items with different order - ignoreSorting true",
() {
testEquality([3, 1, 2], [1, 2, 3], true, ignoreSorting: true);
testEquality(
["c", "a", "b"],
["b", "c", "a"],
true,
ignoreSorting: true,
);
});
test("Lists of comparable items with different order - ignoreSorting false",
() {
testEquality([3, 1, 2], [1, 2, 3], false, ignoreSorting: false);
testEquality(
["c", "a", "b"],
["b", "c", "a"],
false,
ignoreSorting: false,
);
});
test("Lists with nulls and different order - ignoreSorting true", () {
testEquality([null, 3, 1, 2], [1, 2, 3, null], true, ignoreSorting: true);
testEquality([3, 1, 2, null], [null, 1, 2, 3], true, ignoreSorting: true);
});
test("Lists with nulls and different order - ignoreSorting false", () {
testEquality(
[null, 3, 1, 2],
[1, 2, 3, null],
false,
ignoreSorting: false,
);
});
test(
"Lists of non-comparable items with different order - ignoreSorting true",
() {
final List<NonComparable?> list1 = [
NonComparable(1),
NonComparable(2),
NonComparable(3),
];
final List<NonComparable?> list2 = [
NonComparable(3),
NonComparable(1),
NonComparable(2),
];
testEquality(list1, list2, true, ignoreSorting: true);
});
});
}
class NonComparable {
final int value;
NonComparable(this.value);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NonComparable &&
runtimeType == other.runtimeType &&
value == other.value;
@override
int get hashCode => value.hashCode;
@override
String toString() => "NonComparable($value)";
}