edf2csv 0.4.78 → 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +340 -0
- package/README.md +7 -3
- package/dist/cli/report.d.ts.map +1 -1
- package/dist/cli/report.js +12 -3
- package/dist/cli/report.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +86 -8
- package/dist/cli.js.map +1 -1
- package/dist/convert/plan.d.ts +20 -0
- package/dist/convert/plan.d.ts.map +1 -1
- package/dist/convert/plan.js +54 -10
- package/dist/convert/plan.js.map +1 -1
- package/dist/convert/run.d.ts +12 -3
- package/dist/convert/run.d.ts.map +1 -1
- package/dist/convert/run.js +182 -16
- package/dist/convert/run.js.map +1 -1
- package/dist/format/csv.d.ts +8 -0
- package/dist/format/csv.d.ts.map +1 -1
- package/dist/format/csv.js +8 -2
- package/dist/format/csv.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -45,6 +45,9 @@ Options
|
|
|
45
45
|
--annotations-only Write only the EDF+ annotations, no signal data
|
|
46
46
|
--decimals <n> Fix the decimal places instead of deriving them per channel
|
|
47
47
|
--checksum Record a SHA-256 of the input in metadata.json
|
|
48
|
+
--layout <kind> wide (default): one column per channel, one file per
|
|
49
|
+
sampling rate. long: one file of time_s,channel,value,
|
|
50
|
+
every rate together, one row per sample
|
|
48
51
|
--gzip Compress every CSV, writing .csv.gz files
|
|
49
52
|
--bom Start each CSV with a UTF-8 byte order mark, so Excel
|
|
50
53
|
reads accented text and units like µV correctly
|
|
@@ -54,7 +57,7 @@ Options
|
|
|
54
57
|
--json Print machine-readable JSON to stdout (works with --info too)
|
|
55
58
|
--strict Exit 1 if the recording raised any warning
|
|
56
59
|
--stdout Write the signal CSV to stdout instead of a directory
|
|
57
|
-
(
|
|
60
|
+
(one table only: one sampling rate, or --layout long)
|
|
58
61
|
-h, --help Show this help
|
|
59
62
|
-V, --version Show the version
|
|
60
63
|
|
|
@@ -74,6 +77,9 @@ Output
|
|
|
74
77
|
With --gzip each CSV becomes a .csv.gz; metadata.json stays plain text so the
|
|
75
78
|
directory can still be read at a glance. With --bom each CSV starts with a
|
|
76
79
|
UTF-8 byte order mark and metadata.json does not, since JSON.parse rejects one.
|
|
80
|
+
With --layout long every channel goes into one signals.csv as time_s, channel
|
|
81
|
+
and value, in time order, whatever rates the recording mixes — which is also
|
|
82
|
+
the one arrangement --stdout can stream for a mixed-rate file.
|
|
77
83
|
|
|
78
84
|
Examples
|
|
79
85
|
edf2csv recording.edf
|
|
@@ -152,6 +158,7 @@ export async function main(argv) {
|
|
|
152
158
|
'annotations-only': { type: 'boolean' },
|
|
153
159
|
decimals: { type: 'string' },
|
|
154
160
|
checksum: { type: 'boolean' },
|
|
161
|
+
layout: { type: 'string' },
|
|
155
162
|
gzip: { type: 'boolean' },
|
|
156
163
|
bom: { type: 'boolean' },
|
|
157
164
|
jobs: { type: 'string', short: 'j' },
|
|
@@ -201,6 +208,24 @@ export async function main(argv) {
|
|
|
201
208
|
process.stderr.write(`error: ${printable(entry)}: could not be read, so any recordings inside it were skipped.\n`);
|
|
202
209
|
}
|
|
203
210
|
if (expanded.length === 0) {
|
|
211
|
+
/*
|
|
212
|
+
"None here" and "could not look" are different answers, and so are their exit codes.
|
|
213
|
+
|
|
214
|
+
A folder the process cannot read gave the same exit 2 and the same "No EDF or BDF
|
|
215
|
+
recordings found" as an empty one — while the line above it said the folder could not
|
|
216
|
+
be read. Exit 2 is this tool's code for "the command itself was wrong", so a script
|
|
217
|
+
was being told to fix its arguments when what needed fixing was a permission. The
|
|
218
|
+
command was fine; the filesystem refused.
|
|
219
|
+
|
|
220
|
+
And the sentence itself claimed a fact the run is in no position to state: nothing was
|
|
221
|
+
found because nothing was looked at.
|
|
222
|
+
*/
|
|
223
|
+
if (unreadable.length > 0) {
|
|
224
|
+
process.stderr.write(`Nothing could be converted: ${unreadable.length === 1 ? 'that path' : 'those paths'} ` +
|
|
225
|
+
`could not be read, so whether ${unreadable.length === 1 ? 'it holds' : 'they hold'} ` +
|
|
226
|
+
`recordings is unknown.\n`);
|
|
227
|
+
return EXIT_ERROR;
|
|
228
|
+
}
|
|
204
229
|
process.stderr.write(`No EDF or BDF recordings found in ${listed(positionals.map((p) => `"${p}"`))}.\n`);
|
|
205
230
|
return EXIT_USAGE;
|
|
206
231
|
}
|
|
@@ -228,14 +253,47 @@ export async function main(argv) {
|
|
|
228
253
|
'Use --stdout for the CSV, or --json for the summary.\n');
|
|
229
254
|
return EXIT_USAGE;
|
|
230
255
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
256
|
+
/*
|
|
257
|
+
One stream holds one table, for the same reason it holds one recording: concatenating
|
|
258
|
+
them would give a CSV whose rows come from different files with nothing marking where
|
|
259
|
+
one ends. Naming the count makes it obvious a glob was the cause — except when the count
|
|
260
|
+
is one, which happens for a folder holding a single recording. That read "--stdout writes
|
|
261
|
+
a single CSV, so it cannot take 1 recordings": ungrammatical, and wrong on its face, since
|
|
262
|
+
one recording is exactly what it can take. What it cannot take is a folder, whose contents
|
|
263
|
+
are not known until they are walked.
|
|
264
|
+
*/
|
|
234
265
|
if (toStdout && batch) {
|
|
235
|
-
process.stderr.write(
|
|
236
|
-
|
|
266
|
+
process.stderr.write(inputs.length === 1
|
|
267
|
+
? `--stdout writes a single CSV, and a folder is converted as a batch even when it ` +
|
|
268
|
+
`holds one recording.\nName the recording itself — ${inputs[0]} — or convert to a ` +
|
|
269
|
+
`directory instead.\n`
|
|
270
|
+
: `--stdout writes a single CSV, so it cannot take ${inputs.length} recordings.\n` +
|
|
271
|
+
`Convert them to directories instead, or run edf2csv once per file.\n`);
|
|
237
272
|
return EXIT_USAGE;
|
|
238
273
|
}
|
|
274
|
+
/*
|
|
275
|
+
Flags that --stdout has nowhere to put.
|
|
276
|
+
|
|
277
|
+
Both were accepted and dropped in silence. `--out` names a directory that is never
|
|
278
|
+
created, so the run looked like it had written one. `--checksum` is worse than useless:
|
|
279
|
+
the hash is computed before the first record is read, which is a second full pass over
|
|
280
|
+
the input, and then the only file it is ever written to — metadata.json — is not written
|
|
281
|
+
at all. A recording large enough to want a checksum is large enough to notice reading it
|
|
282
|
+
twice for nothing.
|
|
283
|
+
|
|
284
|
+
Refusing rather than ignoring is what this tool already does for `--stdout --json` and
|
|
285
|
+
`--stdout --annotations-only`.
|
|
286
|
+
*/
|
|
287
|
+
for (const [flag, given] of [
|
|
288
|
+
['--out', values['out'] !== undefined],
|
|
289
|
+
['--checksum', values['checksum'] === true],
|
|
290
|
+
]) {
|
|
291
|
+
if (toStdout && given) {
|
|
292
|
+
process.stderr.write(`--stdout and ${flag} cannot be combined: --stdout writes no files, and ${flag} has ` +
|
|
293
|
+
`nothing to act on.\nDrop ${flag}, or drop --stdout and convert to a directory.\n`);
|
|
294
|
+
return EXIT_USAGE;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
239
297
|
/*
|
|
240
298
|
One recording prints the document it always printed; several print one per line.
|
|
241
299
|
|
|
@@ -264,6 +322,7 @@ export async function main(argv) {
|
|
|
264
322
|
annotationsOnly: values['annotations-only'] === true,
|
|
265
323
|
gzip: values['gzip'] === true,
|
|
266
324
|
bom: values['bom'] === true,
|
|
325
|
+
layout: optionalLayout(values['layout']),
|
|
267
326
|
};
|
|
268
327
|
// Validated before the --info branch, not inside the conversion path: a flag that
|
|
269
328
|
// cannot be honoured is a usage error whatever mode it was given in, and accepting
|
|
@@ -573,7 +632,18 @@ async function convertOne(input, destination, options, emit = writeThrough) {
|
|
|
573
632
|
// thing worth saying — on stderr, so the CSV on stdout stays clean.
|
|
574
633
|
if (toStdout) {
|
|
575
634
|
const rows = result.files[0]?.rows ?? 0;
|
|
576
|
-
|
|
635
|
+
/*
|
|
636
|
+
A reader that closed the pipe did not receive a conversion, so it does not get a
|
|
637
|
+
conversion's summary. `edf2csv rec.edf --stdout | head -1` announced "Wrote 52,507
|
|
638
|
+
rows to stdout" — a number that is neither the recording's 102,400 nor the one row
|
|
639
|
+
head took, but however many had been formatted before the close was noticed. The
|
|
640
|
+
count that reached the reader is not knowable from this side; that it stopped early
|
|
641
|
+
is, so that is what is said.
|
|
642
|
+
*/
|
|
643
|
+
emit('err', result.readerHungUp
|
|
644
|
+
? `Stopped: the reader closed the pipe after ${rows.toLocaleString('en-US')} rows ` +
|
|
645
|
+
`had been written. The recording was not converted in full.\n`
|
|
646
|
+
: `Wrote ${rows.toLocaleString('en-US')} rows to stdout.\n`);
|
|
577
647
|
}
|
|
578
648
|
else {
|
|
579
649
|
emit('err', `${formatSummary(result)}\n`);
|
|
@@ -974,7 +1044,7 @@ async function convertInChild(input, destination, values, running) {
|
|
|
974
1044
|
if (values[flag] === true)
|
|
975
1045
|
args.push(`--${flag}`);
|
|
976
1046
|
}
|
|
977
|
-
for (const flag of ['start', 'duration', 'end', 'decimals']) {
|
|
1047
|
+
for (const flag of ['start', 'duration', 'end', 'decimals', 'layout']) {
|
|
978
1048
|
if (typeof values[flag] === 'string')
|
|
979
1049
|
args.push(`--${flag}=${values[flag]}`);
|
|
980
1050
|
}
|
|
@@ -1132,6 +1202,14 @@ function optionalTime(raw, option) {
|
|
|
1132
1202
|
return undefined;
|
|
1133
1203
|
return parseTimeSpec(String(raw), option);
|
|
1134
1204
|
}
|
|
1205
|
+
/** `--layout`, which is one of two words and not a guess at what was meant. */
|
|
1206
|
+
function optionalLayout(raw) {
|
|
1207
|
+
if (raw === undefined)
|
|
1208
|
+
return undefined;
|
|
1209
|
+
if (raw === 'wide' || raw === 'long')
|
|
1210
|
+
return raw;
|
|
1211
|
+
throw new OptionError(`--layout must be "wide" or "long", got "${String(raw)}".`);
|
|
1212
|
+
}
|
|
1135
1213
|
function optionalDecimals(raw) {
|
|
1136
1214
|
if (raw === undefined)
|
|
1137
1215
|
return undefined;
|