edf2csv 0.9.34 → 0.9.35

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.
@@ -498,15 +498,41 @@ stdoutRefused = false) {
498
498
  // Named as they will be written. --info is read to find out what a run leaves behind,
499
499
  // and a script that opens the name it was given must find a file there.
500
500
  const suffix = plan.gzip ? '.csv.gz' : '.csv';
501
- lines.push(wrap(file.annotationSignals.length > 0
501
+ const files = file.annotationSignals.length > 0
502
502
  ? events === null
503
- ? `Would write annotations${suffix} and channels${suffix}, and no signal data. How ` +
504
- 'many events there are cannot be told from the header, and finding out means ' +
505
- 'reading the annotation channel record by record.'
506
- : `Would write annotations${suffix} with ${counted(events, 'event')} and ` +
507
- `channels${suffix}, and no signal data.`
508
- : `Would write channels${suffix} and no signal data and no annotations${suffix} ` +
509
- 'either, since this recording has no annotation channel.'));
503
+ ? `annotations${suffix} and channels${suffix}, and no signal data`
504
+ : `annotations${suffix} with ${counted(events, 'event')} and ` +
505
+ `channels${suffix}, and no signal data`
506
+ : `channels${suffix} and no signal data — and no annotations${suffix} ` +
507
+ 'either, since this recording has no annotation channel';
508
+ // The one form of this line whose count is not in hand; see the `events` parameter.
509
+ const unknown = file.annotationSignals.length > 0 && events === null
510
+ ? ' How many events there are cannot be told from the header, and finding out means ' +
511
+ 'reading the annotation channel record by record.'
512
+ : '';
513
+ /*
514
+ And the other half of what 0.9.34 corrected one branch down.
515
+
516
+ Both of the refusals that land here are refusals: `--stdout` has nothing to stream for
517
+ `--annotations-only`, and nothing for a recording with no signal channels. So this line
518
+ named two files for a run that writes none, three lines above the warning saying the
519
+ run will not happen and one of them above a hint offering the same two files as the
520
+ thing to convert to a directory for:
521
+
522
+ Would write annotations.csv and channels.csv, and no signal data.
523
+
524
+ warning: --stdout would refuse this run: has no signal data to write: this
525
+ recording has no signal channels, only EDF+ annotations.
526
+ Convert to a directory to get its annotations.csv, or drop --stdout.
527
+
528
+ Named as the conversion that does write them, which is the one the hint sends you to.
529
+ */
530
+ lines.push(wrap(stdoutRefused
531
+ ? // Refusal first, since two of the three forms end in a subordinate clause that a
532
+ // trailing one would have to be read past.
533
+ `--stdout writes nothing here; converting into a directory would write ` +
534
+ `${files}.${unknown}`
535
+ : `Would write ${files}.${unknown}`));
510
536
  return lines.join('\n');
511
537
  }
512
538
  // The estimate counts the characters of the CSV, which is what --gzip then compresses.
