edf2csv 0.7.119 → 0.7.121
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.d.ts
CHANGED
|
@@ -19,6 +19,14 @@ import type { ConvertResult } from '../convert/run.js';
|
|
|
19
19
|
* are file paths and quoted channel labels, and neither survives being split across lines:
|
|
20
20
|
* the point of printing a path is that it can be copied back out.
|
|
21
21
|
*
|
|
22
|
+
* A quoted span is one word for the same reason, however many spaces are inside it. The
|
|
23
|
+
* sentence above is the promise, and it held only for the paths that have no spaces in them:
|
|
24
|
+
* an interrupted conversion said `Files already written to "/tmp/a very long destination
|
|
25
|
+
* folder name with` / `many spaces here indeed" are incomplete`, splitting the one thing on
|
|
26
|
+
* the line that has to be copied whole — and quoting is what a path with spaces in it is
|
|
27
|
+
* given in the first place. A word wider than the column already overruns rather than
|
|
28
|
+
* breaking; this is that rule reaching the words it was written for.
|
|
29
|
+
*
|
|
22
30
|
* Which is also why the gap between two words is reproduced rather than normalised to one
|
|
23
31
|
* space. Splitting on `\s+` and rejoining with `' '` re-flowed everything, and a quoted value
|
|
24
32
|
* is not prose: a channel labelled `EEG A` was offered back as `Did you mean "EEG A"?`, and
|
package/dist/cli/report.js
CHANGED
|
@@ -13,6 +13,15 @@ import { VERSION } from '../version.js';
|
|
|
13
13
|
const WRAP_COLUMNS = 80;
|
|
14
14
|
/** The continuation indent under a `warning: ` / `note: ` prefix. */
|
|
15
15
|
const HINT_INDENT = ' '.repeat(9);
|
|
16
|
+
/**
|
|
17
|
+
* One unbreakable unit of a wrapped line: a quoted span, or a run of non-space.
|
|
18
|
+
*
|
|
19
|
+
* The lookbehind is what keeps an apostrophe out of it. `the file's own header and it's` has
|
|
20
|
+
* two of them, and without the guard `'s own header and it'` is one quoted span — a token
|
|
21
|
+
* with spaces in it that then refuses to break. A quote opening a span is at the start of a
|
|
22
|
+
* word, so that is what is required of it.
|
|
23
|
+
*/
|
|
24
|
+
const WORD = /(?<=^|[\s(])(?:"[^"]*"|'[^']*')|\S+/gu;
|
|
16
25
|
/**
|
|
17
26
|
* Greedy word wrap, `indent` on every line including the first.
|
|
18
27
|
*
|
|
@@ -24,6 +33,14 @@ const HINT_INDENT = ' '.repeat(9);
|
|
|
24
33
|
* are file paths and quoted channel labels, and neither survives being split across lines:
|
|
25
34
|
* the point of printing a path is that it can be copied back out.
|
|
26
35
|
*
|
|
36
|
+
* A quoted span is one word for the same reason, however many spaces are inside it. The
|
|
37
|
+
* sentence above is the promise, and it held only for the paths that have no spaces in them:
|
|
38
|
+
* an interrupted conversion said `Files already written to "/tmp/a very long destination
|
|
39
|
+
* folder name with` / `many spaces here indeed" are incomplete`, splitting the one thing on
|
|
40
|
+
* the line that has to be copied whole — and quoting is what a path with spaces in it is
|
|
41
|
+
* given in the first place. A word wider than the column already overruns rather than
|
|
42
|
+
* breaking; this is that rule reaching the words it was written for.
|
|
43
|
+
*
|
|
27
44
|
* Which is also why the gap between two words is reproduced rather than normalised to one
|
|
28
45
|
* space. Splitting on `\s+` and rejoining with `' '` re-flowed everything, and a quoted value
|
|
29
46
|
* is not prose: a channel labelled `EEG A` was offered back as `Did you mean "EEG A"?`, and
|
|
@@ -35,14 +52,12 @@ const HINT_INDENT = ' '.repeat(9);
|
|
|
35
52
|
export function wrap(text, indent = '', width = WRAP_COLUMNS) {
|
|
36
53
|
const lines = [];
|
|
37
54
|
let line = indent;
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
continue;
|
|
45
|
-
else if (line === indent)
|
|
55
|
+
let at = 0;
|
|
56
|
+
for (const match of text.matchAll(WORD)) {
|
|
57
|
+
const piece = match[0];
|
|
58
|
+
const gap = text.slice(at, match.index);
|
|
59
|
+
at = match.index + piece.length;
|
|
60
|
+
if (line === indent)
|
|
46
61
|
line += piece;
|
|
47
62
|
else if (line.length + gap.length + piece.length <= width)
|
|
48
63
|
line += gap + piece;
|
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: ` / `note: ` prefix. */\nconst HINT_INDENT = ' '.repeat(9);\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 * 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 gap = ' ';\n // Odd pieces are the separators, since the pattern is captured.\n for (const [index, piece] of text.split(/(\\s+)/u).entries()) {\n if (index % 2 === 1) gap = piece;\n else if (piece === '') continue;\n else 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 === 'warning' ? 'warning' : 'note'}: ${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: ` / `note: ` prefix. */\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 === 'warning' ? 'warning' : 'note'}: ${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"]}
|
|
@@ -76,6 +76,23 @@ export function parseTimeSpec(input, optionName, allowNegative = false) {
|
|
|
76
76
|
*/
|
|
77
77
|
const negative = trimmed.startsWith('-');
|
|
78
78
|
const text = negative ? trimmed.slice(1) : trimmed;
|
|
79
|
+
/*
|
|
80
|
+
A leading plus, named as such.
|
|
81
|
+
|
|
82
|
+
Refusing it is deliberate — see the paragraph above — but it came back as "is not a time I
|
|
83
|
+
understand. Try 30s, 5m, 1h30m, 00:30:00, or a plain number of seconds", which is the
|
|
84
|
+
message for input that could not be read at all. Every part of `+5s` was read: the number
|
|
85
|
+
parsed, the unit was looked up and found. The one thing wrong with it is a character the
|
|
86
|
+
reader put there on purpose, and they were sent to re-check their unit spellings with `5m`
|
|
87
|
+
sitting in the list of suggestions, differing from what they typed by the sign they cannot
|
|
88
|
+
see is the problem. 0.7.102 made this argument about a space; this is the same sentence
|
|
89
|
+
about the other character that reaches it. `--decimals` and `--jobs` both quote the value
|
|
90
|
+
back and say what is wrong with it.
|
|
91
|
+
*/
|
|
92
|
+
if (text.startsWith('+')) {
|
|
93
|
+
throw new TimeRangeError(`${optionName} "${input}" begins with a plus. Write the number on its own: ` +
|
|
94
|
+
`${input.trim().replace('+', '')}`);
|
|
95
|
+
}
|
|
79
96
|
const signed = (seconds) => assertFinite(negative ? -seconds : seconds, optionName, input, allowNegative);
|
|
80
97
|
// Bare number means seconds.
|
|
81
98
|
if (/^\d+(?:\.\d+)?$/u.test(text))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-range.js","sourceRoot":"","sources":["../../src/convert/time-range.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Parsing for the time-range options.\n *\n * Researchers write offsets in whatever form is natural for the recording in front\n * of them: seconds for a short ECG strip, `30m` into a sleep study, `01:23:45` when\n * reading off a clock. All three are accepted; anything ambiguous is rejected with\n * a message that shows the forms that work.\n */\n\nimport { fixed, formatDuration } from '../format/number.js';\n\nexport class TimeRangeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'TimeRangeError';\n }\n}\n\n/**\n * Every spelling of a unit this accepts, and what one of it is worth in seconds.\n *\n * Exported so the spellings can be enumerated by a test rather than listed a second time:\n * twelve of these sixteen are named nowhere in the tool or its documentation, and none of\n * their values was checked by anything. `hrs: 360` would have converted `--start 2hrs` from\n * twelve minutes in and said nothing, which is the one kind of mistake a window may not make.\n */\nexport const UNIT_SECONDS: Record<string, number> = {\n h: 3600,\n hr: 3600,\n hrs: 3600,\n hour: 3600,\n hours: 3600,\n m: 60,\n min: 60,\n mins: 60,\n minute: 60,\n minutes: 60,\n s: 1,\n sec: 1,\n secs: 1,\n second: 1,\n seconds: 1,\n ms: 0.001,\n};\n\nconst CLOCK = /^(?:(\\d+):)?(\\d{1,2}):(\\d{1,2}(?:\\.\\d+)?)$/u;\nconst UNIT_TOKEN = /(\\d+(?:\\.\\d+)?)\\s*([a-z]+)/giu;\n\n/**\n * Parse a duration or offset into seconds.\n *\n * Accepted: `90`, `90s`, `5m`, `1h30m`, `1h 30m 15s`, `00:30:00`, `30:00`, `250ms`.\n *\n * `allowNegative` is for the two options that name a position rather than a length.\n *\n * A recording is timed from its first record's timekeeping annotation, and nothing obliges that\n * to sit at or after zero: a file whose records run from -100 s to -97 s is one this tool reads,\n * times from -100, and describes with\n *\n * Timed from -100.000s (first sample; --start and --end use this clock)\n *\n * That line says the number can be typed straight back in, and it could not be. Every offset\n * such a recording has came back as \"not a time I understand\", so its whole clock was\n * unreachable and no window of it could be converted at all — the one file shape where a\n * window is refused for naming a moment the recording actually contains.\n *\n * A length below zero is still a different thing, and `--duration` still refuses one.\n */\nexport function parseTimeSpec(input: string, optionName: string, allowNegative = false): number {\n const trimmed = input.trim().toLowerCase();\n if (trimmed === '') {\n throw new TimeRangeError(`${optionName} is empty. Try a value like 30s, 5m, or 00:30:00.`);\n }\n\n /*\n The sign is read off the front and applied to the whole value, so `-1h30m` is an hour and a\n half before the origin rather than an hour before it and half an hour after.\n\n Nothing may sit between the sign and the number, and a leading `+` is still refused: every\n other number this tool takes is refused when written in a form nobody types, and `+5` is\n exactly what `--decimals` and `--jobs` reject one flag over.\n */\n const negative = trimmed.startsWith('-');\n const text = negative ? trimmed.slice(1) : trimmed;\n const signed = (seconds: number): number =>\n assertFinite(negative ? -seconds : seconds, optionName, input, allowNegative);\n\n // Bare number means seconds.\n if (/^\\d+(?:\\.\\d+)?$/u.test(text)) return signed(Number(text));\n\n // Clock form: hh:mm:ss or mm:ss.\n const clock = CLOCK.exec(text);\n if (clock) {\n const hours = clock[1] === undefined ? 0 : Number(clock[1]);\n const minutes = Number(clock[2]);\n const seconds = Number(clock[3]);\n if (minutes >= 60 || seconds >= 60) {\n throw new TimeRangeError(\n `${optionName} \"${input}\" has a minutes or seconds field of 60 or more.`,\n );\n }\n return signed(hours * 3600 + minutes * 60 + seconds);\n }\n\n // Unit form: 1h30m, 1h30m 15s, 250ms. A number sits directly against its unit; see below.\n UNIT_TOKEN.lastIndex = 0;\n let total = 0;\n let matched = 0;\n let consumed = 0;\n /** Whether any token had a space between its number and its unit. See the refusal below. */\n let spaced = false;\n let match: RegExpExecArray | null;\n // Which units have already been seen, so a repeat can be rejected rather than added.\n const seen = new Set<number>();\n while ((match = UNIT_TOKEN.exec(text)) !== null) {\n const amount = Number(match[1]);\n const unit = match[2] ?? '';\n const scale = UNIT_SECONDS[unit];\n if (scale === undefined) {\n throw new TimeRangeError(\n `${optionName} \"${input}\" uses an unknown unit \"${unit}\". Use h, m, s or ms, ` +\n `or their long forms: hours, minutes, seconds.`,\n );\n }\n\n // \"1h1h\" is a typo, not a request for two hours. Summing repeated units silently\n // turned a slip into a plausible window that was quietly the wrong length. Units are\n // keyed by their scale so the aliases collapse together: \"1h30min20m\" is caught too.\n if (seen.has(scale)) {\n throw new TimeRangeError(\n `${optionName} \"${input}\" gives the same unit twice. ` +\n `Combine each unit once, as in 1h30m.`,\n );\n }\n seen.add(scale);\n\n total += amount * scale;\n matched++;\n // Counted with the whitespace taken out, so the comparison below is against the same\n // string on both sides and a space inside a token reaches the refusal written for it\n // rather than the one about input that could not be read at all.\n consumed += match[0].replace(/\\s+/gu, '').length;\n if (/\\s/u.test(match[0])) spaced = true;\n }\n\n // Reject partially-understood input like \"5x\" or \"1h banana\".\n if (matched === 0 || consumed !== text.replace(/\\s+/gu, '').length) {\n throw new TimeRangeError(\n `${optionName} \"${input}\" is not a time I understand. ` +\n `Try 30s, 5m, 1h30m, 00:30:00, or a plain number of seconds.`,\n );\n }\n\n /*\n A space between a number and its unit, named as such.\n\n `5 min` is refused on purpose — cli-reference sets the rule out, and it is what keeps\n `1 2h` from being read as anything — but it came back as \"is not a time I understand. Try\n 30s, 5m, 1h30m, 00:30:00, or a plain number of seconds\", which is the message for input\n that could not be read at all. Every part of `5 min` was read: the number parsed, the unit\n was looked up in the table and found. The one thing wrong with it is a space, and the\n reader was sent to re-check their unit spellings instead — with `5m` sitting in the list of\n suggestions, differing from what they typed by a character they cannot see is the problem.\n\n Only reachable now that the count above ignores whitespace; before, the length check caught\n these first and there was nothing left to tell them apart by.\n */\n if (spaced) {\n throw new TimeRangeError(\n `${optionName} \"${input}\" puts a space between a number and its unit. ` +\n `Write them together: ${input.trim().replace(/(\\d)\\s+([a-z])/giu, '$1$2')}`,\n );\n }\n\n return signed(total);\n}\n\nfunction assertFinite(\n value: number,\n optionName: string,\n input: string,\n allowNegative: boolean,\n): number {\n if (!Number.isFinite(value) || (!allowNegative && value < 0)) {\n throw new TimeRangeError(`${optionName} \"${input}\" is not a valid non-negative time.`);\n }\n return value;\n}\n\nexport interface ResolvedRange {\n /** Inclusive start, in seconds from the beginning of the recording. */\n startSeconds: number;\n /** Exclusive end, in seconds. */\n endSeconds: number;\n /** First data record touching the window. */\n startRecord: number;\n /** One past the last data record touching the window. */\n endRecord: number;\n /** True when the window covers the whole recording. */\n isWholeRecording: boolean;\n /** Earliest record start, including EDF+D timing gaps. */\n recordingStartSeconds: number;\n /** End of the latest record, including EDF+D timing gaps. */\n recordingEndSeconds: number;\n}\n\n/**\n * Slack for comparisons at sample/window boundaries.\n *\n * A nanosecond is far below any real sampling interval — 20 kHz is 50 microseconds — so it\n * absorbs the arithmetic error in `recordStart + sample / rate` without reaching a\n * neighbouring sample.\n */\nexport const BOUNDARY_TOLERANCE = 1e-9;\n\n/**\n * The slack to use for a channel sampled this often.\n *\n * Never as much as half a sample interval, because slack that reaches the next sample stops\n * being slack. A fixed nanosecond was applied whatever the rate, and the format does not\n * oblige the interval to be larger than it: EDF's record duration is an 8-character field\n * that accepts `1e-9`. A recording of two 1 ns records holding ten samples each wrote ten of\n * its twenty rows — the window ends at 2e-9, the comparison asked for `time < 2e-9 - 1e-9`,\n * and the entire second record failed it. Exit 0, no warning, half the samples gone.\n */\nexport function toleranceFor(rate: number): number {\n if (!(rate > 0) || !Number.isFinite(rate)) return BOUNDARY_TOLERANCE;\n return Math.min(BOUNDARY_TOLERANCE, 1 / rate / 2);\n}\n\n/** Match the exact half-open boundary rules used while writing signal rows. */\nexport function sampleTimeIsInRange(\n time: number,\n startSeconds: number,\n endSeconds: number,\n tolerance: number = BOUNDARY_TOLERANCE,\n): boolean {\n return time >= startSeconds - tolerance && time < endSeconds - tolerance;\n}\n\n/** Count samples from one record that fall inside a half-open requested window. */\nexport function countSamplesInRange(options: {\n recordStart: number;\n rate: number;\n samplesPerRecord: number;\n startSeconds: number;\n endSeconds: number;\n}): number {\n const slack = toleranceFor(options.rate);\n const lower = Math.ceil(\n (options.startSeconds - slack - options.recordStart) * options.rate,\n );\n const upper = Math.ceil(\n (options.endSeconds - slack - options.recordStart) * options.rate,\n );\n const first = Math.max(0, Math.min(options.samplesPerRecord, lower));\n const last = Math.max(0, Math.min(options.samplesPerRecord, upper));\n return Math.max(0, last - first);\n}\n\n/**\n * Whether two instants differ only by the arithmetic that produced them.\n *\n * A relative epsilon, because the gap between doubles grows with magnitude — the same shape\n * the long layout uses to decide two sample times are one instant. Well below any real sample\n * interval, and well above the rounding that two routes to one quantity produce: a recording's\n * length is `recordCount * recordDuration`, which for 6003 records of 0.1s is not the 600.3 it\n * prints as.\n */\nfunction sameInstant(a: number, b: number): boolean {\n return Math.abs(a - b) <= Math.max(Math.abs(a), Math.abs(b)) * 1e-12;\n}\n\n/**\n * Turn a requested window into both an exact time span and the record range that\n * contains it. Records are the unit the file can be read in; the exact span is what\n * decides which samples inside those records are actually written.\n */\nexport function resolveRange(options: {\n start?: number | undefined;\n /** The `--start` value exactly as typed, quoted back in the past-the-end error. */\n startText?: string | undefined;\n duration?: number | undefined;\n end?: number | undefined;\n /** The `--end` value exactly as typed, quoted back in the window error. */\n endText?: string | undefined;\n recordDuration: number;\n recordCount: number;\n /**\n * True start time of each data record, for discontinuous files. When absent,\n * records are assumed to sit end to end.\n */\n recordStarts?: Float64Array | null | undefined;\n}): ResolvedRange {\n // For a continuous file the recording spans recordCount * recordDuration. A\n // discontinuous one does not: a 10-second recording with a 95-second gap in the\n // middle still ends at 105 seconds. Deriving the span from the records' real\n // positions is what stops a requested window from being clipped back to the\n // amount of *data* in the file and silently discarding everything past it.\n const { earliest, latest } = span(options.recordStarts, options.recordCount, options.recordDuration);\n\n if (options.duration !== undefined && options.end !== undefined) {\n throw new TimeRangeError('Use either --duration or --end, not both.');\n }\n\n const startSeconds = options.start ?? earliest;\n\n let endSeconds: number;\n if (options.duration !== undefined) endSeconds = startSeconds + options.duration;\n else if (options.end !== undefined) endSeconds = options.end;\n else endSeconds = latest;\n\n /*\n At the end, allowing for the arithmetic that produced the end.\n\n `latest` is `recordCount * recordDuration`, and with a fractional duration that is not the\n number it prints as: 6003 records of 0.1s is 600.3000000000001, not 600.3. So `--start\n 600.3` on a recording `--info` calls \"10m 0.3s\" was accepted by a hair, converted nothing,\n and exited 0 with a signals.csv holding its header — which is the empty conversion this\n error exists to prevent, and which the same command on a whole-second recording is refused\n for.\n\n A relative epsilon, the same shape the long layout uses to decide two sample times are one\n instant: well below any real interval, and well above the rounding that two different\n routes to the same quantity produce.\n */\n if (startSeconds >= latest || sameInstant(startSeconds, latest)) {\n /*\n Quote what was typed. Reporting the parsed seconds meant `--start 4h` came back as\n \"--start 14400s is at or past the end\", which reads as a value the user never gave.\n\n In quotation marks, which the parse errors above have always used and this did not.\n Without them the value ran into the sentence: `--start \" 5s \"` printed as\n `--start 5s is at or past the end`, where the value appears to be `5s is` and\n the surrounding spaces — the actual reason a shell-built argument went wrong — are\n invisible.\n */\n throw new TimeRangeError(\n /*\n The recording's length in the same words --info uses for it.\n\n `formatSeconds` renders a bare number of seconds, so this message and the Duration\n line disagreed about one file in one session: --info said \"6m 40s\" and the error said\n \"400s\". On an overnight recording it read \"7950s recording\", leaving the reader to\n divide by 3600 to find out whether their --start was reasonable — which is the one\n question this message exists to answer. cli-reference.md has always documented it\n humanised (\"2h 12m 30s\"), a form no input could produce.\n\n The typed value keeps `quoted`, since that is the user's own text and should come\n back exactly as they wrote it.\n */\n /*\n `latest` is where the recording ends on its own clock, which is its length only when\n it starts at zero. A file timed from its first record's timekeeping TAL need not: one\n whose records run 1000s to 1003s is three seconds long, and this called it \"this\n 16m 43s recording\" — while --info two lines away said \"Duration 3s\". Where the\n recording sits is the useful thing to say in that case, and it is the number --start\n has to be given.\n */\n earliest === 0\n ? `--start ${quoted(options.startText, startSeconds)} is at or past the end of this ` +\n `${formatDuration(latest)} recording.`\n : `--start ${quoted(options.startText, startSeconds)} is at or past the end of this ` +\n `${formatDuration(latest - earliest)} recording, which runs from ` +\n `${formatSeconds(earliest)} to ${formatSeconds(latest)}.`,\n );\n }\n if (endSeconds <= startSeconds) {\n // Quote whatever the caller actually gave, for the same reason as the error above. The\n // end is only echoed when --end was passed: with --duration the end is computed here,\n // so there is no typed value to quote and the arithmetic result is the honest thing.\n const endShown =\n options.end !== undefined && options.endText !== undefined\n ? quoted(options.endText, endSeconds)\n : formatSeconds(endSeconds);\n const startShown = quoted(options.startText, startSeconds);\n throw new TimeRangeError(\n `The requested window ends at ${endShown}, which is not after its start at ${startShown}.`,\n );\n }\n\n const clampedEnd = Math.min(endSeconds, latest);\n /*\n Both ends of the window, clamped the same way.\n\n The end has been clamped to the recording since these fields existed — `--end 999h` on a\n two-hour file is documented as converting to the end, silently — and the start was left as\n typed. So a window asked for before the recording begins was recorded as one:\n\n edf2csv two-second.edf --out csv --start=-500\n metadata.json \"start_seconds\": -500, \"end_seconds\": 2\n\n over a signals.csv whose first row is 0.000, with `records_converted` correctly `[0, 2]`.\n output-files calls the pair \"the resolved time window\", which the end is and the start was\n not, and a script taking `end_seconds - start_seconds` for the span converted got 502 for\n two seconds of data.\n\n `earliest` and not zero, because a recording is timed from its first record and need not\n begin there: negative-origin.edf runs from -100s, and -100 is where a start before the\n recording belongs. Nothing downstream moves — every sample sits at or after `earliest`, so\n the row filter and the record selection answer the same for either value, and the byte\n estimate takes a width from the smaller magnitude, which is the one the column actually\n holds.\n\n Not when the whole window sits before the recording, where there is no overlap to clamp\n to and raising the start past the end would describe the request backwards: `--start 0\n --duration 0.001` on a file beginning at 30s is an empty window, and EMPTY_WINDOW quotes\n the bounds — \"(30.000s to 0.001s)\" says nothing a reader can act on, where the bounds as\n asked for are what its hint is already explaining.\n */\n const clampedStart = clampedEnd <= earliest ? startSeconds : Math.max(startSeconds, earliest);\n const { startRecord, endRecord } = selectRecords(options, clampedStart, clampedEnd);\n\n return {\n startSeconds: clampedStart,\n endSeconds: clampedEnd,\n startRecord,\n endRecord,\n /*\n The same rounding, one field over.\n\n `latest` is `recordCount * recordDuration`, and 6003 records of 0.1s is\n 600.3000000000001. So `--end 600.3` on a recording of exactly that length wrote every\n sample it has — byte-identical to a bare conversion — and metadata.json recorded\n `whole_recording: false` for it, while the bare run recorded true. One conversion, two\n answers, on the field a pipeline reads to decide whether it has the lot.\n */\n isWholeRecording:\n (startSeconds <= earliest || sameInstant(startSeconds, earliest)) &&\n (clampedEnd >= latest || sameInstant(clampedEnd, latest)),\n recordingStartSeconds: earliest,\n recordingEndSeconds: latest,\n };\n}\n\nfunction span(\n recordStarts: Float64Array | null | undefined,\n recordCount: number,\n recordDuration: number,\n): { earliest: number; latest: number } {\n if (!recordStarts || recordStarts.length === 0) {\n return { earliest: 0, latest: recordCount * recordDuration };\n }\n let earliest = Infinity;\n let latest = -Infinity;\n for (const start of recordStarts) {\n if (start < earliest) earliest = start;\n if (start + recordDuration > latest) latest = start + recordDuration;\n }\n if (!Number.isFinite(earliest) || !Number.isFinite(latest)) {\n return { earliest: 0, latest: recordCount * recordDuration };\n }\n return { earliest, latest };\n}\n\n/** Every record whose own time span overlaps the requested window. */\nfunction selectRecords(\n options: { recordDuration: number; recordCount: number; recordStarts?: Float64Array | null | undefined },\n startSeconds: number,\n endSeconds: number,\n): { startRecord: number; endRecord: number } {\n const starts = options.recordStarts;\n if (!starts || starts.length === 0) {\n return {\n startRecord: Math.max(0, Math.floor(startSeconds / options.recordDuration)),\n endRecord: Math.min(options.recordCount, Math.ceil(endSeconds / options.recordDuration)),\n };\n }\n\n let first = options.recordCount;\n let last = 0;\n for (let i = 0; i < starts.length; i++) {\n const begin = starts[i] ?? 0;\n if (begin + options.recordDuration > startSeconds && begin < endSeconds) {\n if (i < first) first = i;\n if (i + 1 > last) last = i + 1;\n }\n }\n return first < last ? { startRecord: first, endRecord: last } : { startRecord: 0, endRecord: 0 };\n}\n\n/**\n * The value as the caller typed it, in quotation marks, or the parsed seconds if they gave\n * none. The marks show where the value begins and ends, which matters most for the values\n * that went wrong because of what surrounds them.\n */\nfunction quoted(text: string | undefined, seconds: number): string {\n return text === undefined ? formatSeconds(seconds) : `\"${text}\"`;\n}\n\nfunction formatSeconds(seconds: number): string {\n /*\n The same 1e21 cliff, in the bounds this message hands back.\n\n `--start \"9e21\"` was answered with \"is at or past the end of this 3e+21s recording, which\n runs from 1e+21s to 4e+21s\" — a sentence whose whole purpose is to say what window there\n is to ask for, ending in two tokens the parser refuses: `--start 1e+21s` is \"uses an\n unknown unit \\\"e\\\"\". `Number(...)` was here to drop the trailing zeros, and it also\n re-introduced the exponent form that `toFixed` had produced.\n */\n const text = fixed(seconds, 3);\n return `${text.includes('.') ? text.replace(/\\.?0+$/u, '') : text}s`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"time-range.js","sourceRoot":"","sources":["../../src/convert/time-range.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Parsing for the time-range options.\n *\n * Researchers write offsets in whatever form is natural for the recording in front\n * of them: seconds for a short ECG strip, `30m` into a sleep study, `01:23:45` when\n * reading off a clock. All three are accepted; anything ambiguous is rejected with\n * a message that shows the forms that work.\n */\n\nimport { fixed, formatDuration } from '../format/number.js';\n\nexport class TimeRangeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'TimeRangeError';\n }\n}\n\n/**\n * Every spelling of a unit this accepts, and what one of it is worth in seconds.\n *\n * Exported so the spellings can be enumerated by a test rather than listed a second time:\n * twelve of these sixteen are named nowhere in the tool or its documentation, and none of\n * their values was checked by anything. `hrs: 360` would have converted `--start 2hrs` from\n * twelve minutes in and said nothing, which is the one kind of mistake a window may not make.\n */\nexport const UNIT_SECONDS: Record<string, number> = {\n h: 3600,\n hr: 3600,\n hrs: 3600,\n hour: 3600,\n hours: 3600,\n m: 60,\n min: 60,\n mins: 60,\n minute: 60,\n minutes: 60,\n s: 1,\n sec: 1,\n secs: 1,\n second: 1,\n seconds: 1,\n ms: 0.001,\n};\n\nconst CLOCK = /^(?:(\\d+):)?(\\d{1,2}):(\\d{1,2}(?:\\.\\d+)?)$/u;\nconst UNIT_TOKEN = /(\\d+(?:\\.\\d+)?)\\s*([a-z]+)/giu;\n\n/**\n * Parse a duration or offset into seconds.\n *\n * Accepted: `90`, `90s`, `5m`, `1h30m`, `1h 30m 15s`, `00:30:00`, `30:00`, `250ms`.\n *\n * `allowNegative` is for the two options that name a position rather than a length.\n *\n * A recording is timed from its first record's timekeeping annotation, and nothing obliges that\n * to sit at or after zero: a file whose records run from -100 s to -97 s is one this tool reads,\n * times from -100, and describes with\n *\n * Timed from -100.000s (first sample; --start and --end use this clock)\n *\n * That line says the number can be typed straight back in, and it could not be. Every offset\n * such a recording has came back as \"not a time I understand\", so its whole clock was\n * unreachable and no window of it could be converted at all — the one file shape where a\n * window is refused for naming a moment the recording actually contains.\n *\n * A length below zero is still a different thing, and `--duration` still refuses one.\n */\nexport function parseTimeSpec(input: string, optionName: string, allowNegative = false): number {\n const trimmed = input.trim().toLowerCase();\n if (trimmed === '') {\n throw new TimeRangeError(`${optionName} is empty. Try a value like 30s, 5m, or 00:30:00.`);\n }\n\n /*\n The sign is read off the front and applied to the whole value, so `-1h30m` is an hour and a\n half before the origin rather than an hour before it and half an hour after.\n\n Nothing may sit between the sign and the number, and a leading `+` is still refused: every\n other number this tool takes is refused when written in a form nobody types, and `+5` is\n exactly what `--decimals` and `--jobs` reject one flag over.\n */\n const negative = trimmed.startsWith('-');\n const text = negative ? trimmed.slice(1) : trimmed;\n\n /*\n A leading plus, named as such.\n\n Refusing it is deliberate — see the paragraph above — but it came back as \"is not a time I\n understand. Try 30s, 5m, 1h30m, 00:30:00, or a plain number of seconds\", which is the\n message for input that could not be read at all. Every part of `+5s` was read: the number\n parsed, the unit was looked up and found. The one thing wrong with it is a character the\n reader put there on purpose, and they were sent to re-check their unit spellings with `5m`\n sitting in the list of suggestions, differing from what they typed by the sign they cannot\n see is the problem. 0.7.102 made this argument about a space; this is the same sentence\n about the other character that reaches it. `--decimals` and `--jobs` both quote the value\n back and say what is wrong with it.\n */\n if (text.startsWith('+')) {\n throw new TimeRangeError(\n `${optionName} \"${input}\" begins with a plus. Write the number on its own: ` +\n `${input.trim().replace('+', '')}`,\n );\n }\n const signed = (seconds: number): number =>\n assertFinite(negative ? -seconds : seconds, optionName, input, allowNegative);\n\n // Bare number means seconds.\n if (/^\\d+(?:\\.\\d+)?$/u.test(text)) return signed(Number(text));\n\n // Clock form: hh:mm:ss or mm:ss.\n const clock = CLOCK.exec(text);\n if (clock) {\n const hours = clock[1] === undefined ? 0 : Number(clock[1]);\n const minutes = Number(clock[2]);\n const seconds = Number(clock[3]);\n if (minutes >= 60 || seconds >= 60) {\n throw new TimeRangeError(\n `${optionName} \"${input}\" has a minutes or seconds field of 60 or more.`,\n );\n }\n return signed(hours * 3600 + minutes * 60 + seconds);\n }\n\n // Unit form: 1h30m, 1h30m 15s, 250ms. A number sits directly against its unit; see below.\n UNIT_TOKEN.lastIndex = 0;\n let total = 0;\n let matched = 0;\n let consumed = 0;\n /** Whether any token had a space between its number and its unit. See the refusal below. */\n let spaced = false;\n let match: RegExpExecArray | null;\n // Which units have already been seen, so a repeat can be rejected rather than added.\n const seen = new Set<number>();\n while ((match = UNIT_TOKEN.exec(text)) !== null) {\n const amount = Number(match[1]);\n const unit = match[2] ?? '';\n const scale = UNIT_SECONDS[unit];\n if (scale === undefined) {\n throw new TimeRangeError(\n `${optionName} \"${input}\" uses an unknown unit \"${unit}\". Use h, m, s or ms, ` +\n `or their long forms: hours, minutes, seconds.`,\n );\n }\n\n // \"1h1h\" is a typo, not a request for two hours. Summing repeated units silently\n // turned a slip into a plausible window that was quietly the wrong length. Units are\n // keyed by their scale so the aliases collapse together: \"1h30min20m\" is caught too.\n if (seen.has(scale)) {\n throw new TimeRangeError(\n `${optionName} \"${input}\" gives the same unit twice. ` +\n `Combine each unit once, as in 1h30m.`,\n );\n }\n seen.add(scale);\n\n total += amount * scale;\n matched++;\n // Counted with the whitespace taken out, so the comparison below is against the same\n // string on both sides and a space inside a token reaches the refusal written for it\n // rather than the one about input that could not be read at all.\n consumed += match[0].replace(/\\s+/gu, '').length;\n if (/\\s/u.test(match[0])) spaced = true;\n }\n\n // Reject partially-understood input like \"5x\" or \"1h banana\".\n if (matched === 0 || consumed !== text.replace(/\\s+/gu, '').length) {\n throw new TimeRangeError(\n `${optionName} \"${input}\" is not a time I understand. ` +\n `Try 30s, 5m, 1h30m, 00:30:00, or a plain number of seconds.`,\n );\n }\n\n /*\n A space between a number and its unit, named as such.\n\n `5 min` is refused on purpose — cli-reference sets the rule out, and it is what keeps\n `1 2h` from being read as anything — but it came back as \"is not a time I understand. Try\n 30s, 5m, 1h30m, 00:30:00, or a plain number of seconds\", which is the message for input\n that could not be read at all. Every part of `5 min` was read: the number parsed, the unit\n was looked up in the table and found. The one thing wrong with it is a space, and the\n reader was sent to re-check their unit spellings instead — with `5m` sitting in the list of\n suggestions, differing from what they typed by a character they cannot see is the problem.\n\n Only reachable now that the count above ignores whitespace; before, the length check caught\n these first and there was nothing left to tell them apart by.\n */\n if (spaced) {\n throw new TimeRangeError(\n `${optionName} \"${input}\" puts a space between a number and its unit. ` +\n `Write them together: ${input.trim().replace(/(\\d)\\s+([a-z])/giu, '$1$2')}`,\n );\n }\n\n return signed(total);\n}\n\nfunction assertFinite(\n value: number,\n optionName: string,\n input: string,\n allowNegative: boolean,\n): number {\n if (!Number.isFinite(value) || (!allowNegative && value < 0)) {\n throw new TimeRangeError(`${optionName} \"${input}\" is not a valid non-negative time.`);\n }\n return value;\n}\n\nexport interface ResolvedRange {\n /** Inclusive start, in seconds from the beginning of the recording. */\n startSeconds: number;\n /** Exclusive end, in seconds. */\n endSeconds: number;\n /** First data record touching the window. */\n startRecord: number;\n /** One past the last data record touching the window. */\n endRecord: number;\n /** True when the window covers the whole recording. */\n isWholeRecording: boolean;\n /** Earliest record start, including EDF+D timing gaps. */\n recordingStartSeconds: number;\n /** End of the latest record, including EDF+D timing gaps. */\n recordingEndSeconds: number;\n}\n\n/**\n * Slack for comparisons at sample/window boundaries.\n *\n * A nanosecond is far below any real sampling interval — 20 kHz is 50 microseconds — so it\n * absorbs the arithmetic error in `recordStart + sample / rate` without reaching a\n * neighbouring sample.\n */\nexport const BOUNDARY_TOLERANCE = 1e-9;\n\n/**\n * The slack to use for a channel sampled this often.\n *\n * Never as much as half a sample interval, because slack that reaches the next sample stops\n * being slack. A fixed nanosecond was applied whatever the rate, and the format does not\n * oblige the interval to be larger than it: EDF's record duration is an 8-character field\n * that accepts `1e-9`. A recording of two 1 ns records holding ten samples each wrote ten of\n * its twenty rows — the window ends at 2e-9, the comparison asked for `time < 2e-9 - 1e-9`,\n * and the entire second record failed it. Exit 0, no warning, half the samples gone.\n */\nexport function toleranceFor(rate: number): number {\n if (!(rate > 0) || !Number.isFinite(rate)) return BOUNDARY_TOLERANCE;\n return Math.min(BOUNDARY_TOLERANCE, 1 / rate / 2);\n}\n\n/** Match the exact half-open boundary rules used while writing signal rows. */\nexport function sampleTimeIsInRange(\n time: number,\n startSeconds: number,\n endSeconds: number,\n tolerance: number = BOUNDARY_TOLERANCE,\n): boolean {\n return time >= startSeconds - tolerance && time < endSeconds - tolerance;\n}\n\n/** Count samples from one record that fall inside a half-open requested window. */\nexport function countSamplesInRange(options: {\n recordStart: number;\n rate: number;\n samplesPerRecord: number;\n startSeconds: number;\n endSeconds: number;\n}): number {\n const slack = toleranceFor(options.rate);\n const lower = Math.ceil(\n (options.startSeconds - slack - options.recordStart) * options.rate,\n );\n const upper = Math.ceil(\n (options.endSeconds - slack - options.recordStart) * options.rate,\n );\n const first = Math.max(0, Math.min(options.samplesPerRecord, lower));\n const last = Math.max(0, Math.min(options.samplesPerRecord, upper));\n return Math.max(0, last - first);\n}\n\n/**\n * Whether two instants differ only by the arithmetic that produced them.\n *\n * A relative epsilon, because the gap between doubles grows with magnitude — the same shape\n * the long layout uses to decide two sample times are one instant. Well below any real sample\n * interval, and well above the rounding that two routes to one quantity produce: a recording's\n * length is `recordCount * recordDuration`, which for 6003 records of 0.1s is not the 600.3 it\n * prints as.\n */\nfunction sameInstant(a: number, b: number): boolean {\n return Math.abs(a - b) <= Math.max(Math.abs(a), Math.abs(b)) * 1e-12;\n}\n\n/**\n * Turn a requested window into both an exact time span and the record range that\n * contains it. Records are the unit the file can be read in; the exact span is what\n * decides which samples inside those records are actually written.\n */\nexport function resolveRange(options: {\n start?: number | undefined;\n /** The `--start` value exactly as typed, quoted back in the past-the-end error. */\n startText?: string | undefined;\n duration?: number | undefined;\n end?: number | undefined;\n /** The `--end` value exactly as typed, quoted back in the window error. */\n endText?: string | undefined;\n recordDuration: number;\n recordCount: number;\n /**\n * True start time of each data record, for discontinuous files. When absent,\n * records are assumed to sit end to end.\n */\n recordStarts?: Float64Array | null | undefined;\n}): ResolvedRange {\n // For a continuous file the recording spans recordCount * recordDuration. A\n // discontinuous one does not: a 10-second recording with a 95-second gap in the\n // middle still ends at 105 seconds. Deriving the span from the records' real\n // positions is what stops a requested window from being clipped back to the\n // amount of *data* in the file and silently discarding everything past it.\n const { earliest, latest } = span(options.recordStarts, options.recordCount, options.recordDuration);\n\n if (options.duration !== undefined && options.end !== undefined) {\n throw new TimeRangeError('Use either --duration or --end, not both.');\n }\n\n const startSeconds = options.start ?? earliest;\n\n let endSeconds: number;\n if (options.duration !== undefined) endSeconds = startSeconds + options.duration;\n else if (options.end !== undefined) endSeconds = options.end;\n else endSeconds = latest;\n\n /*\n At the end, allowing for the arithmetic that produced the end.\n\n `latest` is `recordCount * recordDuration`, and with a fractional duration that is not the\n number it prints as: 6003 records of 0.1s is 600.3000000000001, not 600.3. So `--start\n 600.3` on a recording `--info` calls \"10m 0.3s\" was accepted by a hair, converted nothing,\n and exited 0 with a signals.csv holding its header — which is the empty conversion this\n error exists to prevent, and which the same command on a whole-second recording is refused\n for.\n\n A relative epsilon, the same shape the long layout uses to decide two sample times are one\n instant: well below any real interval, and well above the rounding that two different\n routes to the same quantity produce.\n */\n if (startSeconds >= latest || sameInstant(startSeconds, latest)) {\n /*\n Quote what was typed. Reporting the parsed seconds meant `--start 4h` came back as\n \"--start 14400s is at or past the end\", which reads as a value the user never gave.\n\n In quotation marks, which the parse errors above have always used and this did not.\n Without them the value ran into the sentence: `--start \" 5s \"` printed as\n `--start 5s is at or past the end`, where the value appears to be `5s is` and\n the surrounding spaces — the actual reason a shell-built argument went wrong — are\n invisible.\n */\n throw new TimeRangeError(\n /*\n The recording's length in the same words --info uses for it.\n\n `formatSeconds` renders a bare number of seconds, so this message and the Duration\n line disagreed about one file in one session: --info said \"6m 40s\" and the error said\n \"400s\". On an overnight recording it read \"7950s recording\", leaving the reader to\n divide by 3600 to find out whether their --start was reasonable — which is the one\n question this message exists to answer. cli-reference.md has always documented it\n humanised (\"2h 12m 30s\"), a form no input could produce.\n\n The typed value keeps `quoted`, since that is the user's own text and should come\n back exactly as they wrote it.\n */\n /*\n `latest` is where the recording ends on its own clock, which is its length only when\n it starts at zero. A file timed from its first record's timekeeping TAL need not: one\n whose records run 1000s to 1003s is three seconds long, and this called it \"this\n 16m 43s recording\" — while --info two lines away said \"Duration 3s\". Where the\n recording sits is the useful thing to say in that case, and it is the number --start\n has to be given.\n */\n earliest === 0\n ? `--start ${quoted(options.startText, startSeconds)} is at or past the end of this ` +\n `${formatDuration(latest)} recording.`\n : `--start ${quoted(options.startText, startSeconds)} is at or past the end of this ` +\n `${formatDuration(latest - earliest)} recording, which runs from ` +\n `${formatSeconds(earliest)} to ${formatSeconds(latest)}.`,\n );\n }\n if (endSeconds <= startSeconds) {\n // Quote whatever the caller actually gave, for the same reason as the error above. The\n // end is only echoed when --end was passed: with --duration the end is computed here,\n // so there is no typed value to quote and the arithmetic result is the honest thing.\n const endShown =\n options.end !== undefined && options.endText !== undefined\n ? quoted(options.endText, endSeconds)\n : formatSeconds(endSeconds);\n const startShown = quoted(options.startText, startSeconds);\n throw new TimeRangeError(\n `The requested window ends at ${endShown}, which is not after its start at ${startShown}.`,\n );\n }\n\n const clampedEnd = Math.min(endSeconds, latest);\n /*\n Both ends of the window, clamped the same way.\n\n The end has been clamped to the recording since these fields existed — `--end 999h` on a\n two-hour file is documented as converting to the end, silently — and the start was left as\n typed. So a window asked for before the recording begins was recorded as one:\n\n edf2csv two-second.edf --out csv --start=-500\n metadata.json \"start_seconds\": -500, \"end_seconds\": 2\n\n over a signals.csv whose first row is 0.000, with `records_converted` correctly `[0, 2]`.\n output-files calls the pair \"the resolved time window\", which the end is and the start was\n not, and a script taking `end_seconds - start_seconds` for the span converted got 502 for\n two seconds of data.\n\n `earliest` and not zero, because a recording is timed from its first record and need not\n begin there: negative-origin.edf runs from -100s, and -100 is where a start before the\n recording belongs. Nothing downstream moves — every sample sits at or after `earliest`, so\n the row filter and the record selection answer the same for either value, and the byte\n estimate takes a width from the smaller magnitude, which is the one the column actually\n holds.\n\n Not when the whole window sits before the recording, where there is no overlap to clamp\n to and raising the start past the end would describe the request backwards: `--start 0\n --duration 0.001` on a file beginning at 30s is an empty window, and EMPTY_WINDOW quotes\n the bounds — \"(30.000s to 0.001s)\" says nothing a reader can act on, where the bounds as\n asked for are what its hint is already explaining.\n */\n const clampedStart = clampedEnd <= earliest ? startSeconds : Math.max(startSeconds, earliest);\n const { startRecord, endRecord } = selectRecords(options, clampedStart, clampedEnd);\n\n return {\n startSeconds: clampedStart,\n endSeconds: clampedEnd,\n startRecord,\n endRecord,\n /*\n The same rounding, one field over.\n\n `latest` is `recordCount * recordDuration`, and 6003 records of 0.1s is\n 600.3000000000001. So `--end 600.3` on a recording of exactly that length wrote every\n sample it has — byte-identical to a bare conversion — and metadata.json recorded\n `whole_recording: false` for it, while the bare run recorded true. One conversion, two\n answers, on the field a pipeline reads to decide whether it has the lot.\n */\n isWholeRecording:\n (startSeconds <= earliest || sameInstant(startSeconds, earliest)) &&\n (clampedEnd >= latest || sameInstant(clampedEnd, latest)),\n recordingStartSeconds: earliest,\n recordingEndSeconds: latest,\n };\n}\n\nfunction span(\n recordStarts: Float64Array | null | undefined,\n recordCount: number,\n recordDuration: number,\n): { earliest: number; latest: number } {\n if (!recordStarts || recordStarts.length === 0) {\n return { earliest: 0, latest: recordCount * recordDuration };\n }\n let earliest = Infinity;\n let latest = -Infinity;\n for (const start of recordStarts) {\n if (start < earliest) earliest = start;\n if (start + recordDuration > latest) latest = start + recordDuration;\n }\n if (!Number.isFinite(earliest) || !Number.isFinite(latest)) {\n return { earliest: 0, latest: recordCount * recordDuration };\n }\n return { earliest, latest };\n}\n\n/** Every record whose own time span overlaps the requested window. */\nfunction selectRecords(\n options: { recordDuration: number; recordCount: number; recordStarts?: Float64Array | null | undefined },\n startSeconds: number,\n endSeconds: number,\n): { startRecord: number; endRecord: number } {\n const starts = options.recordStarts;\n if (!starts || starts.length === 0) {\n return {\n startRecord: Math.max(0, Math.floor(startSeconds / options.recordDuration)),\n endRecord: Math.min(options.recordCount, Math.ceil(endSeconds / options.recordDuration)),\n };\n }\n\n let first = options.recordCount;\n let last = 0;\n for (let i = 0; i < starts.length; i++) {\n const begin = starts[i] ?? 0;\n if (begin + options.recordDuration > startSeconds && begin < endSeconds) {\n if (i < first) first = i;\n if (i + 1 > last) last = i + 1;\n }\n }\n return first < last ? { startRecord: first, endRecord: last } : { startRecord: 0, endRecord: 0 };\n}\n\n/**\n * The value as the caller typed it, in quotation marks, or the parsed seconds if they gave\n * none. The marks show where the value begins and ends, which matters most for the values\n * that went wrong because of what surrounds them.\n */\nfunction quoted(text: string | undefined, seconds: number): string {\n return text === undefined ? formatSeconds(seconds) : `\"${text}\"`;\n}\n\nfunction formatSeconds(seconds: number): string {\n /*\n The same 1e21 cliff, in the bounds this message hands back.\n\n `--start \"9e21\"` was answered with \"is at or past the end of this 3e+21s recording, which\n runs from 1e+21s to 4e+21s\" — a sentence whose whole purpose is to say what window there\n is to ask for, ending in two tokens the parser refuses: `--start 1e+21s` is \"uses an\n unknown unit \\\"e\\\"\". `Number(...)` was here to drop the trailing zeros, and it also\n re-introduced the exponent form that `toFixed` had produced.\n */\n const text = fixed(seconds, 3);\n return `${text.includes('.') ? text.replace(/\\.?0+$/u, '') : text}s`;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "edf2csv",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.121",
|
|
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",
|