edf2csv 0.5.58 → 0.5.59

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,39 @@
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.59
7
+
8
+ ### Fixed: `--info` never said where a recording starts, on the recordings that do not start at zero
9
+
10
+ 0.4.9 made the first data record's timekeeping annotation the point a recording is timed
11
+ from. A file whose first record says `+1000` therefore writes `time_s` from `1000.000`, and
12
+ `--start` and `--end` are read on that same clock. None of that appeared in `--info`, which
13
+ said
14
+
15
+ ```
16
+ Duration 3s (3 records of 1s)
17
+ ```
18
+
19
+ and nothing else — three seconds, which reads as 0 to 3. `--start 0 --end 1` on that file
20
+ then converted nothing and explained: "The window is inside the recording but lands where
21
+ there is no data — past the last sample, or inside a gap in a discontinuous file. Run with
22
+ --info to see where the records actually sit." `--info` was the one place the number was
23
+ missing, and the advice was a loop.
24
+
25
+ It is printed now, whenever it is not zero, and in seconds rather than through the duration
26
+ formatter — this number exists to be typed back in, and `--start` takes `1000s` where it does
27
+ not take "16m 40s":
28
+
29
+ ```
30
+ Duration 3s (3 records of 1s)
31
+ Timed from 1000.000s (first sample; --start and --end use this clock)
32
+ ```
33
+
34
+ Under `--json` it is `first_sample_seconds`. `duration_seconds` and `time_span_seconds` were
35
+ both already there and are both lengths; neither says where the length sits. The value was
36
+ already in `plan.range` and already governed the row estimate printed underneath — it was
37
+ simply never shown. A recording timed from zero, which is nearly all of them, gains no line.
38
+
6
39
  ## 0.5.58
7
40
 
8
41
  ### Fixed: a duration below zero was exported without comment
