edf2csv 0.1.0

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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/dist/cli/report.d.ts +17 -0
  4. package/dist/cli/report.d.ts.map +1 -0
  5. package/dist/cli/report.js +107 -0
  6. package/dist/cli/report.js.map +1 -0
  7. package/dist/cli.d.ts +13 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +315 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/convert/channels.d.ts +40 -0
  12. package/dist/convert/channels.d.ts.map +1 -0
  13. package/dist/convert/channels.js +134 -0
  14. package/dist/convert/channels.js.map +1 -0
  15. package/dist/convert/plan.d.ts +70 -0
  16. package/dist/convert/plan.d.ts.map +1 -0
  17. package/dist/convert/plan.js +127 -0
  18. package/dist/convert/plan.js.map +1 -0
  19. package/dist/convert/run.d.ts +48 -0
  20. package/dist/convert/run.d.ts.map +1 -0
  21. package/dist/convert/run.js +358 -0
  22. package/dist/convert/run.js.map +1 -0
  23. package/dist/convert/time-range.d.ts +63 -0
  24. package/dist/convert/time-range.d.ts.map +1 -0
  25. package/dist/convert/time-range.js +188 -0
  26. package/dist/convert/time-range.js.map +1 -0
  27. package/dist/convert/timing.d.ts +18 -0
  28. package/dist/convert/timing.d.ts.map +1 -0
  29. package/dist/convert/timing.js +69 -0
  30. package/dist/convert/timing.js.map +1 -0
  31. package/dist/edf/annotations.d.ts +43 -0
  32. package/dist/edf/annotations.d.ts.map +1 -0
  33. package/dist/edf/annotations.js +88 -0
  34. package/dist/edf/annotations.js.map +1 -0
  35. package/dist/edf/errors.d.ts +27 -0
  36. package/dist/edf/errors.d.ts.map +1 -0
  37. package/dist/edf/errors.js +23 -0
  38. package/dist/edf/errors.js.map +1 -0
  39. package/dist/edf/header.d.ts +115 -0
  40. package/dist/edf/header.d.ts.map +1 -0
  41. package/dist/edf/header.js +383 -0
  42. package/dist/edf/header.js.map +1 -0
  43. package/dist/edf/reader.d.ts +75 -0
  44. package/dist/edf/reader.d.ts.map +1 -0
  45. package/dist/edf/reader.js +224 -0
  46. package/dist/edf/reader.js.map +1 -0
  47. package/dist/edf/scale.d.ts +46 -0
  48. package/dist/edf/scale.d.ts.map +1 -0
  49. package/dist/edf/scale.js +76 -0
  50. package/dist/edf/scale.js.map +1 -0
  51. package/dist/format/csv.d.ts +34 -0
  52. package/dist/format/csv.d.ts.map +1 -0
  53. package/dist/format/csv.js +136 -0
  54. package/dist/format/csv.js.map +1 -0
  55. package/dist/format/number.d.ts +42 -0
  56. package/dist/format/number.d.ts.map +1 -0
  57. package/dist/format/number.js +108 -0
  58. package/dist/format/number.js.map +1 -0
  59. package/dist/index.d.ts +25 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +17 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/version.d.ts +3 -0
  64. package/dist/version.d.ts.map +1 -0
  65. package/dist/version.js +8 -0
  66. package/dist/version.js.map +1 -0
  67. package/package.json +73 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sarthak Tayal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # edf2csv
