edf2csv 0.5.20 → 0.5.23

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 CHANGED
@@ -3,6 +3,67 @@
3
3
  Notable changes to edf2csv. Versions follow [semantic versioning](https://semver.org); while the
4
4
  major version is 0, a minor bump may contain breaking changes.
5
5
 
6
+ ## 0.5.23
7
+
8
+ ### Fixed: the `time_s` precision rule was documented as it worked before 0.4.55
9
+
10
+ Two pages still described the old bound.
11
+
12
+ output-files.md said the search for a terminating expansion stops at nine places, and its
13
+ table listed 1024 Hz as getting seven decimals and being rounded. The bound has been fifteen
14
+ since 0.4.55; 1024 Hz gets ten and is exact, since 1/1024 terminates at ten. Two of the three
15
+ columns wrong on one row, in a table whose whole subject is that these numbers are derived
16
+ rather than chosen. The closing paragraph then explained a cap that no longer exists.
17
+
18
+ warnings-and-errors.md said times are "written to at most nine decimal places, which
19
+ separates everything up to a gigahertz", and illustrated `TIME_RESOLUTION` with a warning at
20
+ 10 GHz — a rate that terminates at ten places and is written exactly, and does not warn. The
21
+ replacement shows the rate that does: 3e15 Hz, whose reciprocal never terminates.
22
+
23
+ A test now recomputes every row of that table from `timeDecimals`, including the exact-or-
24
+ rounded column, which it derives rather than reads. Every power of two through 32768 Hz
25
+ terminates inside fifteen places, so the table gains 4096 Hz as well.
26
+
27
+ ## 0.5.22
28
+
29
+ ### Fixed: 0.5.12 called a finished conversion unfinished
30
+
31
+ Whether the conversion stopped early and whether the reader stopped reading are different
32
+ questions, and 0.5.12 answered the first with the second. Any `--stdout` run whose reader
33
+ closed the pipe got "The recording was not converted in full."
34
+
35
+ For a large recording that is true. For one whose CSV outruns the 64 KiB pipe buffer but fits
36
+ inside a single flush — 10,000 rows, 166 KB — it is not: every row is formatted and handed
37
+ over, and only then does the final write meet the closed pipe. The conversion finished. The
38
+ delivery did not.
39
+
40
+ The estimate's row count is exact, so the two can be told apart, and now are. A run that
41
+ stopped short says how far it got out of how many; a run that finished says it finished and
42
+ that not all of it arrived. Both still exit 0, because piping to `head` is an ordinary thing
43
+ to type and not a failure.
44
+
45
+ ## 0.5.21
46
+
47
+ ### Fixed: 0.5.10 silenced the warning it was meant to keep
48
+
49
+ `VALUE_RESOLUTION` fired on every channel of an ordinary EEG at `--decimals 2`, which made
50
+ `--decimals 2 --strict` impossible on any recording, so 0.5.10 stopped raising it whenever
51
+ `--decimals` was given.
52
+
53
+ That is the wrong question. It suppressed the real case along with the false one: at
54
+ `--decimals 20`, a channel whose quantization step is 1e-106 printed every code it had as
55
+ `0.00000000000000000000` — total collapse, nothing recoverable — and said nothing at all.
56
+
57
+ The question is not who chose the precision. It is whether *any* precision this can print
58
+ would separate consecutive codes. A channel needing 3 places and given 2 is a trade the
59
+ caller made knowingly. A channel needing 108 places has no trade available, and that is true
60
+ whatever `--decimals` says.
61
+
62
+ Asked of the ceiling now. An ordinary EEG stays quiet at any `--decimals`; the gravimeter
63
+ warns at every `--decimals` and when the precision is derived. The message says "less than
64
+ any number of decimals this can print", since naming a number was what made it sound like a
65
+ setting.
66
+
6
67
  ## 0.5.20
7
68
 
8
69
  ### Fixed: 0.5.6 shared one of the two per-table buffers and left the other
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG;AAeH,OAAO,EAA+C,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AA6HjG,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAkanE;AA0zBD,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG;AAeH,OAAO,EAA+C,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AA6HjG,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAkanE;AAq0BD,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
package/dist/cli.js CHANGED
@@ -636,14 +636,25 @@ async function convertOne(input, destination, options, emit = writeThrough) {
636
636
  A reader that closed the pipe did not receive a conversion, so it does not get a
637
637
  conversion's summary. `edf2csv rec.edf --stdout | head -1` announced "Wrote 52,507
638
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.
639
+ head took, but however many had been formatted before the close was noticed.
640
+
641
+ Whether the *conversion* stopped early is a separate question from whether the
642
+ reader did, and 0.5.12 answered the first with the second. A 10,000-row recording
643
+ whose CSV outruns the pipe buffer but fits one flush is written in full and only
644
+ then meets the closed pipe: every row formatted, every row handed over, and the
645
+ summary said "The recording was not converted in full." The estimate's row count is
646
+ exact, so the two cases can be told apart and told apart honestly — what reached
647
+ the reader is not knowable from this side either way.
642
648
  */
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`);
649
+ const expected = result.plan.estimate.rows;
650
+ emit('err', !result.readerHungUp
651
+ ? `Wrote ${rows.toLocaleString('en-US')} rows to stdout.\n`
652
+ : rows < expected
653
+ ? `Stopped: the reader closed the pipe after ${rows.toLocaleString('en-US')} of ` +
654
+ `${expected.toLocaleString('en-US')} rows had been written. The recording was ` +
655
+ `not converted in full.\n`
656
+ : `Wrote ${rows.toLocaleString('en-US')} rows to stdout, but the reader closed ` +
657
+ `the pipe before the end, so not all of them reached it.\n`);
647
658
  }
648
659
  else {
649
660
  emit('err', `${formatSummary(result)}\n`);
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * Command-line entry point.\n *\n * Output discipline: anything that is the *result* of a command goes to stdout\n * (`--info`'s channel table, `--json`'s summary). Progress, warnings and the\n * conversion summary go to stderr, so a conversion can be run in a pipeline without\n * its chatter contaminating the data.\n */\n\nimport { parseArgs } from 'node:util';\nimport { pathToFileURL, fileURLToPath } from 'node:url';\nimport { realpathSync } from 'node:fs';\nimport { lstat, readdir, realpath, stat } from 'node:fs/promises';\nimport { cpus } from 'node:os';\nimport { fork } from 'node:child_process';\nimport type { ChildProcess } from 'node:child_process';\nimport path from 'node:path';\nimport process from 'node:process';\n\nimport { EdfError } from './edf/errors.js';\nimport { EdfFile } from './edf/reader.js';\nimport { buildPlan, withoutFileRateWarning } from './convert/plan.js';\nimport { ConversionError, USAGE_ERROR_CODES, convert, defaultOutputDir } from './convert/run.js';\nimport { ChannelSelectionError } from './convert/channels.js';\n// Shared with the library so a bad option is the same error whichever way it arrived.\nimport { OptionError } from './convert/options.js';\nimport { TimeRangeError, parseTimeSpec } from './convert/time-range.js';\nimport { deriveRecordStarts } from './convert/timing.js';\nimport { formatDiagnostics, formatInfo, infoJson, formatSummary, printable, printableLines, summaryJson } from './cli/report.js';\nimport { listed } from './format/list.js';\nimport { VERSION } from './version.js';\n\nconst USAGE = `edf2csv ${VERSION}\nConvert EDF, EDF+ and BDF recordings to CSV\n\nUsage\n edf2csv <recording.edf | folder> [more ...] [options]\n\nOptions\n -i, --info Show the recording's structure and estimated output size,\n without converting anything\n -o, --out <dir> Output directory (default: <recording>_csv beside the input)\n -c, --channels <list> Only these channels, comma-separated. Use #N to pick a\n channel by position when two share a label\n --start <time> Begin at this offset (30s, 5m, 1h30m, 00:30:00)\n --duration <time> Convert this much\n --end <time> Stop at this offset (instead of --duration)\n --annotations-only Write only the EDF+ annotations, no signal data\n --decimals <n> Fix the decimal places instead of deriving them per channel\n --checksum Record a SHA-256 of the input in metadata.json\n --layout <kind> wide (default): one column per channel, one file per\n sampling rate. long: one file of time_s,channel,value,\n every rate together, one row per sample\n --gzip Compress every CSV, writing .csv.gz files\n --bom Start each CSV with a UTF-8 byte order mark, so Excel\n reads accented text and units like µV correctly\n -j, --jobs <n> Convert this many recordings at once, or \"auto\" (default: 1)\n -f, --force Overwrite the output directory if it exists\n -q, --quiet Suppress the summary; warnings and errors still print\n --json Print machine-readable JSON to stdout (works with --info too)\n --strict Exit 1 if the recording raised any warning\n --stdout Write the signal CSV to stdout instead of a directory\n (one table only: one sampling rate, or --layout long)\n -h, --help Show this help\n -V, --version Show the version\n\nSeveral recordings\n A folder is expanded to every .edf and .bdf inside it, at any depth, and the\n layout is kept: recordings in sub-folders come out in sub-folders.\n Pass more than one and each is converted in turn. Without --out each lands\n beside itself as usual; with --out that directory becomes the parent and each\n recording gets its own inside it. A file that cannot be read is reported and\n the rest still convert, with a non-zero exit at the end. --jobs converts\n several at once, which is worth it for a folder of them.\n\nOutput\n A directory containing signals.csv, channels.csv, metadata.json, and\n annotations.csv when the recording carries EDF+ annotations. Channels recorded\n at different sampling rates are written to separate files, never resampled.\n With --gzip each CSV becomes a .csv.gz; metadata.json stays plain text so the\n directory can still be read at a glance. With --bom each CSV starts with a\n UTF-8 byte order mark and metadata.json does not, since JSON.parse rejects one.\n With --layout long every channel goes into one signals.csv as time_s, channel\n and value, in time order, whatever rates the recording mixes — which is also\n the one arrangement --stdout can stream for a mixed-rate file.\n\nExamples\n edf2csv recording.edf\n edf2csv recording.edf --info\n edf2csv recording.edf --channels \"EEG Fpz-Cz,ECG\" --out ./converted\n edf2csv recording.edf --start 30m --duration 5m\n edf2csv recording.edf --annotations-only\n edf2csv /data/*.edf --out ./converted\n edf2csv /data/*.edf --out ./converted --jobs auto\n edf2csv /data/study --out ./converted --jobs auto\n`;\n\nconst EXIT_OK = 0;\nconst EXIT_ERROR = 1;\nconst EXIT_USAGE = 2;\n\n/**\n * Whether this run is already shutting its children down.\n *\n * A child dying by signal is normally worth a line of its own, but not when this process is\n * the one that killed it: the interrupt handler names every abandoned directory in a single\n * message, and a per-child line underneath it would say the same thing again, once per job.\n */\nlet stopping = false;\n\n/**\n * Piping into a consumer that exits early (`| head -1`) closes our stdout, and the\n * next write raises EPIPE. That is normal in a shell pipeline, not an error worth a\n * stack trace, so it is swallowed while anything else still surfaces.\n */\nfunction ignoreBrokenPipe(stream: NodeJS.WriteStream): void {\n stream.on('error', (error: NodeJS.ErrnoException) => {\n if (error.code === 'EPIPE') {\n // A reader closing early (`edf2csv ... --info | head -5`) is not a failure, so the\n // error is swallowed rather than thrown. It deliberately does NOT set an exit code:\n // forcing 0 here would erase a real failure whenever the pipe happened to close\n // after the run had already failed, reporting success for a conversion that did\n // not happen. Node exits 0 on its own when nothing sets a code.\n return;\n }\n\n /*\n Anything else is a real write failure, and this used to rethrow it.\n\n The throw lands on a nextTick, outside whatever try/catch the conversion is running\n inside, so it became an uncaught exception: `--stdout` redirected onto a full disk\n died with a raw stack trace headed `dist/cli.js: throw error;` and lost the warning\n that the CSV it had already produced was truncated. The same failure through `--out`\n printed the ordinary message, and so did `--stdout` through the library API, which\n never registers this listener — the designed path exists and works, and this was\n preempting it.\n\n A second listener means the conversion's writer is watching this stream and will\n turn the failure into that ordinary message. Saying it here too would report one\n failure twice, so this only speaks when nothing else will.\n */\n if (stream.listenerCount('error') > 1) return;\n process.stderr.write(`error: Writing to ${stream === process.stdout ? 'stdout' : 'stderr'} failed: ${error.message}\\n`);\n process.exitCode = EXIT_ERROR;\n });\n}\n\nexport async function main(argv: readonly string[]): Promise<number> {\n ignoreBrokenPipe(process.stdout);\n ignoreBrokenPipe(process.stderr);\n\n let values: Record<string, unknown>;\n let positionals: string[];\n\n try {\n const parsed = parseArgs({\n args: [...argv],\n allowPositionals: true,\n strict: true,\n options: {\n info: { type: 'boolean', short: 'i' },\n out: { type: 'string', short: 'o' },\n channels: { type: 'string', short: 'c', multiple: true },\n start: { type: 'string' },\n duration: { type: 'string' },\n end: { type: 'string' },\n 'annotations-only': { type: 'boolean' },\n decimals: { type: 'string' },\n checksum: { type: 'boolean' },\n layout: { type: 'string' },\n gzip: { type: 'boolean' },\n bom: { type: 'boolean' },\n jobs: { type: 'string', short: 'j' },\n force: { type: 'boolean', short: 'f' },\n quiet: { type: 'boolean', short: 'q' },\n json: { type: 'boolean' },\n strict: { type: 'boolean' },\n stdout: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean', short: 'V' },\n },\n });\n values = parsed.values as Record<string, unknown>;\n positionals = parsed.positionals;\n } catch (error) {\n process.stderr.write(`${message(error)}\\n\\nRun edf2csv --help to see the options.\\n`);\n return EXIT_USAGE;\n }\n\n if (values['help'] === true) {\n process.stdout.write(USAGE);\n return EXIT_OK;\n }\n if (values['version'] === true) {\n process.stdout.write(`${VERSION}\\n`);\n return EXIT_OK;\n }\n\n if (positionals.length === 0) {\n process.stderr.write(`No input file given.\\n\\n${USAGE}`);\n return EXIT_USAGE;\n }\n\n let expanded: Input[];\n let unreadable: string[];\n let namedDirectory: boolean;\n try {\n const found = await expandInputs(positionals);\n expanded = found.inputs;\n unreadable = found.unreadable;\n namedDirectory = found.namedDirectory;\n } catch (error) {\n return reportError(error);\n }\n\n // Reported before anything is converted, so it cannot be lost among the summaries, and\n // counted against the run so the exit code does not call a partial sweep a success.\n for (const entry of unreadable) {\n process.stderr.write(\n `error: ${printable(entry)}: could not be read, so any recordings inside it were skipped.\\n`,\n );\n }\n\n if (expanded.length === 0) {\n /*\n \"None here\" and \"could not look\" are different answers, and so are their exit codes.\n\n A folder the process cannot read gave the same exit 2 and the same \"No EDF or BDF\n recordings found\" as an empty one — while the line above it said the folder could not\n be read. Exit 2 is this tool's code for \"the command itself was wrong\", so a script\n was being told to fix its arguments when what needed fixing was a permission. The\n command was fine; the filesystem refused.\n\n And the sentence itself claimed a fact the run is in no position to state: nothing was\n found because nothing was looked at.\n */\n if (unreadable.length > 0) {\n process.stderr.write(\n `Nothing could be converted: ${unreadable.length === 1 ? 'that path' : 'those paths'} ` +\n `could not be read, so whether ${unreadable.length === 1 ? 'it holds' : 'they hold'} ` +\n `recordings is unknown.\\n`,\n );\n return EXIT_ERROR;\n }\n process.stderr.write(\n `No EDF or BDF recordings found in ${listed(positionals.map((p) => `\"${p}\"`))}.\\n`,\n );\n return EXIT_USAGE;\n }\n const inputs = expanded.map((entry) => entry.path);\n /*\n A batch is what you asked for, not what happened to be there.\n\n Counting the recordings made the shape of the run depend on the contents of a folder.\n Under --json a study holding one night printed a pretty-printed object and the same study\n holding two printed two compact lines, so a script written against one of them broke on\n the other — and it broke the day a recording was added, not the day the script changed.\n An input going missing did it in reverse. This is the same count 0.4.20 took out of\n `--out` for the same reason.\n */\n const batch = inputs.length > 1 || namedDirectory;\n const quiet = values['quiet'] === true;\n const asJson = values['json'] === true;\n const strict = values['strict'] === true;\n const toStdout = values['stdout'] === true;\n\n // Both of these claim stdout. Allowing them together wrote the CSV and then the summary\n // object onto one stream, producing a document that is neither valid CSV nor valid\n // JSON — and silently, since each half looked right on its own.\n if (toStdout && asJson) {\n process.stderr.write(\n '--stdout and --json both write to stdout, so they cannot be combined.\\n' +\n 'Use --stdout for the CSV, or --json for the summary.\\n',\n );\n return EXIT_USAGE;\n }\n\n /*\n One stream holds one table, for the same reason it holds one recording: concatenating\n them would give a CSV whose rows come from different files with nothing marking where\n one ends. Naming the count makes it obvious a glob was the cause — except when the count\n is one, which happens for a folder holding a single recording. That read \"--stdout writes\n a single CSV, so it cannot take 1 recordings\": ungrammatical, and wrong on its face, since\n one recording is exactly what it can take. What it cannot take is a folder, whose contents\n are not known until they are walked.\n */\n if (toStdout && batch) {\n process.stderr.write(\n inputs.length === 1\n ? `--stdout writes a single CSV, and a folder is converted as a batch even when it ` +\n `holds one recording.\\nName the recording itself — ${inputs[0]} — or convert to a ` +\n `directory instead.\\n`\n : `--stdout writes a single CSV, so it cannot take ${inputs.length} recordings.\\n` +\n `Convert them to directories instead, or run edf2csv once per file.\\n`,\n );\n return EXIT_USAGE;\n }\n\n /*\n Flags that --stdout has nowhere to put.\n\n Both were accepted and dropped in silence. `--out` names a directory that is never\n created, so the run looked like it had written one. `--checksum` is worse than useless:\n the hash is computed before the first record is read, which is a second full pass over\n the input, and then the only file it is ever written to — metadata.json — is not written\n at all. A recording large enough to want a checksum is large enough to notice reading it\n twice for nothing.\n\n Refusing rather than ignoring is what this tool already does for `--stdout --json` and\n `--stdout --annotations-only`.\n */\n for (const [flag, given] of [\n ['--out', values['out'] !== undefined],\n ['--checksum', values['checksum'] === true],\n ] as const) {\n if (toStdout && given) {\n process.stderr.write(\n `--stdout and ${flag} cannot be combined: --stdout writes no files, and ${flag} has ` +\n `nothing to act on.\\nDrop ${flag}, or drop --stdout and convert to a directory.\\n`,\n );\n return EXIT_USAGE;\n }\n }\n\n /*\n One recording prints the document it always printed; several print one per line.\n\n Pretty-printed objects run together are still readable by a streaming JSON parser, but\n not by anything that reads a record per line — and a batch is exactly where that is\n wanted. Emitting a batch in the single-file shape would repeat 0.2.28's mistake of\n putting two documents on one stream and leaving the caller to work out the boundary.\n\n null, not undefined: the serialisers default this argument to 2, and a default parameter\n takes effect for undefined, which silently gave a batch the indentation it was supposed\n to be dropping.\n */\n const jsonIndent = batch ? null : 2;\n\n try {\n const outOption = typeof values['out'] === 'string' ? values['out'] : undefined;\n const destinations = destinationsFor(expanded, outOption, batch);\n assertDistinct(inputs, destinations);\n\n const shared = {\n channels: splitChannels(values['channels']),\n start: optionalTime(values['start'], '--start'),\n startText: typeof values['start'] === 'string' ? values['start'] : undefined,\n duration: optionalTime(values['duration'], '--duration'),\n end: optionalTime(values['end'], '--end'),\n endText: typeof values['end'] === 'string' ? values['end'] : undefined,\n decimals: optionalDecimals(values['decimals']),\n annotationsOnly: values['annotations-only'] === true,\n gzip: values['gzip'] === true,\n bom: values['bom'] === true,\n layout: optionalLayout(values['layout']),\n };\n\n // Validated before the --info branch, not inside the conversion path: a flag that\n // cannot be honoured is a usage error whatever mode it was given in, and accepting\n // \"--jobs 0\" in silence under --info is the kind of quiet that this tool avoids.\n // Parsed before it is overridden: --stdout converts one recording however many jobs\n // were asked for, but \"--stdout --jobs 0\" is still a request that cannot be honoured,\n // and accepting it in silence is the thing 0.4.2 fixed for --info.\n const requestedJobs = parseJobs(values['jobs'], inputs.length);\n const jobs = toStdout ? 1 : requestedJobs;\n\n if (values['info'] === true) {\n const failures: number[] = [];\n let warnings = 0;\n for (const [index, input] of inputs.entries()) {\n // A blank line between reports, so several tables read as one document.\n if (batch && !asJson && index > 0) process.stdout.write('\\n');\n try {\n warnings += await showInfo(input as string, shared, asJson, jsonIndent);\n } catch (error) {\n failures.push(reportError(error, batch ? (input as string) : undefined));\n }\n }\n if (unreadable.length > 0) failures.push(EXIT_ERROR);\n if (failures.length > 0) return worstOf(failures);\n return strict && warnings > 0 ? EXIT_ERROR : EXIT_OK;\n }\n\n // The meter redraws one line in place, which two conversions cannot share. Running them\n // at once replaces it with the completion count each file prints as it lands.\n const showProgress =\n !quiet && !asJson && !toStdout && jobs === 1 && process.stderr.isTTY === true;\n\n const failures: number[] = [];\n let warnings = 0;\n let converted = 0;\n\n /** Convert input `index`, sending its output wherever the caller wants it. */\n const runOne = async (index: number, emit: Emit): Promise<void> => {\n const input = inputs[index] as string;\n const destination = destinations[index] as string;\n // Which recording this is, before anything it prints. Without it a batch produces a\n // stack of summaries and errors with nothing saying which file each belongs to.\n if (batch && !quiet && !asJson) {\n emit('err', `[${index + 1}/${inputs.length}] ${printable(input)}\\n`);\n }\n try {\n warnings += await convertOne(\n input,\n destination,\n {\n ...shared,\n // With one input and no --out, convert() derives the default itself, exactly as\n // it did before batches existed.\n outputDir: outOption === undefined && !batch ? undefined : destination,\n checksum: values['checksum'] === true,\n force: values['force'] === true,\n toStdout,\n quiet,\n asJson,\n showProgress,\n jsonIndent,\n },\n emit,\n );\n converted++;\n } catch (error) {\n /*\n A batch keeps going. One unreadable recording among five hundred is a reason to\n report that file, not to abandon the ones already converted and refuse the rest.\n The exit code still reports the run as failed, and the closing line says how many\n of them made it, so nothing about the failure is quiet.\n */\n failures.push(reportError(error, batch ? input : undefined, emit));\n }\n };\n\n if (jobs === 1) {\n for (let index = 0; index < inputs.length; index++) {\n await runOne(index, writeThrough);\n // A single recording keeps the exit code it has always had rather than the batch's.\n if (!batch && failures.length > 0) return failures[0] as number;\n }\n } else {\n /*\n Real processes, not concurrent promises.\n\n Converting is almost entirely arithmetic and string building — 1.17 s of CPU for\n 1.24 s of wall clock on a 168 MB conversion — and Node runs that on one thread. An\n in-process pool was tried first and gained 6% on eight recordings, which is the\n overlap in the file reads and nothing else. Each conversion is already a whole\n command, so each one gets its own process, which is what `xargs -P` would do by hand.\n\n Workers take the next recording as they free up rather than splitting the list into\n equal shares. Recordings in a folder differ wildly in length, and a fixed split\n leaves every worker but one idle behind the longest file.\n\n Each child's output is held until it exits, so two finishing together cannot\n interleave one's summary with the other's warnings.\n */\n const running = new Map<ChildProcess, string>();\n\n /*\n Ctrl-C in a terminal reaches every process in the group, so the children would stop\n anyway; a signal sent to this process alone does not, and that is how a batch gets\n run from a script or a CI job. Interrupting one left four conversions writing\n gigabytes into a directory their owner believed abandoned, and the last thing on\n screen was a successful \"Done in 1.6s\" from whichever recording had just landed —\n the run read as if it had finished.\n */\n const onInterrupt = (signal: NodeJS.Signals): void => {\n stopping = true;\n const abandoned = [...running.values()];\n for (const child of running.keys()) child.kill('SIGTERM');\n process.stderr.write(\n `\\ninterrupted (${signal}): ${abandoned.length} conversion` +\n `${abandoned.length === 1 ? '' : 's'} stopped part way through.\\n` +\n (abandoned.length > 0\n ? ` Incomplete, and should not be used: ${listed(abandoned)}\\n`\n : ''),\n );\n process.exit(signal === 'SIGINT' ? 130 : 143);\n };\n process.once('SIGINT', onInterrupt);\n process.once('SIGTERM', onInterrupt);\n\n let next = 0;\n const worker = async (): Promise<void> => {\n for (let index = next++; index < inputs.length; index = next++) {\n const input = inputs[index] as string;\n const sink = buffered();\n if (!quiet && !asJson) {\n sink.emit('err', `[${index + 1}/${inputs.length}] ${printable(input)}\\n`);\n }\n const child = await convertInChild(\n input,\n destinations[index] as string,\n values,\n running,\n );\n // The report says what the child actually did; the exit code says whether it\n // got there. A child that converted and warned has a report and exits 0, since\n // --strict is the parent's to apply.\n if (child.report) {\n converted += child.report.converted;\n warnings += child.report.warnings;\n } else if (child.code === 0) {\n converted++;\n }\n if (child.code !== 0) failures.push(child.code);\n // The child saw one recording, so it printed the indented document a single\n // conversion prints. A batch is one object per line.\n sink.emit('out', asJson ? compactJson(child.out) : child.out);\n // The child converted a single recording, so it named no file in its errors the\n // way a batch does. Naming it here keeps the two paths identical to a reader and\n // to anything grepping a log, where the [n/m] header may not be alongside.\n sink.emit('err', named(child.err, input));\n sink.flush();\n }\n };\n try {\n await Promise.all(Array.from({ length: jobs }, () => worker()));\n } finally {\n process.off('SIGINT', onInterrupt);\n process.off('SIGTERM', onInterrupt);\n }\n }\n\n if (batch && !quiet && !asJson) {\n process.stderr.write(\n `\\nConverted ${converted} of ${inputs.length} recordings` +\n `${failures.length > 0 ? `; ${failures.length} failed` : ''}.\\n`,\n );\n }\n\n /*\n A parent that forked this process needs the counts, not just an exit status.\n\n An exit code cannot separate \"converted, and raised warnings\" from \"did not convert\",\n and under --strict those are the same code. The parent read it as a failure, so a\n parallel run of two recordings — one of which merely warned — reported \"Converted 1 of\n 2 recordings; 1 failed\" for a run in which both converted, while the serial path said\n \"Converted 2 of 2\". `process.send` exists only when this process was forked with a\n channel, so nothing changes for an ordinary invocation.\n */\n process.send?.({ edf2csv: { converted, warnings } });\n\n if (unreadable.length > 0) failures.push(EXIT_ERROR);\n if (failures.length > 0) return worstOf(failures);\n\n /*\n --strict turns any warning into a non-zero exit, for pipelines that would rather stop\n than proceed on a recording the tool had something to say about.\n\n The output is still written. A warning describes the recording, not a failure to\n convert it — a truncated file converts correctly for the records that are there — so\n destroying that work would be the wrong response. The exit code is the signal; what\n to do about it is the caller's decision, and they still have the files to inspect.\n */\n if (strict && warnings > 0) {\n process.stderr.write(\n `\\n--strict: ${warnings} warning${warnings === 1 ? '' : 's'} raised, so this run is ` +\n `reported as a failure. The output was still written.\\n`,\n );\n return EXIT_ERROR;\n }\n return EXIT_OK;\n } catch (error) {\n return reportError(error);\n }\n}\n\n/** Print one recording's `--info`, and return how many diagnostics it raised. */\nasync function showInfo(\n input: string,\n shared: Record<string, unknown>,\n asJson: boolean,\n jsonIndent: number | null,\n): Promise<number> {\n const file = await EdfFile.open(input);\n try {\n /*\n Read the annotation channel only when the timing actually depends on it.\n\n --info is documented as a header-only summary that returns immediately whatever\n the file's size, but it called readAnnotations() on every EDF+/BDF+ file — a seek\n into every data record. deriveRecordStarts discards that data unless the file is\n EDF+D, where record start times are stored rather than arithmetic, so on a\n continuous recording the whole scan was thrown away. It cost 0.29 s on a 12 MB\n file and scaled with record count.\n\n A discontinuous file still needs the scan: without it the reported span and row\n estimate are wrong, which is a bug that has already been fixed once here.\n */\n const hasAnnotations = file.annotationSignals.length > 0;\n const needsEveryRecordStart = file.header.continuity === 'EDF+D' && hasAnnotations;\n /*\n A continuous recording needs its origin too, which is one read rather than one per\n record: 0.4.9 made its first record's timekeeping TAL the point the samples are timed\n from, and this report went on placing a requested window against zero. `--info --start 1`\n predicted 8 rows where the conversion wrote 10, on a file whose discontinuous twin —\n identical but for the reserved field — agreed with itself.\n */\n const annotationData = needsEveryRecordStart\n ? await file.readAnnotations()\n : {\n annotations: [],\n recordStarts:\n file.header.continuity === 'EDF+C' && hasAnnotations\n ? [await file.readOrigin()]\n : [],\n malformed: 0,\n };\n const timing = deriveRecordStarts(file, annotationData);\n const plan = buildPlan(\n {\n signals: file.header.signals,\n recordDuration: file.header.recordDuration,\n recordCount: file.recordCount,\n hasAnnotationChannel: file.annotationSignals.length > 0,\n recordStarts: timing.starts,\n },\n shared,\n );\n plan.diagnostics.push(...timing.diagnostics);\n process.stdout.write(\n asJson ? `${infoJson(file, plan, jsonIndent)}\\n` : `${formatInfo(file, plan)}\\n`,\n );\n\n // Under --json the warnings travel inside the document, exactly as they do for a\n // conversion, so stderr stays empty and the whole result is one parseable thing.\n const diagnostics = [...withoutFileRateWarning(file.diagnostics), ...plan.diagnostics];\n if (!asJson && diagnostics.length > 0) {\n process.stderr.write(`\\n${formatDiagnostics(diagnostics)}\\n`);\n }\n return diagnostics.length;\n } finally {\n await file.close();\n }\n}\n\n/** Convert one recording, and return how many diagnostics it raised. */\nasync function convertOne(\n input: string,\n destination: string,\n options: Record<string, unknown> & {\n quiet: boolean;\n asJson: boolean;\n showProgress: boolean;\n toStdout: boolean;\n jsonIndent: number | null;\n },\n emit: Emit = writeThrough,\n): Promise<number> {\n const { quiet, asJson, showProgress, toStdout } = options;\n let lastTick = 0;\n\n /*\n Interrupting a conversion leaves a CSV that stops mid-recording but is still\n perfectly well-formed, so nothing about the file itself reveals that half the\n data is missing. Saying so on the way out is the whole point of a tool that\n claims it will not go quiet when something is wrong.\n */\n const onInterrupt = (signal: NodeJS.Signals): void => {\n if (showProgress) process.stderr.write('\\r\\u001b[K');\n // --stdout writes no directory, so there is none to warn about. Naming one anyway\n // pointed at a path that was never created — the same \"files that were never written\"\n // that 0.2.30 removed from this path's error message.\n process.stderr.write(\n `\\ninterrupted (${signal}): the conversion stopped part way through.\\n` +\n (toStdout\n ? ` The CSV on stdout stops mid-recording and should not be used.\\n`\n : ` Files already written to \"${destination}\" are incomplete and should not be used.\\n`),\n );\n // 128 + signal number, the conventional exit status for dying to a signal.\n process.exit(signal === 'SIGINT' ? 130 : 143);\n };\n process.once('SIGINT', onInterrupt);\n process.once('SIGTERM', onInterrupt);\n\n try {\n const result = await convert(input, {\n ...options,\n onProgress: showProgress\n ? (progress): void => {\n const now = Date.now();\n if (now - lastTick < 100) return;\n lastTick = now;\n const percent =\n progress.recordsTotal === 0\n ? 100\n : Math.floor((progress.recordsDone / progress.recordsTotal) * 100);\n process.stderr.write(`\\r converting… ${percent}%`);\n }\n : undefined,\n });\n\n if (showProgress) process.stderr.write('\\r\\u001b[K');\n\n if (result.diagnostics.length > 0 && !asJson) {\n emit('err', `${formatDiagnostics(result.diagnostics)}\\n\\n`);\n }\n if (asJson) {\n // One object per line, so a batch is JSON Lines — `jq` reads a record at a time\n // rather than waiting for the whole run to finish.\n emit('out', `${summaryJson(result, options.jsonIndent as number | null)}\\n`);\n } else if (!quiet) {\n // With --stdout there is no directory to summarise, and the row count is the only\n // thing worth saying — on stderr, so the CSV on stdout stays clean.\n if (toStdout) {\n const rows = result.files[0]?.rows ?? 0;\n /*\n A reader that closed the pipe did not receive a conversion, so it does not get a\n conversion's summary. `edf2csv rec.edf --stdout | head -1` announced \"Wrote 52,507\n rows to stdout\" — a number that is neither the recording's 102,400 nor the one row\n head took, but however many had been formatted before the close was noticed. The\n count that reached the reader is not knowable from this side; that it stopped early\n is, so that is what is said.\n */\n emit(\n 'err',\n result.readerHungUp\n ? `Stopped: the reader closed the pipe after ${rows.toLocaleString('en-US')} rows ` +\n `had been written. The recording was not converted in full.\\n`\n : `Wrote ${rows.toLocaleString('en-US')} rows to stdout.\\n`,\n );\n } else {\n emit('err', `${formatSummary(result)}\\n`);\n }\n }\n return result.diagnostics.length;\n } finally {\n process.off('SIGINT', onInterrupt);\n process.off('SIGTERM', onInterrupt);\n }\n}\n\n/** A recording to convert, and the path its output should be named after. */\ninterface Input {\n /** Where the recording is. */\n path: string;\n /**\n * What to call its output, relative to `--out`.\n *\n * For a file named on the command line this is just its own name. For one found by\n * expanding a directory it keeps the position it had inside that directory, so a study\n * laid out as one folder per night comes out the same shape — and, more to the point, so\n * that fifty recordings all named `rec.edf` do not all claim `<out>/rec`.\n */\n name: string;\n /**\n * Whether this recording was found by expanding a directory rather than named directly.\n *\n * `--out` means two different things — the output directory itself, or a parent to put one\n * directory per recording inside — and this is what decides which. See `destinationsFor`.\n */\n fromDirectory: boolean;\n}\n\n/**\n * Recordings to convert, with directories expanded to what is inside them.\n *\n * A directory yields every `.edf` and `.bdf` beneath it, at any depth. Recordings arrive\n * organised into folders, and a shell has no tidy way to reach them — which is why the\n * recipes here carried a `find` incantation to do it. Passing the folder is the obvious\n * thing to try, and it used to fail with \"is a directory, not an EDF file\".\n */\nasync function expandInputs(\n positionals: readonly string[],\n): Promise<{ inputs: Input[]; unreadable: string[]; namedDirectory: boolean }> {\n const found: Input[] = [];\n const unreadable: string[] = [];\n /*\n Whether a directory was NAMED, which is not the same as whether one yielded anything.\n\n `fromDirectory` on the inputs answers the second question, and using it for the first put\n the 0.4.20 defect back one step along: `edf2csv study named.edf --out csv` wrote\n csv/named/ while the study held recordings and csv/signals.csv once it held none, because\n the surviving input then looked like a lone file. Whether some unrelated folder happens\n to contain anything decided where a different recording's output went.\n */\n let namedDirectory = false;\n for (const given of positionals) {\n const info = await stat(given).catch(() => null);\n if (info === null || !info.isDirectory()) {\n // Anything that is not a directory is passed through untouched, so a file that does\n // not exist still reports itself rather than vanishing from the list.\n found.push({ path: given, name: path.basename(given), fromDirectory: false });\n continue;\n }\n namedDirectory = true;\n const walked = await walk(given);\n unreadable.push(...walked.unreadable);\n for (const file of walked.files) {\n found.push({ path: file, name: path.relative(given, file), fromDirectory: true });\n }\n }\n /*\n One recording, however many ways it was named.\n\n The walk already does this for links inside a folder, and the two halves disagreed:\n `edf2csv study` converted a recording reached twice once, while `edf2csv a.edf a.edf`\n refused the whole run for the collision it would cause. A shell makes the second easy\n to produce by accident — `edf2csv *.edf recording.edf` — and there is nothing ambiguous\n about it: it is one recording, and converting it once is what was meant.\n\n A path that does not resolve keeps its own identity so that a file which is not there\n still reports itself rather than being folded into another entry.\n\n Which of the names survives is decided by the names themselves, not by the order they\n turned up in. Keeping the first arrival meant the output directory was named by argument\n order — `edf2csv data/one.edf data/alias.edf` wrote out/one and the same two swapped\n wrote out/alias — and a shell orders a glob however it likes. Inside a folder it was the\n order `readdir` returned, which differs between filesystems, so copying a study to\n another machine could rename its output. A recording that is not a link is preferred over\n a link to it, since that is the name the recording actually has; two of a kind are\n settled by the path that sorts first.\n */\n const identified: { entry: Input; identity: string; link: boolean }[] = [];\n for (const entry of found) {\n const identity = await realpath(entry.path).catch(() => `?${path.resolve(entry.path)}`);\n const own = await lstat(entry.path).catch(() => null);\n identified.push({ entry, identity, link: own?.isSymbolicLink() ?? false });\n }\n\n const byIdentity = new Map<string, Input>();\n const winners = new Map<string, (typeof identified)[number]>();\n for (const candidate of identified) {\n const held = winners.get(candidate.identity);\n if (held !== undefined && !outnames(candidate, held)) continue;\n winners.set(candidate.identity, candidate);\n byIdentity.set(candidate.identity, candidate.entry);\n }\n\n // A directory hands its entries back in whatever order the filesystem stored them.\n const unique = [...byIdentity.values()];\n unique.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n\n return { inputs: unique, unreadable, namedDirectory };\n}\n\n/**\n * Which of two names for one recording the output should be called after.\n *\n * A name the recording actually has beats a link pointing at it, and two of a kind are\n * settled by sort order. Both are properties of the names, so the answer does not move when\n * a shell expands a glob differently or a filesystem enumerates a folder in another order.\n */\nfunction outnames(\n candidate: { entry: Input; link: boolean },\n held: { entry: Input; link: boolean },\n): boolean {\n if (candidate.link !== held.link) return !candidate.link;\n if (candidate.entry.path !== held.entry.path) return candidate.entry.path < held.entry.path;\n /*\n One recording, one path, two names — reached directly and through a named folder.\n\n `edf2csv study study/night-01/rec.edf` and the same two swapped are the same request, and\n they disagreed: the folder gives the recording its position inside the folder\n (`night-01/rec`), a direct mention gives it its bare name (`rec`), and whichever spelling\n the loop met first won. So argument order decided the output directory's name, and, once\n a second `study/rec.edf` was in play, decided whether the run happened at all — the bare\n name collides with it and the run is refused, exit 2, while the other order converts both.\n\n The nested name wins. It is what the folder promised — \"the layout is kept: recordings in\n sub-folders come out in sub-folders\" — and it is the one that does not collide, since\n collapsing a recording to its bare name is what puts it on top of a sibling.\n */\n const depth = (entry: Input): number => entry.name.split(path.sep).length;\n if (depth(candidate.entry) !== depth(held.entry)) return depth(candidate.entry) > depth(held.entry);\n return candidate.entry.name < held.entry.name;\n}\n\n/**\n * Every recording under a directory, following symbolic links.\n *\n * Written by hand rather than with a recursive `readdir` because that reports a symlink as\n * a symlink and never as a file, so a linked recording was skipped without a word — and the\n * closing \"converted 3 of 3\" then described the three it had noticed rather than what was in\n * the folder. Data organised by linking recordings into a working directory is ordinary, and\n * quietly converting fewer files than were asked for is the failure this tool exists to\n * avoid. Naming the same link on the command line always worked, which made the omission\n * harder to notice rather than easier.\n *\n * Following links means they can form a cycle, so directories are recorded by their resolved\n * identity and visited once. A link to a file already reached another way is likewise\n * converted once.\n */\nasync function walk(root: string): Promise<{ files: string[]; unreadable: string[] }> {\n const files: string[] = [];\n const unreadable: string[] = [];\n const seen = new Set<string>();\n /*\n A queue, taken from the front, and each directory's children entered in a settled order.\n\n This was a stack popped from the back, so which of two names for one directory was\n visited first came down to the order `readdir` happened to return them — and the loser\n was then skipped as already seen, taking its name out of the run with it. A folder\n holding `aaa-real/` beside `zzz-alias -> aaa-real` converted into `<out>/zzz-alias/`: the\n link's name, chosen by a hash order that differs between filesystems. 0.4.29 settled this\n for two names of one *file* and left the directory above it deciding by accident.\n\n Breadth first, and each directory's sub-directories entered with real names before links\n and alphabetically within each, so the name that survives is a property of the tree: the\n shallowest, then the one that is not a link, then the first in sort order.\n */\n const queue = [root];\n let next = 0;\n\n while (next < queue.length) {\n const directory = queue[next++] as string;\n const real = await realpath(directory).catch(() => directory);\n if (seen.has(real)) continue;\n seen.add(real);\n\n /*\n A directory that cannot be listed is reported, not stepped over.\n\n Skipping it in silence meant a folder holding three recordings, one of them inside a\n sub-directory without read permission, converted two and said \"Converted 2 of 2\n recordings\" — a total that agreed with itself and with nothing else. That is the same\n failure 0.4.4 fixed for symbolic links, arriving by a different route: converting fewer\n recordings than were asked for and reporting success.\n */\n let entries;\n try {\n entries = await readdir(directory, { withFileTypes: true });\n } catch {\n unreadable.push(directory);\n continue;\n }\n\n // A real name before a link to the same thing, then alphabetically. The dirent describes\n // the entry itself rather than its target, which is exactly the question here.\n const ordered = [...entries].sort((a, b) => {\n const link = Number(a.isSymbolicLink()) - Number(b.isSymbolicLink());\n return link !== 0 ? link : a.name < b.name ? -1 : a.name > b.name ? 1 : 0;\n });\n\n for (const entry of ordered) {\n const full = path.join(directory, entry.name);\n // stat, not the dirent: a dirent describes the link, and what matters is its target.\n const info = await stat(full).catch(() => null);\n if (info === null) {\n /*\n Anything that cannot be inspected is reported, whatever it is called.\n\n This used to report only names ending in `.edf` or `.bdf`, on the reasoning that a\n broken link to something else is nobody's business. A directory carries no such\n name. A study kept as one folder per night, with one night linked to an external\n drive, converted the nights that were mounted and said nothing about the one that\n was not — and because losing that input left a single recording, `--out` stopped\n meaning \"a parent to fill\" and started meaning \"the directory to write\", so the\n survivor landed somewhere else as well. Whether a drive happened to be mounted\n changed both what was converted and where it went, in silence, exit 0.\n\n The walk cannot know what was behind a link it cannot follow, which is exactly the\n reason to say so rather than to guess.\n */\n unreadable.push(full);\n continue;\n }\n if (info.isDirectory()) {\n queue.push(full);\n } else if (info.isFile() && /\\.(edf|bdf)$/iu.test(entry.name)) {\n // Duplicates are collapsed once for the whole list in expandInputs; this only has\n // to find them. The `seen` set here is for directory cycles.\n files.push(full);\n }\n }\n }\n return { files, unreadable };\n}\n\n/**\n * Where each recording's output goes.\n *\n * Naming one recording, `--out` is the output directory itself, which is what it has always\n * meant. Naming a folder, or several recordings, it is a parent and each recording gets its\n * own directory inside it, because writing several recordings into one directory would have\n * them overwrite each other's `signals.csv` — the one thing a batch must not do quietly.\n *\n * With no `--out` at all, every recording converts beside itself exactly as it would have\n * done alone, so a glob behaves like the shell loop it replaces.\n *\n * What a folder means is decided by the folder, not by how much is in it. Counting the\n * recordings instead meant `edf2csv study --out csv` wrote `csv/signals.csv` while the study\n * held one night and `csv/night-01/rec/signals.csv` once it held two: adding a recording\n * moved the output of a recording that had not changed. The same count made the destination\n * depend on things no one had touched either — a night on an unmounted drive, a\n * sub-directory that could not be read — so where the data landed turned on the state of the\n * machine rather than on the command.\n */\nfunction destinationsFor(\n inputs: readonly Input[],\n out: string | undefined,\n /** True when a folder was named, or more than one recording was. See `batch` in `main`. */\n batch: boolean,\n): string[] {\n if (out === undefined) return inputs.map((input) => defaultOutputDir(input.path));\n if (!batch) return [out];\n return inputs.map((input) => path.join(out, stemOf(input.name)));\n}\n\n/** A name without its extension, keeping a leading dot: `.hidden.edf` -> `.hidden`. */\nfunction stemOf(name: string): string {\n const dot = name.lastIndexOf('.');\n const slash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\\\'));\n return dot > slash + 1 ? name.slice(0, dot) : name;\n}\n\n/**\n * Refuse a batch in which two recordings would land in the same directory.\n *\n * Two files with the same name in different folders — `night-1/rec.edf` and\n * `night-2/rec.edf`, which is how recordings usually get organised — both resolve to\n * `<out>/rec`. Converting them in turn would leave one recording's data sitting under the\n * other's name with nothing to show it had happened, so the run stops before writing\n * anything at all.\n */\nfunction assertDistinct(inputs: readonly string[], destinations: readonly string[]): void {\n /*\n Two names that differ only in case are one directory on a filesystem that does not\n distinguish them, which is the default on macOS and the rule on Windows.\n\n `a/REC.edf` and `b/rec.edf` produce `<out>/REC` and `<out>/rec`. Compared exactly those\n are different, so both went through, and with --force the second conversion wrote into\n the directory the first had made: one directory holding one recording's `signals.csv`\n beside the other's `signals_256hz.csv`, under a single `metadata.json` naming only one\n of them. The run reported \"Converted 2 of 2 recordings\" and exited 0. A directory whose\n provenance file describes a recording other than the data beside it is the one outcome\n this tool exists to prevent.\n\n The comparison follows the platform rather than being applied everywhere, so a\n case-sensitive filesystem — where those really are two directories — keeps converting\n both. A case-sensitive volume on macOS is the exception it gets wrong, and it gets it\n wrong in the safe direction: a refusal that names both recordings, not a silent merge.\n */\n const foldsCase = process.platform === 'darwin' || process.platform === 'win32';\n /*\n macOS filesystems fold Unicode normalisation as well as case.\n\n HFS+ and APFS compare names in a normalised form, so `café` written as e + U+0301 and\n `café` written as U+00E9 are one directory — while remaining two different JavaScript\n strings, which is all this guard was comparing. Two recordings whose stems differ only\n that way therefore both passed the check and both converted into the same place:\n\n study/café.edf (NFC) -> csv/café/signals.csv\n study/café.bdf (NFD) -> csv/café/signals_256hz.csv, _128hz, _1hz\n\n one directory holding both, under a single metadata.json naming one of them, reported as\n \"Converted 2 of 2 recordings\" and exit 0 under --force. Without --force the second run\n happened to hit \"already exists\", which is the accidental save rather than the check\n doing its job — and it named the wrong problem.\n\n Not folded on Linux, where the two names are genuinely two directories and refusing them\n would be inventing a collision that does not exist. Windows preserves normalisation too,\n so only darwin. This is the same platform-shaped assumption the case fold above already\n makes, and it has the same limit: a network or removable volume that normalises while the\n running platform does not is not covered by either.\n */\n const identity = (destination: string): string => {\n const resolved = path.resolve(destination);\n if (!foldsCase) return resolved;\n const folded = resolved.toLowerCase();\n return process.platform === 'darwin' ? folded.normalize('NFC') : folded;\n };\n\n const claimed = new Map<string, string>();\n for (const [index, destination] of destinations.entries()) {\n const key = identity(destination);\n const first = claimed.get(key);\n if (first !== undefined) {\n throw new OptionError(\n `\"${inputs[index]}\" and \"${first}\" would both be converted into \"${destination}\", ` +\n `so one would overwrite the other.\\n` +\n `Convert them separately, or rename one of them.`,\n );\n }\n claimed.set(key, inputs[index] as string);\n }\n\n /*\n And refuse one destination sitting inside another.\n\n A recording named `rec.edf` beside a folder named `rec` is enough: their outputs are\n `<out>/rec` and `<out>/rec/inner`, which are not equal, so the check above let both\n through. What happened next depended on which conversion got there first. Each creates\n its own directory with a single non-recursive mkdir but its parents recursively, so\n whichever started second either claimed a directory the other had already made as a\n parent — failing with \"already exists\" — or did not. Five runs in twenty failed that way\n under --jobs 2, converting one recording of two; the same command succeeded the other\n fifteen times.\n\n Sorting first puts an ancestor next to its descendant: anything sorting between them\n shares the same prefix, and would be caught as its own adjacent pair.\n */\n /*\n Each destination is checked against its own ancestors, by name.\n\n The first version of this sorted the resolved paths and compared neighbours, on the\n reasoning that an ancestor and its descendant end up adjacent. They do not: the\n separator is not the lowest character, so any sibling whose name begins with one of the\n thirteen printable characters below '/' lands between them. With `rec.edf`, `rec!x.edf`\n and `rec/inner.edf` in one folder, '!' sorts between `out/rec` and `out/rec/inner` and\n the pair was never compared — three recordings converted, one of them inside another's\n directory, reported as \"Converted 3 of 3\".\n\n Walking up from each destination has no such gap. Output trees are shallow, so this is a\n handful of string lookups per recording.\n */\n const byDestination = new Map<string, number>();\n for (const [index, destination] of destinations.entries()) {\n byDestination.set(identity(destination), index);\n }\n\n for (const [index, destination] of destinations.entries()) {\n let ancestor = path.dirname(path.resolve(destination));\n for (let parent = ''; ancestor !== parent; ancestor = path.dirname(ancestor)) {\n parent = ancestor;\n const owner = byDestination.get(identity(ancestor));\n if (owner === undefined || owner === index) continue;\n throw new OptionError(\n `\"${inputs[index]}\" would be converted into \"${destination}\", which is inside ` +\n `\"${destinations[owner]}\" — where \"${inputs[owner]}\" is converted.\\n` +\n `One recording's output cannot sit inside another's. Convert them separately, or ` +\n `rename one of them.`,\n );\n }\n }\n}\n\n/**\n * The exit code for a run in which several files failed for different reasons.\n *\n * 2 means \"you invoked it wrong\" and 1 means \"something went wrong with a file\". When both\n * happened, 1 is the honest answer: the invocation cannot be the whole story once a file has\n * genuinely failed. With a single input there is only ever one code, so its exit status is\n * exactly what it always was.\n */\nfunction worstOf(codes: readonly number[]): number {\n return codes.includes(EXIT_ERROR) ? EXIT_ERROR : EXIT_USAGE;\n}\n\n/**\n * Convert one recording in a separate process, and collect everything it printed.\n *\n * The arguments are rebuilt from the parsed options rather than sliced out of argv, so the\n * child receives exactly the flags this run was given and nothing that only makes sense to\n * the parent: not the other recordings, not --jobs, and not --info or --stdout, neither of\n * which reaches this path.\n */\nasync function convertInChild(\n input: string,\n destination: string,\n values: Record<string, unknown>,\n running: Map<ChildProcess, string>,\n): Promise<{\n code: number;\n out: string;\n err: string;\n report: { converted: number; warnings: number } | null;\n}> {\n /*\n The recording goes last, behind `--`.\n\n As the first argument it was parsed as an option whenever its path began with a dash,\n which `path.join` produces from a folder given as `.` — `./-lead.edf` normalises to\n `-lead.edf`. The child then failed on a file the parent had converted happily, so the\n same command converted two recordings serially and one under --jobs.\n */\n /*\n Option values go in the `--flag=value` form, never as two arguments.\n\n Split across two arguments, a value beginning with a dash is another option as far as the\n child's parser is concerned: `--out ./-nightly` reached it as `--out` followed by\n `-nightly`, and the child died on \"Option '--out' argument is ambiguous\" while the serial\n path converted the same command without complaint. A leading dash is not exotic —\n `path.join` produces one from a folder given as `.`, and directories get named after\n dates and flags often enough. Same failure 0.4.19 fixed for the recording's own path, on\n everything that carries a value rather than on the positional.\n */\n const args = [`--out=${destination}`];\n /*\n --strict is deliberately absent: it is a verdict on the whole run, and a child converting\n one recording is not the whole run. Passing it down made each child announce \"--strict: 1\n warning raised, so this run is reported as a failure\" about its own single file, and exit\n 1 for it, which the parent then counted as a conversion that had not happened. The parent\n applies it once, from the counts the children report.\n */\n for (const flag of ['annotations-only', 'checksum', 'gzip', 'bom', 'force', 'quiet', 'json']) {\n if (values[flag] === true) args.push(`--${flag}`);\n }\n for (const flag of ['start', 'duration', 'end', 'decimals', 'layout']) {\n if (typeof values[flag] === 'string') args.push(`--${flag}=${values[flag] as string}`);\n }\n // --channels is repeatable, and each term is passed as given so that a label containing a\n // comma survives: joining them back into one list would split it in the child.\n const channels = values['channels'];\n if (channels !== undefined) {\n for (const term of Array.isArray(channels) ? (channels as string[]) : [String(channels)]) {\n args.push(`--channels=${term}`);\n }\n }\n args.push('--', input);\n\n return new Promise((resolve) => {\n const child = fork(fileURLToPath(import.meta.url), args, {\n stdio: ['ignore', 'pipe', 'pipe', 'ipc'],\n });\n running.set(child, destination);\n let out = '';\n let err = '';\n let report: { converted: number; warnings: number } | null = null;\n child.on('message', (message: unknown) => {\n const payload = (message as { edf2csv?: { converted: number; warnings: number } })?.edf2csv;\n if (payload) report = payload;\n });\n child.stdout?.setEncoding('utf8').on('data', (chunk: string) => {\n out += chunk;\n });\n child.stderr?.setEncoding('utf8').on('data', (chunk: string) => {\n err += chunk;\n });\n child.on('error', (error) => {\n running.delete(child);\n resolve({ code: EXIT_ERROR, out, err: `${err}error: ${input}: ${error.message}\\n`, report });\n });\n child.on('close', (code, signal) => {\n running.delete(child);\n /*\n A child that was killed says nothing on its way out, so the parent has to.\n\n `code` is null when a process dies by signal, which left the parent with `code ??\n EXIT_ERROR` — a failure with an empty `err`, so the run printed nothing but \"Converted\n 1 of 2 recordings; 1 failed.\" and stopped. Nothing said which recording, nothing said\n why, and nothing said that its directory held a 194 MB signals.csv cut off mid-row\n with no channels.csv beside it. That file looks exactly like a finished one to\n anything that opens it.\n\n The out-of-memory killer, a job scheduler's time limit and `kill` all arrive this way.\n A run stopped from the keyboard is reported by the interrupt handler instead, which\n names every directory at once rather than one line per child.\n */\n if (signal !== null && !stopping) {\n err +=\n `error: stopped by ${signal} before it finished.\\n` +\n ` Incomplete, and should not be used: ${destination}\\n`;\n }\n resolve({ code: code ?? EXIT_ERROR, out, err, report });\n });\n });\n}\n\n/** Put the recording's name into the error lines a child produced. */\nfunction named(text: string, input: string): string {\n // A function, not a string: `$&`, `$\\'`, `` $` `` and `$1` in a replacement string are\n // patterns, and a file may legitimately be called any of them. `bad$&name.edf` re-injected\n // the text it had just matched and reported itself as `baderror: name.edf`.\n return text.replace(/^error: /gmu, () => `error: ${printable(input)}: `);\n}\n\n/** Re-render a child's pretty-printed summary onto one line, leaving anything else alone. */\nfunction compactJson(text: string): string {\n const trimmed = text.trim();\n if (trimmed === '') return '';\n try {\n return `${JSON.stringify(JSON.parse(trimmed))}\\n`;\n } catch {\n // Not the document expected; passing it through unchanged beats losing it.\n return text;\n }\n}\n\n/**\n * Where a conversion's output goes.\n *\n * Serial runs write straight through, which is what they have always done. Running several\n * conversions at once needs the alternative: each one's lines are collected and released in\n * a block when it finishes, so two recordings finishing together cannot interleave a summary\n * with a warning belonging to the other file.\n */\ntype Emit = (stream: 'out' | 'err', text: string) => void;\n\nconst writeThrough: Emit = (stream, text) => {\n (stream === 'out' ? process.stdout : process.stderr).write(text);\n};\n\n/** Collects output so it can be released in one piece when a conversion finishes. */\nfunction buffered(): { emit: Emit; flush: () => void } {\n const parts: [('out' | 'err'), string][] = [];\n return {\n emit: (stream, text) => parts.push([stream, text]),\n flush: () => {\n for (const [stream, text] of parts) writeThrough(stream, text);\n },\n };\n}\n\n/**\n * Report an error the way this command always has, and give the exit code it implies.\n *\n * `input` names the recording while a batch is running: several failures otherwise arrive\n * as a stack of messages with nothing saying which file each belongs to.\n */\nfunction reportError(error: unknown, input?: string, emit: Emit = writeThrough): number {\n const where = input === undefined ? '' : `${printable(input)}: `;\n if (error instanceof EdfError || error instanceof ConversionError) {\n emit('err', `error: ${where}${printableLines(error.message, ' ')}\\n`);\n if (error.hint) emit('err', ` ${error.hint}\\n`);\n // A request the tool cannot carry out is the command line's problem, not the file's,\n // whatever layer noticed it. See USAGE_ERROR_CODES.\n return error instanceof ConversionError && USAGE_ERROR_CODES.has(error.code)\n ? EXIT_USAGE\n : EXIT_ERROR;\n }\n if (\n error instanceof ChannelSelectionError ||\n error instanceof TimeRangeError ||\n error instanceof OptionError\n ) {\n emit('err', `error: ${where}${printableLines(error.message, ' ')}\\n`);\n return EXIT_USAGE;\n }\n emit('err', `error: ${where}${message(error, ' ')}\\n`);\n return EXIT_ERROR;\n}\n\n/**\n * How many conversions to run at once.\n *\n * `auto` is one per core, less one, so a long batch does not take the machine over. A batch\n * is the only place this means anything: a single recording is a single conversion however\n * many jobs are asked for.\n */\nfunction parseJobs(raw: unknown, inputs: number): number {\n if (raw === undefined) return 1;\n const text = String(raw).trim();\n if (text === 'auto') return Math.max(1, Math.min(inputs, cpus().length - 1));\n const value = Number(text);\n if (!Number.isInteger(value) || value < 1) {\n throw new OptionError(`--jobs must be a whole number of 1 or more, or \"auto\", got \"${text}\".`);\n }\n return Math.min(value, Math.max(1, inputs));\n}\n\nfunction splitChannels(raw: unknown): string[] | undefined {\n if (raw === undefined) return undefined;\n const list = Array.isArray(raw) ? (raw as string[]) : [String(raw)];\n const terms = list.flatMap((entry) => entry.split(',')).map((t) => t.trim()).filter((t) => t !== '');\n // Returning undefined here would mean \"no --channels given\" and convert everything,\n // which is the opposite of what someone passing an empty list is asking for.\n if (terms.length === 0) {\n throw new OptionError('--channels was given but lists no channel names.');\n }\n return terms;\n}\n\nfunction optionalTime(raw: unknown, option: string): number | undefined {\n if (raw === undefined) return undefined;\n return parseTimeSpec(String(raw), option);\n}\n\n/** `--layout`, which is one of two words and not a guess at what was meant. */\nfunction optionalLayout(raw: unknown): 'wide' | 'long' | undefined {\n if (raw === undefined) return undefined;\n if (raw === 'wide' || raw === 'long') return raw;\n throw new OptionError(`--layout must be \"wide\" or \"long\", got \"${String(raw)}\".`);\n}\n\nfunction optionalDecimals(raw: unknown): number | undefined {\n if (raw === undefined) return undefined;\n const text = String(raw).trim();\n // Number('') is 0, which would quietly round every physical value to a whole number.\n if (text === '') {\n throw new OptionError('--decimals needs a number, for example --decimals 3.');\n }\n const value = Number(text);\n if (!Number.isInteger(value) || value < 0 || value > 20) {\n throw new OptionError(`--decimals must be a whole number between 0 and 20, got \"${String(raw)}\".`);\n }\n return value;\n}\n\n/*\n Error text is escaped for the same reason --info's table is: a fatal header error quotes\n the channel label that caused it, and that label is free text out of the file. A recording\n declaring a negative sample count under a label containing `\\x1b[2J` cleared the reader's\n screen on the way out.\n*/\nfunction message(error: unknown, indent = ''): string {\n // The indent lines continuation up under an \"error: \" prefix. Usage problems print without\n // one, alongside the usage text, so they pass nothing and stay flush left.\n return printableLines(error instanceof Error ? error.message : String(error), indent);\n}\n\nexport { defaultOutputDir };\n\n/**\n * Whether this file was executed rather than imported.\n *\n * npm installs a bin as a symlink (node_modules/.bin/edf2csv -> ../edf2csv/dist/cli.js),\n * and that is the path `npx` runs. In that case process.argv[1] is the symlink while\n * import.meta.url is already resolved to the real file, so comparing the two directly\n * reports \"imported\" and the command silently does nothing. Both sides are resolved\n * through realpath before comparing.\n */\nfunction isMainModule(): boolean {\n const entry = process.argv[1];\n if (entry === undefined) return false;\n try {\n return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));\n } catch {\n // An unreadable or deleted entry path is not a reason to refuse to run.\n return pathToFileURL(entry).href === import.meta.url;\n }\n}\n\nconst invokedDirectly = isMainModule();\n\nif (invokedDirectly) {\n main(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code;\n })\n .catch((error: unknown) => {\n process.stderr.write(`error: ${message(error)}\\n`);\n process.exitCode = EXIT_ERROR;\n });\n}\n"]}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * Command-line entry point.\n *\n * Output discipline: anything that is the *result* of a command goes to stdout\n * (`--info`'s channel table, `--json`'s summary). Progress, warnings and the\n * conversion summary go to stderr, so a conversion can be run in a pipeline without\n * its chatter contaminating the data.\n */\n\nimport { parseArgs } from 'node:util';\nimport { pathToFileURL, fileURLToPath } from 'node:url';\nimport { realpathSync } from 'node:fs';\nimport { lstat, readdir, realpath, stat } from 'node:fs/promises';\nimport { cpus } from 'node:os';\nimport { fork } from 'node:child_process';\nimport type { ChildProcess } from 'node:child_process';\nimport path from 'node:path';\nimport process from 'node:process';\n\nimport { EdfError } from './edf/errors.js';\nimport { EdfFile } from './edf/reader.js';\nimport { buildPlan, withoutFileRateWarning } from './convert/plan.js';\nimport { ConversionError, USAGE_ERROR_CODES, convert, defaultOutputDir } from './convert/run.js';\nimport { ChannelSelectionError } from './convert/channels.js';\n// Shared with the library so a bad option is the same error whichever way it arrived.\nimport { OptionError } from './convert/options.js';\nimport { TimeRangeError, parseTimeSpec } from './convert/time-range.js';\nimport { deriveRecordStarts } from './convert/timing.js';\nimport { formatDiagnostics, formatInfo, infoJson, formatSummary, printable, printableLines, summaryJson } from './cli/report.js';\nimport { listed } from './format/list.js';\nimport { VERSION } from './version.js';\n\nconst USAGE = `edf2csv ${VERSION}\nConvert EDF, EDF+ and BDF recordings to CSV\n\nUsage\n edf2csv <recording.edf | folder> [more ...] [options]\n\nOptions\n -i, --info Show the recording's structure and estimated output size,\n without converting anything\n -o, --out <dir> Output directory (default: <recording>_csv beside the input)\n -c, --channels <list> Only these channels, comma-separated. Use #N to pick a\n channel by position when two share a label\n --start <time> Begin at this offset (30s, 5m, 1h30m, 00:30:00)\n --duration <time> Convert this much\n --end <time> Stop at this offset (instead of --duration)\n --annotations-only Write only the EDF+ annotations, no signal data\n --decimals <n> Fix the decimal places instead of deriving them per channel\n --checksum Record a SHA-256 of the input in metadata.json\n --layout <kind> wide (default): one column per channel, one file per\n sampling rate. long: one file of time_s,channel,value,\n every rate together, one row per sample\n --gzip Compress every CSV, writing .csv.gz files\n --bom Start each CSV with a UTF-8 byte order mark, so Excel\n reads accented text and units like µV correctly\n -j, --jobs <n> Convert this many recordings at once, or \"auto\" (default: 1)\n -f, --force Overwrite the output directory if it exists\n -q, --quiet Suppress the summary; warnings and errors still print\n --json Print machine-readable JSON to stdout (works with --info too)\n --strict Exit 1 if the recording raised any warning\n --stdout Write the signal CSV to stdout instead of a directory\n (one table only: one sampling rate, or --layout long)\n -h, --help Show this help\n -V, --version Show the version\n\nSeveral recordings\n A folder is expanded to every .edf and .bdf inside it, at any depth, and the\n layout is kept: recordings in sub-folders come out in sub-folders.\n Pass more than one and each is converted in turn. Without --out each lands\n beside itself as usual; with --out that directory becomes the parent and each\n recording gets its own inside it. A file that cannot be read is reported and\n the rest still convert, with a non-zero exit at the end. --jobs converts\n several at once, which is worth it for a folder of them.\n\nOutput\n A directory containing signals.csv, channels.csv, metadata.json, and\n annotations.csv when the recording carries EDF+ annotations. Channels recorded\n at different sampling rates are written to separate files, never resampled.\n With --gzip each CSV becomes a .csv.gz; metadata.json stays plain text so the\n directory can still be read at a glance. With --bom each CSV starts with a\n UTF-8 byte order mark and metadata.json does not, since JSON.parse rejects one.\n With --layout long every channel goes into one signals.csv as time_s, channel\n and value, in time order, whatever rates the recording mixes — which is also\n the one arrangement --stdout can stream for a mixed-rate file.\n\nExamples\n edf2csv recording.edf\n edf2csv recording.edf --info\n edf2csv recording.edf --channels \"EEG Fpz-Cz,ECG\" --out ./converted\n edf2csv recording.edf --start 30m --duration 5m\n edf2csv recording.edf --annotations-only\n edf2csv /data/*.edf --out ./converted\n edf2csv /data/*.edf --out ./converted --jobs auto\n edf2csv /data/study --out ./converted --jobs auto\n`;\n\nconst EXIT_OK = 0;\nconst EXIT_ERROR = 1;\nconst EXIT_USAGE = 2;\n\n/**\n * Whether this run is already shutting its children down.\n *\n * A child dying by signal is normally worth a line of its own, but not when this process is\n * the one that killed it: the interrupt handler names every abandoned directory in a single\n * message, and a per-child line underneath it would say the same thing again, once per job.\n */\nlet stopping = false;\n\n/**\n * Piping into a consumer that exits early (`| head -1`) closes our stdout, and the\n * next write raises EPIPE. That is normal in a shell pipeline, not an error worth a\n * stack trace, so it is swallowed while anything else still surfaces.\n */\nfunction ignoreBrokenPipe(stream: NodeJS.WriteStream): void {\n stream.on('error', (error: NodeJS.ErrnoException) => {\n if (error.code === 'EPIPE') {\n // A reader closing early (`edf2csv ... --info | head -5`) is not a failure, so the\n // error is swallowed rather than thrown. It deliberately does NOT set an exit code:\n // forcing 0 here would erase a real failure whenever the pipe happened to close\n // after the run had already failed, reporting success for a conversion that did\n // not happen. Node exits 0 on its own when nothing sets a code.\n return;\n }\n\n /*\n Anything else is a real write failure, and this used to rethrow it.\n\n The throw lands on a nextTick, outside whatever try/catch the conversion is running\n inside, so it became an uncaught exception: `--stdout` redirected onto a full disk\n died with a raw stack trace headed `dist/cli.js: throw error;` and lost the warning\n that the CSV it had already produced was truncated. The same failure through `--out`\n printed the ordinary message, and so did `--stdout` through the library API, which\n never registers this listener — the designed path exists and works, and this was\n preempting it.\n\n A second listener means the conversion's writer is watching this stream and will\n turn the failure into that ordinary message. Saying it here too would report one\n failure twice, so this only speaks when nothing else will.\n */\n if (stream.listenerCount('error') > 1) return;\n process.stderr.write(`error: Writing to ${stream === process.stdout ? 'stdout' : 'stderr'} failed: ${error.message}\\n`);\n process.exitCode = EXIT_ERROR;\n });\n}\n\nexport async function main(argv: readonly string[]): Promise<number> {\n ignoreBrokenPipe(process.stdout);\n ignoreBrokenPipe(process.stderr);\n\n let values: Record<string, unknown>;\n let positionals: string[];\n\n try {\n const parsed = parseArgs({\n args: [...argv],\n allowPositionals: true,\n strict: true,\n options: {\n info: { type: 'boolean', short: 'i' },\n out: { type: 'string', short: 'o' },\n channels: { type: 'string', short: 'c', multiple: true },\n start: { type: 'string' },\n duration: { type: 'string' },\n end: { type: 'string' },\n 'annotations-only': { type: 'boolean' },\n decimals: { type: 'string' },\n checksum: { type: 'boolean' },\n layout: { type: 'string' },\n gzip: { type: 'boolean' },\n bom: { type: 'boolean' },\n jobs: { type: 'string', short: 'j' },\n force: { type: 'boolean', short: 'f' },\n quiet: { type: 'boolean', short: 'q' },\n json: { type: 'boolean' },\n strict: { type: 'boolean' },\n stdout: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean', short: 'V' },\n },\n });\n values = parsed.values as Record<string, unknown>;\n positionals = parsed.positionals;\n } catch (error) {\n process.stderr.write(`${message(error)}\\n\\nRun edf2csv --help to see the options.\\n`);\n return EXIT_USAGE;\n }\n\n if (values['help'] === true) {\n process.stdout.write(USAGE);\n return EXIT_OK;\n }\n if (values['version'] === true) {\n process.stdout.write(`${VERSION}\\n`);\n return EXIT_OK;\n }\n\n if (positionals.length === 0) {\n process.stderr.write(`No input file given.\\n\\n${USAGE}`);\n return EXIT_USAGE;\n }\n\n let expanded: Input[];\n let unreadable: string[];\n let namedDirectory: boolean;\n try {\n const found = await expandInputs(positionals);\n expanded = found.inputs;\n unreadable = found.unreadable;\n namedDirectory = found.namedDirectory;\n } catch (error) {\n return reportError(error);\n }\n\n // Reported before anything is converted, so it cannot be lost among the summaries, and\n // counted against the run so the exit code does not call a partial sweep a success.\n for (const entry of unreadable) {\n process.stderr.write(\n `error: ${printable(entry)}: could not be read, so any recordings inside it were skipped.\\n`,\n );\n }\n\n if (expanded.length === 0) {\n /*\n \"None here\" and \"could not look\" are different answers, and so are their exit codes.\n\n A folder the process cannot read gave the same exit 2 and the same \"No EDF or BDF\n recordings found\" as an empty one — while the line above it said the folder could not\n be read. Exit 2 is this tool's code for \"the command itself was wrong\", so a script\n was being told to fix its arguments when what needed fixing was a permission. The\n command was fine; the filesystem refused.\n\n And the sentence itself claimed a fact the run is in no position to state: nothing was\n found because nothing was looked at.\n */\n if (unreadable.length > 0) {\n process.stderr.write(\n `Nothing could be converted: ${unreadable.length === 1 ? 'that path' : 'those paths'} ` +\n `could not be read, so whether ${unreadable.length === 1 ? 'it holds' : 'they hold'} ` +\n `recordings is unknown.\\n`,\n );\n return EXIT_ERROR;\n }\n process.stderr.write(\n `No EDF or BDF recordings found in ${listed(positionals.map((p) => `\"${p}\"`))}.\\n`,\n );\n return EXIT_USAGE;\n }\n const inputs = expanded.map((entry) => entry.path);\n /*\n A batch is what you asked for, not what happened to be there.\n\n Counting the recordings made the shape of the run depend on the contents of a folder.\n Under --json a study holding one night printed a pretty-printed object and the same study\n holding two printed two compact lines, so a script written against one of them broke on\n the other — and it broke the day a recording was added, not the day the script changed.\n An input going missing did it in reverse. This is the same count 0.4.20 took out of\n `--out` for the same reason.\n */\n const batch = inputs.length > 1 || namedDirectory;\n const quiet = values['quiet'] === true;\n const asJson = values['json'] === true;\n const strict = values['strict'] === true;\n const toStdout = values['stdout'] === true;\n\n // Both of these claim stdout. Allowing them together wrote the CSV and then the summary\n // object onto one stream, producing a document that is neither valid CSV nor valid\n // JSON — and silently, since each half looked right on its own.\n if (toStdout && asJson) {\n process.stderr.write(\n '--stdout and --json both write to stdout, so they cannot be combined.\\n' +\n 'Use --stdout for the CSV, or --json for the summary.\\n',\n );\n return EXIT_USAGE;\n }\n\n /*\n One stream holds one table, for the same reason it holds one recording: concatenating\n them would give a CSV whose rows come from different files with nothing marking where\n one ends. Naming the count makes it obvious a glob was the cause — except when the count\n is one, which happens for a folder holding a single recording. That read \"--stdout writes\n a single CSV, so it cannot take 1 recordings\": ungrammatical, and wrong on its face, since\n one recording is exactly what it can take. What it cannot take is a folder, whose contents\n are not known until they are walked.\n */\n if (toStdout && batch) {\n process.stderr.write(\n inputs.length === 1\n ? `--stdout writes a single CSV, and a folder is converted as a batch even when it ` +\n `holds one recording.\\nName the recording itself — ${inputs[0]} — or convert to a ` +\n `directory instead.\\n`\n : `--stdout writes a single CSV, so it cannot take ${inputs.length} recordings.\\n` +\n `Convert them to directories instead, or run edf2csv once per file.\\n`,\n );\n return EXIT_USAGE;\n }\n\n /*\n Flags that --stdout has nowhere to put.\n\n Both were accepted and dropped in silence. `--out` names a directory that is never\n created, so the run looked like it had written one. `--checksum` is worse than useless:\n the hash is computed before the first record is read, which is a second full pass over\n the input, and then the only file it is ever written to — metadata.json — is not written\n at all. A recording large enough to want a checksum is large enough to notice reading it\n twice for nothing.\n\n Refusing rather than ignoring is what this tool already does for `--stdout --json` and\n `--stdout --annotations-only`.\n */\n for (const [flag, given] of [\n ['--out', values['out'] !== undefined],\n ['--checksum', values['checksum'] === true],\n ] as const) {\n if (toStdout && given) {\n process.stderr.write(\n `--stdout and ${flag} cannot be combined: --stdout writes no files, and ${flag} has ` +\n `nothing to act on.\\nDrop ${flag}, or drop --stdout and convert to a directory.\\n`,\n );\n return EXIT_USAGE;\n }\n }\n\n /*\n One recording prints the document it always printed; several print one per line.\n\n Pretty-printed objects run together are still readable by a streaming JSON parser, but\n not by anything that reads a record per line — and a batch is exactly where that is\n wanted. Emitting a batch in the single-file shape would repeat 0.2.28's mistake of\n putting two documents on one stream and leaving the caller to work out the boundary.\n\n null, not undefined: the serialisers default this argument to 2, and a default parameter\n takes effect for undefined, which silently gave a batch the indentation it was supposed\n to be dropping.\n */\n const jsonIndent = batch ? null : 2;\n\n try {\n const outOption = typeof values['out'] === 'string' ? values['out'] : undefined;\n const destinations = destinationsFor(expanded, outOption, batch);\n assertDistinct(inputs, destinations);\n\n const shared = {\n channels: splitChannels(values['channels']),\n start: optionalTime(values['start'], '--start'),\n startText: typeof values['start'] === 'string' ? values['start'] : undefined,\n duration: optionalTime(values['duration'], '--duration'),\n end: optionalTime(values['end'], '--end'),\n endText: typeof values['end'] === 'string' ? values['end'] : undefined,\n decimals: optionalDecimals(values['decimals']),\n annotationsOnly: values['annotations-only'] === true,\n gzip: values['gzip'] === true,\n bom: values['bom'] === true,\n layout: optionalLayout(values['layout']),\n };\n\n // Validated before the --info branch, not inside the conversion path: a flag that\n // cannot be honoured is a usage error whatever mode it was given in, and accepting\n // \"--jobs 0\" in silence under --info is the kind of quiet that this tool avoids.\n // Parsed before it is overridden: --stdout converts one recording however many jobs\n // were asked for, but \"--stdout --jobs 0\" is still a request that cannot be honoured,\n // and accepting it in silence is the thing 0.4.2 fixed for --info.\n const requestedJobs = parseJobs(values['jobs'], inputs.length);\n const jobs = toStdout ? 1 : requestedJobs;\n\n if (values['info'] === true) {\n const failures: number[] = [];\n let warnings = 0;\n for (const [index, input] of inputs.entries()) {\n // A blank line between reports, so several tables read as one document.\n if (batch && !asJson && index > 0) process.stdout.write('\\n');\n try {\n warnings += await showInfo(input as string, shared, asJson, jsonIndent);\n } catch (error) {\n failures.push(reportError(error, batch ? (input as string) : undefined));\n }\n }\n if (unreadable.length > 0) failures.push(EXIT_ERROR);\n if (failures.length > 0) return worstOf(failures);\n return strict && warnings > 0 ? EXIT_ERROR : EXIT_OK;\n }\n\n // The meter redraws one line in place, which two conversions cannot share. Running them\n // at once replaces it with the completion count each file prints as it lands.\n const showProgress =\n !quiet && !asJson && !toStdout && jobs === 1 && process.stderr.isTTY === true;\n\n const failures: number[] = [];\n let warnings = 0;\n let converted = 0;\n\n /** Convert input `index`, sending its output wherever the caller wants it. */\n const runOne = async (index: number, emit: Emit): Promise<void> => {\n const input = inputs[index] as string;\n const destination = destinations[index] as string;\n // Which recording this is, before anything it prints. Without it a batch produces a\n // stack of summaries and errors with nothing saying which file each belongs to.\n if (batch && !quiet && !asJson) {\n emit('err', `[${index + 1}/${inputs.length}] ${printable(input)}\\n`);\n }\n try {\n warnings += await convertOne(\n input,\n destination,\n {\n ...shared,\n // With one input and no --out, convert() derives the default itself, exactly as\n // it did before batches existed.\n outputDir: outOption === undefined && !batch ? undefined : destination,\n checksum: values['checksum'] === true,\n force: values['force'] === true,\n toStdout,\n quiet,\n asJson,\n showProgress,\n jsonIndent,\n },\n emit,\n );\n converted++;\n } catch (error) {\n /*\n A batch keeps going. One unreadable recording among five hundred is a reason to\n report that file, not to abandon the ones already converted and refuse the rest.\n The exit code still reports the run as failed, and the closing line says how many\n of them made it, so nothing about the failure is quiet.\n */\n failures.push(reportError(error, batch ? input : undefined, emit));\n }\n };\n\n if (jobs === 1) {\n for (let index = 0; index < inputs.length; index++) {\n await runOne(index, writeThrough);\n // A single recording keeps the exit code it has always had rather than the batch's.\n if (!batch && failures.length > 0) return failures[0] as number;\n }\n } else {\n /*\n Real processes, not concurrent promises.\n\n Converting is almost entirely arithmetic and string building — 1.17 s of CPU for\n 1.24 s of wall clock on a 168 MB conversion — and Node runs that on one thread. An\n in-process pool was tried first and gained 6% on eight recordings, which is the\n overlap in the file reads and nothing else. Each conversion is already a whole\n command, so each one gets its own process, which is what `xargs -P` would do by hand.\n\n Workers take the next recording as they free up rather than splitting the list into\n equal shares. Recordings in a folder differ wildly in length, and a fixed split\n leaves every worker but one idle behind the longest file.\n\n Each child's output is held until it exits, so two finishing together cannot\n interleave one's summary with the other's warnings.\n */\n const running = new Map<ChildProcess, string>();\n\n /*\n Ctrl-C in a terminal reaches every process in the group, so the children would stop\n anyway; a signal sent to this process alone does not, and that is how a batch gets\n run from a script or a CI job. Interrupting one left four conversions writing\n gigabytes into a directory their owner believed abandoned, and the last thing on\n screen was a successful \"Done in 1.6s\" from whichever recording had just landed —\n the run read as if it had finished.\n */\n const onInterrupt = (signal: NodeJS.Signals): void => {\n stopping = true;\n const abandoned = [...running.values()];\n for (const child of running.keys()) child.kill('SIGTERM');\n process.stderr.write(\n `\\ninterrupted (${signal}): ${abandoned.length} conversion` +\n `${abandoned.length === 1 ? '' : 's'} stopped part way through.\\n` +\n (abandoned.length > 0\n ? ` Incomplete, and should not be used: ${listed(abandoned)}\\n`\n : ''),\n );\n process.exit(signal === 'SIGINT' ? 130 : 143);\n };\n process.once('SIGINT', onInterrupt);\n process.once('SIGTERM', onInterrupt);\n\n let next = 0;\n const worker = async (): Promise<void> => {\n for (let index = next++; index < inputs.length; index = next++) {\n const input = inputs[index] as string;\n const sink = buffered();\n if (!quiet && !asJson) {\n sink.emit('err', `[${index + 1}/${inputs.length}] ${printable(input)}\\n`);\n }\n const child = await convertInChild(\n input,\n destinations[index] as string,\n values,\n running,\n );\n // The report says what the child actually did; the exit code says whether it\n // got there. A child that converted and warned has a report and exits 0, since\n // --strict is the parent's to apply.\n if (child.report) {\n converted += child.report.converted;\n warnings += child.report.warnings;\n } else if (child.code === 0) {\n converted++;\n }\n if (child.code !== 0) failures.push(child.code);\n // The child saw one recording, so it printed the indented document a single\n // conversion prints. A batch is one object per line.\n sink.emit('out', asJson ? compactJson(child.out) : child.out);\n // The child converted a single recording, so it named no file in its errors the\n // way a batch does. Naming it here keeps the two paths identical to a reader and\n // to anything grepping a log, where the [n/m] header may not be alongside.\n sink.emit('err', named(child.err, input));\n sink.flush();\n }\n };\n try {\n await Promise.all(Array.from({ length: jobs }, () => worker()));\n } finally {\n process.off('SIGINT', onInterrupt);\n process.off('SIGTERM', onInterrupt);\n }\n }\n\n if (batch && !quiet && !asJson) {\n process.stderr.write(\n `\\nConverted ${converted} of ${inputs.length} recordings` +\n `${failures.length > 0 ? `; ${failures.length} failed` : ''}.\\n`,\n );\n }\n\n /*\n A parent that forked this process needs the counts, not just an exit status.\n\n An exit code cannot separate \"converted, and raised warnings\" from \"did not convert\",\n and under --strict those are the same code. The parent read it as a failure, so a\n parallel run of two recordings — one of which merely warned — reported \"Converted 1 of\n 2 recordings; 1 failed\" for a run in which both converted, while the serial path said\n \"Converted 2 of 2\". `process.send` exists only when this process was forked with a\n channel, so nothing changes for an ordinary invocation.\n */\n process.send?.({ edf2csv: { converted, warnings } });\n\n if (unreadable.length > 0) failures.push(EXIT_ERROR);\n if (failures.length > 0) return worstOf(failures);\n\n /*\n --strict turns any warning into a non-zero exit, for pipelines that would rather stop\n than proceed on a recording the tool had something to say about.\n\n The output is still written. A warning describes the recording, not a failure to\n convert it — a truncated file converts correctly for the records that are there — so\n destroying that work would be the wrong response. The exit code is the signal; what\n to do about it is the caller's decision, and they still have the files to inspect.\n */\n if (strict && warnings > 0) {\n process.stderr.write(\n `\\n--strict: ${warnings} warning${warnings === 1 ? '' : 's'} raised, so this run is ` +\n `reported as a failure. The output was still written.\\n`,\n );\n return EXIT_ERROR;\n }\n return EXIT_OK;\n } catch (error) {\n return reportError(error);\n }\n}\n\n/** Print one recording's `--info`, and return how many diagnostics it raised. */\nasync function showInfo(\n input: string,\n shared: Record<string, unknown>,\n asJson: boolean,\n jsonIndent: number | null,\n): Promise<number> {\n const file = await EdfFile.open(input);\n try {\n /*\n Read the annotation channel only when the timing actually depends on it.\n\n --info is documented as a header-only summary that returns immediately whatever\n the file's size, but it called readAnnotations() on every EDF+/BDF+ file — a seek\n into every data record. deriveRecordStarts discards that data unless the file is\n EDF+D, where record start times are stored rather than arithmetic, so on a\n continuous recording the whole scan was thrown away. It cost 0.29 s on a 12 MB\n file and scaled with record count.\n\n A discontinuous file still needs the scan: without it the reported span and row\n estimate are wrong, which is a bug that has already been fixed once here.\n */\n const hasAnnotations = file.annotationSignals.length > 0;\n const needsEveryRecordStart = file.header.continuity === 'EDF+D' && hasAnnotations;\n /*\n A continuous recording needs its origin too, which is one read rather than one per\n record: 0.4.9 made its first record's timekeeping TAL the point the samples are timed\n from, and this report went on placing a requested window against zero. `--info --start 1`\n predicted 8 rows where the conversion wrote 10, on a file whose discontinuous twin —\n identical but for the reserved field — agreed with itself.\n */\n const annotationData = needsEveryRecordStart\n ? await file.readAnnotations()\n : {\n annotations: [],\n recordStarts:\n file.header.continuity === 'EDF+C' && hasAnnotations\n ? [await file.readOrigin()]\n : [],\n malformed: 0,\n };\n const timing = deriveRecordStarts(file, annotationData);\n const plan = buildPlan(\n {\n signals: file.header.signals,\n recordDuration: file.header.recordDuration,\n recordCount: file.recordCount,\n hasAnnotationChannel: file.annotationSignals.length > 0,\n recordStarts: timing.starts,\n },\n shared,\n );\n plan.diagnostics.push(...timing.diagnostics);\n process.stdout.write(\n asJson ? `${infoJson(file, plan, jsonIndent)}\\n` : `${formatInfo(file, plan)}\\n`,\n );\n\n // Under --json the warnings travel inside the document, exactly as they do for a\n // conversion, so stderr stays empty and the whole result is one parseable thing.\n const diagnostics = [...withoutFileRateWarning(file.diagnostics), ...plan.diagnostics];\n if (!asJson && diagnostics.length > 0) {\n process.stderr.write(`\\n${formatDiagnostics(diagnostics)}\\n`);\n }\n return diagnostics.length;\n } finally {\n await file.close();\n }\n}\n\n/** Convert one recording, and return how many diagnostics it raised. */\nasync function convertOne(\n input: string,\n destination: string,\n options: Record<string, unknown> & {\n quiet: boolean;\n asJson: boolean;\n showProgress: boolean;\n toStdout: boolean;\n jsonIndent: number | null;\n },\n emit: Emit = writeThrough,\n): Promise<number> {\n const { quiet, asJson, showProgress, toStdout } = options;\n let lastTick = 0;\n\n /*\n Interrupting a conversion leaves a CSV that stops mid-recording but is still\n perfectly well-formed, so nothing about the file itself reveals that half the\n data is missing. Saying so on the way out is the whole point of a tool that\n claims it will not go quiet when something is wrong.\n */\n const onInterrupt = (signal: NodeJS.Signals): void => {\n if (showProgress) process.stderr.write('\\r\\u001b[K');\n // --stdout writes no directory, so there is none to warn about. Naming one anyway\n // pointed at a path that was never created — the same \"files that were never written\"\n // that 0.2.30 removed from this path's error message.\n process.stderr.write(\n `\\ninterrupted (${signal}): the conversion stopped part way through.\\n` +\n (toStdout\n ? ` The CSV on stdout stops mid-recording and should not be used.\\n`\n : ` Files already written to \"${destination}\" are incomplete and should not be used.\\n`),\n );\n // 128 + signal number, the conventional exit status for dying to a signal.\n process.exit(signal === 'SIGINT' ? 130 : 143);\n };\n process.once('SIGINT', onInterrupt);\n process.once('SIGTERM', onInterrupt);\n\n try {\n const result = await convert(input, {\n ...options,\n onProgress: showProgress\n ? (progress): void => {\n const now = Date.now();\n if (now - lastTick < 100) return;\n lastTick = now;\n const percent =\n progress.recordsTotal === 0\n ? 100\n : Math.floor((progress.recordsDone / progress.recordsTotal) * 100);\n process.stderr.write(`\\r converting… ${percent}%`);\n }\n : undefined,\n });\n\n if (showProgress) process.stderr.write('\\r\\u001b[K');\n\n if (result.diagnostics.length > 0 && !asJson) {\n emit('err', `${formatDiagnostics(result.diagnostics)}\\n\\n`);\n }\n if (asJson) {\n // One object per line, so a batch is JSON Lines — `jq` reads a record at a time\n // rather than waiting for the whole run to finish.\n emit('out', `${summaryJson(result, options.jsonIndent as number | null)}\\n`);\n } else if (!quiet) {\n // With --stdout there is no directory to summarise, and the row count is the only\n // thing worth saying — on stderr, so the CSV on stdout stays clean.\n if (toStdout) {\n const rows = result.files[0]?.rows ?? 0;\n /*\n A reader that closed the pipe did not receive a conversion, so it does not get a\n conversion's summary. `edf2csv rec.edf --stdout | head -1` announced \"Wrote 52,507\n rows to stdout\" — a number that is neither the recording's 102,400 nor the one row\n head took, but however many had been formatted before the close was noticed.\n\n Whether the *conversion* stopped early is a separate question from whether the\n reader did, and 0.5.12 answered the first with the second. A 10,000-row recording\n whose CSV outruns the pipe buffer but fits one flush is written in full and only\n then meets the closed pipe: every row formatted, every row handed over, and the\n summary said \"The recording was not converted in full.\" The estimate's row count is\n exact, so the two cases can be told apart and told apart honestly — what reached\n the reader is not knowable from this side either way.\n */\n const expected = result.plan.estimate.rows;\n emit(\n 'err',\n !result.readerHungUp\n ? `Wrote ${rows.toLocaleString('en-US')} rows to stdout.\\n`\n : rows < expected\n ? `Stopped: the reader closed the pipe after ${rows.toLocaleString('en-US')} of ` +\n `${expected.toLocaleString('en-US')} rows had been written. The recording was ` +\n `not converted in full.\\n`\n : `Wrote ${rows.toLocaleString('en-US')} rows to stdout, but the reader closed ` +\n `the pipe before the end, so not all of them reached it.\\n`,\n );\n } else {\n emit('err', `${formatSummary(result)}\\n`);\n }\n }\n return result.diagnostics.length;\n } finally {\n process.off('SIGINT', onInterrupt);\n process.off('SIGTERM', onInterrupt);\n }\n}\n\n/** A recording to convert, and the path its output should be named after. */\ninterface Input {\n /** Where the recording is. */\n path: string;\n /**\n * What to call its output, relative to `--out`.\n *\n * For a file named on the command line this is just its own name. For one found by\n * expanding a directory it keeps the position it had inside that directory, so a study\n * laid out as one folder per night comes out the same shape — and, more to the point, so\n * that fifty recordings all named `rec.edf` do not all claim `<out>/rec`.\n */\n name: string;\n /**\n * Whether this recording was found by expanding a directory rather than named directly.\n *\n * `--out` means two different things — the output directory itself, or a parent to put one\n * directory per recording inside — and this is what decides which. See `destinationsFor`.\n */\n fromDirectory: boolean;\n}\n\n/**\n * Recordings to convert, with directories expanded to what is inside them.\n *\n * A directory yields every `.edf` and `.bdf` beneath it, at any depth. Recordings arrive\n * organised into folders, and a shell has no tidy way to reach them — which is why the\n * recipes here carried a `find` incantation to do it. Passing the folder is the obvious\n * thing to try, and it used to fail with \"is a directory, not an EDF file\".\n */\nasync function expandInputs(\n positionals: readonly string[],\n): Promise<{ inputs: Input[]; unreadable: string[]; namedDirectory: boolean }> {\n const found: Input[] = [];\n const unreadable: string[] = [];\n /*\n Whether a directory was NAMED, which is not the same as whether one yielded anything.\n\n `fromDirectory` on the inputs answers the second question, and using it for the first put\n the 0.4.20 defect back one step along: `edf2csv study named.edf --out csv` wrote\n csv/named/ while the study held recordings and csv/signals.csv once it held none, because\n the surviving input then looked like a lone file. Whether some unrelated folder happens\n to contain anything decided where a different recording's output went.\n */\n let namedDirectory = false;\n for (const given of positionals) {\n const info = await stat(given).catch(() => null);\n if (info === null || !info.isDirectory()) {\n // Anything that is not a directory is passed through untouched, so a file that does\n // not exist still reports itself rather than vanishing from the list.\n found.push({ path: given, name: path.basename(given), fromDirectory: false });\n continue;\n }\n namedDirectory = true;\n const walked = await walk(given);\n unreadable.push(...walked.unreadable);\n for (const file of walked.files) {\n found.push({ path: file, name: path.relative(given, file), fromDirectory: true });\n }\n }\n /*\n One recording, however many ways it was named.\n\n The walk already does this for links inside a folder, and the two halves disagreed:\n `edf2csv study` converted a recording reached twice once, while `edf2csv a.edf a.edf`\n refused the whole run for the collision it would cause. A shell makes the second easy\n to produce by accident — `edf2csv *.edf recording.edf` — and there is nothing ambiguous\n about it: it is one recording, and converting it once is what was meant.\n\n A path that does not resolve keeps its own identity so that a file which is not there\n still reports itself rather than being folded into another entry.\n\n Which of the names survives is decided by the names themselves, not by the order they\n turned up in. Keeping the first arrival meant the output directory was named by argument\n order — `edf2csv data/one.edf data/alias.edf` wrote out/one and the same two swapped\n wrote out/alias — and a shell orders a glob however it likes. Inside a folder it was the\n order `readdir` returned, which differs between filesystems, so copying a study to\n another machine could rename its output. A recording that is not a link is preferred over\n a link to it, since that is the name the recording actually has; two of a kind are\n settled by the path that sorts first.\n */\n const identified: { entry: Input; identity: string; link: boolean }[] = [];\n for (const entry of found) {\n const identity = await realpath(entry.path).catch(() => `?${path.resolve(entry.path)}`);\n const own = await lstat(entry.path).catch(() => null);\n identified.push({ entry, identity, link: own?.isSymbolicLink() ?? false });\n }\n\n const byIdentity = new Map<string, Input>();\n const winners = new Map<string, (typeof identified)[number]>();\n for (const candidate of identified) {\n const held = winners.get(candidate.identity);\n if (held !== undefined && !outnames(candidate, held)) continue;\n winners.set(candidate.identity, candidate);\n byIdentity.set(candidate.identity, candidate.entry);\n }\n\n // A directory hands its entries back in whatever order the filesystem stored them.\n const unique = [...byIdentity.values()];\n unique.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));\n\n return { inputs: unique, unreadable, namedDirectory };\n}\n\n/**\n * Which of two names for one recording the output should be called after.\n *\n * A name the recording actually has beats a link pointing at it, and two of a kind are\n * settled by sort order. Both are properties of the names, so the answer does not move when\n * a shell expands a glob differently or a filesystem enumerates a folder in another order.\n */\nfunction outnames(\n candidate: { entry: Input; link: boolean },\n held: { entry: Input; link: boolean },\n): boolean {\n if (candidate.link !== held.link) return !candidate.link;\n if (candidate.entry.path !== held.entry.path) return candidate.entry.path < held.entry.path;\n /*\n One recording, one path, two names — reached directly and through a named folder.\n\n `edf2csv study study/night-01/rec.edf` and the same two swapped are the same request, and\n they disagreed: the folder gives the recording its position inside the folder\n (`night-01/rec`), a direct mention gives it its bare name (`rec`), and whichever spelling\n the loop met first won. So argument order decided the output directory's name, and, once\n a second `study/rec.edf` was in play, decided whether the run happened at all — the bare\n name collides with it and the run is refused, exit 2, while the other order converts both.\n\n The nested name wins. It is what the folder promised — \"the layout is kept: recordings in\n sub-folders come out in sub-folders\" — and it is the one that does not collide, since\n collapsing a recording to its bare name is what puts it on top of a sibling.\n */\n const depth = (entry: Input): number => entry.name.split(path.sep).length;\n if (depth(candidate.entry) !== depth(held.entry)) return depth(candidate.entry) > depth(held.entry);\n return candidate.entry.name < held.entry.name;\n}\n\n/**\n * Every recording under a directory, following symbolic links.\n *\n * Written by hand rather than with a recursive `readdir` because that reports a symlink as\n * a symlink and never as a file, so a linked recording was skipped without a word — and the\n * closing \"converted 3 of 3\" then described the three it had noticed rather than what was in\n * the folder. Data organised by linking recordings into a working directory is ordinary, and\n * quietly converting fewer files than were asked for is the failure this tool exists to\n * avoid. Naming the same link on the command line always worked, which made the omission\n * harder to notice rather than easier.\n *\n * Following links means they can form a cycle, so directories are recorded by their resolved\n * identity and visited once. A link to a file already reached another way is likewise\n * converted once.\n */\nasync function walk(root: string): Promise<{ files: string[]; unreadable: string[] }> {\n const files: string[] = [];\n const unreadable: string[] = [];\n const seen = new Set<string>();\n /*\n A queue, taken from the front, and each directory's children entered in a settled order.\n\n This was a stack popped from the back, so which of two names for one directory was\n visited first came down to the order `readdir` happened to return them — and the loser\n was then skipped as already seen, taking its name out of the run with it. A folder\n holding `aaa-real/` beside `zzz-alias -> aaa-real` converted into `<out>/zzz-alias/`: the\n link's name, chosen by a hash order that differs between filesystems. 0.4.29 settled this\n for two names of one *file* and left the directory above it deciding by accident.\n\n Breadth first, and each directory's sub-directories entered with real names before links\n and alphabetically within each, so the name that survives is a property of the tree: the\n shallowest, then the one that is not a link, then the first in sort order.\n */\n const queue = [root];\n let next = 0;\n\n while (next < queue.length) {\n const directory = queue[next++] as string;\n const real = await realpath(directory).catch(() => directory);\n if (seen.has(real)) continue;\n seen.add(real);\n\n /*\n A directory that cannot be listed is reported, not stepped over.\n\n Skipping it in silence meant a folder holding three recordings, one of them inside a\n sub-directory without read permission, converted two and said \"Converted 2 of 2\n recordings\" — a total that agreed with itself and with nothing else. That is the same\n failure 0.4.4 fixed for symbolic links, arriving by a different route: converting fewer\n recordings than were asked for and reporting success.\n */\n let entries;\n try {\n entries = await readdir(directory, { withFileTypes: true });\n } catch {\n unreadable.push(directory);\n continue;\n }\n\n // A real name before a link to the same thing, then alphabetically. The dirent describes\n // the entry itself rather than its target, which is exactly the question here.\n const ordered = [...entries].sort((a, b) => {\n const link = Number(a.isSymbolicLink()) - Number(b.isSymbolicLink());\n return link !== 0 ? link : a.name < b.name ? -1 : a.name > b.name ? 1 : 0;\n });\n\n for (const entry of ordered) {\n const full = path.join(directory, entry.name);\n // stat, not the dirent: a dirent describes the link, and what matters is its target.\n const info = await stat(full).catch(() => null);\n if (info === null) {\n /*\n Anything that cannot be inspected is reported, whatever it is called.\n\n This used to report only names ending in `.edf` or `.bdf`, on the reasoning that a\n broken link to something else is nobody's business. A directory carries no such\n name. A study kept as one folder per night, with one night linked to an external\n drive, converted the nights that were mounted and said nothing about the one that\n was not — and because losing that input left a single recording, `--out` stopped\n meaning \"a parent to fill\" and started meaning \"the directory to write\", so the\n survivor landed somewhere else as well. Whether a drive happened to be mounted\n changed both what was converted and where it went, in silence, exit 0.\n\n The walk cannot know what was behind a link it cannot follow, which is exactly the\n reason to say so rather than to guess.\n */\n unreadable.push(full);\n continue;\n }\n if (info.isDirectory()) {\n queue.push(full);\n } else if (info.isFile() && /\\.(edf|bdf)$/iu.test(entry.name)) {\n // Duplicates are collapsed once for the whole list in expandInputs; this only has\n // to find them. The `seen` set here is for directory cycles.\n files.push(full);\n }\n }\n }\n return { files, unreadable };\n}\n\n/**\n * Where each recording's output goes.\n *\n * Naming one recording, `--out` is the output directory itself, which is what it has always\n * meant. Naming a folder, or several recordings, it is a parent and each recording gets its\n * own directory inside it, because writing several recordings into one directory would have\n * them overwrite each other's `signals.csv` — the one thing a batch must not do quietly.\n *\n * With no `--out` at all, every recording converts beside itself exactly as it would have\n * done alone, so a glob behaves like the shell loop it replaces.\n *\n * What a folder means is decided by the folder, not by how much is in it. Counting the\n * recordings instead meant `edf2csv study --out csv` wrote `csv/signals.csv` while the study\n * held one night and `csv/night-01/rec/signals.csv` once it held two: adding a recording\n * moved the output of a recording that had not changed. The same count made the destination\n * depend on things no one had touched either — a night on an unmounted drive, a\n * sub-directory that could not be read — so where the data landed turned on the state of the\n * machine rather than on the command.\n */\nfunction destinationsFor(\n inputs: readonly Input[],\n out: string | undefined,\n /** True when a folder was named, or more than one recording was. See `batch` in `main`. */\n batch: boolean,\n): string[] {\n if (out === undefined) return inputs.map((input) => defaultOutputDir(input.path));\n if (!batch) return [out];\n return inputs.map((input) => path.join(out, stemOf(input.name)));\n}\n\n/** A name without its extension, keeping a leading dot: `.hidden.edf` -> `.hidden`. */\nfunction stemOf(name: string): string {\n const dot = name.lastIndexOf('.');\n const slash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\\\'));\n return dot > slash + 1 ? name.slice(0, dot) : name;\n}\n\n/**\n * Refuse a batch in which two recordings would land in the same directory.\n *\n * Two files with the same name in different folders — `night-1/rec.edf` and\n * `night-2/rec.edf`, which is how recordings usually get organised — both resolve to\n * `<out>/rec`. Converting them in turn would leave one recording's data sitting under the\n * other's name with nothing to show it had happened, so the run stops before writing\n * anything at all.\n */\nfunction assertDistinct(inputs: readonly string[], destinations: readonly string[]): void {\n /*\n Two names that differ only in case are one directory on a filesystem that does not\n distinguish them, which is the default on macOS and the rule on Windows.\n\n `a/REC.edf` and `b/rec.edf` produce `<out>/REC` and `<out>/rec`. Compared exactly those\n are different, so both went through, and with --force the second conversion wrote into\n the directory the first had made: one directory holding one recording's `signals.csv`\n beside the other's `signals_256hz.csv`, under a single `metadata.json` naming only one\n of them. The run reported \"Converted 2 of 2 recordings\" and exited 0. A directory whose\n provenance file describes a recording other than the data beside it is the one outcome\n this tool exists to prevent.\n\n The comparison follows the platform rather than being applied everywhere, so a\n case-sensitive filesystem — where those really are two directories — keeps converting\n both. A case-sensitive volume on macOS is the exception it gets wrong, and it gets it\n wrong in the safe direction: a refusal that names both recordings, not a silent merge.\n */\n const foldsCase = process.platform === 'darwin' || process.platform === 'win32';\n /*\n macOS filesystems fold Unicode normalisation as well as case.\n\n HFS+ and APFS compare names in a normalised form, so `café` written as e + U+0301 and\n `café` written as U+00E9 are one directory — while remaining two different JavaScript\n strings, which is all this guard was comparing. Two recordings whose stems differ only\n that way therefore both passed the check and both converted into the same place:\n\n study/café.edf (NFC) -> csv/café/signals.csv\n study/café.bdf (NFD) -> csv/café/signals_256hz.csv, _128hz, _1hz\n\n one directory holding both, under a single metadata.json naming one of them, reported as\n \"Converted 2 of 2 recordings\" and exit 0 under --force. Without --force the second run\n happened to hit \"already exists\", which is the accidental save rather than the check\n doing its job — and it named the wrong problem.\n\n Not folded on Linux, where the two names are genuinely two directories and refusing them\n would be inventing a collision that does not exist. Windows preserves normalisation too,\n so only darwin. This is the same platform-shaped assumption the case fold above already\n makes, and it has the same limit: a network or removable volume that normalises while the\n running platform does not is not covered by either.\n */\n const identity = (destination: string): string => {\n const resolved = path.resolve(destination);\n if (!foldsCase) return resolved;\n const folded = resolved.toLowerCase();\n return process.platform === 'darwin' ? folded.normalize('NFC') : folded;\n };\n\n const claimed = new Map<string, string>();\n for (const [index, destination] of destinations.entries()) {\n const key = identity(destination);\n const first = claimed.get(key);\n if (first !== undefined) {\n throw new OptionError(\n `\"${inputs[index]}\" and \"${first}\" would both be converted into \"${destination}\", ` +\n `so one would overwrite the other.\\n` +\n `Convert them separately, or rename one of them.`,\n );\n }\n claimed.set(key, inputs[index] as string);\n }\n\n /*\n And refuse one destination sitting inside another.\n\n A recording named `rec.edf` beside a folder named `rec` is enough: their outputs are\n `<out>/rec` and `<out>/rec/inner`, which are not equal, so the check above let both\n through. What happened next depended on which conversion got there first. Each creates\n its own directory with a single non-recursive mkdir but its parents recursively, so\n whichever started second either claimed a directory the other had already made as a\n parent — failing with \"already exists\" — or did not. Five runs in twenty failed that way\n under --jobs 2, converting one recording of two; the same command succeeded the other\n fifteen times.\n\n Sorting first puts an ancestor next to its descendant: anything sorting between them\n shares the same prefix, and would be caught as its own adjacent pair.\n */\n /*\n Each destination is checked against its own ancestors, by name.\n\n The first version of this sorted the resolved paths and compared neighbours, on the\n reasoning that an ancestor and its descendant end up adjacent. They do not: the\n separator is not the lowest character, so any sibling whose name begins with one of the\n thirteen printable characters below '/' lands between them. With `rec.edf`, `rec!x.edf`\n and `rec/inner.edf` in one folder, '!' sorts between `out/rec` and `out/rec/inner` and\n the pair was never compared — three recordings converted, one of them inside another's\n directory, reported as \"Converted 3 of 3\".\n\n Walking up from each destination has no such gap. Output trees are shallow, so this is a\n handful of string lookups per recording.\n */\n const byDestination = new Map<string, number>();\n for (const [index, destination] of destinations.entries()) {\n byDestination.set(identity(destination), index);\n }\n\n for (const [index, destination] of destinations.entries()) {\n let ancestor = path.dirname(path.resolve(destination));\n for (let parent = ''; ancestor !== parent; ancestor = path.dirname(ancestor)) {\n parent = ancestor;\n const owner = byDestination.get(identity(ancestor));\n if (owner === undefined || owner === index) continue;\n throw new OptionError(\n `\"${inputs[index]}\" would be converted into \"${destination}\", which is inside ` +\n `\"${destinations[owner]}\" — where \"${inputs[owner]}\" is converted.\\n` +\n `One recording's output cannot sit inside another's. Convert them separately, or ` +\n `rename one of them.`,\n );\n }\n }\n}\n\n/**\n * The exit code for a run in which several files failed for different reasons.\n *\n * 2 means \"you invoked it wrong\" and 1 means \"something went wrong with a file\". When both\n * happened, 1 is the honest answer: the invocation cannot be the whole story once a file has\n * genuinely failed. With a single input there is only ever one code, so its exit status is\n * exactly what it always was.\n */\nfunction worstOf(codes: readonly number[]): number {\n return codes.includes(EXIT_ERROR) ? EXIT_ERROR : EXIT_USAGE;\n}\n\n/**\n * Convert one recording in a separate process, and collect everything it printed.\n *\n * The arguments are rebuilt from the parsed options rather than sliced out of argv, so the\n * child receives exactly the flags this run was given and nothing that only makes sense to\n * the parent: not the other recordings, not --jobs, and not --info or --stdout, neither of\n * which reaches this path.\n */\nasync function convertInChild(\n input: string,\n destination: string,\n values: Record<string, unknown>,\n running: Map<ChildProcess, string>,\n): Promise<{\n code: number;\n out: string;\n err: string;\n report: { converted: number; warnings: number } | null;\n}> {\n /*\n The recording goes last, behind `--`.\n\n As the first argument it was parsed as an option whenever its path began with a dash,\n which `path.join` produces from a folder given as `.` — `./-lead.edf` normalises to\n `-lead.edf`. The child then failed on a file the parent had converted happily, so the\n same command converted two recordings serially and one under --jobs.\n */\n /*\n Option values go in the `--flag=value` form, never as two arguments.\n\n Split across two arguments, a value beginning with a dash is another option as far as the\n child's parser is concerned: `--out ./-nightly` reached it as `--out` followed by\n `-nightly`, and the child died on \"Option '--out' argument is ambiguous\" while the serial\n path converted the same command without complaint. A leading dash is not exotic —\n `path.join` produces one from a folder given as `.`, and directories get named after\n dates and flags often enough. Same failure 0.4.19 fixed for the recording's own path, on\n everything that carries a value rather than on the positional.\n */\n const args = [`--out=${destination}`];\n /*\n --strict is deliberately absent: it is a verdict on the whole run, and a child converting\n one recording is not the whole run. Passing it down made each child announce \"--strict: 1\n warning raised, so this run is reported as a failure\" about its own single file, and exit\n 1 for it, which the parent then counted as a conversion that had not happened. The parent\n applies it once, from the counts the children report.\n */\n for (const flag of ['annotations-only', 'checksum', 'gzip', 'bom', 'force', 'quiet', 'json']) {\n if (values[flag] === true) args.push(`--${flag}`);\n }\n for (const flag of ['start', 'duration', 'end', 'decimals', 'layout']) {\n if (typeof values[flag] === 'string') args.push(`--${flag}=${values[flag] as string}`);\n }\n // --channels is repeatable, and each term is passed as given so that a label containing a\n // comma survives: joining them back into one list would split it in the child.\n const channels = values['channels'];\n if (channels !== undefined) {\n for (const term of Array.isArray(channels) ? (channels as string[]) : [String(channels)]) {\n args.push(`--channels=${term}`);\n }\n }\n args.push('--', input);\n\n return new Promise((resolve) => {\n const child = fork(fileURLToPath(import.meta.url), args, {\n stdio: ['ignore', 'pipe', 'pipe', 'ipc'],\n });\n running.set(child, destination);\n let out = '';\n let err = '';\n let report: { converted: number; warnings: number } | null = null;\n child.on('message', (message: unknown) => {\n const payload = (message as { edf2csv?: { converted: number; warnings: number } })?.edf2csv;\n if (payload) report = payload;\n });\n child.stdout?.setEncoding('utf8').on('data', (chunk: string) => {\n out += chunk;\n });\n child.stderr?.setEncoding('utf8').on('data', (chunk: string) => {\n err += chunk;\n });\n child.on('error', (error) => {\n running.delete(child);\n resolve({ code: EXIT_ERROR, out, err: `${err}error: ${input}: ${error.message}\\n`, report });\n });\n child.on('close', (code, signal) => {\n running.delete(child);\n /*\n A child that was killed says nothing on its way out, so the parent has to.\n\n `code` is null when a process dies by signal, which left the parent with `code ??\n EXIT_ERROR` — a failure with an empty `err`, so the run printed nothing but \"Converted\n 1 of 2 recordings; 1 failed.\" and stopped. Nothing said which recording, nothing said\n why, and nothing said that its directory held a 194 MB signals.csv cut off mid-row\n with no channels.csv beside it. That file looks exactly like a finished one to\n anything that opens it.\n\n The out-of-memory killer, a job scheduler's time limit and `kill` all arrive this way.\n A run stopped from the keyboard is reported by the interrupt handler instead, which\n names every directory at once rather than one line per child.\n */\n if (signal !== null && !stopping) {\n err +=\n `error: stopped by ${signal} before it finished.\\n` +\n ` Incomplete, and should not be used: ${destination}\\n`;\n }\n resolve({ code: code ?? EXIT_ERROR, out, err, report });\n });\n });\n}\n\n/** Put the recording's name into the error lines a child produced. */\nfunction named(text: string, input: string): string {\n // A function, not a string: `$&`, `$\\'`, `` $` `` and `$1` in a replacement string are\n // patterns, and a file may legitimately be called any of them. `bad$&name.edf` re-injected\n // the text it had just matched and reported itself as `baderror: name.edf`.\n return text.replace(/^error: /gmu, () => `error: ${printable(input)}: `);\n}\n\n/** Re-render a child's pretty-printed summary onto one line, leaving anything else alone. */\nfunction compactJson(text: string): string {\n const trimmed = text.trim();\n if (trimmed === '') return '';\n try {\n return `${JSON.stringify(JSON.parse(trimmed))}\\n`;\n } catch {\n // Not the document expected; passing it through unchanged beats losing it.\n return text;\n }\n}\n\n/**\n * Where a conversion's output goes.\n *\n * Serial runs write straight through, which is what they have always done. Running several\n * conversions at once needs the alternative: each one's lines are collected and released in\n * a block when it finishes, so two recordings finishing together cannot interleave a summary\n * with a warning belonging to the other file.\n */\ntype Emit = (stream: 'out' | 'err', text: string) => void;\n\nconst writeThrough: Emit = (stream, text) => {\n (stream === 'out' ? process.stdout : process.stderr).write(text);\n};\n\n/** Collects output so it can be released in one piece when a conversion finishes. */\nfunction buffered(): { emit: Emit; flush: () => void } {\n const parts: [('out' | 'err'), string][] = [];\n return {\n emit: (stream, text) => parts.push([stream, text]),\n flush: () => {\n for (const [stream, text] of parts) writeThrough(stream, text);\n },\n };\n}\n\n/**\n * Report an error the way this command always has, and give the exit code it implies.\n *\n * `input` names the recording while a batch is running: several failures otherwise arrive\n * as a stack of messages with nothing saying which file each belongs to.\n */\nfunction reportError(error: unknown, input?: string, emit: Emit = writeThrough): number {\n const where = input === undefined ? '' : `${printable(input)}: `;\n if (error instanceof EdfError || error instanceof ConversionError) {\n emit('err', `error: ${where}${printableLines(error.message, ' ')}\\n`);\n if (error.hint) emit('err', ` ${error.hint}\\n`);\n // A request the tool cannot carry out is the command line's problem, not the file's,\n // whatever layer noticed it. See USAGE_ERROR_CODES.\n return error instanceof ConversionError && USAGE_ERROR_CODES.has(error.code)\n ? EXIT_USAGE\n : EXIT_ERROR;\n }\n if (\n error instanceof ChannelSelectionError ||\n error instanceof TimeRangeError ||\n error instanceof OptionError\n ) {\n emit('err', `error: ${where}${printableLines(error.message, ' ')}\\n`);\n return EXIT_USAGE;\n }\n emit('err', `error: ${where}${message(error, ' ')}\\n`);\n return EXIT_ERROR;\n}\n\n/**\n * How many conversions to run at once.\n *\n * `auto` is one per core, less one, so a long batch does not take the machine over. A batch\n * is the only place this means anything: a single recording is a single conversion however\n * many jobs are asked for.\n */\nfunction parseJobs(raw: unknown, inputs: number): number {\n if (raw === undefined) return 1;\n const text = String(raw).trim();\n if (text === 'auto') return Math.max(1, Math.min(inputs, cpus().length - 1));\n const value = Number(text);\n if (!Number.isInteger(value) || value < 1) {\n throw new OptionError(`--jobs must be a whole number of 1 or more, or \"auto\", got \"${text}\".`);\n }\n return Math.min(value, Math.max(1, inputs));\n}\n\nfunction splitChannels(raw: unknown): string[] | undefined {\n if (raw === undefined) return undefined;\n const list = Array.isArray(raw) ? (raw as string[]) : [String(raw)];\n const terms = list.flatMap((entry) => entry.split(',')).map((t) => t.trim()).filter((t) => t !== '');\n // Returning undefined here would mean \"no --channels given\" and convert everything,\n // which is the opposite of what someone passing an empty list is asking for.\n if (terms.length === 0) {\n throw new OptionError('--channels was given but lists no channel names.');\n }\n return terms;\n}\n\nfunction optionalTime(raw: unknown, option: string): number | undefined {\n if (raw === undefined) return undefined;\n return parseTimeSpec(String(raw), option);\n}\n\n/** `--layout`, which is one of two words and not a guess at what was meant. */\nfunction optionalLayout(raw: unknown): 'wide' | 'long' | undefined {\n if (raw === undefined) return undefined;\n if (raw === 'wide' || raw === 'long') return raw;\n throw new OptionError(`--layout must be \"wide\" or \"long\", got \"${String(raw)}\".`);\n}\n\nfunction optionalDecimals(raw: unknown): number | undefined {\n if (raw === undefined) return undefined;\n const text = String(raw).trim();\n // Number('') is 0, which would quietly round every physical value to a whole number.\n if (text === '') {\n throw new OptionError('--decimals needs a number, for example --decimals 3.');\n }\n const value = Number(text);\n if (!Number.isInteger(value) || value < 0 || value > 20) {\n throw new OptionError(`--decimals must be a whole number between 0 and 20, got \"${String(raw)}\".`);\n }\n return value;\n}\n\n/*\n Error text is escaped for the same reason --info's table is: a fatal header error quotes\n the channel label that caused it, and that label is free text out of the file. A recording\n declaring a negative sample count under a label containing `\\x1b[2J` cleared the reader's\n screen on the way out.\n*/\nfunction message(error: unknown, indent = ''): string {\n // The indent lines continuation up under an \"error: \" prefix. Usage problems print without\n // one, alongside the usage text, so they pass nothing and stay flush left.\n return printableLines(error instanceof Error ? error.message : String(error), indent);\n}\n\nexport { defaultOutputDir };\n\n/**\n * Whether this file was executed rather than imported.\n *\n * npm installs a bin as a symlink (node_modules/.bin/edf2csv -> ../edf2csv/dist/cli.js),\n * and that is the path `npx` runs. In that case process.argv[1] is the symlink while\n * import.meta.url is already resolved to the real file, so comparing the two directly\n * reports \"imported\" and the command silently does nothing. Both sides are resolved\n * through realpath before comparing.\n */\nfunction isMainModule(): boolean {\n const entry = process.argv[1];\n if (entry === undefined) return false;\n try {\n return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));\n } catch {\n // An unreadable or deleted entry path is not a reason to refuse to run.\n return pathToFileURL(entry).href === import.meta.url;\n }\n}\n\nconst invokedDirectly = isMainModule();\n\nif (invokedDirectly) {\n main(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code;\n })\n .catch((error: unknown) => {\n process.stderr.write(`error: ${message(error)}\\n`);\n process.exitCode = EXIT_ERROR;\n });\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/convert/plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AASlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,SAAS,SAAS,EAAE,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,CAAC;CAChD;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACzC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,0EAA0E;IAC1E,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,+EAA+E;IAC/E,GAAG,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB;;;;;;;;;OASG;IACH,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IAExB;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,aAAa,CAAC;IACrB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,uBAAuB,EAAE,OAAO,CAAC;CAClC;AAID,oFAAoF;AACpF,eAAO,MAAM,qBAAqB,UAAY,CAAC;AAE/C,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,WAAgB,GAAG,cAAc,CA2KrF;AAiFD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,EAAE,CAEvF;AAED,gEAAgE;AAChE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C"}
1
+ {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/convert/plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AASlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,SAAS,SAAS,EAAE,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,CAAC;CAChD;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACzC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,0EAA0E;IAC1E,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B,+EAA+E;IAC/E,GAAG,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB;;;;;;;;;OASG;IACH,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IAExB;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,aAAa,CAAC;IACrB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,uBAAuB,EAAE,OAAO,CAAC;CAClC;AAID,oFAAoF;AACpF,eAAO,MAAM,qBAAqB,UAAY,CAAC;AAE/C,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,WAAgB,GAAG,cAAc,CAiLrF;AAiFD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,EAAE,CAEvF;AAED,gEAAgE;AAChE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C"}
@@ -134,22 +134,28 @@ export function buildPlan(input, options = {}) {
134
134
  MAX_DERIVED_DECIMALS. It is rare now, but "rare" is the reason to say so rather than the
135
135
  reason not to.
136
136
 
137
- Only when the precision was derived. `--decimals` exists to set a coarser one, so
138
- reporting the consequence of it is reporting the flag back at the caller who typed it:
139
- `--decimals 2` raised this on every channel of an ordinary EEG, and since --strict turns
140
- any diagnostic into exit 1, `--decimals 2 --strict` could not succeed on any recording.
141
- The warning is about a ceiling the caller cannot move, not about a floor they chose.
137
+ Asked of the ceiling, not of the precision in use, and so asked whatever `--decimals`
138
+ says. `--decimals 2` on a channel needing 3 is a trade the caller made knowingly, and
139
+ reporting it was reporting the flag back at the person who typed it every channel of an
140
+ ordinary EEG raised this, and since --strict turns any diagnostic into exit 1,
141
+ `--decimals 2 --strict` could not succeed on any recording at all.
142
+
143
+ 0.5.10 fixed that by skipping the check whenever `--decimals` was given, which suppressed
144
+ the real case along with the false one: at `--decimals 20` a channel stepping by 1e-106
145
+ printed every code it had as `0.00000000000000000000`, and said nothing. The question is
146
+ not who chose the precision. It is whether any precision the tool can print would
147
+ separate consecutive codes.
142
148
  */
143
- for (const group of options.decimals === undefined ? groups : []) {
144
- const short = group.channels.filter((c) => decimalsAreClamped(c.signal, c.decimals));
149
+ for (const group of groups) {
150
+ const short = group.channels.filter((c) => decimalsAreClamped(c.signal));
145
151
  if (short.length === 0)
146
152
  continue;
147
153
  diagnostics.push({
148
154
  code: 'VALUE_RESOLUTION',
149
155
  severity: 'warning',
150
156
  message: `${listed(short.map((c) => c.column))} ${short.length === 1 ? 'steps' : 'step'} by less ` +
151
- `than the ${short[0]?.decimals} decimals written can express, so some consecutive ` +
152
- `samples round to the same value in ${group.fileName}.`,
157
+ `than any number of decimals this can print, so some consecutive samples round to ` +
158
+ `the same value in ${group.fileName}.`,
153
159
  hint: 'Every sample is written, in order, and the physical values are computed at full ' +
154
160
  'precision either way. What is lost is only in the printed text.',
155
161
  });
@@ -1 +1 @@
1
- {"version":3,"file":"plan.js","sourceRoot":"","sources":["../../src/convert/plan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Turning a request into a concrete conversion plan.\n *\n * The plan is where the tool's central promise is enforced: channels recorded at\n * different sampling rates are never merged into one table. A single wide CSV can\n * only hold mixed rates by inventing samples for the slow channels — MNE, for\n * instance, expands three genuine 1 Hz temperature readings into 768 interpolated\n * values without warning. Instead each distinct rate gets its own file, so every\n * number in every output file is a number that was actually recorded.\n */\n\nimport type { Diagnostic } from '../edf/errors.js';\nimport type { EdfSignal } from '../edf/header.js';\nimport { formatRate, formatRates } from '../edf/header.js';\nimport { decimalsAreClamped, decimalsForSignal } from '../edf/scale.js';\nimport { UTF8_BOM, csvRow, escapeCsvField } from '../format/csv.js';\nimport { listed } from '../format/list.js';\nimport { timeDecimals } from '../format/number.js';\nimport { buildColumnNames, renamedByCollision, selectChannels } from './channels.js';\nimport { assertOptions } from './options.js';\nimport { countSamplesInRange, resolveRange } from './time-range.js';\nimport type { ResolvedRange } from './time-range.js';\n\nexport interface PlannedChannel {\n signal: EdfSignal;\n column: string;\n decimals: number;\n}\n\nexport interface RateGroup {\n /** Sampling rate in Hz shared by every channel in this group. */\n rate: number;\n samplesPerRecord: number;\n fileName: string;\n timeDecimals: number;\n channels: PlannedChannel[];\n}\n\nexport interface PlanInput {\n signals: readonly EdfSignal[];\n recordDuration: number;\n recordCount: number;\n hasAnnotationChannel: boolean;\n /**\n * True start time of each data record, supplied for discontinuous files. The\n * requested time window is resolved against these rather than against\n * `recordCount * recordDuration`, which for a file with gaps is the amount of\n * data rather than the span of time it covers.\n */\n recordStarts?: Float64Array | null | undefined;\n}\n\nexport interface PlanOptions {\n channels?: readonly string[] | undefined;\n start?: number | undefined;\n /** The `--start` value exactly as typed, for error messages. */\n startText?: string | undefined;\n duration?: number | undefined;\n end?: number | undefined;\n /** The `--end` value exactly as typed, for error messages. */\n endText?: string | undefined;\n annotationsOnly?: boolean | undefined;\n /** Force a fixed number of decimals instead of deriving it per channel. */\n decimals?: number | undefined;\n /** Compress each CSV with gzip, giving every one of them a `.gz` name. */\n gzip?: boolean | undefined;\n /** Start each CSV with a UTF-8 byte order mark, so Excel reads it as UTF-8. */\n bom?: boolean | undefined;\n /**\n * How the samples are arranged in the CSV.\n *\n * `'wide'`, the default, gives one column per channel and one file per sampling rate.\n * `'long'` gives one file, three columns — `time_s`, `channel`, `value` — and one row per\n * sample. See ConversionPlan.layout for why that is the only way to put channels recorded\n * at different rates in one table without inventing samples.\n */\n layout?: 'wide' | 'long' | undefined;\n}\n\nexport interface ConversionPlan {\n groups: RateGroup[];\n /**\n * How the samples are arranged. `'wide'` is a column per channel and a file per rate;\n * `'long'` is `time_s,channel,value`, one row per sample, all rates in one file.\n *\n * The wide layout has to split a mixed-rate recording across files: a 100 Hz channel and\n * a 1 Hz channel share no rows, and putting them in one wide table means either 99 empty\n * cells out of every hundred or inventing the samples that would fill them. In the long\n * layout each sample carries its own time, so nothing has to line up and nothing is\n * invented — which also makes it the one layout `--stdout` can stream for such a file.\n */\n layout: 'wide' | 'long';\n\n /**\n * Whether the CSVs will be compressed.\n *\n * Recorded rather than inferred from the group file names. Under `--annotations-only`\n * there are no groups to read it off, and `--info` named `annotations.csv` for a run that\n * wrote `annotations.csv.gz`.\n */\n gzip: boolean;\n range: ResolvedRange;\n columnNames: Map<number, string>;\n writeSignals: boolean;\n diagnostics: Diagnostic[];\n estimate: OutputEstimate;\n}\n\nexport interface OutputEstimate {\n /** Total data rows across every signal file. */\n rows: number;\n /** Approximate size of the signal CSVs on disk. */\n bytes: number;\n /** True when any single file would exceed Excel's row limit. */\n exceedsSpreadsheetLimit: boolean;\n}\n\nconst BOM_BYTES = Buffer.byteLength(UTF8_BOM);\n\n/** Excel and most spreadsheet tools stop at 1,048,576 rows including the header. */\nexport const SPREADSHEET_ROW_LIMIT = 1_048_576;\n\nexport function buildPlan(input: PlanInput, options: PlanOptions = {}): ConversionPlan {\n // First, and before a directory is created or a stream opened, so a rejected option\n // leaves nothing behind. See assertOptions for what used to get through.\n assertOptions(options);\n\n const diagnostics: Diagnostic[] = [];\n const columnNames = buildColumnNames(input.signals);\n\n // A channel whose own label was taken by another channel's disambiguating suffix. The\n // duplicate-label warning is about the labels that collided; this is about the channel\n // that lost its name to them, which is the one whose column no longer matches the file.\n for (const signal of renamedByCollision(input.signals, columnNames)) {\n diagnostics.push({\n code: 'DUPLICATE_LABEL',\n severity: 'warning',\n message:\n `Signal ${signal.index} is labelled \"${signal.label}\", which is also the column name ` +\n `another channel's \"_ch\" suffix produces, so its column is \"${columnNames.get(signal.index)}\".`,\n hint: 'Column names are unique; look this channel up in channels.csv by its signal_index.',\n });\n }\n\n const range = resolveRange({\n start: options.start,\n startText: options.startText,\n duration: options.duration,\n end: options.end,\n endText: options.endText,\n recordDuration: input.recordDuration,\n recordCount: input.recordCount,\n recordStarts: input.recordStarts,\n });\n\n const writeSignals = options.annotationsOnly !== true;\n\n let chosen: EdfSignal[] = input.signals.filter((s) => !s.isAnnotations);\n\n /*\n Channel names are checked even under --annotations-only, where the selection is not\n otherwise used.\n\n Skipping the check meant `--channels TYPO --annotations-only` exited 0 in silence while\n the same typo without the flag was a usage error, and `--channels \"\"` stayed an error\n in both — so a mistyped name was the one form of bad input the tool accepted quietly.\n Everywhere else a term matching nothing is reported rather than ignored; a flag that\n happens not to apply is a poor reason to make an exception.\n */\n if (options.channels && options.channels.length > 0) {\n const selection = selectChannels(input.signals, options.channels);\n if (writeSignals) chosen = selection.signals;\n for (const { term, matched } of selection.ambiguous) {\n diagnostics.push({\n code: 'DUPLICATE_LABEL',\n severity: 'warning',\n message:\n `\"${term}\" matches ${matched.length} channels (positions ` +\n `${listed(matched.map((s) => `#${s.index}`))}); all of them were selected.`,\n hint: `Use --channels \"#${matched[0]?.index ?? 0}\" to pick just one.`,\n });\n }\n }\n\n const layout = options.layout ?? 'wide';\n const groups = writeSignals\n ? groupByRate(chosen, columnNames, options.decimals, options.gzip === true, layout)\n : [];\n const estimate = estimateOutput(\n groups,\n range,\n input.recordDuration,\n input.recordStarts,\n options.bom === true,\n layout,\n );\n\n /*\n The mixed-rate warning describes what this conversion does, not what the file holds.\n\n The header parser raises its own, which is right for `parseHeader` — but it sees every\n channel and knows nothing about `--channels`. Converting one channel out of a three-rate\n recording therefore announced \"3 different sampling rates ... written to one file per\n rate\" over a run that wrote one file, in the same output where `--info` had already\n marked the other two \"(not selected)\". Selecting two of the three was wrong the other\n way: still \"3\".\n\n Callers combining these with a file's own diagnostics drop that copy in favour of this\n one; see `withoutFileRateWarning`.\n */\n if (groups.length > 1) {\n diagnostics.push({\n code: 'MIXED_SAMPLING_RATES',\n severity: 'warning',\n message:\n `Channels use ${groups.length} different sampling rates ` +\n `(${listed(formatRates(groups.map((g) => g.rate)).map((r) => `${r} Hz`))}).`,\n hint:\n layout === 'long'\n ? 'They share one table, each row carrying its own time, so no channel is resampled.'\n : 'They are written to one file per rate so no channel is resampled.',\n });\n }\n\n /*\n A time column that cannot tell two samples apart.\n\n Sample times are written to at most nine decimal places, which separates everything up to\n a gigahertz. Below that the column repeats: a recording of 1 ns records holding ten\n samples each writes twenty rows carrying three distinct times, so joining or plotting on\n `time_s` silently collapses them. Nothing is lost from the file — every sample is there,\n in order — but the column stops being an identifier, and that is worth saying rather than\n leaving to be discovered.\n */\n for (const group of groups) {\n const step = group.rate > 0 ? 1 / group.rate : 0;\n if (step > 0 && step < 10 ** -group.timeDecimals) {\n diagnostics.push({\n code: 'TIME_RESOLUTION',\n severity: 'warning',\n message:\n `Channels at ${formatRate(group.rate)} Hz sample faster than the time column can ` +\n `distinguish, so consecutive rows in ${group.fileName} carry the same time_s value.`,\n hint:\n 'Every sample is written, in order. Use the row number rather than time_s to tell ' +\n 'them apart, or convert one rate at a time with --channels.',\n });\n }\n }\n\n /*\n The same failure as TIME_RESOLUTION, one column over.\n\n A channel whose quantization step is below 1e-98 needs more decimals than `toFixed` can\n print, so consecutive digital codes round to the same text and the arithmetic the FAQ\n gives for recovering them stops working. That used to happen at 1e-20 and silently — see\n MAX_DERIVED_DECIMALS. It is rare now, but \"rare\" is the reason to say so rather than the\n reason not to.\n\n Only when the precision was derived. `--decimals` exists to set a coarser one, so\n reporting the consequence of it is reporting the flag back at the caller who typed it:\n `--decimals 2` raised this on every channel of an ordinary EEG, and since --strict turns\n any diagnostic into exit 1, `--decimals 2 --strict` could not succeed on any recording.\n The warning is about a ceiling the caller cannot move, not about a floor they chose.\n */\n for (const group of options.decimals === undefined ? groups : []) {\n const short = group.channels.filter((c) => decimalsAreClamped(c.signal, c.decimals));\n if (short.length === 0) continue;\n diagnostics.push({\n code: 'VALUE_RESOLUTION',\n severity: 'warning',\n message:\n `${listed(short.map((c) => c.column))} ${short.length === 1 ? 'steps' : 'step'} by less ` +\n `than the ${short[0]?.decimals} decimals written can express, so some consecutive ` +\n `samples round to the same value in ${group.fileName}.`,\n hint:\n 'Every sample is written, in order, and the physical values are computed at full ' +\n 'precision either way. What is lost is only in the printed text.',\n });\n }\n\n if (estimate.exceedsSpreadsheetLimit) {\n diagnostics.push({\n code: 'LARGE_OUTPUT',\n severity: 'warning',\n message:\n `At least one output file will have more than ${SPREADSHEET_ROW_LIMIT.toLocaleString('en-US')} ` +\n `rows, which is more than Excel or Numbers can open.`,\n hint: 'Use --start and --duration to convert a section, or read the file with pandas or R.',\n });\n }\n\n return { groups, layout, gzip: options.gzip === true, range, columnNames, writeSignals, diagnostics, estimate };\n}\n\n/**\n * Partition channels by sampling rate, largest first.\n *\n * The common case — every channel at one rate — collapses to a single group and a\n * single `signals.csv`, so the honest behaviour costs nothing when there is nothing\n * to be honest about.\n */\nfunction groupByRate(\n signals: readonly EdfSignal[],\n columnNames: Map<number, string>,\n forcedDecimals: number | undefined,\n gzip: boolean,\n layout: 'wide' | 'long',\n): RateGroup[] {\n const byRate = new Map<number, EdfSignal[]>();\n for (const signal of signals) {\n // A channel with no samples has no sampling rate to group by, and would\n // otherwise produce an empty \"0hz\" file. The header parser already warned.\n if (signal.samplesPerRecord === 0) continue;\n const bucket = byRate.get(signal.samplingRate);\n if (bucket) bucket.push(signal);\n else byRate.set(signal.samplingRate, [signal]);\n }\n\n const rates = [...byRate.keys()].sort((a, b) => b - a);\n // The long layout writes one table whatever the rates are, so every group names it.\n const single = rates.length === 1 || layout === 'long';\n\n /*\n Two distinct rates can produce the same slug, because the slug rounds to six decimal\n places. Rates come from samplesPerRecord / recordDuration and every channel shares the\n record duration, so the closest two rates can be is 1 / recordDuration — which drops\n below 1e-6 once a record is longer than about eleven days. Absurd, but the header\n permits it, and the failure was silent and destructive: both groups opened a write\n stream on the same path, so the file ended up holding interleaved rows from both\n channels under a header naming only one of them.\n\n Distinct rates therefore get distinct files, always. The suffix is only ever reached by\n a collision, so ordinary recordings keep the names they have always had.\n\n Naming from the whole set of rates at once removes most of those collisions before the\n suffix has to. Rounding each rate on its own gave 1e-6 Hz and 1.25e-6 Hz the same slug,\n and the numbering below then produced signals_0_000001hz.csv and signals_0_000001hz_2.csv\n — two files that no longer overwrite each other, but of which only one is named for the\n rate it holds. The suffix stays as the backstop for anything this still cannot separate.\n */\n const suffix = gzip ? '.csv.gz' : '.csv';\n const slugs = formatRates(rates).map((text) => `${text.replace('.', '_')}hz`);\n const used = new Set<string>();\n const uniqueName = (index: number): string => {\n const base = `signals_${slugs[index]}`;\n let name = `${base}${suffix}`;\n for (let n = 2; used.has(name); n++) name = `${base}_${n}${suffix}`;\n used.add(name);\n return name;\n };\n\n return rates.map((rate, index) => {\n const members = byRate.get(rate) ?? [];\n const first = members[0];\n return {\n rate,\n samplesPerRecord: first ? first.samplesPerRecord : 0,\n fileName: single ? `signals${suffix}` : uniqueName(index),\n /*\n In the long layout every rate shares a `time_s` column, so they share its precision:\n the finest any of them needs. Writing 100 Hz at three places and 256 Hz at eight in\n the same column would make the column's meaning depend on the row.\n */\n timeDecimals: layout === 'long' ? Math.max(...rates.map(timeDecimals)) : timeDecimals(rate),\n channels: members.map((signal) => ({\n signal,\n column: columnNames.get(signal.index) ?? `signal_${signal.index}`,\n decimals: forcedDecimals ?? decimalsForSignal(signal),\n })),\n };\n });\n}\n\n/**\n * A file's diagnostics with the header's mixed-rate warning removed.\n *\n * `buildPlan` raises that warning for the channels actually being converted, so keeping both\n * would either duplicate it or contradict it. The header parser's copy stays where it is, for\n * callers reading a header without planning a conversion.\n */\nexport function withoutFileRateWarning(diagnostics: readonly Diagnostic[]): Diagnostic[] {\n return diagnostics.filter((d) => d.code !== 'MIXED_SAMPLING_RATES');\n}\n\n/** `256hz`, `12_5hz` — safe in a filename on every platform. */\nexport function rateSlug(rate: number): string {\n return `${formatRate(rate).replace('.', '_')}hz`;\n}\n\n/** Characters a fixed-decimal number of this magnitude occupies, sign included. */\nfunction widthOf(magnitude: number, decimals: number, signed = false): number {\n const size = Math.abs(magnitude);\n const sign = signed ? 1 : 0;\n const fraction = decimals > 0 ? 1 + decimals : 0;\n\n /*\n Cells are written with toFixed, which rounds. Taking the integer digits from the floor of\n the bound therefore under-counted whenever rounding carried into a new digit: a channel\n bounded at 9999.999 and written to zero decimals produces \"10000\", five characters where\n the floor of 9999.999 suggests four. Every cell on such a channel was a byte short, and\n `--info` reported 127 KB for a file that came out 131 KB.\n\n Measuring the bound as rendered removes that. toFixed switches to exponential notation\n past 1e21, so the arithmetic form still covers magnitudes beyond it.\n */\n if (!Number.isFinite(size)) return sign + 1 + fraction;\n if (size < 1e21) return sign + size.toFixed(Math.min(decimals, 100)).length;\n return sign + (Math.floor(Math.log10(size)) + 1) + fraction;\n}\n\nfunction estimateOutput(\n groups: readonly RateGroup[],\n range: ResolvedRange,\n recordDuration: number,\n recordStarts: Float64Array | null | undefined,\n bom: boolean,\n layout: 'wide' | 'long',\n): OutputEstimate {\n let rows = 0;\n let bytes = 0;\n let exceeds = false;\n // One table in the long layout, so the row limit applies to the sum rather than the\n // largest group, and the header and mark are counted once rather than once per group.\n let longRows = 0;\n\n for (const group of groups) {\n let groupRows = 0;\n for (let record = range.startRecord; record < range.endRecord; record++) {\n const recordStart = recordStarts\n ? (recordStarts[record] ?? record * recordDuration)\n : record * recordDuration;\n groupRows += countSamplesInRange({\n recordStart,\n rate: group.rate,\n samplesPerRecord: group.samplesPerRecord,\n startSeconds: range.startSeconds,\n endSeconds: range.endSeconds,\n });\n }\n if (layout === 'long') {\n // A row per sample per channel rather than a row per sample time.\n const groupCells = groupRows * group.channels.length;\n rows += groupCells;\n longRows += groupCells;\n /*\n `time_s,channel,value`: the time, the channel name as it will be escaped into the\n cell, and the widest the value can print. Same over-counting rule as the wide\n layout — the declared physical range bounds a cell, and most samples sit under it.\n */\n const timeWidth = widthOf(range.endSeconds, group.timeDecimals);\n for (const channel of group.channels) {\n const valueWidth = widthOf(\n Math.max(Math.abs(channel.signal.physicalMin), Math.abs(channel.signal.physicalMax)),\n channel.decimals,\n channel.signal.physicalMin < 0 || channel.signal.physicalMax < 0,\n );\n const nameWidth = Buffer.byteLength(escapeCsvField(channel.column));\n // Two commas and the newline.\n bytes += groupRows * (timeWidth + nameWidth + valueWidth + 3);\n }\n continue;\n }\n\n rows += groupRows;\n if (groupRows + 1 > SPREADSHEET_ROW_LIMIT) exceeds = true;\n\n /*\n Width per cell, from the channel's own calibration rather than a flat allowance.\n\n The old `decimals + 6` budgeted six characters for the sign, integer part and decimal\n point on every channel, whatever it actually held. That over-counted a millivolt\n channel spanning ±5 by four characters a cell and ran 30-55% high across the fixture\n set — on a number people use to decide whether a conversion is worth starting.\n\n The channel's declared physical range is what bounds a cell, so that bound is what is\n used. Most samples sit below it, so this still reads high, which is the direction a\n size estimate should err in.\n\n One case is outside the bound rather than under it: nothing obliges a recording to keep\n its samples inside the digital range it declares, and one that does not maps outside the\n physical range too. Such a file can convert larger than the estimate. Clamping the data\n to make the estimate true is not a trade worth making — the samples are what they are.\n */\n const timeWidth = widthOf(range.endSeconds, group.timeDecimals);\n const cellWidth = group.channels.reduce(\n (sum, c) =>\n sum +\n widthOf(\n Math.max(Math.abs(c.signal.physicalMin), Math.abs(c.signal.physicalMax)),\n c.decimals,\n c.signal.physicalMin < 0 || c.signal.physicalMax < 0,\n ),\n 0,\n );\n // One comma per channel, plus the newline.\n bytes += groupRows * (timeWidth + cellWidth + group.channels.length + 1);\n /*\n The header row, measured as it will be written rather than as the labels are stored.\n\n A column name is quoted when it contains a comma, a quote, a newline or a leading or\n trailing space, and every quote inside it is doubled. Counting the raw label under-counted\n that row: three channels labelled `a,b,c,d,e`, `x\"y` and `plain` write a 32-byte header\n and were budgeted 27. EDF labels are free text, so commas in them are ordinary — a montage\n written as `EEG Fpz-Cz, ref` is exactly the kind of thing this is for.\n\n csvRow is the function that writes it, so it is the function that measures it. Nothing\n else is in a position to stay correct when the quoting rules change.\n */\n bytes += Buffer.byteLength(csvRow(['time_s', ...group.channels.map((c) => c.column)])) + 1;\n // Three bytes per file under --bom. Small, but the estimate promises never to read\n // under what gets written, and a one-row conversion is small enough for it to matter.\n if (bom) bytes += BOM_BYTES;\n }\n\n if (layout === 'long' && groups.length > 0) {\n if (longRows + 1 > SPREADSHEET_ROW_LIMIT) exceeds = true;\n bytes += Buffer.byteLength(csvRow(['time_s', 'channel', 'value'])) + 1;\n if (bom) bytes += BOM_BYTES;\n }\n\n return { rows, bytes, exceedsSpreadsheetLimit: exceeds };\n}\n"]}
1
+ {"version":3,"file":"plan.js","sourceRoot":"","sources":["../../src/convert/plan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Turning a request into a concrete conversion plan.\n *\n * The plan is where the tool's central promise is enforced: channels recorded at\n * different sampling rates are never merged into one table. A single wide CSV can\n * only hold mixed rates by inventing samples for the slow channels — MNE, for\n * instance, expands three genuine 1 Hz temperature readings into 768 interpolated\n * values without warning. Instead each distinct rate gets its own file, so every\n * number in every output file is a number that was actually recorded.\n */\n\nimport type { Diagnostic } from '../edf/errors.js';\nimport type { EdfSignal } from '../edf/header.js';\nimport { formatRate, formatRates } from '../edf/header.js';\nimport { decimalsAreClamped, decimalsForSignal } from '../edf/scale.js';\nimport { UTF8_BOM, csvRow, escapeCsvField } from '../format/csv.js';\nimport { listed } from '../format/list.js';\nimport { timeDecimals } from '../format/number.js';\nimport { buildColumnNames, renamedByCollision, selectChannels } from './channels.js';\nimport { assertOptions } from './options.js';\nimport { countSamplesInRange, resolveRange } from './time-range.js';\nimport type { ResolvedRange } from './time-range.js';\n\nexport interface PlannedChannel {\n signal: EdfSignal;\n column: string;\n decimals: number;\n}\n\nexport interface RateGroup {\n /** Sampling rate in Hz shared by every channel in this group. */\n rate: number;\n samplesPerRecord: number;\n fileName: string;\n timeDecimals: number;\n channels: PlannedChannel[];\n}\n\nexport interface PlanInput {\n signals: readonly EdfSignal[];\n recordDuration: number;\n recordCount: number;\n hasAnnotationChannel: boolean;\n /**\n * True start time of each data record, supplied for discontinuous files. The\n * requested time window is resolved against these rather than against\n * `recordCount * recordDuration`, which for a file with gaps is the amount of\n * data rather than the span of time it covers.\n */\n recordStarts?: Float64Array | null | undefined;\n}\n\nexport interface PlanOptions {\n channels?: readonly string[] | undefined;\n start?: number | undefined;\n /** The `--start` value exactly as typed, for error messages. */\n startText?: string | undefined;\n duration?: number | undefined;\n end?: number | undefined;\n /** The `--end` value exactly as typed, for error messages. */\n endText?: string | undefined;\n annotationsOnly?: boolean | undefined;\n /** Force a fixed number of decimals instead of deriving it per channel. */\n decimals?: number | undefined;\n /** Compress each CSV with gzip, giving every one of them a `.gz` name. */\n gzip?: boolean | undefined;\n /** Start each CSV with a UTF-8 byte order mark, so Excel reads it as UTF-8. */\n bom?: boolean | undefined;\n /**\n * How the samples are arranged in the CSV.\n *\n * `'wide'`, the default, gives one column per channel and one file per sampling rate.\n * `'long'` gives one file, three columns — `time_s`, `channel`, `value` — and one row per\n * sample. See ConversionPlan.layout for why that is the only way to put channels recorded\n * at different rates in one table without inventing samples.\n */\n layout?: 'wide' | 'long' | undefined;\n}\n\nexport interface ConversionPlan {\n groups: RateGroup[];\n /**\n * How the samples are arranged. `'wide'` is a column per channel and a file per rate;\n * `'long'` is `time_s,channel,value`, one row per sample, all rates in one file.\n *\n * The wide layout has to split a mixed-rate recording across files: a 100 Hz channel and\n * a 1 Hz channel share no rows, and putting them in one wide table means either 99 empty\n * cells out of every hundred or inventing the samples that would fill them. In the long\n * layout each sample carries its own time, so nothing has to line up and nothing is\n * invented — which also makes it the one layout `--stdout` can stream for such a file.\n */\n layout: 'wide' | 'long';\n\n /**\n * Whether the CSVs will be compressed.\n *\n * Recorded rather than inferred from the group file names. Under `--annotations-only`\n * there are no groups to read it off, and `--info` named `annotations.csv` for a run that\n * wrote `annotations.csv.gz`.\n */\n gzip: boolean;\n range: ResolvedRange;\n columnNames: Map<number, string>;\n writeSignals: boolean;\n diagnostics: Diagnostic[];\n estimate: OutputEstimate;\n}\n\nexport interface OutputEstimate {\n /** Total data rows across every signal file. */\n rows: number;\n /** Approximate size of the signal CSVs on disk. */\n bytes: number;\n /** True when any single file would exceed Excel's row limit. */\n exceedsSpreadsheetLimit: boolean;\n}\n\nconst BOM_BYTES = Buffer.byteLength(UTF8_BOM);\n\n/** Excel and most spreadsheet tools stop at 1,048,576 rows including the header. */\nexport const SPREADSHEET_ROW_LIMIT = 1_048_576;\n\nexport function buildPlan(input: PlanInput, options: PlanOptions = {}): ConversionPlan {\n // First, and before a directory is created or a stream opened, so a rejected option\n // leaves nothing behind. See assertOptions for what used to get through.\n assertOptions(options);\n\n const diagnostics: Diagnostic[] = [];\n const columnNames = buildColumnNames(input.signals);\n\n // A channel whose own label was taken by another channel's disambiguating suffix. The\n // duplicate-label warning is about the labels that collided; this is about the channel\n // that lost its name to them, which is the one whose column no longer matches the file.\n for (const signal of renamedByCollision(input.signals, columnNames)) {\n diagnostics.push({\n code: 'DUPLICATE_LABEL',\n severity: 'warning',\n message:\n `Signal ${signal.index} is labelled \"${signal.label}\", which is also the column name ` +\n `another channel's \"_ch\" suffix produces, so its column is \"${columnNames.get(signal.index)}\".`,\n hint: 'Column names are unique; look this channel up in channels.csv by its signal_index.',\n });\n }\n\n const range = resolveRange({\n start: options.start,\n startText: options.startText,\n duration: options.duration,\n end: options.end,\n endText: options.endText,\n recordDuration: input.recordDuration,\n recordCount: input.recordCount,\n recordStarts: input.recordStarts,\n });\n\n const writeSignals = options.annotationsOnly !== true;\n\n let chosen: EdfSignal[] = input.signals.filter((s) => !s.isAnnotations);\n\n /*\n Channel names are checked even under --annotations-only, where the selection is not\n otherwise used.\n\n Skipping the check meant `--channels TYPO --annotations-only` exited 0 in silence while\n the same typo without the flag was a usage error, and `--channels \"\"` stayed an error\n in both — so a mistyped name was the one form of bad input the tool accepted quietly.\n Everywhere else a term matching nothing is reported rather than ignored; a flag that\n happens not to apply is a poor reason to make an exception.\n */\n if (options.channels && options.channels.length > 0) {\n const selection = selectChannels(input.signals, options.channels);\n if (writeSignals) chosen = selection.signals;\n for (const { term, matched } of selection.ambiguous) {\n diagnostics.push({\n code: 'DUPLICATE_LABEL',\n severity: 'warning',\n message:\n `\"${term}\" matches ${matched.length} channels (positions ` +\n `${listed(matched.map((s) => `#${s.index}`))}); all of them were selected.`,\n hint: `Use --channels \"#${matched[0]?.index ?? 0}\" to pick just one.`,\n });\n }\n }\n\n const layout = options.layout ?? 'wide';\n const groups = writeSignals\n ? groupByRate(chosen, columnNames, options.decimals, options.gzip === true, layout)\n : [];\n const estimate = estimateOutput(\n groups,\n range,\n input.recordDuration,\n input.recordStarts,\n options.bom === true,\n layout,\n );\n\n /*\n The mixed-rate warning describes what this conversion does, not what the file holds.\n\n The header parser raises its own, which is right for `parseHeader` — but it sees every\n channel and knows nothing about `--channels`. Converting one channel out of a three-rate\n recording therefore announced \"3 different sampling rates ... written to one file per\n rate\" over a run that wrote one file, in the same output where `--info` had already\n marked the other two \"(not selected)\". Selecting two of the three was wrong the other\n way: still \"3\".\n\n Callers combining these with a file's own diagnostics drop that copy in favour of this\n one; see `withoutFileRateWarning`.\n */\n if (groups.length > 1) {\n diagnostics.push({\n code: 'MIXED_SAMPLING_RATES',\n severity: 'warning',\n message:\n `Channels use ${groups.length} different sampling rates ` +\n `(${listed(formatRates(groups.map((g) => g.rate)).map((r) => `${r} Hz`))}).`,\n hint:\n layout === 'long'\n ? 'They share one table, each row carrying its own time, so no channel is resampled.'\n : 'They are written to one file per rate so no channel is resampled.',\n });\n }\n\n /*\n A time column that cannot tell two samples apart.\n\n Sample times are written to at most nine decimal places, which separates everything up to\n a gigahertz. Below that the column repeats: a recording of 1 ns records holding ten\n samples each writes twenty rows carrying three distinct times, so joining or plotting on\n `time_s` silently collapses them. Nothing is lost from the file — every sample is there,\n in order — but the column stops being an identifier, and that is worth saying rather than\n leaving to be discovered.\n */\n for (const group of groups) {\n const step = group.rate > 0 ? 1 / group.rate : 0;\n if (step > 0 && step < 10 ** -group.timeDecimals) {\n diagnostics.push({\n code: 'TIME_RESOLUTION',\n severity: 'warning',\n message:\n `Channels at ${formatRate(group.rate)} Hz sample faster than the time column can ` +\n `distinguish, so consecutive rows in ${group.fileName} carry the same time_s value.`,\n hint:\n 'Every sample is written, in order. Use the row number rather than time_s to tell ' +\n 'them apart, or convert one rate at a time with --channels.',\n });\n }\n }\n\n /*\n The same failure as TIME_RESOLUTION, one column over.\n\n A channel whose quantization step is below 1e-98 needs more decimals than `toFixed` can\n print, so consecutive digital codes round to the same text and the arithmetic the FAQ\n gives for recovering them stops working. That used to happen at 1e-20 and silently — see\n MAX_DERIVED_DECIMALS. It is rare now, but \"rare\" is the reason to say so rather than the\n reason not to.\n\n Asked of the ceiling, not of the precision in use, and so asked whatever `--decimals`\n says. `--decimals 2` on a channel needing 3 is a trade the caller made knowingly, and\n reporting it was reporting the flag back at the person who typed it — every channel of an\n ordinary EEG raised this, and since --strict turns any diagnostic into exit 1,\n `--decimals 2 --strict` could not succeed on any recording at all.\n\n 0.5.10 fixed that by skipping the check whenever `--decimals` was given, which suppressed\n the real case along with the false one: at `--decimals 20` a channel stepping by 1e-106\n printed every code it had as `0.00000000000000000000`, and said nothing. The question is\n not who chose the precision. It is whether any precision the tool can print would\n separate consecutive codes.\n */\n for (const group of groups) {\n const short = group.channels.filter((c) => decimalsAreClamped(c.signal));\n if (short.length === 0) continue;\n diagnostics.push({\n code: 'VALUE_RESOLUTION',\n severity: 'warning',\n message:\n `${listed(short.map((c) => c.column))} ${short.length === 1 ? 'steps' : 'step'} by less ` +\n `than any number of decimals this can print, so some consecutive samples round to ` +\n `the same value in ${group.fileName}.`,\n hint:\n 'Every sample is written, in order, and the physical values are computed at full ' +\n 'precision either way. What is lost is only in the printed text.',\n });\n }\n\n if (estimate.exceedsSpreadsheetLimit) {\n diagnostics.push({\n code: 'LARGE_OUTPUT',\n severity: 'warning',\n message:\n `At least one output file will have more than ${SPREADSHEET_ROW_LIMIT.toLocaleString('en-US')} ` +\n `rows, which is more than Excel or Numbers can open.`,\n hint: 'Use --start and --duration to convert a section, or read the file with pandas or R.',\n });\n }\n\n return { groups, layout, gzip: options.gzip === true, range, columnNames, writeSignals, diagnostics, estimate };\n}\n\n/**\n * Partition channels by sampling rate, largest first.\n *\n * The common case — every channel at one rate — collapses to a single group and a\n * single `signals.csv`, so the honest behaviour costs nothing when there is nothing\n * to be honest about.\n */\nfunction groupByRate(\n signals: readonly EdfSignal[],\n columnNames: Map<number, string>,\n forcedDecimals: number | undefined,\n gzip: boolean,\n layout: 'wide' | 'long',\n): RateGroup[] {\n const byRate = new Map<number, EdfSignal[]>();\n for (const signal of signals) {\n // A channel with no samples has no sampling rate to group by, and would\n // otherwise produce an empty \"0hz\" file. The header parser already warned.\n if (signal.samplesPerRecord === 0) continue;\n const bucket = byRate.get(signal.samplingRate);\n if (bucket) bucket.push(signal);\n else byRate.set(signal.samplingRate, [signal]);\n }\n\n const rates = [...byRate.keys()].sort((a, b) => b - a);\n // The long layout writes one table whatever the rates are, so every group names it.\n const single = rates.length === 1 || layout === 'long';\n\n /*\n Two distinct rates can produce the same slug, because the slug rounds to six decimal\n places. Rates come from samplesPerRecord / recordDuration and every channel shares the\n record duration, so the closest two rates can be is 1 / recordDuration — which drops\n below 1e-6 once a record is longer than about eleven days. Absurd, but the header\n permits it, and the failure was silent and destructive: both groups opened a write\n stream on the same path, so the file ended up holding interleaved rows from both\n channels under a header naming only one of them.\n\n Distinct rates therefore get distinct files, always. The suffix is only ever reached by\n a collision, so ordinary recordings keep the names they have always had.\n\n Naming from the whole set of rates at once removes most of those collisions before the\n suffix has to. Rounding each rate on its own gave 1e-6 Hz and 1.25e-6 Hz the same slug,\n and the numbering below then produced signals_0_000001hz.csv and signals_0_000001hz_2.csv\n — two files that no longer overwrite each other, but of which only one is named for the\n rate it holds. The suffix stays as the backstop for anything this still cannot separate.\n */\n const suffix = gzip ? '.csv.gz' : '.csv';\n const slugs = formatRates(rates).map((text) => `${text.replace('.', '_')}hz`);\n const used = new Set<string>();\n const uniqueName = (index: number): string => {\n const base = `signals_${slugs[index]}`;\n let name = `${base}${suffix}`;\n for (let n = 2; used.has(name); n++) name = `${base}_${n}${suffix}`;\n used.add(name);\n return name;\n };\n\n return rates.map((rate, index) => {\n const members = byRate.get(rate) ?? [];\n const first = members[0];\n return {\n rate,\n samplesPerRecord: first ? first.samplesPerRecord : 0,\n fileName: single ? `signals${suffix}` : uniqueName(index),\n /*\n In the long layout every rate shares a `time_s` column, so they share its precision:\n the finest any of them needs. Writing 100 Hz at three places and 256 Hz at eight in\n the same column would make the column's meaning depend on the row.\n */\n timeDecimals: layout === 'long' ? Math.max(...rates.map(timeDecimals)) : timeDecimals(rate),\n channels: members.map((signal) => ({\n signal,\n column: columnNames.get(signal.index) ?? `signal_${signal.index}`,\n decimals: forcedDecimals ?? decimalsForSignal(signal),\n })),\n };\n });\n}\n\n/**\n * A file's diagnostics with the header's mixed-rate warning removed.\n *\n * `buildPlan` raises that warning for the channels actually being converted, so keeping both\n * would either duplicate it or contradict it. The header parser's copy stays where it is, for\n * callers reading a header without planning a conversion.\n */\nexport function withoutFileRateWarning(diagnostics: readonly Diagnostic[]): Diagnostic[] {\n return diagnostics.filter((d) => d.code !== 'MIXED_SAMPLING_RATES');\n}\n\n/** `256hz`, `12_5hz` — safe in a filename on every platform. */\nexport function rateSlug(rate: number): string {\n return `${formatRate(rate).replace('.', '_')}hz`;\n}\n\n/** Characters a fixed-decimal number of this magnitude occupies, sign included. */\nfunction widthOf(magnitude: number, decimals: number, signed = false): number {\n const size = Math.abs(magnitude);\n const sign = signed ? 1 : 0;\n const fraction = decimals > 0 ? 1 + decimals : 0;\n\n /*\n Cells are written with toFixed, which rounds. Taking the integer digits from the floor of\n the bound therefore under-counted whenever rounding carried into a new digit: a channel\n bounded at 9999.999 and written to zero decimals produces \"10000\", five characters where\n the floor of 9999.999 suggests four. Every cell on such a channel was a byte short, and\n `--info` reported 127 KB for a file that came out 131 KB.\n\n Measuring the bound as rendered removes that. toFixed switches to exponential notation\n past 1e21, so the arithmetic form still covers magnitudes beyond it.\n */\n if (!Number.isFinite(size)) return sign + 1 + fraction;\n if (size < 1e21) return sign + size.toFixed(Math.min(decimals, 100)).length;\n return sign + (Math.floor(Math.log10(size)) + 1) + fraction;\n}\n\nfunction estimateOutput(\n groups: readonly RateGroup[],\n range: ResolvedRange,\n recordDuration: number,\n recordStarts: Float64Array | null | undefined,\n bom: boolean,\n layout: 'wide' | 'long',\n): OutputEstimate {\n let rows = 0;\n let bytes = 0;\n let exceeds = false;\n // One table in the long layout, so the row limit applies to the sum rather than the\n // largest group, and the header and mark are counted once rather than once per group.\n let longRows = 0;\n\n for (const group of groups) {\n let groupRows = 0;\n for (let record = range.startRecord; record < range.endRecord; record++) {\n const recordStart = recordStarts\n ? (recordStarts[record] ?? record * recordDuration)\n : record * recordDuration;\n groupRows += countSamplesInRange({\n recordStart,\n rate: group.rate,\n samplesPerRecord: group.samplesPerRecord,\n startSeconds: range.startSeconds,\n endSeconds: range.endSeconds,\n });\n }\n if (layout === 'long') {\n // A row per sample per channel rather than a row per sample time.\n const groupCells = groupRows * group.channels.length;\n rows += groupCells;\n longRows += groupCells;\n /*\n `time_s,channel,value`: the time, the channel name as it will be escaped into the\n cell, and the widest the value can print. Same over-counting rule as the wide\n layout — the declared physical range bounds a cell, and most samples sit under it.\n */\n const timeWidth = widthOf(range.endSeconds, group.timeDecimals);\n for (const channel of group.channels) {\n const valueWidth = widthOf(\n Math.max(Math.abs(channel.signal.physicalMin), Math.abs(channel.signal.physicalMax)),\n channel.decimals,\n channel.signal.physicalMin < 0 || channel.signal.physicalMax < 0,\n );\n const nameWidth = Buffer.byteLength(escapeCsvField(channel.column));\n // Two commas and the newline.\n bytes += groupRows * (timeWidth + nameWidth + valueWidth + 3);\n }\n continue;\n }\n\n rows += groupRows;\n if (groupRows + 1 > SPREADSHEET_ROW_LIMIT) exceeds = true;\n\n /*\n Width per cell, from the channel's own calibration rather than a flat allowance.\n\n The old `decimals + 6` budgeted six characters for the sign, integer part and decimal\n point on every channel, whatever it actually held. That over-counted a millivolt\n channel spanning ±5 by four characters a cell and ran 30-55% high across the fixture\n set — on a number people use to decide whether a conversion is worth starting.\n\n The channel's declared physical range is what bounds a cell, so that bound is what is\n used. Most samples sit below it, so this still reads high, which is the direction a\n size estimate should err in.\n\n One case is outside the bound rather than under it: nothing obliges a recording to keep\n its samples inside the digital range it declares, and one that does not maps outside the\n physical range too. Such a file can convert larger than the estimate. Clamping the data\n to make the estimate true is not a trade worth making — the samples are what they are.\n */\n const timeWidth = widthOf(range.endSeconds, group.timeDecimals);\n const cellWidth = group.channels.reduce(\n (sum, c) =>\n sum +\n widthOf(\n Math.max(Math.abs(c.signal.physicalMin), Math.abs(c.signal.physicalMax)),\n c.decimals,\n c.signal.physicalMin < 0 || c.signal.physicalMax < 0,\n ),\n 0,\n );\n // One comma per channel, plus the newline.\n bytes += groupRows * (timeWidth + cellWidth + group.channels.length + 1);\n /*\n The header row, measured as it will be written rather than as the labels are stored.\n\n A column name is quoted when it contains a comma, a quote, a newline or a leading or\n trailing space, and every quote inside it is doubled. Counting the raw label under-counted\n that row: three channels labelled `a,b,c,d,e`, `x\"y` and `plain` write a 32-byte header\n and were budgeted 27. EDF labels are free text, so commas in them are ordinary — a montage\n written as `EEG Fpz-Cz, ref` is exactly the kind of thing this is for.\n\n csvRow is the function that writes it, so it is the function that measures it. Nothing\n else is in a position to stay correct when the quoting rules change.\n */\n bytes += Buffer.byteLength(csvRow(['time_s', ...group.channels.map((c) => c.column)])) + 1;\n // Three bytes per file under --bom. Small, but the estimate promises never to read\n // under what gets written, and a one-row conversion is small enough for it to matter.\n if (bom) bytes += BOM_BYTES;\n }\n\n if (layout === 'long' && groups.length > 0) {\n if (longRows + 1 > SPREADSHEET_ROW_LIMIT) exceeds = true;\n bytes += Buffer.byteLength(csvRow(['time_s', 'channel', 'value'])) + 1;\n if (bom) bytes += BOM_BYTES;\n }\n\n return { rows, bytes, exceedsSpreadsheetLimit: exceeds };\n}\n"]}
@@ -44,11 +44,17 @@ export declare function quantizationStep(signal: EdfSignal): number;
44
44
  */
45
45
  export declare function decimalsForSignal(signal: EdfSignal, max?: number): number;
46
46
  /**
47
- * Whether the decimals a channel gets are fewer than its quantization step needs.
47
+ * Whether this channel's step is finer than any precision the tool can print.
48
48
  *
49
- * True only when the step is below 1e-98 and the ceiling above bites. Asked by the planner,
50
- * which turns it into a warning, because a value column that cannot separate consecutive
51
- * codes is the same failure the time column raises TIME_RESOLUTION for.
49
+ * Asked of the ceiling, not of the precision in use. `--decimals 2` on a channel needing 3
50
+ * is a trade the caller made knowingly and is not this warning's business — 0.5.10 fixed a
51
+ * version of this that fired on every ordinary EEG at `--decimals 2` and made
52
+ * `--decimals 2 --strict` impossible. But it fixed it by asking "did the caller choose the
53
+ * precision", which suppressed the real case too: at `--decimals 20` a channel stepping by
54
+ * 1e-106 printed every one of its codes as `0.00000000000000000000`, in silence.
55
+ *
56
+ * The question is whether anything the tool can print would separate consecutive codes. When
57
+ * the answer is no, that is a ceiling nobody chose, and it holds whatever `--decimals` says.
52
58
  */
53
- export declare function decimalsAreClamped(signal: EdfSignal, decimals: number): boolean;
59
+ export declare function decimalsAreClamped(signal: EdfSignal): boolean;
54
60
  //# sourceMappingURL=scale.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scale.d.ts","sourceRoot":"","sources":["../../src/edf/scale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;AAEjD,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAqCpD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAI1D;AAcD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,SAAuB,GAAG,MAAM,CAKvF;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI/E"}
1
+ {"version":3,"file":"scale.d.ts","sourceRoot":"","sources":["../../src/edf/scale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;AAEjD,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAqCpD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAI1D;AAcD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,SAAuB,GAAG,MAAM,CAKvF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAI7D"}
package/dist/edf/scale.js CHANGED
@@ -98,16 +98,22 @@ export function decimalsForSignal(signal, max = MAX_DERIVED_DECIMALS) {
98
98
  return Math.min(max, Math.max(0, needed));
99
99
  }
100
100
  /**
101
- * Whether the decimals a channel gets are fewer than its quantization step needs.
101
+ * Whether this channel's step is finer than any precision the tool can print.
102
102
  *
103
- * True only when the step is below 1e-98 and the ceiling above bites. Asked by the planner,
104
- * which turns it into a warning, because a value column that cannot separate consecutive
105
- * codes is the same failure the time column raises TIME_RESOLUTION for.
103
+ * Asked of the ceiling, not of the precision in use. `--decimals 2` on a channel needing 3
104
+ * is a trade the caller made knowingly and is not this warning's business — 0.5.10 fixed a
105
+ * version of this that fired on every ordinary EEG at `--decimals 2` and made
106
+ * `--decimals 2 --strict` impossible. But it fixed it by asking "did the caller choose the
107
+ * precision", which suppressed the real case too: at `--decimals 20` a channel stepping by
108
+ * 1e-106 printed every one of its codes as `0.00000000000000000000`, in silence.
109
+ *
110
+ * The question is whether anything the tool can print would separate consecutive codes. When
111
+ * the answer is no, that is a ceiling nobody chose, and it holds whatever `--decimals` says.
106
112
  */
107
- export function decimalsAreClamped(signal, decimals) {
113
+ export function decimalsAreClamped(signal) {
108
114
  const step = quantizationStep(signal);
109
115
  if (!(step > 0) || !Number.isFinite(step))
110
116
  return false;
111
- return Math.ceil(-Math.log10(step)) + 2 > decimals;
117
+ return Math.ceil(-Math.log10(step)) + 2 > MAX_DERIVED_DECIMALS;
112
118
  }
113
119
  //# sourceMappingURL=scale.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"scale.js","sourceRoot":"","sources":["../../src/edf/scale.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Digital-to-physical conversion.\n *\n * EDF defines the mapping by two calibration points, (digitalMin -> physicalMin)\n * and (digitalMax -> physicalMax), which the specification writes as:\n *\n * gain = (physicalMax - physicalMin) / (digitalMax - digitalMin)\n * physical = (digital - digitalMin) * gain + physicalMin\n *\n * That form is evaluated here in EDFlib's algebraically equivalent arrangement:\n *\n * offset = physicalMax / gain - digitalMax\n * physical = gain * (offset + digital)\n *\n * The rearrangement is not cosmetic. Written the first way, a channel spanning\n * +/-800 uV computes a value near 800 and then subtracts 800, and the cancellation\n * throws away low-order bits: digital 0 yields 0.19536019536019467 when the exact\n * value is 0.19536019536019536. EDFlib's form keeps the intermediate small\n * (offset + digital = 0.5 here) and returns the correctly rounded result.\n *\n * Both properties matter. The values are as accurate as a double can express, and\n * they are bit-identical to pyEDFlib and EDFbrowser, which share EDFlib's arithmetic,\n * so the test suite can assert exact equality against a reference implementation\n * rather than settling for a tolerance.\n */\n\nimport type { EdfSignal } from './header.js';\n\nexport type Scaler = (digital: number) => number;\n\nexport function makeScaler(signal: EdfSignal): Scaler {\n const { digitalMin, digitalMax, physicalMin, physicalMax } = signal;\n\n // A zero digital span leaves the mapping undefined — the header contradicts itself,\n // so there is no physical value for any sample on this channel.\n //\n // NaN rather than a stand-in number. Writing the physical minimum produces a column\n // of plausible readings (\"-100.000\" repeated) that is indistinguishable from a real\n // flat recording once the CSV is opened somewhere else, which is exactly the kind of\n // invented data this tool exists to avoid. NaN carries through to an empty CSV cell\n // and reads back as NaN in pandas, matching how a missing annotation duration is\n // already written. DEGENERATE_DIGITAL_RANGE is raised alongside it.\n if (digitalMax === digitalMin) return () => NaN;\n\n const gain = (physicalMax - physicalMin) / (digitalMax - digitalMin);\n\n // A flat physical range makes every sample the same value, and would divide by\n // zero in the offset below. That mapping is defined, so its constant is written.\n if (gain === 0) return () => physicalMin;\n\n // A non-finite gain is a different thing: the physical span overflowed a double, so\n // there is no mapping at all. Returning physicalMin filled the column with one enormous\n // constant — every distinct sample rendered as the same 300-digit number — and raised\n // nothing. NaN takes the same route as a degenerate digital range: empty cells, plus\n // UNUSABLE_PHYSICAL_RANGE from the header parser.\n if (!Number.isFinite(gain)) return () => NaN;\n\n // Deriving the offset divides by the gain. For every realistic calibration that is\n // both safe and more accurate, but an absurd header (a huge physical range over a\n // near-zero gain) could overflow it, so fall back to the specification's own\n // arrangement rather than emitting Infinity.\n const offset = physicalMax / gain - digitalMax;\n if (!Number.isFinite(offset)) {\n return (digital: number): number => (digital - digitalMin) * gain + physicalMin;\n }\n\n return (digital: number): number => gain * (offset + digital);\n}\n\n/**\n * Smallest physical step this channel can express — one digital unit.\n * Used to choose a decimal precision that preserves every distinct sample value.\n */\nexport function quantizationStep(signal: EdfSignal): number {\n const digitalSpan = signal.digitalMax - signal.digitalMin;\n if (digitalSpan === 0) return 0;\n return Math.abs((signal.physicalMax - signal.physicalMin) / digitalSpan);\n}\n\n/**\n * The most `toFixed` accepts. 101 is a RangeError, so this is the ceiling, not a taste.\n *\n * It used to be 20, on the stated grounds that 20 was what `toFixed` allowed. It is not,\n * and the gap was not academic: a magnetometer channel spanning ±1e-16 T over a 16-bit\n * converter has a step of 3.05e-21 and needs 23 places. Clamped to 20, every value landed\n * on a 1e-20 grid — about three digital codes to a printed value — so 69% of the samples\n * could not be recovered, the conversion exited 0, and nothing said a word. The channel\n * type the old comment named as the reason for the ceiling was the one it broke.\n */\nconst MAX_DERIVED_DECIMALS = 100;\n\n/**\n * Decimal places needed so that two adjacent digital codes never round to the same\n * string. Two places past the quantization step keep rounding error far below the\n * resolution the hardware actually recorded, without padding the file with digits\n * that carry no information.\n *\n * Ordinary channels land at three or four: a ±800 µV channel over 12 bits steps by\n * 0.39 µV and needs three. The ceiling is only reached by calibrations whose step is\n * below 1e-98, which an 8-character physical bound can still express — `1e-99` is five\n * characters. Those get VALUE_RESOLUTION rather than silence.\n */\nexport function decimalsForSignal(signal: EdfSignal, max = MAX_DERIVED_DECIMALS): number {\n const step = quantizationStep(signal);\n if (!(step > 0) || !Number.isFinite(step)) return 3;\n const needed = Math.ceil(-Math.log10(step)) + 2;\n return Math.min(max, Math.max(0, needed));\n}\n\n/**\n * Whether the decimals a channel gets are fewer than its quantization step needs.\n *\n * True only when the step is below 1e-98 and the ceiling above bites. Asked by the planner,\n * which turns it into a warning, because a value column that cannot separate consecutive\n * codes is the same failure the time column raises TIME_RESOLUTION for.\n */\nexport function decimalsAreClamped(signal: EdfSignal, decimals: number): boolean {\n const step = quantizationStep(signal);\n if (!(step > 0) || !Number.isFinite(step)) return false;\n return Math.ceil(-Math.log10(step)) + 2 > decimals;\n}\n"]}
1
+ {"version":3,"file":"scale.js","sourceRoot":"","sources":["../../src/edf/scale.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Digital-to-physical conversion.\n *\n * EDF defines the mapping by two calibration points, (digitalMin -> physicalMin)\n * and (digitalMax -> physicalMax), which the specification writes as:\n *\n * gain = (physicalMax - physicalMin) / (digitalMax - digitalMin)\n * physical = (digital - digitalMin) * gain + physicalMin\n *\n * That form is evaluated here in EDFlib's algebraically equivalent arrangement:\n *\n * offset = physicalMax / gain - digitalMax\n * physical = gain * (offset + digital)\n *\n * The rearrangement is not cosmetic. Written the first way, a channel spanning\n * +/-800 uV computes a value near 800 and then subtracts 800, and the cancellation\n * throws away low-order bits: digital 0 yields 0.19536019536019467 when the exact\n * value is 0.19536019536019536. EDFlib's form keeps the intermediate small\n * (offset + digital = 0.5 here) and returns the correctly rounded result.\n *\n * Both properties matter. The values are as accurate as a double can express, and\n * they are bit-identical to pyEDFlib and EDFbrowser, which share EDFlib's arithmetic,\n * so the test suite can assert exact equality against a reference implementation\n * rather than settling for a tolerance.\n */\n\nimport type { EdfSignal } from './header.js';\n\nexport type Scaler = (digital: number) => number;\n\nexport function makeScaler(signal: EdfSignal): Scaler {\n const { digitalMin, digitalMax, physicalMin, physicalMax } = signal;\n\n // A zero digital span leaves the mapping undefined — the header contradicts itself,\n // so there is no physical value for any sample on this channel.\n //\n // NaN rather than a stand-in number. Writing the physical minimum produces a column\n // of plausible readings (\"-100.000\" repeated) that is indistinguishable from a real\n // flat recording once the CSV is opened somewhere else, which is exactly the kind of\n // invented data this tool exists to avoid. NaN carries through to an empty CSV cell\n // and reads back as NaN in pandas, matching how a missing annotation duration is\n // already written. DEGENERATE_DIGITAL_RANGE is raised alongside it.\n if (digitalMax === digitalMin) return () => NaN;\n\n const gain = (physicalMax - physicalMin) / (digitalMax - digitalMin);\n\n // A flat physical range makes every sample the same value, and would divide by\n // zero in the offset below. That mapping is defined, so its constant is written.\n if (gain === 0) return () => physicalMin;\n\n // A non-finite gain is a different thing: the physical span overflowed a double, so\n // there is no mapping at all. Returning physicalMin filled the column with one enormous\n // constant — every distinct sample rendered as the same 300-digit number — and raised\n // nothing. NaN takes the same route as a degenerate digital range: empty cells, plus\n // UNUSABLE_PHYSICAL_RANGE from the header parser.\n if (!Number.isFinite(gain)) return () => NaN;\n\n // Deriving the offset divides by the gain. For every realistic calibration that is\n // both safe and more accurate, but an absurd header (a huge physical range over a\n // near-zero gain) could overflow it, so fall back to the specification's own\n // arrangement rather than emitting Infinity.\n const offset = physicalMax / gain - digitalMax;\n if (!Number.isFinite(offset)) {\n return (digital: number): number => (digital - digitalMin) * gain + physicalMin;\n }\n\n return (digital: number): number => gain * (offset + digital);\n}\n\n/**\n * Smallest physical step this channel can express — one digital unit.\n * Used to choose a decimal precision that preserves every distinct sample value.\n */\nexport function quantizationStep(signal: EdfSignal): number {\n const digitalSpan = signal.digitalMax - signal.digitalMin;\n if (digitalSpan === 0) return 0;\n return Math.abs((signal.physicalMax - signal.physicalMin) / digitalSpan);\n}\n\n/**\n * The most `toFixed` accepts. 101 is a RangeError, so this is the ceiling, not a taste.\n *\n * It used to be 20, on the stated grounds that 20 was what `toFixed` allowed. It is not,\n * and the gap was not academic: a magnetometer channel spanning ±1e-16 T over a 16-bit\n * converter has a step of 3.05e-21 and needs 23 places. Clamped to 20, every value landed\n * on a 1e-20 grid — about three digital codes to a printed value — so 69% of the samples\n * could not be recovered, the conversion exited 0, and nothing said a word. The channel\n * type the old comment named as the reason for the ceiling was the one it broke.\n */\nconst MAX_DERIVED_DECIMALS = 100;\n\n/**\n * Decimal places needed so that two adjacent digital codes never round to the same\n * string. Two places past the quantization step keep rounding error far below the\n * resolution the hardware actually recorded, without padding the file with digits\n * that carry no information.\n *\n * Ordinary channels land at three or four: a ±800 µV channel over 12 bits steps by\n * 0.39 µV and needs three. The ceiling is only reached by calibrations whose step is\n * below 1e-98, which an 8-character physical bound can still express — `1e-99` is five\n * characters. Those get VALUE_RESOLUTION rather than silence.\n */\nexport function decimalsForSignal(signal: EdfSignal, max = MAX_DERIVED_DECIMALS): number {\n const step = quantizationStep(signal);\n if (!(step > 0) || !Number.isFinite(step)) return 3;\n const needed = Math.ceil(-Math.log10(step)) + 2;\n return Math.min(max, Math.max(0, needed));\n}\n\n/**\n * Whether this channel's step is finer than any precision the tool can print.\n *\n * Asked of the ceiling, not of the precision in use. `--decimals 2` on a channel needing 3\n * is a trade the caller made knowingly and is not this warning's business — 0.5.10 fixed a\n * version of this that fired on every ordinary EEG at `--decimals 2` and made\n * `--decimals 2 --strict` impossible. But it fixed it by asking \"did the caller choose the\n * precision\", which suppressed the real case too: at `--decimals 20` a channel stepping by\n * 1e-106 printed every one of its codes as `0.00000000000000000000`, in silence.\n *\n * The question is whether anything the tool can print would separate consecutive codes. When\n * the answer is no, that is a ceiling nobody chose, and it holds whatever `--decimals` says.\n */\nexport function decimalsAreClamped(signal: EdfSignal): boolean {\n const step = quantizationStep(signal);\n if (!(step > 0) || !Number.isFinite(step)) return false;\n return Math.ceil(-Math.log10(step)) + 2 > MAX_DERIVED_DECIMALS;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edf2csv",
3
- "version": "0.5.20",
3
+ "version": "0.5.23",
4
4
  "description": "Convert EDF, EDF+ and BDF biosignal recordings (European Data Format) to CSV from the command line. Local, streaming, and never resamples or alters units.",
5
5
  "keywords": [
6
6
  "edf",