Skip to content

Commit 56ffa23

Browse files
committed
Add a zipLongest function and padLeft/padRight extensions on Iterable
Add a `zipLongest` function that is like `zip` from `package:quiver` (or like `IterableZip` from `package:collection`) but that stops when the longest `Iterable` is exhausted instead of the shortest. My initial implementation involved padding the input `Iterables` to have equal lengths and leveraging `zip` or `IterableZip`, leading me to add an `Iterable.padRight` extension method (and an `padLeft` one for symmetry). However, I changed my mind because I wanted to support `Iterable`s of infinite length. Meanwhile, `padLeft` and `padRight` might be useful for other cases, so I'm leaving them in.
1 parent d223253 commit 56ffa23

File tree

4 files changed

+322
-6
lines changed

4 files changed

+322
-6
lines changed

CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
## 0.9.0
22

3-
* Enabled lints introduced by Dart 3.4.0.
3+
* Require Dart 3.4.0 and enable new lints.
4+
5+
* Added `padLeft` and `padRight` extension methods on `Iterable`.
6+
7+
* Added a `zipLongest` function.
48

59
* Replaced the `identityType<T>()` function with an `IdentityType<T>`
610
`typedef`.

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ rain
2828
* `Iterable.startsWith` returns whether one `Iterable` starts with the same
2929
sequence of elements as another `Iterable`.
3030

31+
* `Iterable.padLeft` and `Iterable.padRight` add elements to an `Iterable` to
32+
have a specified length.
33+
34+
* `zipLongest` is a version of [`zip`] that stops only after the longest
35+
`Iterable` is exhausted instead of the shortest.
36+
3137
* `LinkedHashMap.sort`.
3238

3339
* `mergeMaps`s combines an `Iterable` of `Map`s into a single `Map`.
@@ -168,3 +174,4 @@ rain
168174
[readable_numbers]: https://pub.dev/documentation/dartbag/latest/readable_numbers/readable_numbers-library.html
169175
[timer]: https://pub.dev/documentation/dartbag/latest/timer/timer-library.html
170176
[tty]: https://pub.dev/documentation/dartbag/latest/tty/tty-library.html
177+
[zip]: https://pub.dev/documentation/quiver/latest/quiver.iterables/zip.html

lib/collection.dart

+75
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,47 @@ Iterable<T> flattenDeep<T>(Iterable<Object?> list) sync* {
1919
}
2020
}
2121

22+
/// Like [`zip`] or [`IterableZip`] except that `zip_longest` stops only after
23+
/// the longest `Iterable` is exhausted instead of the shortest.
24+
///
25+
/// All [Iterable]s shorter than the longest [Iterable] will be padded with
26+
/// [fillValue].
27+
///
28+
/// If any of the input [Iterable]s is infinitely long, the returned [Iterable]
29+
/// also will be infinitely long.
30+
///
31+
/// [zip]: https://pub.dev/documentation/quiver/latest/quiver.iterables/zip.html
32+
///
33+
// N.B.: It'd be nice if we could leverage [zip] or [IterableZip] by using the
34+
// [Iterable.padRight] extension method, but doing so would require determining
35+
// the length of the input [Iterable]s, which cannot work on ones that are
36+
// infinitely long.
37+
Iterable<List<E>> zipLongest<E>(
38+
Iterable<Iterable<E>> iterables,
39+
E fillValue,
40+
) sync* {
41+
var iterators = [for (var iterable in iterables) iterable.iterator];
42+
43+
while (true) {
44+
var exhaustedCount = 0;
45+
var current = <E>[];
46+
for (var i = 0; i < iterators.length; i += 1) {
47+
if (iterators[i].moveNext()) {
48+
current.add(iterators[i].current);
49+
continue;
50+
}
51+
52+
exhaustedCount += 1;
53+
if (exhaustedCount == iterators.length) {
54+
return;
55+
}
56+
current.add(fillValue);
57+
}
58+
59+
yield current;
60+
}
61+
}
62+
2263
/// Extension methods on [List] that do work in-place.
2364
extension InPlaceOperations<E> on List<E> {
2465
/// Reverses the [List] in-place.
@@ -137,6 +178,40 @@ extension IterableUtils<E> on Iterable<E> {
137178
}
138179
}
139180
}
181+
182+
/// Pads elements to the beginning of this [Iterable] to make it have the
183+
/// specified length.
184+
///
185+
/// If the [length] of this [Iterable] is already [totalLength] or greater,
186+
/// returns this [Iterable].
187+
Iterable<E> padLeft(int totalLength, {required E padValue}) {
188+
if (totalLength <= length) {
189+
return this;
190+
}
191+
192+
return Iterable<E>.generate(
193+
totalLength - length,
194+
(_) => padValue,
195+
).followedBy(this);
196+
}
197+
198+
/// Pads elements to the end of this [Iterable] to make it have the specified
199+
/// length.
200+
///
201+
/// If the [length] of this [Iterable] is already [totalLength] or greater,
202+
/// returns this [Iterable].
203+
Iterable<E> padRight(int totalLength, {required E padValue}) {
204+
if (totalLength <= length) {
205+
return this;
206+
}
207+
208+
return followedBy(
209+
Iterable<E>.generate(
210+
totalLength - length,
211+
(_) => padValue,
212+
),
213+
);
214+
}
140215
}
141216

