Enumerable and its methods
An Enumerable is an async sequence with the methods you know from arrays.
run(), read(), and range() return one, and enumerate() wraps anything
else you can iterate: an array, a Set, a generator, a ReadableStream.
import { read } from "@j50n/proc";
// How many lines at each level, leaving out INFO.
const levels = await read("app.log")
.lines
.map((line) => line.split(/\s+/)[2]) // "INFO", "ERROR", "WARN"
.filterNot((level) => level === "INFO")
.reduce(
(counts, level) => counts.set(level, (counts.get(level) ?? 0) + 1),
new Map<string, number>(),
);
console.log(levels);
Map(2) { "ERROR" => 2, "WARN" => 1 }
Each step returns a new Enumerable, and the consumer at the end (reduce
here) pulls the items through and returns a promise. Callbacks may be async;
each call is awaited before the next one starts. To run several at once, use
concurrentMap. Key ideas covers
how sources, steps, and consumers fit together.
enumerate() is not .enum()
import { enumerate } from "@j50n/proc";
// enumerate() wraps an iterable. The items stay as they are.
console.log(await enumerate(["apple", "pear"]).collect());
// .enum() numbers the items. Each becomes [item, index].
console.log(await enumerate(["apple", "pear"]).enum().collect());
[ "apple", "pear" ]
[ [ "apple", 0 ], [ "pear", 1 ] ]
enumerate(iterable) is a function: it wraps an iterable so you can call the
methods on it, and leaves the items alone. .enum() is a method on an
Enumerable: it numbers the items, turning each into [item, index] from 0.
Python’s enumerate is proc’s .enum().
The methods
Change items:
| Member | What it does |
|---|---|
map(fn) | Replace each item with fn(item), one call at a time. |
enum() | Number the items: each becomes [item, index]. |
concurrentMap(fn, opts) | map with several calls at once; results in input order. |
concurrentUnorderedMap(fn, opts) | map with several calls at once; results as they finish. |
transform(step) | Pass the whole sequence through a generator or TransformStream. |
Filter:
| Member | What it does |
|---|---|
filter(fn) | Keep the items for which fn is true. |
filterNot(fn) | Drop the items for which fn is true. |
Slice:
| Member | What it does |
|---|---|
take(n) | The first n items (default 1); then close the source. |
drop(n) | Skip the first n items (default 1). |
Combine:
| Member | What it does |
|---|---|
concat(other) | These items, then other’s. |
zip(other) | Pairs [mine, other's], ending with the shorter sequence. |
flatten() | Replace each item, itself iterable, with its items. |
flatMap(fn) | map, then flatten. |
Split:
| Member | What it does |
|---|---|
tee(n) | n Enumerables (default 2), each with every item. |
unzip() | Pairs [a, b] into two Enumerables, the as and the bs. |
Consume (each returns a promise):
| Member | What it does |
|---|---|
collect() | Every item, in an array. toArray() is the same. |
forEach(fn) | Call fn on each item. |
reduce(fn, zero) | Fold into one value, as Array.prototype.reduce does. |
count(fn) | How many items, or how many for which fn is true. |
first | The first item. A getter; throws RangeError if there is none. |
find(fn) | The first item for which fn is true, or undefined. |
some(fn) | Whether fn is true for any item; stops at the first. |
every(fn) | Whether fn is true for all items; stops at the first false. |
A for await loop is a consumer too.
Text, processes, and output:
| Member | What it does |
|---|---|
lines | Bytes decoded as UTF-8 lines of text. A getter. |
chunkedLines | The same lines in arrays, one per chunk read; faster. A getter. |
run(...cmd) | Pipe the items into a command’s stdin; yields its stdout. |
writeTo(path) | Write bytes to a file, or items to a WritableStream. |
writeBytesTo(writer) | Write bytes to a Writer & Closer, such as a Deno.FsFile. |
toStdout() | Write lines or bytes to stdout. |
The output of run() also has
pid and
status;
see Running a command. Files and stdout are in
Files and standard streams.
Steps to pass to transform()
| Function | What it does |
|---|---|
toBytes | Lines of text (or bytes) into byte chunks, a "\n" after each line. |
toLines | Bytes into lines; what .lines uses. |
toChunkedLines | Bytes into arrays of lines; what .chunkedLines uses. |
toByteLines | Bytes split into lines without decoding, in arrays. |
buffer(size) | Join small byte chunks into chunks of at least size bytes. |
gzip, gunzip | Compress or decompress; take lines or bytes. |
jsonStringify, jsonParse | One JSON value per string, each way. |
debug | Print each item as it passes, unchanged. |
The data formats (fromCsvToRows(), toTsv(), …) are steps too, from
@j50n/proc/transforms; see Data formats. To write your
own, see Writing your own steps.
More examples
import { enumerate, range } from "@j50n/proc";
const words = ["one", "two", "three", "four", "five"];
console.log(await enumerate(words).drop(1).take(2).collect());
console.log(await range({ from: 1, to: 4 }).zip(enumerate(words)).collect());
console.log(
await enumerate(["a b", "c"]).flatMap((s) => s.split(" ")).collect(),
);
console.log(
await enumerate(["x", "y"]).concat(enumerate(["z"])).collect(),
);
[ "two", "three" ]
[ [ 1, "one" ], [ 2, "two" ], [ 3, "three" ] ]
[ "a", "b", "c" ]
[ "x", "y", "z" ]
concat and zip take an async iterable, so wrap an array in enumerate()
first; the types refuse a plain array.
import { enumerate, range } from "@j50n/proc";
const numbers = () => range({ from: 1, until: 6 }); // 1 to 6, a new one each time
console.log(await numbers().count((n) => n % 2 === 0)); // 3
console.log(await numbers().find((n) => n > 4)); // 5
console.log(await numbers().find((n) => n > 10)); // undefined
console.log(await numbers().some((n) => n > 5)); // true
console.log(await numbers().every((n) => n > 5)); // false
console.log(await numbers().first); // 1, a getter
try {
await enumerate<number>([]).first;
} catch (error) {
if (error instanceof RangeError) console.log("empty: no first item");
}
3
5
undefined
true
false
1
empty: no first item
numbers() makes a new Enumerable for each line because each one can be read
only once. find, some, every, and first stop reading as soon as they
have their answer, and close the source.
import { read } from "@j50n/proc";
// Read the file once, and go over the lines two ways side by side.
const [a, b] = read("app.log").lines.tee();
const [total, errors] = await Promise.all([
a.count(),
b.count((line) => line.includes("ERROR")),
]);
console.log(`${errors} of ${total} lines are errors`);
2 of 7 lines are errors
Traps
- An
Enumerableis read once. A secondcollect()gets nothing, without an error. Keep the array from the first, ortee()the sequence. tee()andunzip()keep an item until every branch has read it. Branches read side by side hold little; a branch that runs far ahead of another makes everything in between pile up in memory. If the source throws, every branch throws, after the items before the error.- Read every branch of a
tee(), at least to abreak. The source is closed once every branch has stopped; a branch that is never read keeps it open, and keeps every item from the start. firstthrows on an empty sequence (RangeError), andreducewith no starting value throwsTypeError.findreturnsundefinedinstead.- There is no
sort. Sorting needs every item at once, so it isn’t a step. Filter first to keep only what you need,collect(), and sort the array (or pipe lines through thesortcommand, which handles more than fits in memory). flatten()on strings gives characters, because a string is iterable.linesandchunkedLinesare getters:.lines, not.lines(). Only anEnumerableof bytes has them; on anything else the type isnever.