Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 7 additions & 0 deletions packages/stream_core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## Upcoming

### ✨ Features

- Added `partition` method for splitting lists into two based on a filter condition
- Added `compare` parameter to `updateWhere` for optional sorting after updates

## 0.3.2

### ✨ Features
Expand Down
144 changes: 107 additions & 37 deletions packages/stream_core/lib/src/utils/list_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,6 @@ extension IterableExtensions<T extends Object> on Iterable<T> {
/// These extensions return new lists rather than modifying existing ones,
/// following immutable patterns for safer concurrent programming.
extension ListExtensions<T extends Object> on List<T> {
/// Updates all elements in the list that match the filter condition.
///
/// Returns a new list where elements matching [filter] are transformed
/// using the [update] function, while non-matching elements remain unchanged.
/// Time complexity: O(n) where n is the list length.
///
/// ```dart
/// final users = [
/// User(id: '1', name: 'Alice', active: true),
/// User(id: '2', name: 'Bob', active: false),
/// User(id: '3', name: 'Charlie', active: true),
/// ];
///
/// // Update all active users
/// final updated = users.updateWhere(
/// (user) => user.active,
/// update: (user) => user.copyWith(lastSeen: DateTime.now()),
/// );
/// // Result: Active users have updated lastSeen, inactive users unchanged
///
/// // Update users by name
/// final renamed = users.updateWhere(
/// (user) => user.name == 'Bob',
/// update: (user) => user.copyWith(name: 'Robert'),
/// );
/// // Result: [User(name: 'Alice'), User(name: 'Robert'), User(name: 'Charlie')]
/// ```
List<T> updateWhere(
bool Function(T item) filter, {
required T Function(T original) update,
}) {
return map((it) => filter(it) ? update(it) : it).toList();
}

/// Inserts or replaces an element in the list based on a key.
///
/// If an element with the same key already exists, it will be replaced.
Expand Down Expand Up @@ -170,13 +136,117 @@ extension ListExtensions<T extends Object> on List<T> {

return result;
}

/// Splits the list into two lists based on a filter condition.
///
/// Returns a record where the first list contains elements that match the
/// filter (return true), and the second list contains elements that don't
/// match (return false). This is useful for separating elements into two
/// groups in a single pass. Time complexity: O(n).
///
/// ```dart
/// final numbers = [1, 2, 3, 4, 5, 6];
/// final (even, odd) = numbers.partition((n) => n.isEven);
/// // even: [2, 4, 6]
/// // odd: [1, 3, 5]
///
/// final users = [
/// User(id: '1', name: 'Alice', active: true),
/// User(id: '2', name: 'Bob', active: false),
/// User(id: '3', name: 'Charlie', active: true),
/// User(id: '4', name: 'David', active: false),
/// ];
///
/// // Separate active and inactive users
/// final (active, inactive) = users.partition((user) => user.active);
/// // active: [User(name: 'Alice'), User(name: 'Charlie')]
/// // inactive: [User(name: 'Bob'), User(name: 'David')]
///
/// // Partition by score threshold
/// final scores = [
/// Score(userId: '1', points: 150),
/// Score(userId: '2', points: 80),
/// Score(userId: '3', points: 200),
/// Score(userId: '4', points: 45),
/// ];
/// final (high, low) = scores.partition((score) => score.points >= 100);
/// // high: [Score(points: 150), Score(points: 200)]
/// // low: [Score(points: 80), Score(points: 45)]
/// ```
(List<T>, List<T>) partition(bool Function(T element) test) {
final matching = <T>[];
final notMatching = <T>[];

for (final element in this) {
if (test(element)) {
matching.add(element);
} else {
notMatching.add(element);
}
}

return (matching, notMatching);
}
}

/// Extensions for operations on sorted lists.
/// Extensions for list operations that provide optional or required sorting.
///
/// These extensions maintain list order and provide efficient operations
/// for sorted collections using binary search algorithms where applicable.
/// These extensions return new lists with optional or required sorting capabilities.
/// Some methods like [sortedInsert] and [sortedUpsert] use binary search algorithms
/// for efficient insertion into already-sorted lists, while others like [updateWhere]
/// and [merge] provide optional sorting as a convenience.
extension SortedListExtensions<T extends Object> on List<T> {
/// Updates all elements in the list that match the filter condition.
///
/// Returns a new list where elements matching [filter] are transformed
/// using the [update] function, while non-matching elements remain unchanged.
/// Optionally sorts the result if a [compare] function is provided.
/// Time complexity: O(n) where n is the list length, or O(n log n) if sorting.
///
/// ```dart
/// final users = [
/// User(id: '1', name: 'Alice', active: true),
/// User(id: '2', name: 'Bob', active: false),
/// User(id: '3', name: 'Charlie', active: true),
/// ];
///
/// // Update all active users
/// final updated = users.updateWhere(
/// (user) => user.active,
/// update: (user) => user.copyWith(lastSeen: DateTime.now()),
/// );
/// // Result: Active users have updated lastSeen, inactive users unchanged
///
/// // Update users by name
/// final renamed = users.updateWhere(
/// (user) => user.name == 'Bob',
/// update: (user) => user.copyWith(name: 'Robert'),
/// );
/// // Result: [User(name: 'Alice'), User(name: 'Robert'), User(name: 'Charlie')]
///
/// // Update and sort by score
/// final scores = [
/// Score(userId: '1', points: 100),
/// Score(userId: '2', points: 200),
/// Score(userId: '3', points: 150),
/// ];
/// final boosted = scores.updateWhere(
/// (score) => score.userId == '1',
/// update: (score) => score.copyWith(points: score.points + 150),
/// compare: (a, b) => b.points.compareTo(a.points), // Sort descending
/// );
/// // Result: [Score(userId: '1', points: 250), Score(userId: '2', points: 200), Score(userId: '3', points: 150)]
/// ```
List<T> updateWhere(
bool Function(T item) filter, {
required T Function(T original) update,
Comparator<T>? compare,
}) {
final iterable = map((it) => filter(it) ? update(it) : it);
if (compare != null) return iterable.sorted(compare);
return iterable.toList();
}

/// Inserts an element into the list, ensuring uniqueness by key.
///
/// Removes any existing element with the same key and appends the new element
Expand Down
105 changes: 105 additions & 0 deletions packages/stream_core/test/query/list_extensions_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,48 @@ void main() {
expect(result[2].points, 200); // Updated (150 + 50)
expect(result[3].points, 250); // Unchanged
});

test('should sort list after update when compare is provided', () {
final scores = [
const _TestScore(userId: 1, points: 100),
const _TestScore(userId: 2, points: 200),
const _TestScore(userId: 3, points: 150),
];

final result = scores.updateWhere(
(score) => score.userId == 1,
update: (score) => _TestScore(userId: score.userId, points: 250),
compare: (a, b) => b.points.compareTo(a.points), // Descending
);

expect(result.length, 3);
expect(result[0].points, 250); // Updated and now first
expect(result[1].points, 200);
expect(result[2].points, 150);
});

test('should sort multiple updated elements when compare is provided',
() {
final users = [
const _TestUser(id: '1', name: 'Alice'),
const _TestUser(id: '2', name: 'Bob'),
const _TestUser(id: '3', name: 'Charlie'),
const _TestUser(id: '4', name: 'David'),
];

final result = users.updateWhere(
(user) => user.id == '2' || user.id == '4',
update: (user) =>
_TestUser(id: user.id, name: 'Z${user.name}'), // Prefix with Z
compare: (a, b) => a.name.compareTo(b.name), // Ascending by name
);

expect(result.length, 4);
expect(result[0].name, 'Alice');
expect(result[1].name, 'Charlie');
expect(result[2].name, 'ZBob');
expect(result[3].name, 'ZDavid');
});
});