@@ -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 { fixed, formatBytes, formatDuration, plainSeconds } from '../format/number.js';\nimport { counted, grouped } from '../format/list.js';\nimport { escapeJsonText, printable } from '../format/unprintable.js';\nimport type { ConversionPlan } from '../convert/plan.js';\nimport { outputCsvName } from '../convert/plan.js';\nimport type { ConvertResult } from '../convert/run.js';\nimport { VERSION } from '../version.js';\n\n/** Where terminal prose wraps. The width --help is written to, and the ANSI default. */\nconst WRAP_COLUMNS = 80;\n\n/** The continuation indent under a `warning: ` prefix, which is the only one there is. */\nconst HINT_INDENT = ' '.repeat(9);\n\n/**\n * One unbreakable unit of a wrapped line: a quoted span, or a run of non-space.\n *\n * The lookbehind is what keeps an apostrophe out of it. `the file's own header and it's` has\n * two of them, and without the guard `'s own header and it'` is one quoted span — a token\n * with spaces in it that then refuses to break. A quote opening a span is at the start of a\n * word, so that is what is required of it.\n */\nconst WORD = /(?<=^|[\\s(])(?:\"[^\"]*\"|'[^']*')|\\S+/gu;\n\n/**\n * Greedy word wrap, `indent` on every line including the first.\n *\n * Only free prose goes through this. The aligned parts of `--info` — the `Format`/`Size`\n * key-value lines and the channel table — are laid out in columns, and re-flowing a column\n * is how you turn a table into a paragraph.\n *\n * A word wider than the column is left to overrun rather than broken. The long words here\n * are file paths and quoted channel labels, and neither survives being split across lines:\n * the point of printing a path is that it can be copied back out.\n *\n * A quoted span is one word for the same reason, however many spaces are inside it. The\n * sentence above is the promise, and it held only for the paths that have no spaces in them:\n * an interrupted conversion said `Files already written to \"/tmp/a very long destination\n * folder name with` / `many spaces here indeed\" are incomplete`, splitting the one thing on\n * the line that has to be copied whole — and quoting is what a path with spaces in it is\n * given in the first place. A word wider than the column already overruns rather than\n * breaking; this is that rule reaching the words it was written for.\n *\n * Which is also why the gap between two words is reproduced rather than normalised to one\n * space. Splitting on `\\s+` and rejoining with `' '` re-flowed everything, and a quoted value\n * is not prose: a channel labelled `EEG A` was offered back as `Did you mean \"EEG A\"?`, and\n * a destination given as `-my nightly` as `Write it as one argument instead:\n * '--out=-my nightly'`. Following either gets a different channel or a different directory\n * than the one the sentence is about, and the run before it had already quoted the name\n * correctly on the line above.\n */\nexport function wrap(text: string, indent = '', width = WRAP_COLUMNS): string {\n const lines: string[] = [];\n let line = indent;\n let at = 0;\n for (const match of text.matchAll(WORD)) {\n const piece = match[0];\n const gap = text.slice(at, match.index);\n at = match.index + piece.length;\n if (line === indent) line += piece;\n else if (line.length + gap.length + piece.length <= width) line += gap + piece;\n else {\n lines.push(line);\n line = indent + piece;\n }\n }\n if (line !== indent) lines.push(line);\n return lines.join('\\n');\n}\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 */\n// Defined in src/format/unprintable.ts, beside the character class it uses, since the library's\n// own message builders need it too — see 0.8.92. Re-exported here, which is where every caller\n// in this package already imports it from.\nexport { printable };\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) => {\n const safe = printable(line);\n // The first line is the one that follows `error: ` or `warning: `, so it is the line\n // a log gets grepped for, and it stays whole at whatever width it runs to.\n if (index === 0) return safe;\n\n /*\n Callers write the continuation indent one of two ways: most pass it here and leave\n their message lines flush, but the two usage builders bake ` ` into the string\n itself because they are printed without an `error: ` prefix in front. Reading the\n line's own leading space when none was passed keeps both working, and keeps the\n wrap aligned under the same column either way.\n */\n const body = safe.trimStart();\n const pad = indent === '' ? safe.slice(0, safe.length - body.length) : indent;\n\n /*\n A continuation that is a command is copied, not read.\n\n `edf2csv -- \"--chanels\"` is the line the unknown-option error ends on, and it exists\n to be pasted back into the shell. Wrapping puts `edf2csv --` on one line and the\n flag on the next, and what gets pasted is half a command. Prose survives being\n re-flowed and a command does not, so the two are told apart rather than being\n treated alike and hoping today's strings stay short.\n */\n if (body.startsWith('edf2csv ')) return pad + body;\n return wrap(body, pad);\n })\n .join('\\n');\n}\n\n/** The `--info` view: what is in this recording, and what would converting it produce. */\nexport function formatInfo(\n file: EdfFile,\n plan: ConversionPlan,\n /**\n * How many events a conversion would write, when that is already known.\n *\n * Null when it is not. `--info` reads the whole annotation channel of a discontinuous file,\n * because that is where its record times are; a continuous one it reads only far enough to\n * find the origin. So the count is in hand for one of the two, and the line below said \"How\n * many events there are cannot be told from the header\" for both — true of the header, and\n * beside the point on a file whose events had just been read and counted.\n */\n events: number | null = null,\n /**\n * Whether the run would stream to stdout, in which case no file is written at all.\n *\n * The OUTPUT column is \"Named as they will be written. --info is read to find out what a\n * run leaves behind, and a script that opens the name it was given must find a file there\"\n * — and under `--stdout` it named `signals.csv`, or `signals.csv.gz`, for a run that\n * creates no directory and no file. The same table already tells a channel that will not\n * be converted from one the file gives nothing to convert; this is the third thing it\n * could not say.\n */\n toStdout = false,\n /**\n * Whether `--stdout` would refuse the run this describes, in which case it writes nothing.\n *\n * The paragraph above the estimate already says so for the commonest of the three\n * refusals — \"this recording makes 3 tables, one per rate — more than `--stdout` can\n * write\" — and the line under it went on predicting the rows anyway:\n *\n * Sampling rates differ, so this recording makes 3 tables, one per rate — more\n * than --stdout can write. Converting into a directory writes one file each;\n * --layout long puts them all in one table.\n * Would write 14 rows, roughly 262 B.\n *\n * warning: --stdout would refuse this run: needs exactly one table, ...\n *\n * Three statements about one command, and the middle one is of a run that exits 1 having\n * written nothing. The estimate is still the answer to a question worth asking — it is\n * what converting into a directory would write — so it is kept and its subject corrected,\n * which is the same repair `--annotations-only` got on this line at 0.4.51.\n *\n * Taken from the caller rather than worked out here: `stdoutRefusal` is the conversion's\n * own guard, and the warning below the report is already built from it, so there is one\n * answer rather than two that can drift.\n */\n stdoutRefused = false,\n): 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 /*\n The two raw fields, with edges.\n\n Echoed bare, they ran into each other and into the parenthetical, and a header field is\n free to be empty or to be nothing but padding — `trimField` takes the padding off, so\n both of those arrive here as \"\". A file whose date field is blank printed\n\n Recorded 22.15.00 (unparseable)\n\n where the one value on the line is the *time* and nothing says so, and a file with both\n blank printed `Recorded (unparseable)`: a label, a gap, and a verdict about nothing\n visible. ` . . ` in both fields came out `. . . .`, which is four fields or two\n depending on how the reader counts.\n\n Quoted, which is what `START_TIME_UNREADABLE` — the warning printed under this very line,\n about these very fields — has always done: `(\"\" and \"\")`. Same argument 0.7.x made for the\n time-range refusals, in its words: without them \"the value ran into the sentence ... and\n the surrounding spaces — the actual reason a shell-built argument went wrong — are\n invisible\". `--info --json` has the two as separate strings and never had the problem.\n */\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 ${plainSeconds(header.recordDuration)}s)`,\n );\n const elapsedSpan = plan.range.recordingEndSeconds - plan.range.recordingStartSeconds;\n if (Math.abs(elapsedSpan - file.durationSeconds) > 1e-9) {\n /*\n Which way the two differ decides what to call it, and the parenthetical used to say\n \"includes discontinuities\" both ways round.\n\n A span LONGER than the duration is the gap case this line was written for: 3 records of\n 1s covering 11 seconds. A span SHORTER than the duration cannot be a gap — it is records\n that overlap, which an EDF+D file gets when a device re-sends a buffer. Three records of\n 1s starting at 0, 0.5 and 1 print:\n\n Duration 3s (3 records of 1s)\n Time span 2s (includes discontinuities)\n\n A recording covering less time than its own records account for, blamed on gaps it does\n not have — while the warning below it says, correctly, that two records overlap.\n\n A file holding both is described by whichever wins the subtraction, and the overlap\n warning is printed either way.\n */\n const overlapping = elapsedSpan < file.durationSeconds;\n lines.push(\n `Time span ${formatDuration(elapsedSpan)} ` +\n `(${overlapping ? 'records overlap in time' : 'includes discontinuities'})`,\n );\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 //\n // Through `fixed` rather than `toFixed`, which switches to exponent notation at 1e21 —\n // and `--start 1e+21s` is refused by the time parser with \"uses an unknown unit \\\"e\\\"\",\n // so the one line that says which clock to use handed back a number that clock rejects.\n // Reachable from a conforming file: an EDF+ onset is plain digits of any length, and a\n // record duration large enough to keep samples apart at that magnitude is four\n // characters. `fixed` expands these with BigInt, which is exact past 2^53 where a double\n // carries no fraction anyway, and is byte-for-byte `toFixed` everywhere else.\n lines.push(\n `Timed from ${fixed(startsAt, 3)}s (first sample; --start and --end use this clock)`,\n );\n }\n /*\n And the window, when one was asked for, which this report did not mention at all.\n\n `--info` is the mode whose purpose is to say what a conversion will do, and every other\n flag that changes what gets written is visible in it: `--channels` puts `(not selected)`\n in the OUTPUT column, `--gzip` changes the names there, `--layout long` changes the\n sentence under the table, `--annotations-only` replaces the estimate outright. `--start`\n and `--end` changed one number:\n\n $ edf2csv rec.edf --info $ edf2csv rec.edf --info --start 1s --end 2s\n Duration 3s (3 records of 1s) Duration 3s (3 records of 1s)\n ... ...\n Would write 1,155 rows, roughly 22.2 KB. Would write 385 rows, roughly 7.4 KB.\n\n Byte-for-byte identical otherwise — and the Duration line, which is what a reader checks a\n window against, goes on describing the whole file above an estimate that describes a third\n of it. The window is in `plan.range` already, which is the same thing `Timed from` above\n was added for: it governs the estimate printed below and was simply never shown.\n\n In seconds rather than through `formatDuration`, for the reason that line gives: these are\n numbers meant to be typed back into `--start` and `--end`, and \"16m 40s\" is not one.\n */\n const { range } = plan;\n /*\n An end that a double cannot hold is still an end.\n\n `endSeconds` is `recordCount * recordDuration` when no `--end` was given, and a header may\n state a record duration of 1e308 in five characters — two of those records overflow, so\n the figure comes back `Infinity`. This line required it to be finite and so vanished\n altogether on exactly the recording whose estimate is hardest to account for: the whole\n point of the line is to say why the row count changed, and `--start 1s` on such a file\n changed it from 8 rows to 7 with nothing on screen to say a window had been asked for.\n\n The window is still perfectly ordinary from this side — it begins where it was told to and\n runs to the end of the recording, which is what the line says now.\n */\n const endsAtInfinity = !Number.isFinite(range.endSeconds);\n if (!range.isWholeRecording && Number.isFinite(range.startSeconds)) {\n /*\n What the window selects, which is not data records in every mode.\n\n `--annotations-only` reads the annotation channel and converts no data records at all —\n the OUTPUT column of the table below says `(no signal data)` for every channel, and the\n line under it says \"and no signal data\". This counted them anyway:\n\n Window 1.000s to 3.000s (2 of 3 data records)\n ...\n Would write annotations.csv and channels.csv, and no signal data.\n\n The window is not idle in that mode: it decides which events are exported, by onset. So\n the range stays and the parenthetical says what it is doing.\n */\n const selects = plan.writeSignals\n ? `${grouped(range.endRecord - range.startRecord)} of ${counted(file.recordCount, 'data record')}`\n : 'events outside it are not exported';\n const to = endsAtInfinity ? 'the end' : `${fixed(range.endSeconds, 3)}s`;\n lines.push(`Window ${fixed(range.startSeconds, 3)}s to ${to} (${selects})`);\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 /*\n Both counts through `counted`, which this function already uses one line up for the record\n count and which exists so a count and its noun cannot disagree.\n\n The annotation-channel half was pluralised by hand when it was fixed — a file carrying two\n of them read \"2 annotation channel\", and EDF+ permits more than one — and the signal half\n had been hand-rolled since it was written. Two spellings of one rule in one line, in the\n file that imports the rule.\n */\n const annotationCount = file.annotationSignals.length;\n const annotationPart =\n annotationCount > 0 ? ` + ${counted(annotationCount, 'annotation channel')}` : '';\n lines.push(`Channels ${counted(signals.length, 'signal')}${annotationPart}`);\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 /*\n And under --annotations-only nothing was deselected either — the run writes no signal\n table at all, which is the same distinction one paragraph up.\n\n `--info --annotations-only --channels \"EEG Fpz-Cz\"` accepted the name, checked it\n against the file, and then printed \"(not selected)\" against the very channel it had\n just been asked for. The sentence three lines under the table already says the truth,\n \"and no signal data\", and `channels.csv` and `--info --json` both say it too — the\n cell is empty and `output_file` is null. The human table was the one place claiming a\n choice nobody made.\n */\n plan.writeSignals\n ? fileFor.has(signal.index)\n ? toStdout\n ? '(stdout)'\n : (fileFor.get(signal.index) as string)\n : signal.samplesPerRecord === 0\n ? '(no samples)'\n : '(not selected)'\n : '(no signal data)',\n ]);\n }\n /*\n A column head over nothing is not a table.\n\n The line above has just said `Channels 0 signals + 1 annotation channel`, and this then\n printed\n\n # COLUMN LABEL UNIT RATE RANGE OUTPUT\n\n with no row under it — seven headings for a set of no channels, on a recording holding\n only events, which is what `annotations-only.edf` in this repository is and what a\n companion event file out of a scoring system is. The same sentence `--channels \"#0\"` was\n given its own wording for: \"No channel at position #0. This file has signal channels at\n .\" states there are some and then names none.\n\n Nothing goes in its place. The count is stated one line up and the body three lines down\n says what the conversion would write; a table saying \"none\" twice more adds nothing.\n */\n if (signals.length > 0) {\n lines.push(table(rows, new Set([0])));\n lines.push('');\n }\n /*\n From here down --info stops laying out columns and starts explaining itself, so from\n here down it wraps. The key-value lines and the channel table above are aligned to each\n other and must not be re-flowed; these are sentences, and the longest of them ran to 156\n columns — which is not a line anyone reads, it is a line a terminal breaks somewhere.\n --help has been written to 80 since it existed and hints joined it in 0.7.1; --info is\n the mode whose whole purpose is being read by a person, and it was the last one guessing.\n */\n if (plan.groups.length > 1) {\n /*\n And the third thing this sentence could not say, after the OUTPUT column above it.\n\n `--stdout` on a recording with more than one rate is refused — it writes one table and\n this makes several — so no file is written and the run does not happen. The sentence\n described it anyway, three lines under a column saying `(stdout)` and six above the\n warning that the run would be refused:\n\n 0 EEG Fpz-Cz ... 256 Hz -250 to 250 (stdout)\n Sampling rates differ, so channels are written to 3 files, one per rate.\n warning: --stdout would refuse this run: needs exactly one table, but this\n recording produces 3 ...\n\n Three statements about one run, and the middle one names an outcome none of the others\n allows. What the rates decide is how many tables there are; where those go is the\n question this mode has already answered differently.\n */\n lines.push(\n wrap(\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 : toStdout\n ? `Sampling rates differ, so this recording makes ${counted(plan.groups.length, 'table')}, ` +\n `one per rate — more than --stdout can write. Converting into a directory writes one ` +\n `file each; --layout long puts them all in one table.`\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 /*\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 wrap(\n file.annotationSignals.length > 0\n ? events === null\n ? `Would write annotations${suffix} and channels${suffix}, and no signal data. How ` +\n 'many events there are cannot be told from the header, and finding out means ' +\n 'reading the annotation channel record by record.'\n : `Would write annotations${suffix} with ${counted(events, 'event')} and ` +\n `channels${suffix}, and no signal data.`\n : `Would write channels${suffix} and no signal data — and no annotations${suffix} ` +\n 'either, since this recording has no annotation channel.',\n ),\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 /*\n And the events file, which this line named for every run except the ones that write it.\n\n The branch above names `annotations.csv` because those runs write nothing else; a recording\n with signals *and* events falls through to here, and here the sentence is about the signal\n tables alone. So `--info` on an ordinary EDF+ recording described three of the four files a\n conversion leaves behind and never mentioned the fourth — the one carrying the scoring,\n which is the reason most people convert an EDF+ file at all.\n\n \"Named as they will be written\", like the OUTPUT column and the branch above: `--gzip`\n changes the name, and `--stdout` writes no sidecar at all, so it says nothing there. The\n count is the one `--info` already has — read on a discontinuous file, and honestly absent\n on a continuous one, where finding it means the scan this mode exists to avoid.\n */\n const alsoEvents =\n !toStdout && file.annotationSignals.length > 0\n ? `, and ${outputCsvName('annotations', plan.gzip)}` +\n `${events === null ? '' : ` with ${counted(events, 'event')}`}`\n : '';\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 const size =\n `${counted(plan.estimate.rows, 'row')}, roughly ` +\n `${formatBytes(plan.estimate.bytes)}${compressing ? ' before compression' : ''}`;\n lines.push(\n wrap(\n stdoutRefused\n ? // \"That conversion\" is the directory one the paragraph above has just named, which\n // is the only shape this branch refuses: the other two refusals write no signal\n // table at all and are answered on the line above.\n `That conversion would write ${size}; --stdout writes none of them.`\n : `Would write ${size}${alsoEvents}.`,\n ),\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(\n file: EdfFile,\n plan: ConversionPlan,\n /**\n * The warnings as `--info` prints them, which is not the list they start as.\n *\n * This built its own — `withoutFileRateWarning(file.diagnostics).concat(plan.diagnostics)` —\n * and so missed every amendment the text form makes on the way out. `--info --json --gzip`\n * said a label \"will appear as the channel's name in signals.csv\" for a run that writes\n * `signals.csv.gz`; `--stdout` named the same file for a run that writes no file at all;\n * `--annotations-only` promised a signal table it does not write, and `--channels` promised\n * one for a channel it leaves out. Thirteen sentences across the fixtures, all of them in\n * the surface a script reads, and the text beside them already correct.\n */\n warnings: readonly Diagnostic[],\n /**\n * The event count, on the files where `--info` has already read and counted them.\n *\n * `formatInfo` has taken this since 0.7.x and prints it — \"Would write annotations.csv with\n * 3 events\" — and the same number was dropped on the way to the JSON, which is the surface\n * a script reads and the reason `--info --json` exists. Null where the count is not in\n * hand, by the same rule `estimate` is null for a run that writes no signal table: a\n * continuous file is read only as far as its origin, so its events have not been counted\n * and there is no honest number to put here.\n */\n events: number | null = null,\n indent: number | null = 2,\n /** See `formatInfo`: under `--stdout` there is no file to name. */\n toStdout = false,\n): 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 const document = 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 /*\n The window, under the names `metadata.json` gives it, which is the rule this document\n follows for everything describing the run.\n\n The text form did not show it either until 0.8.82; here the only trace of `--start`\n and `--end` was `estimate.rows` coming back smaller, with nothing in the document\n saying why. A survey that runs `--info --json` over a folder with a window on the\n command line archives a description of each recording in which the window does not\n appear.\n */\n start_seconds: plan.range.startSeconds,\n end_seconds: plan.range.endSeconds,\n whole_recording: plan.range.isWholeRecording,\n annotation_channels: file.annotationSignals.length,\n // Named as `summaryJson` names the same count, so a prediction and a conversion read\n // the same field.\n annotations: events,\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 /*\n `-`, the placeholder `ConvertResult.outputDir` already uses for this mode, and not\n `null`: null is this document's word for \"would not be converted\", which is a\n different thing from \"converted, to a stream\".\n */\n output_file: fileFor.has(signal.index)\n ? toStdout\n ? '-'\n : (fileFor.get(signal.index) as string)\n : 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: warnings.map((d) => ({ code: d.code, severity: d.severity, message: d.message })),\n },\n null,\n indent ?? undefined,\n );\n // A path is untrusted text on this surface too; see escapeJsonText.\n return escapeJsonText(document);\n}\n\n/**\n * One diagnostic per `warning: ` line, prefixed so warnings are greppable; the hint below it\n * wrapped to the terminal.\n *\n * The hint has been on its own unprefixed continuation line since these gained hints at all,\n * so grepping for `warning:` never picked it up and wrapping it costs nothing that was being\n * relied on — which is what 0.6.132 got wrong when it left every diagnostic long on the\n * grounds that they are one line each. Half of that is true. The `warning:` head is a line\n * per diagnostic and stays one, at whatever width the message runs to; the hint underneath\n * it is prose addressed to a person reading a terminal, and 17 of them ran past 80 columns,\n * the widest to 180. At that width the second half of the advice is wherever the terminal\n * decided to put it, indented under nothing, and the 9-space rule that says \"this belongs to\n * the warning above\" is lost at exactly the moment there is enough text for it to matter.\n */\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}: ${printable(d.message)}`;\n return d.hint ? `${head}\\n${wrap(printable(d.hint), HINT_INDENT)}` : 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 grouped(file.rows),\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 ${elapsed(result.elapsedMs)}.`);\n return lines.join('\\n');\n}\n\n/**\n * How long the run took, in a unit it can be stated in.\n *\n * `elapsedMs` is a difference of two `Date.now()` readings, so it is whole milliseconds, and\n * `(ms / 1000).toFixed(1)` rounds everything under fifty of them to `0.0`. That is the last\n * line of every conversion of a small recording — `--annotations-only` on a three-record\n * file, every fixture in this repository, and the transcript on three documentation pages,\n * all of which read `Done in 0.0s.` for a run that read a file and wrote three.\n *\n * The same refusal `formatDuration` and `formatBytes` make one file over, and for the reason\n * stated there: none of them is a rounding rule, and each declines to print a form the\n * quantity cannot take — 1023.999 KB is not \"1024 KB\", two records of 1e-15s are not \"0s\",\n * and a conversion that happened did not take no time.\n *\n * A clock that reports no change at all has measured something below what it can resolve,\n * which is a different statement from zero and is what it says.\n */\nexport function elapsed(milliseconds: number): string {\n /*\n And the other end of it, which could only say seconds.\n\n This is the one place in the tool that states a length of time without `formatDuration`,\n and it is the line under a summary whose `Duration` field is printed *with* it. A\n conversion of an overnight recording read:\n\n Duration 8h 00m 0s (28800 records of 1s)\n ...\n Done in 412.7s.\n\n Two lengths of time on one screen, one decomposed and one not, from the same program. 412\n seconds is a number a reader has to divide by sixty to hold, which is the whole reason\n `formatDuration` exists and the same complaint `grouped` makes about counts.\n\n Handed over at a minute, since below that the seconds are the readable form — and rounded\n to the tenth this already prints first, because `formatDuration` keeps three decimals and\n \"6m 52.734s\" claims a precision two `Date.now()` readings do not have.\n */\n /*\n Rounded to the precision that will be printed, and only then given a form.\n\n The branch was chosen at sixty thousand milliseconds and the seconds were rounded to one\n decimal afterwards, so the last fifty milliseconds below the handover rounded up through\n it: 59,950 to 59,999 printed `60.0s`, a minute stated in seconds, one millisecond under\n the line that exists so a minute is stated as a minute.\n\n Done in 60.0s. 59,999 ms\n Done in 1m 0s. 60,000 ms\n\n Which is the slip `formatBytes` fixed in the same words — \"Rounding can carry into the\n next unit, and the unit was chosen before it: 1,048,575 bytes is 1023.999 KB, which\n printed as 1024 KB\" — and the one the paragraph above cites as the rule this function\n follows. Rounding first is what `formatDuration` does before it splits into hours and\n minutes, for the same reason.\n */\n const tenths = Math.round(milliseconds / 100) / 10;\n if (tenths >= 60) return formatDuration(tenths);\n if (milliseconds >= 50) return `${tenths.toFixed(1)}s`;\n return milliseconds > 0 ? `${milliseconds}ms` : 'under 1ms';\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 const document = 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 // A path is untrusted text on this surface too; see escapeJsonText.\n return escapeJsonText(document);\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 { fixed, formatBytes, formatDuration, plainSeconds } from '../format/number.js';\nimport { counted, grouped } from '../format/list.js';\nimport { escapeJsonText, printable } from '../format/unprintable.js';\nimport type { ConversionPlan } from '../convert/plan.js';\nimport { outputCsvName } from '../convert/plan.js';\nimport type { ConvertResult } from '../convert/run.js';\nimport { VERSION } from '../version.js';\n\n/** Where terminal prose wraps. The width --help is written to, and the ANSI default. */\nconst WRAP_COLUMNS = 80;\n\n/** The continuation indent under a `warning: ` prefix, which is the only one there is. */\nconst HINT_INDENT = ' '.repeat(9);\n\n/**\n * One unbreakable unit of a wrapped line: a quoted span, or a run of non-space.\n *\n * The lookbehind is what keeps an apostrophe out of it. `the file's own header and it's` has\n * two of them, and without the guard `'s own header and it'` is one quoted span — a token\n * with spaces in it that then refuses to break. A quote opening a span is at the start of a\n * word, so that is what is required of it.\n */\nconst WORD = /(?<=^|[\\s(])(?:\"[^\"]*\"|'[^']*')|\\S+/gu;\n\n/**\n * Greedy word wrap, `indent` on every line including the first.\n *\n * Only free prose goes through this. The aligned parts of `--info` — the `Format`/`Size`\n * key-value lines and the channel table — are laid out in columns, and re-flowing a column\n * is how you turn a table into a paragraph.\n *\n * A word wider than the column is left to overrun rather than broken. The long words here\n * are file paths and quoted channel labels, and neither survives being split across lines:\n * the point of printing a path is that it can be copied back out.\n *\n * A quoted span is one word for the same reason, however many spaces are inside it. The\n * sentence above is the promise, and it held only for the paths that have no spaces in them:\n * an interrupted conversion said `Files already written to \"/tmp/a very long destination\n * folder name with` / `many spaces here indeed\" are incomplete`, splitting the one thing on\n * the line that has to be copied whole — and quoting is what a path with spaces in it is\n * given in the first place. A word wider than the column already overruns rather than\n * breaking; this is that rule reaching the words it was written for.\n *\n * Which is also why the gap between two words is reproduced rather than normalised to one\n * space. Splitting on `\\s+` and rejoining with `' '` re-flowed everything, and a quoted value\n * is not prose: a channel labelled `EEG A` was offered back as `Did you mean \"EEG A\"?`, and\n * a destination given as `-my nightly` as `Write it as one argument instead:\n * '--out=-my nightly'`. Following either gets a different channel or a different directory\n * than the one the sentence is about, and the run before it had already quoted the name\n * correctly on the line above.\n */\nexport function wrap(text: string, indent = '', width = WRAP_COLUMNS): string {\n const lines: string[] = [];\n let line = indent;\n let at = 0;\n for (const match of text.matchAll(WORD)) {\n const piece = match[0];\n const gap = text.slice(at, match.index);\n at = match.index + piece.length;\n if (line === indent) line += piece;\n else if (line.length + gap.length + piece.length <= width) line += gap + piece;\n else {\n lines.push(line);\n line = indent + piece;\n }\n }\n if (line !== indent) lines.push(line);\n return lines.join('\\n');\n}\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 */\n// Defined in src/format/unprintable.ts, beside the character class it uses, since the library's\n// own message builders need it too — see 0.8.92. Re-exported here, which is where every caller\n// in this package already imports it from.\nexport { printable };\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) => {\n const safe = printable(line);\n // The first line is the one that follows `error: ` or `warning: `, so it is the line\n // a log gets grepped for, and it stays whole at whatever width it runs to.\n if (index === 0) return safe;\n\n /*\n Callers write the continuation indent one of two ways: most pass it here and leave\n their message lines flush, but the two usage builders bake ` ` into the string\n itself because they are printed without an `error: ` prefix in front. Reading the\n line's own leading space when none was passed keeps both working, and keeps the\n wrap aligned under the same column either way.\n */\n const body = safe.trimStart();\n const pad = indent === '' ? safe.slice(0, safe.length - body.length) : indent;\n\n /*\n A continuation that is a command is copied, not read.\n\n `edf2csv -- \"--chanels\"` is the line the unknown-option error ends on, and it exists\n to be pasted back into the shell. Wrapping puts `edf2csv --` on one line and the\n flag on the next, and what gets pasted is half a command. Prose survives being\n re-flowed and a command does not, so the two are told apart rather than being\n treated alike and hoping today's strings stay short.\n */\n if (body.startsWith('edf2csv ')) return pad + body;\n return wrap(body, pad);\n })\n .join('\\n');\n}\n\n/** The `--info` view: what is in this recording, and what would converting it produce. */\nexport function formatInfo(\n file: EdfFile,\n plan: ConversionPlan,\n /**\n * How many events a conversion would write, when that is already known.\n *\n * Null when it is not. `--info` reads the whole annotation channel of a discontinuous file,\n * because that is where its record times are; a continuous one it reads only far enough to\n * find the origin. So the count is in hand for one of the two, and the line below said \"How\n * many events there are cannot be told from the header\" for both — true of the header, and\n * beside the point on a file whose events had just been read and counted.\n */\n events: number | null = null,\n /**\n * Whether the run would stream to stdout, in which case no file is written at all.\n *\n * The OUTPUT column is \"Named as they will be written. --info is read to find out what a\n * run leaves behind, and a script that opens the name it was given must find a file there\"\n * — and under `--stdout` it named `signals.csv`, or `signals.csv.gz`, for a run that\n * creates no directory and no file. The same table already tells a channel that will not\n * be converted from one the file gives nothing to convert; this is the third thing it\n * could not say.\n */\n toStdout = false,\n /**\n * Whether `--stdout` would refuse the run this describes, in which case it writes nothing.\n *\n * The paragraph above the estimate already says so for the commonest of the three\n * refusals — \"this recording makes 3 tables, one per rate — more than `--stdout` can\n * write\" — and the line under it went on predicting the rows anyway:\n *\n * Sampling rates differ, so this recording makes 3 tables, one per rate — more\n * than --stdout can write. Converting into a directory writes one file each;\n * --layout long puts them all in one table.\n * Would write 14 rows, roughly 262 B.\n *\n * warning: --stdout would refuse this run: needs exactly one table, ...\n *\n * Three statements about one command, and the middle one is of a run that exits 1 having\n * written nothing. The estimate is still the answer to a question worth asking — it is\n * what converting into a directory would write — so it is kept and its subject corrected,\n * which is the same repair `--annotations-only` got on this line at 0.4.51.\n *\n * Taken from the caller rather than worked out here: `stdoutRefusal` is the conversion's\n * own guard, and the warning below the report is already built from it, so there is one\n * answer rather than two that can drift.\n */\n stdoutRefused = false,\n): 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 /*\n The two raw fields, with edges.\n\n Echoed bare, they ran into each other and into the parenthetical, and a header field is\n free to be empty or to be nothing but padding — `trimField` takes the padding off, so\n both of those arrive here as \"\". A file whose date field is blank printed\n\n Recorded 22.15.00 (unparseable)\n\n where the one value on the line is the *time* and nothing says so, and a file with both\n blank printed `Recorded (unparseable)`: a label, a gap, and a verdict about nothing\n visible. ` . . ` in both fields came out `. . . .`, which is four fields or two\n depending on how the reader counts.\n\n Quoted, which is what `START_TIME_UNREADABLE` — the warning printed under this very line,\n about these very fields — has always done: `(\"\" and \"\")`. Same argument 0.7.x made for the\n time-range refusals, in its words: without them \"the value ran into the sentence ... and\n the surrounding spaces — the actual reason a shell-built argument went wrong — are\n invisible\". `--info --json` has the two as separate strings and never had the problem.\n */\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 ${plainSeconds(header.recordDuration)}s)`,\n );\n const elapsedSpan = plan.range.recordingEndSeconds - plan.range.recordingStartSeconds;\n if (Math.abs(elapsedSpan - file.durationSeconds) > 1e-9) {\n /*\n Which way the two differ decides what to call it, and the parenthetical used to say\n \"includes discontinuities\" both ways round.\n\n A span LONGER than the duration is the gap case this line was written for: 3 records of\n 1s covering 11 seconds. A span SHORTER than the duration cannot be a gap — it is records\n that overlap, which an EDF+D file gets when a device re-sends a buffer. Three records of\n 1s starting at 0, 0.5 and 1 print:\n\n Duration 3s (3 records of 1s)\n Time span 2s (includes discontinuities)\n\n A recording covering less time than its own records account for, blamed on gaps it does\n not have — while the warning below it says, correctly, that two records overlap.\n\n A file holding both is described by whichever wins the subtraction, and the overlap\n warning is printed either way.\n */\n const overlapping = elapsedSpan < file.durationSeconds;\n lines.push(\n `Time span ${formatDuration(elapsedSpan)} ` +\n `(${overlapping ? 'records overlap in time' : 'includes discontinuities'})`,\n );\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 //\n // Through `fixed` rather than `toFixed`, which switches to exponent notation at 1e21 —\n // and `--start 1e+21s` is refused by the time parser with \"uses an unknown unit \\\"e\\\"\",\n // so the one line that says which clock to use handed back a number that clock rejects.\n // Reachable from a conforming file: an EDF+ onset is plain digits of any length, and a\n // record duration large enough to keep samples apart at that magnitude is four\n // characters. `fixed` expands these with BigInt, which is exact past 2^53 where a double\n // carries no fraction anyway, and is byte-for-byte `toFixed` everywhere else.\n lines.push(\n `Timed from ${fixed(startsAt, 3)}s (first sample; --start and --end use this clock)`,\n );\n }\n /*\n And the window, when one was asked for, which this report did not mention at all.\n\n `--info` is the mode whose purpose is to say what a conversion will do, and every other\n flag that changes what gets written is visible in it: `--channels` puts `(not selected)`\n in the OUTPUT column, `--gzip` changes the names there, `--layout long` changes the\n sentence under the table, `--annotations-only` replaces the estimate outright. `--start`\n and `--end` changed one number:\n\n $ edf2csv rec.edf --info $ edf2csv rec.edf --info --start 1s --end 2s\n Duration 3s (3 records of 1s) Duration 3s (3 records of 1s)\n ... ...\n Would write 1,155 rows, roughly 22.2 KB. Would write 385 rows, roughly 7.4 KB.\n\n Byte-for-byte identical otherwise — and the Duration line, which is what a reader checks a\n window against, goes on describing the whole file above an estimate that describes a third\n of it. The window is in `plan.range` already, which is the same thing `Timed from` above\n was added for: it governs the estimate printed below and was simply never shown.\n\n In seconds rather than through `formatDuration`, for the reason that line gives: these are\n numbers meant to be typed back into `--start` and `--end`, and \"16m 40s\" is not one.\n */\n const { range } = plan;\n /*\n An end that a double cannot hold is still an end.\n\n `endSeconds` is `recordCount * recordDuration` when no `--end` was given, and a header may\n state a record duration of 1e308 in five characters — two of those records overflow, so\n the figure comes back `Infinity`. This line required it to be finite and so vanished\n altogether on exactly the recording whose estimate is hardest to account for: the whole\n point of the line is to say why the row count changed, and `--start 1s` on such a file\n changed it from 8 rows to 7 with nothing on screen to say a window had been asked for.\n\n The window is still perfectly ordinary from this side — it begins where it was told to and\n runs to the end of the recording, which is what the line says now.\n */\n const endsAtInfinity = !Number.isFinite(range.endSeconds);\n if (!range.isWholeRecording && Number.isFinite(range.startSeconds)) {\n /*\n What the window selects, which is not data records in every mode.\n\n `--annotations-only` reads the annotation channel and converts no data records at all —\n the OUTPUT column of the table below says `(no signal data)` for every channel, and the\n line under it says \"and no signal data\". This counted them anyway:\n\n Window 1.000s to 3.000s (2 of 3 data records)\n ...\n Would write annotations.csv and channels.csv, and no signal data.\n\n The window is not idle in that mode: it decides which events are exported, by onset. So\n the range stays and the parenthetical says what it is doing.\n */\n const selects = plan.writeSignals\n ? `${grouped(range.endRecord - range.startRecord)} of ${counted(file.recordCount, 'data record')}`\n : 'events outside it are not exported';\n const to = endsAtInfinity ? 'the end' : `${fixed(range.endSeconds, 3)}s`;\n lines.push(`Window ${fixed(range.startSeconds, 3)}s to ${to} (${selects})`);\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 /*\n Both counts through `counted`, which this function already uses one line up for the record\n count and which exists so a count and its noun cannot disagree.\n\n The annotation-channel half was pluralised by hand when it was fixed — a file carrying two\n of them read \"2 annotation channel\", and EDF+ permits more than one — and the signal half\n had been hand-rolled since it was written. Two spellings of one rule in one line, in the\n file that imports the rule.\n */\n const annotationCount = file.annotationSignals.length;\n const annotationPart =\n annotationCount > 0 ? ` + ${counted(annotationCount, 'annotation channel')}` : '';\n lines.push(`Channels ${counted(signals.length, 'signal')}${annotationPart}`);\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 /*\n And under --annotations-only nothing was deselected either — the run writes no signal\n table at all, which is the same distinction one paragraph up.\n\n `--info --annotations-only --channels \"EEG Fpz-Cz\"` accepted the name, checked it\n against the file, and then printed \"(not selected)\" against the very channel it had\n just been asked for. The sentence three lines under the table already says the truth,\n \"and no signal data\", and `channels.csv` and `--info --json` both say it too — the\n cell is empty and `output_file` is null. The human table was the one place claiming a\n choice nobody made.\n */\n plan.writeSignals\n ? fileFor.has(signal.index)\n ? toStdout\n ? '(stdout)'\n : (fileFor.get(signal.index) as string)\n : signal.samplesPerRecord === 0\n ? '(no samples)'\n : '(not selected)'\n : '(no signal data)',\n ]);\n }\n /*\n A column head over nothing is not a table.\n\n The line above has just said `Channels 0 signals + 1 annotation channel`, and this then\n printed\n\n # COLUMN LABEL UNIT RATE RANGE OUTPUT\n\n with no row under it — seven headings for a set of no channels, on a recording holding\n only events, which is what `annotations-only.edf` in this repository is and what a\n companion event file out of a scoring system is. The same sentence `--channels \"#0\"` was\n given its own wording for: \"No channel at position #0. This file has signal channels at\n .\" states there are some and then names none.\n\n Nothing goes in its place. The count is stated one line up and the body three lines down\n says what the conversion would write; a table saying \"none\" twice more adds nothing.\n */\n if (signals.length > 0) {\n lines.push(table(rows, new Set([0])));\n lines.push('');\n }\n /*\n From here down --info stops laying out columns and starts explaining itself, so from\n here down it wraps. The key-value lines and the channel table above are aligned to each\n other and must not be re-flowed; these are sentences, and the longest of them ran to 156\n columns — which is not a line anyone reads, it is a line a terminal breaks somewhere.\n --help has been written to 80 since it existed and hints joined it in 0.7.1; --info is\n the mode whose whole purpose is being read by a person, and it was the last one guessing.\n */\n if (plan.groups.length > 1) {\n /*\n And the third thing this sentence could not say, after the OUTPUT column above it.\n\n `--stdout` on a recording with more than one rate is refused — it writes one table and\n this makes several — so no file is written and the run does not happen. The sentence\n described it anyway, three lines under a column saying `(stdout)` and six above the\n warning that the run would be refused:\n\n 0 EEG Fpz-Cz ... 256 Hz -250 to 250 (stdout)\n Sampling rates differ, so channels are written to 3 files, one per rate.\n warning: --stdout would refuse this run: needs exactly one table, but this\n recording produces 3 ...\n\n Three statements about one run, and the middle one names an outcome none of the others\n allows. What the rates decide is how many tables there are; where those go is the\n question this mode has already answered differently.\n */\n lines.push(\n wrap(\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 : toStdout\n ? `Sampling rates differ, so this recording makes ${counted(plan.groups.length, 'table')}, ` +\n `one per rate — more than --stdout can write. Converting into a directory writes one ` +\n `file each; --layout long puts them all in one table.`\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 /*\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 const files =\n file.annotationSignals.length > 0\n ? events === null\n ? `annotations${suffix} and channels${suffix}, and no signal data`\n : `annotations${suffix} with ${counted(events, 'event')} and ` +\n `channels${suffix}, and no signal data`\n : `channels${suffix} and no signal data — and no annotations${suffix} ` +\n 'either, since this recording has no annotation channel';\n // The one form of this line whose count is not in hand; see the `events` parameter.\n const unknown =\n file.annotationSignals.length > 0 && events === null\n ? ' How many events there are cannot be told from the header, and finding out means ' +\n 'reading the annotation channel record by record.'\n : '';\n /*\n And the other half of what 0.9.34 corrected one branch down.\n\n Both of the refusals that land here are refusals: `--stdout` has nothing to stream for\n `--annotations-only`, and nothing for a recording with no signal channels. So this line\n named two files for a run that writes none, three lines above the warning saying the\n run will not happen and one of them above a hint offering the same two files as the\n thing to convert to a directory for:\n\n Would write annotations.csv and channels.csv, and no signal data.\n\n warning: --stdout would refuse this run: has no signal data to write: this\n recording has no signal channels, only EDF+ annotations.\n Convert to a directory to get its annotations.csv, or drop --stdout.\n\n Named as the conversion that does write them, which is the one the hint sends you to.\n */\n lines.push(\n wrap(\n stdoutRefused\n ? // Refusal first, since two of the three forms end in a subordinate clause that a\n // trailing one would have to be read past.\n `--stdout writes nothing here; converting into a directory would write ` +\n `${files}.${unknown}`\n : `Would write ${files}.${unknown}`,\n ),\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 /*\n And the events file, which this line named for every run except the ones that write it.\n\n The branch above names `annotations.csv` because those runs write nothing else; a recording\n with signals *and* events falls through to here, and here the sentence is about the signal\n tables alone. So `--info` on an ordinary EDF+ recording described three of the four files a\n conversion leaves behind and never mentioned the fourth — the one carrying the scoring,\n which is the reason most people convert an EDF+ file at all.\n\n \"Named as they will be written\", like the OUTPUT column and the branch above: `--gzip`\n changes the name, and `--stdout` writes no sidecar at all, so it says nothing there. The\n count is the one `--info` already has — read on a discontinuous file, and honestly absent\n on a continuous one, where finding it means the scan this mode exists to avoid.\n */\n const alsoEvents =\n !toStdout && file.annotationSignals.length > 0\n ? `, and ${outputCsvName('annotations', plan.gzip)}` +\n `${events === null ? '' : ` with ${counted(events, 'event')}`}`\n : '';\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 const size =\n `${counted(plan.estimate.rows, 'row')}, roughly ` +\n `${formatBytes(plan.estimate.bytes)}${compressing ? ' before compression' : ''}`;\n lines.push(\n wrap(\n stdoutRefused\n ? // \"That conversion\" is the directory one the paragraph above has just named, which\n // is the only shape this branch refuses: the other two refusals write no signal\n // table at all and are answered on the line above.\n `That conversion would write ${size}; --stdout writes none of them.`\n : `Would write ${size}${alsoEvents}.`,\n ),\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(\n file: EdfFile,\n plan: ConversionPlan,\n /**\n * The warnings as `--info` prints them, which is not the list they start as.\n *\n * This built its own — `withoutFileRateWarning(file.diagnostics).concat(plan.diagnostics)` —\n * and so missed every amendment the text form makes on the way out. `--info --json --gzip`\n * said a label \"will appear as the channel's name in signals.csv\" for a run that writes\n * `signals.csv.gz`; `--stdout` named the same file for a run that writes no file at all;\n * `--annotations-only` promised a signal table it does not write, and `--channels` promised\n * one for a channel it leaves out. Thirteen sentences across the fixtures, all of them in\n * the surface a script reads, and the text beside them already correct.\n */\n warnings: readonly Diagnostic[],\n /**\n * The event count, on the files where `--info` has already read and counted them.\n *\n * `formatInfo` has taken this since 0.7.x and prints it — \"Would write annotations.csv with\n * 3 events\" — and the same number was dropped on the way to the JSON, which is the surface\n * a script reads and the reason `--info --json` exists. Null where the count is not in\n * hand, by the same rule `estimate` is null for a run that writes no signal table: a\n * continuous file is read only as far as its origin, so its events have not been counted\n * and there is no honest number to put here.\n */\n events: number | null = null,\n indent: number | null = 2,\n /** See `formatInfo`: under `--stdout` there is no file to name. */\n toStdout = false,\n): 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 const document = 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 /*\n The window, under the names `metadata.json` gives it, which is the rule this document\n follows for everything describing the run.\n\n The text form did not show it either until 0.8.82; here the only trace of `--start`\n and `--end` was `estimate.rows` coming back smaller, with nothing in the document\n saying why. A survey that runs `--info --json` over a folder with a window on the\n command line archives a description of each recording in which the window does not\n appear.\n */\n start_seconds: plan.range.startSeconds,\n end_seconds: plan.range.endSeconds,\n whole_recording: plan.range.isWholeRecording,\n annotation_channels: file.annotationSignals.length,\n // Named as `summaryJson` names the same count, so a prediction and a conversion read\n // the same field.\n annotations: events,\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 /*\n `-`, the placeholder `ConvertResult.outputDir` already uses for this mode, and not\n `null`: null is this document's word for \"would not be converted\", which is a\n different thing from \"converted, to a stream\".\n */\n output_file: fileFor.has(signal.index)\n ? toStdout\n ? '-'\n : (fileFor.get(signal.index) as string)\n : 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: warnings.map((d) => ({ code: d.code, severity: d.severity, message: d.message })),\n },\n null,\n indent ?? undefined,\n );\n // A path is untrusted text on this surface too; see escapeJsonText.\n return escapeJsonText(document);\n}\n\n/**\n * One diagnostic per `warning: ` line, prefixed so warnings are greppable; the hint below it\n * wrapped to the terminal.\n *\n * The hint has been on its own unprefixed continuation line since these gained hints at all,\n * so grepping for `warning:` never picked it up and wrapping it costs nothing that was being\n * relied on — which is what 0.6.132 got wrong when it left every diagnostic long on the\n * grounds that they are one line each. Half of that is true. The `warning:` head is a line\n * per diagnostic and stays one, at whatever width the message runs to; the hint underneath\n * it is prose addressed to a person reading a terminal, and 17 of them ran past 80 columns,\n * the widest to 180. At that width the second half of the advice is wherever the terminal\n * decided to put it, indented under nothing, and the 9-space rule that says \"this belongs to\n * the warning above\" is lost at exactly the moment there is enough text for it to matter.\n */\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}: ${printable(d.message)}`;\n return d.hint ? `${head}\\n${wrap(printable(d.hint), HINT_INDENT)}` : 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 grouped(file.rows),\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 ${elapsed(result.elapsedMs)}.`);\n return lines.join('\\n');\n}\n\n/**\n * How long the run took, in a unit it can be stated in.\n *\n * `elapsedMs` is a difference of two `Date.now()` readings, so it is whole milliseconds, and\n * `(ms / 1000).toFixed(1)` rounds everything under fifty of them to `0.0`. That is the last\n * line of every conversion of a small recording — `--annotations-only` on a three-record\n * file, every fixture in this repository, and the transcript on three documentation pages,\n * all of which read `Done in 0.0s.` for a run that read a file and wrote three.\n *\n * The same refusal `formatDuration` and `formatBytes` make one file over, and for the reason\n * stated there: none of them is a rounding rule, and each declines to print a form the\n * quantity cannot take — 1023.999 KB is not \"1024 KB\", two records of 1e-15s are not \"0s\",\n * and a conversion that happened did not take no time.\n *\n * A clock that reports no change at all has measured something below what it can resolve,\n * which is a different statement from zero and is what it says.\n */\nexport function elapsed(milliseconds: number): string {\n /*\n And the other end of it, which could only say seconds.\n\n This is the one place in the tool that states a length of time without `formatDuration`,\n and it is the line under a summary whose `Duration` field is printed *with* it. A\n conversion of an overnight recording read:\n\n Duration 8h 00m 0s (28800 records of 1s)\n ...\n Done in 412.7s.\n\n Two lengths of time on one screen, one decomposed and one not, from the same program. 412\n seconds is a number a reader has to divide by sixty to hold, which is the whole reason\n `formatDuration` exists and the same complaint `grouped` makes about counts.\n\n Handed over at a minute, since below that the seconds are the readable form — and rounded\n to the tenth this already prints first, because `formatDuration` keeps three decimals and\n \"6m 52.734s\" claims a precision two `Date.now()` readings do not have.\n */\n /*\n Rounded to the precision that will be printed, and only then given a form.\n\n The branch was chosen at sixty thousand milliseconds and the seconds were rounded to one\n decimal afterwards, so the last fifty milliseconds below the handover rounded up through\n it: 59,950 to 59,999 printed `60.0s`, a minute stated in seconds, one millisecond under\n the line that exists so a minute is stated as a minute.\n\n Done in 60.0s. 59,999 ms\n Done in 1m 0s. 60,000 ms\n\n Which is the slip `formatBytes` fixed in the same words — \"Rounding can carry into the\n next unit, and the unit was chosen before it: 1,048,575 bytes is 1023.999 KB, which\n printed as 1024 KB\" — and the one the paragraph above cites as the rule this function\n follows. Rounding first is what `formatDuration` does before it splits into hours and\n minutes, for the same reason.\n */\n const tenths = Math.round(milliseconds / 100) / 10;\n if (tenths >= 60) return formatDuration(tenths);\n if (milliseconds >= 50) return `${tenths.toFixed(1)}s`;\n return milliseconds > 0 ? `${milliseconds}ms` : 'under 1ms';\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 const document = 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 // A path is untrusted text on this surface too; see escapeJsonText.\n return escapeJsonText(document);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edf2csv",
3
- "version": "0.9.34",
3
+ "version": "0.9.35",
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",