edf2csv 0.7.242 → 0.7.244
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/report.js +11 -6
- package/dist/cli/report.js.map +1 -1
- package/dist/convert/timing.js +4 -2
- package/dist/convert/timing.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/report.js
CHANGED
|
@@ -242,13 +242,18 @@ events = null) {
|
|
|
242
242
|
lines.push(`Recording ${printable(header.recordingId)}`);
|
|
243
243
|
const signals = file.dataSignals;
|
|
244
244
|
lines.push('');
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
/*
|
|
246
|
+
Both counts through `counted`, which this function already uses one line up for the record
|
|
247
|
+
count and which exists so a count and its noun cannot disagree.
|
|
248
|
+
|
|
249
|
+
The annotation-channel half was pluralised by hand when it was fixed — a file carrying two
|
|
250
|
+
of them read "2 annotation channel", and EDF+ permits more than one — and the signal half
|
|
251
|
+
had been hand-rolled since it was written. Two spellings of one rule in one line, in the
|
|
252
|
+
file that imports the rule.
|
|
253
|
+
*/
|
|
247
254
|
const annotationCount = file.annotationSignals.length;
|
|
248
|
-
const annotationPart = annotationCount > 0
|
|
249
|
-
|
|
250
|
-
: '';
|
|
251
|
-
lines.push(`Channels ${signals.length} signal${signals.length === 1 ? '' : 's'}${annotationPart}`);
|
|
255
|
+
const annotationPart = annotationCount > 0 ? ` + ${counted(annotationCount, 'annotation channel')}` : '';
|
|
256
|
+
lines.push(`Channels ${counted(signals.length, 'signal')}${annotationPart}`);
|
|
252
257
|
lines.push('');
|
|
253
258
|
const rows = [['#', 'COLUMN', 'LABEL', 'UNIT', 'RATE', 'RANGE', 'OUTPUT']];
|
|
254
259
|
const fileFor = new Map();
|
package/dist/cli/report.js.map
CHANGED
|
@@ -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, plain } 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\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 */\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) => {\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): 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 ${plain(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 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 /*\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 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 : `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 lines.push(\n wrap(\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\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/**\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 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"]}
|
|
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, plain } 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\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 */\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) => {\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): 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 ${plain(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 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 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 /*\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 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 : `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 lines.push(\n wrap(\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\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/**\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 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/dist/convert/timing.js
CHANGED
|
@@ -37,8 +37,10 @@ export function deriveRecordStarts(file, annotationData) {
|
|
|
37
37
|
diagnostics.push({
|
|
38
38
|
code: 'ANNOTATION_DECODE_FAILED',
|
|
39
39
|
severity: 'warning',
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// Through `counted`, with the irregular plural spelled out — which is the case its
|
|
41
|
+
// own comment names and which nothing was using it for.
|
|
42
|
+
message: `${counted(annotationData.malformed, 'annotation entry', 'annotation entries')} ` +
|
|
43
|
+
`${annotationData.malformed === 1 ? 'was' : 'were'} unreadable and could not be exported.`,
|
|
42
44
|
hint: 'The rest were exported normally. The file may have been written by a non-conforming tool.',
|
|
43
45
|
});
|
|
44
46
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timing.js","sourceRoot":"","sources":["../../src/convert/timing.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Diagnostic } from '../edf/errors.js';\nimport type { EdfFile } from '../edf/reader.js';\nimport { counted, listed } from '../format/list.js';\nimport { plain } from '../format/number.js';\n\nexport interface AnnotationTimingData {\n recordStarts: (number | null)[];\n malformed: number;\n /** Unreadable TALs in first position, which carry timing rather than an event. */\n malformedTimekeeping?: number;\n malformedTimekeepingWithText?: number;\n}\n\n/**\n * The EDF+D warning's promise, withdrawn when the file cannot keep it.\n *\n * The header parser raises DISCONTINUOUS with the hint \"Each row carries its true recording\n * time, so gaps stay visible instead of being closed\" — which is what an EDF+D conversion\n * does, when the record times can be read. When they cannot, the very next warning in the\n * same run says the opposite: \"Times are written as if the records were contiguous. Any gaps\n * are lost.\" Two warnings, printed together, and the second denies the first.\n *\n * The parser cannot know: whether the starts can be derived is settled here, after the\n * annotation channel has been read. So the hint is amended where the answer is, the same way\n * `withoutFileRateWarning` drops a header diagnostic the plan has superseded.\n */\nexport function withTimingPromiseKept(\n diagnostics: readonly Diagnostic[],\n derived: boolean,\n): Diagnostic[] {\n if (derived) return [...diagnostics];\n return diagnostics.map((d) =>\n d.code === 'DISCONTINUOUS' && d.hint?.includes('gaps stay visible')\n ? {\n ...d,\n hint:\n 'Where its records sit in time is not recorded in this file, so they are written ' +\n 'as if contiguous — see the warning below.',\n }\n : d,\n );\n}\n\n/**\n * Resolve the true start time of every data record.\n *\n * Continuous recordings need no table because their record positions are\n * arithmetic. EDF+D recordings carry their positions in the annotation channel;\n * missing or malformed timekeeping entries are reported before falling back.\n */\nexport function deriveRecordStarts(\n file: EdfFile,\n annotationData: AnnotationTimingData,\n): { starts: Float64Array | null; diagnostics: Diagnostic[] } {\n const diagnostics: Diagnostic[] = [];\n\n if (annotationData.malformed > 0) {\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n message:\n `${annotationData.malformed} annotation entr${annotationData.malformed === 1 ? 'y was' : 'ies were'} ` +\n `unreadable and could not be exported.`,\n hint: 'The rest were exported normally. The file may have been written by a non-conforming tool.',\n });\n }\n\n\n /*\n A timekeeping TAL is not an event, and saying it \"could not be exported\" describes the\n wrong loss twice over.\n\n These were counted among the annotations, so a file with one unreadable timekeeping TAL\n and three good events announced \"1 annotation entry was unreadable and could not be\n exported\" — while exporting all three. Nothing was missing from annotations.csv; what\n went missing was a record's position in time, which the message never mentioned.\n */\n const lostTimekeeping = annotationData.malformedTimekeeping ?? 0;\n const withText = annotationData.malformedTimekeepingWithText ?? 0;\n // The EDF+D branch below raises its own, which names the records and is more specific.\n // Saying both would report one problem twice.\n if (lostTimekeeping > 0 && file.header.continuity !== 'EDF+D') {\n const one = lostTimekeeping === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n message:\n `${lostTimekeeping} data record${one ? '' : 's'} carr${one ? 'ies' : 'y'} a timekeeping ` +\n `annotation that could not be read, so ${one ? 'it does' : 'they do'} not say where in ` +\n `time ${one ? 'it sits' : 'they sit'}.`,\n hint:\n /*\n \"No event was lost\" was said whatever the entry held.\n\n A first-position TAL states the record's start time and may carry events after it,\n which the specification allows and writers do. When one of those cannot be parsed\n the events go with it: a six-event file came out with two, and the only warning\n about it said in so many words that nothing had gone. Those entries are counted\n among the unreadable ones now, which is the warning printed above this one; this\n sentence keeps to what is true of the entries it is actually about.\n */\n (withText > 0\n ? `${withText === 1 ? 'One of them' : `${withText} of them`} also carried event text, ` +\n `which went with ${withText === 1 ? 'it' : 'them'} and is counted above. A ` +\n `timekeeping annotation itself states a record's start time and is never exported. `\n : 'No event was lost — a timekeeping annotation states a record\\'s start time and ' +\n 'is never exported. ') + 'Times are derived from the records that could be read.',\n });\n }\n\n /*\n A continuous recording's records are contiguous, but the first one need not sit at zero.\n\n EDF+ puts the header's start time and every annotation onset on one origin, and says the\n first data record's timekeeping TAL \"always starts with +0.X\", stating the fraction of a\n second by which that record follows it. Ignoring that fraction timed the samples from 0\n while the events kept their true onsets, so the two ended up on origins half a second\n apart — an event at +0.75 in a 4 Hz recording whose first TAL reads +0.5 landed on sample\n 3 instead of sample 1. The same file marked EDF+D, byte-identical but for the reserved\n field, placed it correctly, which is what gives the omission away.\n\n Records stay contiguous, which is what continuous means: only the origin moves. A first\n TAL of +0 needs no table at all, and that is nearly every file.\n */\n if (file.header.continuity !== 'EDF+D') {\n if (file.header.continuity !== 'EDF+C') {\n /*\n An annotation channel the reserved field never claimed.\n\n Without an `EDF+C` or `EDF+D` marker this is a plain EDF file, so the origin is not\n applied and the samples are timed from zero. The annotation channel is found by label\n rather than by the marker, though, so its events are still read and exported — with\n the onsets the file gives them.\n\n On a file whose timekeeping says the records start at 1000s, that put signals.csv at\n 0.000 and the event at 1000.5 in annotations.csv: two files from one conversion, a\n thousand seconds apart, and nothing said so. output-files promises the opposite —\n \"`onset_s` is on the same clock as `time_s` in the signal files\".\n\n Reported rather than repaired. Which clock is right is not knowable from here: the\n marker says plain EDF and the annotation channel says otherwise, and picking one would\n move either the samples or the events by the origin on a guess.\n */\n const stated = annotationData.recordStarts.find((start) => start !== null) ?? null;\n if (stated !== null && Math.abs(stated) > 0) {\n diagnostics.push({\n code: 'MISSING_EDF_PLUS_MARKER',\n severity: 'warning',\n message:\n `This file has an annotation channel stating that its records begin at ` +\n `${plain(stated)}s, but its reserved field carries no EDF+C or EDF+D marker — so ` +\n `it is read as plain EDF, time_s counts from zero, and the two disagree by ` +\n `${plain(stated)}s.`,\n hint:\n 'annotations.csv keeps the onsets the file gives, so its events and signals.csv ' +\n 'are on different clocks. Mark the file EDF+C, or subtract the offset from the ' +\n 'onsets, before joining them.',\n });\n }\n return { starts: null, diagnostics };\n }\n\n /*\n The origin comes from whichever record first states one, not from record 0 alone.\n\n Reading only `recordStarts[0]` meant a single unreadable timekeeping TAL threw the\n origin away and timed the whole file from zero — while records 1 and 2, saying plainly\n that they start at 1.5s and 2.5s, went unread. A recording whose records sit at 0.5s,\n 1.5s and 2.5s came out with every sample 0.5s earlier than the file states, against\n annotation onsets that kept their true values. That is precisely the mismatch 0.4.9\n fixed, arriving through the one hole left in it, and the byte-identical EDF+D twin\n timed it correctly, which is what gives it away.\n\n Continuity is what makes this recoverable: record i sits at `origin + i * duration`,\n so any readable record determines the origin for all of them.\n */\n const origin = originOf(annotationData.recordStarts, file.header.recordDuration);\n if (origin === null) return { starts: null, diagnostics };\n\n const contiguous = new Float64Array(file.recordCount);\n for (let i = 0; i < file.recordCount; i++) {\n contiguous[i] = origin + i * file.header.recordDuration;\n }\n\n /*\n A file marked continuous whose own records disagree about it.\n\n Nothing looked at records past the first, so an EDF+C file whose records are in fact\n spread out was timed as though they were contiguous and said nothing. The records are\n being read here anyway, so the contradiction costs nothing to notice — and it is the\n file, not the reader, that has to be wrong for this to fire.\n\n Compared against what the file can express, not for equality. 0.4.41 asked whether the\n two doubles were the same, which they are not: a recording of 0.1s records sitting at\n 0.1, 0.2, 0.3 ... is contiguous by construction, and 0.1 + 2 * 0.1 is\n 0.30000000000000004. Two of its eight records were reported as contradicting\n continuity, on an ordinary file — and under --strict that was a failed run. The\n smallest interval the recording distinguishes is one sample of its fastest channel;\n anything below half of that is arithmetic, not a gap. `canCarry` has already refused\n origins where the double spacing swamps that interval, so the representation error is\n under the tolerance by construction rather than by hope.\n */\n const tolerance = finestInterval(file) / 2;\n const contradicting = annotationData.recordStarts.filter(\n (declared, i) =>\n typeof declared === 'number' && Math.abs(declared - (contiguous[i] as number)) > tolerance,\n ).length;\n if (contradicting > 0) {\n const continuous = file.header.isBdf ? 'BDF+C' : 'EDF+C';\n const discontinuous = file.header.isBdf ? 'BDF+D' : 'EDF+D';\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n /*\n The markers as the file spells them.\n\n `continuity` normalises `BDF+C` to the internal `EDF+C` tag, and that tag reached\n the message: a BDF+ recording was told it is \"marked continuous (EDF+C)\" — a string\n it does not contain — and advised that it \"should have been marked EDF+D\", which is\n not a value BDF+ defines. A reader grepping the header for either finds nothing.\n\n The sibling discontinuous warning has done this since 0.3.x: `${'$'}{isBdf ? 'BDF+D' :\n 'EDF+D'}`. Same code, same header field, and the continuous branch never got it.\n */\n message:\n `This file is marked continuous (${continuous}), but ${contradicting} of its ` +\n `${counted(file.recordCount, 'data record')} ` +\n `${contradicting === 1 ? 'says it starts' : 'say they start'} somewhere other than ` +\n `where continuity puts ${contradicting === 1 ? 'it' : 'them'}.`,\n hint:\n `Times are written as if the records were contiguous, which is what ${continuous} ` +\n `means. If the recording really has gaps, the file should have been marked ` +\n `${discontinuous}.`,\n });\n }\n const first = origin;\n const last = contiguous[file.recordCount - 1] ?? first;\n if (!canCarry(last, file)) {\n diagnostics.push(unusableOrigin(first, file));\n return { starts: null, diagnostics };\n }\n /*\n An origin of zero is the same as no origin, for timing. It is not the same for the\n check above.\n\n This returned early on `origin === 0`, which is right about the times — contiguous\n starts from zero are what timing from zero already produces — and skipped the\n contradiction check on the way past. So an EDF+C file whose records say 0, 5 and 10 on\n one-second records went unreported, while the same file shifted one second, saying 1, 6\n and 11, was reported. The contradiction is in records 1 and 2 either way; where record 0\n happens to sit decides nothing about it.\n */\n if (origin === 0) return { starts: null, diagnostics };\n return { starts: contiguous, diagnostics };\n }\n\n if (file.annotationSignals.length === 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n 'This file is marked discontinuous but has no annotation channel, so where its ' +\n 'records sit in time is not recorded anywhere.',\n hint: 'Times are written as if the records were contiguous. Any gaps are lost.',\n });\n return { starts: null, diagnostics };\n }\n\n /*\n A record with no readable time is placed from the origin the other records establish,\n not from zero.\n\n `i * recordDuration` assumed the recording began at zero, which is the one thing the\n other records are in a position to contradict: a file starting at 0.5s put its\n unreadable record at 0.000 while its neighbours sat at 1.5s and 2.5s. The guess is still\n a guess — a discontinuous file may have a gap exactly there — and it is still reported\n below, but starting it from where the recording actually begins is strictly closer, and\n it makes an EDF+D file agree with its byte-identical EDF+C twin about record 0.\n */\n const base = originOf(annotationData.recordStarts, file.header.recordDuration) ?? 0;\n const starts = new Float64Array(file.recordCount);\n const missing: number[] = [];\n for (let i = 0; i < file.recordCount; i++) {\n const declared = annotationData.recordStarts[i];\n if (declared === null || declared === undefined) {\n missing.push(i);\n starts[i] = base + i * file.header.recordDuration;\n } else {\n starts[i] = declared;\n }\n }\n\n if (missing.length > 0) {\n /*\n Cut by the function that cuts every other list in a sentence here.\n\n This one rolled its own: five items and a bare `…`, where `listed` shows eight and counts\n what it left — \"and 32 more\" on the rate warning, \"and 112 more\" on the leftover files.\n The count is the honest half, and 0.7.58 taught the shared one not to hide a single item\n behind a phrase longer than the item. None of that reached this message, because it was\n never asking. Two implementations of one job, differing in the limit and in what they say\n about the tail.\n */\n const shown = listed(missing.map(String));\n // \"1 of 3 data records carry ... their true position\" — the subject is the one, not the three.\n const one = missing.length === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n message:\n `${missing.length} of ${counted(file.recordCount, 'data record')} ${one ? 'carries' : 'carry'} no ` +\n `readable timekeeping annotation (record${one ? '' : 's'} ${shown}), so ` +\n `${one ? 'its' : 'their'} true position in time is unknown.`,\n hint: one\n ? 'That record is timed as if it were contiguous; treat its timestamp as unreliable.'\n : 'Those records are timed as if they were contiguous; treat their timestamps as unreliable.',\n });\n }\n\n /*\n Furthest from zero, in either direction.\n\n This took the signed maximum and seeded it with 0, so a recording whose records all sit\n at negative onsets never got past the seed: `furthest` stayed 0, which any interval can\n carry. Then the samples collapsed anyway, because the arithmetic that defeats a large\n positive origin defeats a large negative one identically — at -1e16 seconds, adding a\n 1-second sample interval leaves the double unchanged.\n\n A four-record recording of eight samples wrote two rows, exit 0, no warning. Its\n byte-for-byte positive mirror wrote all eight and explained why it had to time them from\n zero. Same file, same failure, opposite sign, opposite outcome — and the silent one is\n the one that loses data, which is exactly what unusableOrigin exists to prevent.\n */\n let furthest = 0;\n for (const start of starts) if (Math.abs(start) > Math.abs(furthest)) furthest = start;\n if (!canCarry(furthest, file)) {\n diagnostics.push(unusableOrigin(furthest, file));\n return { starts: null, diagnostics };\n }\n\n /*\n Two ways a record can put the time column out of order, and only one was being looked for.\n\n A record starting before the one before it is the obvious case. The other is a record\n starting before the one before it *ends*: starts of 0, 0.5 and 1.0 on one-second records\n are strictly increasing, so nothing fired, and the rows still came out 0.25, 0.5, 0.75,\n 0.5 — because record 0's samples run to 0.75 while record 1 begins at 0.5. Overlapping\n acquisition is what a device does when it re-sends a buffer, and the reader has no more\n to say about it than about the reversed case: every sample is written, in file order,\n with the time the file gives it.\n\n Contiguity is not overlap. A continuous recording has `starts[i] === starts[i-1] +\n duration` exactly, so the comparison is made strict by a fraction of the finest interval\n the recording can express — the same measure the origin check uses.\n */\n const slack = finestInterval(file) / 2;\n let outOfOrder = 0;\n let overlapping = 0;\n for (let i = 1; i < starts.length; i++) {\n const previous = starts[i - 1] as number;\n const current = starts[i] as number;\n if (current < previous) outOfOrder++;\n else if (current + slack < previous + file.header.recordDuration) overlapping++;\n }\n if (outOfOrder > 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `${counted(outOfOrder, 'data record')} ` +\n `${outOfOrder === 1 ? 'starts' : 'start'} earlier than the record before ` +\n `${outOfOrder === 1 ? 'it' : 'them'}.`,\n hint: 'Rows are written in file order, so the time column will not increase monotonically.',\n });\n }\n if (overlapping > 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `${counted(overlapping, 'data record')} ` +\n `${overlapping === 1 ? 'starts' : 'start'} before the record before ` +\n `${overlapping === 1 ? 'it' : 'them'} ends, so ${overlapping === 1 ? 'its' : 'their'} ` +\n `samples overlap in time.`,\n hint: 'Rows are written in file order, so the time column will not increase monotonically.',\n });\n }\n\n return { starts, diagnostics };\n}\n\n/**\n * The recording's origin, from the first record that states where it is.\n *\n * Records of a continuous recording sit end to end, so record `i` beginning at `t` puts the\n * origin at `t - i * duration`. Any one readable timekeeping TAL is therefore enough, which\n * is what stops one unreadable entry from costing the whole file its position in time.\n */\nfunction originOf(recordStarts: readonly (number | null)[], recordDuration: number): number | null {\n for (const [index, declared] of recordStarts.entries()) {\n if (typeof declared !== 'number') continue;\n const origin = declared - index * recordDuration;\n return Number.isFinite(origin) ? origin : null;\n }\n return null;\n}\n\n/**\n * Whether times this far out can still tell one sample from the next.\n *\n * A double spaces its values further apart the larger they get: at 1e16 the gap is 2\n * seconds, so `t + 1` is `t`. Past that point a recording's declared origin stops being a\n * position and becomes a wall — the arithmetic that places records and samples returns the\n * origin itself, whatever is added to it.\n *\n * The finest thing that has to survive is the gap between two consecutive samples of the\n * fastest channel, since that is what the time column is made of. If that survives, so does\n * a whole record.\n */\nfunction canCarry(origin: number, file: EdfFile): boolean {\n if (!Number.isFinite(origin)) return false;\n // Asked of the origin furthest from zero, whichever side it is on: the spacing of doubles\n // grows with magnitude, not with value, so -1e16 and +1e16 fail this identically.\n return origin + finestInterval(file) > origin;\n}\n\n/**\n * The shortest span this recording can tell apart: one sample of its fastest channel.\n *\n * The time column is made of these, so nothing below one is a distinction the file is in a\n * position to make — which is what makes it the right size for both the \"can this origin\n * still separate two samples\" question and the \"is this record really somewhere else\"\n * question.\n */\nfunction finestInterval(file: EdfFile): number {\n let interval = file.header.recordDuration;\n for (const signal of file.header.signals) {\n if (signal.isAnnotations || !(signal.samplesPerRecord > 0)) continue;\n const step = file.header.recordDuration / signal.samplesPerRecord;\n if (step > 0 && step < interval) interval = step;\n }\n return interval;\n}\n\n/**\n * An origin the file's own arithmetic cannot express, reported rather than acted on.\n *\n * Two things went wrong when this was taken at face value, both of them quiet. A file whose\n * records all collapsed onto one instant made the recording zero seconds long, and the\n * window resolver — which had no reason to suspect the recording rather than the request —\n * blamed a flag nobody had passed:\n *\n * error: --start 100000000000000000s is at or past the end of this\n * 100000000000000000s recording.\n *\n * Slightly below that, the collapse is partial: `records[i].start + recordDuration` equals\n * the start again, so the test for \"does this record overlap the window\" fails for every\n * record whose neighbour rounded onto it. A twelve-row recording wrote four rows, exit 0,\n * no warning — the eight that vanished looked exactly like a file that never had them.\n *\n * Timing from zero is what the file did before 0.4.9 taught it to honour the first\n * timekeeping TAL, and at this magnitude it is the only column that can hold distinct\n * values. The origin is lost, so this says so.\n */\nfunction unusableOrigin(origin: number, file: EdfFile): Diagnostic {\n /*\n `Infinity` is a token, not a distance.\n\n `plain` expands exponent notation and hands anything else back, so a non-finite origin\n arrived on screen as \"place it Infinitys from its own start date\" — the exact form\n `formatDuration` exists to keep off the screen, its comment naming \"NaNs\" and \"Infinitys\"\n as what the fallback used to print, and the word the rest of this tool uses for the same\n quantity is \"unknown\". The origin overflows for the reason the recording's length does:\n a record duration near the top of a double, multiplied out over three records, leaves it.\n */\n const away = Number.isFinite(origin)\n ? `${plain(origin)}s from its own start date`\n : 'further from its own start date than a number can hold';\n return {\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `This recording's timekeeping annotations place it ${away}, which is too far out for ` +\n `its ${plain(file.header.recordDuration)}s records to be told apart: at that magnitude ` +\n `adding a sample interval leaves the number unchanged.`,\n hint:\n 'Sample times are written from zero instead, so every row is present and the column ' +\n 'increases. Add the onsets in annotations.csv to recover absolute times if you need them.',\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"timing.js","sourceRoot":"","sources":["../../src/convert/timing.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Diagnostic } from '../edf/errors.js';\nimport type { EdfFile } from '../edf/reader.js';\nimport { counted, listed } from '../format/list.js';\nimport { plain } from '../format/number.js';\n\nexport interface AnnotationTimingData {\n recordStarts: (number | null)[];\n malformed: number;\n /** Unreadable TALs in first position, which carry timing rather than an event. */\n malformedTimekeeping?: number;\n malformedTimekeepingWithText?: number;\n}\n\n/**\n * The EDF+D warning's promise, withdrawn when the file cannot keep it.\n *\n * The header parser raises DISCONTINUOUS with the hint \"Each row carries its true recording\n * time, so gaps stay visible instead of being closed\" — which is what an EDF+D conversion\n * does, when the record times can be read. When they cannot, the very next warning in the\n * same run says the opposite: \"Times are written as if the records were contiguous. Any gaps\n * are lost.\" Two warnings, printed together, and the second denies the first.\n *\n * The parser cannot know: whether the starts can be derived is settled here, after the\n * annotation channel has been read. So the hint is amended where the answer is, the same way\n * `withoutFileRateWarning` drops a header diagnostic the plan has superseded.\n */\nexport function withTimingPromiseKept(\n diagnostics: readonly Diagnostic[],\n derived: boolean,\n): Diagnostic[] {\n if (derived) return [...diagnostics];\n return diagnostics.map((d) =>\n d.code === 'DISCONTINUOUS' && d.hint?.includes('gaps stay visible')\n ? {\n ...d,\n hint:\n 'Where its records sit in time is not recorded in this file, so they are written ' +\n 'as if contiguous — see the warning below.',\n }\n : d,\n );\n}\n\n/**\n * Resolve the true start time of every data record.\n *\n * Continuous recordings need no table because their record positions are\n * arithmetic. EDF+D recordings carry their positions in the annotation channel;\n * missing or malformed timekeeping entries are reported before falling back.\n */\nexport function deriveRecordStarts(\n file: EdfFile,\n annotationData: AnnotationTimingData,\n): { starts: Float64Array | null; diagnostics: Diagnostic[] } {\n const diagnostics: Diagnostic[] = [];\n\n if (annotationData.malformed > 0) {\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n // Through `counted`, with the irregular plural spelled out — which is the case its\n // own comment names and which nothing was using it for.\n message:\n `${counted(annotationData.malformed, 'annotation entry', 'annotation entries')} ` +\n `${annotationData.malformed === 1 ? 'was' : 'were'} unreadable and could not be exported.`,\n hint: 'The rest were exported normally. The file may have been written by a non-conforming tool.',\n });\n }\n\n\n /*\n A timekeeping TAL is not an event, and saying it \"could not be exported\" describes the\n wrong loss twice over.\n\n These were counted among the annotations, so a file with one unreadable timekeeping TAL\n and three good events announced \"1 annotation entry was unreadable and could not be\n exported\" — while exporting all three. Nothing was missing from annotations.csv; what\n went missing was a record's position in time, which the message never mentioned.\n */\n const lostTimekeeping = annotationData.malformedTimekeeping ?? 0;\n const withText = annotationData.malformedTimekeepingWithText ?? 0;\n // The EDF+D branch below raises its own, which names the records and is more specific.\n // Saying both would report one problem twice.\n if (lostTimekeeping > 0 && file.header.continuity !== 'EDF+D') {\n const one = lostTimekeeping === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n message:\n `${lostTimekeeping} data record${one ? '' : 's'} carr${one ? 'ies' : 'y'} a timekeeping ` +\n `annotation that could not be read, so ${one ? 'it does' : 'they do'} not say where in ` +\n `time ${one ? 'it sits' : 'they sit'}.`,\n hint:\n /*\n \"No event was lost\" was said whatever the entry held.\n\n A first-position TAL states the record's start time and may carry events after it,\n which the specification allows and writers do. When one of those cannot be parsed\n the events go with it: a six-event file came out with two, and the only warning\n about it said in so many words that nothing had gone. Those entries are counted\n among the unreadable ones now, which is the warning printed above this one; this\n sentence keeps to what is true of the entries it is actually about.\n */\n (withText > 0\n ? `${withText === 1 ? 'One of them' : `${withText} of them`} also carried event text, ` +\n `which went with ${withText === 1 ? 'it' : 'them'} and is counted above. A ` +\n `timekeeping annotation itself states a record's start time and is never exported. `\n : 'No event was lost — a timekeeping annotation states a record\\'s start time and ' +\n 'is never exported. ') + 'Times are derived from the records that could be read.',\n });\n }\n\n /*\n A continuous recording's records are contiguous, but the first one need not sit at zero.\n\n EDF+ puts the header's start time and every annotation onset on one origin, and says the\n first data record's timekeeping TAL \"always starts with +0.X\", stating the fraction of a\n second by which that record follows it. Ignoring that fraction timed the samples from 0\n while the events kept their true onsets, so the two ended up on origins half a second\n apart — an event at +0.75 in a 4 Hz recording whose first TAL reads +0.5 landed on sample\n 3 instead of sample 1. The same file marked EDF+D, byte-identical but for the reserved\n field, placed it correctly, which is what gives the omission away.\n\n Records stay contiguous, which is what continuous means: only the origin moves. A first\n TAL of +0 needs no table at all, and that is nearly every file.\n */\n if (file.header.continuity !== 'EDF+D') {\n if (file.header.continuity !== 'EDF+C') {\n /*\n An annotation channel the reserved field never claimed.\n\n Without an `EDF+C` or `EDF+D` marker this is a plain EDF file, so the origin is not\n applied and the samples are timed from zero. The annotation channel is found by label\n rather than by the marker, though, so its events are still read and exported — with\n the onsets the file gives them.\n\n On a file whose timekeeping says the records start at 1000s, that put signals.csv at\n 0.000 and the event at 1000.5 in annotations.csv: two files from one conversion, a\n thousand seconds apart, and nothing said so. output-files promises the opposite —\n \"`onset_s` is on the same clock as `time_s` in the signal files\".\n\n Reported rather than repaired. Which clock is right is not knowable from here: the\n marker says plain EDF and the annotation channel says otherwise, and picking one would\n move either the samples or the events by the origin on a guess.\n */\n const stated = annotationData.recordStarts.find((start) => start !== null) ?? null;\n if (stated !== null && Math.abs(stated) > 0) {\n diagnostics.push({\n code: 'MISSING_EDF_PLUS_MARKER',\n severity: 'warning',\n message:\n `This file has an annotation channel stating that its records begin at ` +\n `${plain(stated)}s, but its reserved field carries no EDF+C or EDF+D marker — so ` +\n `it is read as plain EDF, time_s counts from zero, and the two disagree by ` +\n `${plain(stated)}s.`,\n hint:\n 'annotations.csv keeps the onsets the file gives, so its events and signals.csv ' +\n 'are on different clocks. Mark the file EDF+C, or subtract the offset from the ' +\n 'onsets, before joining them.',\n });\n }\n return { starts: null, diagnostics };\n }\n\n /*\n The origin comes from whichever record first states one, not from record 0 alone.\n\n Reading only `recordStarts[0]` meant a single unreadable timekeeping TAL threw the\n origin away and timed the whole file from zero — while records 1 and 2, saying plainly\n that they start at 1.5s and 2.5s, went unread. A recording whose records sit at 0.5s,\n 1.5s and 2.5s came out with every sample 0.5s earlier than the file states, against\n annotation onsets that kept their true values. That is precisely the mismatch 0.4.9\n fixed, arriving through the one hole left in it, and the byte-identical EDF+D twin\n timed it correctly, which is what gives it away.\n\n Continuity is what makes this recoverable: record i sits at `origin + i * duration`,\n so any readable record determines the origin for all of them.\n */\n const origin = originOf(annotationData.recordStarts, file.header.recordDuration);\n if (origin === null) return { starts: null, diagnostics };\n\n const contiguous = new Float64Array(file.recordCount);\n for (let i = 0; i < file.recordCount; i++) {\n contiguous[i] = origin + i * file.header.recordDuration;\n }\n\n /*\n A file marked continuous whose own records disagree about it.\n\n Nothing looked at records past the first, so an EDF+C file whose records are in fact\n spread out was timed as though they were contiguous and said nothing. The records are\n being read here anyway, so the contradiction costs nothing to notice — and it is the\n file, not the reader, that has to be wrong for this to fire.\n\n Compared against what the file can express, not for equality. 0.4.41 asked whether the\n two doubles were the same, which they are not: a recording of 0.1s records sitting at\n 0.1, 0.2, 0.3 ... is contiguous by construction, and 0.1 + 2 * 0.1 is\n 0.30000000000000004. Two of its eight records were reported as contradicting\n continuity, on an ordinary file — and under --strict that was a failed run. The\n smallest interval the recording distinguishes is one sample of its fastest channel;\n anything below half of that is arithmetic, not a gap. `canCarry` has already refused\n origins where the double spacing swamps that interval, so the representation error is\n under the tolerance by construction rather than by hope.\n */\n const tolerance = finestInterval(file) / 2;\n const contradicting = annotationData.recordStarts.filter(\n (declared, i) =>\n typeof declared === 'number' && Math.abs(declared - (contiguous[i] as number)) > tolerance,\n ).length;\n if (contradicting > 0) {\n const continuous = file.header.isBdf ? 'BDF+C' : 'EDF+C';\n const discontinuous = file.header.isBdf ? 'BDF+D' : 'EDF+D';\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n /*\n The markers as the file spells them.\n\n `continuity` normalises `BDF+C` to the internal `EDF+C` tag, and that tag reached\n the message: a BDF+ recording was told it is \"marked continuous (EDF+C)\" — a string\n it does not contain — and advised that it \"should have been marked EDF+D\", which is\n not a value BDF+ defines. A reader grepping the header for either finds nothing.\n\n The sibling discontinuous warning has done this since 0.3.x: `${'$'}{isBdf ? 'BDF+D' :\n 'EDF+D'}`. Same code, same header field, and the continuous branch never got it.\n */\n message:\n `This file is marked continuous (${continuous}), but ${contradicting} of its ` +\n `${counted(file.recordCount, 'data record')} ` +\n `${contradicting === 1 ? 'says it starts' : 'say they start'} somewhere other than ` +\n `where continuity puts ${contradicting === 1 ? 'it' : 'them'}.`,\n hint:\n `Times are written as if the records were contiguous, which is what ${continuous} ` +\n `means. If the recording really has gaps, the file should have been marked ` +\n `${discontinuous}.`,\n });\n }\n const first = origin;\n const last = contiguous[file.recordCount - 1] ?? first;\n if (!canCarry(last, file)) {\n diagnostics.push(unusableOrigin(first, file));\n return { starts: null, diagnostics };\n }\n /*\n An origin of zero is the same as no origin, for timing. It is not the same for the\n check above.\n\n This returned early on `origin === 0`, which is right about the times — contiguous\n starts from zero are what timing from zero already produces — and skipped the\n contradiction check on the way past. So an EDF+C file whose records say 0, 5 and 10 on\n one-second records went unreported, while the same file shifted one second, saying 1, 6\n and 11, was reported. The contradiction is in records 1 and 2 either way; where record 0\n happens to sit decides nothing about it.\n */\n if (origin === 0) return { starts: null, diagnostics };\n return { starts: contiguous, diagnostics };\n }\n\n if (file.annotationSignals.length === 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n 'This file is marked discontinuous but has no annotation channel, so where its ' +\n 'records sit in time is not recorded anywhere.',\n hint: 'Times are written as if the records were contiguous. Any gaps are lost.',\n });\n return { starts: null, diagnostics };\n }\n\n /*\n A record with no readable time is placed from the origin the other records establish,\n not from zero.\n\n `i * recordDuration` assumed the recording began at zero, which is the one thing the\n other records are in a position to contradict: a file starting at 0.5s put its\n unreadable record at 0.000 while its neighbours sat at 1.5s and 2.5s. The guess is still\n a guess — a discontinuous file may have a gap exactly there — and it is still reported\n below, but starting it from where the recording actually begins is strictly closer, and\n it makes an EDF+D file agree with its byte-identical EDF+C twin about record 0.\n */\n const base = originOf(annotationData.recordStarts, file.header.recordDuration) ?? 0;\n const starts = new Float64Array(file.recordCount);\n const missing: number[] = [];\n for (let i = 0; i < file.recordCount; i++) {\n const declared = annotationData.recordStarts[i];\n if (declared === null || declared === undefined) {\n missing.push(i);\n starts[i] = base + i * file.header.recordDuration;\n } else {\n starts[i] = declared;\n }\n }\n\n if (missing.length > 0) {\n /*\n Cut by the function that cuts every other list in a sentence here.\n\n This one rolled its own: five items and a bare `…`, where `listed` shows eight and counts\n what it left — \"and 32 more\" on the rate warning, \"and 112 more\" on the leftover files.\n The count is the honest half, and 0.7.58 taught the shared one not to hide a single item\n behind a phrase longer than the item. None of that reached this message, because it was\n never asking. Two implementations of one job, differing in the limit and in what they say\n about the tail.\n */\n const shown = listed(missing.map(String));\n // \"1 of 3 data records carry ... their true position\" — the subject is the one, not the three.\n const one = missing.length === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n message:\n `${missing.length} of ${counted(file.recordCount, 'data record')} ${one ? 'carries' : 'carry'} no ` +\n `readable timekeeping annotation (record${one ? '' : 's'} ${shown}), so ` +\n `${one ? 'its' : 'their'} true position in time is unknown.`,\n hint: one\n ? 'That record is timed as if it were contiguous; treat its timestamp as unreliable.'\n : 'Those records are timed as if they were contiguous; treat their timestamps as unreliable.',\n });\n }\n\n /*\n Furthest from zero, in either direction.\n\n This took the signed maximum and seeded it with 0, so a recording whose records all sit\n at negative onsets never got past the seed: `furthest` stayed 0, which any interval can\n carry. Then the samples collapsed anyway, because the arithmetic that defeats a large\n positive origin defeats a large negative one identically — at -1e16 seconds, adding a\n 1-second sample interval leaves the double unchanged.\n\n A four-record recording of eight samples wrote two rows, exit 0, no warning. Its\n byte-for-byte positive mirror wrote all eight and explained why it had to time them from\n zero. Same file, same failure, opposite sign, opposite outcome — and the silent one is\n the one that loses data, which is exactly what unusableOrigin exists to prevent.\n */\n let furthest = 0;\n for (const start of starts) if (Math.abs(start) > Math.abs(furthest)) furthest = start;\n if (!canCarry(furthest, file)) {\n diagnostics.push(unusableOrigin(furthest, file));\n return { starts: null, diagnostics };\n }\n\n /*\n Two ways a record can put the time column out of order, and only one was being looked for.\n\n A record starting before the one before it is the obvious case. The other is a record\n starting before the one before it *ends*: starts of 0, 0.5 and 1.0 on one-second records\n are strictly increasing, so nothing fired, and the rows still came out 0.25, 0.5, 0.75,\n 0.5 — because record 0's samples run to 0.75 while record 1 begins at 0.5. Overlapping\n acquisition is what a device does when it re-sends a buffer, and the reader has no more\n to say about it than about the reversed case: every sample is written, in file order,\n with the time the file gives it.\n\n Contiguity is not overlap. A continuous recording has `starts[i] === starts[i-1] +\n duration` exactly, so the comparison is made strict by a fraction of the finest interval\n the recording can express — the same measure the origin check uses.\n */\n const slack = finestInterval(file) / 2;\n let outOfOrder = 0;\n let overlapping = 0;\n for (let i = 1; i < starts.length; i++) {\n const previous = starts[i - 1] as number;\n const current = starts[i] as number;\n if (current < previous) outOfOrder++;\n else if (current + slack < previous + file.header.recordDuration) overlapping++;\n }\n if (outOfOrder > 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `${counted(outOfOrder, 'data record')} ` +\n `${outOfOrder === 1 ? 'starts' : 'start'} earlier than the record before ` +\n `${outOfOrder === 1 ? 'it' : 'them'}.`,\n hint: 'Rows are written in file order, so the time column will not increase monotonically.',\n });\n }\n if (overlapping > 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `${counted(overlapping, 'data record')} ` +\n `${overlapping === 1 ? 'starts' : 'start'} before the record before ` +\n `${overlapping === 1 ? 'it' : 'them'} ends, so ${overlapping === 1 ? 'its' : 'their'} ` +\n `samples overlap in time.`,\n hint: 'Rows are written in file order, so the time column will not increase monotonically.',\n });\n }\n\n return { starts, diagnostics };\n}\n\n/**\n * The recording's origin, from the first record that states where it is.\n *\n * Records of a continuous recording sit end to end, so record `i` beginning at `t` puts the\n * origin at `t - i * duration`. Any one readable timekeeping TAL is therefore enough, which\n * is what stops one unreadable entry from costing the whole file its position in time.\n */\nfunction originOf(recordStarts: readonly (number | null)[], recordDuration: number): number | null {\n for (const [index, declared] of recordStarts.entries()) {\n if (typeof declared !== 'number') continue;\n const origin = declared - index * recordDuration;\n return Number.isFinite(origin) ? origin : null;\n }\n return null;\n}\n\n/**\n * Whether times this far out can still tell one sample from the next.\n *\n * A double spaces its values further apart the larger they get: at 1e16 the gap is 2\n * seconds, so `t + 1` is `t`. Past that point a recording's declared origin stops being a\n * position and becomes a wall — the arithmetic that places records and samples returns the\n * origin itself, whatever is added to it.\n *\n * The finest thing that has to survive is the gap between two consecutive samples of the\n * fastest channel, since that is what the time column is made of. If that survives, so does\n * a whole record.\n */\nfunction canCarry(origin: number, file: EdfFile): boolean {\n if (!Number.isFinite(origin)) return false;\n // Asked of the origin furthest from zero, whichever side it is on: the spacing of doubles\n // grows with magnitude, not with value, so -1e16 and +1e16 fail this identically.\n return origin + finestInterval(file) > origin;\n}\n\n/**\n * The shortest span this recording can tell apart: one sample of its fastest channel.\n *\n * The time column is made of these, so nothing below one is a distinction the file is in a\n * position to make — which is what makes it the right size for both the \"can this origin\n * still separate two samples\" question and the \"is this record really somewhere else\"\n * question.\n */\nfunction finestInterval(file: EdfFile): number {\n let interval = file.header.recordDuration;\n for (const signal of file.header.signals) {\n if (signal.isAnnotations || !(signal.samplesPerRecord > 0)) continue;\n const step = file.header.recordDuration / signal.samplesPerRecord;\n if (step > 0 && step < interval) interval = step;\n }\n return interval;\n}\n\n/**\n * An origin the file's own arithmetic cannot express, reported rather than acted on.\n *\n * Two things went wrong when this was taken at face value, both of them quiet. A file whose\n * records all collapsed onto one instant made the recording zero seconds long, and the\n * window resolver — which had no reason to suspect the recording rather than the request —\n * blamed a flag nobody had passed:\n *\n * error: --start 100000000000000000s is at or past the end of this\n * 100000000000000000s recording.\n *\n * Slightly below that, the collapse is partial: `records[i].start + recordDuration` equals\n * the start again, so the test for \"does this record overlap the window\" fails for every\n * record whose neighbour rounded onto it. A twelve-row recording wrote four rows, exit 0,\n * no warning — the eight that vanished looked exactly like a file that never had them.\n *\n * Timing from zero is what the file did before 0.4.9 taught it to honour the first\n * timekeeping TAL, and at this magnitude it is the only column that can hold distinct\n * values. The origin is lost, so this says so.\n */\nfunction unusableOrigin(origin: number, file: EdfFile): Diagnostic {\n /*\n `Infinity` is a token, not a distance.\n\n `plain` expands exponent notation and hands anything else back, so a non-finite origin\n arrived on screen as \"place it Infinitys from its own start date\" — the exact form\n `formatDuration` exists to keep off the screen, its comment naming \"NaNs\" and \"Infinitys\"\n as what the fallback used to print, and the word the rest of this tool uses for the same\n quantity is \"unknown\". The origin overflows for the reason the recording's length does:\n a record duration near the top of a double, multiplied out over three records, leaves it.\n */\n const away = Number.isFinite(origin)\n ? `${plain(origin)}s from its own start date`\n : 'further from its own start date than a number can hold';\n return {\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `This recording's timekeeping annotations place it ${away}, which is too far out for ` +\n `its ${plain(file.header.recordDuration)}s records to be told apart: at that magnitude ` +\n `adding a sample interval leaves the number unchanged.`,\n hint:\n 'Sample times are written from zero instead, so every row is present and the column ' +\n 'increases. Add the onsets in annotations.csv to recover absolute times if you need them.',\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "edf2csv",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.244",
|
|
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",
|