142217
/// Provides a [sort] extension method on [LinkedHashMap].

test/collection_test.dart

+235-5
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,22 @@ import 'package:dartbag/readable_numbers.dart';
77
import 'package:test/test.dart';
88

99
extension<E> on List<E> {
10-
void rotateLeftSlow(
10+
List<E> rotateLeftCopy(
1111
int shiftAmount, {
1212
int? start,
1313
int? end,
1414
}) {
15-
for (var i = 0; i < shiftAmount; i += 1) {
16-
rotateLeft(1, start: start, end: end);
15+
var copy = toList();
16+
17+
start ??= 0;
18+
end ??= length;
19+
20+
var rotatedLength = end - start;
21+
for (var i = 0; i < rotatedLength; i += 1) {
22+
copy[i + start] = this[(i + shiftAmount) % rotatedLength + start];
1723
}
24+
25+
return copy;
1826
}
1927
}
2028

@@ -102,7 +110,7 @@ void main() {
102110
for (var i = 0; i < list.length; i += 1) {
103111
expect(
104112
[...list]..rotateLeft(i),
105-
[...list]..rotateLeftSlow(i),
113+
[...list].rotateLeftCopy(i),
106114
reason: 'shiftAmount: $i',
107115
);
108116
}
@@ -123,7 +131,7 @@ void main() {
123131
for (var i = 0; i < list.length; i += 1) {
124132
expect(
125133
[...list]..rotateLeft(-i),
126-
[...list]..rotateLeft(list.length - i),
134+
[...list].rotateLeftCopy(-i),
127135
reason: 'shiftAmount: -$i',
128136
);
129137
}
@@ -293,6 +301,219 @@ void main() {
293301
});
294302
});
295303

304+
test('Iterable.padLeft:', () {
305+
const padValue = -48;
306+
307+
expect(<int>[].padLeft(0, padValue: padValue).toList(), <int>[]);
308+
expect(<int>[].padLeft(1, padValue: padValue).toList(), [padValue]);
309+
expect(
310+
<int>[].padLeft(2, padValue: padValue).toList(),
311+
[padValue, padValue],
312+
);
313+
expect(<int>[].padLeft(2, padValue: padValue).length, 2);
314+
expect([0].padLeft(0, padValue: padValue).toList(), [0]);
315+
expect([0].padLeft(1, padValue: padValue).toList(), [0]);
316+
expect([0].padLeft(2, padValue: padValue).toList(), [padValue, 0]);
317+
expect(
318+
[0].padLeft(3, padValue: padValue).toList(),
319+
[padValue, padValue, 0],
320+
);
321+
expect([0, 1].padLeft(0, padValue: padValue).toList(), [0, 1]);
322+
expect([0, 1].padLeft(1, padValue: padValue).toList(), [0, 1]);
323+
expect([0, 1].padLeft(2, padValue: padValue).toList(), [0, 1]);
324+
expect([0, 1].padLeft(3, padValue: padValue).toList(), [padValue, 0, 1]);
325+
expect([0, 1].padLeft(3, padValue: padValue).length, 3);
326+
});
327+
328+
test('Iterable.padRight:', () {
329+
const padValue = -48;
330+
331+
expect(<int>[].padRight(0, padValue: 0).toList(), <int>[]);
332+
expect(<int>[].padRight(1, padValue: padValue).toList(), [padValue]);
333+
expect(
334+
<int>[].padRight(2, padValue: padValue).toList(),
335+
[padValue, padValue],
336+
);
337+
expect(<int>[].padRight(2, padValue: padValue).length, 2);
338+
expect([0].padRight(0, padValue: padValue).toList(), [0]);
339+
expect([0].padRight(1, padValue: padValue).toList(), [0]);
340+
expect([0].padRight(2, padValue: padValue).toList(), [0, padValue]);
341+
expect(
342+
[0].padRight(3, padValue: padValue).toList(),
343+
[0, padValue, padValue],
344+
);
345+
expect([0, 1].padRight(0, padValue: padValue).toList(), [0, 1]);
346+
expect([0, 1].padRight(1, padValue: padValue).toList(), [0, 1]);
347+
expect([0, 1].padRight(2, padValue: padValue).toList(), [0, 1]);
348+
expect([0, 1].padRight(3, padValue: padValue).toList(), [0, 1, padValue]);
349+
expect([0, 1].padRight(3, padValue: padValue).length, 3);
350+
});
351+
352+
group('zipLongest', () {
353+
const padValue = -48;
354+
355+
test('Works with finite iterables', () {
356+
expect(zipLongest([<int>[]], padValue).toList(), <List<int>>[]);
357+
expect(
358+
zipLongest(
359+
[
360+
<int>[],
361+
[1],
362+
],
363+
padValue,
364+
).toList(),
365+
[
366+
[padValue, 1],
367+
]);
368+
expect(
369+
zipLongest(
370+
[
371+
[1],
372+
<int>[],
373+
],
374+
padValue,
375+
).toList(),
376+
[
377+
[1, padValue],
378+
]);
379+
expect(
380+
zipLongest(
381+
[
382+
<int>[],
383+
[1],
384+
<int>[],
385+
],
386+
padValue,
387+
).toList(),
388+
[
389+
[padValue, 1, padValue],
390+
]);
391+
392+
expect(
393+
zipLongest(
394+
[
395+
[1, 2, 3],
396+
],
397+
padValue,
398+
).toList(),
399+
[
400+
[1],
401+
[2],
402+
[3],
403+
]);
404+
expect(
405+
zipLongest(
406+
[
407+
<int>[],
408+
[1, 2, 3],
409+
],
410+
padValue,
411+
).toList(),
412+
[
413+
[padValue, 1],
414+
[padValue, 2],
415+
[padValue, 3],
416+
]);
417+
expect(
418+
zipLongest(
419+
[
420+
[1, 2, 3],
421+
<int>[],
422+
],
423+
padValue,
424+
).toList(),
425+
[
426+
[1, padValue],
427+
[2, padValue],
428+
[3, padValue],
429+
]);
430+
431+
expect(
432+
zipLongest(
433+
[
434+
[1, 2, 3],
435+
[4, 5],
436+
[6],
437+
],
438+
padValue,
439+
).toList(),
440+
[
441+
[1, 4, 6],
442+
[2, 5, padValue],
443+
[3, padValue, padValue],
444+
]);
445+
});
446+
447+
test('Works with infinite iterables', () {
448+
expect(
449+
zipLongest(
450+
[
451+
_naturalNumbers,
452+
],
453+
padValue,
454+
).take(5).toList(),
455+
[
456+
[1],
457+
[2],
458+
[3],
459+
[4],
460+
[5],
461+
],
462+
);
463+
464+
expect(
465+
zipLongest(
466+
[
467+
_naturalNumbers,
468+
<int>[],
469+
],
470+
padValue,
471+
).take(5).toList(),
472+
[
473+
[1, padValue],
474+
[2, padValue],
475+
[3, padValue],
476+
[4, padValue],
477+
[5, padValue],
478+
],
479+
);
480+
481+
expect(
482+
zipLongest(
483+
[
484+
<int>[],
485+
_naturalNumbers,
486+
],
487+
padValue,
488+
).take(5).toList(),
489+
[
490+
[padValue, 1],
491+
[padValue, 2],
492+
[padValue, 3],
493+
[padValue, 4],
494+
[padValue, 5],
495+
],
496+
);
497+
498+
expect(
499+
zipLongest(
500+
[
501+
[-1, -2, -3],
502+
_naturalNumbers,
503+
],
504+
padValue,
505+
).take(5).toList(),
506+
[
507+
[-1, 1],
508+
[-2, 2],
509+
[-3, 3],
510+
[padValue, 4],
511+
[padValue, 5],
512+
],
513+
);
514+
});
515+
});
516+
296517
test('LinkedHashMap.sort', () {
297518
final random = Random(0);
298519

@@ -462,3 +683,12 @@ bool _isSorted<E extends Comparable<Object>>(Iterable<E> iterable) {
462683
}
463684
return true;
464685
}
686+
687+
/// Returns an infinite [Iterable] of the natural numbers.
688+
Iterable<int> get _naturalNumbers sync* {
689+
var i = 1;
690+
while (true) {
691+
yield i;
692+
i += 1;
693+
}
694+
}

0 commit comments

Comments
 (0)