Skip to content

Commit 25ecdbc

Browse files
authored
feat(llc): Add insertAt to upsert for controlling insertion position (#21)
1 parent 296d4b8 commit 25ecdbc

File tree

3 files changed

+142
-3
lines changed

3 files changed

+142
-3
lines changed

packages/stream_core/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Upcoming
2+
3+
### ✨ Features
4+
5+
- Added `insertAt` parameter to `upsert` for controlling insertion position of new elements
6+
17
## 0.3.1
28

39
### ✨ Features

packages/stream_core/lib/src/utils/list_extensions.dart

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ extension ListExtensions<T extends Object> on List<T> {
5353
/// Inserts or replaces an element in the list based on a key.
5454
///
5555
/// If an element with the same key already exists, it will be replaced.
56-
/// Otherwise, the new element will be appended to the end of the list.
56+
/// Otherwise, the new element will be inserted at the position determined by
57+
/// [insertAt] (defaults to appending at the end).
5758
/// Time complexity: O(n) for search, O(n) for list creation.
5859
///
5960
/// ```dart
@@ -64,23 +65,37 @@ extension ListExtensions<T extends Object> on List<T> {
6465
/// );
6566
/// // Result: [User(id: '1', name: 'Alice Updated'), User(id: '2', name: 'Bob')]
6667
///
67-
/// // Adding new element
68+
/// // Adding new element (appends to end)
6869
/// final withNew = users.upsert(
6970
/// User(id: '3', name: 'Charlie'),
7071
/// key: (user) => user.id,
7172
/// );
7273
/// // Result: [User(id: '1', name: 'Alice'), User(id: '2', name: 'Bob'), User(id: '3', name: 'Charlie')]
74+
///
75+
/// // Insert at specific position
76+
/// final inserted = users.upsert(
77+
/// User(id: '3', name: 'Charlie'),
78+
/// key: (user) => user.id,
79+
/// insertAt: (list) => 0, // Insert at beginning
80+
/// );
81+
/// // Result: [User(id: '3', name: 'Charlie'), User(id: '1', name: 'Alice'), User(id: '2', name: 'Bob')]
7382
/// ```
7483
List<T> upsert<K>(
7584
T element, {
7685
required K Function(T item) key,
86+
int Function(List<T> list)? insertAt,
7787
T Function(T original, T updated)? update,
7888
}) {
7989
final elementKey = key(element);
8090
final index = indexWhere((e) => key(e) == elementKey);
8191

8292
// Add the element if it does not exist
83-
if (index == -1) return [...this, element];
93+
if (index == -1) {
94+
final insertionIndex = insertAt?.call(this) ?? length;
95+
// Clamp index to valid range [0, length]
96+
final validIndex = insertionIndex.clamp(0, length);
97+
return [...this].apply((it) => it.insert(validIndex, element));
98+
}
8499

85100
T handleUpdate(T original, T updated) {
86101
if (update != null) return update(original, updated);

packages/stream_core/test/query/list_extensions_test.dart

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,124 @@ void main() {
135135
expect(result.first.content, 'Hello Updated');
136136
expect(result.last.content, 'World');
137137
});
138+
139+
test('should insert at beginning when insertAt returns 0', () {
140+
final users = [
141+
const _TestUser(id: '1', name: 'Alice'),
142+
const _TestUser(id: '2', name: 'Bob'),
143+
];
144+
145+
final result = users.upsert(
146+
const _TestUser(id: '3', name: 'Charlie'),
147+
key: (user) => user.id,
148+
insertAt: (list) => 0,
149+
);
150+
151+
expect(result.length, 3);
152+
expect(result[0].name, 'Charlie');
153+
expect(result[1].name, 'Alice');
154+
expect(result[2].name, 'Bob');
155+
});
156+
157+
test('should insert at middle position when insertAt specifies', () {
158+
final users = [
159+
const _TestUser(id: '1', name: 'Alice'),
160+
const _TestUser(id: '2', name: 'Bob'),
161+
const _TestUser(id: '3', name: 'Charlie'),
162+
];
163+
164+
final result = users.upsert(
165+
const _TestUser(id: '4', name: 'David'),
166+
key: (user) => user.id,
167+
insertAt: (list) => 1,
168+
);
169+
170+
expect(result.length, 4);
171+
expect(result[0].name, 'Alice');
172+
expect(result[1].name, 'David');
173+
expect(result[2].name, 'Bob');
174+
expect(result[3].name, 'Charlie');
175+
});
176+
177+
test('should clamp insertAt index to valid range', () {
178+
final users = [
179+
const _TestUser(id: '1', name: 'Alice'),
180+
const _TestUser(id: '2', name: 'Bob'),
181+
];
182+
183+
// Index too large
184+
final resultLarge = users.upsert(
185+
const _TestUser(id: '3', name: 'Charlie'),
186+
key: (user) => user.id,
187+
insertAt: (list) => 100, // Way beyond list length
188+
);
189+
190+
expect(resultLarge.length, 3);
191+
expect(resultLarge.last.name, 'Charlie'); // Should be clamped to end
192+
193+
// Negative index
194+
final resultNegative = users.upsert(
195+
const _TestUser(id: '4', name: 'David'),
196+
key: (user) => user.id,
197+
insertAt: (list) => -5,
198+
);
199+
200+
expect(resultNegative.length, 3);
201+
expect(
202+
resultNegative.first.name,
203+
'David',
204+
); // Should be clamped to start
205+
});
206+
207+
test('should use insertAt with list information', () {
208+
final scores = [
209+
const _TestScore(userId: 1, points: 100),
210+
const _TestScore(userId: 2, points: 200),
211+
const _TestScore(userId: 3, points: 150),
212+
];
213+
214+
// Insert at position based on list length
215+
final result = scores.upsert(
216+
const _TestScore(userId: 4, points: 175),
217+
key: (score) => score.userId,
218+
insertAt: (list) => list.length ~/ 2, // Insert at middle
219+
);
220+
221+
expect(result.length, 4);
222+
expect(result[1].userId, 4); // Inserted at index 1 (3 ~/ 2 = 1)
223+
});
224+
225+
test('should not use insertAt when replacing existing element', () {
226+
final users = [
227+
const _TestUser(id: '1', name: 'Alice'),
228+
const _TestUser(id: '2', name: 'Bob'),
229+
const _TestUser(id: '3', name: 'Charlie'),
230+
];
231+
232+
final result = users.upsert(
233+
const _TestUser(id: '2', name: 'Bob Updated'),
234+
key: (user) => user.id,
235+
insertAt: (list) => 0, // Should be ignored when replacing
236+
);
237+
238+
expect(result.length, 3);
239+
expect(result[0].name, 'Alice');
240+
expect(result[1].name, 'Bob Updated'); // Replaced in place
241+
expect(result[2].name, 'Charlie');
242+
});
243+
244+
test('should work with insertAt on empty list', () {
245+
final users = <_TestUser>[];
246+
247+
final result = users.upsert(
248+
const _TestUser(id: '1', name: 'Alice'),
249+
key: (user) => user.id,
250+
insertAt: (list) => 0,
251+
);
252+
253+
expect(result.length, 1);
254+
expect(result.first.name, 'Alice');
255+
});
138256
});
139257

140258
group('updateWhere', () {

0 commit comments

Comments
 (0)