edf2csv 0.8.73 → 0.8.75

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.
@@ -93,6 +93,29 @@ export declare function assertPlanInput(input: {
93
93
  recordDuration?: unknown;
94
94
  recordCount?: unknown;
95
95
  }): void;
96
+ /**
97
+ * The two numbers a window is measured against, apart from the channel list.
98
+ *
99
+ * `resolveRange` is exported on its own and has its own signature block on the api page, and
100
+ * `buildPlan` calls it — so it was covered only from above. Called directly it took both
101
+ * numbers unexamined and answered with a range:
102
+ *
103
+ * resolveRange({ recordDuration: 1 }) // recordCount undefined
104
+ * // { startSeconds: 0, endSeconds: null, startRecord: 0, endRecord: 0 }
105
+ *
106
+ * A range over no records, returned as a fact about a recording. Its own opening comment
107
+ * already says why that is the wrong answer — "no error, no warning, and a range read back as
108
+ * `startSeconds: null, startRecord: null`, which is the 'takes the whole recording without
109
+ * saying so'" — about the three fields it does check. `resolveRange(42)` went the same way,
110
+ * since reading `.start` off a number is `undefined` rather than a throw.
111
+ *
112
+ * Split out rather than calling `assertPlanInput`, which would demand a channel list this
113
+ * function never looks at.
114
+ */
115
+ export declare function assertRecordShape(input: {
116
+ recordDuration?: unknown;
117
+ recordCount?: unknown;
118
+ }): void;
96
119
  /**
97
120
  * The recording to read, checked before it is opened.
98
121
  *
@@ -256,7 +256,29 @@ export function assertSignals(signals) {
256
256
  */
257
257
  export function assertPlanInput(input) {
258
258
  assertSignals(input.signals);
259
- const { recordDuration, recordCount } = input;
259
+ assertRecordShape(input);
260
+ }
261
+ /**
262
+ * The two numbers a window is measured against, apart from the channel list.
263
+ *
264
+ * `resolveRange` is exported on its own and has its own signature block on the api page, and
265
+ * `buildPlan` calls it — so it was covered only from above. Called directly it took both
266
+ * numbers unexamined and answered with a range:
267
+ *
268
+ * resolveRange({ recordDuration: 1 }) // recordCount undefined
269
+ * // { startSeconds: 0, endSeconds: null, startRecord: 0, endRecord: 0 }
270
+ *
271
+ * A range over no records, returned as a fact about a recording. Its own opening comment
272
+ * already says why that is the wrong answer — "no error, no warning, and a range read back as
273
+ * `startSeconds: null, startRecord: null`, which is the 'takes the whole recording without
274
+ * saying so'" — about the three fields it does check. `resolveRange(42)` went the same way,
275
+ * since reading `.start` off a number is `undefined` rather than a throw.
276
+ *
277
+ * Split out rather than calling `assertPlanInput`, which would demand a channel list this
278
+ * function never looks at.
279
+ */
280
+ export function assertRecordShape(input) {
281
+ const { recordDuration, recordCount } = input ?? {};
260
282
  if (typeof recordDuration !== 'number' || !Number.isFinite(recordDuration) || recordDuration <= 0) {
261
283
  throw new OptionError(`recordDuration must be a positive number of seconds, got ${describeValue(recordDuration)}.`);
262
284
  }
@@ -1 +1 @@
1
- {"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/convert/options.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Checking the options a caller passed, before anything is written.\n *\n * The command line has always validated these — `--decimals 1.5` is a usage error and always\n * has been — and the library did not, so the same value behaved differently depending on how\n * it arrived. `convert(file, { decimals: NaN })` resolved successfully having written whole\n * numbers into a column the caller had asked for decimals in, which is the worst of the\n * three: no error, no warning, and output that looks like a deliberate choice. `decimals: -1`\n * reached `toFixed` and came back as a bare RangeError from deep inside the formatter, naming\n * nothing the caller had written. `start: NaN` created the output directory, wrote\n * signals.csv, and then failed with a message about the input being unreadable — a partial\n * conversion, blamed on the file.\n *\n * These run at the top of `buildPlan`, which every path goes through before a directory is\n * created or a stream is opened, so a rejected option leaves nothing behind.\n */\n\n/** A problem with the options a caller passed, as opposed to a problem with the file. */\nexport class OptionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'OptionError';\n }\n}\n\n/**\n * The largest `--decimals` accepts, and what both documentation pages state.\n *\n * Not a limit of `toFixed`, whatever this comment used to say. `toFixed` takes 0 to 100 and\n * throws a RangeError at 101 — which is exactly the belief `MAX_DERIVED_DECIMALS` in\n * edf/scale.ts exists to correct, having once clamped the *derived* precision to 20 on the\n * same wrong grounds and rounded a magnetometer channel needing 23 places onto a grid three\n * digital codes wide, losing 69% of its samples in silence.\n *\n * Twenty is a bound on a number a person types by hand, not on what the format can express.\n * The derived precision, which nobody types, runs to 100 and says so.\n */\nexport const MAX_DECIMALS = 20;\n\nexport function assertOptions(options: {\n decimals?: number | undefined;\n start?: number | undefined;\n duration?: number | undefined;\n end?: number | undefined;\n layout?: string | undefined;\n channels?: readonly string[] | undefined;\n outputDir?: string | undefined;\n annotationsOnly?: boolean | undefined;\n gzip?: boolean | undefined;\n bom?: boolean | undefined;\n force?: boolean | undefined;\n checksum?: boolean | undefined;\n toStdout?: boolean | undefined;\n onProgress?: unknown;\n}): void {\n const { decimals } = options;\n if (decimals !== undefined) {\n if (!Number.isInteger(decimals) || decimals < 0 || decimals > MAX_DECIMALS) {\n throw new OptionError(\n `decimals must be a whole number between 0 and ${MAX_DECIMALS}, got ${describeValue(decimals)}.`,\n );\n }\n }\n\n /*\n `start` and `end` are positions on the recording's own clock, `duration` is a length.\n\n All three were held above zero, which is right for a length and wrong for a position: a\n recording timed from its first record's timekeeping annotation may sit before zero, and\n -100 is then where its first sample is. So a caller could read `plan.range` back as\n `recordingStartSeconds: -100` and not be allowed to ask for it — the same wall\n `parseTimeSpec` put in front of the command line until 0.5.120.\n\n Non-finite is still refused for all three, since NaN reaches a comparison as false and\n would take the whole recording without saying so.\n */\n for (const name of ['start', 'duration', 'end'] as const) {\n const value = options[name];\n if (value === undefined) continue;\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n throw new OptionError(`${name} must be a number of seconds, got ${describeValue(value)}.`);\n }\n /*\n A length below zero is refused for what it is, rather than for not being a number.\n\n `duration: -1` came back as \"duration must be a number of seconds, got -1\", which is\n not the reason and not true: -1 is a number of seconds, and this same call accepts it\n for `start` and for `end`, where a recording timed from before zero makes it an\n ordinary position. What is wrong is that a duration is a length, and no length is\n negative — which is what the command line says for the same value, and what the API\n reference has always said this check enforces.\n */\n if (name === 'duration' && value < 0) {\n throw new OptionError(`duration is a length of time, so it cannot be ${describeValue(value)}.`);\n }\n }\n\n /*\n Two words, and a caller who writes a third means something the tool cannot do.\n\n The command line has always rejected `--layout tall`. The library took it, put it in\n `plan.layout` for the caller to read back, and wrote the wide layout — so a programmatic\n caller with a typo got a conversion that was not the one they asked for, described by a\n plan that agreed with the typo. Every other option that has a shape is checked here; this\n one was added in 0.5.0 and never joined them.\n */\n const { layout } = options;\n if (layout !== undefined && layout !== 'wide' && layout !== 'long') {\n throw new OptionError(`layout must be \"wide\" or \"long\", got ${describeValue(layout)}.`);\n }\n\n /*\n A selection that names nothing is not the absence of a selection.\n\n `buildPlan` asks `options.channels.length > 0` before selecting, so an empty array fell\n through to the branch that means \"no channels option was given\" — and `convert(file, {\n channels: [] })` wrote every channel in the recording, resolved, and said nothing. That is\n the one shape of this the command line has always refused, in as many words: \"Returning\n undefined here would mean 'no --channels given' and convert everything, which is the\n opposite of what someone passing an empty list is asking for.\"\n\n A list of blanks is the same request written differently — it is what `''.split(',')`\n produces, which is how a caller building the array from user input arrives here — and it\n reached `selectChannels` and came back \"No channels were selected\", a sentence about the\n file rather than about the call. Both are the option being wrong, so both are refused\n here, before a directory exists.\n */\n /*\n An empty destination, refused here rather than by the filesystem.\n\n `convert(file, { outputDir: '' })` went the whole way to `mkdir('')` and came back a\n `ConversionError`: \"Cannot create \\\"\\\": part of the path does not exist. Check the path\n exists and that you have permission to write there.\" Advice about a path and a permission\n for a value that is neither, and a failure class that means the conversion went wrong\n where the option did.\n\n The command line refused this at 0.6.x, with the reasoning that `--out \"$DEST\"` and `DEST`\n unset is how it gets written by accident — and left the library, which a caller building\n the path in code reaches the same way. Not trimmed, for the reason given there: a\n directory whose name is a space is a strange thing to ask for, but it is a thing the\n filesystem has and a path is not a keyword.\n */\n /*\n And its shape, which was the one option with a value and no check on it.\n\n The empty string was refused and nothing else was, so `outputDir` failed two ways that the\n paragraph above describes for the flags. A value of the wrong type reached `path.join` and\n came back as a Node error about an argument this caller never passed:\n\n convert('rec.edf', { outputDir: 42 })\n TypeError: The \"path\" argument must be of type string. Received type number (42)\n\n And `null` — which is what `JSON.parse` of a config file gives for a field left unset, the\n same door `1` and `'true'` come through — was not an error at all. It is not `undefined`,\n so it never meant \"use the default\", but every read of it is `?? default` or a truthiness\n test, so that is what it did: the rows went to `<recording>_csv` beside the input, a\n directory the caller had not named, and the run reported success.\n */\n const { outputDir } = options;\n if (outputDir !== undefined && typeof outputDir !== 'string') {\n throw new OptionError(`outputDir must be a path, got ${describeValue(outputDir)}.`);\n }\n if (outputDir === '') {\n throw new OptionError('outputDir is empty. Give a directory, for example \"./converted\".');\n }\n\n /*\n The six flags, every one of which is read as `=== true` where it is read.\n\n Which means a value that is not a boolean is not merely tolerated: it is taken as the\n opposite of what it says. `convert(file, { annotationsOnly: 'true' })` wrote every signal\n the caller had asked to leave out; `{ gzip: 1 }` wrote plain CSVs under names ending\n `.csv`, so a caller who then opened `signals.csv.gz` found nothing there. No error, no\n warning, and output that looks like a deliberate choice — which is the sentence at the\n top of this file, describing the case it was written for.\n\n `1` and `'true'` are how a flag arrives from `JSON.parse` of a config file, from a query\n string, or from a CLI wrapper that did not coerce; none of them is a caller being\n careless in a way TypeScript would catch, since the callers this reaches are the ones not\n using it. The layout check above states the rule these were missing from: \"Every other\n option that has a shape is checked here.\"\n */\n for (const name of ['annotationsOnly', 'gzip', 'bom', 'force', 'checksum', 'toStdout'] as const) {\n const value = options[name];\n if (value !== undefined && typeof value !== 'boolean') {\n throw new OptionError(`${name} must be true or false, got ${describeValue(value)}.`);\n }\n }\n\n /*\n The one option that is called rather than read, and the only one that was not checked.\n\n `convert` invokes it as `options.onProgress?.(...)` once a record has been written, so a\n value that is not a function passes every check here, opens the destination, writes rows\n into it and then fails from inside the loop:\n\n convert('rec.edf', { outputDir: 'out', onProgress: 'every record' })\n ConversionError: The onProgress callback threw: options.onProgress is not a function\n\n A callback that threw is what that sentence reports, and no callback was given; the text\n after the colon names an expression inside this package. Worse is what it leaves: `out`\n exists with a half-written signals.csv in it, which is the case the paragraph at the top\n of this file describes — \"`start: NaN` created the output directory, wrote signals.csv,\n and then failed with a message about the input being unreadable — a partial conversion,\n blamed on the file\". Checked here, the same call writes nothing and says which argument\n is wrong.\n */\n const { onProgress } = options;\n if (onProgress !== undefined && typeof onProgress !== 'function') {\n throw new OptionError(`onProgress must be a function, got ${describeValue(onProgress)}.`);\n }\n\n const { channels } = options;\n if (channels !== undefined) {\n /*\n A list of strings, checked as one. `selectChannels` calls `.trim()` on every term, so a\n caller who passed the string `'ECG'` had it iterated character by character and was told\n `No channel named \"E\"`, and one who passed `[1]` — a position, reasonably enough — got\n `TypeError: rawTerm.trim is not a function` out of the middle of the selector, naming\n nothing they had written. Both are the option being the wrong shape, which is the case\n this function exists for.\n */\n if (!Array.isArray(channels) || channels.some((term) => typeof term !== 'string')) {\n throw new OptionError(`channels must be a list of channel names, got ${describeValue(channels)}.`);\n }\n if (channels.every((term) => term.trim() === '')) {\n throw new OptionError('channels was given but lists no channel names.');\n }\n }\n}\n\n/**\n * The channel list two exported functions take, checked the way their other argument is.\n *\n * `selectChannels(signals, terms)` has checked `terms` since 0.6.x — \"`'ECG'` was iterated\n * character by character and answered `No channel named \\\"E\\\"`… naming nothing the caller had\n * written\" — and never checked `signals`, which is the argument in front of it. Passing one\n * signal where the list goes, or a header where its `signals` goes, came back as\n * `TypeError: signals.filter is not a function`: a local of this package, over a value the\n * caller did write.\n *\n * `buildColumnNames` is worse off, because a string is iterable. `buildColumnNames('ECG')`\n * returned `Map { null => 'undefined_chundefined' }` and no error at all — a column name for a\n * channel that does not exist, keyed by a position that is not one.\n *\n * The bad entry is named by position rather than the whole list being printed back: a header\n * may declare hundreds of channels, and a message is not the place for all of them.\n */\nexport function assertSignals(signals: unknown): void {\n if (!Array.isArray(signals)) {\n throw new OptionError(\n `signals must be the channel list from a header, got ${describeValue(signals)}.`,\n );\n }\n const wrong = signals.findIndex(\n (signal) =>\n typeof signal !== 'object' ||\n signal === null ||\n typeof (signal as { index?: unknown }).index !== 'number',\n );\n if (wrong !== -1) {\n throw new OptionError(\n `signals[${wrong}] is not a channel from a header, got ${describeValue(signals[wrong])}.`,\n );\n }\n}\n\n/**\n * What `buildPlan` is told about the recording, checked the way what it is asked for is.\n *\n * `assertOptions` runs at the top of `buildPlan` and covers the second argument completely.\n * The first was not looked at, and it is the one carrying the numbers every figure in the plan\n * is derived from. Two of them missing produced a plan rather than an error:\n *\n * buildPlan({ signals, recordDuration: 1 }, {})\n * // groups: 3, estimate.rows: 0, range.endSeconds: null\n *\n * A plan saying the conversion writes nothing, handed back as an answer — which is the \"takes\n * the whole recording without saying so\" this checker exists to stop, one field over. A record\n * count below zero was worse: it came back as\n *\n * TimeRangeError: --start 0s is at or past the end of this -5s recording.\n *\n * a flag the caller never passed, about a recording that cannot exist, blaming the request for\n * the input. And `recordDuration: '1'` was coerced by the arithmetic and accepted, where the\n * same string is refused for `end` two functions down.\n *\n * A real header cannot produce any of them: the parser refuses a record duration that is not a\n * positive number, \"Infinity\" included.\n */\nexport function assertPlanInput(input: {\n signals?: unknown;\n recordDuration?: unknown;\n recordCount?: unknown;\n}): void {\n assertSignals(input.signals);\n const { recordDuration, recordCount } = input;\n if (typeof recordDuration !== 'number' || !Number.isFinite(recordDuration) || recordDuration <= 0) {\n throw new OptionError(\n `recordDuration must be a positive number of seconds, got ${describeValue(recordDuration)}.`,\n );\n }\n if (!Number.isInteger(recordCount) || (recordCount as number) < 0) {\n throw new OptionError(\n `recordCount must be a whole number of data records, got ${describeValue(recordCount)}.`,\n );\n }\n}\n\n/**\n * The recording to read, checked before it is opened.\n *\n * `EdfFile.open` hands whatever it is given to `fs`, and the refusal comes back as an\n * `EdfError` coded `UNREADABLE`, hinted \"Check the path is spelled the way it is on disk and\n * that you have permission to read it\" — advice about a path, over a value that is not one,\n * filed as a problem with the recording rather than with the call. `convert({ input: 'a.edf' })`,\n * which is the option-bag shape the second parameter has, answered `Cannot read \"[object\n * Object]\"`; `convert(['a.edf', 'b.edf'])` answered `Cannot read \"a.edf,b.edf\"`, a path the\n * caller never wrote, because `String` of an array joins it with commas.\n *\n * The empty string is left to `fs`, which has no such file and says so truthfully — the same\n * reasoning `outputDir` states for not trimming: a path is not a keyword.\n */\nexport function assertInputPath(input: unknown): void {\n if (typeof input !== 'string') {\n throw new OptionError(`input must be a path to a recording, got ${describeValue(input)}.`);\n }\n}\n\n/**\n * How a rejected value reads in the refusal: numbers bare, everything else quoted so its\n * type is visible.\n *\n * `JSON.stringify` has no text for a function or a symbol — it returns `undefined`, not a\n * string — so `layout: () => 'long'` came back as `layout must be \"wide\" or \"long\", got\n * undefined.`, which names the one value that does not raise this: every option here is\n * optional, and `undefined` is how a caller says they are not passing it. `input` was worse,\n * since `convert(undefined)` and `convert(someFunction)` then produced the same sentence, and\n * the first is a forgotten argument while the second is a wrong one.\n *\n * Exported because time-range.ts had the identical function, fixed there and not here — the\n * same two-copies-of-one-helper the derived precision and the pluraliser were each pulled\n * together for.\n */\nexport function describeValue(value: unknown): string {\n return typeof value === 'number' ? String(value) : JSON.stringify(value) ?? String(value);\n}\n"]}
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/convert/options.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Checking the options a caller passed, before anything is written.\n *\n * The command line has always validated these — `--decimals 1.5` is a usage error and always\n * has been — and the library did not, so the same value behaved differently depending on how\n * it arrived. `convert(file, { decimals: NaN })` resolved successfully having written whole\n * numbers into a column the caller had asked for decimals in, which is the worst of the\n * three: no error, no warning, and output that looks like a deliberate choice. `decimals: -1`\n * reached `toFixed` and came back as a bare RangeError from deep inside the formatter, naming\n * nothing the caller had written. `start: NaN` created the output directory, wrote\n * signals.csv, and then failed with a message about the input being unreadable — a partial\n * conversion, blamed on the file.\n *\n * These run at the top of `buildPlan`, which every path goes through before a directory is\n * created or a stream is opened, so a rejected option leaves nothing behind.\n */\n\n/** A problem with the options a caller passed, as opposed to a problem with the file. */\nexport class OptionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'OptionError';\n }\n}\n\n/**\n * The largest `--decimals` accepts, and what both documentation pages state.\n *\n * Not a limit of `toFixed`, whatever this comment used to say. `toFixed` takes 0 to 100 and\n * throws a RangeError at 101 — which is exactly the belief `MAX_DERIVED_DECIMALS` in\n * edf/scale.ts exists to correct, having once clamped the *derived* precision to 20 on the\n * same wrong grounds and rounded a magnetometer channel needing 23 places onto a grid three\n * digital codes wide, losing 69% of its samples in silence.\n *\n * Twenty is a bound on a number a person types by hand, not on what the format can express.\n * The derived precision, which nobody types, runs to 100 and says so.\n */\nexport const MAX_DECIMALS = 20;\n\nexport function assertOptions(options: {\n decimals?: number | undefined;\n start?: number | undefined;\n duration?: number | undefined;\n end?: number | undefined;\n layout?: string | undefined;\n channels?: readonly string[] | undefined;\n outputDir?: string | undefined;\n annotationsOnly?: boolean | undefined;\n gzip?: boolean | undefined;\n bom?: boolean | undefined;\n force?: boolean | undefined;\n checksum?: boolean | undefined;\n toStdout?: boolean | undefined;\n onProgress?: unknown;\n}): void {\n const { decimals } = options;\n if (decimals !== undefined) {\n if (!Number.isInteger(decimals) || decimals < 0 || decimals > MAX_DECIMALS) {\n throw new OptionError(\n `decimals must be a whole number between 0 and ${MAX_DECIMALS}, got ${describeValue(decimals)}.`,\n );\n }\n }\n\n /*\n `start` and `end` are positions on the recording's own clock, `duration` is a length.\n\n All three were held above zero, which is right for a length and wrong for a position: a\n recording timed from its first record's timekeeping annotation may sit before zero, and\n -100 is then where its first sample is. So a caller could read `plan.range` back as\n `recordingStartSeconds: -100` and not be allowed to ask for it — the same wall\n `parseTimeSpec` put in front of the command line until 0.5.120.\n\n Non-finite is still refused for all three, since NaN reaches a comparison as false and\n would take the whole recording without saying so.\n */\n for (const name of ['start', 'duration', 'end'] as const) {\n const value = options[name];\n if (value === undefined) continue;\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n throw new OptionError(`${name} must be a number of seconds, got ${describeValue(value)}.`);\n }\n /*\n A length below zero is refused for what it is, rather than for not being a number.\n\n `duration: -1` came back as \"duration must be a number of seconds, got -1\", which is\n not the reason and not true: -1 is a number of seconds, and this same call accepts it\n for `start` and for `end`, where a recording timed from before zero makes it an\n ordinary position. What is wrong is that a duration is a length, and no length is\n negative — which is what the command line says for the same value, and what the API\n reference has always said this check enforces.\n */\n if (name === 'duration' && value < 0) {\n throw new OptionError(`duration is a length of time, so it cannot be ${describeValue(value)}.`);\n }\n }\n\n /*\n Two words, and a caller who writes a third means something the tool cannot do.\n\n The command line has always rejected `--layout tall`. The library took it, put it in\n `plan.layout` for the caller to read back, and wrote the wide layout — so a programmatic\n caller with a typo got a conversion that was not the one they asked for, described by a\n plan that agreed with the typo. Every other option that has a shape is checked here; this\n one was added in 0.5.0 and never joined them.\n */\n const { layout } = options;\n if (layout !== undefined && layout !== 'wide' && layout !== 'long') {\n throw new OptionError(`layout must be \"wide\" or \"long\", got ${describeValue(layout)}.`);\n }\n\n /*\n A selection that names nothing is not the absence of a selection.\n\n `buildPlan` asks `options.channels.length > 0` before selecting, so an empty array fell\n through to the branch that means \"no channels option was given\" — and `convert(file, {\n channels: [] })` wrote every channel in the recording, resolved, and said nothing. That is\n the one shape of this the command line has always refused, in as many words: \"Returning\n undefined here would mean 'no --channels given' and convert everything, which is the\n opposite of what someone passing an empty list is asking for.\"\n\n A list of blanks is the same request written differently — it is what `''.split(',')`\n produces, which is how a caller building the array from user input arrives here — and it\n reached `selectChannels` and came back \"No channels were selected\", a sentence about the\n file rather than about the call. Both are the option being wrong, so both are refused\n here, before a directory exists.\n */\n /*\n An empty destination, refused here rather than by the filesystem.\n\n `convert(file, { outputDir: '' })` went the whole way to `mkdir('')` and came back a\n `ConversionError`: \"Cannot create \\\"\\\": part of the path does not exist. Check the path\n exists and that you have permission to write there.\" Advice about a path and a permission\n for a value that is neither, and a failure class that means the conversion went wrong\n where the option did.\n\n The command line refused this at 0.6.x, with the reasoning that `--out \"$DEST\"` and `DEST`\n unset is how it gets written by accident — and left the library, which a caller building\n the path in code reaches the same way. Not trimmed, for the reason given there: a\n directory whose name is a space is a strange thing to ask for, but it is a thing the\n filesystem has and a path is not a keyword.\n */\n /*\n And its shape, which was the one option with a value and no check on it.\n\n The empty string was refused and nothing else was, so `outputDir` failed two ways that the\n paragraph above describes for the flags. A value of the wrong type reached `path.join` and\n came back as a Node error about an argument this caller never passed:\n\n convert('rec.edf', { outputDir: 42 })\n TypeError: The \"path\" argument must be of type string. Received type number (42)\n\n And `null` — which is what `JSON.parse` of a config file gives for a field left unset, the\n same door `1` and `'true'` come through — was not an error at all. It is not `undefined`,\n so it never meant \"use the default\", but every read of it is `?? default` or a truthiness\n test, so that is what it did: the rows went to `<recording>_csv` beside the input, a\n directory the caller had not named, and the run reported success.\n */\n const { outputDir } = options;\n if (outputDir !== undefined && typeof outputDir !== 'string') {\n throw new OptionError(`outputDir must be a path, got ${describeValue(outputDir)}.`);\n }\n if (outputDir === '') {\n throw new OptionError('outputDir is empty. Give a directory, for example \"./converted\".');\n }\n\n /*\n The six flags, every one of which is read as `=== true` where it is read.\n\n Which means a value that is not a boolean is not merely tolerated: it is taken as the\n opposite of what it says. `convert(file, { annotationsOnly: 'true' })` wrote every signal\n the caller had asked to leave out; `{ gzip: 1 }` wrote plain CSVs under names ending\n `.csv`, so a caller who then opened `signals.csv.gz` found nothing there. No error, no\n warning, and output that looks like a deliberate choice — which is the sentence at the\n top of this file, describing the case it was written for.\n\n `1` and `'true'` are how a flag arrives from `JSON.parse` of a config file, from a query\n string, or from a CLI wrapper that did not coerce; none of them is a caller being\n careless in a way TypeScript would catch, since the callers this reaches are the ones not\n using it. The layout check above states the rule these were missing from: \"Every other\n option that has a shape is checked here.\"\n */\n for (const name of ['annotationsOnly', 'gzip', 'bom', 'force', 'checksum', 'toStdout'] as const) {\n const value = options[name];\n if (value !== undefined && typeof value !== 'boolean') {\n throw new OptionError(`${name} must be true or false, got ${describeValue(value)}.`);\n }\n }\n\n /*\n The one option that is called rather than read, and the only one that was not checked.\n\n `convert` invokes it as `options.onProgress?.(...)` once a record has been written, so a\n value that is not a function passes every check here, opens the destination, writes rows\n into it and then fails from inside the loop:\n\n convert('rec.edf', { outputDir: 'out', onProgress: 'every record' })\n ConversionError: The onProgress callback threw: options.onProgress is not a function\n\n A callback that threw is what that sentence reports, and no callback was given; the text\n after the colon names an expression inside this package. Worse is what it leaves: `out`\n exists with a half-written signals.csv in it, which is the case the paragraph at the top\n of this file describes — \"`start: NaN` created the output directory, wrote signals.csv,\n and then failed with a message about the input being unreadable — a partial conversion,\n blamed on the file\". Checked here, the same call writes nothing and says which argument\n is wrong.\n */\n const { onProgress } = options;\n if (onProgress !== undefined && typeof onProgress !== 'function') {\n throw new OptionError(`onProgress must be a function, got ${describeValue(onProgress)}.`);\n }\n\n const { channels } = options;\n if (channels !== undefined) {\n /*\n A list of strings, checked as one. `selectChannels` calls `.trim()` on every term, so a\n caller who passed the string `'ECG'` had it iterated character by character and was told\n `No channel named \"E\"`, and one who passed `[1]` — a position, reasonably enough — got\n `TypeError: rawTerm.trim is not a function` out of the middle of the selector, naming\n nothing they had written. Both are the option being the wrong shape, which is the case\n this function exists for.\n */\n if (!Array.isArray(channels) || channels.some((term) => typeof term !== 'string')) {\n throw new OptionError(`channels must be a list of channel names, got ${describeValue(channels)}.`);\n }\n if (channels.every((term) => term.trim() === '')) {\n throw new OptionError('channels was given but lists no channel names.');\n }\n }\n}\n\n/**\n * The channel list two exported functions take, checked the way their other argument is.\n *\n * `selectChannels(signals, terms)` has checked `terms` since 0.6.x — \"`'ECG'` was iterated\n * character by character and answered `No channel named \\\"E\\\"`… naming nothing the caller had\n * written\" — and never checked `signals`, which is the argument in front of it. Passing one\n * signal where the list goes, or a header where its `signals` goes, came back as\n * `TypeError: signals.filter is not a function`: a local of this package, over a value the\n * caller did write.\n *\n * `buildColumnNames` is worse off, because a string is iterable. `buildColumnNames('ECG')`\n * returned `Map { null => 'undefined_chundefined' }` and no error at all — a column name for a\n * channel that does not exist, keyed by a position that is not one.\n *\n * The bad entry is named by position rather than the whole list being printed back: a header\n * may declare hundreds of channels, and a message is not the place for all of them.\n */\nexport function assertSignals(signals: unknown): void {\n if (!Array.isArray(signals)) {\n throw new OptionError(\n `signals must be the channel list from a header, got ${describeValue(signals)}.`,\n );\n }\n const wrong = signals.findIndex(\n (signal) =>\n typeof signal !== 'object' ||\n signal === null ||\n typeof (signal as { index?: unknown }).index !== 'number',\n );\n if (wrong !== -1) {\n throw new OptionError(\n `signals[${wrong}] is not a channel from a header, got ${describeValue(signals[wrong])}.`,\n );\n }\n}\n\n/**\n * What `buildPlan` is told about the recording, checked the way what it is asked for is.\n *\n * `assertOptions` runs at the top of `buildPlan` and covers the second argument completely.\n * The first was not looked at, and it is the one carrying the numbers every figure in the plan\n * is derived from. Two of them missing produced a plan rather than an error:\n *\n * buildPlan({ signals, recordDuration: 1 }, {})\n * // groups: 3, estimate.rows: 0, range.endSeconds: null\n *\n * A plan saying the conversion writes nothing, handed back as an answer — which is the \"takes\n * the whole recording without saying so\" this checker exists to stop, one field over. A record\n * count below zero was worse: it came back as\n *\n * TimeRangeError: --start 0s is at or past the end of this -5s recording.\n *\n * a flag the caller never passed, about a recording that cannot exist, blaming the request for\n * the input. And `recordDuration: '1'` was coerced by the arithmetic and accepted, where the\n * same string is refused for `end` two functions down.\n *\n * A real header cannot produce any of them: the parser refuses a record duration that is not a\n * positive number, \"Infinity\" included.\n */\nexport function assertPlanInput(input: {\n signals?: unknown;\n recordDuration?: unknown;\n recordCount?: unknown;\n}): void {\n assertSignals(input.signals);\n assertRecordShape(input);\n}\n\n/**\n * The two numbers a window is measured against, apart from the channel list.\n *\n * `resolveRange` is exported on its own and has its own signature block on the api page, and\n * `buildPlan` calls it — so it was covered only from above. Called directly it took both\n * numbers unexamined and answered with a range:\n *\n * resolveRange({ recordDuration: 1 }) // recordCount undefined\n * // { startSeconds: 0, endSeconds: null, startRecord: 0, endRecord: 0 }\n *\n * A range over no records, returned as a fact about a recording. Its own opening comment\n * already says why that is the wrong answer — \"no error, no warning, and a range read back as\n * `startSeconds: null, startRecord: null`, which is the 'takes the whole recording without\n * saying so'\" — about the three fields it does check. `resolveRange(42)` went the same way,\n * since reading `.start` off a number is `undefined` rather than a throw.\n *\n * Split out rather than calling `assertPlanInput`, which would demand a channel list this\n * function never looks at.\n */\nexport function assertRecordShape(input: {\n recordDuration?: unknown;\n recordCount?: unknown;\n}): void {\n const { recordDuration, recordCount } = input ?? {};\n if (typeof recordDuration !== 'number' || !Number.isFinite(recordDuration) || recordDuration <= 0) {\n throw new OptionError(\n `recordDuration must be a positive number of seconds, got ${describeValue(recordDuration)}.`,\n );\n }\n if (!Number.isInteger(recordCount) || (recordCount as number) < 0) {\n throw new OptionError(\n `recordCount must be a whole number of data records, got ${describeValue(recordCount)}.`,\n );\n }\n}\n\n/**\n * The recording to read, checked before it is opened.\n *\n * `EdfFile.open` hands whatever it is given to `fs`, and the refusal comes back as an\n * `EdfError` coded `UNREADABLE`, hinted \"Check the path is spelled the way it is on disk and\n * that you have permission to read it\" — advice about a path, over a value that is not one,\n * filed as a problem with the recording rather than with the call. `convert({ input: 'a.edf' })`,\n * which is the option-bag shape the second parameter has, answered `Cannot read \"[object\n * Object]\"`; `convert(['a.edf', 'b.edf'])` answered `Cannot read \"a.edf,b.edf\"`, a path the\n * caller never wrote, because `String` of an array joins it with commas.\n *\n * The empty string is left to `fs`, which has no such file and says so truthfully — the same\n * reasoning `outputDir` states for not trimming: a path is not a keyword.\n */\nexport function assertInputPath(input: unknown): void {\n if (typeof input !== 'string') {\n throw new OptionError(`input must be a path to a recording, got ${describeValue(input)}.`);\n }\n}\n\n/**\n * How a rejected value reads in the refusal: numbers bare, everything else quoted so its\n * type is visible.\n *\n * `JSON.stringify` has no text for a function or a symbol — it returns `undefined`, not a\n * string — so `layout: () => 'long'` came back as `layout must be \"wide\" or \"long\", got\n * undefined.`, which names the one value that does not raise this: every option here is\n * optional, and `undefined` is how a caller says they are not passing it. `input` was worse,\n * since `convert(undefined)` and `convert(someFunction)` then produced the same sentence, and\n * the first is a forgotten argument while the second is a wrong one.\n *\n * Exported because time-range.ts had the identical function, fixed there and not here — the\n * same two-copies-of-one-helper the derived precision and the pluraliser were each pulled\n * together for.\n */\nexport function describeValue(value: unknown): string {\n return typeof value === 'number' ? String(value) : JSON.stringify(value) ?? String(value);\n}\n"]}
@@ -7,7 +7,7 @@
7
7
  * a message that shows the forms that work.
