edf2csv 0.6.125 → 0.6.127

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.
@@ -8,6 +8,7 @@ import { describeFormat, formatRates, formatWallClock } from '../edf/header.js';
8
8
  import { formatBytes, formatDuration } from '../format/number.js';
9
9
  import { counted } from '../format/list.js';
10
10
  import { withoutFileRateWarning } from '../convert/plan.js';
11
+ import { VERSION } from '../version.js';
11
12
  function table(rows, alignRight) {
12
13
  if (rows.length === 0)
13
14
  return '';
@@ -232,6 +233,7 @@ export function infoJson(file, plan, indent = 2) {
232
233
  fileFor.set(channel.signal.index, group.fileName);
233
234
  }
234
235
  return JSON.stringify({
236
+ tool: TOOL,
235
237
  path: file.path,
236
238
  bytes: file.fileSize,
237
239
  format: describeFormat(header),
@@ -326,8 +328,19 @@ export function formatSummary(result) {
326
328
  lines.push(`Done in ${(result.elapsedMs / 1000).toFixed(1)}s.`);
327
329
  return lines.join('\n');
328
330
  }
331
+ /**
332
+ * Which version produced this record.
333
+ *
334
+ * `metadata.json` has carried it since the file existed, because a conversion should be
335
+ * reproducible later. The two JSON *streams* did not, and they are the ones most likely to
336
+ * outlive the run: `--json` exists to be piped into something, logged, or committed beside a
337
+ * result, where the question a year on is which release's field names and rounding these are.
338
+ * The same shape as metadata.json's, so a consumer reads one field either way.
339
+ */
340
+ const TOOL = { name: 'edf2csv', version: VERSION };
329
341
  export function summaryJson(result, indent = 2) {
330
342
  return JSON.stringify({
343
+ tool: TOOL,
331
344
  output_dir: result.outputDir,
332
345
  files: result.files,
333
346
  annotations: result.annotationCount,
@@ -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 { counted } from '../format/list.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 // The control characters are what this function is for, not an oversight in it.\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 /*\n Escaped, like every other value that came out of the filesystem.\n\n A path is untrusted text: a folder may be named with an ESC byte, and a file name may\n hold a newline on every platform this runs on. The `[n/m]` header a batch prints has\n always escaped it and these two lines did not, so one line of a run reached the terminal\n as `study/esc\\x1b[31mred.edf` and the next as a live colour change — and a name holding a\n newline split `Wrote` across two lines, so the summary reported a path that reads as two.\n NONPRINTABLE_LABEL exists because a header field can carry these bytes; a directory entry\n can carry them just as easily.\n */\n lines.push(`File ${printable(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)} (${counted(file.recordCount, 'record')} 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 ${counted(plan.groups.length, 'file')}, 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 // A window narrow enough to select one sample is an ordinary thing to ask for, and this\n // read \"Would write 1 rows, roughly 22 B.\" — the slip 0.5.74 fixed on the lines above it\n // and missed here, because the recording that test builds never estimates exactly one.\n `Would write ${plan.estimate.rows.toLocaleString('en-US')} ` +\n `${plan.estimate.rows === 1 ? 'row' : '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 /*\n Null rather than zero when the run writes no signal table.\n\n The text form has refused to say \"Would write 0 rows, roughly 0 B.\" since 0.4.51,\n because a run that goes on to write an annotations.csv with events in it has not\n written nothing — and it is `--annotations-only`, or a recording holding only\n annotations, that reaches this. The JSON went on saying it to the surface a script\n reads. There is no estimate for a table that does not exist, and null is how this\n document already says that.\n */\n estimate:\n plan.writeSignals && plan.groups.length > 0\n ? {\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 : { rows: null, bytes: null, exceeds_spreadsheet_limit: false },\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 // The file's own first, then the plan's, which is the order the text form prints them\n // in. Concatenating the other way round listed the same warnings about the same\n // recording in two different sequences depending on which form you asked for.\n warnings: withoutFileRateWarning(file.diagnostics)\n .concat(plan.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 // Singular at one, like every other count this prints: a one-row table is what a\n // narrow window produces, and \"1 rows\" is the same slip 0.5.74 fixed elsewhere.\n /\\.csv(\\.gz)?$/u.test(file.name) ? (file.rows === 1 ? 'row' : 'rows') : '',\n ]);\n }\n lines.push(`Wrote ${printable(result.outputDir)}`); // Escaped; see the File line above.\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 { counted } from '../format/list.js';\nimport type { ConversionPlan } from '../convert/plan.js';\nimport { withoutFileRateWarning } from '../convert/plan.js';\nimport type { ConvertResult } from '../convert/run.js';\nimport { VERSION } from '../version.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 // The control characters are what this function is for, not an oversight in it.\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 /*\n Escaped, like every other value that came out of the filesystem.\n\n A path is untrusted text: a folder may be named with an ESC byte, and a file name may\n hold a newline on every platform this runs on. The `[n/m]` header a batch prints has\n always escaped it and these two lines did not, so one line of a run reached the terminal\n as `study/esc\\x1b[31mred.edf` and the next as a live colour change — and a name holding a\n newline split `Wrote` across two lines, so the summary reported a path that reads as two.\n NONPRINTABLE_LABEL exists because a header field can carry these bytes; a directory entry\n can carry them just as easily.\n */\n lines.push(`File ${printable(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)} (${counted(file.recordCount, 'record')} 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 ${counted(plan.groups.length, 'file')}, 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 // A window narrow enough to select one sample is an ordinary thing to ask for, and this\n // read \"Would write 1 rows, roughly 22 B.\" — the slip 0.5.74 fixed on the lines above it\n // and missed here, because the recording that test builds never estimates exactly one.\n `Would write ${plan.estimate.rows.toLocaleString('en-US')} ` +\n `${plan.estimate.rows === 1 ? 'row' : '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 tool: TOOL,\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 /*\n Null rather than zero when the run writes no signal table.\n\n The text form has refused to say \"Would write 0 rows, roughly 0 B.\" since 0.4.51,\n because a run that goes on to write an annotations.csv with events in it has not\n written nothing — and it is `--annotations-only`, or a recording holding only\n annotations, that reaches this. The JSON went on saying it to the surface a script\n reads. There is no estimate for a table that does not exist, and null is how this\n document already says that.\n */\n estimate:\n plan.writeSignals && plan.groups.length > 0\n ? {\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 : { rows: null, bytes: null, exceeds_spreadsheet_limit: false },\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 // The file's own first, then the plan's, which is the order the text form prints them\n // in. Concatenating the other way round listed the same warnings about the same\n // recording in two different sequences depending on which form you asked for.\n warnings: withoutFileRateWarning(file.diagnostics)\n .concat(plan.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 // Singular at one, like every other count this prints: a one-row table is what a\n // narrow window produces, and \"1 rows\" is the same slip 0.5.74 fixed elsewhere.\n /\\.csv(\\.gz)?$/u.test(file.name) ? (file.rows === 1 ? 'row' : 'rows') : '',\n ]);\n }\n lines.push(`Wrote ${printable(result.outputDir)}`); // Escaped; see the File line above.\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\n/**\n * Which version produced this record.\n *\n * `metadata.json` has carried it since the file existed, because a conversion should be\n * reproducible later. The two JSON *streams* did not, and they are the ones most likely to\n * outlive the run: `--json` exists to be piped into something, logged, or committed beside a\n * result, where the question a year on is which release's field names and rounding these are.\n * The same shape as metadata.json's, so a consumer reads one field either way.\n */\nconst TOOL = { name: 'edf2csv', version: VERSION } as const;\n\nexport function summaryJson(result: ConvertResult, indent: number | null = 2): string {\n return JSON.stringify(\n {\n tool: TOOL,\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.6.125",
3
+ "version": "0.6.127",
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",