2
+
3
+ Convert an EDF, EDF+ or BDF recording to CSV with one command.
4
+
5
+ ```bash
6
+ npx edf2csv recording.edf
7
+ ```
8
+
9
+ That's it. You get a `recording_csv/` folder you can open in pandas, R, MATLAB, or
10
+ Excel. Nothing is uploaded, nothing is installed permanently, and the numbers are
11
+ not altered on the way through.
12
+
13
+ Full documentation: **[edf2csv.vercel.app](https://edf2csv.vercel.app)**
14
+
15
+ ## What you get
16
+
17
+ ```text
18
+ recording_csv/
19
+ ├── signals.csv the data, one row per time point
20
+ ├── channels.csv each channel's unit, sampling rate and range
21
+ ├── annotations.csv EDF+ events (only if the file has them)
22
+ └── metadata.json recording details
23
+ ```
24
+
25
+ ```csv
26
+ time_s,FP1-F7,F7-T7,T7-P7
27
+ 0.000000,-17.778,39.267,-3.712
28
+ 0.003906,0.195,0.195,0.195
29
+ ```
30
+
31
+ `time_s` is seconds from the start. Columns are the channel names from the file.
32
+
33
+ ## Check before you convert
34
+
35
+ An hour of 23-channel EEG makes a 159 MB CSV with 921,600 rows, which only just
36
+ fits inside Excel's 1,048,576-row limit. Two hours does not. `--info` shows you what
37
+ you'd get, without writing anything:
38
+
39
+ ```bash
40
+ npx edf2csv recording.edf --info
41
+ ```
42
+
43
+ Want a piece instead of the whole thing:
44
+
45
+ ```bash
46
+ npx edf2csv recording.edf --start 30m --duration 5m
47
+ ```
48
+
49
+ ## Options
50
+
51
+ ```text
52
+ -i, --info Show what's in the file, convert nothing
53
+ -o, --out <dir> Where to write it
54
+ -c, --channels <list> Only these channels, e.g. "EEG Fpz-Cz,ECG"
55
+ --start <time> Start here (30s, 5m, 1h30m, 00:30:00)
56
+ --duration <time> How much to convert
57
+ --end <time> Or stop here instead
58
+ --annotations-only Just the EDF+ events
59
+ --decimals <n> Force a number of decimal places
60
+ --checksum Put a SHA-256 of the input in metadata.json
61
+ -f, --force Overwrite the output folder
62
+ -q, --quiet Less output
63
+ --json Machine-readable summary on stdout
64
+ -h, --help Help
65
+ ```
66
+
67
+ Exit codes: `0` fine, `1` couldn't read or write the file, `2` bad command.
68
+
69
+ ## What it won't do to your data
70
+
71
+ **It won't resample.** Some EDF files mix rates: EEG at 256 Hz, temperature at
72
+ 1 Hz. Squeezing those into one table means inventing samples, so each rate gets its
73
+ own file (`signals_256hz.csv`, `signals_1hz.csv`). A 1 Hz channel over 3 seconds
74
+ gets 3 rows, because that's how many readings exist.
75
+
76
+ For comparison, `mne.io.read_raw_edf` turns those same 3 readings into 768
77
+ interpolated values with no warning. That's the right call for MNE, which needs one
78
+ uniform array to run its analysis. It's the wrong call for a converter.
79
+
80
+ **It won't convert units.** Microvolts stay microvolts.
81
+
82
+ **It won't hide gaps.** EDF+D recordings have real breaks in time. They show up as
83
+ a jump in `time_s` rather than being closed up.
84
+
85
+ **It won't stay quiet about problems.** Truncated files, headers that contradict
86
+ the data, duplicate channel names, calibration that can't be applied. You get told,
87
+ in plain words, before you rely on the output.
88
+
89
+ ## Is it right?
90
+
91
+ Values are checked against [pyEDFlib](https://github.com/holgern/pyedflib). On the
92
+ recordings used for testing, 129,536 sample values came out **bit-for-bit
93
+ identical**. Not close. Identical.
94
+
95
+ ```bash
96
+ npm test
97
+ ```
98
+
99
+ ## When to use something else
100
+
101
+ This is a converter. It is the right tool when you want the numbers out of an EDF
102
+ file and into something else. It is the wrong tool for several neighbouring jobs, and
103
+ saying so is cheaper than you finding out later.
104
+
105
+ - **Analysing the signal.** Filtering, epoching, re-referencing, ICA, spectral work:
106
+ use [MNE-Python](https://mne.tools). It is excellent and built for exactly this.
107
+ - **Reading EDF inside Python.** [pyEDFlib](https://github.com/holgern/pyedflib) hands
108
+ you arrays directly, with no CSV in the middle. edf2csv is checked against it.
109
+ - **Writing EDF files.** This only reads them.
110
+ - **Viewing a recording.** [EDFbrowser](https://www.teuniz.net/edfbrowser/) is a real
111
+ viewer and will beat a spreadsheet every time.
112
+ - **Huge recordings you will analyse repeatedly.** CSV is a poor archival format; an
113
+ EDF roughly quadruples in size as text. Convert a window, or read the file directly.
114
+
115
+ Use edf2csv when the destination is a spreadsheet, a colleague who does not use
116
+ Python, a quick look at part of a recording, or a pipeline that speaks CSV.
117
+
118
+ ## Notes
119
+
120
+ Needs Node 20+. No runtime dependencies. MIT licensed.
121
+
122
+ Reads EDF, EDF+ and BDF/BDF+ (BioSemi 24-bit). No filtering, no artifact removal,
123
+ no AI, no network calls. It reads a file and writes files.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Human-readable output for the terminal.
3
+ *
4
+ * Everything here is plain text with no colour codes, so piping to a file or a log
5
+ * produces exactly what appeared on screen.
6
+ */
7
+ import type { Diagnostic } from '../edf/errors.js';
8
+ import type { EdfFile } from '../edf/reader.js';
9
+ import type { ConversionPlan } from '../convert/plan.js';
10
+ import type { ConvertResult } from '../convert/run.js';
11
+ /** The `--info` view: what is in this recording, and what would converting it produce. */
12
+ export declare function formatInfo(file: EdfFile, plan: ConversionPlan): string;
13
+ /** One line per diagnostic, prefixed so warnings are greppable. */
14
+ export declare function formatDiagnostics(diagnostics: readonly Diagnostic[]): string;
15
+ export declare function formatSummary(result: ConvertResult): string;
16
+ export declare function summaryJson(result: ConvertResult): string;
17
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAGhD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAuBvD,0FAA0F;AAC1F,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,GAAG,MAAM,CA0DtE;AAED,mEAAmE;AACnE,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAO5E;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAU3D;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAczD"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Human-readable output for the terminal.
3
+ *
4
+ * Everything here is plain text with no colour codes, so piping to a file or a log
5
+ * produces exactly what appeared on screen.
6
+ */
7
+ import { describeFormat, formatRate, formatWallClock } from '../edf/header.js';
8
+ import { formatBytes, formatDuration } from '../format/number.js';
9
+ function table(rows, alignRight) {
10
+ if (rows.length === 0)
11
+ return '';
12
+ const width = [];
13
+ for (const row of rows) {
14
+ row.forEach((cell, i) => {
15
+ width[i] = Math.max(width[i] ?? 0, cell.length);
16
+ });
17
+ }
18
+ return rows
19
+ .map((row) => row
20
+ .map((cell, i) => {
21
+ const w = width[i] ?? 0;
22
+ return alignRight.has(i) ? cell.padStart(w) : cell.padEnd(w);
23
+ })
24
+ .join(' ')
25
+ .trimEnd())
26
+ .join('\n');
27
+ }
28
+ /** The `--info` view: what is in this recording, and what would converting it produce. */
29
+ export function formatInfo(file, plan) {
30
+ const { header } = file;
31
+ const lines = [];
32
+ lines.push(`File ${file.path}`);
33
+ lines.push(`Format ${describeFormat(header)}`);
34
+ lines.push(`Recorded ${formatWallClock(header.startDateTime)?.replace('T', ' ') ??
35
+ `${header.startDateRaw} ${header.startTimeRaw} (unparseable)`}`);
36
+ lines.push(`Duration ${formatDuration(file.durationSeconds)} (${file.recordCount} records of ${header.recordDuration}s)`);
37
+ const elapsedSpan = plan.range.recordingEndSeconds - plan.range.recordingStartSeconds;
38
+ if (Math.abs(elapsedSpan - file.durationSeconds) > 1e-9) {
39
+ lines.push(`Time span ${formatDuration(elapsedSpan)} (includes discontinuities)`);
40
+ }
41
+ lines.push(`Size ${formatBytes(file.fileSize)}`);
42
+ if (header.patientId)
43
+ lines.push(`Patient ${header.patientId}`);
44
+ if (header.recordingId)
45
+ lines.push(`Recording ${header.recordingId}`);
46
+ const signals = file.dataSignals;
47
+ lines.push('');
48
+ lines.push(`Channels ${signals.length} signal${signals.length === 1 ? '' : 's'}${file.annotationSignals.length > 0 ? ` + ${file.annotationSignals.length} annotation channel` : ''}`);
49
+ lines.push('');
50
+ const rows = [['#', 'COLUMN', 'LABEL', 'UNIT', 'RATE', 'RANGE', 'OUTPUT']];
51
+ const fileFor = new Map();
52
+ for (const group of plan.groups) {
53
+ for (const channel of group.channels)
54
+ fileFor.set(channel.signal.index, group.fileName);
55
+ }
56
+ for (const signal of signals) {
57
+ rows.push([
58
+ String(signal.index),
59
+ plan.columnNames.get(signal.index) ?? '',
60
+ signal.label,
61
+ signal.physicalDimension,
62
+ `${formatRate(signal.samplingRate)} Hz`,
63
+ `${signal.physicalMin} to ${signal.physicalMax}`,
64
+ fileFor.get(signal.index) ?? '(not selected)',
65
+ ]);
66
+ }
67
+ lines.push(table(rows, new Set([0])));
68
+ lines.push('');
69
+ if (plan.groups.length > 1) {
70
+ lines.push(`Sampling rates differ, so channels are written to ${plan.groups.length} files, one per rate. ` +
71
+ `No channel is resampled.`);
72
+ }
73
+ lines.push(`Would write ${plan.estimate.rows.toLocaleString('en-US')} rows, roughly ${formatBytes(plan.estimate.bytes)}.`);
74
+ return lines.join('\n');
75
+ }
76
+ /** One line per diagnostic, prefixed so warnings are greppable. */
77
+ export function formatDiagnostics(diagnostics) {
78
+ return diagnostics
79
+ .map((d) => {
80
+ const head = `${d.severity === 'warning' ? 'warning' : 'note'}: ${d.message}`;
81
+ return d.hint ? `${head}\n ${d.hint}` : head;
82
+ })
83
+ .join('\n');
84
+ }
85
+ export function formatSummary(result) {
86
+ const lines = [];
87
+ const rows = [];
88
+ for (const file of result.files) {
89
+ rows.push([` ${file.name}`, file.rows.toLocaleString('en-US'), file.name.endsWith('.csv') ? 'rows' : '']);
90
+ }
91
+ lines.push(`Wrote ${result.outputDir}`);
92
+ lines.push(table(rows, new Set([1])));
93
+ lines.push(`Done in ${(result.elapsedMs / 1000).toFixed(1)}s.`);
94
+ return lines.join('\n');
95
+ }
96
+ export function summaryJson(result) {
97
+ return JSON.stringify({
98
+ output_dir: result.outputDir,
99
+ files: result.files,
100
+ annotations: result.annotationCount,
101
+ duration_seconds: result.file.durationSeconds,
102
+ records: result.file.recordCount,
103
+ elapsed_ms: result.elapsedMs,
104
+ warnings: result.diagnostics.map((d) => ({ code: d.code, severity: d.severity, message: d.message })),
105
+ }, null, 2);
106
+ }
107
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Command-line entry point.
4
+ *
5
+ * Output discipline: anything that is the *result* of a command goes to stdout
6
+ * (`--info`'s channel table, `--json`'s summary). Progress, warnings and the
7
+ * conversion summary go to stderr, so a conversion can be run in a pipeline without
8
+ * its chatter contaminating the data.
9
+ */
10
+ import { defaultOutputDir } from './convert/run.js';
11
+ export declare function main(argv: readonly string[]): Promise<number>;
12
+ export { defaultOutputDir };
13
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG;AAUH,OAAO,EAA4B,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAuE9E,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAqLnE;AAqCD,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
package/dist/cli.js ADDED
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Command-line entry point.
4
+ *
5
+ * Output discipline: anything that is the *result* of a command goes to stdout
6
+ * (`--info`'s channel table, `--json`'s summary). Progress, warnings and the
7
+ * conversion summary go to stderr, so a conversion can be run in a pipeline without
8
+ * its chatter contaminating the data.
9
+ */
10
+ import { parseArgs } from 'node:util';
11
+ import { pathToFileURL, fileURLToPath } from 'node:url';
12
+ import { realpathSync } from 'node:fs';
13
+ import process from 'node:process';
14
+ import { EdfError } from './edf/errors.js';
15
+ import { EdfFile } from './edf/reader.js';
16
+ import { buildPlan } from './convert/plan.js';
17
+ import { ConversionError, convert, defaultOutputDir } from './convert/run.js';
18
+ import { ChannelSelectionError } from './convert/channels.js';
19
+ import { TimeRangeError, parseTimeSpec } from './convert/time-range.js';
20
+ import { deriveRecordStarts } from './convert/timing.js';
21
+ import { formatDiagnostics, formatInfo, formatSummary, summaryJson } from './cli/report.js';
22
+ import { VERSION } from './version.js';
23
+ const USAGE = `edf2csv ${VERSION}
24
+ Convert EDF, EDF+ and BDF recordings to CSV
25
+
26
+ Usage
27
+ edf2csv <recording.edf> [options]
28
+
29
+ Options
30
+ -i, --info Show the recording's structure and estimated output size,
31
+ without converting anything
32
+ -o, --out <dir> Output directory (default: <recording>_csv beside the input)
33
+ -c, --channels <list> Only these channels, comma-separated. Use #N to pick a
34
+ channel by position when two share a label
35
+ --start <time> Begin at this offset (30s, 5m, 1h30m, 00:30:00)
36
+ --duration <time> Convert this much
37
+ --end <time> Stop at this offset (instead of --duration)
38
+ --annotations-only Write only the EDF+ annotations, no signal data
39
+ --decimals <n> Fix the decimal places instead of deriving them per channel
40
+ --checksum Record a SHA-256 of the input in metadata.json
41
+ -f, --force Overwrite the output directory if it exists
42
+ -q, --quiet Suppress the summary; warnings and errors still print
43
+ --json Print a machine-readable summary to stdout
44
+ -h, --help Show this help
45
+ -V, --version Show the version
46
+
47
+ Output
48
+ A directory containing signals.csv, channels.csv, metadata.json, and
49
+ annotations.csv when the recording carries EDF+ annotations. Channels recorded
50
+ at different sampling rates are written to separate files, never resampled.
51
+
52
+ Examples
53
+ edf2csv recording.edf
54
+ edf2csv recording.edf --info
55
+ edf2csv recording.edf --channels "EEG Fpz-Cz,ECG" --out ./converted
56
+ edf2csv recording.edf --start 30m --duration 5m
57
+ edf2csv recording.edf --annotations-only
58
+ `;
59
+ /** A problem with how the command was invoked, as opposed to a problem with the file. */
60
+ class OptionError extends Error {
61
+ constructor(message) {
62
+ super(message);
63
+ this.name = 'OptionError';
64
+ }
65
+ }
66
+ const EXIT_OK = 0;
67
+ const EXIT_ERROR = 1;
68
+ const EXIT_USAGE = 2;
69
+ /**
70
+ * Piping into a consumer that exits early (`| head -1`) closes our stdout, and the
71
+ * next write raises EPIPE. That is normal in a shell pipeline, not an error worth a
72
+ * stack trace, so it is swallowed while anything else still surfaces.
73
+ */
74
+ function ignoreBrokenPipe(stream) {
75
+ stream.on('error', (error) => {
76
+ if (error.code === 'EPIPE') {
77
+ process.exitCode = 0;
78
+ return;
79
+ }
80
+ throw error;
81
+ });
82
+ }
83
+ export async function main(argv) {
84
+ ignoreBrokenPipe(process.stdout);
85
+ ignoreBrokenPipe(process.stderr);
86
+ let values;
87
+ let positionals;
88
+ try {
89
+ const parsed = parseArgs({
90
+ args: [...argv],
91
+ allowPositionals: true,
92
+ strict: true,
93
+ options: {
94
+ info: { type: 'boolean', short: 'i' },
95
+ out: { type: 'string', short: 'o' },
96
+ channels: { type: 'string', short: 'c', multiple: true },
97
+ start: { type: 'string' },
98
+ duration: { type: 'string' },
99
+ end: { type: 'string' },
100
+ 'annotations-only': { type: 'boolean' },
101
+ decimals: { type: 'string' },
102
+ checksum: { type: 'boolean' },
103
+ force: { type: 'boolean', short: 'f' },
104
+ quiet: { type: 'boolean', short: 'q' },
105
+ json: { type: 'boolean' },
106
+ help: { type: 'boolean', short: 'h' },
107
+ version: { type: 'boolean', short: 'V' },
108
+ },
109
+ });
110
+ values = parsed.values;
111
+ positionals = parsed.positionals;
112
+ }
113
+ catch (error) {
114
+ process.stderr.write(`${message(error)}\n\nRun edf2csv --help to see the options.\n`);
115
+ return EXIT_USAGE;
116
+ }
117
+ if (values['help'] === true) {
118
+ process.stdout.write(USAGE);
119
+ return EXIT_OK;
120
+ }
121
+ if (values['version'] === true) {
122
+ process.stdout.write(`${VERSION}\n`);
123
+ return EXIT_OK;
124
+ }
125
+ if (positionals.length === 0) {
126
+ process.stderr.write(`No input file given.\n\n${USAGE}`);
127
+ return EXIT_USAGE;
128
+ }
129
+ if (positionals.length > 1) {
130
+ process.stderr.write(`Expected one input file but got ${positionals.length}: ${positionals.join(', ')}\n` +
131
+ `Convert them one at a time, or use a shell loop.\n`);
132
+ return EXIT_USAGE;
133
+ }
134
+ const input = positionals[0];
135
+ const quiet = values['quiet'] === true;
136
+ const asJson = values['json'] === true;
137
+ try {
138
+ const channels = splitChannels(values['channels']);
139
+ const start = optionalTime(values['start'], '--start');
140
+ const duration = optionalTime(values['duration'], '--duration');
141
+ const end = optionalTime(values['end'], '--end');
142
+ const decimals = optionalDecimals(values['decimals']);
143
+ if (values['info'] === true) {
144
+ const file = await EdfFile.open(input);
145
+ try {
146
+ const annotationData = file.annotationSignals.length > 0
147
+ ? await file.readAnnotations()
148
+ : { annotations: [], recordStarts: [], malformed: 0 };
149
+ const timing = deriveRecordStarts(file, annotationData);
150
+ const plan = buildPlan({
151
+ signals: file.header.signals,
152
+ recordDuration: file.header.recordDuration,
153
+ recordCount: file.recordCount,
154
+ hasAnnotationChannel: file.annotationSignals.length > 0,
155
+ recordStarts: timing.starts,
156
+ }, {
157
+ channels,
158
+ start,
159
+ duration,
160
+ end,
161
+ decimals,
162
+ annotationsOnly: values['annotations-only'] === true,
163
+ });
164
+ plan.diagnostics.push(...timing.diagnostics);
165
+ process.stdout.write(`${formatInfo(file, plan)}\n`);
166
+ const diagnostics = [...file.diagnostics, ...plan.diagnostics];
167
+ if (diagnostics.length > 0) {
168
+ process.stderr.write(`\n${formatDiagnostics(diagnostics)}\n`);
169
+ }
170
+ }
171
+ finally {
172
+ await file.close();
173
+ }
174
+ return EXIT_OK;
175
+ }
176
+ const showProgress = !quiet && !asJson && process.stderr.isTTY === true;
177
+ let lastTick = 0;
178
+ const destination = typeof values['out'] === 'string' ? values['out'] : defaultOutputDir(input);
179
+ /*
180
+ Interrupting a conversion leaves a CSV that stops mid-recording but is still
181
+ perfectly well-formed, so nothing about the file itself reveals that half the
182
+ data is missing. Saying so on the way out is the whole point of a tool that
183
+ claims it will not go quiet when something is wrong.
184
+ */
185
+ const onInterrupt = (signal) => {
186
+ if (showProgress)
187
+ process.stderr.write('\r\u001b[K');
188
+ process.stderr.write(`\ninterrupted (${signal}): the conversion stopped part way through.\n` +
189
+ ` Files already written to "${destination}" are incomplete and should not be used.\n`);
190
+ // 128 + signal number, the conventional exit status for dying to a signal.
191
+ process.exit(signal === 'SIGINT' ? 130 : 143);
192
+ };
193
+ process.once('SIGINT', onInterrupt);
194
+ process.once('SIGTERM', onInterrupt);
195
+ const result = await convert(input, {
196
+ outputDir: typeof values['out'] === 'string' ? values['out'] : undefined,
197
+ channels,
198
+ start,
199
+ duration,
200
+ end,
201
+ decimals,
202
+ annotationsOnly: values['annotations-only'] === true,
203
+ checksum: values['checksum'] === true,
204
+ force: values['force'] === true,
205
+ onProgress: showProgress
206
+ ? (progress) => {
207
+ const now = Date.now();
208
+ if (now - lastTick < 100)
209
+ return;
210
+ lastTick = now;
211
+ const percent = progress.recordsTotal === 0 ? 100 : Math.floor((progress.recordsDone / progress.recordsTotal) * 100);
212
+ process.stderr.write(`\r converting… ${percent}%`);
213
+ }
214
+ : undefined,
215
+ });
216
+ process.off('SIGINT', onInterrupt);
217
+ process.off('SIGTERM', onInterrupt);
218
+ if (showProgress)
219
+ process.stderr.write('\r\u001b[K');
220
+ if (result.diagnostics.length > 0 && !asJson) {
221
+ process.stderr.write(`${formatDiagnostics(result.diagnostics)}\n\n`);
222
+ }
223
+ if (asJson) {
224
+ process.stdout.write(`${summaryJson(result)}\n`);
225
+ }
226
+ else if (!quiet) {
227
+ process.stderr.write(`${formatSummary(result)}\n`);
228
+ }
229
+ return EXIT_OK;
230
+ }
231
+ catch (error) {
232
+ if (error instanceof EdfError || error instanceof ConversionError) {
233
+ process.stderr.write(`error: ${error.message}\n`);
234
+ if (error.hint)
235
+ process.stderr.write(` ${error.hint}\n`);
236
+ return EXIT_ERROR;
237
+ }
238
+ if (error instanceof ChannelSelectionError ||
239
+ error instanceof TimeRangeError ||
240
+ error instanceof OptionError) {
241
+ process.stderr.write(`error: ${error.message}\n`);
242
+ return EXIT_USAGE;
243
+ }
244
+ process.stderr.write(`error: ${message(error)}\n`);
245
+ return EXIT_ERROR;
246
+ }
247
+ }
248
+ function splitChannels(raw) {
249
+ if (raw === undefined)
250
+ return undefined;
251
+ const list = Array.isArray(raw) ? raw : [String(raw)];
252
+ const terms = list.flatMap((entry) => entry.split(',')).map((t) => t.trim()).filter((t) => t !== '');
253
+ // Returning undefined here would mean "no --channels given" and convert everything,
254
+ // which is the opposite of what someone passing an empty list is asking for.
255
+ if (terms.length === 0) {
256
+ throw new OptionError('--channels was given but lists no channel names.');
257
+ }
258
+ return terms;
259
+ }
260
+ function optionalTime(raw, option) {
261
+ if (raw === undefined)
262
+ return undefined;
263
+ return parseTimeSpec(String(raw), option);
264
+ }
265
+ function optionalDecimals(raw) {
266
+ if (raw === undefined)
267
+ return undefined;
268
+ const text = String(raw).trim();
269
+ // Number('') is 0, which would quietly round every physical value to a whole number.
270
+ if (text === '') {
271
+ throw new OptionError('--decimals needs a number, for example --decimals 3.');
272
+ }
273
+ const value = Number(text);
274
+ if (!Number.isInteger(value) || value < 0 || value > 15) {
275
+ throw new OptionError(`--decimals must be a whole number between 0 and 15, got "${String(raw)}".`);
276
+ }
277
+ return value;
278
+ }
279
+ function message(error) {
280
+ return error instanceof Error ? error.message : String(error);
281
+ }
282
+ export { defaultOutputDir };
283
+ /**
284
+ * Whether this file was executed rather than imported.
285
+ *
286
+ * npm installs a bin as a symlink (node_modules/.bin/edf2csv -> ../edf2csv/dist/cli.js),
287
+ * and that is the path `npx` runs. In that case process.argv[1] is the symlink while
288
+ * import.meta.url is already resolved to the real file, so comparing the two directly
289
+ * reports "imported" and the command silently does nothing. Both sides are resolved
290
+ * through realpath before comparing.
291
+ */
292
+ function isMainModule() {
293
+ const entry = process.argv[1];
294
+ if (entry === undefined)
295
+ return false;
296
+ try {
297
+ return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
298
+ }
299
+ catch {
300
+ // An unreadable or deleted entry path is not a reason to refuse to run.
301
+ return pathToFileURL(entry).href === import.meta.url;
302
+ }
303
+ }
304
+ const invokedDirectly = isMainModule();
305
+ if (invokedDirectly) {
306
+ main(process.argv.slice(2))
307
+ .then((code) => {
308
+ process.exitCode = code;
309
+ })
310
+ .catch((error) => {
311
+ process.stderr.write(`error: ${message(error)}\n`);
312
+ process.exitCode = EXIT_ERROR;
313
+ });
314
+ }
315
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}