8
8
  */
9
9
  import { fixed, formatDuration } from '../format/number.js';
10
- import { assertOptions, describeValue } from './options.js';
10
+ import { assertOptions, assertRecordShape, describeValue, OptionError } from './options.js';
11
11
  export class TimeRangeError extends Error {
12
12
  constructor(message) {
13
13
  super(message);
@@ -327,7 +327,32 @@ export function resolveRange(options) {
327
327
  accepted; `{ start: '30' }` reached the past-the-end error and printed it with the value
328
328
  missing — `--start s is at or past the end of this 3s recording`.
329
329
  */
330
+ /*
331
+ The argument bag itself, before three properties are read off it.
332
+
333
+ `resolveRange(42)` read `.start` off a number, which is `undefined` rather than a throw, and
334
+ went on to answer with a range; `resolveRange(null)` reached the same line and came back as
335
+ `TypeError: Cannot read properties of null (reading 'start')`, naming a property of this
336
+ function's own parameter.
337
+ */
338
+ if (options === null || typeof options !== 'object') {
339
+ throw new OptionError(`resolveRange takes one object — the recording's shape and the window asked of it — ` +
340
+ `got ${describeValue(options)}.`);
341
+ }
330
342
  assertOptions({ start: options.start, duration: options.duration, end: options.end });
343
+ /*
344
+ And the two numbers the window is measured against, which that call does not cover.
345
+
346
+ `assertOptions` checks what was asked for; these are what it is asked of, and nothing
347
+ looked at them. `resolveRange({ start: 1, recordDuration: 1 })` — no `recordCount` —
348
+ answered `{ startSeconds: 0, endSeconds: null, startRecord: 0, endRecord: 0 }`: a window
349
+ over no records, handed back as a fact about a recording. `resolveRange(42)` reached the
350
+ same answer, since reading `.start` off a number is `undefined` rather than a throw.
351
+
352
+ The check `buildPlan` has made since 0.8.64, made here too, because this is exported on
353
+ its own — the same sentence the line above this one is here for.
354
+ */
355
+ assertRecordShape(options);
331
356
  // For a continuous file the recording spans recordCount * recordDuration. A
332
357
  // discontinuous one does not: a 10-second recording with a 95-second gap in the
333
358
  // middle still ends at 105 seconds. Deriving the span from the records' real
@@ -1 +1 @@
1
- {"version":3,"file":"time-range.js","sourceRoot":"","sources":["../../src/convert/time-range.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Parsing for the time-range options.\n *\n * Researchers write offsets in whatever form is natural for the recording in front\n * of them: seconds for a short ECG strip, `30m` into a sleep study, `01:23:45` when\n * reading off a clock. All three are accepted; anything ambiguous is rejected with\n * a message that shows the forms that work.\n */\n\nimport { fixed, formatDuration } from '../format/number.js';\nimport { assertOptions, describeValue } from './options.js';\n\nexport class TimeRangeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'TimeRangeError';\n }\n}\n\n/**\n * Every spelling of a unit this accepts, and what one of it is worth in seconds.\n *\n * Exported so the spellings can be enumerated by a test rather than listed a second time:\n * twelve of these sixteen are named nowhere in the tool or its documentation, and none of\n * their values was checked by anything. `hrs: 360` would have converted `--start 2hrs` from\n * twelve minutes in and said nothing, which is the one kind of mistake a window may not make.\n */\nexport const UNIT_SECONDS: Record<string, number> = {\n h: 3600,\n hr: 3600,\n hrs: 3600,\n hour: 3600,\n hours: 3600,\n m: 60,\n min: 60,\n mins: 60,\n minute: 60,\n minutes: 60,\n s: 1,\n sec: 1,\n secs: 1,\n second: 1,\n seconds: 1,\n ms: 0.001,\n};\n\nconst CLOCK = /^(?:(\\d+):)?(\\d{1,2}):(\\d{1,2}(?:\\.\\d+)?)$/u;\nconst UNIT_TOKEN = /(\\d+(?:\\.\\d+)?)\\s*([a-z]+)/giu;\n\n/**\n * Parse a duration or offset into seconds.\n *\n * Accepted: `90`, `90s`, `5m`, `1h30m`, `1h 30m 15s`, `00:30:00`, `30:00`, `250ms`.\n *\n * `allowNegative` is for the two options that name a position rather than a length.\n *\n * A recording is timed from its first record's timekeeping annotation, and nothing obliges that\n * to sit at or after zero: a file whose records run from -100 s to -97 s is one this tool reads,\n * times from -100, and describes with\n *\n * Timed from -100.000s (first sample; --start and --end use this clock)\n *\n * That line says the number can be typed straight back in, and it could not be. Every offset\n * such a recording has came back as \"not a time I understand\", so its whole clock was\n * unreachable and no window of it could be converted at all — the one file shape where a\n * window is refused for naming a moment the recording actually contains.\n *\n * A length below zero is still a different thing, and `--duration` still refuses one.\n */\nexport function parseTimeSpec(input: string, optionName?: string, allowNegative = false): number {\n /*\n A name for the value, when the caller has not given one.\n\n Every refusal below opens with it, and the CLI passes the flag — `--start \"1,5\" is not a\n time I understand`. This is also the function the api page sends other people's users to,\n and a caller who writes the one-argument call its neighbours take got\n\n TimeRangeError: undefined is empty. Try a value like 30s, 5m, or 00:30:00.\n\n on every one of the eleven messages in here. The paragraph below hardened `input` for\n exactly that caller; the argument beside it could still put the word `undefined` at the\n front of the sentence.\n\n Coerced rather than refused, unlike `input`: this one names the value in a message rather\n than being the value, so there is a right answer to fall back on and nothing is lost by\n using it. An empty or non-string name takes the same route, since `\"\" is empty.` is the\n same sentence with a different hole in it.\n */\n const named =\n typeof optionName === 'string' && optionName.trim() !== '' ? optionName : 'The value';\n /*\n Text, because this is the function the API page points other people's users at: \"Use\n `parseTimeSpec` if you want to accept those forms from your own users.\"\n\n A value arriving from a JSON config or a form field is a number as often as a string, and\n `{ \"start\": 30 }` came back as `TypeError: input.trim is not a function` — a variable name\n from inside this file, thrown past a caller who had done exactly what the page told them\n to. Every other refusal here is a TimeRangeError quoting the value and the option. The\n number is not coerced, for the reason `channels: 'ECG'` is not iterated: a shape that\n happens to work for 30 also swallows NaN, and the CLI already writes `String(raw)`.\n */\n if (typeof input !== 'string') {\n throw new TimeRangeError(\n `${named} must be given as text, not ${describeValue(input)}. ` +\n `Seconds are written \"30\", and the other forms are \"30s\", \"5m\", \"1h30m\", \"00:30:00\".`,\n );\n }\n const trimmed = input.trim().toLowerCase();\n if (trimmed === '') {\n throw new TimeRangeError(`${named} is empty. Try a value like 30s, 5m, or 00:30:00.`);\n }\n\n /*\n The sign is read off the front and applied to the whole value, so `-1h30m` is an hour and a\n half before the origin rather than an hour before it and half an hour after.\n\n Nothing may sit between the sign and the number, and a leading `+` is still refused: every\n other number this tool takes is refused when written in a form nobody types, and `+5` is\n exactly what `--decimals` and `--jobs` reject one flag over.\n */\n const negative = trimmed.startsWith('-');\n const text = negative ? trimmed.slice(1) : trimmed;\n\n /*\n A leading plus, named as such.\n\n Refusing it is deliberate — see the paragraph above — but it came back as \"is not a time I\n understand. Try 30s, 5m, 1h30m, 00:30:00, or a plain number of seconds\", which is the\n message for input that could not be read at all. Every part of `+5s` was read: the number\n parsed, the unit was looked up and found. The one thing wrong with it is a character the\n reader put there on purpose, and they were sent to re-check their unit spellings with `5m`\n sitting in the list of suggestions, differing from what they typed by the sign they cannot\n see is the problem. 0.7.102 made this argument about a space; this is the same sentence\n about the other character that reaches it. `--decimals` and `--jobs` both quote the value\n back and say what is wrong with it.\n\n Every plus, not the first one, and the leftover space with it — the same rule the spaced\n refusal below applies to spaces, for the same reason: a hint whose command this function\n refuses is worse than no hint. `+1h+30m` was answered with `1h+30m`, which comes straight\n back as \"is not a time I understand\", and `+ 5s` with a value that opened on a space.\n */\n if (text.startsWith('+')) {\n throw new TimeRangeError(\n `${named} \"${input}\" begins with a plus. Write the number on its own: ` +\n `${input.trim().replaceAll('+', '').trim()}`,\n );\n }\n const signed = (seconds: number): number =>\n assertFinite(negative ? -seconds : seconds, named, input, allowNegative);\n\n // Bare number means seconds.\n if (/^\\d+(?:\\.\\d+)?$/u.test(text)) return signed(Number(text));\n\n // Clock form: hh:mm:ss or mm:ss.\n const clock = CLOCK.exec(text);\n if (clock) {\n const hours = clock[1] === undefined ? 0 : Number(clock[1]);\n const minutes = Number(clock[2]);\n const seconds = Number(clock[3]);\n if (minutes >= 60 || seconds >= 60) {\n throw new TimeRangeError(\n `${named} \"${input}\" has a minutes or seconds field of 60 or more.`,\n );\n }\n return signed(hours * 3600 + minutes * 60 + seconds);\n }\n\n // Unit form: 1h30m, 1h30m 15s, 250ms. A number sits directly against its unit; see below.\n UNIT_TOKEN.lastIndex = 0;\n let total = 0;\n let matched = 0;\n let consumed = 0;\n /** Whether any token had a space between its number and its unit. See the refusal below. */\n let spaced = false;\n let match: RegExpExecArray | null;\n // Which units have already been seen, so a repeat can be rejected rather than added.\n const seen = new Set<number>();\n while ((match = UNIT_TOKEN.exec(text)) !== null) {\n const amount = Number(match[1]);\n const unit = match[2] ?? '';\n const scale = UNIT_SECONDS[unit];\n if (scale === undefined) {\n throw new TimeRangeError(\n `${named} \"${input}\" uses an unknown unit \"${unit}\". Use h, m, s or ms, ` +\n `or their long forms: hours, minutes, seconds.`,\n );\n }\n\n // \"1h1h\" is a typo, not a request for two hours. Summing repeated units silently\n // turned a slip into a plausible window that was quietly the wrong length. Units are\n // keyed by their scale so the aliases collapse together: \"1h30min20m\" is caught too.\n if (seen.has(scale)) {\n throw new TimeRangeError(\n `${named} \"${input}\" gives the same unit twice. ` +\n `Combine each unit once, as in 1h30m.`,\n );\n }\n seen.add(scale);\n\n total += amount * scale;\n matched++;\n // Counted with the whitespace taken out, so the comparison below is against the same\n // string on both sides and a space inside a token reaches the refusal written for it\n // rather than the one about input that could not be read at all.\n consumed += match[0].replace(/\\s+/gu, '').length;\n if (/\\s/u.test(match[0])) spaced = true;\n }\n\n // Reject partially-understood input like \"5x\" or \"1h banana\".\n if (matched === 0 || consumed !== text.replace(/\\s+/gu, '').length) {\n throw new TimeRangeError(\n `${named} \"${input}\" is not a time I understand. ` +\n `Try 30s, 5m, 1h30m, 00:30:00, or a plain number of seconds.`,\n );\n }\n\n /*\n A space between a number and its unit, named as such.\n\n `5 min` is refused on purpose — cli-reference sets the rule out, and it is what keeps\n `1 2h` from being read as anything — but it came back as \"is not a time I understand. Try\n 30s, 5m, 1h30m, 00:30:00, or a plain number of seconds\", which is the message for input\n that could not be read at all. Every part of `5 min` was read: the number parsed, the unit\n was looked up in the table and found. The one thing wrong with it is a space, and the\n reader was sent to re-check their unit spellings instead — with `5m` sitting in the list of\n suggestions, differing from what they typed by a character they cannot see is the problem.\n\n Only reachable now that the count above ignores whitespace; before, the length check caught\n these first and there was nothing left to tell them apart by.\n */\n if (spaced) {\n throw new TimeRangeError(\n `${named} \"${input}\" puts a space between a number and its unit. ` +\n `Write them together: ${input.trim().replace(/(\\d)\\s+([a-z])/giu, '$1$2')}`,\n );\n }\n\n return signed(total);\n}\n\nfunction assertFinite(\n value: number,\n optionName: string,\n input: string,\n allowNegative: boolean,\n): number {\n /*\n Overflow answers for itself, rather than borrowing the sentence about signs.\n\n One branch covered two rejections. `--start` with four hundred nines is a positive value\n that `Number` returns as `Infinity`, and the answer was `is not a valid non-negative\n time` — which names the one thing about it that is not the problem. Worse on the two\n options this is called with `allowNegative` for: `--start` and `--end` take a sign on\n purpose, since a recording timed from before zero has no other way to be addressed, so\n \"non-negative\" is not their rule at all and overflow is the only way they reach here.\n */\n if (!Number.isFinite(value)) {\n throw new TimeRangeError(\n `${optionName} \"${input}\" is further from zero than a number of seconds can hold.`,\n );\n }\n if (!allowNegative && value < 0) {\n throw new TimeRangeError(`${optionName} \"${input}\" is not a valid non-negative time.`);\n }\n return value;\n}\n\nexport interface ResolvedRange {\n /** Inclusive start, in seconds from the beginning of the recording. */\n startSeconds: number;\n /** Exclusive end, in seconds. */\n endSeconds: number;\n /** First data record touching the window. */\n startRecord: number;\n /** One past the last data record touching the window. */\n endRecord: number;\n /** True when the window covers the whole recording. */\n isWholeRecording: boolean;\n /** Earliest record start, including EDF+D timing gaps. */\n recordingStartSeconds: number;\n /** End of the latest record, including EDF+D timing gaps. */\n recordingEndSeconds: number;\n}\n\n/**\n * Slack for comparisons at sample/window boundaries.\n *\n * A nanosecond is far below any real sampling interval — 20 kHz is 50 microseconds — so it\n * absorbs the arithmetic error in `recordStart + sample / rate` without reaching a\n * neighbouring sample.\n */\nexport const BOUNDARY_TOLERANCE = 1e-9;\n\n/**\n * The slack to use for a channel sampled this often.\n *\n * Never past half a sample interval, because slack that reaches the next sample stops being\n * slack — half is the most that cannot, since the nearest sample below a bound sits a whole\n * interval away from it. `Math.min` takes exactly half at rates above 500 MHz, which is where\n * this said \"never as much as half\" of a formula that returns it. A fixed nanosecond was\n * applied whatever the rate, and the format does not\n * oblige the interval to be larger than it: EDF's record duration is an 8-character field\n * that accepts `1e-9`. A recording of two 1 ns records holding ten samples each wrote ten of\n * its twenty rows — the window ends at 2e-9, the comparison asked for `time < 2e-9 - 1e-9`,\n * and the entire second record failed it. Exit 0, no warning, half the samples gone.\n */\nexport function toleranceFor(rate: number): number {\n if (!(rate > 0) || !Number.isFinite(rate)) return BOUNDARY_TOLERANCE;\n return Math.min(BOUNDARY_TOLERANCE, 1 / rate / 2);\n}\n\n/** Match the exact half-open boundary rules used while writing signal rows. */\nexport function sampleTimeIsInRange(\n time: number,\n startSeconds: number,\n endSeconds: number,\n tolerance: number = BOUNDARY_TOLERANCE,\n): boolean {\n return time >= startSeconds - tolerance && time < endSeconds - tolerance;\n}\n\n/** Count samples from one record that fall inside a half-open requested window. */\nexport function countSamplesInRange(options: {\n recordStart: number;\n rate: number;\n samplesPerRecord: number;\n startSeconds: number;\n endSeconds: number;\n}): number {\n /*\n A record with no place on the clock contributes no rows, which is what the conversion\n already does with one: its sample times are non-finite, so every one of them fails the\n range test and none is written. The arithmetic below reached `Infinity - Infinity` and\n answered `NaN`, which `--info` then printed — \"Would write NaN rows, roughly NaN B.\" on a\n recording that goes on to write eight. Reachable from a header: a record duration of 1e308\n is five characters in an eight-character field, and the third record of one is past what a\n double can hold.\n */\n if (!Number.isFinite(options.recordStart)) return 0;\n const slack = toleranceFor(options.rate);\n const lower = Math.ceil(\n (options.startSeconds - slack - options.recordStart) * options.rate,\n );\n const upper = Math.ceil(\n (options.endSeconds - slack - options.recordStart) * options.rate,\n );\n const first = Math.max(0, Math.min(options.samplesPerRecord, lower));\n const last = Math.max(0, Math.min(options.samplesPerRecord, upper));\n return Math.max(0, last - first);\n}\n\n/**\n * Whether two instants differ only by the arithmetic that produced them.\n *\n * A relative epsilon, because the gap between doubles grows with magnitude — the same shape\n * the long layout uses to decide two sample times are one instant. Well below any real sample\n * interval, and well above the rounding that two routes to one quantity produce: a recording's\n * length is `recordCount * recordDuration`, which for 6003 records of 0.1s is not the 600.3 it\n * prints as.\n */\nfunction sameInstant(a: number, b: number): boolean {\n /*\n A relative tolerance has nothing to be relative to at infinity.\n\n `Math.abs(a - Infinity)` is Infinity, and so is the allowance beside it, so\n `Infinity <= Infinity` made every instant the same instant as an infinite one. A\n recording whose end overflows a double — three records of 1e308 seconds, which an\n 8-character record-duration field can state and a timekeeping TAL can place — was\n therefore refused the window nobody asked for:\n\n $ edf2csv rec.edf --info\n error: --start 0s is at or past the end of this unknown recording.\n\n Exit 2 for a file, naming a flag the command did not carry, quoting a length that is\n `formatDuration`'s word for a number it will not print. `--info` is the one command that\n would have explained the file and it is the one that could not run.\n\n Finite or equal, which is the only reading that means anything: two infinities are the\n same non-instant, and a real offset is not one of them.\n */\n if (!Number.isFinite(a) || !Number.isFinite(b)) return a === b;\n return Math.abs(a - b) <= Math.max(Math.abs(a), Math.abs(b)) * 1e-12;\n}\n\n/**\n * Turn a requested window into both an exact time span and the record range that\n * contains it. Records are the unit the file can be read in; the exact span is what\n * decides which samples inside those records are actually written.\n */\nexport function resolveRange(options: {\n start?: number | undefined;\n /** The `--start` value exactly as typed, quoted back in the past-the-end error. */\n startText?: string | undefined;\n duration?: number | undefined;\n /** The `--duration` value exactly as typed, quoted back in the window error. */\n durationText?: string | undefined;\n end?: number | undefined;\n /** The `--end` value exactly as typed, quoted back in the window error. */\n endText?: string | undefined;\n recordDuration: number;\n recordCount: number;\n /**\n * True start time of each data record, for discontinuous files. When absent,\n * records are assumed to sit end to end.\n */\n recordStarts?: Float64Array | null | undefined;\n}): ResolvedRange {\n /*\n The same check `buildPlan` makes, made here too, because this is exported on its own.\n\n `assertOptions` sits at the top of `buildPlan`, so `convert` was covered and the function\n underneath it was not — and this one has its own signature block on the API page.\n `resolveRange({ start: NaN, ... })` resolved: no error, no warning, and a range read back\n as `startSeconds: null, startRecord: null`, which is the \"takes the whole recording\n without saying so\" that comment names. `{ end: '2' }` was coerced by the arithmetic and\n accepted; `{ start: '30' }` reached the past-the-end error and printed it with the value\n missing — `--start s is at or past the end of this 3s recording`.\n */\n assertOptions({ start: options.start, duration: options.duration, end: options.end });\n\n // For a continuous file the recording spans recordCount * recordDuration. A\n // discontinuous one does not: a 10-second recording with a 95-second gap in the\n // middle still ends at 105 seconds. Deriving the span from the records' real\n // positions is what stops a requested window from being clipped back to the\n // amount of *data* in the file and silently discarding everything past it.\n const { earliest, latest } = span(options.recordStarts, options.recordCount, options.recordDuration);\n\n if (options.duration !== undefined && options.end !== undefined) {\n throw new TimeRangeError('Use either --duration or --end, not both.');\n }\n\n const startSeconds = options.start ?? earliest;\n\n let endSeconds: number;\n if (options.duration !== undefined) endSeconds = startSeconds + options.duration;\n else if (options.end !== undefined) endSeconds = options.end;\n else endSeconds = latest;\n\n /*\n At the end, allowing for the arithmetic that produced the end.\n\n `latest` is `recordCount * recordDuration`, and with a fractional duration that is not the\n number it prints as: 6003 records of 0.1s is 600.3000000000001, not 600.3. So `--start\n 600.3` on a recording `--info` calls \"10m 0.3s\" was accepted by a hair, converted nothing,\n and exited 0 with a signals.csv holding its header — which is the empty conversion this\n error exists to prevent, and which the same command on a whole-second recording is refused\n for.\n\n A relative epsilon, the same shape the long layout uses to decide two sample times are one\n instant: well below any real interval, and well above the rounding that two different\n routes to the same quantity produce.\n */\n if (startSeconds >= latest || sameInstant(startSeconds, latest)) {\n /*\n Quote what was typed. Reporting the parsed seconds meant `--start 4h` came back as\n \"--start 14400s is at or past the end\", which reads as a value the user never gave.\n\n In quotation marks, which the parse errors above have always used and this did not.\n Without them the value ran into the sentence: `--start \" 5s \"` printed as\n `--start 5s is at or past the end`, where the value appears to be `5s is` and\n the surrounding spaces — the actual reason a shell-built argument went wrong — are\n invisible.\n */\n throw new TimeRangeError(\n /*\n The recording's length in the same words --info uses for it.\n\n `formatSeconds` renders a bare number of seconds, so this message and the Duration\n line disagreed about one file in one session: --info said \"6m 40s\" and the error said\n \"400s\". On an overnight recording it read \"7950s recording\", leaving the reader to\n divide by 3600 to find out whether their --start was reasonable — which is the one\n question this message exists to answer. cli-reference.md has always documented it\n humanised (\"2h 12m 30s\"), a form no input could produce.\n\n The typed value keeps `quoted`, since that is the user's own text and should come\n back exactly as they wrote it.\n */\n /*\n `latest` is where the recording ends on its own clock, which is its length only when\n it starts at zero. A file timed from its first record's timekeeping TAL need not: one\n whose records run 1000s to 1003s is three seconds long, and this called it \"this\n 16m 43s recording\" — while --info two lines away said \"Duration 3s\". Where the\n recording sits is the useful thing to say in that case, and it is the number --start\n has to be given.\n */\n earliest === 0\n ? `--start ${quoted(options.startText, startSeconds)} is at or past the end of this ` +\n `${formatDuration(latest)} recording.`\n : `--start ${quoted(options.startText, startSeconds)} is at or past the end of this ` +\n `${formatDuration(latest - earliest)} recording, which runs from ` +\n `${formatSeconds(earliest)} to ${formatSeconds(latest)}.`,\n );\n }\n if (endSeconds <= startSeconds) {\n // Quote whatever the caller actually gave, for the same reason as the error above.\n const endShown =\n options.end !== undefined && options.endText !== undefined\n ? quoted(options.endText, endSeconds)\n : formatSeconds(endSeconds);\n const startShown = quoted(options.startText, startSeconds);\n /*\n Name the option that caused it, when only one of them could have.\n\n `end = start + duration`, and a negative duration is refused before this, so a window\n that ends where it starts and was built from `--duration` was built from a duration of\n zero — the one value in the command that is wrong. The message named neither it nor the\n flag:\n\n edf2csv rec.edf --start 5s --duration 0\n error: The requested window ends at 5s, which is not after its start at \"5s\".\n\n Two mentions of the value that is fine, none of the one that is not, in a file whose\n other refusals quote what was typed on the reasoning that \"`--start 4h` came back as\n '--start 14400s is at or past the end', which reads as a value the user never gave\".\n `--duration` had no typed text to quote because nothing passed it; it does now, like\n its two neighbours.\n */\n throw new TimeRangeError(\n options.end === undefined && options.duration !== undefined\n ? `--duration ${quoted(options.durationText, options.duration)} is not a length of ` +\n `time, so the window ends where it starts, at ${startShown}. Give a length above ` +\n `zero, or leave --duration out to convert to the end.`\n : `The requested window ends at ${endShown}, which is not after its start at ${startShown}.`,\n );\n }\n\n const clampedEnd = Math.min(endSeconds, latest);\n /*\n Both ends of the window, clamped the same way.\n\n The end has been clamped to the recording since these fields existed — `--end 999h` on a\n two-hour file is documented as converting to the end, silently — and the start was left as\n typed. So a window asked for before the recording begins was recorded as one:\n\n edf2csv two-second.edf --out csv --start=-500\n metadata.json \"start_seconds\": -500, \"end_seconds\": 2\n\n over a signals.csv whose first row is 0.000, with `records_converted` correctly `[0, 2]`.\n output-files calls the pair \"the resolved time window\", which the end is and the start was\n not, and a script taking `end_seconds - start_seconds` for the span converted got 502 for\n two seconds of data.\n\n `earliest` and not zero, because a recording is timed from its first record and need not\n begin there: negative-origin.edf runs from -100s, and -100 is where a start before the\n recording belongs. Nothing downstream moves — every sample sits at or after `earliest`, so\n the row filter and the record selection answer the same for either value, and the byte\n estimate takes a width from the smaller magnitude, which is the one the column actually\n holds.\n\n Not when the whole window sits before the recording, where there is no overlap to clamp\n to and raising the start past the end would describe the request backwards: `--start 0\n --duration 0.001` on a file beginning at 30s is an empty window, and EMPTY_WINDOW quotes\n the bounds — \"(30.000s to 0.001s)\" says nothing a reader can act on, where the bounds as\n asked for are what its hint is already explaining.\n */\n const clampedStart = clampedEnd <= earliest ? startSeconds : Math.max(startSeconds, earliest);\n const { startRecord, endRecord } = selectRecords(options, clampedStart, clampedEnd);\n\n return {\n startSeconds: clampedStart,\n endSeconds: clampedEnd,\n startRecord,\n endRecord,\n /*\n The same rounding, one field over.\n\n `latest` is `recordCount * recordDuration`, and 6003 records of 0.1s is\n 600.3000000000001. So `--end 600.3` on a recording of exactly that length wrote every\n sample it has — byte-identical to a bare conversion — and metadata.json recorded\n `whole_recording: false` for it, while the bare run recorded true. One conversion, two\n answers, on the field a pipeline reads to decide whether it has the lot.\n */\n isWholeRecording:\n (startSeconds <= earliest || sameInstant(startSeconds, earliest)) &&\n (clampedEnd >= latest || sameInstant(clampedEnd, latest)),\n recordingStartSeconds: earliest,\n recordingEndSeconds: latest,\n };\n}\n\nfunction span(\n recordStarts: Float64Array | null | undefined,\n recordCount: number,\n recordDuration: number,\n): { earliest: number; latest: number } {\n if (!recordStarts || recordStarts.length === 0) {\n return { earliest: 0, latest: recordCount * recordDuration };\n }\n let earliest = Infinity;\n let latest = -Infinity;\n for (const start of recordStarts) {\n if (start < earliest) earliest = start;\n if (start + recordDuration > latest) latest = start + recordDuration;\n }\n if (!Number.isFinite(earliest) || !Number.isFinite(latest)) {\n return { earliest: 0, latest: recordCount * recordDuration };\n }\n return { earliest, latest };\n}\n\n/** Every record whose own time span overlaps the requested window. */\nfunction selectRecords(\n options: { recordDuration: number; recordCount: number; recordStarts?: Float64Array | null | undefined },\n startSeconds: number,\n endSeconds: number,\n): { startRecord: number; endRecord: number } {\n const starts = options.recordStarts;\n if (!starts || starts.length === 0) {\n /*\n An empty range is `[0, 0]` here as it is below, rather than whatever the arithmetic\n produced. Each bound was clamped on one side only — the start up to zero, the end down\n to the record count — so a window lying entirely before the recording came back as\n `records_converted: [0, -1]` in metadata.json, over a `endRecord` documented as \"one\n past the last data record touching the window\". A script differencing the pair read\n minus one record converted, and `ConversionProgress.recordsTotal` reported the same.\n\n The discontinuous branch below has always answered `[0, 0]` for exactly this request,\n so the two halves of one function disagreed about how to say \"none\": the same window on\n `annotations.edf` and on `discontinuous.edf` gave `[0, -1]` and `[0, 0]`.\n */\n const first = Math.max(0, Math.floor(startSeconds / options.recordDuration));\n const last = Math.min(options.recordCount, Math.ceil(endSeconds / options.recordDuration));\n return first < last ? { startRecord: first, endRecord: last } : { startRecord: 0, endRecord: 0 };\n }\n\n let first = options.recordCount;\n let last = 0;\n for (let i = 0; i < starts.length; i++) {\n const begin = starts[i] ?? 0;\n if (begin + options.recordDuration > startSeconds && begin < endSeconds) {\n if (i < first) first = i;\n if (i + 1 > last) last = i + 1;\n }\n }\n return first < last ? { startRecord: first, endRecord: last } : { startRecord: 0, endRecord: 0 };\n}\n\n/**\n * The value as the caller typed it, in quotation marks, or the parsed seconds if they gave\n * none. The marks show where the value begins and ends, which matters most for the values\n * that went wrong because of what surrounds them.\n */\nfunction quoted(text: string | undefined, seconds: number): string {\n return text === undefined ? formatSeconds(seconds) : `\"${text}\"`;\n}\n\nfunction formatSeconds(seconds: number): string {\n /*\n The same 1e21 cliff, in the bounds this message hands back.\n\n `--start \"9e21\"` was answered with \"is at or past the end of this 3e+21s recording, which\n runs from 1e+21s to 4e+21s\" — a sentence whose whole purpose is to say what window there\n is to ask for, ending in two tokens the parser refuses: `--start 1e+21s` is \"uses an\n unknown unit \\\"e\\\"\". `Number(...)` was here to drop the trailing zeros, and it also\n re-introduced the exponent form that `toFixed` had produced.\n */\n const text = fixed(seconds, 3);\n return `${text.includes('.') ? text.replace(/\\.?0+$/u, '') : text}s`;\n}\n"]}
1
+ {"version":3,"file":"time-range.js","sourceRoot":"","sources":["../../src/convert/time-range.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Parsing for the time-range options.\n *\n * Researchers write offsets in whatever form is natural for the recording in front\n * of them: seconds for a short ECG strip, `30m` into a sleep study, `01:23:45` when\n * reading off a clock. All three are accepted; anything ambiguous is rejected with\n * a message that shows the forms that work.\n */\n\nimport { fixed, formatDuration } from '../format/number.js';\nimport { assertOptions, assertRecordShape, describeValue, OptionError } from './options.js';\n\nexport class TimeRangeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'TimeRangeError';\n }\n}\n\n/**\n * Every spelling of a unit this accepts, and what one of it is worth in seconds.\n *\n * Exported so the spellings can be enumerated by a test rather than listed a second time:\n * twelve of these sixteen are named nowhere in the tool or its documentation, and none of\n * their values was checked by anything. `hrs: 360` would have converted `--start 2hrs` from\n * twelve minutes in and said nothing, which is the one kind of mistake a window may not make.\n */\nexport const UNIT_SECONDS: Record<string, number> = {\n h: 3600,\n hr: 3600,\n hrs: 3600,\n hour: 3600,\n hours: 3600,\n m: 60,\n min: 60,\n mins: 60,\n minute: 60,\n minutes: 60,\n s: 1,\n sec: 1,\n secs: 1,\n second: 1,\n seconds: 1,\n ms: 0.001,\n};\n\nconst CLOCK = /^(?:(\\d+):)?(\\d{1,2}):(\\d{1,2}(?:\\.\\d+)?)$/u;\nconst UNIT_TOKEN = /(\\d+(?:\\.\\d+)?)\\s*([a-z]+)/giu;\n\n/**\n * Parse a duration or offset into seconds.\n *\n * Accepted: `90`, `90s`, `5m`, `1h30m`, `1h 30m 15s`, `00:30:00`, `30:00`, `250ms`.\n *\n * `allowNegative` is for the two options that name a position rather than a length.\n *\n * A recording is timed from its first record's timekeeping annotation, and nothing obliges that\n * to sit at or after zero: a file whose records run from -100 s to -97 s is one this tool reads,\n * times from -100, and describes with\n *\n * Timed from -100.000s (first sample; --start and --end use this clock)\n *\n * That line says the number can be typed straight back in, and it could not be. Every offset\n * such a recording has came back as \"not a time I understand\", so its whole clock was\n * unreachable and no window of it could be converted at all — the one file shape where a\n * window is refused for naming a moment the recording actually contains.\n *\n * A length below zero is still a different thing, and `--duration` still refuses one.\n */\nexport function parseTimeSpec(input: string, optionName?: string, allowNegative = false): number {\n /*\n A name for the value, when the caller has not given one.\n\n Every refusal below opens with it, and the CLI passes the flag — `--start \"1,5\" is not a\n time I understand`. This is also the function the api page sends other people's users to,\n and a caller who writes the one-argument call its neighbours take got\n\n TimeRangeError: undefined is empty. Try a value like 30s, 5m, or 00:30:00.\n\n on every one of the eleven messages in here. The paragraph below hardened `input` for\n exactly that caller; the argument beside it could still put the word `undefined` at the\n front of the sentence.\n\n Coerced rather than refused, unlike `input`: this one names the value in a message rather\n than being the value, so there is a right answer to fall back on and nothing is lost by\n using it. An empty or non-string name takes the same route, since `\"\" is empty.` is the\n same sentence with a different hole in it.\n */\n const named =\n typeof optionName === 'string' && optionName.trim() !== '' ? optionName : 'The value';\n /*\n Text, because this is the function the API page points other people's users at: \"Use\n `parseTimeSpec` if you want to accept those forms from your own users.\"\n\n A value arriving from a JSON config or a form field is a number as often as a string, and\n `{ \"start\": 30 }` came back as `TypeError: input.trim is not a function` — a variable name\n from inside this file, thrown past a caller who had done exactly what the page told them\n to. Every other refusal here is a TimeRangeError quoting the value and the option. The\n number is not coerced, for the reason `channels: 'ECG'` is not iterated: a shape that\n happens to work for 30 also swallows NaN, and the CLI already writes `String(raw)`.\n */\n if (typeof input !== 'string') {\n throw new TimeRangeError(\n `${named} must be given as text, not ${describeValue(input)}. ` +\n `Seconds are written \"30\", and the other forms are \"30s\", \"5m\", \"1h30m\", \"00:30:00\".`,\n );\n }\n const trimmed = input.trim().toLowerCase();\n if (trimmed === '') {\n throw new TimeRangeError(`${named} is empty. Try a value like 30s, 5m, or 00:30:00.`);\n }\n\n /*\n The sign is read off the front and applied to the whole value, so `-1h30m` is an hour and a\n half before the origin rather than an hour before it and half an hour after.\n\n Nothing may sit between the sign and the number, and a leading `+` is still refused: every\n other number this tool takes is refused when written in a form nobody types, and `+5` is\n exactly what `--decimals` and `--jobs` reject one flag over.\n */\n const negative = trimmed.startsWith('-');\n const text = negative ? trimmed.slice(1) : trimmed;\n\n /*\n A leading plus, named as such.\n\n Refusing it is deliberate — see the paragraph above — but it came back as \"is not a time I\n understand. Try 30s, 5m, 1h30m, 00:30:00, or a plain number of seconds\", which is the\n message for input that could not be read at all. Every part of `+5s` was read: the number\n parsed, the unit was looked up and found. The one thing wrong with it is a character the\n reader put there on purpose, and they were sent to re-check their unit spellings with `5m`\n sitting in the list of suggestions, differing from what they typed by the sign they cannot\n see is the problem. 0.7.102 made this argument about a space; this is the same sentence\n about the other character that reaches it. `--decimals` and `--jobs` both quote the value\n back and say what is wrong with it.\n\n Every plus, not the first one, and the leftover space with it — the same rule the spaced\n refusal below applies to spaces, for the same reason: a hint whose command this function\n refuses is worse than no hint. `+1h+30m` was answered with `1h+30m`, which comes straight\n back as \"is not a time I understand\", and `+ 5s` with a value that opened on a space.\n */\n if (text.startsWith('+')) {\n throw new TimeRangeError(\n `${named} \"${input}\" begins with a plus. Write the number on its own: ` +\n `${input.trim().replaceAll('+', '').trim()}`,\n );\n }\n const signed = (seconds: number): number =>\n assertFinite(negative ? -seconds : seconds, named, input, allowNegative);\n\n // Bare number means seconds.\n if (/^\\d+(?:\\.\\d+)?$/u.test(text)) return signed(Number(text));\n\n // Clock form: hh:mm:ss or mm:ss.\n const clock = CLOCK.exec(text);\n if (clock) {\n const hours = clock[1] === undefined ? 0 : Number(clock[1]);\n const minutes = Number(clock[2]);\n const seconds = Number(clock[3]);\n if (minutes >= 60 || seconds >= 60) {\n throw new TimeRangeError(\n `${named} \"${input}\" has a minutes or seconds field of 60 or more.`,\n );\n }\n return signed(hours * 3600 + minutes * 60 + seconds);\n }\n\n // Unit form: 1h30m, 1h30m 15s, 250ms. A number sits directly against its unit; see below.\n UNIT_TOKEN.lastIndex = 0;\n let total = 0;\n let matched = 0;\n let consumed = 0;\n /** Whether any token had a space between its number and its unit. See the refusal below. */\n let spaced = false;\n let match: RegExpExecArray | null;\n // Which units have already been seen, so a repeat can be rejected rather than added.\n const seen = new Set<number>();\n while ((match = UNIT_TOKEN.exec(text)) !== null) {\n const amount = Number(match[1]);\n const unit = match[2] ?? '';\n const scale = UNIT_SECONDS[unit];\n if (scale === undefined) {\n throw new TimeRangeError(\n `${named} \"${input}\" uses an unknown unit \"${unit}\". Use h, m, s or ms, ` +\n `or their long forms: hours, minutes, seconds.`,\n );\n }\n\n // \"1h1h\" is a typo, not a request for two hours. Summing repeated units silently\n // turned a slip into a plausible window that was quietly the wrong length. Units are\n // keyed by their scale so the aliases collapse together: \"1h30min20m\" is caught too.\n if (seen.has(scale)) {\n throw new TimeRangeError(\n `${named} \"${input}\" gives the same unit twice. ` +\n `Combine each unit once, as in 1h30m.`,\n );\n }\n seen.add(scale);\n\n total += amount * scale;\n matched++;\n // Counted with the whitespace taken out, so the comparison below is against the same\n // string on both sides and a space inside a token reaches the refusal written for it\n // rather than the one about input that could not be read at all.\n consumed += match[0].replace(/\\s+/gu, '').length;\n if (/\\s/u.test(match[0])) spaced = true;\n }\n\n // Reject partially-understood input like \"5x\" or \"1h banana\".\n if (matched === 0 || consumed !== text.replace(/\\s+/gu, '').length) {\n throw new TimeRangeError(\n `${named} \"${input}\" is not a time I understand. ` +\n `Try 30s, 5m, 1h30m, 00:30:00, or a plain number of seconds.`,\n );\n }\n\n /*\n A space between a number and its unit, named as such.\n\n `5 min` is refused on purpose — cli-reference sets the rule out, and it is what keeps\n `1 2h` from being read as anything — but it came back as \"is not a time I understand. Try\n 30s, 5m, 1h30m, 00:30:00, or a plain number of seconds\", which is the message for input\n that could not be read at all. Every part of `5 min` was read: the number parsed, the unit\n was looked up in the table and found. The one thing wrong with it is a space, and the\n reader was sent to re-check their unit spellings instead — with `5m` sitting in the list of\n suggestions, differing from what they typed by a character they cannot see is the problem.\n\n Only reachable now that the count above ignores whitespace; before, the length check caught\n these first and there was nothing left to tell them apart by.\n */\n if (spaced) {\n throw new TimeRangeError(\n `${named} \"${input}\" puts a space between a number and its unit. ` +\n `Write them together: ${input.trim().replace(/(\\d)\\s+([a-z])/giu, '$1$2')}`,\n );\n }\n\n return signed(total);\n}\n\nfunction assertFinite(\n value: number,\n optionName: string,\n input: string,\n allowNegative: boolean,\n): number {\n /*\n Overflow answers for itself, rather than borrowing the sentence about signs.\n\n One branch covered two rejections. `--start` with four hundred nines is a positive value\n that `Number` returns as `Infinity`, and the answer was `is not a valid non-negative\n time` — which names the one thing about it that is not the problem. Worse on the two\n options this is called with `allowNegative` for: `--start` and `--end` take a sign on\n purpose, since a recording timed from before zero has no other way to be addressed, so\n \"non-negative\" is not their rule at all and overflow is the only way they reach here.\n */\n if (!Number.isFinite(value)) {\n throw new TimeRangeError(\n `${optionName} \"${input}\" is further from zero than a number of seconds can hold.`,\n );\n }\n if (!allowNegative && value < 0) {\n throw new TimeRangeError(`${optionName} \"${input}\" is not a valid non-negative time.`);\n }\n return value;\n}\n\nexport interface ResolvedRange {\n /** Inclusive start, in seconds from the beginning of the recording. */\n startSeconds: number;\n /** Exclusive end, in seconds. */\n endSeconds: number;\n /** First data record touching the window. */\n startRecord: number;\n /** One past the last data record touching the window. */\n endRecord: number;\n /** True when the window covers the whole recording. */\n isWholeRecording: boolean;\n /** Earliest record start, including EDF+D timing gaps. */\n recordingStartSeconds: number;\n /** End of the latest record, including EDF+D timing gaps. */\n recordingEndSeconds: number;\n}\n\n/**\n * Slack for comparisons at sample/window boundaries.\n *\n * A nanosecond is far below any real sampling interval — 20 kHz is 50 microseconds — so it\n * absorbs the arithmetic error in `recordStart + sample / rate` without reaching a\n * neighbouring sample.\n */\nexport const BOUNDARY_TOLERANCE = 1e-9;\n\n/**\n * The slack to use for a channel sampled this often.\n *\n * Never past half a sample interval, because slack that reaches the next sample stops being\n * slack — half is the most that cannot, since the nearest sample below a bound sits a whole\n * interval away from it. `Math.min` takes exactly half at rates above 500 MHz, which is where\n * this said \"never as much as half\" of a formula that returns it. A fixed nanosecond was\n * applied whatever the rate, and the format does not\n * oblige the interval to be larger than it: EDF's record duration is an 8-character field\n * that accepts `1e-9`. A recording of two 1 ns records holding ten samples each wrote ten of\n * its twenty rows — the window ends at 2e-9, the comparison asked for `time < 2e-9 - 1e-9`,\n * and the entire second record failed it. Exit 0, no warning, half the samples gone.\n */\nexport function toleranceFor(rate: number): number {\n if (!(rate > 0) || !Number.isFinite(rate)) return BOUNDARY_TOLERANCE;\n return Math.min(BOUNDARY_TOLERANCE, 1 / rate / 2);\n}\n\n/** Match the exact half-open boundary rules used while writing signal rows. */\nexport function sampleTimeIsInRange(\n time: number,\n startSeconds: number,\n endSeconds: number,\n tolerance: number = BOUNDARY_TOLERANCE,\n): boolean {\n return time >= startSeconds - tolerance && time < endSeconds - tolerance;\n}\n\n/** Count samples from one record that fall inside a half-open requested window. */\nexport function countSamplesInRange(options: {\n recordStart: number;\n rate: number;\n samplesPerRecord: number;\n startSeconds: number;\n endSeconds: number;\n}): number {\n /*\n A record with no place on the clock contributes no rows, which is what the conversion\n already does with one: its sample times are non-finite, so every one of them fails the\n range test and none is written. The arithmetic below reached `Infinity - Infinity` and\n answered `NaN`, which `--info` then printed — \"Would write NaN rows, roughly NaN B.\" on a\n recording that goes on to write eight. Reachable from a header: a record duration of 1e308\n is five characters in an eight-character field, and the third record of one is past what a\n double can hold.\n */\n if (!Number.isFinite(options.recordStart)) return 0;\n const slack = toleranceFor(options.rate);\n const lower = Math.ceil(\n (options.startSeconds - slack - options.recordStart) * options.rate,\n );\n const upper = Math.ceil(\n (options.endSeconds - slack - options.recordStart) * options.rate,\n );\n const first = Math.max(0, Math.min(options.samplesPerRecord, lower));\n const last = Math.max(0, Math.min(options.samplesPerRecord, upper));\n return Math.max(0, last - first);\n}\n\n/**\n * Whether two instants differ only by the arithmetic that produced them.\n *\n * A relative epsilon, because the gap between doubles grows with magnitude — the same shape\n * the long layout uses to decide two sample times are one instant. Well below any real sample\n * interval, and well above the rounding that two routes to one quantity produce: a recording's\n * length is `recordCount * recordDuration`, which for 6003 records of 0.1s is not the 600.3 it\n * prints as.\n */\nfunction sameInstant(a: number, b: number): boolean {\n /*\n A relative tolerance has nothing to be relative to at infinity.\n\n `Math.abs(a - Infinity)` is Infinity, and so is the allowance beside it, so\n `Infinity <= Infinity` made every instant the same instant as an infinite one. A\n recording whose end overflows a double — three records of 1e308 seconds, which an\n 8-character record-duration field can state and a timekeeping TAL can place — was\n therefore refused the window nobody asked for:\n\n $ edf2csv rec.edf --info\n error: --start 0s is at or past the end of this unknown recording.\n\n Exit 2 for a file, naming a flag the command did not carry, quoting a length that is\n `formatDuration`'s word for a number it will not print. `--info` is the one command that\n would have explained the file and it is the one that could not run.\n\n Finite or equal, which is the only reading that means anything: two infinities are the\n same non-instant, and a real offset is not one of them.\n */\n if (!Number.isFinite(a) || !Number.isFinite(b)) return a === b;\n return Math.abs(a - b) <= Math.max(Math.abs(a), Math.abs(b)) * 1e-12;\n}\n\n/**\n * Turn a requested window into both an exact time span and the record range that\n * contains it. Records are the unit the file can be read in; the exact span is what\n * decides which samples inside those records are actually written.\n */\nexport function resolveRange(options: {\n start?: number | undefined;\n /** The `--start` value exactly as typed, quoted back in the past-the-end error. */\n startText?: string | undefined;\n duration?: number | undefined;\n /** The `--duration` value exactly as typed, quoted back in the window error. */\n durationText?: string | undefined;\n end?: number | undefined;\n /** The `--end` value exactly as typed, quoted back in the window error. */\n endText?: string | undefined;\n recordDuration: number;\n recordCount: number;\n /**\n * True start time of each data record, for discontinuous files. When absent,\n * records are assumed to sit end to end.\n */\n recordStarts?: Float64Array | null | undefined;\n}): ResolvedRange {\n /*\n The same check `buildPlan` makes, made here too, because this is exported on its own.\n\n `assertOptions` sits at the top of `buildPlan`, so `convert` was covered and the function\n underneath it was not — and this one has its own signature block on the API page.\n `resolveRange({ start: NaN, ... })` resolved: no error, no warning, and a range read back\n as `startSeconds: null, startRecord: null`, which is the \"takes the whole recording\n without saying so\" that comment names. `{ end: '2' }` was coerced by the arithmetic and\n accepted; `{ start: '30' }` reached the past-the-end error and printed it with the value\n missing — `--start s is at or past the end of this 3s recording`.\n */\n /*\n The argument bag itself, before three properties are read off it.\n\n `resolveRange(42)` read `.start` off a number, which is `undefined` rather than a throw, and\n went on to answer with a range; `resolveRange(null)` reached the same line and came back as\n `TypeError: Cannot read properties of null (reading 'start')`, naming a property of this\n function's own parameter.\n */\n if (options === null || typeof options !== 'object') {\n throw new OptionError(\n `resolveRange takes one object — the recording's shape and the window asked of it — ` +\n `got ${describeValue(options)}.`,\n );\n }\n assertOptions({ start: options.start, duration: options.duration, end: options.end });\n /*\n And the two numbers the window is measured against, which that call does not cover.\n\n `assertOptions` checks what was asked for; these are what it is asked of, and nothing\n looked at them. `resolveRange({ start: 1, recordDuration: 1 })` — no `recordCount` —\n answered `{ startSeconds: 0, endSeconds: null, startRecord: 0, endRecord: 0 }`: a window\n over no records, handed back as a fact about a recording. `resolveRange(42)` reached the\n same answer, since reading `.start` off a number is `undefined` rather than a throw.\n\n The check `buildPlan` has made since 0.8.64, made here too, because this is exported on\n its own — the same sentence the line above this one is here for.\n */\n assertRecordShape(options);\n\n // For a continuous file the recording spans recordCount * recordDuration. A\n // discontinuous one does not: a 10-second recording with a 95-second gap in the\n // middle still ends at 105 seconds. Deriving the span from the records' real\n // positions is what stops a requested window from being clipped back to the\n // amount of *data* in the file and silently discarding everything past it.\n const { earliest, latest } = span(options.recordStarts, options.recordCount, options.recordDuration);\n\n if (options.duration !== undefined && options.end !== undefined) {\n throw new TimeRangeError('Use either --duration or --end, not both.');\n }\n\n const startSeconds = options.start ?? earliest;\n\n let endSeconds: number;\n if (options.duration !== undefined) endSeconds = startSeconds + options.duration;\n else if (options.end !== undefined) endSeconds = options.end;\n else endSeconds = latest;\n\n /*\n At the end, allowing for the arithmetic that produced the end.\n\n `latest` is `recordCount * recordDuration`, and with a fractional duration that is not the\n number it prints as: 6003 records of 0.1s is 600.3000000000001, not 600.3. So `--start\n 600.3` on a recording `--info` calls \"10m 0.3s\" was accepted by a hair, converted nothing,\n and exited 0 with a signals.csv holding its header — which is the empty conversion this\n error exists to prevent, and which the same command on a whole-second recording is refused\n for.\n\n A relative epsilon, the same shape the long layout uses to decide two sample times are one\n instant: well below any real interval, and well above the rounding that two different\n routes to the same quantity produce.\n */\n if (startSeconds >= latest || sameInstant(startSeconds, latest)) {\n /*\n Quote what was typed. Reporting the parsed seconds meant `--start 4h` came back as\n \"--start 14400s is at or past the end\", which reads as a value the user never gave.\n\n In quotation marks, which the parse errors above have always used and this did not.\n Without them the value ran into the sentence: `--start \" 5s \"` printed as\n `--start 5s is at or past the end`, where the value appears to be `5s is` and\n the surrounding spaces — the actual reason a shell-built argument went wrong — are\n invisible.\n */\n throw new TimeRangeError(\n /*\n The recording's length in the same words --info uses for it.\n\n `formatSeconds` renders a bare number of seconds, so this message and the Duration\n line disagreed about one file in one session: --info said \"6m 40s\" and the error said\n \"400s\". On an overnight recording it read \"7950s recording\", leaving the reader to\n divide by 3600 to find out whether their --start was reasonable — which is the one\n question this message exists to answer. cli-reference.md has always documented it\n humanised (\"2h 12m 30s\"), a form no input could produce.\n\n The typed value keeps `quoted`, since that is the user's own text and should come\n back exactly as they wrote it.\n */\n /*\n `latest` is where the recording ends on its own clock, which is its length only when\n it starts at zero. A file timed from its first record's timekeeping TAL need not: one\n whose records run 1000s to 1003s is three seconds long, and this called it \"this\n 16m 43s recording\" — while --info two lines away said \"Duration 3s\". Where the\n recording sits is the useful thing to say in that case, and it is the number --start\n has to be given.\n */\n earliest === 0\n ? `--start ${quoted(options.startText, startSeconds)} is at or past the end of this ` +\n `${formatDuration(latest)} recording.`\n : `--start ${quoted(options.startText, startSeconds)} is at or past the end of this ` +\n `${formatDuration(latest - earliest)} recording, which runs from ` +\n `${formatSeconds(earliest)} to ${formatSeconds(latest)}.`,\n );\n }\n if (endSeconds <= startSeconds) {\n // Quote whatever the caller actually gave, for the same reason as the error above.\n const endShown =\n options.end !== undefined && options.endText !== undefined\n ? quoted(options.endText, endSeconds)\n : formatSeconds(endSeconds);\n const startShown = quoted(options.startText, startSeconds);\n /*\n Name the option that caused it, when only one of them could have.\n\n `end = start + duration`, and a negative duration is refused before this, so a window\n that ends where it starts and was built from `--duration` was built from a duration of\n zero — the one value in the command that is wrong. The message named neither it nor the\n flag:\n\n edf2csv rec.edf --start 5s --duration 0\n error: The requested window ends at 5s, which is not after its start at \"5s\".\n\n Two mentions of the value that is fine, none of the one that is not, in a file whose\n other refusals quote what was typed on the reasoning that \"`--start 4h` came back as\n '--start 14400s is at or past the end', which reads as a value the user never gave\".\n `--duration` had no typed text to quote because nothing passed it; it does now, like\n its two neighbours.\n */\n throw new TimeRangeError(\n options.end === undefined && options.duration !== undefined\n ? `--duration ${quoted(options.durationText, options.duration)} is not a length of ` +\n `time, so the window ends where it starts, at ${startShown}. Give a length above ` +\n `zero, or leave --duration out to convert to the end.`\n : `The requested window ends at ${endShown}, which is not after its start at ${startShown}.`,\n );\n }\n\n const clampedEnd = Math.min(endSeconds, latest);\n /*\n Both ends of the window, clamped the same way.\n\n The end has been clamped to the recording since these fields existed — `--end 999h` on a\n two-hour file is documented as converting to the end, silently — and the start was left as\n typed. So a window asked for before the recording begins was recorded as one:\n\n edf2csv two-second.edf --out csv --start=-500\n metadata.json \"start_seconds\": -500, \"end_seconds\": 2\n\n over a signals.csv whose first row is 0.000, with `records_converted` correctly `[0, 2]`.\n output-files calls the pair \"the resolved time window\", which the end is and the start was\n not, and a script taking `end_seconds - start_seconds` for the span converted got 502 for\n two seconds of data.\n\n `earliest` and not zero, because a recording is timed from its first record and need not\n begin there: negative-origin.edf runs from -100s, and -100 is where a start before the\n recording belongs. Nothing downstream moves — every sample sits at or after `earliest`, so\n the row filter and the record selection answer the same for either value, and the byte\n estimate takes a width from the smaller magnitude, which is the one the column actually\n holds.\n\n Not when the whole window sits before the recording, where there is no overlap to clamp\n to and raising the start past the end would describe the request backwards: `--start 0\n --duration 0.001` on a file beginning at 30s is an empty window, and EMPTY_WINDOW quotes\n the bounds — \"(30.000s to 0.001s)\" says nothing a reader can act on, where the bounds as\n asked for are what its hint is already explaining.\n */\n const clampedStart = clampedEnd <= earliest ? startSeconds : Math.max(startSeconds, earliest);\n const { startRecord, endRecord } = selectRecords(options, clampedStart, clampedEnd);\n\n return {\n startSeconds: clampedStart,\n endSeconds: clampedEnd,\n startRecord,\n endRecord,\n /*\n The same rounding, one field over.\n\n `latest` is `recordCount * recordDuration`, and 6003 records of 0.1s is\n 600.3000000000001. So `--end 600.3` on a recording of exactly that length wrote every\n sample it has — byte-identical to a bare conversion — and metadata.json recorded\n `whole_recording: false` for it, while the bare run recorded true. One conversion, two\n answers, on the field a pipeline reads to decide whether it has the lot.\n */\n isWholeRecording:\n (startSeconds <= earliest || sameInstant(startSeconds, earliest)) &&\n (clampedEnd >= latest || sameInstant(clampedEnd, latest)),\n recordingStartSeconds: earliest,\n recordingEndSeconds: latest,\n };\n}\n\nfunction span(\n recordStarts: Float64Array | null | undefined,\n recordCount: number,\n recordDuration: number,\n): { earliest: number; latest: number } {\n if (!recordStarts || recordStarts.length === 0) {\n return { earliest: 0, latest: recordCount * recordDuration };\n }\n let earliest = Infinity;\n let latest = -Infinity;\n for (const start of recordStarts) {\n if (start < earliest) earliest = start;\n if (start + recordDuration > latest) latest = start + recordDuration;\n }\n if (!Number.isFinite(earliest) || !Number.isFinite(latest)) {\n return { earliest: 0, latest: recordCount * recordDuration };\n }\n return { earliest, latest };\n}\n\n/** Every record whose own time span overlaps the requested window. */\nfunction selectRecords(\n options: { recordDuration: number; recordCount: number; recordStarts?: Float64Array | null | undefined },\n startSeconds: number,\n endSeconds: number,\n): { startRecord: number; endRecord: number } {\n const starts = options.recordStarts;\n if (!starts || starts.length === 0) {\n /*\n An empty range is `[0, 0]` here as it is below, rather than whatever the arithmetic\n produced. Each bound was clamped on one side only — the start up to zero, the end down\n to the record count — so a window lying entirely before the recording came back as\n `records_converted: [0, -1]` in metadata.json, over a `endRecord` documented as \"one\n past the last data record touching the window\". A script differencing the pair read\n minus one record converted, and `ConversionProgress.recordsTotal` reported the same.\n\n The discontinuous branch below has always answered `[0, 0]` for exactly this request,\n so the two halves of one function disagreed about how to say \"none\": the same window on\n `annotations.edf` and on `discontinuous.edf` gave `[0, -1]` and `[0, 0]`.\n */\n const first = Math.max(0, Math.floor(startSeconds / options.recordDuration));\n const last = Math.min(options.recordCount, Math.ceil(endSeconds / options.recordDuration));\n return first < last ? { startRecord: first, endRecord: last } : { startRecord: 0, endRecord: 0 };\n }\n\n let first = options.recordCount;\n let last = 0;\n for (let i = 0; i < starts.length; i++) {\n const begin = starts[i] ?? 0;\n if (begin + options.recordDuration > startSeconds && begin < endSeconds) {\n if (i < first) first = i;\n if (i + 1 > last) last = i + 1;\n }\n }\n return first < last ? { startRecord: first, endRecord: last } : { startRecord: 0, endRecord: 0 };\n}\n\n/**\n * The value as the caller typed it, in quotation marks, or the parsed seconds if they gave\n * none. The marks show where the value begins and ends, which matters most for the values\n * that went wrong because of what surrounds them.\n */\nfunction quoted(text: string | undefined, seconds: number): string {\n return text === undefined ? formatSeconds(seconds) : `\"${text}\"`;\n}\n\nfunction formatSeconds(seconds: number): string {\n /*\n The same 1e21 cliff, in the bounds this message hands back.\n\n `--start \"9e21\"` was answered with \"is at or past the end of this 3e+21s recording, which\n runs from 1e+21s to 4e+21s\" — a sentence whose whole purpose is to say what window there\n is to ask for, ending in two tokens the parser refuses: `--start 1e+21s` is \"uses an\n unknown unit \\\"e\\\"\". `Number(...)` was here to drop the trailing zeros, and it also\n re-introduced the exponent form that `toFixed` had produced.\n */\n const text = fixed(seconds, 3);\n return `${text.includes('.') ? text.replace(/\\.?0+$/u, '') : text}s`;\n}\n"]}
@@ -240,6 +240,30 @@ function resolveStartDateTime(dateRaw, timeRaw, recordingId = '') {
240
240
  * @param fileSize Total size of the file on disk, used to derive the real record count.
241
241
  */
242
242
  export function parseHeader(buf, fileSize) {
243
+ /*
244
+ The first argument, checked before anything is read out of it.
245
+
246
+ The api page offers this function on its own, so it is reached by a caller holding bytes
247
+ they read themselves — and it took whatever it was handed. A number or a plain object came
248
+ back as Node's own `TypeError: bytes.subarray is not a function`, which names a method the
249
+ caller never called; `undefined` as a property read on a local.
250
+
251
+ Worse, a string got an answer *about the recording*:
252
+
253
+ parseHeader('0 ...', 4_000_000)
254
+ EdfError FILE_TOO_SMALL: An EDF header alone needs 256 bytes, but only 11 of this
255
+ 4,000,000-byte file reached the parser.
256
+
257
+ A string has a `length`, so it reached the short-header branch below and was reported as a
258
+ truncated file — the code a script branches on to quarantine a bad recording, raised over a
259
+ recording nobody had read. That is the distinction `readRecords` draws in as many words:
260
+ "an `OptionError`, because it is the call that is wrong and not the recording". The same
261
+ check `decodeRecordAnnotations` makes of its own byte argument, for the reason given there.
262
+ */
263
+ if (!ArrayBuffer.isView(buf)) {
264
+ throw new OptionError(`buf must be the header block as bytes, got ${describeValue(buf)}. parseHeader takes ` +
265
+ `the bytes read from the file and the size of the file they came out of.`);
266
+ }
243
267
  /*
244
268
  The second argument, checked before anything is derived from it.
245
269
 
@@ -1 +1 @@
1
- {"version":3,"file":"header.js","sourceRoot":"","sources":["../../src/edf/header.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * EDF / EDF+ header parsing.\n *\n * Layout (all fields are ASCII, left-justified, space-padded):\n *\n * fixed header, 256 bytes\n * 0 8 version ('0', or 255 + 'BIOSEMI' for BDF)\n * 8 80 patient identification\n * 88 80 recording identification\n * 168 8 start date dd.mm.yy\n * 176 8 start time hh.mm.ss\n * 184 8 number of bytes in the header record\n * 192 44 reserved ('EDF+C' / 'EDF+D' live here)\n * 236 8 number of data records (-1 if unknown)\n * 244 8 duration of a data record, in seconds (may be fractional)\n * 252 4 number of signals (ns)\n *\n * signal header, ns * 256 bytes, stored FIELD-major rather than signal-major:\n * all ns labels, then all ns transducer types, and so on.\n * ns * 16 label\n * ns * 80 transducer type\n * ns * 8 physical dimension\n * ns * 8 physical minimum\n * ns * 8 physical maximum\n * ns * 8 digital minimum\n * ns * 8 digital maximum\n * ns * 80 prefiltering\n * ns * 8 number of samples in each data record\n * ns * 32 reserved\n */\n\nimport { EdfError } from './errors.js';\nimport type { Diagnostic } from './errors.js';\nimport { counted, grouped, listed } from '../format/list.js';\nimport { plain } from '../format/number.js';\n// The shell-quoting rule for a label, shared rather than repeated: this hint and\n// `--channels`' own \"did you mean\" have to print the same command for the same label.\nimport { typeable } from '../convert/channels.js';\nimport { OptionError, describeValue } from '../convert/options.js';\nimport { decodeLatin1 } from './bytes.js';\n\n/** Label the EDF+ spec reserves for the annotations channel. */\nexport const ANNOTATIONS_LABEL = 'EDF Annotations';\n/** BDF+ uses its own spelling for the same channel. */\nexport const BDF_ANNOTATIONS_LABEL = 'BDF Annotations';\n\nexport const FIXED_HEADER_BYTES = 256;\nexport const SIGNAL_HEADER_BYTES = 256;\n\nexport interface EdfSignal {\n /** Position in the file, 0-based. Stable even when labels collide. */\n index: number;\n label: string;\n transducer: string;\n physicalDimension: string;\n physicalMin: number;\n physicalMax: number;\n digitalMin: number;\n digitalMax: number;\n prefiltering: string;\n samplesPerRecord: number;\n reserved: string;\n /** True for the EDF+ 'EDF Annotations' channel, which carries text, not signal. */\n isAnnotations: boolean;\n /** samplesPerRecord / recordDuration, in Hz. */\n samplingRate: number;\n /** Byte offset of this signal's samples within one data record. */\n byteOffsetInRecord: number;\n}\n\nexport interface EdfHeader {\n version: string;\n patientId: string;\n recordingId: string;\n /** Raw 'dd.mm.yy' as written in the file. */\n startDateRaw: string;\n /** Raw 'hh.mm.ss' as written in the file. */\n startTimeRaw: string;\n /** Resolved start instant, or null when the file's date/time fields are unusable. */\n startDateTime: Date | null;\n /**\n * Header size in bytes, computed from the signal count rather than read from the field.\n *\n * Every data record offset is derived from this, so it has to be the one the layout\n * actually uses: 256 for the fixed header plus 256 per signal. A writer that fills the\n * field in carelessly is common enough to have its own warning, HEADER_BYTES_MISMATCH,\n * and trusting the field over the arithmetic would put every sample at the wrong offset.\n */\n headerBytes: number;\n /**\n * What the header's own length field says, which need not be the above.\n *\n * Exposed for the same reason `declaredRecordCount` is: the two disagreeing is a fact\n * about the file, and a caller checking how a recording was written should be able to see\n * what it claimed rather than only what was believed.\n */\n declaredHeaderBytes: number;\n reserved: string;\n isEdfPlus: boolean;\n /** True for BioSemi BDF/BDF+ files, whose samples are 3 bytes rather than 2. */\n isBdf: boolean;\n /** 'EDF+C' continuous, 'EDF+D' discontinuous, or null for plain EDF. */\n continuity: 'EDF+C' | 'EDF+D' | null;\n /** As declared in the header. -1 means \"unknown\", which the spec permits. */\n declaredRecordCount: number;\n recordDuration: number;\n signalCount: number;\n signals: EdfSignal[];\n bytesPerSample: number;\n recordBytes: number;\n}\n\n/** Everything derived by combining the header with the file's real size. */\nexport interface EdfHeaderInfo {\n header: EdfHeader;\n /** Record count implied by the actual file size — the one we trust for reading. */\n recordCount: number;\n /** Bytes after the last complete data record. */\n trailingBytes: number;\n diagnostics: Diagnostic[];\n}\n\n/**\n * Whether a spreadsheet reads this field as the start of a formula rather than as text.\n *\n * `=` and `@` unconditionally; every list of these characters names two more, and this had\n * `-` as an exception with the reason written out on the warnings page: \"a lone `-` is a\n * real convention for no unit ... and neither is evaluated unless what follows it parses as a\n * formula\". Which is the condition, and it was not being applied — nothing with a leading\n * minus was flagged at all. A channel labelled `-2+3` opens as a column headed `1`, and\n * `-HYPERLINK(\"http://...\",\"EEG\")` is a name the spreadsheet resolves, in silence.\n *\n * So the exception is what it says it is rather than the whole character. A lone `-` is left\n * as text by every spreadsheet and is not flagged; a field that is entirely a number reads as\n * that number, which is what the header says, and is not flagged either. Anything else after\n * the minus is arithmetic or a name.\n *\n * And `+` takes the same exception, which it did not. It is the same rule in the spreadsheet —\n * Lotus compatibility, which converts a leading `+` or `-` to a formula when what follows one\n * parses as a formula and leaves it as text when it does not — so the two signs cannot differ\n * here for a reason that comes from the sign. A channel labelled `+100` was warned about as\n * something a spreadsheet \"reads as the start of a formula rather than as text\", over a cell\n * that opens as 100, which is what the header says; `-100` beside it said nothing, and under\n * `--strict` the difference was an exit code. `+1+1` is still arithmetic and still flagged.\n */\nexport function startsFormula(text: string): boolean {\n if (/^[=@]/u.test(text)) return true;\n if (!/^[+-]./u.test(text)) return false;\n // `\\.\\d*` and not `\\.\\d+`, which is the spelling DECIMAL_FIELD and DECIMAL_DURATION both\n // use for the same question. A trailing point is a number with nothing after it — Excel\n // opens `+1.` as 1 — and one character of difference between three copies of one grammar\n // had this warning firing on a field the paragraph above exempts by name, and failing\n // --strict for it.\n return !/^[+-](?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$/u.test(text);\n}\n\n/**\n * A byte the terminal treats as an instruction rather than as text.\n *\n * C0 and C1, plus DEL. Tab is included deliberately: it is harmless to a terminal but it\n * makes a CSV column name that cannot be typed or matched reliably, which is the other half\n * of what this warning is for.\n */\nfunction isControlCharacter(character: string): boolean {\n const code = character.codePointAt(0) as number;\n return code <= 0x1f || (code >= 0x7f && code <= 0x9f);\n}\n\nconst dec = (buf: Uint8Array, start: number, len: number): string =>\n decodeLatin1(buf, start, start + len);\n\n/** EDF fields are space-padded; trailing NULs also occur in files written by sloppy tools. */\nconst trimField = (s: string): string => s.replace(/[\\0\\s]+$/u, '').replace(/^\\s+/u, '');\n\n/**\n * How many signals the fixed header says there are, read exactly as `parseHeader` will.\n *\n * `EdfFile.open` needs this before it can know how much header to read, and it used to work\n * it out with its own `Number(...)` — which was NUL-tolerant but not comma-tolerant, unlike\n * every other numeric field here. A header written with a comma decimal separator, which\n * COMMA_DECIMAL exists to accept and which the documentation lists this field among, was\n * therefore never given its signal headers at all, and the file died on a message that\n * contradicted itself: \"needs a 768-byte header, but the file is only 848 bytes\".\n *\n * Sharing the parse is what keeps the two from disagreeing again about which files are\n * readable. Null means \"not a usable count\", and the caller reads no further header — the\n * real error then comes from `parseHeader`, which is the one place that decides.\n */\nexport function peekSignalCount(fixed: Uint8Array): number | null {\n const text = normaliseNumberField(dec(fixed, 252, 4)).text;\n if (!DECIMAL_FIELD.test(text)) return null;\n const count = Number(text);\n return Number.isInteger(count) && count > 0 ? count : null;\n}\n\n/**\n * What EDF allows a numeric field to look like, which is less than `Number()` allows.\n *\n * A sign, digits, an optional fractional part, an optional exponent — the spec's own grammar,\n * plus the exponent form the 8-character physical bounds need to reach a magnetometer's range\n * and which real headers use.\n *\n * `Number()` accepts a great deal more, and every one of those forms is a header this tool\n * would have read as a number nobody wrote. A physical maximum of `0x64` came out as 100: it\n * printed as `-100 to 100` in the channel table, went into channels.csv as `physical_max,100`,\n * and set the gain every sample on that channel was scaled by — a whole calibration invented\n * from four bytes that are not a decimal number, exit 0, no diagnostic. `0b1100100` and `0o144`\n * are the same hundred, and `0x02` in the signal-count field is a two-channel recording.\n *\n * The same mistake as `#0x2` reaching channel 2 through `--channels`, `--decimals 0o5` writing\n * five places and `--jobs 0x10` running sixteen, all of which have their own comments and their\n * own fixes. Those were values somebody typed. These are the fields every number in the output\n * is computed from, and the page describing them says \"all fields are ASCII\" and gives the\n * layout digit by digit.\n */\nconst DECIMAL_FIELD = /^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$/u;\n\n/** A numeric header field, trimmed and with a comma decimal separator turned into a dot. */\nfunction normaliseNumberField(raw: string): { text: string; sawComma: boolean } {\n const text = trimField(raw);\n // Some writers emit a comma decimal separator despite the spec requiring '.'.\n if (text.includes(',') && !text.includes('.')) {\n return { text: text.replace(',', '.'), sawComma: true };\n }\n return { text, sawComma: false };\n}\n\n/**\n * What a header field that is not the number it must be usually means.\n *\n * `BAD_HEADER_FIELD` is raised four ways — an empty numeric field, one that does not parse, one\n * that must be whole and is not, and a signal declaring a negative sample count — and one of\n * the four carried this. The other three printed a bare sentence about a field and stopped,\n * which is the same gap `EdfFile.#UNREADABLE_HINT` was written to close on the read side: a\n * family of errors where one member says what the cause usually is and the rest do not.\n *\n * An empty field is if anything the plainer symptom of the two. A truncated download stops\n * mid-header and leaves the rest as spaces; a byte-shifted file reads a field off the wrong\n * offset and finds whitespace as often as it finds digits. The page has said all four mean\n * this since they were documented together.\n */\nconst BAD_FIELD_HINT = 'The file may be truncated, byte-shifted, or not an EDF file at all.';\n\nfunction parseNumberField(\n raw: string,\n field: string,\n { integer = false, sawComma }: { integer?: boolean; sawComma?: { value: boolean } } = {},\n): number {\n const normalised = normaliseNumberField(raw);\n const text = normalised.text;\n if (normalised.sawComma && sawComma) sawComma.value = true;\n if (text === '') {\n throw new EdfError('BAD_HEADER_FIELD', `Header field \"${field}\" is empty.`, BAD_FIELD_HINT);\n }\n const n = DECIMAL_FIELD.test(text) ? Number(text) : NaN;\n if (!Number.isFinite(n)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Header field \"${field}\" is not a number (found ${JSON.stringify(text)}).`,\n BAD_FIELD_HINT,\n );\n }\n if (integer && !Number.isInteger(n)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Header field \"${field}\" must be a whole number (found ${JSON.stringify(text)}).`,\n BAD_FIELD_HINT,\n );\n }\n return n;\n}\n\n/** Whether the start time names the sixtieth second. See LEAP_SECOND_START. */\nfunction namesLeapSecond(timeRaw: string): boolean {\n const t = /^(\\d{2})[.:\\-](\\d{2})[.:\\-](\\d{2})$/u.exec(trimField(timeRaw));\n return t !== null && Number(t[3]) === 60;\n}\n\nconst MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'];\n\n/**\n * The date an EDF+ recording ID states, which is the only place the file writes a full year.\n *\n * EDF+ requires the recording identification field to begin `Startdate dd-MMM-yyyy`, and\n * requires it to agree with the header's own date field. It is a four-digit year, so it says\n * something the eight-character date field physically cannot.\n */\nfunction recordingIdStartdate(\n recordingId: string,\n): { day: number; month: number; year: number } | null {\n const stated = /^Startdate\\s+(\\d{2})-([A-Za-z]{3})-(\\d{4})(?:\\s|$)/u.exec(recordingId.trim());\n if (!stated) return null;\n const month = MONTHS.indexOf((stated[2] as string).toUpperCase()) + 1;\n if (month === 0) return null;\n return { day: Number(stated[1]), month, year: Number(stated[3]) };\n}\n\n/**\n * EDF stores a two-digit year. The spec pins the century: 85-99 mean 1985-1999\n * and 00-84 mean 2000-2084. Files outside 1985-2084 cannot express their date.\n *\n * Which is why EDF+ writes it again in full, in the recording identification field, and why\n * that is used here when it is there. The rule alone reports a recording made in 1984 as 2084\n * and one made in 2085 as 1985 — a hundred years out, on a file that states the year plainly\n * four fields earlier. Taken only where the two agree about everything the header can express:\n * the same day, the same month, and a four-digit year ending in the two digits the header\n * wrote. A recording ID that contradicts the header is a different problem and is left to the\n * spec's rule, which is at least the one the format defines.\n */\nfunction resolveStartDateTime(\n dateRaw: string,\n timeRaw: string,\n recordingId = '',\n): Date | null {\n const d = /^(\\d{2})[.\\-/](\\d{2})[.\\-/](\\d{2})$/u.exec(trimField(dateRaw));\n const t = /^(\\d{2})[.:\\-](\\d{2})[.:\\-](\\d{2})$/u.exec(trimField(timeRaw));\n if (!d || !t) return null;\n\n const dd = Number(d[1]);\n const mm = Number(d[2]);\n const yy = Number(d[3]);\n const hh = Number(t[1]);\n const mi = Number(t[2]);\n const ss = Number(t[3]);\n\n if (mm < 1 || mm > 12 || dd < 1 || dd > 31 || hh > 23 || mi > 59 || ss > 60) return null;\n\n const stated = recordingIdStartdate(recordingId);\n const year =\n stated !== null && stated.day === dd && stated.month === mm && stated.year % 100 === yy\n ? stated.year\n : yy >= 85\n ? 1900 + yy\n : 2000 + yy;\n const date = new Date(Date.UTC(year, mm - 1, dd, hh, mi, Math.min(ss, 59)));\n // Reject dates that rolled over, e.g. 31.02.\n if (date.getUTCMonth() !== mm - 1 || date.getUTCDate() !== dd) return null;\n return date;\n}\n\n/**\n * Parse the fixed 256-byte header plus the per-signal header block.\n *\n * @param buf At least FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES bytes.\n * @param fileSize Total size of the file on disk, used to derive the real record count.\n */\nexport function parseHeader(buf: Uint8Array, fileSize: number): EdfHeaderInfo {\n /*\n The second argument, checked before anything is derived from it.\n\n Every count this function reports about the data comes out of `fileSize`, and nothing\n asked what it was. Omit it — which a JavaScript caller does by writing the call the way\n the one-argument functions beside it are written — and the arithmetic runs on `undefined`:\n\n parseHeader(bytes).recordCount // NaN\n parseHeader(bytes).diagnostics[0].message\n // \"The header declares 2 data records but the file contains NaN. Converting the NaN\n // records that are present.\"\n\n A header object claiming NaN records, a warning that says NaN out loud, and no error —\n and `recordCount * recordDuration`, which is how the duration is worked out everywhere\n this value goes, is NaN from there on. `null` was worse from 0.8.5, when the counts in\n these messages started being grouped: grouping it is a TypeError raised from inside the\n number formatter, naming nothing the caller wrote.\n\n The same `OptionError` `assertInputPath` raises for the first argument of `EdfFile.open`,\n for the same reason given there: it is the call that is wrong, not the recording.\n */\n if (typeof fileSize !== 'number' || !Number.isFinite(fileSize) || fileSize < 0) {\n throw new OptionError(\n `fileSize must be the number of bytes in the file, got ${describeValue(fileSize)}.`,\n );\n }\n\n const diagnostics: Diagnostic[] = [];\n const sawComma = { value: false };\n\n if (buf.length < FIXED_HEADER_BYTES) {\n /*\n Which of the two is actually short — the same question the FILE_TOO_SMALL fifty lines\n down was fixed for, in the words its comment still uses: \"The file size was quoted\n either way, so a caller that had read too little ... produced arithmetic that refuted\n itself.\" This is the raising that comment describes, and it went on doing it:\n\n parseHeader(new Uint8Array(100), 5000)\n File is 5,000 bytes; an EDF header alone needs at least 256.\n\n 5,000 is not less than 256. A reader following that sentence looks for a truncation\n that is not there, and the number they are given to check against the file is the one\n that is fine. Reached from the library, where the caller holds both — `EdfFile.open`\n reads 256 bytes before it reads anything else, so the two agree by construction there.\n */\n const short = fileSize < FIXED_HEADER_BYTES;\n throw new EdfError(\n 'FILE_TOO_SMALL',\n short\n ? `File is ${counted(fileSize, 'byte')}; an EDF header alone needs at least ${FIXED_HEADER_BYTES}.`\n : `An EDF header alone needs ${FIXED_HEADER_BYTES} bytes, but only ` +\n `${grouped(buf.length)} of this ${grouped(fileSize)}-byte file reached the parser.`,\n short\n ? 'A file this small is truncated, or is not an EDF or BDF recording at all: every one of ' +\n 'them opens with a fixed 256-byte header.'\n : 'parseHeader takes the header block and the size of the file it came out of. This ' +\n 'call was given fewer bytes than the header it was asked to read.',\n );\n }\n\n // BDF (BioSemi) marks itself with byte 255 followed by 'BIOSEMI', and stores\n // 3-byte samples instead of 2. Everything else about the layout is identical.\n const isBdf = buf[0] === 0xff && dec(buf, 1, 7) === 'BIOSEMI';\n const version = isBdf ? 'BIOSEMI' : trimField(dec(buf, 0, 8));\n\n const patientId = trimField(dec(buf, 8, 80));\n const recordingId = trimField(dec(buf, 88, 80));\n const startDateRaw = trimField(dec(buf, 168, 8));\n const startTimeRaw = trimField(dec(buf, 176, 8));\n const headerBytes = parseNumberField(dec(buf, 184, 8), 'number of header bytes', {\n integer: true,\n sawComma,\n });\n const reserved = trimField(dec(buf, 192, 44));\n const declaredRecordCount = parseNumberField(dec(buf, 236, 8), 'number of data records', {\n integer: true,\n sawComma,\n });\n const recordDuration = parseNumberField(dec(buf, 244, 8), 'duration of a data record', {\n sawComma,\n });\n const signalCount = parseNumberField(dec(buf, 252, 4), 'number of signals', {\n integer: true,\n sawComma,\n });\n\n if (signalCount <= 0) {\n throw new EdfError(\n 'INVALID_SIGNAL_COUNT',\n `Header declares ${counted(signalCount, 'signal')}; expected at least 1.`,\n BAD_FIELD_HINT,\n );\n }\n if (!(recordDuration > 0)) {\n throw new EdfError(\n 'INVALID_RECORD_DURATION',\n `Header declares a data record duration of ${plain(recordDuration)}s; expected a positive number.`,\n BAD_FIELD_HINT,\n );\n }\n\n const expectedHeaderBytes = FIXED_HEADER_BYTES + signalCount * SIGNAL_HEADER_BYTES;\n if (buf.length < expectedHeaderBytes) {\n throw new EdfError(\n 'FILE_TOO_SMALL',\n /*\n Which of the two is actually short.\n\n The file size was quoted either way, so a caller that had read too little — the\n signal count parsed one way here and another way there — produced arithmetic that\n refuted itself: \"needs a 768-byte header, but the file is only 848 bytes\". A reader\n following that looks for a truncation that is not there.\n */\n `File declares ${counted(signalCount, 'signal')}, which needs a ${grouped(expectedHeaderBytes)}-byte header, ` +\n (fileSize < expectedHeaderBytes\n ? `but the file is only ${grouped(fileSize)} bytes.`\n : `but only ${grouped(buf.length)} bytes of it were handed to the parser.`),\n 'Either the recording is truncated inside its own header, or the signal count was ' +\n 'read from bytes that are not it. Both leave the channel descriptions unfinished.',\n );\n }\n if (headerBytes !== expectedHeaderBytes) {\n diagnostics.push({\n code: 'HEADER_BYTES_MISMATCH',\n severity: 'warning',\n message:\n `Header says it is ${grouped(headerBytes)} bytes, but ${counted(signalCount, 'signal')} ` +\n `${signalCount === 1 ? 'requires' : 'require'} ${grouped(expectedHeaderBytes)} bytes. ` +\n `Using the value computed from the signal count.`,\n hint:\n 'Where the data starts is worked out from the signal count, not from this field, so ' +\n 'nothing is read from the wrong offset. A writer that got this one wrong may have ' +\n 'got others wrong too.',\n });\n }\n\n // Signal headers are field-major: all labels, then all transducers, and so on.\n const base = FIXED_HEADER_BYTES;\n const readField = (offsetUnits: number, width: number, i: number): string =>\n dec(buf, base + offsetUnits * signalCount + i * width, width);\n\n // EDF+ writes 'EDF+C'/'EDF+D' here; BDF+ writes 'BDF+C'/'BDF+D'. The two mean the\n // same thing, so both are normalised to a single continuity marker.\n const continuityTag = /^(?:EDF|BDF)\\+([CD])/u.exec(reserved);\n const continuity: 'EDF+C' | 'EDF+D' | null =\n continuityTag === null ? null : continuityTag[1] === 'D' ? 'EDF+D' : 'EDF+C';\n\n const signals: EdfSignal[] = [];\n let byteOffsetInRecord = 0;\n const bytesPerSample = isBdf ? 3 : 2;\n const seenLabels = new Map<string, number[]>();\n const emptyLabels: number[] = [];\n\n for (let i = 0; i < signalCount; i++) {\n const label = trimField(readField(0, 16, i));\n const transducer = trimField(readField(16, 80, i));\n const physicalDimension = trimField(readField(96, 8, i));\n const physicalMin = parseNumberField(readField(104, 8, i), `physical minimum (signal ${i})`, {\n sawComma,\n });\n const physicalMax = parseNumberField(readField(112, 8, i), `physical maximum (signal ${i})`, {\n sawComma,\n });\n const digitalMin = parseNumberField(readField(120, 8, i), `digital minimum (signal ${i})`, {\n integer: true,\n sawComma,\n });\n const digitalMax = parseNumberField(readField(128, 8, i), `digital maximum (signal ${i})`, {\n integer: true,\n sawComma,\n });\n const prefiltering = trimField(readField(136, 80, i));\n const samplesPerRecord = parseNumberField(\n readField(216, 8, i),\n `samples per record (signal ${i})`,\n { integer: true, sawComma },\n );\n const sigReserved = trimField(readField(224, 32, i));\n\n if (samplesPerRecord < 0) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Signal ${i} (\"${label}\") declares ${samplesPerRecord} samples per record.`,\n BAD_FIELD_HINT,\n );\n }\n\n const isAnnotations = label === ANNOTATIONS_LABEL || label === BDF_ANNOTATIONS_LABEL;\n\n signals.push({\n index: i,\n label,\n transducer,\n physicalDimension,\n physicalMin,\n physicalMax,\n digitalMin,\n digitalMax,\n prefiltering,\n samplesPerRecord,\n reserved: sigReserved,\n isAnnotations,\n samplingRate: samplesPerRecord / recordDuration,\n byteOffsetInRecord,\n });\n byteOffsetInRecord += samplesPerRecord * bytesPerSample;\n\n if (!isAnnotations) {\n /*\n A label is free text out of the file, and it becomes a column name in signals.csv.\n\n `--info` has escaped control bytes since it was written, because an ANSI escape in a\n header can drive the reader's terminal — `\\x1b[2J` clears the screen. The CSV had no\n such protection and needed none for correctness: quoting makes any byte safe for a\n parser, and this still passes the label through exactly as the file gives it, because\n losing what the header says is not an improvement.\n\n What was missing is the sentence saying so. A recording whose channel is labelled\n `\\x1b[2Jgone` converted with no warning at all, and `cat signals.csv` then cleared\n the terminal — while a script referencing that column by name carried an invisible\n control character in it. NONPRINTABLE_LABEL has been declared and documented as\n reserved since 0.1; this is it doing its job.\n */\n /*\n Which of the two fields carries them, because the consequences are not the same.\n\n The message said \"label or unit\", and then said the bytes \"will appear in the CSV\n column name\" and that \"the name cannot be typed\" — both of which are about the label.\n A channel labelled plainly `ECG` in a unit of `u\\x07V` got all of it: its column is\n `ECG`, `--channels ECG` selects it and exits 0, and the byte is in channels.csv's\n `unit` cell, which the warning never mentioned. Three sentences, none of them true of\n the file that raised it, on a warning whose whole purpose is to say where an invisible\n byte went.\n */\n /*\n All four free-text fields, not the two that were checked.\n\n `transducer` and `prefiltering` are free text out of the header exactly as the label\n and the unit are, and they land in channels.csv exactly as the unit does — so an ESC\n byte in a transducer field reached the CSV raw with nothing said, and `cat\n channels.csv` would drive the terminal. That is the hazard this warning exists for,\n two columns over. 0.5.71 made it name which field carries them; this is the rest of\n the fields it can name.\n */\n const fields = [\n ['label', label],\n ['unit', physicalDimension],\n ['transducer', transducer],\n ['prefiltering', prefiltering],\n ] as const;\n const affected = fields.filter(([, text]) => [...text].some(isControlCharacter));\n const control = affected.flatMap(([, text]) => [...text].filter(isControlCharacter));\n if (control.length > 0) {\n /*\n How many of them get named is the header's decision, and it was unbounded.\n\n Every other enumeration whose length the file controls goes through `listed`, which\n is the helper written for exactly this — \"without letting the file decide how long\n the sentence gets\". This one joined the whole set. Four free-text fields of 16, 8, 80\n and 80 bytes can carry 63 distinct control codes between them, and did:\n\n warning: Signal 0's label, unit and transducer contain 63 control characters\n (\\x01, \\x02, \\x03, ... \\x9e), which will appear in the CSV column name and in\n channels.csv exactly as the header has them.\n\n 549 characters on one line, from a warning whose point is the short sentence around\n the list. The count is already in that sentence — \"63 control characters\" — so the\n tail the cut hides is the part the reader was told twice.\n */\n const shown = listed(\n [...new Set(control)].map(\n (c) => `\\\\x${(c.codePointAt(0) as number).toString(16).padStart(2, '0')}`,\n ),\n );\n const inLabel = affected.some(([name]) => name === 'label');\n // \"label and unit\", not \"label, unit\" — `listed` is for long enumerations that get\n // truncated, and this is a sentence with at most four items in it.\n const names = affected.map(([name]) => name);\n const named =\n names.length === 1\n ? (names[0] as string)\n : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1] as string}`;\n // Where they land, which is the question the reader has. A label becomes a column\n // name in signals.csv; the other three are cells of channels.csv and nothing else.\n // Named down to the cell when there is one of them, because that is the answer to\n // \"where did it go\" — `channels.csv` alone leaves a reader scanning fourteen columns.\n /*\n And whether there is a channels.csv at all, which this took for granted.\n\n `--stdout` writes the signal table and nothing else — no channels.csv, no\n annotations.csv, no metadata.json. A control byte in a unit reaches no file at all\n in that mode, and this said it would be in a cell of one:\n\n $ edf2csv control-labels.edf --stdout | less\n warning: Signal 0's label and unit contain 2 control characters (\\x1b), which\n will appear in the CSV column name and in channels.csv's unit cell\n exactly as the header has them.\n ... Printing the CSV to a terminal may do more than print it.\n\n Which is the mode this warning matters most in: `--stdout` *is* printing the CSV to\n a terminal, and the hint two lines down says so. The header is parsed before any\n destination is chosen, so the sentence says what is true of both rather than\n guessing — the same reason the `DUPLICATE_LABEL` hint above names two layouts.\n */\n const cells = affected.filter(([name]) => name !== 'label').map(([name]) => name);\n const where =\n cells.length === 1\n ? `channels.csv's ${cells[0] as string} cell in any conversion that writes one`\n : 'the channels.csv of any conversion that writes one';\n /*\n \"The CSV column name\" is the wide layout's answer, given in both.\n\n A long signals.csv has three columns — time_s, channel, value — and a label is not\n one of them: it is a value in the `channel` column, once per row. So a control byte\n in a label reaches one cell of the header line under the default layout and every\n row of the table under `--layout long`, and the sentence naming where it went named\n only the first. Same fault 0.8.24 fixed for `DUPLICATE_LABEL`'s message and 0.8.43\n for its hint.\n\n The parser cannot know the layout, and unlike those two this sentence does not have\n room to name both — it already carries the field list, the byte list and where the\n cell fields land. \"The channel's name\" is what both layouts call it: the term the\n long layout's own warnings use, and what a column name is.\n */\n const lands =\n inLabel && cells.length > 0\n ? `which will appear as the channel's name in signals.csv and in ${where}`\n : inLabel\n ? \"which will appear as the channel's name in signals.csv\"\n : `which will appear in ${where}`;\n diagnostics.push({\n code: 'NONPRINTABLE_LABEL',\n severity: 'warning',\n message:\n `Signal ${i}'s ${named} ${affected.length === 1 ? 'contains' : 'contain'} ` +\n // A comma before the closing clause, which now has a clause of its own inside it.\n `${counted(control.length, 'control character')} (${shown}), ${lands}, exactly as the ` +\n `header has ${control.length === 1 ? 'it' : 'them'}.`,\n hint:\n /*\n Every branch has to print a command that works.\n\n The middle one quoted the label back, which is right until the label is empty:\n an unlabelled channel got `--channels \"\"`, and that exits 2 with \"--channels was\n given but lists no channel names\". A hint whose command fails is worse than no\n hint, and this warning's whole job is to say how to reach a channel whose header\n text you cannot type. `EMPTY_LABEL` already says the position is the only way in\n for such a channel; so does this now.\n\n A comma is the third way. `--channels` separates names with one, and splits on\n every occurrence, so a channel labelled `EEG Fpz-Cz, ref` cannot be selected by\n name at all: the quoted-back advice printed `--channels \"EEG Fpz-Cz, ref\"`, which\n exits 2 with `No channel named \"EEG Fpz-Cz\"` — a channel the file does not have,\n named after half of one it does. Commas in labels are ordinary, since EDF labels\n are free text, and the CSV header quotes them; only this one hint claimed\n something about them that isn't so.\n */\n /*\n And a fourth way, which this branch printed straight past.\n\n A shell expands `$`, a backtick and a backslash inside double quotes, so a\n channel labelled `EEG $ref` was answered with `--channels \"EEG $ref\"` — which\n arrives as `EEG ` and exits 2 on a channel the file does not have. A backtick\n opens a command substitution and the pasted line does not even finish. That is\n the failure 0.7.18 fixed for `--channels`' own suggestion, and `typeable` is the\n rule it fixed it with: double quotes where they survive, single quotes where\n they do not, null where nothing does. Asking it settles which branch this takes\n as well, so the two cannot disagree about one label again.\n */\n (typeable(label) === null\n ? `Address the channel by position with --channels \"#${i}\" rather than by name, ` +\n `since ${\n inLabel\n ? 'the name cannot be typed'\n : label === ''\n ? 'it has no label'\n : 'a comma in the label would read as two names'\n }. `\n : `The column name is unaffected, so --channels ${typeable(label) as string} still selects it. `) +\n 'Printing the CSV to a terminal may do more than print it.',\n });\n }\n\n /*\n Header text a spreadsheet will run rather than read.\n\n The same four free-text fields, asked a different question. `=`, `+` and `@` start a\n formula in Excel, LibreOffice and Sheets no matter which file the cell came from, and\n these fields land in a CSV header row and in channels.csv verbatim — so a channel\n labelled `=1+1` opens as a column headed 2, and `=HYPERLINK(...)` opens as a link\n nobody in the reading chain wrote. SECURITY.md already calls these fields\n attacker-controlled because they reach filenames; this is where they reach a program\n that executes text.\n\n Not `-`, which the same advice usually includes. A lone `-` is a real convention for\n \"no unit\" and appears in the fixtures, a leading `-` on a montage label is ordinary,\n and neither is executed unless what follows parses as a formula — so warning on it\n would fire on files that are fine, which is how a warning gets ignored.\n\n Said, not fixed. Prefixing the cell with a quote is the usual mitigation and would mean\n writing something the header does not say, which is the one thing this tool refuses to\n do; NONPRINTABLE_LABEL answers control bytes the same way.\n */\n const formulaic = fields.filter(([, text]) => startsFormula(text));\n if (formulaic.length > 0) {\n const names = formulaic.map(([name]) => name);\n const named =\n names.length === 1\n ? (names[0] as string)\n : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1] as string}`;\n const shown = [...new Set(formulaic.map(([, text]) => text[0] as string))].join(', ');\n diagnostics.push({\n code: 'FORMULA_LABEL',\n severity: 'warning',\n message:\n `Signal ${i}'s ${named} ${formulaic.length === 1 ? 'starts' : 'start'} with ` +\n `${shown}, which Excel, LibreOffice and Google Sheets read as the start of a ` +\n `formula rather than as text.`,\n hint:\n 'The text is written exactly as the header has it, so the cell is what the ' +\n 'recording says. Open the CSV with pandas or R, or import it into the ' +\n 'spreadsheet as text, if you do not want it evaluated.',\n });\n }\n\n if (label === '') {\n // Collected, not reported here: what this channel's column ends up called depends on\n // whether some later channel is literally labelled `signal_<i>`, and inside this loop\n // the later channels do not exist yet. See the pass below.\n emptyLabels.push(i);\n } else {\n // Collected rather than reported here: a label repeated five times should\n // produce one warning naming all five, not four near-identical pairs.\n const seen = seenLabels.get(label);\n if (seen) seen.push(i);\n else seenLabels.set(label, [i]);\n }\n\n if (samplesPerRecord === 0) {\n diagnostics.push({\n code: 'NO_SAMPLES',\n severity: 'warning',\n message: `Signal ${i} (\"${label}\") carries no samples at all (0 per data record).`,\n hint: 'It is described in channels.csv but left out of the converted data.',\n });\n }\n\n /*\n Too large to represent, and too small — the second was silent.\n\n The gain is the span divided by the digital range, and a span of 2e-320 over 65,535\n codes is 3e-325: below the smallest subnormal double, so it underflows to +0. The\n scaler's flat-range branch then handed every code the same physical value, and a\n channel of 65,536 distinct readings became one repeated number with nothing raised at\n all. One power of ten away, at 1e-319, the same file raises VALUE_RESOLUTION.\n\n Both are the same fact about the header — the span cannot be turned into a mapping —\n so both get this code, and both leave the cells empty rather than filling them with a\n value the header cannot justify.\n */\n const span = physicalMax - physicalMin;\n const underflowed = span !== 0 && span / (digitalMax - digitalMin) === 0;\n if (!Number.isFinite(span) || underflowed) {\n diagnostics.push({\n code: 'UNUSABLE_PHYSICAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") declares a physical range from ${physicalMin} to ` +\n `${physicalMax}, whose span is too ${underflowed ? 'small' : 'large'} to ` +\n `represent, so its values cannot be scaled.`,\n hint: 'Its cells are left empty rather than filled with a value the header cannot justify.',\n });\n } else if (digitalMax === digitalMin) {\n diagnostics.push({\n code: 'DEGENERATE_DIGITAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") has digital minimum equal to digital maximum ` +\n `(${digitalMin}), so its values cannot be scaled.`,\n hint: 'Its cells are left empty rather than filled with a value the header cannot justify.',\n });\n } else if (physicalMax === physicalMin) {\n diagnostics.push({\n code: 'DEGENERATE_PHYSICAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") has physical minimum equal to physical maximum ` +\n `(${physicalMin}), so every sample converts to the same value.`,\n /*\n The one branch of the four that fills the cells, and the only one that said\n nothing about them.\n\n Its three neighbours all end \"Its cells are left empty rather than filled with a\n value the header cannot justify\", which is the opposite of what happens here: the\n mapping is defined — it has one point in it — so `physicalMin` is written into\n every cell, and a reader who had learned the family's answer from the branch above\n gets it backwards. A column of one repeated number is also the shape a genuinely\n flat recording takes, which is the confusion worth heading off.\n */\n hint:\n 'Its cells carry that value rather than being left empty, since the mapping is ' +\n 'defined — it just has one point in it. The distinct digital codes behind them ' +\n 'are not recoverable from the CSV.',\n });\n } else if ((physicalMax - physicalMin) * (digitalMax - digitalMin) < 0) {\n /*\n Polarity is inverted when the gain is negative, and the gain is\n (physicalMax - physicalMin) / (digitalMax - digitalMin) — so it is the sign of the\n two spans together that matters, not the physical pair alone.\n\n Testing only `physicalMax < physicalMin` was wrong in both directions. A file with\n its DIGITAL bounds reversed is just as inverted and drew no warning at all, handing\n back sign-flipped EEG with nothing to indicate it. A file with BOTH pairs reversed\n has a positive gain and is not inverted, yet was warned about — a message that was\n simply untrue of that recording.\n */\n const reversed =\n physicalMax < physicalMin\n ? `physical minimum ${physicalMin} above physical maximum ${physicalMax}`\n : `digital minimum ${digitalMin} above digital maximum ${digitalMax}`;\n diagnostics.push({\n code: 'INVERTED_PHYSICAL_RANGE',\n severity: 'warning',\n message: `Signal ${i} (\"${label}\") declares ${reversed}, which inverts its polarity.`,\n hint: 'The values are converted exactly as the header specifies, inversion included.',\n });\n }\n }\n }\n\n /*\n What an unlabelled channel is actually called, which the message used to guess.\n\n A channel with no label takes `signal_<index>` — unless another channel is literally\n labelled that, which EDF permits, since labels are free text and nothing enforces anything\n about them. Then both collide and both are suffixed. The warning said \"It will appear as\n \"signal_0\"\" while the file's header read `time_s,signal_0_ch0,signal_0_ch1`: the one\n sentence the run printed named a column that exists in neither signals.csv nor\n channels.csv.\n\n The other half was silent. The channel that genuinely carries the label `signal_0` lost\n its own column name to a collision with a synthesised one, and nothing said so —\n DUPLICATE_LABEL did not fire, because the two labels are not the same label. Both halves\n are one sentence here, because they are one event.\n\n No specific suffixed name is quoted. The suffix rule has a second pass for names that are\n still shared afterwards, and a message that hard-coded `_ch<index>` would be guessing again\n in exactly the way this is fixing.\n */\n for (const index of emptyLabels) {\n const taken = seenLabels.get(`signal_${index}`);\n diagnostics.push({\n code: 'EMPTY_LABEL',\n severity: 'warning',\n message:\n taken === undefined\n ? `Signal ${index} has no label. It will appear as \"signal_${index}\".`\n : `Signal ${index} has no label, so it takes the name \"signal_${index}\" — which ` +\n `${taken.length === 1 ? 'signal' : 'signals'} ${listed(taken.map(String))} already ` +\n `${taken.length === 1 ? 'carries' : 'carry'} as a label, so both columns are ` +\n `suffixed with their position instead.`,\n /*\n The position, not the word \"position\".\n\n A hint that offers a command has to offer one that can be typed — 0.7.18 and 0.7.19\n went through the others making sure each one could be pasted, and NONPRINTABLE_LABEL\n two hundred lines up writes `--channels \"#${i}\"` with the number in it. This one said\n `--channels \"#<position>\"` while the sentence above it began \"Signal 0\", so the\n reader was asked to carry a number across from the message and put it in themselves.\n */\n hint:\n 'The name is built from the position rather than read from the header, so it moves ' +\n `if the file's channels do. Select the channel as --channels \"#${index}\" to say ` +\n 'which one you mean without depending on it.',\n });\n }\n\n /*\n A timestamp that is not one.\n\n EDF gives the start date and time eight characters each, and nothing stops a writer\n putting `32.13.99` and `25.61.61` there. `--info` has always echoed the raw fields with\n \"(unparseable)\" beside them, but nothing was raised: the conversion exited 0, `--strict`\n passed, and metadata.json recorded `start_datetime_local: null` with no note against it.\n\n Every other unusable header field reports itself — a degenerate digital range, a physical\n span that cannot be represented, a comma decimal separator, a header whose declared size\n disagrees with its signal count. This was the one that did not, and it is the field\n output-files points at for turning `time_s` into an absolute instant.\n */\n /*\n The sixtieth second, which is a second UTC has and a calendar date does not.\n\n `resolveStartDateTime` admits `ss === 60` on purpose — a recorder synchronised to UTC\n through a leap second writes `23.59.60`, and refusing it would throw away a date that is\n otherwise perfectly good over one second. What it then does is `Math.min(ss, 59)`, because\n `Date.UTC(..., 60)` rolls over into the next minute and would move the instant the other\n way, by fifty-nine seconds more.\n\n Keeping the nearest instant is the right answer. Keeping it in silence was not: `--info`\n printed `Recorded 2020-01-01 23:59:59` and metadata.json recorded the same, for a header\n that says `23.59.60`, with `--strict` exiting 0. Every other header field this tool cannot\n represent exactly says so — a comma decimal separator, a physical span that overflows, a\n record count that disagrees with the file — and this is the field `time_s` is documented as\n being added to.\n */\n if (\n namesLeapSecond(startTimeRaw) &&\n resolveStartDateTime(startDateRaw, startTimeRaw, recordingId) !== null\n ) {\n diagnostics.push({\n code: 'LEAP_SECOND_START',\n severity: 'warning',\n message:\n `The header's start time (\"${startTimeRaw}\") names the sixtieth second of a minute, ` +\n `which no calendar date has.`,\n hint:\n 'It is recorded as the fifty-ninth second, one second earlier, since that is the ' +\n 'nearest instant a date can hold. time_s is unaffected — it counts from the start of ' +\n 'the recording either way.',\n });\n }\n\n /*\n Two dates in one header, disagreeing.\n\n EDF+ requires the recording identification field's `Startdate` to be the header's start\n date. Where it is, its four digits settle the century — see resolveStartDateTime. Where it\n is not, one of the two is wrong and there is no way to tell which, so the date field is\n used, being the one the format defines. That was done in silence, on a header that plainly\n contradicts itself:\n\n Recorded 2002-03-02 22:15:00\n Recording Startdate 05-MAR-2002 PSG-1234/2002 NN Telemetry03\n\n The same shape as a record count that disagrees with the file, or a declared header size\n that disagrees with the signal count, both of which have said so for versions.\n */\n const statedDate = recordingIdStartdate(recordingId);\n const headerDate = /^(\\d{2})[.\\-/](\\d{2})[.\\-/](\\d{2})$/u.exec(trimField(startDateRaw));\n if (statedDate !== null && headerDate !== null) {\n const day = Number(headerDate[1]);\n const month = Number(headerDate[2]);\n const yy = Number(headerDate[3]);\n if (statedDate.day !== day || statedDate.month !== month || statedDate.year % 100 !== yy) {\n diagnostics.push({\n code: 'START_DATE_MISMATCH',\n severity: 'warning',\n message:\n `The header's start date (\"${startDateRaw}\") and the date its recording ` +\n `identification states (\"${String(statedDate.day).padStart(2, '0')}-` +\n `${MONTHS[statedDate.month - 1] as string}-${statedDate.year}\") are different ` +\n `dates, which EDF+ does not permit.`,\n hint:\n 'The start date field is used, since that is the one the format defines. Which of ' +\n 'the two is right is not knowable from the file, so start_datetime_local may name ' +\n 'the wrong day.',\n });\n }\n }\n\n if (resolveStartDateTime(startDateRaw, startTimeRaw, recordingId) === null) {\n diagnostics.push({\n code: 'START_TIME_UNREADABLE',\n severity: 'warning',\n message:\n `The header's start date and time (\"${startDateRaw}\" and ` +\n `\"${startTimeRaw}\") are not a date and a time, so the recording has ` +\n `no start instant.`,\n hint:\n 'time_s is unaffected — it counts from the start of the recording either way. What ' +\n 'cannot be done is turning it into a wall-clock instant, and metadata.json records ' +\n 'start_datetime_local as null.',\n });\n }\n\n for (const [label, indices] of seenLabels) {\n if (indices.length < 2) continue;\n diagnostics.push({\n code: 'DUPLICATE_LABEL',\n severity: 'warning',\n /*\n Cut by the function that cuts every other list in a sentence here.\n\n `join` names all of them, however many there are, and a header may declare as many\n channels as it likes under one label: a 200-channel montage all labelled `T8-P8` — which\n is exactly the kind of file this warning is for, since CHB-MIT ships two of them —\n produced a single 1,100-character line of positions with the sentence that mattered at\n the front of it. `listed` shows eight and counts the rest, which is what the rate\n warning, the leftover-file warning, the channel-position lists in `--channels` and the\n `EMPTY_LABEL` message one loop up all already do. This was the last `join` of a\n file-controlled list left in a diagnostic.\n\n Two positions render identically either way, so the ordinary duplicate reads as it\n always has.\n */\n /*\n Positions written `#N`, which is how a position is written everywhere it is meant to be\n typed: `--channels \"#0\"`, \"This file has signal channels at #0, #1, #2\", and this\n warning's own namesake from channel selection — \"(positions #0, #1); all of them were\n selected\". This one said \"(positions 0, 1)\", two paragraphs above the page that tells\n the reader to \"address it by position with #N\". The number is the same; the form that\n works is not.\n */\n message:\n `${counted(indices.length, 'signal')} share the label \"${label}\" ` +\n `(positions ${listed(indices.map((i) => `#${i}`))}).`,\n /*\n A sentence about the wide layout, printed in both — the same fault 0.8.24 fixed for\n the other raiser of this code, two lines further down the same run.\n\n A long signals.csv has three columns, `time_s`, `channel` and `value`, and none of\n them comes from a label: a channel appears there as a value in the `channel` column.\n `label-suffix-collision.edf --layout long` printed both halves back to back:\n\n warning: 2 signals share the label \"T8\" (positions #0, #1).\n Their columns are suffixed with the signal number so they stay\n distinguishable.\n warning: Signal 2 is labelled \"T8_ch0\", ... so it is named \"T8_ch0_ch2\" in the\n channel column.\n Channel names are unique; look this channel up in channels.csv by its\n signal_index.\n\n The plan raises the second and knows the layout. The header raises this one and\n cannot: it is parsed before any conversion is planned, and `EdfFile.open` hands these\n diagnostics to callers who have asked for no layout at all. So the sentence names\n both, rather than guessing which one is being written.\n */\n hint:\n 'Their names are suffixed with the signal number so they stay distinguishable: a ' +\n 'column name each in the wide layout, and a distinct value in the channel column ' +\n 'under --layout long.',\n });\n }\n\n const recordBytes = byteOffsetInRecord;\n if (recordBytes <= 0) {\n throw new EdfError(\n 'NO_SAMPLES',\n 'No signal in this file carries any samples (every channel declares 0 samples per record).',\n 'The header describes the channels and nothing has data behind them, so there is no ' +\n 'record to read. A header written before recording began, or copied from another ' +\n 'file, produces this.',\n );\n }\n\n if (sawComma.value) {\n diagnostics.push({\n code: 'COMMA_DECIMAL',\n severity: 'warning',\n message: 'Some header numbers use a comma decimal separator, which the EDF spec does not allow.',\n hint: 'They were read as decimal points. Check the values in the channel table.',\n });\n }\n\n const dataBytes = fileSize - expectedHeaderBytes;\n if (dataBytes < 0) {\n // With the two figures, like the other two FILE_TOO_SMALL messages and unlike this one,\n // which named neither — the same thing the NO_DATA_RECORDS message below was fixed for:\n // \"the message carried no figures at all, so nothing in it could be checked against the\n // file\". Reached by a caller handing `parseHeader` a header and a smaller size than the\n // one it came out of, which is a mismatch worth being able to see.\n throw new EdfError(\n 'FILE_TOO_SMALL',\n `File is ${counted(fileSize, 'byte')}, which is less than the ${grouped(expectedHeaderBytes)} ` +\n `its own header occupies.`,\n 'The header and the size came from different reads: parseHeader was handed a byte ' +\n 'count smaller than the header block it was given.',\n );\n }\n const recordCount = Math.floor(dataBytes / recordBytes);\n const trailingBytes = dataBytes - recordCount * recordBytes;\n\n if (recordCount === 0) {\n /*\n Which of the two, and with the numbers.\n\n \"The recording was probably interrupted before any data was written\" is right about an\n empty file and wrong about the other way to get here: a header declaring records larger\n than the data present. A 606 KB file holding 589 KB of samples — 60% of one record, more\n than half a million readings — was told no data was written, and the message carried no\n figures at all, so nothing in it could be checked against the file. The declared record\n size is the thing to look at, and it was the one thing not said.\n\n Still an error either way. A record is the unit the format is addressed in, and there is\n nothing smaller to convert.\n */\n const empty = dataBytes === 0;\n throw new EdfError(\n 'NO_DATA_RECORDS',\n empty\n ? 'The file contains a header and no data at all.'\n : `The file contains ${counted(dataBytes, 'byte')} of data, which is less than the ` +\n `${grouped(recordBytes)} its header says one data record takes.`,\n empty\n ? 'The recording was probably interrupted before any data was written.'\n : 'Either the recording was cut short part way through its first record, or the ' +\n 'header describes records larger than the ones actually written. Check the ' +\n 'samples-per-record fields against the file size.',\n );\n }\n\n if (declaredRecordCount === -1) {\n diagnostics.push({\n code: 'RECORD_COUNT_UNKNOWN',\n severity: 'warning',\n message:\n `The header does not say how many data records the file has (-1), which the spec allows ` +\n `for recordings still in progress. Using the ${counted(recordCount, 'record')} the file actually contains.`,\n hint:\n 'That count comes from the size of the file, so a recording still being written ' +\n 'converts as much of it as was on disk when this run read it.',\n });\n } else if (declaredRecordCount !== recordCount) {\n diagnostics.push({\n code: 'RECORD_COUNT_MISMATCH',\n severity: 'warning',\n // Both counts through `counted`, not only the second. A header declaring one record\n // over a file holding three read \"declares 1 data records\" — the slip the sentence\n // beside it has been holding to since it was written.\n message:\n `The header declares ${counted(declaredRecordCount, 'data record')} but the file ` +\n `contains ${grouped(recordCount)}. Converting the ${counted(recordCount, 'record')} that ${recordCount === 1 ? 'is' : 'are'} present.`,\n hint:\n declaredRecordCount > recordCount\n ? 'The recording looks truncated. It may have been cut short or copied incompletely.'\n : 'The file is longer than its header claims.',\n });\n }\n\n if (trailingBytes > 0) {\n diagnostics.push({\n code: 'TRAILING_BYTES',\n severity: 'warning',\n message: `${counted(trailingBytes, 'byte')} after the last complete data record ${trailingBytes === 1 ? 'was' : 'were'} ignored.`,\n hint:\n 'A record is the unit this format is addressed in, and a partial one says nothing ' +\n 'about which samples it holds or when they were taken. Every complete record was ' +\n 'converted.',\n });\n }\n\n const isEdfPlus = continuity !== null;\n if (continuity === 'EDF+D') {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `This is a discontinuous (${isBdf ? 'BDF+D' : 'EDF+D'}) recording: its data records are ` +\n `not contiguous in time.`,\n hint: 'Each row carries its true recording time, so gaps stay visible instead of being closed.',\n });\n }\n\n const dataSignals = signals.filter((s) => !s.isAnnotations);\n if (dataSignals.length === 0) {\n diagnostics.push({\n code: 'NO_SIGNAL_CHANNELS',\n severity: 'warning',\n /*\n The marker as the file spells it, which is what the DISCONTINUOUS warning ten lines up\n already does and what `describeFormat` was fixed for.\n\n A BDF+ recording holding nothing but its `BDF Annotations` channel — a label this\n parser recognises by name, beside `EDF Annotations`, and has since annotations were\n read at all — was told it \"contains only EDF+ annotations\", naming a channel it does\n not carry and a format it is not. `--info` two lines above says `BDF+ (continuous)`\n and the channel table names the channel; this warning was the one line disagreeing\n with both.\n */\n message: `This file has no signal channels; it contains only ${isBdf ? 'BDF+' : 'EDF+'} annotations.`,\n /*\n What the file is, not what the run will write. The warning underneath it — raised by\n the conversion rather than by the parser — already answers the second question, in\n those words: \"annotations.csv holds whatever events it carries. channels.csv lists\n signal channels, so it has none to list.\" Two consecutive hints saying that would be\n worse than the one that was missing.\n */\n hint:\n `An ${isBdf ? 'BDF+' : 'EDF+'} file of events and no signal is an ordinary thing: a ` +\n 'scoring file distributed beside the recording it annotates has exactly this shape.',\n });\n }\n\n // A channel declaring zero samples per record has no sampling rate to speak of — it is\n // reported separately as NO_SAMPLES and no file is written for it. Counting its nominal\n // 0 Hz as a rate made a single-rate recording warn that it used \"2 different sampling\n // rates (4 Hz, 0 Hz)\" and claim it was splitting output it never split.\n const rates = new Set(dataSignals.filter((s) => s.samplesPerRecord > 0).map((s) => s.samplingRate));\n if (rates.size > 1) {\n diagnostics.push({\n code: 'MIXED_SAMPLING_RATES',\n severity: 'warning',\n message:\n `Channels use ${counted(rates.size, 'different sampling rate')} ` +\n `(${listed(formatRates([...rates].sort((a, b) => b - a)).map((r) => `${r} Hz`))}).`,\n hint: 'They are written to one file per rate so no channel is resampled.',\n });\n }\n\n return {\n header: {\n version,\n patientId,\n recordingId,\n startDateRaw,\n startTimeRaw,\n startDateTime: resolveStartDateTime(startDateRaw, startTimeRaw, recordingId),\n headerBytes: expectedHeaderBytes,\n declaredHeaderBytes: headerBytes,\n reserved,\n isEdfPlus,\n isBdf,\n continuity,\n declaredRecordCount,\n recordDuration,\n signalCount,\n signals,\n bytesPerSample,\n recordBytes,\n },\n recordCount,\n trailingBytes,\n diagnostics,\n };\n}\n\n/**\n * The recording start as a zone-less wall clock, \"YYYY-MM-DDTHH:MM:SS\".\n *\n * EDF stores the start time as local wall-clock digits with no timezone anywhere in\n * the format. `startDateTime` is built with Date.UTC purely so those digits survive a\n * round trip unshifted, which makes it a carrier for the wall clock rather than a\n * real instant. Serialising it with `toISOString()` would append a Z and assert UTC,\n * and any reader converting to local time would then shift the recording by their own\n * offset: 13:43:04 in the file becomes 08:43:04 in New York. The Z is omitted because\n * the file genuinely does not say which zone it meant.\n */\nexport function formatWallClock(date: Date | null): string | null {\n if (!date) return null;\n if (!(date instanceof Date)) {\n throw new OptionError(`date must be a Date or null, got ${describeValue(date)}.`);\n }\n /*\n And an unparseable one, which is a date this cannot state rather than a bad argument.\n\n `new Date('the third').toISOString()` is a `RangeError: Invalid time value` out of the\n middle of this function. `null` is what this returns for a recording with no start\n instant, and that is what an invalid Date is: `START_TIME_UNREADABLE` says the same thing\n about the same fact, and metadata.json writes `start_datetime_local: null` for it.\n */\n if (Number.isNaN(date.getTime())) return null;\n return date.toISOString().slice(0, 19);\n}\n\n/**\n * The recording's format, as `--info`, `metadata.json` and `--json` all name it.\n *\n * `\"EDF\"`, `\"BDF\"`, or one of `\"EDF+ (continuous)\"`, `\"EDF+ (discontinuous)\"`,\n * `\"BDF+ (continuous)\"`, `\"BDF+ (discontinuous)\"`. A BDF+ file reports its own spelling even\n * though `continuity` normalises the marker to the `EDF+` form.\n *\n * This said `EDF+ (EDF+D)` and `BDF+ (EDF+C)`, which are not strings it can return — the\n * parenthetical is the word, not the marker. It is a one-line doc comment on a public export,\n * so it is what a TypeScript consumer's editor shows and what `dist/edf/header.d.ts` ships,\n * and the value it describes is `recording.format` in every metadata.json this tool writes.\n * A consumer branching on the tooltip's spelling never matches. Every documentation page had\n * it right; this was the only place that did not.\n */\nexport function describeFormat(header: EdfHeader): string {\n /*\n The argument, which this read two booleans off and asked no questions about.\n\n Both reads are truthiness tests, so anything without them answers `'EDF'` — including the\n `EdfFile` whose `.header` this wants, which is one property away and is how the api page\n writes every other call. `describeFormat(file)` on a discontinuous BDF+ recording returned\n `'EDF'`: not an error, not a fallback, a confident wrong answer about the file in hand.\n\n The same check `makeScaler` got in 0.8.61, for the same reason — an exported function that\n reads fields off an object cannot tell a header from anything else that lacks them.\n */\n if (\n typeof header !== 'object' ||\n header === null ||\n typeof header.isBdf !== 'boolean' ||\n typeof header.isEdfPlus !== 'boolean'\n ) {\n throw new OptionError(`header must be a parsed EDF header, got ${describeValue(header)}.`);\n }\n const base = header.isBdf ? 'BDF' : 'EDF';\n if (!header.isEdfPlus) return base;\n return `${base}+ (${header.continuity === 'EDF+D' ? 'discontinuous' : 'continuous'})`;\n}\n\n/** Render a sampling rate without trailing noise: 256, 0.5, 12.5. */\nexport function formatRate(hz: number): string {\n // A number, like the neighbours. `formatRate(NaN)` came back as the string \"NaN\", and\n // `rateSlug` turned it into \"NaNhz\" — a file name this tool cannot write, handed to a\n // caller the doc comment below describes as \"reaching for the exported slug function to\n // predict a filename\". A rate that is not a number is not a rate.\n if (typeof hz !== 'number' || Number.isNaN(hz)) {\n throw new OptionError(`hz must be a sampling rate in hertz, got ${describeValue(hz)}.`);\n }\n if (Number.isInteger(hz)) {\n /*\n The same six decimals, in the notation `toFixed` cannot reach.\n\n Every double past 2^53 is an integer, so this branch takes every large rate — and\n `String` switches to exponent form at 1e21 and carries the full seventeen digits with\n it. Four samples in a record of 1e-300s is 3.9999999999999996e+300, which is exactly the\n float noise the rounding below exists to remove, printed in a `RATE` column of otherwise\n plain numbers and pasted into an output filename by `rateSlug`. Below 1e21 `String` is\n already exact and is left alone, so an integer rate a file can really state — 2^53\n samples in a second — is not rounded to something it is not.\n */\n const text = String(hz);\n return text.includes('e') ? String(Number(hz.toExponential(6))) : text;\n }\n const rounded = Number(hz.toFixed(6));\n // A rate below 5e-7 rounds away to \"0\", which reads as \"this channel has no sampling\n // rate\" and made the mixed-rate warning contradict itself: it announced two different\n // rates and then printed both as \"0 Hz\". Exponent form keeps a real rate legible, and\n // keeps distinct rates distinct in the channel table and in output filenames.\n if (rounded === 0) return hz.toExponential(3);\n return String(rounded);\n}\n\n/**\n * Renders a group of rates so that rates which differ read as differing.\n *\n * `formatRate` rounds to six decimals, which is what keeps an ordinary rate free of\n * float noise — 30 samples in a 0.1-second record is 299.99999999999994 as a double,\n * and belongs on screen as 300. Two rates separated by less than that round to one\n * string, so a file carrying 1e-6 Hz and 1.25e-6 Hz warned that it used \"2 different\n * sampling rates (0.000001 Hz, 0.000001 Hz)\" and named both files the same thing.\n *\n * That is the contradiction the exponent fallback above already removes for rates that\n * round away to zero; this is the same one a step further out. On a collision every rate\n * in the group switches to its shortest exact form, which is unique for distinct values,\n * rather than only the pair that collided — one column in one notation reads better than\n * two.\n */\nexport function formatRates(rates: readonly number[]): string[] {\n // `rates.map` on anything else is a TypeError naming a local of this package; the members\n // are `formatRate`'s business, one line down.\n if (!Array.isArray(rates)) {\n throw new OptionError(`rates must be a list of sampling rates, got ${describeValue(rates)}.`);\n }\n const rounded = rates.map(formatRate);\n const distinct = new Set(rates).size;\n return new Set(rounded).size === distinct ? rounded : rates.map((hz) => String(hz));\n}\n"]}
1
+ {"version":3,"file":"header.js","sourceRoot":"","sources":["../../src/edf/header.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * EDF / EDF+ header parsing.\n *\n * Layout (all fields are ASCII, left-justified, space-padded):\n *\n * fixed header, 256 bytes\n * 0 8 version ('0', or 255 + 'BIOSEMI' for BDF)\n * 8 80 patient identification\n * 88 80 recording identification\n * 168 8 start date dd.mm.yy\n * 176 8 start time hh.mm.ss\n * 184 8 number of bytes in the header record\n * 192 44 reserved ('EDF+C' / 'EDF+D' live here)\n * 236 8 number of data records (-1 if unknown)\n * 244 8 duration of a data record, in seconds (may be fractional)\n * 252 4 number of signals (ns)\n *\n * signal header, ns * 256 bytes, stored FIELD-major rather than signal-major:\n * all ns labels, then all ns transducer types, and so on.\n * ns * 16 label\n * ns * 80 transducer type\n * ns * 8 physical dimension\n * ns * 8 physical minimum\n * ns * 8 physical maximum\n * ns * 8 digital minimum\n * ns * 8 digital maximum\n * ns * 80 prefiltering\n * ns * 8 number of samples in each data record\n * ns * 32 reserved\n */\n\nimport { EdfError } from './errors.js';\nimport type { Diagnostic } from './errors.js';\nimport { counted, grouped, listed } from '../format/list.js';\nimport { plain } from '../format/number.js';\n// The shell-quoting rule for a label, shared rather than repeated: this hint and\n// `--channels`' own \"did you mean\" have to print the same command for the same label.\nimport { typeable } from '../convert/channels.js';\nimport { OptionError, describeValue } from '../convert/options.js';\nimport { decodeLatin1 } from './bytes.js';\n\n/** Label the EDF+ spec reserves for the annotations channel. */\nexport const ANNOTATIONS_LABEL = 'EDF Annotations';\n/** BDF+ uses its own spelling for the same channel. */\nexport const BDF_ANNOTATIONS_LABEL = 'BDF Annotations';\n\nexport const FIXED_HEADER_BYTES = 256;\nexport const SIGNAL_HEADER_BYTES = 256;\n\nexport interface EdfSignal {\n /** Position in the file, 0-based. Stable even when labels collide. */\n index: number;\n label: string;\n transducer: string;\n physicalDimension: string;\n physicalMin: number;\n physicalMax: number;\n digitalMin: number;\n digitalMax: number;\n prefiltering: string;\n samplesPerRecord: number;\n reserved: string;\n /** True for the EDF+ 'EDF Annotations' channel, which carries text, not signal. */\n isAnnotations: boolean;\n /** samplesPerRecord / recordDuration, in Hz. */\n samplingRate: number;\n /** Byte offset of this signal's samples within one data record. */\n byteOffsetInRecord: number;\n}\n\nexport interface EdfHeader {\n version: string;\n patientId: string;\n recordingId: string;\n /** Raw 'dd.mm.yy' as written in the file. */\n startDateRaw: string;\n /** Raw 'hh.mm.ss' as written in the file. */\n startTimeRaw: string;\n /** Resolved start instant, or null when the file's date/time fields are unusable. */\n startDateTime: Date | null;\n /**\n * Header size in bytes, computed from the signal count rather than read from the field.\n *\n * Every data record offset is derived from this, so it has to be the one the layout\n * actually uses: 256 for the fixed header plus 256 per signal. A writer that fills the\n * field in carelessly is common enough to have its own warning, HEADER_BYTES_MISMATCH,\n * and trusting the field over the arithmetic would put every sample at the wrong offset.\n */\n headerBytes: number;\n /**\n * What the header's own length field says, which need not be the above.\n *\n * Exposed for the same reason `declaredRecordCount` is: the two disagreeing is a fact\n * about the file, and a caller checking how a recording was written should be able to see\n * what it claimed rather than only what was believed.\n */\n declaredHeaderBytes: number;\n reserved: string;\n isEdfPlus: boolean;\n /** True for BioSemi BDF/BDF+ files, whose samples are 3 bytes rather than 2. */\n isBdf: boolean;\n /** 'EDF+C' continuous, 'EDF+D' discontinuous, or null for plain EDF. */\n continuity: 'EDF+C' | 'EDF+D' | null;\n /** As declared in the header. -1 means \"unknown\", which the spec permits. */\n declaredRecordCount: number;\n recordDuration: number;\n signalCount: number;\n signals: EdfSignal[];\n bytesPerSample: number;\n recordBytes: number;\n}\n\n/** Everything derived by combining the header with the file's real size. */\nexport interface EdfHeaderInfo {\n header: EdfHeader;\n /** Record count implied by the actual file size — the one we trust for reading. */\n recordCount: number;\n /** Bytes after the last complete data record. */\n trailingBytes: number;\n diagnostics: Diagnostic[];\n}\n\n/**\n * Whether a spreadsheet reads this field as the start of a formula rather than as text.\n *\n * `=` and `@` unconditionally; every list of these characters names two more, and this had\n * `-` as an exception with the reason written out on the warnings page: \"a lone `-` is a\n * real convention for no unit ... and neither is evaluated unless what follows it parses as a\n * formula\". Which is the condition, and it was not being applied — nothing with a leading\n * minus was flagged at all. A channel labelled `-2+3` opens as a column headed `1`, and\n * `-HYPERLINK(\"http://...\",\"EEG\")` is a name the spreadsheet resolves, in silence.\n *\n * So the exception is what it says it is rather than the whole character. A lone `-` is left\n * as text by every spreadsheet and is not flagged; a field that is entirely a number reads as\n * that number, which is what the header says, and is not flagged either. Anything else after\n * the minus is arithmetic or a name.\n *\n * And `+` takes the same exception, which it did not. It is the same rule in the spreadsheet —\n * Lotus compatibility, which converts a leading `+` or `-` to a formula when what follows one\n * parses as a formula and leaves it as text when it does not — so the two signs cannot differ\n * here for a reason that comes from the sign. A channel labelled `+100` was warned about as\n * something a spreadsheet \"reads as the start of a formula rather than as text\", over a cell\n * that opens as 100, which is what the header says; `-100` beside it said nothing, and under\n * `--strict` the difference was an exit code. `+1+1` is still arithmetic and still flagged.\n */\nexport function startsFormula(text: string): boolean {\n if (/^[=@]/u.test(text)) return true;\n if (!/^[+-]./u.test(text)) return false;\n // `\\.\\d*` and not `\\.\\d+`, which is the spelling DECIMAL_FIELD and DECIMAL_DURATION both\n // use for the same question. A trailing point is a number with nothing after it — Excel\n // opens `+1.` as 1 — and one character of difference between three copies of one grammar\n // had this warning firing on a field the paragraph above exempts by name, and failing\n // --strict for it.\n return !/^[+-](?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$/u.test(text);\n}\n\n/**\n * A byte the terminal treats as an instruction rather than as text.\n *\n * C0 and C1, plus DEL. Tab is included deliberately: it is harmless to a terminal but it\n * makes a CSV column name that cannot be typed or matched reliably, which is the other half\n * of what this warning is for.\n */\nfunction isControlCharacter(character: string): boolean {\n const code = character.codePointAt(0) as number;\n return code <= 0x1f || (code >= 0x7f && code <= 0x9f);\n}\n\nconst dec = (buf: Uint8Array, start: number, len: number): string =>\n decodeLatin1(buf, start, start + len);\n\n/** EDF fields are space-padded; trailing NULs also occur in files written by sloppy tools. */\nconst trimField = (s: string): string => s.replace(/[\\0\\s]+$/u, '').replace(/^\\s+/u, '');\n\n/**\n * How many signals the fixed header says there are, read exactly as `parseHeader` will.\n *\n * `EdfFile.open` needs this before it can know how much header to read, and it used to work\n * it out with its own `Number(...)` — which was NUL-tolerant but not comma-tolerant, unlike\n * every other numeric field here. A header written with a comma decimal separator, which\n * COMMA_DECIMAL exists to accept and which the documentation lists this field among, was\n * therefore never given its signal headers at all, and the file died on a message that\n * contradicted itself: \"needs a 768-byte header, but the file is only 848 bytes\".\n *\n * Sharing the parse is what keeps the two from disagreeing again about which files are\n * readable. Null means \"not a usable count\", and the caller reads no further header — the\n * real error then comes from `parseHeader`, which is the one place that decides.\n */\nexport function peekSignalCount(fixed: Uint8Array): number | null {\n const text = normaliseNumberField(dec(fixed, 252, 4)).text;\n if (!DECIMAL_FIELD.test(text)) return null;\n const count = Number(text);\n return Number.isInteger(count) && count > 0 ? count : null;\n}\n\n/**\n * What EDF allows a numeric field to look like, which is less than `Number()` allows.\n *\n * A sign, digits, an optional fractional part, an optional exponent — the spec's own grammar,\n * plus the exponent form the 8-character physical bounds need to reach a magnetometer's range\n * and which real headers use.\n *\n * `Number()` accepts a great deal more, and every one of those forms is a header this tool\n * would have read as a number nobody wrote. A physical maximum of `0x64` came out as 100: it\n * printed as `-100 to 100` in the channel table, went into channels.csv as `physical_max,100`,\n * and set the gain every sample on that channel was scaled by — a whole calibration invented\n * from four bytes that are not a decimal number, exit 0, no diagnostic. `0b1100100` and `0o144`\n * are the same hundred, and `0x02` in the signal-count field is a two-channel recording.\n *\n * The same mistake as `#0x2` reaching channel 2 through `--channels`, `--decimals 0o5` writing\n * five places and `--jobs 0x10` running sixteen, all of which have their own comments and their\n * own fixes. Those were values somebody typed. These are the fields every number in the output\n * is computed from, and the page describing them says \"all fields are ASCII\" and gives the\n * layout digit by digit.\n */\nconst DECIMAL_FIELD = /^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$/u;\n\n/** A numeric header field, trimmed and with a comma decimal separator turned into a dot. */\nfunction normaliseNumberField(raw: string): { text: string; sawComma: boolean } {\n const text = trimField(raw);\n // Some writers emit a comma decimal separator despite the spec requiring '.'.\n if (text.includes(',') && !text.includes('.')) {\n return { text: text.replace(',', '.'), sawComma: true };\n }\n return { text, sawComma: false };\n}\n\n/**\n * What a header field that is not the number it must be usually means.\n *\n * `BAD_HEADER_FIELD` is raised four ways — an empty numeric field, one that does not parse, one\n * that must be whole and is not, and a signal declaring a negative sample count — and one of\n * the four carried this. The other three printed a bare sentence about a field and stopped,\n * which is the same gap `EdfFile.#UNREADABLE_HINT` was written to close on the read side: a\n * family of errors where one member says what the cause usually is and the rest do not.\n *\n * An empty field is if anything the plainer symptom of the two. A truncated download stops\n * mid-header and leaves the rest as spaces; a byte-shifted file reads a field off the wrong\n * offset and finds whitespace as often as it finds digits. The page has said all four mean\n * this since they were documented together.\n */\nconst BAD_FIELD_HINT = 'The file may be truncated, byte-shifted, or not an EDF file at all.';\n\nfunction parseNumberField(\n raw: string,\n field: string,\n { integer = false, sawComma }: { integer?: boolean; sawComma?: { value: boolean } } = {},\n): number {\n const normalised = normaliseNumberField(raw);\n const text = normalised.text;\n if (normalised.sawComma && sawComma) sawComma.value = true;\n if (text === '') {\n throw new EdfError('BAD_HEADER_FIELD', `Header field \"${field}\" is empty.`, BAD_FIELD_HINT);\n }\n const n = DECIMAL_FIELD.test(text) ? Number(text) : NaN;\n if (!Number.isFinite(n)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Header field \"${field}\" is not a number (found ${JSON.stringify(text)}).`,\n BAD_FIELD_HINT,\n );\n }\n if (integer && !Number.isInteger(n)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Header field \"${field}\" must be a whole number (found ${JSON.stringify(text)}).`,\n BAD_FIELD_HINT,\n );\n }\n return n;\n}\n\n/** Whether the start time names the sixtieth second. See LEAP_SECOND_START. */\nfunction namesLeapSecond(timeRaw: string): boolean {\n const t = /^(\\d{2})[.:\\-](\\d{2})[.:\\-](\\d{2})$/u.exec(trimField(timeRaw));\n return t !== null && Number(t[3]) === 60;\n}\n\nconst MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'];\n\n/**\n * The date an EDF+ recording ID states, which is the only place the file writes a full year.\n *\n * EDF+ requires the recording identification field to begin `Startdate dd-MMM-yyyy`, and\n * requires it to agree with the header's own date field. It is a four-digit year, so it says\n * something the eight-character date field physically cannot.\n */\nfunction recordingIdStartdate(\n recordingId: string,\n): { day: number; month: number; year: number } | null {\n const stated = /^Startdate\\s+(\\d{2})-([A-Za-z]{3})-(\\d{4})(?:\\s|$)/u.exec(recordingId.trim());\n if (!stated) return null;\n const month = MONTHS.indexOf((stated[2] as string).toUpperCase()) + 1;\n if (month === 0) return null;\n return { day: Number(stated[1]), month, year: Number(stated[3]) };\n}\n\n/**\n * EDF stores a two-digit year. The spec pins the century: 85-99 mean 1985-1999\n * and 00-84 mean 2000-2084. Files outside 1985-2084 cannot express their date.\n *\n * Which is why EDF+ writes it again in full, in the recording identification field, and why\n * that is used here when it is there. The rule alone reports a recording made in 1984 as 2084\n * and one made in 2085 as 1985 — a hundred years out, on a file that states the year plainly\n * four fields earlier. Taken only where the two agree about everything the header can express:\n * the same day, the same month, and a four-digit year ending in the two digits the header\n * wrote. A recording ID that contradicts the header is a different problem and is left to the\n * spec's rule, which is at least the one the format defines.\n */\nfunction resolveStartDateTime(\n dateRaw: string,\n timeRaw: string,\n recordingId = '',\n): Date | null {\n const d = /^(\\d{2})[.\\-/](\\d{2})[.\\-/](\\d{2})$/u.exec(trimField(dateRaw));\n const t = /^(\\d{2})[.:\\-](\\d{2})[.:\\-](\\d{2})$/u.exec(trimField(timeRaw));\n if (!d || !t) return null;\n\n const dd = Number(d[1]);\n const mm = Number(d[2]);\n const yy = Number(d[3]);\n const hh = Number(t[1]);\n const mi = Number(t[2]);\n const ss = Number(t[3]);\n\n if (mm < 1 || mm > 12 || dd < 1 || dd > 31 || hh > 23 || mi > 59 || ss > 60) return null;\n\n const stated = recordingIdStartdate(recordingId);\n const year =\n stated !== null && stated.day === dd && stated.month === mm && stated.year % 100 === yy\n ? stated.year\n : yy >= 85\n ? 1900 + yy\n : 2000 + yy;\n const date = new Date(Date.UTC(year, mm - 1, dd, hh, mi, Math.min(ss, 59)));\n // Reject dates that rolled over, e.g. 31.02.\n if (date.getUTCMonth() !== mm - 1 || date.getUTCDate() !== dd) return null;\n return date;\n}\n\n/**\n * Parse the fixed 256-byte header plus the per-signal header block.\n *\n * @param buf At least FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES bytes.\n * @param fileSize Total size of the file on disk, used to derive the real record count.\n */\nexport function parseHeader(buf: Uint8Array, fileSize: number): EdfHeaderInfo {\n /*\n The first argument, checked before anything is read out of it.\n\n The api page offers this function on its own, so it is reached by a caller holding bytes\n they read themselves — and it took whatever it was handed. A number or a plain object came\n back as Node's own `TypeError: bytes.subarray is not a function`, which names a method the\n caller never called; `undefined` as a property read on a local.\n\n Worse, a string got an answer *about the recording*:\n\n parseHeader('0 ...', 4_000_000)\n EdfError FILE_TOO_SMALL: An EDF header alone needs 256 bytes, but only 11 of this\n 4,000,000-byte file reached the parser.\n\n A string has a `length`, so it reached the short-header branch below and was reported as a\n truncated file — the code a script branches on to quarantine a bad recording, raised over a\n recording nobody had read. That is the distinction `readRecords` draws in as many words:\n \"an `OptionError`, because it is the call that is wrong and not the recording\". The same\n check `decodeRecordAnnotations` makes of its own byte argument, for the reason given there.\n */\n if (!ArrayBuffer.isView(buf)) {\n throw new OptionError(\n `buf must be the header block as bytes, got ${describeValue(buf)}. parseHeader takes ` +\n `the bytes read from the file and the size of the file they came out of.`,\n );\n }\n\n /*\n The second argument, checked before anything is derived from it.\n\n Every count this function reports about the data comes out of `fileSize`, and nothing\n asked what it was. Omit it — which a JavaScript caller does by writing the call the way\n the one-argument functions beside it are written — and the arithmetic runs on `undefined`:\n\n parseHeader(bytes).recordCount // NaN\n parseHeader(bytes).diagnostics[0].message\n // \"The header declares 2 data records but the file contains NaN. Converting the NaN\n // records that are present.\"\n\n A header object claiming NaN records, a warning that says NaN out loud, and no error —\n and `recordCount * recordDuration`, which is how the duration is worked out everywhere\n this value goes, is NaN from there on. `null` was worse from 0.8.5, when the counts in\n these messages started being grouped: grouping it is a TypeError raised from inside the\n number formatter, naming nothing the caller wrote.\n\n The same `OptionError` `assertInputPath` raises for the first argument of `EdfFile.open`,\n for the same reason given there: it is the call that is wrong, not the recording.\n */\n if (typeof fileSize !== 'number' || !Number.isFinite(fileSize) || fileSize < 0) {\n throw new OptionError(\n `fileSize must be the number of bytes in the file, got ${describeValue(fileSize)}.`,\n );\n }\n\n const diagnostics: Diagnostic[] = [];\n const sawComma = { value: false };\n\n if (buf.length < FIXED_HEADER_BYTES) {\n /*\n Which of the two is actually short — the same question the FILE_TOO_SMALL fifty lines\n down was fixed for, in the words its comment still uses: \"The file size was quoted\n either way, so a caller that had read too little ... produced arithmetic that refuted\n itself.\" This is the raising that comment describes, and it went on doing it:\n\n parseHeader(new Uint8Array(100), 5000)\n File is 5,000 bytes; an EDF header alone needs at least 256.\n\n 5,000 is not less than 256. A reader following that sentence looks for a truncation\n that is not there, and the number they are given to check against the file is the one\n that is fine. Reached from the library, where the caller holds both — `EdfFile.open`\n reads 256 bytes before it reads anything else, so the two agree by construction there.\n */\n const short = fileSize < FIXED_HEADER_BYTES;\n throw new EdfError(\n 'FILE_TOO_SMALL',\n short\n ? `File is ${counted(fileSize, 'byte')}; an EDF header alone needs at least ${FIXED_HEADER_BYTES}.`\n : `An EDF header alone needs ${FIXED_HEADER_BYTES} bytes, but only ` +\n `${grouped(buf.length)} of this ${grouped(fileSize)}-byte file reached the parser.`,\n short\n ? 'A file this small is truncated, or is not an EDF or BDF recording at all: every one of ' +\n 'them opens with a fixed 256-byte header.'\n : 'parseHeader takes the header block and the size of the file it came out of. This ' +\n 'call was given fewer bytes than the header it was asked to read.',\n );\n }\n\n // BDF (BioSemi) marks itself with byte 255 followed by 'BIOSEMI', and stores\n // 3-byte samples instead of 2. Everything else about the layout is identical.\n const isBdf = buf[0] === 0xff && dec(buf, 1, 7) === 'BIOSEMI';\n const version = isBdf ? 'BIOSEMI' : trimField(dec(buf, 0, 8));\n\n const patientId = trimField(dec(buf, 8, 80));\n const recordingId = trimField(dec(buf, 88, 80));\n const startDateRaw = trimField(dec(buf, 168, 8));\n const startTimeRaw = trimField(dec(buf, 176, 8));\n const headerBytes = parseNumberField(dec(buf, 184, 8), 'number of header bytes', {\n integer: true,\n sawComma,\n });\n const reserved = trimField(dec(buf, 192, 44));\n const declaredRecordCount = parseNumberField(dec(buf, 236, 8), 'number of data records', {\n integer: true,\n sawComma,\n });\n const recordDuration = parseNumberField(dec(buf, 244, 8), 'duration of a data record', {\n sawComma,\n });\n const signalCount = parseNumberField(dec(buf, 252, 4), 'number of signals', {\n integer: true,\n sawComma,\n });\n\n if (signalCount <= 0) {\n throw new EdfError(\n 'INVALID_SIGNAL_COUNT',\n `Header declares ${counted(signalCount, 'signal')}; expected at least 1.`,\n BAD_FIELD_HINT,\n );\n }\n if (!(recordDuration > 0)) {\n throw new EdfError(\n 'INVALID_RECORD_DURATION',\n `Header declares a data record duration of ${plain(recordDuration)}s; expected a positive number.`,\n BAD_FIELD_HINT,\n );\n }\n\n const expectedHeaderBytes = FIXED_HEADER_BYTES + signalCount * SIGNAL_HEADER_BYTES;\n if (buf.length < expectedHeaderBytes) {\n throw new EdfError(\n 'FILE_TOO_SMALL',\n /*\n Which of the two is actually short.\n\n The file size was quoted either way, so a caller that had read too little — the\n signal count parsed one way here and another way there — produced arithmetic that\n refuted itself: \"needs a 768-byte header, but the file is only 848 bytes\". A reader\n following that looks for a truncation that is not there.\n */\n `File declares ${counted(signalCount, 'signal')}, which needs a ${grouped(expectedHeaderBytes)}-byte header, ` +\n (fileSize < expectedHeaderBytes\n ? `but the file is only ${grouped(fileSize)} bytes.`\n : `but only ${grouped(buf.length)} bytes of it were handed to the parser.`),\n 'Either the recording is truncated inside its own header, or the signal count was ' +\n 'read from bytes that are not it. Both leave the channel descriptions unfinished.',\n );\n }\n if (headerBytes !== expectedHeaderBytes) {\n diagnostics.push({\n code: 'HEADER_BYTES_MISMATCH',\n severity: 'warning',\n message:\n `Header says it is ${grouped(headerBytes)} bytes, but ${counted(signalCount, 'signal')} ` +\n `${signalCount === 1 ? 'requires' : 'require'} ${grouped(expectedHeaderBytes)} bytes. ` +\n `Using the value computed from the signal count.`,\n hint:\n 'Where the data starts is worked out from the signal count, not from this field, so ' +\n 'nothing is read from the wrong offset. A writer that got this one wrong may have ' +\n 'got others wrong too.',\n });\n }\n\n // Signal headers are field-major: all labels, then all transducers, and so on.\n const base = FIXED_HEADER_BYTES;\n const readField = (offsetUnits: number, width: number, i: number): string =>\n dec(buf, base + offsetUnits * signalCount + i * width, width);\n\n // EDF+ writes 'EDF+C'/'EDF+D' here; BDF+ writes 'BDF+C'/'BDF+D'. The two mean the\n // same thing, so both are normalised to a single continuity marker.\n const continuityTag = /^(?:EDF|BDF)\\+([CD])/u.exec(reserved);\n const continuity: 'EDF+C' | 'EDF+D' | null =\n continuityTag === null ? null : continuityTag[1] === 'D' ? 'EDF+D' : 'EDF+C';\n\n const signals: EdfSignal[] = [];\n let byteOffsetInRecord = 0;\n const bytesPerSample = isBdf ? 3 : 2;\n const seenLabels = new Map<string, number[]>();\n const emptyLabels: number[] = [];\n\n for (let i = 0; i < signalCount; i++) {\n const label = trimField(readField(0, 16, i));\n const transducer = trimField(readField(16, 80, i));\n const physicalDimension = trimField(readField(96, 8, i));\n const physicalMin = parseNumberField(readField(104, 8, i), `physical minimum (signal ${i})`, {\n sawComma,\n });\n const physicalMax = parseNumberField(readField(112, 8, i), `physical maximum (signal ${i})`, {\n sawComma,\n });\n const digitalMin = parseNumberField(readField(120, 8, i), `digital minimum (signal ${i})`, {\n integer: true,\n sawComma,\n });\n const digitalMax = parseNumberField(readField(128, 8, i), `digital maximum (signal ${i})`, {\n integer: true,\n sawComma,\n });\n const prefiltering = trimField(readField(136, 80, i));\n const samplesPerRecord = parseNumberField(\n readField(216, 8, i),\n `samples per record (signal ${i})`,\n { integer: true, sawComma },\n );\n const sigReserved = trimField(readField(224, 32, i));\n\n if (samplesPerRecord < 0) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Signal ${i} (\"${label}\") declares ${samplesPerRecord} samples per record.`,\n BAD_FIELD_HINT,\n );\n }\n\n const isAnnotations = label === ANNOTATIONS_LABEL || label === BDF_ANNOTATIONS_LABEL;\n\n signals.push({\n index: i,\n label,\n transducer,\n physicalDimension,\n physicalMin,\n physicalMax,\n digitalMin,\n digitalMax,\n prefiltering,\n samplesPerRecord,\n reserved: sigReserved,\n isAnnotations,\n samplingRate: samplesPerRecord / recordDuration,\n byteOffsetInRecord,\n });\n byteOffsetInRecord += samplesPerRecord * bytesPerSample;\n\n if (!isAnnotations) {\n /*\n A label is free text out of the file, and it becomes a column name in signals.csv.\n\n `--info` has escaped control bytes since it was written, because an ANSI escape in a\n header can drive the reader's terminal — `\\x1b[2J` clears the screen. The CSV had no\n such protection and needed none for correctness: quoting makes any byte safe for a\n parser, and this still passes the label through exactly as the file gives it, because\n losing what the header says is not an improvement.\n\n What was missing is the sentence saying so. A recording whose channel is labelled\n `\\x1b[2Jgone` converted with no warning at all, and `cat signals.csv` then cleared\n the terminal — while a script referencing that column by name carried an invisible\n control character in it. NONPRINTABLE_LABEL has been declared and documented as\n reserved since 0.1; this is it doing its job.\n */\n /*\n Which of the two fields carries them, because the consequences are not the same.\n\n The message said \"label or unit\", and then said the bytes \"will appear in the CSV\n column name\" and that \"the name cannot be typed\" — both of which are about the label.\n A channel labelled plainly `ECG` in a unit of `u\\x07V` got all of it: its column is\n `ECG`, `--channels ECG` selects it and exits 0, and the byte is in channels.csv's\n `unit` cell, which the warning never mentioned. Three sentences, none of them true of\n the file that raised it, on a warning whose whole purpose is to say where an invisible\n byte went.\n */\n /*\n All four free-text fields, not the two that were checked.\n\n `transducer` and `prefiltering` are free text out of the header exactly as the label\n and the unit are, and they land in channels.csv exactly as the unit does — so an ESC\n byte in a transducer field reached the CSV raw with nothing said, and `cat\n channels.csv` would drive the terminal. That is the hazard this warning exists for,\n two columns over. 0.5.71 made it name which field carries them; this is the rest of\n the fields it can name.\n */\n const fields = [\n ['label', label],\n ['unit', physicalDimension],\n ['transducer', transducer],\n ['prefiltering', prefiltering],\n ] as const;\n const affected = fields.filter(([, text]) => [...text].some(isControlCharacter));\n const control = affected.flatMap(([, text]) => [...text].filter(isControlCharacter));\n if (control.length > 0) {\n /*\n How many of them get named is the header's decision, and it was unbounded.\n\n Every other enumeration whose length the file controls goes through `listed`, which\n is the helper written for exactly this — \"without letting the file decide how long\n the sentence gets\". This one joined the whole set. Four free-text fields of 16, 8, 80\n and 80 bytes can carry 63 distinct control codes between them, and did:\n\n warning: Signal 0's label, unit and transducer contain 63 control characters\n (\\x01, \\x02, \\x03, ... \\x9e), which will appear in the CSV column name and in\n channels.csv exactly as the header has them.\n\n 549 characters on one line, from a warning whose point is the short sentence around\n the list. The count is already in that sentence — \"63 control characters\" — so the\n tail the cut hides is the part the reader was told twice.\n */\n const shown = listed(\n [...new Set(control)].map(\n (c) => `\\\\x${(c.codePointAt(0) as number).toString(16).padStart(2, '0')}`,\n ),\n );\n const inLabel = affected.some(([name]) => name === 'label');\n // \"label and unit\", not \"label, unit\" — `listed` is for long enumerations that get\n // truncated, and this is a sentence with at most four items in it.\n const names = affected.map(([name]) => name);\n const named =\n names.length === 1\n ? (names[0] as string)\n : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1] as string}`;\n // Where they land, which is the question the reader has. A label becomes a column\n // name in signals.csv; the other three are cells of channels.csv and nothing else.\n // Named down to the cell when there is one of them, because that is the answer to\n // \"where did it go\" — `channels.csv` alone leaves a reader scanning fourteen columns.\n /*\n And whether there is a channels.csv at all, which this took for granted.\n\n `--stdout` writes the signal table and nothing else — no channels.csv, no\n annotations.csv, no metadata.json. A control byte in a unit reaches no file at all\n in that mode, and this said it would be in a cell of one:\n\n $ edf2csv control-labels.edf --stdout | less\n warning: Signal 0's label and unit contain 2 control characters (\\x1b), which\n will appear in the CSV column name and in channels.csv's unit cell\n exactly as the header has them.\n ... Printing the CSV to a terminal may do more than print it.\n\n Which is the mode this warning matters most in: `--stdout` *is* printing the CSV to\n a terminal, and the hint two lines down says so. The header is parsed before any\n destination is chosen, so the sentence says what is true of both rather than\n guessing — the same reason the `DUPLICATE_LABEL` hint above names two layouts.\n */\n const cells = affected.filter(([name]) => name !== 'label').map(([name]) => name);\n const where =\n cells.length === 1\n ? `channels.csv's ${cells[0] as string} cell in any conversion that writes one`\n : 'the channels.csv of any conversion that writes one';\n /*\n \"The CSV column name\" is the wide layout's answer, given in both.\n\n A long signals.csv has three columns — time_s, channel, value — and a label is not\n one of them: it is a value in the `channel` column, once per row. So a control byte\n in a label reaches one cell of the header line under the default layout and every\n row of the table under `--layout long`, and the sentence naming where it went named\n only the first. Same fault 0.8.24 fixed for `DUPLICATE_LABEL`'s message and 0.8.43\n for its hint.\n\n The parser cannot know the layout, and unlike those two this sentence does not have\n room to name both — it already carries the field list, the byte list and where the\n cell fields land. \"The channel's name\" is what both layouts call it: the term the\n long layout's own warnings use, and what a column name is.\n */\n const lands =\n inLabel && cells.length > 0\n ? `which will appear as the channel's name in signals.csv and in ${where}`\n : inLabel\n ? \"which will appear as the channel's name in signals.csv\"\n : `which will appear in ${where}`;\n diagnostics.push({\n code: 'NONPRINTABLE_LABEL',\n severity: 'warning',\n message:\n `Signal ${i}'s ${named} ${affected.length === 1 ? 'contains' : 'contain'} ` +\n // A comma before the closing clause, which now has a clause of its own inside it.\n `${counted(control.length, 'control character')} (${shown}), ${lands}, exactly as the ` +\n `header has ${control.length === 1 ? 'it' : 'them'}.`,\n hint:\n /*\n Every branch has to print a command that works.\n\n The middle one quoted the label back, which is right until the label is empty:\n an unlabelled channel got `--channels \"\"`, and that exits 2 with \"--channels was\n given but lists no channel names\". A hint whose command fails is worse than no\n hint, and this warning's whole job is to say how to reach a channel whose header\n text you cannot type. `EMPTY_LABEL` already says the position is the only way in\n for such a channel; so does this now.\n\n A comma is the third way. `--channels` separates names with one, and splits on\n every occurrence, so a channel labelled `EEG Fpz-Cz, ref` cannot be selected by\n name at all: the quoted-back advice printed `--channels \"EEG Fpz-Cz, ref\"`, which\n exits 2 with `No channel named \"EEG Fpz-Cz\"` — a channel the file does not have,\n named after half of one it does. Commas in labels are ordinary, since EDF labels\n are free text, and the CSV header quotes them; only this one hint claimed\n something about them that isn't so.\n */\n /*\n And a fourth way, which this branch printed straight past.\n\n A shell expands `$`, a backtick and a backslash inside double quotes, so a\n channel labelled `EEG $ref` was answered with `--channels \"EEG $ref\"` — which\n arrives as `EEG ` and exits 2 on a channel the file does not have. A backtick\n opens a command substitution and the pasted line does not even finish. That is\n the failure 0.7.18 fixed for `--channels`' own suggestion, and `typeable` is the\n rule it fixed it with: double quotes where they survive, single quotes where\n they do not, null where nothing does. Asking it settles which branch this takes\n as well, so the two cannot disagree about one label again.\n */\n (typeable(label) === null\n ? `Address the channel by position with --channels \"#${i}\" rather than by name, ` +\n `since ${\n inLabel\n ? 'the name cannot be typed'\n : label === ''\n ? 'it has no label'\n : 'a comma in the label would read as two names'\n }. `\n : `The column name is unaffected, so --channels ${typeable(label) as string} still selects it. `) +\n 'Printing the CSV to a terminal may do more than print it.',\n });\n }\n\n /*\n Header text a spreadsheet will run rather than read.\n\n The same four free-text fields, asked a different question. `=`, `+` and `@` start a\n formula in Excel, LibreOffice and Sheets no matter which file the cell came from, and\n these fields land in a CSV header row and in channels.csv verbatim — so a channel\n labelled `=1+1` opens as a column headed 2, and `=HYPERLINK(...)` opens as a link\n nobody in the reading chain wrote. SECURITY.md already calls these fields\n attacker-controlled because they reach filenames; this is where they reach a program\n that executes text.\n\n Not `-`, which the same advice usually includes. A lone `-` is a real convention for\n \"no unit\" and appears in the fixtures, a leading `-` on a montage label is ordinary,\n and neither is executed unless what follows parses as a formula — so warning on it\n would fire on files that are fine, which is how a warning gets ignored.\n\n Said, not fixed. Prefixing the cell with a quote is the usual mitigation and would mean\n writing something the header does not say, which is the one thing this tool refuses to\n do; NONPRINTABLE_LABEL answers control bytes the same way.\n */\n const formulaic = fields.filter(([, text]) => startsFormula(text));\n if (formulaic.length > 0) {\n const names = formulaic.map(([name]) => name);\n const named =\n names.length === 1\n ? (names[0] as string)\n : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1] as string}`;\n const shown = [...new Set(formulaic.map(([, text]) => text[0] as string))].join(', ');\n diagnostics.push({\n code: 'FORMULA_LABEL',\n severity: 'warning',\n message:\n `Signal ${i}'s ${named} ${formulaic.length === 1 ? 'starts' : 'start'} with ` +\n `${shown}, which Excel, LibreOffice and Google Sheets read as the start of a ` +\n `formula rather than as text.`,\n hint:\n 'The text is written exactly as the header has it, so the cell is what the ' +\n 'recording says. Open the CSV with pandas or R, or import it into the ' +\n 'spreadsheet as text, if you do not want it evaluated.',\n });\n }\n\n if (label === '') {\n // Collected, not reported here: what this channel's column ends up called depends on\n // whether some later channel is literally labelled `signal_<i>`, and inside this loop\n // the later channels do not exist yet. See the pass below.\n emptyLabels.push(i);\n } else {\n // Collected rather than reported here: a label repeated five times should\n // produce one warning naming all five, not four near-identical pairs.\n const seen = seenLabels.get(label);\n if (seen) seen.push(i);\n else seenLabels.set(label, [i]);\n }\n\n if (samplesPerRecord === 0) {\n diagnostics.push({\n code: 'NO_SAMPLES',\n severity: 'warning',\n message: `Signal ${i} (\"${label}\") carries no samples at all (0 per data record).`,\n hint: 'It is described in channels.csv but left out of the converted data.',\n });\n }\n\n /*\n Too large to represent, and too small — the second was silent.\n\n The gain is the span divided by the digital range, and a span of 2e-320 over 65,535\n codes is 3e-325: below the smallest subnormal double, so it underflows to +0. The\n scaler's flat-range branch then handed every code the same physical value, and a\n channel of 65,536 distinct readings became one repeated number with nothing raised at\n all. One power of ten away, at 1e-319, the same file raises VALUE_RESOLUTION.\n\n Both are the same fact about the header — the span cannot be turned into a mapping —\n so both get this code, and both leave the cells empty rather than filling them with a\n value the header cannot justify.\n */\n const span = physicalMax - physicalMin;\n const underflowed = span !== 0 && span / (digitalMax - digitalMin) === 0;\n if (!Number.isFinite(span) || underflowed) {\n diagnostics.push({\n code: 'UNUSABLE_PHYSICAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") declares a physical range from ${physicalMin} to ` +\n `${physicalMax}, whose span is too ${underflowed ? 'small' : 'large'} to ` +\n `represent, so its values cannot be scaled.`,\n hint: 'Its cells are left empty rather than filled with a value the header cannot justify.',\n });\n } else if (digitalMax === digitalMin) {\n diagnostics.push({\n code: 'DEGENERATE_DIGITAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") has digital minimum equal to digital maximum ` +\n `(${digitalMin}), so its values cannot be scaled.`,\n hint: 'Its cells are left empty rather than filled with a value the header cannot justify.',\n });\n } else if (physicalMax === physicalMin) {\n diagnostics.push({\n code: 'DEGENERATE_PHYSICAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") has physical minimum equal to physical maximum ` +\n `(${physicalMin}), so every sample converts to the same value.`,\n /*\n The one branch of the four that fills the cells, and the only one that said\n nothing about them.\n\n Its three neighbours all end \"Its cells are left empty rather than filled with a\n value the header cannot justify\", which is the opposite of what happens here: the\n mapping is defined — it has one point in it — so `physicalMin` is written into\n every cell, and a reader who had learned the family's answer from the branch above\n gets it backwards. A column of one repeated number is also the shape a genuinely\n flat recording takes, which is the confusion worth heading off.\n */\n hint:\n 'Its cells carry that value rather than being left empty, since the mapping is ' +\n 'defined — it just has one point in it. The distinct digital codes behind them ' +\n 'are not recoverable from the CSV.',\n });\n } else if ((physicalMax - physicalMin) * (digitalMax - digitalMin) < 0) {\n /*\n Polarity is inverted when the gain is negative, and the gain is\n (physicalMax - physicalMin) / (digitalMax - digitalMin) — so it is the sign of the\n two spans together that matters, not the physical pair alone.\n\n Testing only `physicalMax < physicalMin` was wrong in both directions. A file with\n its DIGITAL bounds reversed is just as inverted and drew no warning at all, handing\n back sign-flipped EEG with nothing to indicate it. A file with BOTH pairs reversed\n has a positive gain and is not inverted, yet was warned about — a message that was\n simply untrue of that recording.\n */\n const reversed =\n physicalMax < physicalMin\n ? `physical minimum ${physicalMin} above physical maximum ${physicalMax}`\n : `digital minimum ${digitalMin} above digital maximum ${digitalMax}`;\n diagnostics.push({\n code: 'INVERTED_PHYSICAL_RANGE',\n severity: 'warning',\n message: `Signal ${i} (\"${label}\") declares ${reversed}, which inverts its polarity.`,\n hint: 'The values are converted exactly as the header specifies, inversion included.',\n });\n }\n }\n }\n\n /*\n What an unlabelled channel is actually called, which the message used to guess.\n\n A channel with no label takes `signal_<index>` — unless another channel is literally\n labelled that, which EDF permits, since labels are free text and nothing enforces anything\n about them. Then both collide and both are suffixed. The warning said \"It will appear as\n \"signal_0\"\" while the file's header read `time_s,signal_0_ch0,signal_0_ch1`: the one\n sentence the run printed named a column that exists in neither signals.csv nor\n channels.csv.\n\n The other half was silent. The channel that genuinely carries the label `signal_0` lost\n its own column name to a collision with a synthesised one, and nothing said so —\n DUPLICATE_LABEL did not fire, because the two labels are not the same label. Both halves\n are one sentence here, because they are one event.\n\n No specific suffixed name is quoted. The suffix rule has a second pass for names that are\n still shared afterwards, and a message that hard-coded `_ch<index>` would be guessing again\n in exactly the way this is fixing.\n */\n for (const index of emptyLabels) {\n const taken = seenLabels.get(`signal_${index}`);\n diagnostics.push({\n code: 'EMPTY_LABEL',\n severity: 'warning',\n message:\n taken === undefined\n ? `Signal ${index} has no label. It will appear as \"signal_${index}\".`\n : `Signal ${index} has no label, so it takes the name \"signal_${index}\" — which ` +\n `${taken.length === 1 ? 'signal' : 'signals'} ${listed(taken.map(String))} already ` +\n `${taken.length === 1 ? 'carries' : 'carry'} as a label, so both columns are ` +\n `suffixed with their position instead.`,\n /*\n The position, not the word \"position\".\n\n A hint that offers a command has to offer one that can be typed — 0.7.18 and 0.7.19\n went through the others making sure each one could be pasted, and NONPRINTABLE_LABEL\n two hundred lines up writes `--channels \"#${i}\"` with the number in it. This one said\n `--channels \"#<position>\"` while the sentence above it began \"Signal 0\", so the\n reader was asked to carry a number across from the message and put it in themselves.\n */\n hint:\n 'The name is built from the position rather than read from the header, so it moves ' +\n `if the file's channels do. Select the channel as --channels \"#${index}\" to say ` +\n 'which one you mean without depending on it.',\n });\n }\n\n /*\n A timestamp that is not one.\n\n EDF gives the start date and time eight characters each, and nothing stops a writer\n putting `32.13.99` and `25.61.61` there. `--info` has always echoed the raw fields with\n \"(unparseable)\" beside them, but nothing was raised: the conversion exited 0, `--strict`\n passed, and metadata.json recorded `start_datetime_local: null` with no note against it.\n\n Every other unusable header field reports itself — a degenerate digital range, a physical\n span that cannot be represented, a comma decimal separator, a header whose declared size\n disagrees with its signal count. This was the one that did not, and it is the field\n output-files points at for turning `time_s` into an absolute instant.\n */\n /*\n The sixtieth second, which is a second UTC has and a calendar date does not.\n\n `resolveStartDateTime` admits `ss === 60` on purpose — a recorder synchronised to UTC\n through a leap second writes `23.59.60`, and refusing it would throw away a date that is\n otherwise perfectly good over one second. What it then does is `Math.min(ss, 59)`, because\n `Date.UTC(..., 60)` rolls over into the next minute and would move the instant the other\n way, by fifty-nine seconds more.\n\n Keeping the nearest instant is the right answer. Keeping it in silence was not: `--info`\n printed `Recorded 2020-01-01 23:59:59` and metadata.json recorded the same, for a header\n that says `23.59.60`, with `--strict` exiting 0. Every other header field this tool cannot\n represent exactly says so — a comma decimal separator, a physical span that overflows, a\n record count that disagrees with the file — and this is the field `time_s` is documented as\n being added to.\n */\n if (\n namesLeapSecond(startTimeRaw) &&\n resolveStartDateTime(startDateRaw, startTimeRaw, recordingId) !== null\n ) {\n diagnostics.push({\n code: 'LEAP_SECOND_START',\n severity: 'warning',\n message:\n `The header's start time (\"${startTimeRaw}\") names the sixtieth second of a minute, ` +\n `which no calendar date has.`,\n hint:\n 'It is recorded as the fifty-ninth second, one second earlier, since that is the ' +\n 'nearest instant a date can hold. time_s is unaffected — it counts from the start of ' +\n 'the recording either way.',\n });\n }\n\n /*\n Two dates in one header, disagreeing.\n\n EDF+ requires the recording identification field's `Startdate` to be the header's start\n date. Where it is, its four digits settle the century — see resolveStartDateTime. Where it\n is not, one of the two is wrong and there is no way to tell which, so the date field is\n used, being the one the format defines. That was done in silence, on a header that plainly\n contradicts itself:\n\n Recorded 2002-03-02 22:15:00\n Recording Startdate 05-MAR-2002 PSG-1234/2002 NN Telemetry03\n\n The same shape as a record count that disagrees with the file, or a declared header size\n that disagrees with the signal count, both of which have said so for versions.\n */\n const statedDate = recordingIdStartdate(recordingId);\n const headerDate = /^(\\d{2})[.\\-/](\\d{2})[.\\-/](\\d{2})$/u.exec(trimField(startDateRaw));\n if (statedDate !== null && headerDate !== null) {\n const day = Number(headerDate[1]);\n const month = Number(headerDate[2]);\n const yy = Number(headerDate[3]);\n if (statedDate.day !== day || statedDate.month !== month || statedDate.year % 100 !== yy) {\n diagnostics.push({\n code: 'START_DATE_MISMATCH',\n severity: 'warning',\n message:\n `The header's start date (\"${startDateRaw}\") and the date its recording ` +\n `identification states (\"${String(statedDate.day).padStart(2, '0')}-` +\n `${MONTHS[statedDate.month - 1] as string}-${statedDate.year}\") are different ` +\n `dates, which EDF+ does not permit.`,\n hint:\n 'The start date field is used, since that is the one the format defines. Which of ' +\n 'the two is right is not knowable from the file, so start_datetime_local may name ' +\n 'the wrong day.',\n });\n }\n }\n\n if (resolveStartDateTime(startDateRaw, startTimeRaw, recordingId) === null) {\n diagnostics.push({\n code: 'START_TIME_UNREADABLE',\n severity: 'warning',\n message:\n `The header's start date and time (\"${startDateRaw}\" and ` +\n `\"${startTimeRaw}\") are not a date and a time, so the recording has ` +\n `no start instant.`,\n hint:\n 'time_s is unaffected — it counts from the start of the recording either way. What ' +\n 'cannot be done is turning it into a wall-clock instant, and metadata.json records ' +\n 'start_datetime_local as null.',\n });\n }\n\n for (const [label, indices] of seenLabels) {\n if (indices.length < 2) continue;\n diagnostics.push({\n code: 'DUPLICATE_LABEL',\n severity: 'warning',\n /*\n Cut by the function that cuts every other list in a sentence here.\n\n `join` names all of them, however many there are, and a header may declare as many\n channels as it likes under one label: a 200-channel montage all labelled `T8-P8` — which\n is exactly the kind of file this warning is for, since CHB-MIT ships two of them —\n produced a single 1,100-character line of positions with the sentence that mattered at\n the front of it. `listed` shows eight and counts the rest, which is what the rate\n warning, the leftover-file warning, the channel-position lists in `--channels` and the\n `EMPTY_LABEL` message one loop up all already do. This was the last `join` of a\n file-controlled list left in a diagnostic.\n\n Two positions render identically either way, so the ordinary duplicate reads as it\n always has.\n */\n /*\n Positions written `#N`, which is how a position is written everywhere it is meant to be\n typed: `--channels \"#0\"`, \"This file has signal channels at #0, #1, #2\", and this\n warning's own namesake from channel selection — \"(positions #0, #1); all of them were\n selected\". This one said \"(positions 0, 1)\", two paragraphs above the page that tells\n the reader to \"address it by position with #N\". The number is the same; the form that\n works is not.\n */\n message:\n `${counted(indices.length, 'signal')} share the label \"${label}\" ` +\n `(positions ${listed(indices.map((i) => `#${i}`))}).`,\n /*\n A sentence about the wide layout, printed in both — the same fault 0.8.24 fixed for\n the other raiser of this code, two lines further down the same run.\n\n A long signals.csv has three columns, `time_s`, `channel` and `value`, and none of\n them comes from a label: a channel appears there as a value in the `channel` column.\n `label-suffix-collision.edf --layout long` printed both halves back to back:\n\n warning: 2 signals share the label \"T8\" (positions #0, #1).\n Their columns are suffixed with the signal number so they stay\n distinguishable.\n warning: Signal 2 is labelled \"T8_ch0\", ... so it is named \"T8_ch0_ch2\" in the\n channel column.\n Channel names are unique; look this channel up in channels.csv by its\n signal_index.\n\n The plan raises the second and knows the layout. The header raises this one and\n cannot: it is parsed before any conversion is planned, and `EdfFile.open` hands these\n diagnostics to callers who have asked for no layout at all. So the sentence names\n both, rather than guessing which one is being written.\n */\n hint:\n 'Their names are suffixed with the signal number so they stay distinguishable: a ' +\n 'column name each in the wide layout, and a distinct value in the channel column ' +\n 'under --layout long.',\n });\n }\n\n const recordBytes = byteOffsetInRecord;\n if (recordBytes <= 0) {\n throw new EdfError(\n 'NO_SAMPLES',\n 'No signal in this file carries any samples (every channel declares 0 samples per record).',\n 'The header describes the channels and nothing has data behind them, so there is no ' +\n 'record to read. A header written before recording began, or copied from another ' +\n 'file, produces this.',\n );\n }\n\n if (sawComma.value) {\n diagnostics.push({\n code: 'COMMA_DECIMAL',\n severity: 'warning',\n message: 'Some header numbers use a comma decimal separator, which the EDF spec does not allow.',\n hint: 'They were read as decimal points. Check the values in the channel table.',\n });\n }\n\n const dataBytes = fileSize - expectedHeaderBytes;\n if (dataBytes < 0) {\n // With the two figures, like the other two FILE_TOO_SMALL messages and unlike this one,\n // which named neither — the same thing the NO_DATA_RECORDS message below was fixed for:\n // \"the message carried no figures at all, so nothing in it could be checked against the\n // file\". Reached by a caller handing `parseHeader` a header and a smaller size than the\n // one it came out of, which is a mismatch worth being able to see.\n throw new EdfError(\n 'FILE_TOO_SMALL',\n `File is ${counted(fileSize, 'byte')}, which is less than the ${grouped(expectedHeaderBytes)} ` +\n `its own header occupies.`,\n 'The header and the size came from different reads: parseHeader was handed a byte ' +\n 'count smaller than the header block it was given.',\n );\n }\n const recordCount = Math.floor(dataBytes / recordBytes);\n const trailingBytes = dataBytes - recordCount * recordBytes;\n\n if (recordCount === 0) {\n /*\n Which of the two, and with the numbers.\n\n \"The recording was probably interrupted before any data was written\" is right about an\n empty file and wrong about the other way to get here: a header declaring records larger\n than the data present. A 606 KB file holding 589 KB of samples — 60% of one record, more\n than half a million readings — was told no data was written, and the message carried no\n figures at all, so nothing in it could be checked against the file. The declared record\n size is the thing to look at, and it was the one thing not said.\n\n Still an error either way. A record is the unit the format is addressed in, and there is\n nothing smaller to convert.\n */\n const empty = dataBytes === 0;\n throw new EdfError(\n 'NO_DATA_RECORDS',\n empty\n ? 'The file contains a header and no data at all.'\n : `The file contains ${counted(dataBytes, 'byte')} of data, which is less than the ` +\n `${grouped(recordBytes)} its header says one data record takes.`,\n empty\n ? 'The recording was probably interrupted before any data was written.'\n : 'Either the recording was cut short part way through its first record, or the ' +\n 'header describes records larger than the ones actually written. Check the ' +\n 'samples-per-record fields against the file size.',\n );\n }\n\n if (declaredRecordCount === -1) {\n diagnostics.push({\n code: 'RECORD_COUNT_UNKNOWN',\n severity: 'warning',\n message:\n `The header does not say how many data records the file has (-1), which the spec allows ` +\n `for recordings still in progress. Using the ${counted(recordCount, 'record')} the file actually contains.`,\n hint:\n 'That count comes from the size of the file, so a recording still being written ' +\n 'converts as much of it as was on disk when this run read it.',\n });\n } else if (declaredRecordCount !== recordCount) {\n diagnostics.push({\n code: 'RECORD_COUNT_MISMATCH',\n severity: 'warning',\n // Both counts through `counted`, not only the second. A header declaring one record\n // over a file holding three read \"declares 1 data records\" — the slip the sentence\n // beside it has been holding to since it was written.\n message:\n `The header declares ${counted(declaredRecordCount, 'data record')} but the file ` +\n `contains ${grouped(recordCount)}. Converting the ${counted(recordCount, 'record')} that ${recordCount === 1 ? 'is' : 'are'} present.`,\n hint:\n declaredRecordCount > recordCount\n ? 'The recording looks truncated. It may have been cut short or copied incompletely.'\n : 'The file is longer than its header claims.',\n });\n }\n\n if (trailingBytes > 0) {\n diagnostics.push({\n code: 'TRAILING_BYTES',\n severity: 'warning',\n message: `${counted(trailingBytes, 'byte')} after the last complete data record ${trailingBytes === 1 ? 'was' : 'were'} ignored.`,\n hint:\n 'A record is the unit this format is addressed in, and a partial one says nothing ' +\n 'about which samples it holds or when they were taken. Every complete record was ' +\n 'converted.',\n });\n }\n\n const isEdfPlus = continuity !== null;\n if (continuity === 'EDF+D') {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `This is a discontinuous (${isBdf ? 'BDF+D' : 'EDF+D'}) recording: its data records are ` +\n `not contiguous in time.`,\n hint: 'Each row carries its true recording time, so gaps stay visible instead of being closed.',\n });\n }\n\n const dataSignals = signals.filter((s) => !s.isAnnotations);\n if (dataSignals.length === 0) {\n diagnostics.push({\n code: 'NO_SIGNAL_CHANNELS',\n severity: 'warning',\n /*\n The marker as the file spells it, which is what the DISCONTINUOUS warning ten lines up\n already does and what `describeFormat` was fixed for.\n\n A BDF+ recording holding nothing but its `BDF Annotations` channel — a label this\n parser recognises by name, beside `EDF Annotations`, and has since annotations were\n read at all — was told it \"contains only EDF+ annotations\", naming a channel it does\n not carry and a format it is not. `--info` two lines above says `BDF+ (continuous)`\n and the channel table names the channel; this warning was the one line disagreeing\n with both.\n */\n message: `This file has no signal channels; it contains only ${isBdf ? 'BDF+' : 'EDF+'} annotations.`,\n /*\n What the file is, not what the run will write. The warning underneath it — raised by\n the conversion rather than by the parser — already answers the second question, in\n those words: \"annotations.csv holds whatever events it carries. channels.csv lists\n signal channels, so it has none to list.\" Two consecutive hints saying that would be\n worse than the one that was missing.\n */\n hint:\n `An ${isBdf ? 'BDF+' : 'EDF+'} file of events and no signal is an ordinary thing: a ` +\n 'scoring file distributed beside the recording it annotates has exactly this shape.',\n });\n }\n\n // A channel declaring zero samples per record has no sampling rate to speak of — it is\n // reported separately as NO_SAMPLES and no file is written for it. Counting its nominal\n // 0 Hz as a rate made a single-rate recording warn that it used \"2 different sampling\n // rates (4 Hz, 0 Hz)\" and claim it was splitting output it never split.\n const rates = new Set(dataSignals.filter((s) => s.samplesPerRecord > 0).map((s) => s.samplingRate));\n if (rates.size > 1) {\n diagnostics.push({\n code: 'MIXED_SAMPLING_RATES',\n severity: 'warning',\n message:\n `Channels use ${counted(rates.size, 'different sampling rate')} ` +\n `(${listed(formatRates([...rates].sort((a, b) => b - a)).map((r) => `${r} Hz`))}).`,\n hint: 'They are written to one file per rate so no channel is resampled.',\n });\n }\n\n return {\n header: {\n version,\n patientId,\n recordingId,\n startDateRaw,\n startTimeRaw,\n startDateTime: resolveStartDateTime(startDateRaw, startTimeRaw, recordingId),\n headerBytes: expectedHeaderBytes,\n declaredHeaderBytes: headerBytes,\n reserved,\n isEdfPlus,\n isBdf,\n continuity,\n declaredRecordCount,\n recordDuration,\n signalCount,\n signals,\n bytesPerSample,\n recordBytes,\n },\n recordCount,\n trailingBytes,\n diagnostics,\n };\n}\n\n/**\n * The recording start as a zone-less wall clock, \"YYYY-MM-DDTHH:MM:SS\".\n *\n * EDF stores the start time as local wall-clock digits with no timezone anywhere in\n * the format. `startDateTime` is built with Date.UTC purely so those digits survive a\n * round trip unshifted, which makes it a carrier for the wall clock rather than a\n * real instant. Serialising it with `toISOString()` would append a Z and assert UTC,\n * and any reader converting to local time would then shift the recording by their own\n * offset: 13:43:04 in the file becomes 08:43:04 in New York. The Z is omitted because\n * the file genuinely does not say which zone it meant.\n */\nexport function formatWallClock(date: Date | null): string | null {\n if (!date) return null;\n if (!(date instanceof Date)) {\n throw new OptionError(`date must be a Date or null, got ${describeValue(date)}.`);\n }\n /*\n And an unparseable one, which is a date this cannot state rather than a bad argument.\n\n `new Date('the third').toISOString()` is a `RangeError: Invalid time value` out of the\n middle of this function. `null` is what this returns for a recording with no start\n instant, and that is what an invalid Date is: `START_TIME_UNREADABLE` says the same thing\n about the same fact, and metadata.json writes `start_datetime_local: null` for it.\n */\n if (Number.isNaN(date.getTime())) return null;\n return date.toISOString().slice(0, 19);\n}\n\n/**\n * The recording's format, as `--info`, `metadata.json` and `--json` all name it.\n *\n * `\"EDF\"`, `\"BDF\"`, or one of `\"EDF+ (continuous)\"`, `\"EDF+ (discontinuous)\"`,\n * `\"BDF+ (continuous)\"`, `\"BDF+ (discontinuous)\"`. A BDF+ file reports its own spelling even\n * though `continuity` normalises the marker to the `EDF+` form.\n *\n * This said `EDF+ (EDF+D)` and `BDF+ (EDF+C)`, which are not strings it can return — the\n * parenthetical is the word, not the marker. It is a one-line doc comment on a public export,\n * so it is what a TypeScript consumer's editor shows and what `dist/edf/header.d.ts` ships,\n * and the value it describes is `recording.format` in every metadata.json this tool writes.\n * A consumer branching on the tooltip's spelling never matches. Every documentation page had\n * it right; this was the only place that did not.\n */\nexport function describeFormat(header: EdfHeader): string {\n /*\n The argument, which this read two booleans off and asked no questions about.\n\n Both reads are truthiness tests, so anything without them answers `'EDF'` — including the\n `EdfFile` whose `.header` this wants, which is one property away and is how the api page\n writes every other call. `describeFormat(file)` on a discontinuous BDF+ recording returned\n `'EDF'`: not an error, not a fallback, a confident wrong answer about the file in hand.\n\n The same check `makeScaler` got in 0.8.61, for the same reason — an exported function that\n reads fields off an object cannot tell a header from anything else that lacks them.\n */\n if (\n typeof header !== 'object' ||\n header === null ||\n typeof header.isBdf !== 'boolean' ||\n typeof header.isEdfPlus !== 'boolean'\n ) {\n throw new OptionError(`header must be a parsed EDF header, got ${describeValue(header)}.`);\n }\n const base = header.isBdf ? 'BDF' : 'EDF';\n if (!header.isEdfPlus) return base;\n return `${base}+ (${header.continuity === 'EDF+D' ? 'discontinuous' : 'continuous'})`;\n}\n\n/** Render a sampling rate without trailing noise: 256, 0.5, 12.5. */\nexport function formatRate(hz: number): string {\n // A number, like the neighbours. `formatRate(NaN)` came back as the string \"NaN\", and\n // `rateSlug` turned it into \"NaNhz\" — a file name this tool cannot write, handed to a\n // caller the doc comment below describes as \"reaching for the exported slug function to\n // predict a filename\". A rate that is not a number is not a rate.\n if (typeof hz !== 'number' || Number.isNaN(hz)) {\n throw new OptionError(`hz must be a sampling rate in hertz, got ${describeValue(hz)}.`);\n }\n if (Number.isInteger(hz)) {\n /*\n The same six decimals, in the notation `toFixed` cannot reach.\n\n Every double past 2^53 is an integer, so this branch takes every large rate — and\n `String` switches to exponent form at 1e21 and carries the full seventeen digits with\n it. Four samples in a record of 1e-300s is 3.9999999999999996e+300, which is exactly the\n float noise the rounding below exists to remove, printed in a `RATE` column of otherwise\n plain numbers and pasted into an output filename by `rateSlug`. Below 1e21 `String` is\n already exact and is left alone, so an integer rate a file can really state — 2^53\n samples in a second — is not rounded to something it is not.\n */\n const text = String(hz);\n return text.includes('e') ? String(Number(hz.toExponential(6))) : text;\n }\n const rounded = Number(hz.toFixed(6));\n // A rate below 5e-7 rounds away to \"0\", which reads as \"this channel has no sampling\n // rate\" and made the mixed-rate warning contradict itself: it announced two different\n // rates and then printed both as \"0 Hz\". Exponent form keeps a real rate legible, and\n // keeps distinct rates distinct in the channel table and in output filenames.\n if (rounded === 0) return hz.toExponential(3);\n return String(rounded);\n}\n\n/**\n * Renders a group of rates so that rates which differ read as differing.\n *\n * `formatRate` rounds to six decimals, which is what keeps an ordinary rate free of\n * float noise — 30 samples in a 0.1-second record is 299.99999999999994 as a double,\n * and belongs on screen as 300. Two rates separated by less than that round to one\n * string, so a file carrying 1e-6 Hz and 1.25e-6 Hz warned that it used \"2 different\n * sampling rates (0.000001 Hz, 0.000001 Hz)\" and named both files the same thing.\n *\n * That is the contradiction the exponent fallback above already removes for rates that\n * round away to zero; this is the same one a step further out. On a collision every rate\n * in the group switches to its shortest exact form, which is unique for distinct values,\n * rather than only the pair that collided — one column in one notation reads better than\n * two.\n */\nexport function formatRates(rates: readonly number[]): string[] {\n // `rates.map` on anything else is a TypeError naming a local of this package; the members\n // are `formatRate`'s business, one line down.\n if (!Array.isArray(rates)) {\n throw new OptionError(`rates must be a list of sampling rates, got ${describeValue(rates)}.`);\n }\n const rounded = rates.map(formatRate);\n const distinct = new Set(rates).size;\n return new Set(rounded).size === distinct ? rounded : rates.map((hz) => String(hz));\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edf2csv",
3
- "version": "0.8.73",
3
+ "version": "0.8.75",
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",