group('batchReplace', () {
Expand Down Expand Up @@ -605,6 +647,53 @@ void main() {
expect(result[2].content, 'Test Updated');
});
});

group('partition', () {
test('should split list into two lists based on filter', () {
final numbers = [1, 2, 3, 4, 5, 6];

final (even, odd) = numbers.partition((n) => n.isEven);

expect(even, [2, 4, 6]);
expect(odd, [1, 3, 5]);
// Original list should be unchanged
expect(numbers, [1, 2, 3, 4, 5, 6]);
});

test('should handle empty list', () {
final users = <_TestUser>[];

final (matching, notMatching) = users.partition((it) => it.id == '1');

expect(matching, isEmpty);
expect(notMatching, isEmpty);
});

test('should handle all or no elements matching filter', () {
final allMatch = [2, 4, 6, 8];
final (even1, odd1) = allMatch.partition((n) => n.isEven);
expect(even1, [2, 4, 6, 8]);
expect(odd1, isEmpty);

final noneMatch = [1, 3, 5, 7];
final (even2, odd2) = noneMatch.partition((n) => n.isEven);
expect(even2, isEmpty);
expect(odd2, [1, 3, 5, 7]);
});

test('should work with complex objects', () {
final users = [
const _TestUserWithStatus(id: '1', name: 'Alice', active: true),
const _TestUserWithStatus(id: '2', name: 'Bob', active: false),
const _TestUserWithStatus(id: '3', name: 'Charlie', active: true),
];

final (active, inactive) = users.partition((user) => user.active);

expect(active.map((u) => u.name), ['Alice', 'Charlie']);
expect(inactive.map((u) => u.name), ['Bob']);
});
});
});

group('SortedListExtensions', () {
Expand Down Expand Up @@ -2016,6 +2105,22 @@ class _TestUser extends Equatable {
List<Object?> get props => [id, name];
}

/// Test model representing a user with active status.
class _TestUserWithStatus extends Equatable {
const _TestUserWithStatus({
required this.id,
required this.name,
required this.active,
});

final String id;
final String name;
final bool active;

@override
List<Object?> get props => [id, name, active];
}

/// Test model representing a score with user ID and points.
class _TestScore extends Equatable {
const _TestScore({required this.userId, required this.points});
Expand Down