@@ -77,6 +77,25 @@ export function formatInfo(file, plan) {
77
77
  if (Math.abs(elapsedSpan - file.durationSeconds) > 1e-9) {
78
78
  lines.push(`Time span ${formatDuration(elapsedSpan)} (includes discontinuities)`);
79
79
  }
80
+ /*
81
+ Where the samples begin, when that is not zero.
82
+
83
+ 0.4.9 made the first record's timekeeping TAL the point a recording is timed from, so a
84
+ file whose TALs start at +1000 writes `time_s` from 1000.000 and takes `--start` and
85
+ `--end` on that same clock. None of that appeared here: the report said "Duration 3s",
86
+ which reads as 0 to 3, and `--start 0 --end 1` then selected nothing and answered with
87
+ "The window is inside the recording but lands where there is no data ... Run with --info
88
+ to see where the records actually sit" — pointing at this report, which was the one place
89
+ the number was missing. It is in `plan.range` already and governs the estimate printed
90
+ below; it was simply never shown.
91
+ */
92
+ const startsAt = plan.range.recordingStartSeconds;
93
+ if (Number.isFinite(startsAt) && Math.abs(startsAt) > 1e-9) {
94
+ // In seconds rather than through formatDuration, because this number is meant to be
95
+ // typed back in: `--start` takes `1000s`, and "16m 40s" is not something it accepts.
96
+ // It is also how the empty-window warning renders the window it was given.
97
+ lines.push(`Timed from ${startsAt.toFixed(3)}s (first sample; --start and --end use this clock)`);
98
+ }
80
99
  lines.push(`Size ${formatBytes(file.fileSize)}`);
81
100
  if (header.patientId)
82
101
  lines.push(`Patient ${printable(header.patientId)}`);
@@ -210,6 +229,10 @@ export function infoJson(file, plan, indent = 2) {
210
229
  duration_seconds: file.durationSeconds,
211
230
  // For a discontinuous file this exceeds duration_seconds by the length of the gaps.
212
231
  time_span_seconds: plan.range.recordingEndSeconds - plan.range.recordingStartSeconds,
232
+ // Where `time_s` begins, and the clock `--start` and `--end` are read against. Usually
233
+ // zero; not when the first record's timekeeping TAL puts the recording elsewhere. Both
234
+ // of the fields above are lengths and neither says where that length sits.
235
+ first_sample_seconds: plan.range.recordingStartSeconds,
213
236
  annotation_channels: file.annotationSignals.length,
214
237
  channels: file.dataSignals.map((signal) => ({
215
238
  signal_index: signal.index,
@@ -1 +1 @@
1
- {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Human-readable output for the terminal.\n *\n * Everything here is plain text with no colour codes, so piping to a file or a log\n * produces exactly what appeared on screen.\n */\n\nimport type { Diagnostic } from '../edf/errors.js';\nimport type { EdfFile } from '../edf/reader.js';\nimport { describeFormat, formatRates, formatWallClock } from '../edf/header.js';\nimport { formatBytes, formatDuration } from '../format/number.js';\nimport type { ConversionPlan } from '../convert/plan.js';\nimport { withoutFileRateWarning } from '../convert/plan.js';\nimport type { ConvertResult } from '../convert/run.js';\n\nfunction table(rows: readonly (readonly string[])[], alignRight: ReadonlySet<number>): string {\n if (rows.length === 0) return '';\n const width: number[] = [];\n for (const row of rows) {\n row.forEach((cell, i) => {\n width[i] = Math.max(width[i] ?? 0, cell.length);\n });\n }\n return rows\n .map((row) =>\n row\n .map((cell, i) => {\n const w = width[i] ?? 0;\n return alignRight.has(i) ? cell.padStart(w) : cell.padEnd(w);\n })\n .join(' ')\n .trimEnd(),\n )\n .join('\\n');\n}\n\n/**\n * Make header text safe to print to a terminal.\n *\n * EDF identification fields and channel labels are free text copied verbatim out of the\n * file, and `--info` puts them straight on stdout. A header carrying ANSI escapes could\n * therefore drive the reader's terminal — `\\x1b[2J\\x1b[H` clears the screen and homes the\n * cursor, which is enough to hide the rest of the output or repaint it as something else.\n * Nobody writes an EDF header that way on purpose, which is exactly why a file that does\n * should not be trusted with the terminal.\n *\n * Control bytes are shown as their escape instead, so a corrupt field stays diagnosable\n * rather than being silently swallowed. This affects display only: `channels.csv` and\n * `metadata.json` still copy the field verbatim, and CSV quoting already makes that safe.\n */\nexport function printable(text: string): string {\n // eslint-disable-next-line no-control-regex\n return text.replace(/[\\u0000-\\u001f\\u007f-\\u009f]/gu, (c) =>\n `\\\\x${c.codePointAt(0)!.toString(16).padStart(2, '0')}`,\n );\n}\n\n/**\n * The same protection for text that is meant to span lines.\n *\n * `printable` escapes newlines along with everything else, which is right for a channel\n * label — one has no business containing a line break, and it would break the `--info`\n * table's alignment. It is wrong for a whole message: several are written on two lines,\n * and Node's own option errors run to three. Escaping those turned the break into text:\n *\n * error: No channel named \"ECQ\". Did you mean \"ECG\"?\\x0aRun with --info to list ...\n *\n * Each line is escaped on its own, so nothing here gains the ability to drive a terminal.\n * A carriage return is still escaped, so no line can be repainted after it is printed —\n * which is the property that mattered. A newline can only add a line, never overwrite one.\n */\nexport function printableLines(text: string, indent = ''): string {\n return text\n .split('\\n')\n .map((line, index) => (index === 0 ? '' : indent) + printable(line))\n .join('\\n');\n}\n\n/** The `--info` view: what is in this recording, and what would converting it produce. */\nexport function formatInfo(file: EdfFile, plan: ConversionPlan): string {\n const { header } = file;\n const lines: string[] = [];\n\n lines.push(`File ${file.path}`);\n lines.push(`Format ${describeFormat(header)}`);\n lines.push(\n `Recorded ${\n formatWallClock(header.startDateTime)?.replace('T', ' ') ??\n `${printable(header.startDateRaw)} ${printable(header.startTimeRaw)} (unparseable)`\n }`,\n );\n lines.push(\n `Duration ${formatDuration(file.durationSeconds)} (${file.recordCount} records of ${header.recordDuration}s)`,\n );\n const elapsedSpan = plan.range.recordingEndSeconds - plan.range.recordingStartSeconds;\n if (Math.abs(elapsedSpan - file.durationSeconds) > 1e-9) {\n lines.push(`Time span ${formatDuration(elapsedSpan)} (includes discontinuities)`);\n }\n lines.push(`Size ${formatBytes(file.fileSize)}`);\n if (header.patientId) lines.push(`Patient ${printable(header.patientId)}`);\n if (header.recordingId) lines.push(`Recording ${printable(header.recordingId)}`);\n\n const signals = file.dataSignals;\n lines.push('');\n // The signal count was pluralised but the annotation-channel count was not, so a file\n // carrying two of them read \"2 annotation channel\". EDF+ permits more than one.\n const annotationCount = file.annotationSignals.length;\n const annotationPart =\n annotationCount > 0\n ? ` + ${annotationCount} annotation channel${annotationCount === 1 ? '' : 's'}`\n : '';\n lines.push(\n `Channels ${signals.length} signal${signals.length === 1 ? '' : 's'}${annotationPart}`,\n );\n lines.push('');\n\n const rows: string[][] = [['#', 'COLUMN', 'LABEL', 'UNIT', 'RATE', 'RANGE', 'OUTPUT']];\n const fileFor = new Map<number, string>();\n for (const group of plan.groups) {\n for (const channel of group.channels) fileFor.set(channel.signal.index, group.fileName);\n }\n // Rendered as a group so that two channels recorded at different rates never show the\n // same figure in the RATE column, which is the one thing this table is asked to settle.\n const rateText = formatRates(signals.map((signal) => signal.samplingRate));\n for (const [row, signal] of signals.entries()) {\n rows.push([\n String(signal.index),\n printable(plan.columnNames.get(signal.index) ?? ''),\n printable(signal.label),\n printable(signal.physicalDimension),\n `${rateText[row]} Hz`,\n `${signal.physicalMin} to ${signal.physicalMax}`,\n /*\n A channel with no samples was reported as \"(not selected)\", which is a different\n thing and not true when it was named on --channels. `edf2csv rec.edf --info\n --channels unused` said the channel the command asked for had not been chosen, when\n what is actually the case is that the file gives it nothing to convert. The\n NO_SAMPLES warning below the table says so; the table contradicted it.\n */\n fileFor.get(signal.index) ??\n (signal.samplesPerRecord === 0 ? '(no samples)' : '(not selected)'),\n ]);\n }\n lines.push(table(rows, new Set([0])));\n\n lines.push('');\n if (plan.groups.length > 1) {\n lines.push(\n plan.layout === 'long'\n ? `Sampling rates differ, and the long layout puts them in one table anyway: each row ` +\n `carries its own time, so nothing has to line up. No channel is resampled.`\n : `Sampling rates differ, so channels are written to ${plan.groups.length} files, one per rate. ` +\n `No channel is resampled.`,\n );\n }\n /*\n The estimate describes the signal tables, and says so when that is not what will be\n written.\n\n Under --annotations-only there are no signal tables, and the line read \"Would write 0\n rows, roughly 0 B.\" for a conversion that goes on to write annotations.csv with three\n events in it. --info exists to say what a conversion will do; asserting it will write\n nothing, when it will write a file, is the one thing it must not do.\n\n How many events there are cannot be answered from the header — the annotation channel has\n to be read record by record, which is the scan --info is for avoiding. So it says which\n file, and that the count is not knowable this cheaply, rather than inventing a zero.\n */\n /*\n No signal table to describe, whichever way that came about.\n\n This asked only whether `--annotations-only` had been given. A recording that has no\n signal channels — one holding nothing but EDF+ annotations — has none either, and fell\n through to the estimate line: \"Would write 0 rows, roughly 0 B.\" for a conversion that\n goes on to write an annotations.csv with events in it, beside channels.csv and\n metadata.json. That is the sentence 0.4.51 removed, arriving by the other route.\n */\n if (!plan.writeSignals || plan.groups.length === 0) {\n // Named as they will be written. --info is read to find out what a run leaves behind,\n // and a script that opens the name it was given must find a file there.\n const suffix = plan.gzip ? '.csv.gz' : '.csv';\n lines.push(\n file.annotationSignals.length > 0\n ? `Would write annotations${suffix} and channels${suffix}, and no signal data. How ` +\n 'many events there are cannot be told from the header.'\n : `Would write channels${suffix} and no signal data — and no annotations${suffix} ` +\n 'either, since this recording has no annotation channel.',\n );\n return lines.join('\\n');\n }\n\n // The estimate counts the characters of the CSV, which is what --gzip then compresses.\n // Reporting it as the size on disk would overstate a compressed conversion several-fold.\n const compressing = plan.gzip;\n lines.push(\n `Would write ${plan.estimate.rows.toLocaleString('en-US')} rows, roughly ` +\n `${formatBytes(plan.estimate.bytes)}${compressing ? ' before compression' : ''}.`,\n );\n\n return lines.join('\\n');\n}\n\n/**\n * The `--info` view as JSON, for surveying files from a script.\n *\n * `indent` is 2 for a single recording, matching what this has always printed, and null for\n * a batch — several pretty-printed documents run together are readable by a streaming parser\n * but not by anything that expects one record per line, and a batch is exactly where\n * line-oriented reading is wanted. null rather than undefined because a default parameter\n * takes effect when undefined is passed, which quietly restored the indentation this was\n * meant to drop; JSON.stringify itself wants undefined, so it is translated at the call.\n *\n * `--info` answers \"what is in this recording and what would converting it cost\", which\n * is exactly the question you want to ask across a directory of hundreds of recordings —\n * and the text table is the wrong shape for that. `--json` previously applied only to\n * conversions, so scripts had to parse the aligned columns or convert files just to learn\n * what was in them.\n *\n * Field names match `metadata.json` where the two describe the same thing, so a survey and\n * a conversion can be read by the same code.\n */\nexport function infoJson(file: EdfFile, plan: ConversionPlan, indent: number | null = 2): string {\n const { header } = file;\n const fileFor = new Map<number, string>();\n for (const group of plan.groups) {\n for (const channel of group.channels) fileFor.set(channel.signal.index, group.fileName);\n }\n\n return JSON.stringify(\n {\n path: file.path,\n bytes: file.fileSize,\n format: describeFormat(header),\n start_datetime_local: formatWallClock(header.startDateTime),\n start_date_raw: header.startDateRaw,\n start_time_raw: header.startTimeRaw,\n patient_id: header.patientId,\n recording_id: header.recordingId,\n data_records: file.recordCount,\n data_records_declared: header.declaredRecordCount,\n record_duration_seconds: header.recordDuration,\n duration_seconds: file.durationSeconds,\n // For a discontinuous file this exceeds duration_seconds by the length of the gaps.\n time_span_seconds: plan.range.recordingEndSeconds - plan.range.recordingStartSeconds,\n annotation_channels: file.annotationSignals.length,\n channels: file.dataSignals.map((signal) => ({\n signal_index: signal.index,\n column: plan.columnNames.get(signal.index) ?? '',\n label: signal.label,\n unit: signal.physicalDimension,\n sampling_rate_hz: signal.samplingRate,\n samples_per_record: signal.samplesPerRecord,\n physical_min: signal.physicalMin,\n physical_max: signal.physicalMax,\n digital_min: signal.digitalMin,\n digital_max: signal.digitalMax,\n transducer: signal.transducer,\n prefiltering: signal.prefiltering,\n output_file: fileFor.get(signal.index) ?? null,\n })),\n estimate: {\n rows: plan.estimate.rows,\n // Character count of the CSV. With --gzip the file on disk is smaller than this.\n bytes: plan.estimate.bytes,\n exceeds_spreadsheet_limit: plan.estimate.exceedsSpreadsheetLimit,\n },\n // The plan's mixed-rate warning replaces the header parser's, as it does everywhere\n // else. This was the one consumer left out of that when 0.3.2 made the warning follow\n // --channels, so `--info --json` carried it twice: once counting the rates being\n // converted and once counting every rate in the file, with the same code and severity.\n warnings: plan.diagnostics\n .concat(withoutFileRateWarning(file.diagnostics))\n .map((d) => ({ code: d.code, severity: d.severity, message: d.message })),\n },\n null,\n indent ?? undefined,\n );\n}\n\n/** One line per diagnostic, prefixed so warnings are greppable. */\nexport function formatDiagnostics(diagnostics: readonly Diagnostic[]): string {\n return diagnostics\n .map((d) => {\n // Diagnostics quote channel labels, which come from the file, so they need the\n // same treatment as the --info table.\n const head = `${d.severity === 'warning' ? 'warning' : 'note'}: ${printable(d.message)}`;\n return d.hint ? `${head}\\n ${printable(d.hint)}` : head;\n })\n .join('\\n');\n}\n\nexport function formatSummary(result: ConvertResult): string {\n const lines: string[] = [];\n const rows: string[][] = [];\n for (const file of result.files) {\n // `.csv.gz` is still a CSV, and its rows are still rows. The suffix test dropped the\n // unit from every line of a --gzip summary, so the numbers stood on their own.\n rows.push([\n ` ${file.name}`,\n file.rows.toLocaleString('en-US'),\n /\\.csv(\\.gz)?$/u.test(file.name) ? 'rows' : '',\n ]);\n }\n lines.push(`Wrote ${result.outputDir}`);\n lines.push(table(rows, new Set([1])));\n lines.push(`Done in ${(result.elapsedMs / 1000).toFixed(1)}s.`);\n return lines.join('\\n');\n}\n\nexport function summaryJson(result: ConvertResult, indent: number | null = 2): string {\n return JSON.stringify(\n {\n output_dir: result.outputDir,\n files: result.files,\n annotations: result.annotationCount,\n duration_seconds: result.file.durationSeconds,\n records: result.file.recordCount,\n elapsed_ms: result.elapsedMs,\n warnings: result.diagnostics.map((d) => ({ code: d.code, severity: d.severity, message: d.message })),\n },\n null,\n indent ?? undefined,\n );\n}\n"]}
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Human-readable output for the terminal.\n *\n * Everything here is plain text with no colour codes, so piping to a file or a log\n * produces exactly what appeared on screen.\n */\n\nimport type { Diagnostic } from '../edf/errors.js';\nimport type { EdfFile } from '../edf/reader.js';\nimport { describeFormat, formatRates, formatWallClock } from '../edf/header.js';\nimport { formatBytes, formatDuration } from '../format/number.js';\nimport type { ConversionPlan } from '../convert/plan.js';\nimport { withoutFileRateWarning } from '../convert/plan.js';\nimport type { ConvertResult } from '../convert/run.js';\n\nfunction table(rows: readonly (readonly string[])[], alignRight: ReadonlySet<number>): string {\n if (rows.length === 0) return '';\n const width: number[] = [];\n for (const row of rows) {\n row.forEach((cell, i) => {\n width[i] = Math.max(width[i] ?? 0, cell.length);\n });\n }\n return rows\n .map((row) =>\n row\n .map((cell, i) => {\n const w = width[i] ?? 0;\n return alignRight.has(i) ? cell.padStart(w) : cell.padEnd(w);\n })\n .join(' ')\n .trimEnd(),\n )\n .join('\\n');\n}\n\n/**\n * Make header text safe to print to a terminal.\n *\n * EDF identification fields and channel labels are free text copied verbatim out of the\n * file, and `--info` puts them straight on stdout. A header carrying ANSI escapes could\n * therefore drive the reader's terminal — `\\x1b[2J\\x1b[H` clears the screen and homes the\n * cursor, which is enough to hide the rest of the output or repaint it as something else.\n * Nobody writes an EDF header that way on purpose, which is exactly why a file that does\n * should not be trusted with the terminal.\n *\n * Control bytes are shown as their escape instead, so a corrupt field stays diagnosable\n * rather than being silently swallowed. This affects display only: `channels.csv` and\n * `metadata.json` still copy the field verbatim, and CSV quoting already makes that safe.\n */\nexport function printable(text: string): string {\n // eslint-disable-next-line no-control-regex\n return text.replace(/[\\u0000-\\u001f\\u007f-\\u009f]/gu, (c) =>\n `\\\\x${c.codePointAt(0)!.toString(16).padStart(2, '0')}`,\n );\n}\n\n/**\n * The same protection for text that is meant to span lines.\n *\n * `printable` escapes newlines along with everything else, which is right for a channel\n * label — one has no business containing a line break, and it would break the `--info`\n * table's alignment. It is wrong for a whole message: several are written on two lines,\n * and Node's own option errors run to three. Escaping those turned the break into text:\n *\n * error: No channel named \"ECQ\". Did you mean \"ECG\"?\\x0aRun with --info to list ...\n *\n * Each line is escaped on its own, so nothing here gains the ability to drive a terminal.\n * A carriage return is still escaped, so no line can be repainted after it is printed —\n * which is the property that mattered. A newline can only add a line, never overwrite one.\n */\nexport function printableLines(text: string, indent = ''): string {\n return text\n .split('\\n')\n .map((line, index) => (index === 0 ? '' : indent) + printable(line))\n .join('\\n');\n}\n\n/** The `--info` view: what is in this recording, and what would converting it produce. */\nexport function formatInfo(file: EdfFile, plan: ConversionPlan): string {\n const { header } = file;\n const lines: string[] = [];\n\n lines.push(`File ${file.path}`);\n lines.push(`Format ${describeFormat(header)}`);\n lines.push(\n `Recorded ${\n formatWallClock(header.startDateTime)?.replace('T', ' ') ??\n `${printable(header.startDateRaw)} ${printable(header.startTimeRaw)} (unparseable)`\n }`,\n );\n lines.push(\n `Duration ${formatDuration(file.durationSeconds)} (${file.recordCount} records of ${header.recordDuration}s)`,\n );\n const elapsedSpan = plan.range.recordingEndSeconds - plan.range.recordingStartSeconds;\n if (Math.abs(elapsedSpan - file.durationSeconds) > 1e-9) {\n lines.push(`Time span ${formatDuration(elapsedSpan)} (includes discontinuities)`);\n }\n /*\n Where the samples begin, when that is not zero.\n\n 0.4.9 made the first record's timekeeping TAL the point a recording is timed from, so a\n file whose TALs start at +1000 writes `time_s` from 1000.000 and takes `--start` and\n `--end` on that same clock. None of that appeared here: the report said \"Duration 3s\",\n which reads as 0 to 3, and `--start 0 --end 1` then selected nothing and answered with\n \"The window is inside the recording but lands where there is no data ... Run with --info\n to see where the records actually sit\" — pointing at this report, which was the one place\n the number was missing. It is in `plan.range` already and governs the estimate printed\n below; it was simply never shown.\n */\n const startsAt = plan.range.recordingStartSeconds;\n if (Number.isFinite(startsAt) && Math.abs(startsAt) > 1e-9) {\n // In seconds rather than through formatDuration, because this number is meant to be\n // typed back in: `--start` takes `1000s`, and \"16m 40s\" is not something it accepts.\n // It is also how the empty-window warning renders the window it was given.\n lines.push(\n `Timed from ${startsAt.toFixed(3)}s (first sample; --start and --end use this clock)`,\n );\n }\n lines.push(`Size ${formatBytes(file.fileSize)}`);\n if (header.patientId) lines.push(`Patient ${printable(header.patientId)}`);\n if (header.recordingId) lines.push(`Recording ${printable(header.recordingId)}`);\n\n const signals = file.dataSignals;\n lines.push('');\n // The signal count was pluralised but the annotation-channel count was not, so a file\n // carrying two of them read \"2 annotation channel\". EDF+ permits more than one.\n const annotationCount = file.annotationSignals.length;\n const annotationPart =\n annotationCount > 0\n ? ` + ${annotationCount} annotation channel${annotationCount === 1 ? '' : 's'}`\n : '';\n lines.push(\n `Channels ${signals.length} signal${signals.length === 1 ? '' : 's'}${annotationPart}`,\n );\n lines.push('');\n\n const rows: string[][] = [['#', 'COLUMN', 'LABEL', 'UNIT', 'RATE', 'RANGE', 'OUTPUT']];\n const fileFor = new Map<number, string>();\n for (const group of plan.groups) {\n for (const channel of group.channels) fileFor.set(channel.signal.index, group.fileName);\n }\n // Rendered as a group so that two channels recorded at different rates never show the\n // same figure in the RATE column, which is the one thing this table is asked to settle.\n const rateText = formatRates(signals.map((signal) => signal.samplingRate));\n for (const [row, signal] of signals.entries()) {\n rows.push([\n String(signal.index),\n printable(plan.columnNames.get(signal.index) ?? ''),\n printable(signal.label),\n printable(signal.physicalDimension),\n `${rateText[row]} Hz`,\n `${signal.physicalMin} to ${signal.physicalMax}`,\n /*\n A channel with no samples was reported as \"(not selected)\", which is a different\n thing and not true when it was named on --channels. `edf2csv rec.edf --info\n --channels unused` said the channel the command asked for had not been chosen, when\n what is actually the case is that the file gives it nothing to convert. The\n NO_SAMPLES warning below the table says so; the table contradicted it.\n */\n fileFor.get(signal.index) ??\n (signal.samplesPerRecord === 0 ? '(no samples)' : '(not selected)'),\n ]);\n }\n lines.push(table(rows, new Set([0])));\n\n lines.push('');\n if (plan.groups.length > 1) {\n lines.push(\n plan.layout === 'long'\n ? `Sampling rates differ, and the long layout puts them in one table anyway: each row ` +\n `carries its own time, so nothing has to line up. No channel is resampled.`\n : `Sampling rates differ, so channels are written to ${plan.groups.length} files, one per rate. ` +\n `No channel is resampled.`,\n );\n }\n /*\n The estimate describes the signal tables, and says so when that is not what will be\n written.\n\n Under --annotations-only there are no signal tables, and the line read \"Would write 0\n rows, roughly 0 B.\" for a conversion that goes on to write annotations.csv with three\n events in it. --info exists to say what a conversion will do; asserting it will write\n nothing, when it will write a file, is the one thing it must not do.\n\n How many events there are cannot be answered from the header — the annotation channel has\n to be read record by record, which is the scan --info is for avoiding. So it says which\n file, and that the count is not knowable this cheaply, rather than inventing a zero.\n */\n /*\n No signal table to describe, whichever way that came about.\n\n This asked only whether `--annotations-only` had been given. A recording that has no\n signal channels — one holding nothing but EDF+ annotations — has none either, and fell\n through to the estimate line: \"Would write 0 rows, roughly 0 B.\" for a conversion that\n goes on to write an annotations.csv with events in it, beside channels.csv and\n metadata.json. That is the sentence 0.4.51 removed, arriving by the other route.\n */\n if (!plan.writeSignals || plan.groups.length === 0) {\n // Named as they will be written. --info is read to find out what a run leaves behind,\n // and a script that opens the name it was given must find a file there.\n const suffix = plan.gzip ? '.csv.gz' : '.csv';\n lines.push(\n file.annotationSignals.length > 0\n ? `Would write annotations${suffix} and channels${suffix}, and no signal data. How ` +\n 'many events there are cannot be told from the header.'\n : `Would write channels${suffix} and no signal data — and no annotations${suffix} ` +\n 'either, since this recording has no annotation channel.',\n );\n return lines.join('\\n');\n }\n\n // The estimate counts the characters of the CSV, which is what --gzip then compresses.\n // Reporting it as the size on disk would overstate a compressed conversion several-fold.\n const compressing = plan.gzip;\n lines.push(\n `Would write ${plan.estimate.rows.toLocaleString('en-US')} rows, roughly ` +\n `${formatBytes(plan.estimate.bytes)}${compressing ? ' before compression' : ''}.`,\n );\n\n return lines.join('\\n');\n}\n\n/**\n * The `--info` view as JSON, for surveying files from a script.\n *\n * `indent` is 2 for a single recording, matching what this has always printed, and null for\n * a batch — several pretty-printed documents run together are readable by a streaming parser\n * but not by anything that expects one record per line, and a batch is exactly where\n * line-oriented reading is wanted. null rather than undefined because a default parameter\n * takes effect when undefined is passed, which quietly restored the indentation this was\n * meant to drop; JSON.stringify itself wants undefined, so it is translated at the call.\n *\n * `--info` answers \"what is in this recording and what would converting it cost\", which\n * is exactly the question you want to ask across a directory of hundreds of recordings —\n * and the text table is the wrong shape for that. `--json` previously applied only to\n * conversions, so scripts had to parse the aligned columns or convert files just to learn\n * what was in them.\n *\n * Field names match `metadata.json` where the two describe the same thing, so a survey and\n * a conversion can be read by the same code.\n */\nexport function infoJson(file: EdfFile, plan: ConversionPlan, indent: number | null = 2): string {\n const { header } = file;\n const fileFor = new Map<number, string>();\n for (const group of plan.groups) {\n for (const channel of group.channels) fileFor.set(channel.signal.index, group.fileName);\n }\n\n return JSON.stringify(\n {\n path: file.path,\n bytes: file.fileSize,\n format: describeFormat(header),\n start_datetime_local: formatWallClock(header.startDateTime),\n start_date_raw: header.startDateRaw,\n start_time_raw: header.startTimeRaw,\n patient_id: header.patientId,\n recording_id: header.recordingId,\n data_records: file.recordCount,\n data_records_declared: header.declaredRecordCount,\n record_duration_seconds: header.recordDuration,\n duration_seconds: file.durationSeconds,\n // For a discontinuous file this exceeds duration_seconds by the length of the gaps.\n time_span_seconds: plan.range.recordingEndSeconds - plan.range.recordingStartSeconds,\n // Where `time_s` begins, and the clock `--start` and `--end` are read against. Usually\n // zero; not when the first record's timekeeping TAL puts the recording elsewhere. Both\n // of the fields above are lengths and neither says where that length sits.\n first_sample_seconds: plan.range.recordingStartSeconds,\n annotation_channels: file.annotationSignals.length,\n channels: file.dataSignals.map((signal) => ({\n signal_index: signal.index,\n column: plan.columnNames.get(signal.index) ?? '',\n label: signal.label,\n unit: signal.physicalDimension,\n sampling_rate_hz: signal.samplingRate,\n samples_per_record: signal.samplesPerRecord,\n physical_min: signal.physicalMin,\n physical_max: signal.physicalMax,\n digital_min: signal.digitalMin,\n digital_max: signal.digitalMax,\n transducer: signal.transducer,\n prefiltering: signal.prefiltering,\n output_file: fileFor.get(signal.index) ?? null,\n })),\n estimate: {\n rows: plan.estimate.rows,\n // Character count of the CSV. With --gzip the file on disk is smaller than this.\n bytes: plan.estimate.bytes,\n exceeds_spreadsheet_limit: plan.estimate.exceedsSpreadsheetLimit,\n },\n // The plan's mixed-rate warning replaces the header parser's, as it does everywhere\n // else. This was the one consumer left out of that when 0.3.2 made the warning follow\n // --channels, so `--info --json` carried it twice: once counting the rates being\n // converted and once counting every rate in the file, with the same code and severity.\n warnings: plan.diagnostics\n .concat(withoutFileRateWarning(file.diagnostics))\n .map((d) => ({ code: d.code, severity: d.severity, message: d.message })),\n },\n null,\n indent ?? undefined,\n );\n}\n\n/** One line per diagnostic, prefixed so warnings are greppable. */\nexport function formatDiagnostics(diagnostics: readonly Diagnostic[]): string {\n return diagnostics\n .map((d) => {\n // Diagnostics quote channel labels, which come from the file, so they need the\n // same treatment as the --info table.\n const head = `${d.severity === 'warning' ? 'warning' : 'note'}: ${printable(d.message)}`;\n return d.hint ? `${head}\\n ${printable(d.hint)}` : head;\n })\n .join('\\n');\n}\n\nexport function formatSummary(result: ConvertResult): string {\n const lines: string[] = [];\n const rows: string[][] = [];\n for (const file of result.files) {\n // `.csv.gz` is still a CSV, and its rows are still rows. The suffix test dropped the\n // unit from every line of a --gzip summary, so the numbers stood on their own.\n rows.push([\n ` ${file.name}`,\n file.rows.toLocaleString('en-US'),\n /\\.csv(\\.gz)?$/u.test(file.name) ? 'rows' : '',\n ]);\n }\n lines.push(`Wrote ${result.outputDir}`);\n lines.push(table(rows, new Set([1])));\n lines.push(`Done in ${(result.elapsedMs / 1000).toFixed(1)}s.`);\n return lines.join('\\n');\n}\n\nexport function summaryJson(result: ConvertResult, indent: number | null = 2): string {\n return JSON.stringify(\n {\n output_dir: result.outputDir,\n files: result.files,\n annotations: result.annotationCount,\n duration_seconds: result.file.durationSeconds,\n records: result.file.recordCount,\n elapsed_ms: result.elapsedMs,\n warnings: result.diagnostics.map((d) => ({ code: d.code, severity: d.severity, message: d.message })),\n },\n null,\n indent ?? undefined,\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edf2csv",
3
- "version": "0.5.58",
3
+ "version": "0.5.59",
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",