edf2csv 0.7.249 → 0.7.251
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.js +1 -26
- package/dist/cli.js.map +1 -1
- package/dist/convert/channels.js +1 -22
- package/dist/convert/channels.js.map +1 -1
- package/dist/convert/timing.js +6 -1
- package/dist/convert/timing.js.map +1 -1
- package/dist/format/distance.d.ts +23 -0
- package/dist/format/distance.js +42 -0
- package/dist/format/distance.js.map +1 -0
- package/package.json +1 -1
package/dist/convert/channels.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* carry a channel labelled "-". Labels are therefore preserved verbatim in output,
|
|
8
8
|
* and only disambiguated when the file itself is ambiguous.
|
|
9
9
|
*/
|
|
10
|
+
import { editDistance } from '../format/distance.js';
|
|
10
11
|
import { listed } from '../format/list.js';
|
|
11
12
|
import { assertOptions } from './options.js';
|
|
12
13
|
/**
|
|
@@ -357,26 +358,4 @@ function untypeableBecause(label) {
|
|
|
357
358
|
return 'a comma in the label would read as two names';
|
|
358
359
|
return 'the label cannot be typed';
|
|
359
360
|
}
|
|
360
|
-
/** Levenshtein distance, two-row variant. */
|
|
361
|
-
function editDistance(a, b) {
|
|
362
|
-
if (a === b)
|
|
363
|
-
return 0;
|
|
364
|
-
if (a.length === 0)
|
|
365
|
-
return b.length;
|
|
366
|
-
if (b.length === 0)
|
|
367
|
-
return a.length;
|
|
368
|
-
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
369
|
-
let current = new Array(b.length + 1).fill(0);
|
|
370
|
-
for (let i = 1; i <= a.length; i++) {
|
|
371
|
-
current[0] = i;
|
|
372
|
-
for (let j = 1; j <= b.length; j++) {
|
|
373
|
-
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
374
|
-
current[j] = Math.min((current[j - 1] ?? 0) + 1, (previous[j] ?? 0) + 1, (previous[j - 1] ?? 0) + cost);
|
|
375
|
-
}
|
|
376
|
-
const swap = previous;
|
|
377
|
-
previous = current;
|
|
378
|
-
current = swap;
|
|
379
|
-
}
|
|
380
|
-
return previous[b.length] ?? 0;
|
|
381
|
-
}
|
|
382
361
|
//# sourceMappingURL=channels.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channels.js","sourceRoot":"","sources":["../../src/convert/channels.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Channel naming and selection.\n *\n * Two real-world facts drive this module. EDF labels are free text that routinely\n * contain spaces and punctuation (\"EEG Fpz-Cz\"), and they are not guaranteed to be\n * unique — CHB-MIT recordings ship two channels both labelled \"T8-P8\", and some\n * carry a channel labelled \"-\". Labels are therefore preserved verbatim in output,\n * and only disambiguated when the file itself is ambiguous.\n */\n\nimport type { EdfSignal } from '../edf/header.js';\nimport { listed } from '../format/list.js';\nimport { assertOptions } from './options.js';\n\n/**\n * The name of the column the writer puts in front of the channels, which no channel may take.\n *\n * Exported and used by the writer rather than repeated there, because the whole point of\n * reserving it here is that the two cannot drift apart.\n */\nexport const TIME_COLUMN = 'time_s';\n\nexport class ChannelSelectionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ChannelSelectionError';\n }\n}\n\n/**\n * Column name for a signal.\n *\n * Names are derived from the whole file, not from the current selection, so a given\n * channel always produces the same column regardless of which channels were asked\n * for. Duplicated labels get a `_ch<index>` suffix pointing at their position in\n * the file, which is the only thing that reliably tells them apart.\n */\nexport function buildColumnNames(signals: readonly EdfSignal[]): Map<number, string> {\n const counts = new Map<string, number>();\n for (const signal of signals) {\n if (signal.isAnnotations) continue;\n const base = baseName(signal);\n counts.set(base, (counts.get(base) ?? 0) + 1);\n }\n\n const names = new Map<number, string>();\n for (const signal of signals) {\n if (signal.isAnnotations) continue;\n const base = baseName(signal);\n const duplicated = (counts.get(base) ?? 0) > 1;\n names.set(signal.index, duplicated ? `${base}_ch${signal.index}` : base);\n }\n\n /*\n The suffix has to be checked against the file, not only against the label it disambiguates.\n\n Counting collisions on the raw label alone made the suffix a guess: `_ch<index>` is unique\n among the channels sharing that label, and nothing stopped it from landing on a label some\n other channel already had. A file labelled T8, T8, T8_ch0 — all three legal, since EDF\n labels are free text and nothing enforces uniqueness — produced\n\n time_s,T8_ch0,T8_ch1,T8_ch0\n\n two columns with one name, while the warning beside it said the suffix kept them\n \"distinguishable\". channels.csv listed the same name against two signal indices, so the\n join it exists for could not resolve it either, and `df[\"T8_ch0\"]` in pandas or R returns\n one of the two with nothing to say which.\n\n Anything still shared after the first pass takes its own position as well. That is unique\n by construction, so the loop settles immediately in practice; the bound is there because\n a second round could in principle land on yet another literal label.\n\n The time column counts as taken, for the same reason. It is a name this file does not\n supply and the writer does, and a channel labelled `time_s` collided with it in silence:\n the header came out `time_s,time_s,ECG`, channels.csv named the channel's column `time_s`,\n and every read-back the documentation gives — `index_col=\"time_s\"`, `pop(\"time_s\")`,\n `pivot(index=\"time_s\")` — resolves that to one of the two columns without saying which.\n pandas and Python's own `csv.DictReader` resolve it opposite ways round.\n */\n for (let round = 0; round < 8; round++) {\n const taken = new Map<string, number>([[TIME_COLUMN, 1]]);\n for (const name of names.values()) taken.set(name, (taken.get(name) ?? 0) + 1);\n\n const clashing = [...names].filter(([, name]) => (taken.get(name) ?? 0) > 1);\n if (clashing.length === 0) break;\n for (const [index, name] of clashing) names.set(index, `${name}_ch${index}`);\n }\n return names;\n}\n\nfunction baseName(signal: EdfSignal): string {\n return signal.label === '' ? `signal_${signal.index}` : signal.label;\n}\n\n/**\n * Channels whose column name was pushed off their own label to keep the header unique.\n *\n * A channel genuinely labelled `T8_ch0` loses that name when another label's disambiguating\n * suffix wants it, and the resulting column is the one thing in the output that no longer\n * matches the file. Silence there is what made the collision hard to see in the first place:\n * the only warning raised was about the *other* label.\n */\nexport function renamedByCollision(\n signals: readonly EdfSignal[],\n columnNames: ReadonlyMap<number, string>,\n): EdfSignal[] {\n const counts = new Map<string, number>();\n for (const signal of signals) {\n if (signal.isAnnotations) continue;\n const base = baseName(signal);\n counts.set(base, (counts.get(base) ?? 0) + 1);\n }\n return signals.filter(\n (signal) =>\n !signal.isAnnotations &&\n (counts.get(baseName(signal)) ?? 0) === 1 &&\n columnNames.get(signal.index) !== baseName(signal),\n );\n}\n\nexport interface ChannelSelection {\n signals: EdfSignal[];\n /** Labels that matched more than one channel, so the user knows why they got extras. */\n ambiguous: { term: string; matched: EdfSignal[] }[];\n}\n\n/**\n * Resolve a `--channels` specification against the file's signals.\n *\n * Matching is case-insensitive on the exact label, with `#<index>` available to\n * address a specific channel when labels collide. A term that matches nothing is an\n * error rather than a silent omission: quietly dropping a requested channel would\n * hand the user a CSV that is missing data they explicitly asked for.\n */\nexport function selectChannels(signals: readonly EdfSignal[], terms: readonly string[]): ChannelSelection {\n /*\n The same check `buildPlan` makes, made here too, because this is exported on its own.\n\n `assertOptions` covers `convert`; a caller holding a header and calling this directly got\n the shapes it names in full. `'ECG'` was iterated character by character and answered `No\n channel named \"E\". Did you mean \"ECG\"?` — which reads like a real answer about the file —\n and `[1]`, a position written the way a position is written, came back as `TypeError:\n rawTerm.trim is not a function` from the loop below. Both are the argument being the wrong\n shape, which is the case that checker exists for, and both now say so.\n */\n assertOptions({ channels: terms });\n\n const candidates = signals.filter((s) => !s.isAnnotations);\n const byLabel = new Map<string, EdfSignal[]>();\n for (const signal of candidates) {\n const key = signal.label.toLowerCase();\n const bucket = byLabel.get(key);\n if (bucket) bucket.push(signal);\n else byLabel.set(key, [signal]);\n }\n\n const chosen = new Map<number, EdfSignal>();\n const ambiguous: { term: string; matched: EdfSignal[] }[] = [];\n /*\n A file may have no signal channels at all, and both position errors below assumed it had\n some. `listed([])` is the empty string, so `--channels \"#0\"` on an annotations-only\n recording — one of this tool's own fixtures — was refused with\n\n No channel at position #0. This file has signal channels at .\n\n a sentence that states there are channels and then names none, ending mid-clause. The\n conversion already raises NO_SIGNAL_CHANNELS for this file and says what it is, so the\n fact was known one layer up; the message that a `--channels` user actually reaches was\n the one that did not have it.\n */\n /*\n And one channel is not \"channels\", which is the same sentence one item further on.\n\n The zero case above was given its own wording because \"signal channels at .\" states there\n are some and then names none. A file with exactly one got \"This file has signal channels\n at #0.\" — a plural over a list of one, on the line whose job is to say what may be typed\n instead. Single-channel recordings are ordinary; two of this repository's own fixtures are\n one.\n */\n const positions =\n candidates.length === 0\n ? 'This file has no signal channels; it contains only annotations.'\n : candidates.length === 1\n ? `This file has one signal channel, at #${candidates[0]?.index ?? 0}.`\n : `This file has signal channels at ${listed(candidates.map((s) => `#${s.index}`))}.`;\n\n for (const rawTerm of terms) {\n const term = rawTerm.trim();\n if (term === '') continue;\n\n // '#N' addresses a channel by position, but a label may literally be \"#5\".\n // A real label always wins, so no channel becomes unreachable.\n const literal = byLabel.get(term.toLowerCase());\n if (term.startsWith('#') && (!literal || literal.length === 0)) {\n /*\n A position must be written in plain digits.\n\n `Number()` was doing the parsing, and it accepts a great deal more than a position:\n `#0x2` reached channel 2 through hex, `#0b1` channel 1 through binary, `#1e0` and\n `#2.0` and `# 2` all landed somewhere, and `#` on its own became `Number('')`, which\n is 0. Every one of them selected a channel and exited 0, so a slip did not fail —\n it quietly converted a different channel than the one asked for, which for this tool\n is the worst way to be wrong.\n */\n const position = term.slice(1);\n if (!/^\\d+$/u.test(position)) {\n throw new ChannelSelectionError(\n `\"${term}\" is not a channel position: a position is #0, #1, #2 and so on.\\n` +\n positions,\n );\n }\n const index = Number(position);\n const signal = candidates.find((s) => s.index === index);\n if (!signal) {\n /*\n The list of positions is advice, and advice goes on the continuation line.\n\n It sat on the first line here and on the second in the refusal six lines up, which\n is the same sentence about the same thing. The first line is the one a log gets\n grepped for and the one `printableLines` leaves whole at any width, so on a\n 40-channel recording this refusal ran to 111 columns where its neighbour ran to 74.\n */\n throw new ChannelSelectionError(\n `No channel at position ${term}.\\n${positions}`,\n );\n }\n chosen.set(signal.index, signal);\n continue;\n }\n\n const matched = byLabel.get(term.toLowerCase());\n if (!matched || matched.length === 0) {\n /*\n The one term that is certainly not a typo: a column name.\n\n Matching is on the label, and where a label collides the column gains a `_ch<index>`\n suffix — so `T8-P8_ch1` is a name this tool invented, prints in the COLUMN column of\n --info, writes into channels.csv and puts at the head of signals.csv, and then\n rejects with \"No channel named \"T8-P8_ch1\". Run with --info to list the channels in\n this file\", which is where the user copied it from. The reference documents the trap;\n the message a user actually hits did not.\n\n Answered where it is asked instead, and with the thing that works: `#<index>` selects\n one channel of a colliding pair, which is exactly what someone reaching for the\n suffixed column name wants and the only way to get it.\n */\n const columns = buildColumnNames(candidates);\n const owner = candidates.find(\n (signal) => (columns.get(signal.index) ?? '').toLowerCase() === term.toLowerCase(),\n );\n if (owner) {\n // A channel with no label at all gets the column `signal_<index>`, and offering its\n // label back would be offering `\"\"` — there is nothing to type. Position is the only\n // way to reach it, and saying so is more use than quoting an empty string twice.\n throw new ChannelSelectionError(\n owner.label === ''\n ? `\"${term}\" is a column name, not a channel name: --channels matches the label, ` +\n `and this channel has none.\\n` +\n `Use \"#${owner.index}\" — a channel with no label can only be addressed by position.`\n : `\"${term}\" is a column name, not a channel name: --channels matches the label, ` +\n `which for this channel is \"${owner.label}\".\\n` +\n (typeable(owner.label) === null\n ? `Use \"#${owner.index}\" — ${untypeableBecause(owner.label)}, so position is ` +\n `the only way to reach it.`\n : `Use \"#${owner.index}\" to select just this one, or ${typeable(owner.label)} ` +\n `for every channel sharing that label.`),\n );\n }\n /*\n The annotation channel is a channel, and this said the file had none by that name.\n\n `EDF Annotations` is a label the file really carries — the spec reserves it, --info\n counts it on the \"Channels\" line, and it is the name anyone reading about EDF+ meets\n first. Asking for it got \"No channel named \"EDF Annotations\". Run with --info to list\n the channels in this file\", which is false about the file and points at a table that\n does not list it either, so following the advice returns the reader to the same\n message. What they were after is already being written: every conversion of a file\n with this channel writes annotations.csv from it.\n */\n const asAnnotations = signals.find(\n (signal) => signal.isAnnotations && signal.label.toLowerCase() === term.toLowerCase(),\n );\n if (asAnnotations) {\n throw new ChannelSelectionError(\n `\"${term}\" is this recording's annotation channel, not a signal: it holds event ` +\n `text rather than samples, so it has no column to select.\\n` +\n `Its events are already written to annotations.csv by any conversion of this ` +\n `file — pass --annotations-only for those and no signal data.`,\n );\n }\n throw new ChannelSelectionError(\n `No channel named \"${term}\".${suggest(term, candidates)}\\n` +\n `Run with --info to list the channels in this file.`,\n );\n }\n if (matched.length > 1) ambiguous.push({ term, matched: [...matched] });\n for (const signal of matched) chosen.set(signal.index, signal);\n }\n\n const selected = [...chosen.values()].sort((a, b) => a.index - b.index);\n if (selected.length === 0) {\n throw new ChannelSelectionError('No channels were selected.');\n }\n return { signals: selected, ambiguous };\n}\n\nfunction suggest(term: string, candidates: readonly EdfSignal[]): string {\n // Duplicated labels would otherwise be suggested twice, which reads like two\n // different options while naming the same thing.\n const unique = [...new Set(candidates.map((s) => s.label))];\n /*\n A label that contains the term is the answer, however many edits away it is.\n\n Edit distance charges one edit per character the label has and the term does not, so an\n abbreviation — the commonest way to get this wrong — scores worse the more of the label it\n leaves out. On a recording whose channels are `EEG Fpz-Cz` and `ECG`, `--channels EEG` is\n seven edits from the channel it names and one from the other, and the answer was `Did you\n mean \"ECG\"?`: a suggestion that is retypeable, close, and about the wrong signal. Taking it\n converts a heart trace under the belief it is an EEG, and the run succeeds.\n */\n const needle = term.toLowerCase();\n const scored = unique\n .map((label) => {\n const lower = label.toLowerCase();\n const distance = needle !== '' && lower.includes(needle) ? 0 : editDistance(needle, lower);\n return { label, distance };\n })\n .filter((c) => c.label !== '' && c.distance <= Math.max(2, Math.floor(term.length / 3)))\n .sort((a, b) => a.distance - b.distance);\n if (scored.length === 0) return '';\n /*\n A suggestion is something to retype, so it has to be retypeable.\n\n `Did you mean \"EEG \"A1\"\"?` collapses in a shell to `EEG A1`, which this then rejects with\n the same sentence and the same suggestion — a loop the reader cannot get out of by doing\n what it says. Same failure the header parser's `--channels` advice was fixed for in\n 0.7.18, and the branch above it here; a label carrying `$` or a backtick is the same thing\n again, since a shell expands both inside double quotes.\n\n Where nothing can be typed — a label with a comma, which --channels splits after the shell\n has finished with it, or one with a control byte in it — the position is offered instead.\n It is not the name they asked about, but it is the answer to what they wanted.\n */\n /*\n Cut to three, and the rest counted rather than dropped.\n\n `.slice(0, 3)` said nothing about what it left. On a recording with channels ECG1 to ECG5,\n `--channels ECG` is one edit from all five and the answer was `Did you mean \"ECG1\", \"ECG2\",\n \"ECG3\"?` — three of five equally good answers, with nothing to say the list was cut. A\n reader has no way to tell ECG4 from a channel that does not exist, and this sentence is the\n only place the tool offers to tell them what does.\n\n Through `listed`, which every other list in a sentence goes through: it counts what it\n leaves, and shows a fourth item rather than hiding it behind a phrase longer than the item.\n */\n const offered = scored.map((c) => typeable(c.label) ?? `\"#${positionOf(c.label, candidates)}\"`);\n return ` Did you mean ${listed(offered, 3)}?`;\n}\n\n/** The first channel carrying this label, for a suggestion that cannot be made by name. */\nfunction positionOf(label: string, candidates: readonly EdfSignal[]): number {\n return candidates.find((signal) => signal.label === label)?.index ?? 0;\n}\n\n/**\n * The label written so that typing it back selects this channel, or null when nothing does.\n *\n * Double quotes wherever they survive, since they also show where the label begins and ends\n * and every documented example is written that way. They do not survive a label containing a\n * quote of their own, and — less obviously — a shell still expands `$`, a backtick and a\n * backslash inside them, so `EEG $ref` would arrive as `EEG ` with nothing said. Those go in\n * single quotes, the one POSIX form with no escapes inside it, where a single quote in the\n * label closes, escapes and reopens.\n *\n * Two labels have no form at all. `--channels` splits its list on commas after the shell has\n * finished quoting, so no quoting reaches a label with one in it; and a control character\n * cannot be typed. Both take a position instead, which is what NONPRINTABLE_LABEL already\n * says for the same two reasons.\n */\nexport function typeable(label: string): string | null {\n if (label === '' || label.includes(',')) return null;\n if (/[\\u0000-\\u001f\\u007f-\\u009f]/u.test(label)) return null;\n if (!/[\"$`\\\\]/u.test(label)) return `\"${label}\"`;\n return `'${label.replaceAll(\"'\", \"'\\\\''\")}'`;\n}\n\n/** Why a label has no typeable form, for the sentence that offers a position instead. */\nfunction untypeableBecause(label: string): string {\n if (label.includes(',')) return 'a comma in the label would read as two names';\n return 'the label cannot be typed';\n}\n\n/** Levenshtein distance, two-row variant. */\nfunction editDistance(a: string, b: string): number {\n if (a === b) return 0;\n if (a.length === 0) return b.length;\n if (b.length === 0) return a.length;\n\n let previous = Array.from({ length: b.length + 1 }, (_, i) => i);\n let current = new Array<number>(b.length + 1).fill(0);\n\n for (let i = 1; i <= a.length; i++) {\n current[0] = i;\n for (let j = 1; j <= b.length; j++) {\n const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;\n current[j] = Math.min(\n (current[j - 1] ?? 0) + 1,\n (previous[j] ?? 0) + 1,\n (previous[j - 1] ?? 0) + cost,\n );\n }\n const swap = previous;\n previous = current;\n current = swap;\n }\n return previous[b.length] ?? 0;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"channels.js","sourceRoot":"","sources":["../../src/convert/channels.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,QAAQ,CAAC;AAEpC,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAA6B;IAC5D,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,aAAa;YAAE,SAAS;QACnC,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC9B,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,aAAa;YAAE,SAAS;QACnC,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC9B,MAAM,UAAU,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAC/C,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC3E,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;MAyBE;IACF,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;QACvC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAiB,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1D,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAE/E,MAAM,QAAQ,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7E,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM;QACjC,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,QAAQ;YAAE,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,IAAI,MAAM,KAAK,EAAE,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,QAAQ,CAAC,MAAiB;IACjC,OAAO,MAAM,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,UAAU,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;AACvE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAA6B,EAC7B,WAAwC;IAExC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,aAAa;YAAE,SAAS;QACnC,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC9B,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,OAAO,CAAC,MAAM,CACnB,CAAC,MAAM,EAAE,EAAE,CACT,CAAC,MAAM,CAAC,aAAa;QACrB,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC;QACzC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,QAAQ,CAAC,MAAM,CAAC,CACrD,CAAC;AACJ,CAAC;AAQD;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,OAA6B,EAAE,KAAwB;IACpF;;;;;;;;;MASE;IACF,aAAa,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;IAEnC,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IAC3D,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC/C,KAAK,MAAM,MAAM,IAAI,UAAU,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;QACvC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,MAAM;YAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;;YAC3B,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAClC,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,GAAG,EAAqB,CAAC;IAC5C,MAAM,SAAS,GAA6C,EAAE,CAAC;IAC/D;;;;;;;;;;;MAWE;IACF;;;;;;;;MAQE;IACF,MAAM,SAAS,GACb,UAAU,CAAC,MAAM,KAAK,CAAC;QACrB,CAAC,CAAC,iEAAiE;QACnE,CAAC,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC;YACvB,CAAC,CAAC,yCAAyC,UAAU,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,GAAG;YACvE,CAAC,CAAC,oCAAoC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC;IAE5F,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,IAAI,KAAK,EAAE;YAAE,SAAS;QAE1B,2EAA2E;QAC3E,+DAA+D;QAC/D,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QAChD,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;YAC/D;;;;;;;;;cASE;YACF,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC/B,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC7B,MAAM,IAAI,qBAAqB,CAC7B,IAAI,IAAI,oEAAoE;oBAC1E,SAAS,CACZ,CAAC;YACJ,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC/B,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;YACzD,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ;;;;;;;kBAOE;gBACF,MAAM,IAAI,qBAAqB,CAC7B,0BAA0B,IAAI,MAAM,SAAS,EAAE,CAChD,CAAC;YACJ,CAAC;YACD,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YACjC,SAAS;QACX,CAAC;QAED,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QAChD,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrC;;;;;;;;;;;;;cAaE;YACF,MAAM,OAAO,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;YAC7C,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAC3B,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,KAAK,IAAI,CAAC,WAAW,EAAE,CACnF,CAAC;YACF,IAAI,KAAK,EAAE,CAAC;gBACV,oFAAoF;gBACpF,qFAAqF;gBACrF,iFAAiF;gBACjF,MAAM,IAAI,qBAAqB,CAC7B,KAAK,CAAC,KAAK,KAAK,EAAE;oBAChB,CAAC,CAAC,IAAI,IAAI,wEAAwE;wBAChF,8BAA8B;wBAC9B,SAAS,KAAK,CAAC,KAAK,gEAAgE;oBACtF,CAAC,CAAC,IAAI,IAAI,wEAAwE;wBAChF,8BAA8B,KAAK,CAAC,KAAK,MAAM;wBAC/C,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,IAAI;4BAC7B,CAAC,CAAC,SAAS,KAAK,CAAC,KAAK,OAAO,iBAAiB,CAAC,KAAK,CAAC,KAAK,CAAC,mBAAmB;gCAC5E,2BAA2B;4BAC7B,CAAC,CAAC,SAAS,KAAK,CAAC,KAAK,iCAAiC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG;gCAC7E,uCAAuC,CAAC,CACjD,CAAC;YACJ,CAAC;YACD;;;;;;;;;;cAUE;YACF,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAChC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,IAAI,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,IAAI,CAAC,WAAW,EAAE,CACtF,CAAC;YACF,IAAI,aAAa,EAAE,CAAC;gBAClB,MAAM,IAAI,qBAAqB,CAC7B,IAAI,IAAI,yEAAyE;oBAC/E,4DAA4D;oBAC5D,8EAA8E;oBAC9E,8DAA8D,CACjE,CAAC;YACJ,CAAC;YACD,MAAM,IAAI,qBAAqB,CAC7B,qBAAqB,IAAI,KAAK,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI;gBACzD,oDAAoD,CACvD,CAAC;QACJ,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;QACxE,KAAK,MAAM,MAAM,IAAI,OAAO;YAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACjE,CAAC;IAED,MAAM,QAAQ,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IACxE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,qBAAqB,CAAC,4BAA4B,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AAC1C,CAAC;AAED,SAAS,OAAO,CAAC,IAAY,EAAE,UAAgC;IAC7D,6EAA6E;IAC7E,iDAAiD;IACjD,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC5D;;;;;;;;;MASE;IACF,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IAClC,MAAM,MAAM,GAAG,MAAM;SAClB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACb,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;QAClC,MAAM,QAAQ,GAAG,MAAM,KAAK,EAAE,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC3F,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;IAC7B,CAAC,CAAC;SACD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,IAAI,CAAC,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;SACvF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACnC;;;;;;;;;;;;MAYE;IACF;;;;;;;;;;;MAWE;IACF,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;IAChG,OAAO,iBAAiB,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC;AAChD,CAAC;AAED,2FAA2F;AAC3F,SAAS,UAAU,CAAC,KAAa,EAAE,UAAgC;IACjE,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,KAAK,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC;AACzE,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,IAAI,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACrD,IAAI,+BAA+B,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7D,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,KAAK,GAAG,CAAC;IACjD,OAAO,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC;AAC/C,CAAC;AAED,yFAAyF;AACzF,SAAS,iBAAiB,CAAC,KAAa;IACtC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,8CAA8C,CAAC;IAC/E,OAAO,2BAA2B,CAAC;AACrC,CAAC","sourcesContent":["/**\n * Channel naming and selection.\n *\n * Two real-world facts drive this module. EDF labels are free text that routinely\n * contain spaces and punctuation (\"EEG Fpz-Cz\"), and they are not guaranteed to be\n * unique — CHB-MIT recordings ship two channels both labelled \"T8-P8\", and some\n * carry a channel labelled \"-\". Labels are therefore preserved verbatim in output,\n * and only disambiguated when the file itself is ambiguous.\n */\n\nimport type { EdfSignal } from '../edf/header.js';\nimport { editDistance } from '../format/distance.js';\nimport { listed } from '../format/list.js';\nimport { assertOptions } from './options.js';\n\n/**\n * The name of the column the writer puts in front of the channels, which no channel may take.\n *\n * Exported and used by the writer rather than repeated there, because the whole point of\n * reserving it here is that the two cannot drift apart.\n */\nexport const TIME_COLUMN = 'time_s';\n\nexport class ChannelSelectionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ChannelSelectionError';\n }\n}\n\n/**\n * Column name for a signal.\n *\n * Names are derived from the whole file, not from the current selection, so a given\n * channel always produces the same column regardless of which channels were asked\n * for. Duplicated labels get a `_ch<index>` suffix pointing at their position in\n * the file, which is the only thing that reliably tells them apart.\n */\nexport function buildColumnNames(signals: readonly EdfSignal[]): Map<number, string> {\n const counts = new Map<string, number>();\n for (const signal of signals) {\n if (signal.isAnnotations) continue;\n const base = baseName(signal);\n counts.set(base, (counts.get(base) ?? 0) + 1);\n }\n\n const names = new Map<number, string>();\n for (const signal of signals) {\n if (signal.isAnnotations) continue;\n const base = baseName(signal);\n const duplicated = (counts.get(base) ?? 0) > 1;\n names.set(signal.index, duplicated ? `${base}_ch${signal.index}` : base);\n }\n\n /*\n The suffix has to be checked against the file, not only against the label it disambiguates.\n\n Counting collisions on the raw label alone made the suffix a guess: `_ch<index>` is unique\n among the channels sharing that label, and nothing stopped it from landing on a label some\n other channel already had. A file labelled T8, T8, T8_ch0 — all three legal, since EDF\n labels are free text and nothing enforces uniqueness — produced\n\n time_s,T8_ch0,T8_ch1,T8_ch0\n\n two columns with one name, while the warning beside it said the suffix kept them\n \"distinguishable\". channels.csv listed the same name against two signal indices, so the\n join it exists for could not resolve it either, and `df[\"T8_ch0\"]` in pandas or R returns\n one of the two with nothing to say which.\n\n Anything still shared after the first pass takes its own position as well. That is unique\n by construction, so the loop settles immediately in practice; the bound is there because\n a second round could in principle land on yet another literal label.\n\n The time column counts as taken, for the same reason. It is a name this file does not\n supply and the writer does, and a channel labelled `time_s` collided with it in silence:\n the header came out `time_s,time_s,ECG`, channels.csv named the channel's column `time_s`,\n and every read-back the documentation gives — `index_col=\"time_s\"`, `pop(\"time_s\")`,\n `pivot(index=\"time_s\")` — resolves that to one of the two columns without saying which.\n pandas and Python's own `csv.DictReader` resolve it opposite ways round.\n */\n for (let round = 0; round < 8; round++) {\n const taken = new Map<string, number>([[TIME_COLUMN, 1]]);\n for (const name of names.values()) taken.set(name, (taken.get(name) ?? 0) + 1);\n\n const clashing = [...names].filter(([, name]) => (taken.get(name) ?? 0) > 1);\n if (clashing.length === 0) break;\n for (const [index, name] of clashing) names.set(index, `${name}_ch${index}`);\n }\n return names;\n}\n\nfunction baseName(signal: EdfSignal): string {\n return signal.label === '' ? `signal_${signal.index}` : signal.label;\n}\n\n/**\n * Channels whose column name was pushed off their own label to keep the header unique.\n *\n * A channel genuinely labelled `T8_ch0` loses that name when another label's disambiguating\n * suffix wants it, and the resulting column is the one thing in the output that no longer\n * matches the file. Silence there is what made the collision hard to see in the first place:\n * the only warning raised was about the *other* label.\n */\nexport function renamedByCollision(\n signals: readonly EdfSignal[],\n columnNames: ReadonlyMap<number, string>,\n): EdfSignal[] {\n const counts = new Map<string, number>();\n for (const signal of signals) {\n if (signal.isAnnotations) continue;\n const base = baseName(signal);\n counts.set(base, (counts.get(base) ?? 0) + 1);\n }\n return signals.filter(\n (signal) =>\n !signal.isAnnotations &&\n (counts.get(baseName(signal)) ?? 0) === 1 &&\n columnNames.get(signal.index) !== baseName(signal),\n );\n}\n\nexport interface ChannelSelection {\n signals: EdfSignal[];\n /** Labels that matched more than one channel, so the user knows why they got extras. */\n ambiguous: { term: string; matched: EdfSignal[] }[];\n}\n\n/**\n * Resolve a `--channels` specification against the file's signals.\n *\n * Matching is case-insensitive on the exact label, with `#<index>` available to\n * address a specific channel when labels collide. A term that matches nothing is an\n * error rather than a silent omission: quietly dropping a requested channel would\n * hand the user a CSV that is missing data they explicitly asked for.\n */\nexport function selectChannels(signals: readonly EdfSignal[], terms: readonly string[]): ChannelSelection {\n /*\n The same check `buildPlan` makes, made here too, because this is exported on its own.\n\n `assertOptions` covers `convert`; a caller holding a header and calling this directly got\n the shapes it names in full. `'ECG'` was iterated character by character and answered `No\n channel named \"E\". Did you mean \"ECG\"?` — which reads like a real answer about the file —\n and `[1]`, a position written the way a position is written, came back as `TypeError:\n rawTerm.trim is not a function` from the loop below. Both are the argument being the wrong\n shape, which is the case that checker exists for, and both now say so.\n */\n assertOptions({ channels: terms });\n\n const candidates = signals.filter((s) => !s.isAnnotations);\n const byLabel = new Map<string, EdfSignal[]>();\n for (const signal of candidates) {\n const key = signal.label.toLowerCase();\n const bucket = byLabel.get(key);\n if (bucket) bucket.push(signal);\n else byLabel.set(key, [signal]);\n }\n\n const chosen = new Map<number, EdfSignal>();\n const ambiguous: { term: string; matched: EdfSignal[] }[] = [];\n /*\n A file may have no signal channels at all, and both position errors below assumed it had\n some. `listed([])` is the empty string, so `--channels \"#0\"` on an annotations-only\n recording — one of this tool's own fixtures — was refused with\n\n No channel at position #0. This file has signal channels at .\n\n a sentence that states there are channels and then names none, ending mid-clause. The\n conversion already raises NO_SIGNAL_CHANNELS for this file and says what it is, so the\n fact was known one layer up; the message that a `--channels` user actually reaches was\n the one that did not have it.\n */\n /*\n And one channel is not \"channels\", which is the same sentence one item further on.\n\n The zero case above was given its own wording because \"signal channels at .\" states there\n are some and then names none. A file with exactly one got \"This file has signal channels\n at #0.\" — a plural over a list of one, on the line whose job is to say what may be typed\n instead. Single-channel recordings are ordinary; two of this repository's own fixtures are\n one.\n */\n const positions =\n candidates.length === 0\n ? 'This file has no signal channels; it contains only annotations.'\n : candidates.length === 1\n ? `This file has one signal channel, at #${candidates[0]?.index ?? 0}.`\n : `This file has signal channels at ${listed(candidates.map((s) => `#${s.index}`))}.`;\n\n for (const rawTerm of terms) {\n const term = rawTerm.trim();\n if (term === '') continue;\n\n // '#N' addresses a channel by position, but a label may literally be \"#5\".\n // A real label always wins, so no channel becomes unreachable.\n const literal = byLabel.get(term.toLowerCase());\n if (term.startsWith('#') && (!literal || literal.length === 0)) {\n /*\n A position must be written in plain digits.\n\n `Number()` was doing the parsing, and it accepts a great deal more than a position:\n `#0x2` reached channel 2 through hex, `#0b1` channel 1 through binary, `#1e0` and\n `#2.0` and `# 2` all landed somewhere, and `#` on its own became `Number('')`, which\n is 0. Every one of them selected a channel and exited 0, so a slip did not fail —\n it quietly converted a different channel than the one asked for, which for this tool\n is the worst way to be wrong.\n */\n const position = term.slice(1);\n if (!/^\\d+$/u.test(position)) {\n throw new ChannelSelectionError(\n `\"${term}\" is not a channel position: a position is #0, #1, #2 and so on.\\n` +\n positions,\n );\n }\n const index = Number(position);\n const signal = candidates.find((s) => s.index === index);\n if (!signal) {\n /*\n The list of positions is advice, and advice goes on the continuation line.\n\n It sat on the first line here and on the second in the refusal six lines up, which\n is the same sentence about the same thing. The first line is the one a log gets\n grepped for and the one `printableLines` leaves whole at any width, so on a\n 40-channel recording this refusal ran to 111 columns where its neighbour ran to 74.\n */\n throw new ChannelSelectionError(\n `No channel at position ${term}.\\n${positions}`,\n );\n }\n chosen.set(signal.index, signal);\n continue;\n }\n\n const matched = byLabel.get(term.toLowerCase());\n if (!matched || matched.length === 0) {\n /*\n The one term that is certainly not a typo: a column name.\n\n Matching is on the label, and where a label collides the column gains a `_ch<index>`\n suffix — so `T8-P8_ch1` is a name this tool invented, prints in the COLUMN column of\n --info, writes into channels.csv and puts at the head of signals.csv, and then\n rejects with \"No channel named \"T8-P8_ch1\". Run with --info to list the channels in\n this file\", which is where the user copied it from. The reference documents the trap;\n the message a user actually hits did not.\n\n Answered where it is asked instead, and with the thing that works: `#<index>` selects\n one channel of a colliding pair, which is exactly what someone reaching for the\n suffixed column name wants and the only way to get it.\n */\n const columns = buildColumnNames(candidates);\n const owner = candidates.find(\n (signal) => (columns.get(signal.index) ?? '').toLowerCase() === term.toLowerCase(),\n );\n if (owner) {\n // A channel with no label at all gets the column `signal_<index>`, and offering its\n // label back would be offering `\"\"` — there is nothing to type. Position is the only\n // way to reach it, and saying so is more use than quoting an empty string twice.\n throw new ChannelSelectionError(\n owner.label === ''\n ? `\"${term}\" is a column name, not a channel name: --channels matches the label, ` +\n `and this channel has none.\\n` +\n `Use \"#${owner.index}\" — a channel with no label can only be addressed by position.`\n : `\"${term}\" is a column name, not a channel name: --channels matches the label, ` +\n `which for this channel is \"${owner.label}\".\\n` +\n (typeable(owner.label) === null\n ? `Use \"#${owner.index}\" — ${untypeableBecause(owner.label)}, so position is ` +\n `the only way to reach it.`\n : `Use \"#${owner.index}\" to select just this one, or ${typeable(owner.label)} ` +\n `for every channel sharing that label.`),\n );\n }\n /*\n The annotation channel is a channel, and this said the file had none by that name.\n\n `EDF Annotations` is a label the file really carries — the spec reserves it, --info\n counts it on the \"Channels\" line, and it is the name anyone reading about EDF+ meets\n first. Asking for it got \"No channel named \"EDF Annotations\". Run with --info to list\n the channels in this file\", which is false about the file and points at a table that\n does not list it either, so following the advice returns the reader to the same\n message. What they were after is already being written: every conversion of a file\n with this channel writes annotations.csv from it.\n */\n const asAnnotations = signals.find(\n (signal) => signal.isAnnotations && signal.label.toLowerCase() === term.toLowerCase(),\n );\n if (asAnnotations) {\n throw new ChannelSelectionError(\n `\"${term}\" is this recording's annotation channel, not a signal: it holds event ` +\n `text rather than samples, so it has no column to select.\\n` +\n `Its events are already written to annotations.csv by any conversion of this ` +\n `file — pass --annotations-only for those and no signal data.`,\n );\n }\n throw new ChannelSelectionError(\n `No channel named \"${term}\".${suggest(term, candidates)}\\n` +\n `Run with --info to list the channels in this file.`,\n );\n }\n if (matched.length > 1) ambiguous.push({ term, matched: [...matched] });\n for (const signal of matched) chosen.set(signal.index, signal);\n }\n\n const selected = [...chosen.values()].sort((a, b) => a.index - b.index);\n if (selected.length === 0) {\n throw new ChannelSelectionError('No channels were selected.');\n }\n return { signals: selected, ambiguous };\n}\n\nfunction suggest(term: string, candidates: readonly EdfSignal[]): string {\n // Duplicated labels would otherwise be suggested twice, which reads like two\n // different options while naming the same thing.\n const unique = [...new Set(candidates.map((s) => s.label))];\n /*\n A label that contains the term is the answer, however many edits away it is.\n\n Edit distance charges one edit per character the label has and the term does not, so an\n abbreviation — the commonest way to get this wrong — scores worse the more of the label it\n leaves out. On a recording whose channels are `EEG Fpz-Cz` and `ECG`, `--channels EEG` is\n seven edits from the channel it names and one from the other, and the answer was `Did you\n mean \"ECG\"?`: a suggestion that is retypeable, close, and about the wrong signal. Taking it\n converts a heart trace under the belief it is an EEG, and the run succeeds.\n */\n const needle = term.toLowerCase();\n const scored = unique\n .map((label) => {\n const lower = label.toLowerCase();\n const distance = needle !== '' && lower.includes(needle) ? 0 : editDistance(needle, lower);\n return { label, distance };\n })\n .filter((c) => c.label !== '' && c.distance <= Math.max(2, Math.floor(term.length / 3)))\n .sort((a, b) => a.distance - b.distance);\n if (scored.length === 0) return '';\n /*\n A suggestion is something to retype, so it has to be retypeable.\n\n `Did you mean \"EEG \"A1\"\"?` collapses in a shell to `EEG A1`, which this then rejects with\n the same sentence and the same suggestion — a loop the reader cannot get out of by doing\n what it says. Same failure the header parser's `--channels` advice was fixed for in\n 0.7.18, and the branch above it here; a label carrying `$` or a backtick is the same thing\n again, since a shell expands both inside double quotes.\n\n Where nothing can be typed — a label with a comma, which --channels splits after the shell\n has finished with it, or one with a control byte in it — the position is offered instead.\n It is not the name they asked about, but it is the answer to what they wanted.\n */\n /*\n Cut to three, and the rest counted rather than dropped.\n\n `.slice(0, 3)` said nothing about what it left. On a recording with channels ECG1 to ECG5,\n `--channels ECG` is one edit from all five and the answer was `Did you mean \"ECG1\", \"ECG2\",\n \"ECG3\"?` — three of five equally good answers, with nothing to say the list was cut. A\n reader has no way to tell ECG4 from a channel that does not exist, and this sentence is the\n only place the tool offers to tell them what does.\n\n Through `listed`, which every other list in a sentence goes through: it counts what it\n leaves, and shows a fourth item rather than hiding it behind a phrase longer than the item.\n */\n const offered = scored.map((c) => typeable(c.label) ?? `\"#${positionOf(c.label, candidates)}\"`);\n return ` Did you mean ${listed(offered, 3)}?`;\n}\n\n/** The first channel carrying this label, for a suggestion that cannot be made by name. */\nfunction positionOf(label: string, candidates: readonly EdfSignal[]): number {\n return candidates.find((signal) => signal.label === label)?.index ?? 0;\n}\n\n/**\n * The label written so that typing it back selects this channel, or null when nothing does.\n *\n * Double quotes wherever they survive, since they also show where the label begins and ends\n * and every documented example is written that way. They do not survive a label containing a\n * quote of their own, and — less obviously — a shell still expands `$`, a backtick and a\n * backslash inside them, so `EEG $ref` would arrive as `EEG ` with nothing said. Those go in\n * single quotes, the one POSIX form with no escapes inside it, where a single quote in the\n * label closes, escapes and reopens.\n *\n * Two labels have no form at all. `--channels` splits its list on commas after the shell has\n * finished quoting, so no quoting reaches a label with one in it; and a control character\n * cannot be typed. Both take a position instead, which is what NONPRINTABLE_LABEL already\n * says for the same two reasons.\n */\nexport function typeable(label: string): string | null {\n if (label === '' || label.includes(',')) return null;\n if (/[\\u0000-\\u001f\\u007f-\\u009f]/u.test(label)) return null;\n if (!/[\"$`\\\\]/u.test(label)) return `\"${label}\"`;\n return `'${label.replaceAll(\"'\", \"'\\\\''\")}'`;\n}\n\n/** Why a label has no typeable form, for the sentence that offers a position instead. */\nfunction untypeableBecause(label: string): string {\n if (label.includes(',')) return 'a comma in the label would read as two names';\n return 'the label cannot be typed';\n}\n"]}
|
package/dist/convert/timing.js
CHANGED
|
@@ -62,7 +62,12 @@ export function deriveRecordStarts(file, annotationData) {
|
|
|
62
62
|
diagnostics.push({
|
|
63
63
|
code: 'ANNOTATION_DECODE_FAILED',
|
|
64
64
|
severity: 'warning',
|
|
65
|
-
|
|
65
|
+
// Through `counted`, like the four other counts of "data record" in this file. This one
|
|
66
|
+
// spelled the noun and the verb by hand — `record${one ? '' : 's'} carr${one ? 'ies' :
|
|
67
|
+
// 'y'}` — while its nearest neighbour, sixty lines down and about the same records,
|
|
68
|
+
// writes `${counted(n, 'data record')} ${one ? 'carries' : 'carry'}`. Same phrase, same
|
|
69
|
+
// file, two spellings, and only one of them can be checked against the helper.
|
|
70
|
+
message: `${counted(lostTimekeeping, 'data record')} ${one ? 'carries' : 'carry'} a timekeeping ` +
|
|
66
71
|
`annotation that could not be read, so ${one ? 'it does' : 'they do'} not say where in ` +
|
|
67
72
|
`time ${one ? 'it sits' : 'they sit'}.`,
|
|
68
73
|
hint:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timing.js","sourceRoot":"","sources":["../../src/convert/timing.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Diagnostic } from '../edf/errors.js';\nimport type { EdfFile } from '../edf/reader.js';\nimport { counted, listed } from '../format/list.js';\nimport { plain } from '../format/number.js';\n\nexport interface AnnotationTimingData {\n recordStarts: (number | null)[];\n malformed: number;\n /** Unreadable TALs in first position, which carry timing rather than an event. */\n malformedTimekeeping?: number;\n malformedTimekeepingWithText?: number;\n}\n\n/**\n * The EDF+D warning's promise, withdrawn when the file cannot keep it.\n *\n * The header parser raises DISCONTINUOUS with the hint \"Each row carries its true recording\n * time, so gaps stay visible instead of being closed\" — which is what an EDF+D conversion\n * does, when the record times can be read. When they cannot, the very next warning in the\n * same run says the opposite: \"Times are written as if the records were contiguous. Any gaps\n * are lost.\" Two warnings, printed together, and the second denies the first.\n *\n * The parser cannot know: whether the starts can be derived is settled here, after the\n * annotation channel has been read. So the hint is amended where the answer is, the same way\n * `withoutFileRateWarning` drops a header diagnostic the plan has superseded.\n */\nexport function withTimingPromiseKept(\n diagnostics: readonly Diagnostic[],\n derived: boolean,\n): Diagnostic[] {\n if (derived) return [...diagnostics];\n return diagnostics.map((d) =>\n d.code === 'DISCONTINUOUS' && d.hint?.includes('gaps stay visible')\n ? {\n ...d,\n hint:\n 'Where its records sit in time is not recorded in this file, so they are written ' +\n 'as if contiguous — see the warning below.',\n }\n : d,\n );\n}\n\n/**\n * Resolve the true start time of every data record.\n *\n * Continuous recordings need no table because their record positions are\n * arithmetic. EDF+D recordings carry their positions in the annotation channel;\n * missing or malformed timekeeping entries are reported before falling back.\n */\nexport function deriveRecordStarts(\n file: EdfFile,\n annotationData: AnnotationTimingData,\n): { starts: Float64Array | null; diagnostics: Diagnostic[] } {\n const diagnostics: Diagnostic[] = [];\n\n if (annotationData.malformed > 0) {\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n // Through `counted`, with the irregular plural spelled out — which is the case its\n // own comment names and which nothing was using it for.\n message:\n `${counted(annotationData.malformed, 'annotation entry', 'annotation entries')} ` +\n `${annotationData.malformed === 1 ? 'was' : 'were'} unreadable and could not be exported.`,\n hint: 'The rest were exported normally. The file may have been written by a non-conforming tool.',\n });\n }\n\n\n /*\n A timekeeping TAL is not an event, and saying it \"could not be exported\" describes the\n wrong loss twice over.\n\n These were counted among the annotations, so a file with one unreadable timekeeping TAL\n and three good events announced \"1 annotation entry was unreadable and could not be\n exported\" — while exporting all three. Nothing was missing from annotations.csv; what\n went missing was a record's position in time, which the message never mentioned.\n */\n const lostTimekeeping = annotationData.malformedTimekeeping ?? 0;\n const withText = annotationData.malformedTimekeepingWithText ?? 0;\n // The EDF+D branch below raises its own, which names the records and is more specific.\n // Saying both would report one problem twice.\n if (lostTimekeeping > 0 && file.header.continuity !== 'EDF+D') {\n const one = lostTimekeeping === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n message:\n `${lostTimekeeping} data record${one ? '' : 's'} carr${one ? 'ies' : 'y'} a timekeeping ` +\n `annotation that could not be read, so ${one ? 'it does' : 'they do'} not say where in ` +\n `time ${one ? 'it sits' : 'they sit'}.`,\n hint:\n /*\n \"No event was lost\" was said whatever the entry held.\n\n A first-position TAL states the record's start time and may carry events after it,\n which the specification allows and writers do. When one of those cannot be parsed\n the events go with it: a six-event file came out with two, and the only warning\n about it said in so many words that nothing had gone. Those entries are counted\n among the unreadable ones now, which is the warning printed above this one; this\n sentence keeps to what is true of the entries it is actually about.\n */\n (withText > 0\n ? `${withText === 1 ? 'One of them' : `${withText} of them`} also carried event text, ` +\n `which went with ${withText === 1 ? 'it' : 'them'} and is counted above. A ` +\n `timekeeping annotation itself states a record's start time and is never exported. `\n : 'No event was lost — a timekeeping annotation states a record\\'s start time and ' +\n 'is never exported. ') + 'Times are derived from the records that could be read.',\n });\n }\n\n /*\n A continuous recording's records are contiguous, but the first one need not sit at zero.\n\n EDF+ puts the header's start time and every annotation onset on one origin, and says the\n first data record's timekeeping TAL \"always starts with +0.X\", stating the fraction of a\n second by which that record follows it. Ignoring that fraction timed the samples from 0\n while the events kept their true onsets, so the two ended up on origins half a second\n apart — an event at +0.75 in a 4 Hz recording whose first TAL reads +0.5 landed on sample\n 3 instead of sample 1. The same file marked EDF+D, byte-identical but for the reserved\n field, placed it correctly, which is what gives the omission away.\n\n Records stay contiguous, which is what continuous means: only the origin moves. A first\n TAL of +0 needs no table at all, and that is nearly every file.\n */\n if (file.header.continuity !== 'EDF+D') {\n if (file.header.continuity !== 'EDF+C') {\n /*\n An annotation channel the reserved field never claimed.\n\n Without an `EDF+C` or `EDF+D` marker this is a plain EDF file, so the origin is not\n applied and the samples are timed from zero. The annotation channel is found by label\n rather than by the marker, though, so its events are still read and exported — with\n the onsets the file gives them.\n\n On a file whose timekeeping says the records start at 1000s, that put signals.csv at\n 0.000 and the event at 1000.5 in annotations.csv: two files from one conversion, a\n thousand seconds apart, and nothing said so. output-files promises the opposite —\n \"`onset_s` is on the same clock as `time_s` in the signal files\".\n\n Reported rather than repaired. Which clock is right is not knowable from here: the\n marker says plain EDF and the annotation channel says otherwise, and picking one would\n move either the samples or the events by the origin on a guess.\n */\n const stated = annotationData.recordStarts.find((start) => start !== null) ?? null;\n if (stated !== null && Math.abs(stated) > 0) {\n diagnostics.push({\n code: 'MISSING_EDF_PLUS_MARKER',\n severity: 'warning',\n message:\n `This file has an annotation channel stating that its records begin at ` +\n `${plain(stated)}s, but its reserved field carries no EDF+C or EDF+D marker — so ` +\n `it is read as plain EDF, time_s counts from zero, and the two disagree by ` +\n `${plain(stated)}s.`,\n hint:\n 'annotations.csv keeps the onsets the file gives, so its events and signals.csv ' +\n 'are on different clocks. Mark the file EDF+C, or subtract the offset from the ' +\n 'onsets, before joining them.',\n });\n }\n return { starts: null, diagnostics };\n }\n\n /*\n The origin comes from whichever record first states one, not from record 0 alone.\n\n Reading only `recordStarts[0]` meant a single unreadable timekeeping TAL threw the\n origin away and timed the whole file from zero — while records 1 and 2, saying plainly\n that they start at 1.5s and 2.5s, went unread. A recording whose records sit at 0.5s,\n 1.5s and 2.5s came out with every sample 0.5s earlier than the file states, against\n annotation onsets that kept their true values. That is precisely the mismatch 0.4.9\n fixed, arriving through the one hole left in it, and the byte-identical EDF+D twin\n timed it correctly, which is what gives it away.\n\n Continuity is what makes this recoverable: record i sits at `origin + i * duration`,\n so any readable record determines the origin for all of them.\n */\n const origin = originOf(annotationData.recordStarts, file.header.recordDuration);\n if (origin === null) return { starts: null, diagnostics };\n\n const contiguous = new Float64Array(file.recordCount);\n for (let i = 0; i < file.recordCount; i++) {\n contiguous[i] = origin + i * file.header.recordDuration;\n }\n\n /*\n A file marked continuous whose own records disagree about it.\n\n Nothing looked at records past the first, so an EDF+C file whose records are in fact\n spread out was timed as though they were contiguous and said nothing. The records are\n being read here anyway, so the contradiction costs nothing to notice — and it is the\n file, not the reader, that has to be wrong for this to fire.\n\n Compared against what the file can express, not for equality. 0.4.41 asked whether the\n two doubles were the same, which they are not: a recording of 0.1s records sitting at\n 0.1, 0.2, 0.3 ... is contiguous by construction, and 0.1 + 2 * 0.1 is\n 0.30000000000000004. Two of its eight records were reported as contradicting\n continuity, on an ordinary file — and under --strict that was a failed run. The\n smallest interval the recording distinguishes is one sample of its fastest channel;\n anything below half of that is arithmetic, not a gap. `canCarry` has already refused\n origins where the double spacing swamps that interval, so the representation error is\n under the tolerance by construction rather than by hope.\n */\n const tolerance = finestInterval(file) / 2;\n const contradicting = annotationData.recordStarts.filter(\n (declared, i) =>\n typeof declared === 'number' && Math.abs(declared - (contiguous[i] as number)) > tolerance,\n ).length;\n if (contradicting > 0) {\n const continuous = file.header.isBdf ? 'BDF+C' : 'EDF+C';\n const discontinuous = file.header.isBdf ? 'BDF+D' : 'EDF+D';\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n /*\n The markers as the file spells them.\n\n `continuity` normalises `BDF+C` to the internal `EDF+C` tag, and that tag reached\n the message: a BDF+ recording was told it is \"marked continuous (EDF+C)\" — a string\n it does not contain — and advised that it \"should have been marked EDF+D\", which is\n not a value BDF+ defines. A reader grepping the header for either finds nothing.\n\n The sibling discontinuous warning has done this since 0.3.x: `${'$'}{isBdf ? 'BDF+D' :\n 'EDF+D'}`. Same code, same header field, and the continuous branch never got it.\n */\n message:\n `This file is marked continuous (${continuous}), but ${contradicting} of its ` +\n `${counted(file.recordCount, 'data record')} ` +\n `${contradicting === 1 ? 'says it starts' : 'say they start'} somewhere other than ` +\n `where continuity puts ${contradicting === 1 ? 'it' : 'them'}.`,\n hint:\n `Times are written as if the records were contiguous, which is what ${continuous} ` +\n `means. If the recording really has gaps, the file should have been marked ` +\n `${discontinuous}.`,\n });\n }\n const first = origin;\n const last = contiguous[file.recordCount - 1] ?? first;\n if (!canCarry(last, file)) {\n diagnostics.push(unusableOrigin(first, file));\n return { starts: null, diagnostics };\n }\n /*\n An origin of zero is the same as no origin, for timing. It is not the same for the\n check above.\n\n This returned early on `origin === 0`, which is right about the times — contiguous\n starts from zero are what timing from zero already produces — and skipped the\n contradiction check on the way past. So an EDF+C file whose records say 0, 5 and 10 on\n one-second records went unreported, while the same file shifted one second, saying 1, 6\n and 11, was reported. The contradiction is in records 1 and 2 either way; where record 0\n happens to sit decides nothing about it.\n */\n if (origin === 0) return { starts: null, diagnostics };\n return { starts: contiguous, diagnostics };\n }\n\n if (file.annotationSignals.length === 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n 'This file is marked discontinuous but has no annotation channel, so where its ' +\n 'records sit in time is not recorded anywhere.',\n hint: 'Times are written as if the records were contiguous. Any gaps are lost.',\n });\n return { starts: null, diagnostics };\n }\n\n /*\n A record with no readable time is placed from the origin the other records establish,\n not from zero.\n\n `i * recordDuration` assumed the recording began at zero, which is the one thing the\n other records are in a position to contradict: a file starting at 0.5s put its\n unreadable record at 0.000 while its neighbours sat at 1.5s and 2.5s. The guess is still\n a guess — a discontinuous file may have a gap exactly there — and it is still reported\n below, but starting it from where the recording actually begins is strictly closer, and\n it makes an EDF+D file agree with its byte-identical EDF+C twin about record 0.\n */\n const base = originOf(annotationData.recordStarts, file.header.recordDuration) ?? 0;\n const starts = new Float64Array(file.recordCount);\n const missing: number[] = [];\n for (let i = 0; i < file.recordCount; i++) {\n const declared = annotationData.recordStarts[i];\n if (declared === null || declared === undefined) {\n missing.push(i);\n starts[i] = base + i * file.header.recordDuration;\n } else {\n starts[i] = declared;\n }\n }\n\n if (missing.length > 0) {\n /*\n Cut by the function that cuts every other list in a sentence here.\n\n This one rolled its own: five items and a bare `…`, where `listed` shows eight and counts\n what it left — \"and 32 more\" on the rate warning, \"and 112 more\" on the leftover files.\n The count is the honest half, and 0.7.58 taught the shared one not to hide a single item\n behind a phrase longer than the item. None of that reached this message, because it was\n never asking. Two implementations of one job, differing in the limit and in what they say\n about the tail.\n */\n const shown = listed(missing.map(String));\n // \"1 of 3 data records carry ... their true position\" — the subject is the one, not the three.\n const one = missing.length === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n message:\n `${missing.length} of ${counted(file.recordCount, 'data record')} ${one ? 'carries' : 'carry'} no ` +\n `readable timekeeping annotation (record${one ? '' : 's'} ${shown}), so ` +\n `${one ? 'its' : 'their'} true position in time is unknown.`,\n hint: one\n ? 'That record is timed as if it were contiguous; treat its timestamp as unreliable.'\n : 'Those records are timed as if they were contiguous; treat their timestamps as unreliable.',\n });\n }\n\n /*\n Furthest from zero, in either direction.\n\n This took the signed maximum and seeded it with 0, so a recording whose records all sit\n at negative onsets never got past the seed: `furthest` stayed 0, which any interval can\n carry. Then the samples collapsed anyway, because the arithmetic that defeats a large\n positive origin defeats a large negative one identically — at -1e16 seconds, adding a\n 1-second sample interval leaves the double unchanged.\n\n A four-record recording of eight samples wrote two rows, exit 0, no warning. Its\n byte-for-byte positive mirror wrote all eight and explained why it had to time them from\n zero. Same file, same failure, opposite sign, opposite outcome — and the silent one is\n the one that loses data, which is exactly what unusableOrigin exists to prevent.\n */\n let furthest = 0;\n for (const start of starts) if (Math.abs(start) > Math.abs(furthest)) furthest = start;\n if (!canCarry(furthest, file)) {\n diagnostics.push(unusableOrigin(furthest, file));\n return { starts: null, diagnostics };\n }\n\n /*\n Two ways a record can put the time column out of order, and only one was being looked for.\n\n A record starting before the one before it is the obvious case. The other is a record\n starting before the one before it *ends*: starts of 0, 0.5 and 1.0 on one-second records\n are strictly increasing, so nothing fired, and the rows still came out 0.25, 0.5, 0.75,\n 0.5 — because record 0's samples run to 0.75 while record 1 begins at 0.5. Overlapping\n acquisition is what a device does when it re-sends a buffer, and the reader has no more\n to say about it than about the reversed case: every sample is written, in file order,\n with the time the file gives it.\n\n Contiguity is not overlap. A continuous recording has `starts[i] === starts[i-1] +\n duration` exactly, so the comparison is made strict by a fraction of the finest interval\n the recording can express — the same measure the origin check uses.\n */\n const slack = finestInterval(file) / 2;\n let outOfOrder = 0;\n let overlapping = 0;\n for (let i = 1; i < starts.length; i++) {\n const previous = starts[i - 1] as number;\n const current = starts[i] as number;\n if (current < previous) outOfOrder++;\n else if (current + slack < previous + file.header.recordDuration) overlapping++;\n }\n if (outOfOrder > 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `${counted(outOfOrder, 'data record')} ` +\n `${outOfOrder === 1 ? 'starts' : 'start'} earlier than the record before ` +\n `${outOfOrder === 1 ? 'it' : 'them'}.`,\n hint: 'Rows are written in file order, so the time column will not increase monotonically.',\n });\n }\n if (overlapping > 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `${counted(overlapping, 'data record')} ` +\n `${overlapping === 1 ? 'starts' : 'start'} before the record before ` +\n `${overlapping === 1 ? 'it' : 'them'} ends, so ${overlapping === 1 ? 'its' : 'their'} ` +\n `samples overlap in time.`,\n hint: 'Rows are written in file order, so the time column will not increase monotonically.',\n });\n }\n\n return { starts, diagnostics };\n}\n\n/**\n * The recording's origin, from the first record that states where it is.\n *\n * Records of a continuous recording sit end to end, so record `i` beginning at `t` puts the\n * origin at `t - i * duration`. Any one readable timekeeping TAL is therefore enough, which\n * is what stops one unreadable entry from costing the whole file its position in time.\n */\nfunction originOf(recordStarts: readonly (number | null)[], recordDuration: number): number | null {\n for (const [index, declared] of recordStarts.entries()) {\n if (typeof declared !== 'number') continue;\n const origin = declared - index * recordDuration;\n return Number.isFinite(origin) ? origin : null;\n }\n return null;\n}\n\n/**\n * Whether times this far out can still tell one sample from the next.\n *\n * A double spaces its values further apart the larger they get: at 1e16 the gap is 2\n * seconds, so `t + 1` is `t`. Past that point a recording's declared origin stops being a\n * position and becomes a wall — the arithmetic that places records and samples returns the\n * origin itself, whatever is added to it.\n *\n * The finest thing that has to survive is the gap between two consecutive samples of the\n * fastest channel, since that is what the time column is made of. If that survives, so does\n * a whole record.\n */\nfunction canCarry(origin: number, file: EdfFile): boolean {\n if (!Number.isFinite(origin)) return false;\n // Asked of the origin furthest from zero, whichever side it is on: the spacing of doubles\n // grows with magnitude, not with value, so -1e16 and +1e16 fail this identically.\n return origin + finestInterval(file) > origin;\n}\n\n/**\n * The shortest span this recording can tell apart: one sample of its fastest channel.\n *\n * The time column is made of these, so nothing below one is a distinction the file is in a\n * position to make — which is what makes it the right size for both the \"can this origin\n * still separate two samples\" question and the \"is this record really somewhere else\"\n * question.\n */\nfunction finestInterval(file: EdfFile): number {\n let interval = file.header.recordDuration;\n for (const signal of file.header.signals) {\n if (signal.isAnnotations || !(signal.samplesPerRecord > 0)) continue;\n const step = file.header.recordDuration / signal.samplesPerRecord;\n if (step > 0 && step < interval) interval = step;\n }\n return interval;\n}\n\n/**\n * An origin the file's own arithmetic cannot express, reported rather than acted on.\n *\n * Two things went wrong when this was taken at face value, both of them quiet. A file whose\n * records all collapsed onto one instant made the recording zero seconds long, and the\n * window resolver — which had no reason to suspect the recording rather than the request —\n * blamed a flag nobody had passed:\n *\n * error: --start 100000000000000000s is at or past the end of this\n * 100000000000000000s recording.\n *\n * Slightly below that, the collapse is partial: `records[i].start + recordDuration` equals\n * the start again, so the test for \"does this record overlap the window\" fails for every\n * record whose neighbour rounded onto it. A twelve-row recording wrote four rows, exit 0,\n * no warning — the eight that vanished looked exactly like a file that never had them.\n *\n * Timing from zero is what the file did before 0.4.9 taught it to honour the first\n * timekeeping TAL, and at this magnitude it is the only column that can hold distinct\n * values. The origin is lost, so this says so.\n */\nfunction unusableOrigin(origin: number, file: EdfFile): Diagnostic {\n /*\n `Infinity` is a token, not a distance.\n\n `plain` expands exponent notation and hands anything else back, so a non-finite origin\n arrived on screen as \"place it Infinitys from its own start date\" — the exact form\n `formatDuration` exists to keep off the screen, its comment naming \"NaNs\" and \"Infinitys\"\n as what the fallback used to print, and the word the rest of this tool uses for the same\n quantity is \"unknown\". The origin overflows for the reason the recording's length does:\n a record duration near the top of a double, multiplied out over three records, leaves it.\n */\n const away = Number.isFinite(origin)\n ? `${plain(origin)}s from its own start date`\n : 'further from its own start date than a number can hold';\n return {\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `This recording's timekeeping annotations place it ${away}, which is too far out for ` +\n `its ${plain(file.header.recordDuration)}s records to be told apart: at that magnitude ` +\n `adding a sample interval leaves the number unchanged.`,\n hint:\n 'Sample times are written from zero instead, so every row is present and the column ' +\n 'increases. Add the onsets in annotations.csv to recover absolute times if you need them.',\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"timing.js","sourceRoot":"","sources":["../../src/convert/timing.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Diagnostic } from '../edf/errors.js';\nimport type { EdfFile } from '../edf/reader.js';\nimport { counted, listed } from '../format/list.js';\nimport { plain } from '../format/number.js';\n\nexport interface AnnotationTimingData {\n recordStarts: (number | null)[];\n malformed: number;\n /** Unreadable TALs in first position, which carry timing rather than an event. */\n malformedTimekeeping?: number;\n malformedTimekeepingWithText?: number;\n}\n\n/**\n * The EDF+D warning's promise, withdrawn when the file cannot keep it.\n *\n * The header parser raises DISCONTINUOUS with the hint \"Each row carries its true recording\n * time, so gaps stay visible instead of being closed\" — which is what an EDF+D conversion\n * does, when the record times can be read. When they cannot, the very next warning in the\n * same run says the opposite: \"Times are written as if the records were contiguous. Any gaps\n * are lost.\" Two warnings, printed together, and the second denies the first.\n *\n * The parser cannot know: whether the starts can be derived is settled here, after the\n * annotation channel has been read. So the hint is amended where the answer is, the same way\n * `withoutFileRateWarning` drops a header diagnostic the plan has superseded.\n */\nexport function withTimingPromiseKept(\n diagnostics: readonly Diagnostic[],\n derived: boolean,\n): Diagnostic[] {\n if (derived) return [...diagnostics];\n return diagnostics.map((d) =>\n d.code === 'DISCONTINUOUS' && d.hint?.includes('gaps stay visible')\n ? {\n ...d,\n hint:\n 'Where its records sit in time is not recorded in this file, so they are written ' +\n 'as if contiguous — see the warning below.',\n }\n : d,\n );\n}\n\n/**\n * Resolve the true start time of every data record.\n *\n * Continuous recordings need no table because their record positions are\n * arithmetic. EDF+D recordings carry their positions in the annotation channel;\n * missing or malformed timekeeping entries are reported before falling back.\n */\nexport function deriveRecordStarts(\n file: EdfFile,\n annotationData: AnnotationTimingData,\n): { starts: Float64Array | null; diagnostics: Diagnostic[] } {\n const diagnostics: Diagnostic[] = [];\n\n if (annotationData.malformed > 0) {\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n // Through `counted`, with the irregular plural spelled out — which is the case its\n // own comment names and which nothing was using it for.\n message:\n `${counted(annotationData.malformed, 'annotation entry', 'annotation entries')} ` +\n `${annotationData.malformed === 1 ? 'was' : 'were'} unreadable and could not be exported.`,\n hint: 'The rest were exported normally. The file may have been written by a non-conforming tool.',\n });\n }\n\n\n /*\n A timekeeping TAL is not an event, and saying it \"could not be exported\" describes the\n wrong loss twice over.\n\n These were counted among the annotations, so a file with one unreadable timekeeping TAL\n and three good events announced \"1 annotation entry was unreadable and could not be\n exported\" — while exporting all three. Nothing was missing from annotations.csv; what\n went missing was a record's position in time, which the message never mentioned.\n */\n const lostTimekeeping = annotationData.malformedTimekeeping ?? 0;\n const withText = annotationData.malformedTimekeepingWithText ?? 0;\n // The EDF+D branch below raises its own, which names the records and is more specific.\n // Saying both would report one problem twice.\n if (lostTimekeeping > 0 && file.header.continuity !== 'EDF+D') {\n const one = lostTimekeeping === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n // Through `counted`, like the four other counts of \"data record\" in this file. This one\n // spelled the noun and the verb by hand — `record${one ? '' : 's'} carr${one ? 'ies' :\n // 'y'}` — while its nearest neighbour, sixty lines down and about the same records,\n // writes `${counted(n, 'data record')} ${one ? 'carries' : 'carry'}`. Same phrase, same\n // file, two spellings, and only one of them can be checked against the helper.\n message:\n `${counted(lostTimekeeping, 'data record')} ${one ? 'carries' : 'carry'} a timekeeping ` +\n `annotation that could not be read, so ${one ? 'it does' : 'they do'} not say where in ` +\n `time ${one ? 'it sits' : 'they sit'}.`,\n hint:\n /*\n \"No event was lost\" was said whatever the entry held.\n\n A first-position TAL states the record's start time and may carry events after it,\n which the specification allows and writers do. When one of those cannot be parsed\n the events go with it: a six-event file came out with two, and the only warning\n about it said in so many words that nothing had gone. Those entries are counted\n among the unreadable ones now, which is the warning printed above this one; this\n sentence keeps to what is true of the entries it is actually about.\n */\n (withText > 0\n ? `${withText === 1 ? 'One of them' : `${withText} of them`} also carried event text, ` +\n `which went with ${withText === 1 ? 'it' : 'them'} and is counted above. A ` +\n `timekeeping annotation itself states a record's start time and is never exported. `\n : 'No event was lost — a timekeeping annotation states a record\\'s start time and ' +\n 'is never exported. ') + 'Times are derived from the records that could be read.',\n });\n }\n\n /*\n A continuous recording's records are contiguous, but the first one need not sit at zero.\n\n EDF+ puts the header's start time and every annotation onset on one origin, and says the\n first data record's timekeeping TAL \"always starts with +0.X\", stating the fraction of a\n second by which that record follows it. Ignoring that fraction timed the samples from 0\n while the events kept their true onsets, so the two ended up on origins half a second\n apart — an event at +0.75 in a 4 Hz recording whose first TAL reads +0.5 landed on sample\n 3 instead of sample 1. The same file marked EDF+D, byte-identical but for the reserved\n field, placed it correctly, which is what gives the omission away.\n\n Records stay contiguous, which is what continuous means: only the origin moves. A first\n TAL of +0 needs no table at all, and that is nearly every file.\n */\n if (file.header.continuity !== 'EDF+D') {\n if (file.header.continuity !== 'EDF+C') {\n /*\n An annotation channel the reserved field never claimed.\n\n Without an `EDF+C` or `EDF+D` marker this is a plain EDF file, so the origin is not\n applied and the samples are timed from zero. The annotation channel is found by label\n rather than by the marker, though, so its events are still read and exported — with\n the onsets the file gives them.\n\n On a file whose timekeeping says the records start at 1000s, that put signals.csv at\n 0.000 and the event at 1000.5 in annotations.csv: two files from one conversion, a\n thousand seconds apart, and nothing said so. output-files promises the opposite —\n \"`onset_s` is on the same clock as `time_s` in the signal files\".\n\n Reported rather than repaired. Which clock is right is not knowable from here: the\n marker says plain EDF and the annotation channel says otherwise, and picking one would\n move either the samples or the events by the origin on a guess.\n */\n const stated = annotationData.recordStarts.find((start) => start !== null) ?? null;\n if (stated !== null && Math.abs(stated) > 0) {\n diagnostics.push({\n code: 'MISSING_EDF_PLUS_MARKER',\n severity: 'warning',\n message:\n `This file has an annotation channel stating that its records begin at ` +\n `${plain(stated)}s, but its reserved field carries no EDF+C or EDF+D marker — so ` +\n `it is read as plain EDF, time_s counts from zero, and the two disagree by ` +\n `${plain(stated)}s.`,\n hint:\n 'annotations.csv keeps the onsets the file gives, so its events and signals.csv ' +\n 'are on different clocks. Mark the file EDF+C, or subtract the offset from the ' +\n 'onsets, before joining them.',\n });\n }\n return { starts: null, diagnostics };\n }\n\n /*\n The origin comes from whichever record first states one, not from record 0 alone.\n\n Reading only `recordStarts[0]` meant a single unreadable timekeeping TAL threw the\n origin away and timed the whole file from zero — while records 1 and 2, saying plainly\n that they start at 1.5s and 2.5s, went unread. A recording whose records sit at 0.5s,\n 1.5s and 2.5s came out with every sample 0.5s earlier than the file states, against\n annotation onsets that kept their true values. That is precisely the mismatch 0.4.9\n fixed, arriving through the one hole left in it, and the byte-identical EDF+D twin\n timed it correctly, which is what gives it away.\n\n Continuity is what makes this recoverable: record i sits at `origin + i * duration`,\n so any readable record determines the origin for all of them.\n */\n const origin = originOf(annotationData.recordStarts, file.header.recordDuration);\n if (origin === null) return { starts: null, diagnostics };\n\n const contiguous = new Float64Array(file.recordCount);\n for (let i = 0; i < file.recordCount; i++) {\n contiguous[i] = origin + i * file.header.recordDuration;\n }\n\n /*\n A file marked continuous whose own records disagree about it.\n\n Nothing looked at records past the first, so an EDF+C file whose records are in fact\n spread out was timed as though they were contiguous and said nothing. The records are\n being read here anyway, so the contradiction costs nothing to notice — and it is the\n file, not the reader, that has to be wrong for this to fire.\n\n Compared against what the file can express, not for equality. 0.4.41 asked whether the\n two doubles were the same, which they are not: a recording of 0.1s records sitting at\n 0.1, 0.2, 0.3 ... is contiguous by construction, and 0.1 + 2 * 0.1 is\n 0.30000000000000004. Two of its eight records were reported as contradicting\n continuity, on an ordinary file — and under --strict that was a failed run. The\n smallest interval the recording distinguishes is one sample of its fastest channel;\n anything below half of that is arithmetic, not a gap. `canCarry` has already refused\n origins where the double spacing swamps that interval, so the representation error is\n under the tolerance by construction rather than by hope.\n */\n const tolerance = finestInterval(file) / 2;\n const contradicting = annotationData.recordStarts.filter(\n (declared, i) =>\n typeof declared === 'number' && Math.abs(declared - (contiguous[i] as number)) > tolerance,\n ).length;\n if (contradicting > 0) {\n const continuous = file.header.isBdf ? 'BDF+C' : 'EDF+C';\n const discontinuous = file.header.isBdf ? 'BDF+D' : 'EDF+D';\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n /*\n The markers as the file spells them.\n\n `continuity` normalises `BDF+C` to the internal `EDF+C` tag, and that tag reached\n the message: a BDF+ recording was told it is \"marked continuous (EDF+C)\" — a string\n it does not contain — and advised that it \"should have been marked EDF+D\", which is\n not a value BDF+ defines. A reader grepping the header for either finds nothing.\n\n The sibling discontinuous warning has done this since 0.3.x: `${'$'}{isBdf ? 'BDF+D' :\n 'EDF+D'}`. Same code, same header field, and the continuous branch never got it.\n */\n message:\n `This file is marked continuous (${continuous}), but ${contradicting} of its ` +\n `${counted(file.recordCount, 'data record')} ` +\n `${contradicting === 1 ? 'says it starts' : 'say they start'} somewhere other than ` +\n `where continuity puts ${contradicting === 1 ? 'it' : 'them'}.`,\n hint:\n `Times are written as if the records were contiguous, which is what ${continuous} ` +\n `means. If the recording really has gaps, the file should have been marked ` +\n `${discontinuous}.`,\n });\n }\n const first = origin;\n const last = contiguous[file.recordCount - 1] ?? first;\n if (!canCarry(last, file)) {\n diagnostics.push(unusableOrigin(first, file));\n return { starts: null, diagnostics };\n }\n /*\n An origin of zero is the same as no origin, for timing. It is not the same for the\n check above.\n\n This returned early on `origin === 0`, which is right about the times — contiguous\n starts from zero are what timing from zero already produces — and skipped the\n contradiction check on the way past. So an EDF+C file whose records say 0, 5 and 10 on\n one-second records went unreported, while the same file shifted one second, saying 1, 6\n and 11, was reported. The contradiction is in records 1 and 2 either way; where record 0\n happens to sit decides nothing about it.\n */\n if (origin === 0) return { starts: null, diagnostics };\n return { starts: contiguous, diagnostics };\n }\n\n if (file.annotationSignals.length === 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n 'This file is marked discontinuous but has no annotation channel, so where its ' +\n 'records sit in time is not recorded anywhere.',\n hint: 'Times are written as if the records were contiguous. Any gaps are lost.',\n });\n return { starts: null, diagnostics };\n }\n\n /*\n A record with no readable time is placed from the origin the other records establish,\n not from zero.\n\n `i * recordDuration` assumed the recording began at zero, which is the one thing the\n other records are in a position to contradict: a file starting at 0.5s put its\n unreadable record at 0.000 while its neighbours sat at 1.5s and 2.5s. The guess is still\n a guess — a discontinuous file may have a gap exactly there — and it is still reported\n below, but starting it from where the recording actually begins is strictly closer, and\n it makes an EDF+D file agree with its byte-identical EDF+C twin about record 0.\n */\n const base = originOf(annotationData.recordStarts, file.header.recordDuration) ?? 0;\n const starts = new Float64Array(file.recordCount);\n const missing: number[] = [];\n for (let i = 0; i < file.recordCount; i++) {\n const declared = annotationData.recordStarts[i];\n if (declared === null || declared === undefined) {\n missing.push(i);\n starts[i] = base + i * file.header.recordDuration;\n } else {\n starts[i] = declared;\n }\n }\n\n if (missing.length > 0) {\n /*\n Cut by the function that cuts every other list in a sentence here.\n\n This one rolled its own: five items and a bare `…`, where `listed` shows eight and counts\n what it left — \"and 32 more\" on the rate warning, \"and 112 more\" on the leftover files.\n The count is the honest half, and 0.7.58 taught the shared one not to hide a single item\n behind a phrase longer than the item. None of that reached this message, because it was\n never asking. Two implementations of one job, differing in the limit and in what they say\n about the tail.\n */\n const shown = listed(missing.map(String));\n // \"1 of 3 data records carry ... their true position\" — the subject is the one, not the three.\n const one = missing.length === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n message:\n `${missing.length} of ${counted(file.recordCount, 'data record')} ${one ? 'carries' : 'carry'} no ` +\n `readable timekeeping annotation (record${one ? '' : 's'} ${shown}), so ` +\n `${one ? 'its' : 'their'} true position in time is unknown.`,\n hint: one\n ? 'That record is timed as if it were contiguous; treat its timestamp as unreliable.'\n : 'Those records are timed as if they were contiguous; treat their timestamps as unreliable.',\n });\n }\n\n /*\n Furthest from zero, in either direction.\n\n This took the signed maximum and seeded it with 0, so a recording whose records all sit\n at negative onsets never got past the seed: `furthest` stayed 0, which any interval can\n carry. Then the samples collapsed anyway, because the arithmetic that defeats a large\n positive origin defeats a large negative one identically — at -1e16 seconds, adding a\n 1-second sample interval leaves the double unchanged.\n\n A four-record recording of eight samples wrote two rows, exit 0, no warning. Its\n byte-for-byte positive mirror wrote all eight and explained why it had to time them from\n zero. Same file, same failure, opposite sign, opposite outcome — and the silent one is\n the one that loses data, which is exactly what unusableOrigin exists to prevent.\n */\n let furthest = 0;\n for (const start of starts) if (Math.abs(start) > Math.abs(furthest)) furthest = start;\n if (!canCarry(furthest, file)) {\n diagnostics.push(unusableOrigin(furthest, file));\n return { starts: null, diagnostics };\n }\n\n /*\n Two ways a record can put the time column out of order, and only one was being looked for.\n\n A record starting before the one before it is the obvious case. The other is a record\n starting before the one before it *ends*: starts of 0, 0.5 and 1.0 on one-second records\n are strictly increasing, so nothing fired, and the rows still came out 0.25, 0.5, 0.75,\n 0.5 — because record 0's samples run to 0.75 while record 1 begins at 0.5. Overlapping\n acquisition is what a device does when it re-sends a buffer, and the reader has no more\n to say about it than about the reversed case: every sample is written, in file order,\n with the time the file gives it.\n\n Contiguity is not overlap. A continuous recording has `starts[i] === starts[i-1] +\n duration` exactly, so the comparison is made strict by a fraction of the finest interval\n the recording can express — the same measure the origin check uses.\n */\n const slack = finestInterval(file) / 2;\n let outOfOrder = 0;\n let overlapping = 0;\n for (let i = 1; i < starts.length; i++) {\n const previous = starts[i - 1] as number;\n const current = starts[i] as number;\n if (current < previous) outOfOrder++;\n else if (current + slack < previous + file.header.recordDuration) overlapping++;\n }\n if (outOfOrder > 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `${counted(outOfOrder, 'data record')} ` +\n `${outOfOrder === 1 ? 'starts' : 'start'} earlier than the record before ` +\n `${outOfOrder === 1 ? 'it' : 'them'}.`,\n hint: 'Rows are written in file order, so the time column will not increase monotonically.',\n });\n }\n if (overlapping > 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `${counted(overlapping, 'data record')} ` +\n `${overlapping === 1 ? 'starts' : 'start'} before the record before ` +\n `${overlapping === 1 ? 'it' : 'them'} ends, so ${overlapping === 1 ? 'its' : 'their'} ` +\n `samples overlap in time.`,\n hint: 'Rows are written in file order, so the time column will not increase monotonically.',\n });\n }\n\n return { starts, diagnostics };\n}\n\n/**\n * The recording's origin, from the first record that states where it is.\n *\n * Records of a continuous recording sit end to end, so record `i` beginning at `t` puts the\n * origin at `t - i * duration`. Any one readable timekeeping TAL is therefore enough, which\n * is what stops one unreadable entry from costing the whole file its position in time.\n */\nfunction originOf(recordStarts: readonly (number | null)[], recordDuration: number): number | null {\n for (const [index, declared] of recordStarts.entries()) {\n if (typeof declared !== 'number') continue;\n const origin = declared - index * recordDuration;\n return Number.isFinite(origin) ? origin : null;\n }\n return null;\n}\n\n/**\n * Whether times this far out can still tell one sample from the next.\n *\n * A double spaces its values further apart the larger they get: at 1e16 the gap is 2\n * seconds, so `t + 1` is `t`. Past that point a recording's declared origin stops being a\n * position and becomes a wall — the arithmetic that places records and samples returns the\n * origin itself, whatever is added to it.\n *\n * The finest thing that has to survive is the gap between two consecutive samples of the\n * fastest channel, since that is what the time column is made of. If that survives, so does\n * a whole record.\n */\nfunction canCarry(origin: number, file: EdfFile): boolean {\n if (!Number.isFinite(origin)) return false;\n // Asked of the origin furthest from zero, whichever side it is on: the spacing of doubles\n // grows with magnitude, not with value, so -1e16 and +1e16 fail this identically.\n return origin + finestInterval(file) > origin;\n}\n\n/**\n * The shortest span this recording can tell apart: one sample of its fastest channel.\n *\n * The time column is made of these, so nothing below one is a distinction the file is in a\n * position to make — which is what makes it the right size for both the \"can this origin\n * still separate two samples\" question and the \"is this record really somewhere else\"\n * question.\n */\nfunction finestInterval(file: EdfFile): number {\n let interval = file.header.recordDuration;\n for (const signal of file.header.signals) {\n if (signal.isAnnotations || !(signal.samplesPerRecord > 0)) continue;\n const step = file.header.recordDuration / signal.samplesPerRecord;\n if (step > 0 && step < interval) interval = step;\n }\n return interval;\n}\n\n/**\n * An origin the file's own arithmetic cannot express, reported rather than acted on.\n *\n * Two things went wrong when this was taken at face value, both of them quiet. A file whose\n * records all collapsed onto one instant made the recording zero seconds long, and the\n * window resolver — which had no reason to suspect the recording rather than the request —\n * blamed a flag nobody had passed:\n *\n * error: --start 100000000000000000s is at or past the end of this\n * 100000000000000000s recording.\n *\n * Slightly below that, the collapse is partial: `records[i].start + recordDuration` equals\n * the start again, so the test for \"does this record overlap the window\" fails for every\n * record whose neighbour rounded onto it. A twelve-row recording wrote four rows, exit 0,\n * no warning — the eight that vanished looked exactly like a file that never had them.\n *\n * Timing from zero is what the file did before 0.4.9 taught it to honour the first\n * timekeeping TAL, and at this magnitude it is the only column that can hold distinct\n * values. The origin is lost, so this says so.\n */\nfunction unusableOrigin(origin: number, file: EdfFile): Diagnostic {\n /*\n `Infinity` is a token, not a distance.\n\n `plain` expands exponent notation and hands anything else back, so a non-finite origin\n arrived on screen as \"place it Infinitys from its own start date\" — the exact form\n `formatDuration` exists to keep off the screen, its comment naming \"NaNs\" and \"Infinitys\"\n as what the fallback used to print, and the word the rest of this tool uses for the same\n quantity is \"unknown\". The origin overflows for the reason the recording's length does:\n a record duration near the top of a double, multiplied out over three records, leaves it.\n */\n const away = Number.isFinite(origin)\n ? `${plain(origin)}s from its own start date`\n : 'further from its own start date than a number can hold';\n return {\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `This recording's timekeeping annotations place it ${away}, which is too far out for ` +\n `its ${plain(file.header.recordDuration)}s records to be told apart: at that magnitude ` +\n `adding a sample interval leaves the number unchanged.`,\n hint:\n 'Sample times are written from zero instead, so every row is present and the column ' +\n 'increases. Add the onsets in annotations.csv to recover absolute times if you need them.',\n };\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit distance counting a swap of two adjacent characters as one edit, not two.
|
|
3
|
+
*
|
|
4
|
+
* Damerau's restricted form, which needs the row two back and so keeps three rather than the
|
|
5
|
+
* two plain Levenshtein needs. The strings compared here are option names and channel labels,
|
|
6
|
+
* none of them long, so the extra row costs nothing worth measuring.
|
|
7
|
+
*
|
|
8
|
+
* Shared because a transposition is the commonest typo there is and the two callers were
|
|
9
|
+
* disagreeing about it. 0.7.232 made the option suggester count `--hlep` as one edit from
|
|
10
|
+
* `--help`, and gave its reasoning: plain Levenshtein charges two, which put `--hlep`,
|
|
11
|
+
* `--jsno` and `--gzpi` past the allowance and answered them with nothing at all. Channel
|
|
12
|
+
* names kept their own copy of the two-row version, and they are the side a person actually
|
|
13
|
+
* types by hand — long, unfamiliar and copied off a screen. On a recording carrying `EEG` and
|
|
14
|
+
* `ECG`, `--channels EGC` scored two against both, so the tie was broken by file order and the
|
|
15
|
+
* answer was
|
|
16
|
+
*
|
|
17
|
+
* No channel named "EGC". Did you mean "EEG", "ECG"?
|
|
18
|
+
*
|
|
19
|
+
* naming the wrong one first. `suggest` in convert/channels.ts has a comment about precisely
|
|
20
|
+
* that outcome — "a suggestion that is retypeable, close, and about the wrong signal. Taking
|
|
21
|
+
* it converts a heart trace under the belief it is an EEG, and the run succeeds."
|
|
22
|
+
*/
|
|
23
|
+
export declare function editDistance(a: string, b: string): number;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit distance counting a swap of two adjacent characters as one edit, not two.
|
|
3
|
+
*
|
|
4
|
+
* Damerau's restricted form, which needs the row two back and so keeps three rather than the
|
|
5
|
+
* two plain Levenshtein needs. The strings compared here are option names and channel labels,
|
|
6
|
+
* none of them long, so the extra row costs nothing worth measuring.
|
|
7
|
+
*
|
|
8
|
+
* Shared because a transposition is the commonest typo there is and the two callers were
|
|
9
|
+
* disagreeing about it. 0.7.232 made the option suggester count `--hlep` as one edit from
|
|
10
|
+
* `--help`, and gave its reasoning: plain Levenshtein charges two, which put `--hlep`,
|
|
11
|
+
* `--jsno` and `--gzpi` past the allowance and answered them with nothing at all. Channel
|
|
12
|
+
* names kept their own copy of the two-row version, and they are the side a person actually
|
|
13
|
+
* types by hand — long, unfamiliar and copied off a screen. On a recording carrying `EEG` and
|
|
14
|
+
* `ECG`, `--channels EGC` scored two against both, so the tie was broken by file order and the
|
|
15
|
+
* answer was
|
|
16
|
+
*
|
|
17
|
+
* No channel named "EGC". Did you mean "EEG", "ECG"?
|
|
18
|
+
*
|
|
19
|
+
* naming the wrong one first. `suggest` in convert/channels.ts has a comment about precisely
|
|
20
|
+
* that outcome — "a suggestion that is retypeable, close, and about the wrong signal. Taking
|
|
21
|
+
* it converts a heart trace under the belief it is an EEG, and the run succeeds."
|
|
22
|
+
*/
|
|
23
|
+
export function editDistance(a, b) {
|
|
24
|
+
let twoBack = [];
|
|
25
|
+
let previous = Array.from({ length: b.length + 1 }, (_unused, i) => i);
|
|
26
|
+
for (let i = 1; i <= a.length; i++) {
|
|
27
|
+
const current = new Array(b.length + 1).fill(0);
|
|
28
|
+
current[0] = i;
|
|
29
|
+
for (let j = 1; j <= b.length; j++) {
|
|
30
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
31
|
+
let best = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + cost);
|
|
32
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
33
|
+
best = Math.min(best, twoBack[j - 2] + 1);
|
|
34
|
+
}
|
|
35
|
+
current[j] = best;
|
|
36
|
+
}
|
|
37
|
+
twoBack = previous;
|
|
38
|
+
previous = current;
|
|
39
|
+
}
|
|
40
|
+
return previous[b.length];
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=distance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"distance.js","sourceRoot":"","sources":["../../src/format/distance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,YAAY,CAAC,CAAS,EAAE,CAAS;IAC/C,IAAI,OAAO,GAAa,EAAE,CAAC;IAC3B,IAAI,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IACvE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,MAAM,OAAO,GAAG,IAAI,KAAK,CAAS,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACxD,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACnC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC3C,IAAI,IAAI,GAAG,IAAI,CAAC,GAAG,CAChB,QAAQ,CAAC,CAAC,CAAY,GAAG,CAAC,EAC1B,OAAO,CAAC,CAAC,GAAG,CAAC,CAAY,GAAG,CAAC,EAC7B,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAY,GAAG,IAAI,CACnC,CAAC;YACF,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBACrE,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAY,GAAG,CAAC,CAAC,CAAC;YACxD,CAAC;YACD,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;QACpB,CAAC;QACD,OAAO,GAAG,QAAQ,CAAC;QACnB,QAAQ,GAAG,OAAO,CAAC;IACrB,CAAC;IACD,OAAO,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAW,CAAC;AACtC,CAAC","sourcesContent":["/**\n * Edit distance counting a swap of two adjacent characters as one edit, not two.\n *\n * Damerau's restricted form, which needs the row two back and so keeps three rather than the\n * two plain Levenshtein needs. The strings compared here are option names and channel labels,\n * none of them long, so the extra row costs nothing worth measuring.\n *\n * Shared because a transposition is the commonest typo there is and the two callers were\n * disagreeing about it. 0.7.232 made the option suggester count `--hlep` as one edit from\n * `--help`, and gave its reasoning: plain Levenshtein charges two, which put `--hlep`,\n * `--jsno` and `--gzpi` past the allowance and answered them with nothing at all. Channel\n * names kept their own copy of the two-row version, and they are the side a person actually\n * types by hand — long, unfamiliar and copied off a screen. On a recording carrying `EEG` and\n * `ECG`, `--channels EGC` scored two against both, so the tie was broken by file order and the\n * answer was\n *\n * No channel named \"EGC\". Did you mean \"EEG\", \"ECG\"?\n *\n * naming the wrong one first. `suggest` in convert/channels.ts has a comment about precisely\n * that outcome — \"a suggestion that is retypeable, close, and about the wrong signal. Taking\n * it converts a heart trace under the belief it is an EEG, and the run succeeds.\"\n */\nexport function editDistance(a: string, b: string): number {\n let twoBack: number[] = [];\n let previous = Array.from({ length: b.length + 1 }, (_unused, i) => i);\n for (let i = 1; i <= a.length; i++) {\n const current = new Array<number>(b.length + 1).fill(0);\n current[0] = i;\n for (let j = 1; j <= b.length; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n let best = Math.min(\n (previous[j] as number) + 1,\n (current[j - 1] as number) + 1,\n (previous[j - 1] as number) + cost,\n );\n if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {\n best = Math.min(best, (twoBack[j - 2] as number) + 1);\n }\n current[j] = best;\n }\n twoBack = previous;\n previous = current;\n }\n return previous[b.length] as number;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "edf2csv",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.251",
|
|
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",
|