edf2csv 0.8.96 → 0.8.98

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.
@@ -1 +1 @@
1
- {"version":3,"file":"reader.js","sourceRoot":"","sources":["../../src/edf/reader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Chunked reader for EDF / EDF+ files.\n *\n * Data records are read in batches sized by a byte budget rather than all at once,\n * so peak memory stays flat regardless of how long the recording is. A 4 GB file\n * and a 4 MB file use the same working set.\n */\n\nimport { open, stat } from 'node:fs/promises';\nimport type { FileHandle } from 'node:fs/promises';\n\nimport { createHash } from 'node:crypto';\n\nimport { EdfError } from './errors.js';\nimport type { Diagnostic } from './errors.js';\nimport { FIXED_HEADER_BYTES, SIGNAL_HEADER_BYTES, parseHeader, peekSignalCount } from './header.js';\nimport type { EdfHeader, EdfSignal } from './header.js';\nimport { decodeRecordAnnotations } from './annotations.js';\nimport type { Annotation } from './annotations.js';\nimport { readInt16LE } from './bytes.js';\nimport { counted, grouped } from '../format/list.js';\n// Crossing into convert/ as header.ts already does for `typeable`: the check belongs to the\n// call rather than to the file, and there is one of it.\nimport { OptionError, assertInputPath, describeValue } from '../convert/options.js';\n// A path comes out of the filesystem and nobody vets it; see printable's own comment.\nimport { printable } from '../format/unprintable.js';\n\n/**\n * How far `readOrigin` looks for a record that states its own start time.\n *\n * Enough that one or two unreadable timekeeping entries at the top of a file cost nothing,\n * few enough that `--info` stays a header read rather than a scan.\n */\nconst RECORDS_SEARCHED_FOR_ORIGIN = 16;\n\n/** Default read budget per batch. Large enough to amortise syscalls, small enough to stay cheap. */\nexport const DEFAULT_CHUNK_BYTES = 8 * 1024 * 1024;\n\nexport interface RecordBatch {\n /** Index of the first record in this batch, relative to the whole file. */\n firstRecordIndex: number;\n recordCount: number;\n /**\n * Raw record bytes, `recordCount * header.recordBytes` long.\n *\n * The buffer is reused between iterations. Copy anything you need to keep past\n * the current loop turn.\n */\n data: Uint8Array;\n}\n\nexport interface ReadRecordsOptions {\n /** First record to read, inclusive. Defaults to 0. */\n startRecord?: number;\n /** Last record to read, exclusive. Defaults to the file's record count. */\n endRecord?: number;\n chunkBytes?: number;\n}\n\nexport class EdfFile {\n readonly path: string;\n readonly fileSize: number;\n /**\n * Last-modified time when this file was opened, in milliseconds, for the same reason as\n * `fileSize`.\n *\n * Kept as the raw number rather than a Date because `new Date(ms).getTime()` truncates to\n * whole milliseconds: comparing that against a later `fstat`, which carries the\n * filesystem's sub-millisecond precision, reported every undisturbed conversion as one\n * whose input had changed underneath it.\n */\n readonly modifiedAtOpenMs: number;\n readonly header: EdfHeader;\n /** Records actually present in the file, which may differ from the header's claim. */\n readonly recordCount: number;\n readonly trailingBytes: number;\n readonly diagnostics: Diagnostic[];\n\n #handle: FileHandle;\n #closed = false;\n /** The last answer `changedSinceOpen` computed, so it survives the file being closed. */\n #changed: boolean | null = null;\n\n private constructor(init: {\n path: string;\n fileSize: number;\n modifiedAtOpenMs: number;\n header: EdfHeader;\n recordCount: number;\n trailingBytes: number;\n diagnostics: Diagnostic[];\n handle: FileHandle;\n }) {\n this.path = init.path;\n this.fileSize = init.fileSize;\n this.modifiedAtOpenMs = init.modifiedAtOpenMs;\n this.header = init.header;\n this.recordCount = init.recordCount;\n this.trailingBytes = init.trailingBytes;\n this.diagnostics = init.diagnostics;\n this.#handle = init.handle;\n }\n\n /**\n * SHA-256 of the bytes this conversion actually read.\n *\n * Hashed through the open descriptor, over exactly the `fileSize` bytes that were there\n * when the file was opened — the same number every record count and window in the output\n * was derived from. Re-opening the path to hash it afterwards described whatever was at\n * that name by then: a recording still being written grew from 2,000 records to 3,000\n * mid-conversion and metadata.json recorded `data_records: 2000` beside the checksum and\n * byte count of the 3,000-record file, which is provenance for bytes nobody converted.\n * Replacing the file at that path did the same thing more completely.\n */\n async sha256(): Promise<string> {\n this.#assertOpen();\n const hash = createHash('sha256');\n const buffer = Buffer.alloc(Math.min(this.fileSize, 4 * 1024 * 1024) || 1);\n for (let at = 0; at < this.fileSize; ) {\n const want = Math.min(buffer.length, this.fileSize - at);\n const { bytesRead } = await this.#handle.read(buffer, 0, want, at);\n if (bytesRead <= 0) {\n throw new EdfError(\n 'UNREADABLE',\n // Both figures grouped, like the shortfall message this file raises one function\n // over — `Expected 8,386,560 bytes of data at record 0 but only 2,899,456 bytes\n // were available` — and for the reason 0.8.5 gives: the sentence exists to put one\n // against the other, and at nine digits that is work the separators do.\n `Expected ${grouped(this.fileSize)} bytes to checksum but the file ended at ` +\n `${grouped(at)}; it appears to have changed size while it was being read.`,\n 'Make sure the recording is not still being written to, then try again.',\n );\n }\n hash.update(buffer.subarray(0, bytesRead));\n at += bytesRead;\n }\n return hash.digest('hex');\n }\n\n /**\n * Whether the file has changed since it was opened, by size or by modification time.\n *\n * Checked through the descriptor, so it answers for the bytes that were read rather than\n * for whatever now answers to the same name. A recording still being written is the\n * ordinary cause, and the conversion is still correct for the data it saw — it is the\n * claim that the output describes the file as it now stands that stops being true.\n */\n async changedSinceOpen(): Promise<boolean> {\n /*\n A closed file remembers its last answer rather than inventing a new one.\n\n Returning false once closed asserted \"it did not change\", which is not something a\n closed descriptor can know — and `convert()` closes the file before it returns, so\n `result.file.changedSinceOpen()` denied the very change the INPUT_CHANGED diagnostic\n in the same result object had just reported. One object, two answers.\n\n `convert()` always asks before closing, so the cached answer is the true one. A caller\n who closed the file without ever asking gets an error, which is the same treatment\n every other method on a closed file gets.\n */\n if (this.#closed) {\n if (this.#changed !== null) return this.#changed;\n throw new EdfError(\n 'UNREADABLE',\n `\"${this.path}\" is closed, and whether it changed while it was open was never checked.`,\n 'Ask before closing the file. A ConvertResult carries the answer already, since ' +\n 'convert() checks it on the way out.',\n );\n }\n const now = await this.#handle.stat().catch(() => null);\n if (now === null) return this.#changed ?? false;\n this.#changed = now.size !== this.fileSize || now.mtimeMs !== this.modifiedAtOpenMs;\n return this.#changed;\n }\n\n /*\n A sentence, and advice under it, like the destination-side twin.\n\n `Cannot read \"rec.edf\": no such file` was the one diagnostic this tool prints that does not\n end in a full stop — 68 of its 69 do — and the only member of its family with nothing\n indented under it. `Cannot create \"out\": part of the path does not exist.` has carried\n advice under it since the destination errors were given sentences — one line for every\n cause until 0.8.12, and the cause's own since — the mid-conversion UNREADABLE beside it\n carries one too, and this is the form a mistyped path actually reaches.\n */\n static readonly #UNREADABLE_HINT =\n 'Check the path is spelled the way it is on disk and that you have permission to read it.';\n\n static async open(path: string): Promise<EdfFile> {\n /*\n A path, checked before `fs` is asked about it.\n\n `assertInputPath` was written for exactly this and applied one level up. Its docstring\n names the function it was describing — \"`EdfFile.open` hands whatever it is given to\n `fs`, and the refusal comes back as an `EdfError` coded `UNREADABLE`, hinted 'Check the\n path is spelled the way it is on disk and that you have permission to read it' — advice\n about a path, over a value that is not one, filed as a problem with the recording rather\n than with the call\" — and then went into `convert`, leaving `EdfFile.open` itself, which\n is exported from the package root and is how the api page says to read a header without\n converting anything, doing the thing being described:\n\n EdfFile.open({ path: 'a.edf' })\n EdfError[UNREADABLE]: Cannot read \"[object Object]\": The \"path\" argument must be of\n type string or an instance of Buffer or URL. Received an instance of Object.\n\n Node's own argument-type text, under a hint about spelling and permissions, over a\n value that has neither. `EdfFile.open(['a.edf', 'b.edf'])` was worse: it answered\n `Cannot read \"a.edf,b.edf\"`, quoting a path the caller never wrote, because `String` of\n an array joins it with commas.\n\n The same `OptionError` `convert` raises for the same mistake, so one mistake has one\n answer whichever entry point it arrives at.\n */\n assertInputPath(path);\n const info = await stat(path).catch((cause: unknown) => {\n throw new EdfError(\n 'UNREADABLE',\n `Cannot read \"${printable(path)}\": ${describe(cause)}.`,\n EdfFile.#UNREADABLE_HINT,\n );\n });\n /*\n Both of these said what was wrong and nothing about what to do, which is the gap\n `#UNREADABLE_HINT` two lines up was added to close for the third member of this family.\n\n Neither is reached from the command line, and that is the point: a folder there is\n expanded to the recordings inside it and a socket is filtered out by the walk, so the\n caller who arrives here is holding a path in code and has no walk behind them.\n */\n if (info.isDirectory()) {\n throw new EdfError(\n 'UNREADABLE',\n `\"${printable(path)}\" is a directory, not an EDF file.`,\n 'Name a recording inside it. The command line expands a folder to the recordings it ' +\n 'holds; this takes one file.',\n );\n }\n if (!info.isFile()) {\n throw new EdfError(\n 'UNREADABLE',\n `\"${printable(path)}\" is not a regular file.`,\n 'A pipe, socket or device cannot be read as a recording: the parser seeks to a byte ' +\n 'offset inside the file, which only a real file supports.',\n );\n }\n\n /*\n Opening is a second chance to be refused, and it was the one that got through.\n\n `stat` needs the parent directory searchable and says nothing about the file's own mode,\n so a recording with no read permission passes it and fails here — the commonest\n permission failure there is. Unwrapped, it escaped as Node's own error: the CLI printed\n `error: EACCES: permission denied, open '...'` where every neighbouring failure prints\n the tool's sentence, and the library threw a plain Error whose `code` was the errno.\n\n api.md says `UNREADABLE` \"covers a missing file, a directory passed where a file was\n expected, a permission failure, and a file that changed size while being read. Branch on\n `code`, never on the message text.\" A consumer doing exactly that fell through to its\n generic handler.\n */\n const handle = await open(path, 'r').catch((cause: unknown) => {\n throw new EdfError(\n 'UNREADABLE',\n `Cannot read \"${printable(path)}\": ${describe(cause)}.`,\n EdfFile.#UNREADABLE_HINT,\n );\n });\n try {\n const fixed = Buffer.alloc(Math.min(FIXED_HEADER_BYTES, info.size));\n if (fixed.length > 0) {\n const bytesRead = await readFully(handle, fixed, 0, fixed.length, 0);\n if (bytesRead < fixed.length) throw changedWhileReading(0, fixed.length, bytesRead);\n }\n\n // The signal count decides how much more header there is to read. Read by the header\n // parser itself, so the two cannot disagree about which files are readable: this used\n // to have its own Number(), which tolerated the NUL padding sloppy writers emit but\n // not the comma decimal separator that COMMA_DECIMAL exists to accept.\n let headerBuffer = fixed;\n if (fixed.length === FIXED_HEADER_BYTES) {\n const ns = peekSignalCount(fixed);\n if (ns !== null) {\n const total = FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES;\n if (total <= info.size) {\n headerBuffer = Buffer.alloc(total);\n const bytesRead = await readFully(handle, headerBuffer, 0, total, 0);\n if (bytesRead < total) throw changedWhileReading(0, total, bytesRead);\n }\n }\n }\n\n const { header, recordCount, trailingBytes, diagnostics } = parseHeader(\n headerBuffer,\n info.size,\n );\n\n return new EdfFile({\n path,\n fileSize: info.size,\n modifiedAtOpenMs: info.mtimeMs,\n header,\n recordCount,\n trailingBytes,\n diagnostics,\n handle,\n });\n } catch (error) {\n await handle.close().catch(() => {});\n throw error;\n }\n }\n\n /** Signal channels, excluding the EDF+ annotations channel. */\n get dataSignals(): EdfSignal[] {\n return this.header.signals.filter((s) => !s.isAnnotations);\n }\n\n /**\n * The annotation channel a record's start time is read from.\n *\n * EDF+ puts the timekeeping TAL first in the first annotation channel, and this was read as\n * `annotationSignals[0]` — the first one declared, whether or not it can hold anything. A\n * writer that declares an annotation channel and gives it zero samples per record leaves a\n * slot of zero bytes, so nothing was read from it, and the timekeeping in the channel after\n * it went unread: a three-record EDF+D reported \"3 of 3 data records carry no readable\n * timekeeping annotation\" about three that were perfectly readable, and timed the file from\n * zero.\n *\n * A channel with no room carries nothing, so it is not the one the TAL is in.\n */\n get timekeepingSignal(): EdfSignal | undefined {\n return this.annotationSignals.find((signal) => signal.samplesPerRecord > 0);\n }\n\n get annotationSignals(): EdfSignal[] {\n return this.header.signals.filter((s) => s.isAnnotations);\n }\n\n /** Total recording duration in seconds, based on records actually present. */\n get durationSeconds(): number {\n return this.recordCount * this.header.recordDuration;\n }\n\n /** Read a half-open range of records in batches. */\n async *readRecords(options: ReadRecordsOptions = {}): AsyncGenerator<RecordBatch> {\n this.#assertOpen();\n\n /*\n Record bounds have to be whole records.\n\n A fractional `startRecord` was carried straight into `position = headerBytes +\n record * recordBytes`, so reading from 1.5 began half a record in and every sample\n after it was decoded from the wrong offset: on the two-channel test fixture it\n returned channel 2's values under channel 1's signal, with no error. Clamping\n silently would be no better, since a caller asking for record 1.5 has a bug the\n library should name rather than paper over.\n */\n for (const [name, value] of [\n ['startRecord', options.startRecord],\n ['endRecord', options.endRecord],\n ] as const) {\n if (value !== undefined && !Number.isInteger(value)) {\n /*\n An `OptionError`, because it is the call that is wrong and not the recording.\n\n This raised an `EdfError` coded `BAD_HEADER_FIELD` — a code the reference defines\n as \"a field that should contain a number doesn't\", about the file's header — for a\n number the *caller* passed. A script branching on that code to report a corrupt\n recording blamed the recording for its own bug, and `chunkBytes` below did the same\n under `UNREADABLE`, which means the file could not be read.\n\n The same class `EdfFile.open` raises for a path that is not one and `parseHeader`\n for a byte count that is not one, both settled in this same series, and for the\n reason `assertInputPath` gives.\n */\n throw new OptionError(\n /*\n Through `describeValue`, like every other refusal that quotes a rejected value.\n\n Its rule is \"numbers bare, everything else quoted so its type is visible\", and\n these two were the sites that never used it — so a refusal *for not being a\n number* showed the value as one: `readRecords({ startRecord: '1' })` came back\n `startRecord must be a whole record index, got 1.`, where 1 is a whole record\n index and the caller is left looking for what else could be wrong. An array came\n back `got .`, a hole where the value should be, and an object came back\n `got [object Object]` — the string `assertInputPath`'s own docstring names as the\n reason it exists.\n */\n `readRecords: ${name} must be a whole record index, got ${describeValue(value)}. ` +\n 'Record boundaries are the unit the file can be read in; a fractional index ' +\n 'would decode samples from the middle of a record.',\n );\n }\n }\n\n const start = Math.max(0, options.startRecord ?? 0);\n const end = Math.min(this.recordCount, options.endRecord ?? this.recordCount);\n if (start >= end) return;\n\n const { recordBytes } = this.header;\n /*\n Checked rather than handed to Buffer.alloc.\n\n `chunkBytes: NaN` came back as `RangeError: The value of \"size\" is out of range` from\n inside Node, with no mention of the option that caused it — while a fractional\n `startRecord` two lines up gets a typed EdfError naming the field. Every other option\n here is checked; this one reached the allocator.\n */\n const budget = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;\n if (!Number.isFinite(budget) || budget < 1) {\n // `OptionError` for the same reason as the record bounds above; see there.\n throw new OptionError(\n `chunkBytes must be a positive number of bytes, got ${describeValue(options.chunkBytes)}. ` +\n 'It is a ceiling on how much of the file is held at once; one record is read ' +\n 'whatever it says.',\n );\n }\n /*\n The budget is a ceiling, not an amount to reserve.\n\n `Math.floor(budget / recordBytes)` is how many records would fit in it, and the buffer\n was that many — whether or not the file had that many. A 848-byte fixture read with a\n 512 MB budget allocated 536,870,880 bytes for its two records, and every ordinary read\n of a small file reserved the full 8 MB default. Nothing was wrong with the data; the\n memory just had nothing to do with it.\n\n Bounded by what is actually going to be read, so a batch of five hundred short\n recordings costs five hundred short buffers rather than five hundred 8 MB ones.\n */\n const perChunk = Math.max(1, Math.min(Math.floor(budget / recordBytes), end - start));\n const buffer = Buffer.alloc(perChunk * recordBytes);\n\n for (let record = start; record < end; record += perChunk) {\n const count = Math.min(perChunk, end - record);\n const bytes = count * recordBytes;\n const position = this.header.headerBytes + record * recordBytes;\n\n const bytesRead = await readFully(this.#handle, buffer, 0, bytes, position);\n if (bytesRead < bytes) {\n // The file is shorter than its own size said. Quietly stopping here would\n // hand back a conversion missing its tail with nothing to show for it.\n //\n // Through the shared builder rather than a second copy of its sentence: the two were\n // character-for-character identical, which is how a wording fixed in one of them\n // would have been fixed in only one of them.\n throw changedWhileReading(record, bytes, bytesRead);\n }\n\n yield { firstRecordIndex: record, recordCount: count, data: buffer.subarray(0, bytes) };\n }\n }\n\n /**\n * The channel, confirmed to be one of this recording's.\n *\n * The three methods that take an `EdfSignal` turn its `byteOffsetInRecord` and\n * `samplesPerRecord` into a position in a batch of this file's bytes. Nothing said the\n * channel had to come from this file, and a channel from another one reads as though it\n * did: handing `sampleAt` a `.bdf` channel — three bytes a sample, its own offset — while\n * reading a `.edf` batch returned the first EDF channel's samples, `0 74 147 219 290`,\n * every one of them a real number from the recording and none of them the caller's.\n *\n * Two open files is how it arrives. It is also what a plain object gets: `{}` and `42`\n * both have an undefined offset, which the arithmetic below turns into `NaN` and then\n * into a sample of 0.\n *\n * By identity at its own index, not by scanning the list: `sampleAt` is called once per\n * sample, and the caller already holds these objects — `header.signals[i]`, or the subsets\n * `annotationSignals` and `selectChannels` filter out of it, which are the same references.\n */\n #assertSignalHere(signal: EdfSignal, method: string): void {\n if (this.header.signals[(signal as { index?: number } | null)?.index as number] === signal) {\n return;\n }\n /*\n A channel-shaped argument is placed rather than dumped.\n\n `describeValue` renders an object as its JSON, and a channel is fourteen fields — a\n 458-character refusal, most of it the caller's own data handed back. Its index is the\n part that locates the mistake, and it is the field this check just read. Anything that\n is not object-shaped is quoted the ordinary way, since there it is the value itself\n that is wrong.\n */\n const elsewhere =\n `A channel read out of a different file names a position in that file's records, ` +\n `not this one's.`;\n throw new OptionError(\n signal !== null && typeof signal === 'object'\n ? `${method}: signal is a channel object, but not one of this recording's — ` +\n `header.signals at index ${describeValue((signal as { index?: unknown }).index)} ` +\n `is a different channel. ${elsewhere}`\n : `${method}: signal must be one of this recording's own channels, from ` +\n `header.signals — got ${describeValue(signal)}. ${elsewhere}`,\n );\n }\n\n /**\n * The record, confirmed to be one this batch holds.\n *\n * 0.8.62 put this on `sampleAt`, where it turns a position into a sample. `offsetOf` does\n * the same arithmetic and hands the position back, and `annotationBytes` slices at it, and\n * neither asked anything of it:\n *\n * file.offsetOf(batch, -5, signal) // -1300\n * file.offsetOf(batch, 1.5, signal) // 390, half a record in\n * file.annotationBytes(batch, 99, s) // Uint8Array(0)\n *\n * A negative byte position, a position that decodes the second half of one record against\n * the first half of the next — the failure `readRecords` refuses a fractional `startRecord`\n * for — and an empty slice that reads as \"this record carries no annotations\" for a record\n * that is not in the batch at all.\n *\n * The batch is checked first, because the bound is read off it: `offsetOf` never touched\n * `batch` before this, so a caller who passed the wrong thing got no complaint from it.\n */\n #assertRecordOffset(batch: RecordBatch, recordOffset: number, method: string): void {\n if (!Number.isInteger((batch as RecordBatch | null)?.recordCount)) {\n throw new OptionError(\n `${method}: batch must be one of the batches readRecords yields, got ` +\n `${describeValue(batch)}.`,\n );\n }\n if (!Number.isInteger(recordOffset) || recordOffset < 0 || recordOffset >= batch.recordCount) {\n throw new OptionError(\n `${method}: recordOffset must be a record's position within this batch, 0 to ` +\n `${batch.recordCount - 1}, got ${describeValue(recordOffset)}. Absolute record ` +\n `indexes are batch.firstRecordIndex higher.`,\n );\n }\n }\n\n /** Read one sample as its raw digital value. */\n sampleAt(batch: RecordBatch, recordOffset: number, signal: EdfSignal, sampleIndex: number): number {\n this.#assertSignalHere(signal, 'sampleAt');\n /*\n In range, because out of it this invented a number.\n\n The arithmetic below turns four values into a byte position and reads there. Nothing\n stopped that position from landing outside the sample it names. Past the end of the\n buffer, `bytes[position]` is `undefined`, which `| 0` and `<< 8` both turn into 0 — so a\n read past the batch came back as a plausible sample of zero. Inside the buffer but past\n the channel's own samples, it came back as the *next channel's* data: a 256-sample\n channel asked for sample 261 returned 243, which is a real number from the recording\n and belongs to another column.\n\n Both are reachable from the mistake the api page warns about in the sentence that\n describes this method — \"`recordOffset` is the record's position within the batch, from\n 0 to `batch.recordCount - 1`, not its index in the file\". A caller who passes the\n absolute index reads past the batch and gets zeros for every sample of it.\n\n Two integer comparisons each, on a call that then formats a number.\n */\n this.#assertRecordOffset(batch, recordOffset, 'sampleAt');\n if (\n !Number.isInteger(sampleIndex) ||\n sampleIndex < 0 ||\n sampleIndex >= signal.samplesPerRecord\n ) {\n throw new OptionError(\n `sampleIndex must be 0 to ${signal.samplesPerRecord - 1} for this channel, got ` +\n `${describeValue(sampleIndex)}.`,\n );\n }\n const position =\n recordOffset * this.header.recordBytes +\n signal.byteOffsetInRecord +\n sampleIndex * this.header.bytesPerSample;\n\n if (this.header.bytesPerSample === 3) {\n // BDF stores 24-bit little-endian two's complement. Loading the three bytes\n // into the top of a 32-bit word and shifting back down sign-extends them.\n const data = batch.data;\n return (\n ((data[position] as number) << 8) |\n ((data[position + 1] as number) << 16) |\n ((data[position + 2] as number) << 24)\n ) >> 8;\n }\n return readInt16LE(batch.data, position);\n }\n\n /** Byte offset of a signal's samples within a batch. */\n offsetOf(batch: RecordBatch, recordOffset: number, signal: EdfSignal): number {\n this.#assertSignalHere(signal, 'offsetOf');\n this.#assertRecordOffset(batch, recordOffset, 'offsetOf');\n return recordOffset * this.header.recordBytes + signal.byteOffsetInRecord;\n }\n\n /** The annotation channel's raw bytes for one record in a batch. */\n annotationBytes(batch: RecordBatch, recordOffset: number, signal: EdfSignal): Uint8Array {\n // Before delegating, so the refusal names the method the caller called rather than the\n // one underneath it.\n this.#assertSignalHere(signal, 'annotationBytes');\n this.#assertRecordOffset(batch, recordOffset, 'annotationBytes');\n const start = this.offsetOf(batch, recordOffset, signal);\n return batch.data.subarray(start, start + signal.samplesPerRecord * this.header.bytesPerSample);\n }\n\n /**\n * Where this continuous recording begins, from the first record that says.\n *\n * A few records' worth of annotation bytes rather than the whole channel. A continuous\n * recording's origin is the fraction of a second by which its first record follows the\n * header's start time, and `--info` needs that to place a requested window — but it does\n * not need the events, and finding one number by reading every record costs a seek per\n * record across the whole file, which is the scan `--info` was deliberately spared.\n *\n * It reads on past record 0 because a conversion does. This used to stop there, so the\n * moment one timekeeping TAL was unreadable the two disagreed: the conversion took the\n * origin from record 1 and timed the file from 0.5s, while `--info` found nothing at\n * record 0 and reported a recording starting at zero — the same file described two ways by\n * one tool. Records are contiguous, so record `i` beginning at `t` puts the origin at\n * `t - i * duration`, and any one of them settles it.\n *\n * The bound is what keeps this cheap: a file whose first `RECORDS_SEARCHED_FOR_ORIGIN`\n * timekeeping entries are all unreadable reports an origin of zero here, and converting it\n * raises ANNOTATION_DECODE_FAILED for every one of them.\n *\n * That mitigation covers records that could not be read, and not records that said nothing:\n * an empty annotation slot is not a TAL that failed, so nothing is counted and nothing is\n * raised. Twenty records whose only timekeeping entry is in record 16 therefore convert with\n * `time_s` from the origin it states and are reported here as beginning at zero, in silence\n * on both sides — and `--start` and `--end` are read against that same clock. The bound\n * stays, since it is what makes `--info` a header read on a file of any size; what was\n * wrong was the account of what it costs, which every page giving it said was a warning.\n *\n * Returns null when there is nothing to read it from, in which case the origin is zero.\n */\n async readOrigin(): Promise<number | null> {\n return (await this.scanOrigin()).origin;\n }\n\n /**\n * The origin, and what the search saw on the way to it.\n *\n * `--info` takes this route for a continuous recording rather than reading every record,\n * and reported nothing when the timekeeping it read was unreadable: the count was hard-coded\n * to zero at the call site, so a file whose first TAL cannot be parsed raised\n * ANNOTATION_DECODE_FAILED when converted and nothing under `--info`. Its byte-identical\n * EDF+D twin — same bytes but for the reserved field, which has nothing to do with the\n * defect — raised it both ways, because that path reads every record and counts as it goes.\n *\n * The failure was being read and then thrown away. `readOrigin` keeps its shape for callers\n * who only want the number.\n *\n * All three counters, not one. A first-position TAL may carry events after the start time,\n * and when it cannot be parsed those go with it — which is what `malformedTimekeepingWithText`\n * counts and what decides whether the warning says \"No event was lost\" or names the events\n * that were. Counting only the first meant `--info` took the first sentence every time: it\n * announced that a record had lost its position and that nothing else had gone, over a file\n * whose conversion said, correctly, that an event had gone with it. One file, two answers,\n * and the confident one was `--info`, which is the command run first to find out what a\n * conversion will say.\n *\n * `malformed` comes back for the same reason one sentence further on: that hint ends \"and is\n * counted above\", which is only true where the entry warning is printed too.\n *\n * All three are of the records this actually read, which is as far as the first record that\n * states a time — so they are lower bounds on the file, as `malformedTimekeeping` has been\n * since it was returned at all. A conversion reads every record and may count more. What\n * they must not be is inconsistent with each other, which is what a hard-coded zero made\n * them.\n */\n async scanOrigin(): Promise<{\n origin: number | null;\n malformed: number;\n malformedTimekeeping: number;\n malformedTimekeepingWithText: number;\n }> {\n this.#assertOpen();\n\n const counts = { malformed: 0, malformedTimekeeping: 0, malformedTimekeepingWithText: 0 };\n const channel = this.timekeepingSignal;\n if (!channel || this.recordCount === 0) return { origin: null, ...counts };\n\n const { headerBytes, bytesPerSample, recordBytes, recordDuration } = this.header;\n const buffer = Buffer.alloc(channel.samplesPerRecord * bytesPerSample);\n if (buffer.length === 0) return { origin: null, ...counts };\n\n const searched = Math.min(this.recordCount, RECORDS_SEARCHED_FOR_ORIGIN);\n for (let record = 0; record < searched; record++) {\n const offset = headerBytes + record * recordBytes + channel.byteOffsetInRecord;\n const bytesRead = await readFully(this.#handle, buffer, 0, buffer.length, offset);\n if (bytesRead < buffer.length) return { origin: null, ...counts };\n\n const decoded = decodeRecordAnnotations(buffer, record);\n counts.malformed += decoded.malformed;\n counts.malformedTimekeeping += decoded.malformedTimekeeping;\n counts.malformedTimekeepingWithText += decoded.malformedTimekeepingWithText;\n if (decoded.recordStart !== null) {\n return { origin: decoded.recordStart - record * recordDuration, ...counts };\n }\n }\n return { origin: null, ...counts };\n }\n\n /**\n * Read every EDF+ annotation in the file, plus the start time each record declares.\n *\n * Only the annotation channel is read, seeking straight to it inside each record\n * rather than pulling whole records through memory. On a multi-gigabyte recording\n * that is the difference between a few kilobytes of I/O and all of it.\n *\n * The whole file is always scanned, never just the records inside a requested\n * window: writers are not obliged to store an annotation in the record its onset\n * falls in, and some put every annotation in the first record. Reading only the\n * window's records would drop those entirely.\n */\n async readAnnotations(): Promise<{\n annotations: Annotation[];\n recordStarts: (number | null)[];\n malformed: number;\n /** Unreadable TALs in first position, which carry timing rather than an event. */\n malformedTimekeeping: number;\n /** How many of those also carried event text, so events were lost with the position. */\n malformedTimekeepingWithText: number;\n /** Events kept whose stated duration could not be read; see Annotation.duration. */\n unreadableDurations: number;\n /** Events kept whose stated duration read as a number below zero. */\n negativeDurations: number;\n }> {\n this.#assertOpen();\n\n const annotations: Annotation[] = [];\n const recordStarts: (number | null)[] = new Array<number | null>(this.recordCount).fill(null);\n let malformed = 0;\n let malformedTimekeeping = 0;\n let malformedTimekeepingWithText = 0;\n let unreadableDurations = 0;\n let negativeDurations = 0;\n\n const channels = this.annotationSignals;\n if (channels.length === 0) {\n return {\n annotations,\n recordStarts,\n malformed,\n malformedTimekeeping,\n malformedTimekeepingWithText,\n unreadableDurations,\n negativeDurations,\n };\n }\n\n const { headerBytes, recordBytes, bytesPerSample } = this.header;\n const buffers = channels.map((c) => Buffer.alloc(c.samplesPerRecord * bytesPerSample));\n const timekeeping = this.timekeepingSignal;\n\n for (let record = 0; record < this.recordCount; record++) {\n for (const [position, channel] of channels.entries()) {\n const buffer = buffers[position];\n if (!buffer || buffer.length === 0) continue;\n\n const offset = headerBytes + record * recordBytes + channel.byteOffsetInRecord;\n const bytesRead = await readFully(this.#handle, buffer, 0, buffer.length, offset);\n if (bytesRead < buffer.length) {\n throw changedWhileReading(record, buffer.length, bytesRead, 'annotation data');\n }\n\n // Only the timekeeping channel carries the record's start; see timekeepingSignal.\n const decoded = decodeRecordAnnotations(buffer, record, channel === timekeeping);\n if (channel === timekeeping) recordStarts[record] = decoded.recordStart;\n for (const annotation of decoded.annotations) annotations.push(annotation);\n malformed += decoded.malformed;\n malformedTimekeeping += decoded.malformedTimekeeping;\n malformedTimekeepingWithText += decoded.malformedTimekeepingWithText;\n unreadableDurations += decoded.unreadableDurations;\n negativeDurations += decoded.negativeDurations;\n }\n }\n\n annotations.sort((a, b) => a.onset - b.onset || a.recordIndex - b.recordIndex);\n return {\n annotations,\n recordStarts,\n malformed,\n malformedTimekeeping,\n malformedTimekeepingWithText,\n unreadableDurations,\n negativeDurations,\n };\n }\n\n async close(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n await this.#handle.close();\n }\n\n #assertOpen(): void {\n if (this.#closed) {\n throw new EdfError(\n 'UNREADABLE',\n 'This EDF file has already been closed.',\n // The same advice `changedSinceOpen` gives for the same mistake, which is the only\n // other method that has anything to say about a closed file.\n 'Open it again, or keep it open until the last read.',\n );\n }\n }\n}\n\nfunction describe(cause: unknown): string {\n if (cause instanceof Error) {\n const code = (cause as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return 'no such file';\n // EPERM beside EACCES, because everywhere else in this codebase that reads an errno pairs\n // the two, and ENOTDIR because a path that runs through a regular file — `rec.edf/inner`,\n // which a shell completes and a script builds by joining — is otherwise the one input\n // failure that answers in errno text while its output-side twin answers in a sentence.\n if (code === 'EACCES' || code === 'EPERM') return 'permission denied';\n if (code === 'ENOTDIR') return 'part of the path is a file, not a directory';\n return cause.message;\n }\n return String(cause);\n}\n\n/**\n * The most `fs.read` will accept as a length.\n *\n * Node asserts on a length that does not fit in a signed 32-bit integer, and it asserts in\n * C++: `Assertion failed: args[3]->IsInt32()`, forty frames of native stack, SIGABRT. Not an\n * exception — nothing in JavaScript sees it, so no catch block and no `uncaughtException`\n * handler runs, and a library consumer's whole process goes down with it.\n *\n * A round gigabyte rather than the exact limit, so the loop below does whole even reads.\n */\nconst MAX_READ_BYTES = 1024 * 1024 * 1024;\n\n/** Fill a requested region unless EOF is reached; regular-file reads may legally be short. */\nasync function readFully(\n handle: FileHandle,\n buffer: Buffer,\n offset: number,\n length: number,\n position: number,\n): Promise<number> {\n let total = 0;\n while (total < length) {\n /*\n Capped, because one data record can be larger than a single read may be.\n\n A record is read in one call when it exceeds the chunk budget — there is nothing\n smaller to divide it by, since a record is the unit the format is addressed in. EDF's\n samples-per-record field is 8 characters, so eleven channels at 99,999,999 samples make\n a record of 2.2 GB, and a long record duration at ordinary rates gets there too. That\n went to `fs.read` as a single length over 2^31-1 and took the process out with a native\n assertion rather than an error.\n\n Looping was already how a short read is handled, so the cap costs one more iteration\n per gigabyte and nothing else.\n */\n const want = Math.min(length - total, MAX_READ_BYTES);\n const { bytesRead } = await handle.read(buffer, offset + total, want, position + total);\n if (bytesRead === 0) break;\n total += bytesRead;\n }\n return total;\n}\n\nfunction changedWhileReading(\n record: number,\n expected: number,\n actual: number,\n subject = 'data',\n): EdfError {\n return new EdfError(\n 'UNREADABLE',\n `Expected ${grouped(expected)} bytes of ${subject} at record ${record} but only ` +\n `${counted(actual, 'byte')} ${actual === 1 ? 'was' : 'were'} available; the file appears ` +\n `to have changed size while it was being read.`,\n 'Make sure the recording is not still being written to, then try again.',\n );\n}\n"]}
1
+ {"version":3,"file":"reader.js","sourceRoot":"","sources":["../../src/edf/reader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Chunked reader for EDF / EDF+ files.\n *\n * Data records are read in batches sized by a byte budget rather than all at once,\n * so peak memory stays flat regardless of how long the recording is. A 4 GB file\n * and a 4 MB file use the same working set.\n */\n\nimport { open, stat } from 'node:fs/promises';\nimport type { FileHandle } from 'node:fs/promises';\n\nimport { createHash } from 'node:crypto';\n\nimport { EdfError } from './errors.js';\nimport type { Diagnostic } from './errors.js';\nimport { FIXED_HEADER_BYTES, SIGNAL_HEADER_BYTES, parseHeader, peekSignalCount } from './header.js';\nimport type { EdfHeader, EdfSignal } from './header.js';\nimport { decodeRecordAnnotations } from './annotations.js';\nimport type { Annotation } from './annotations.js';\nimport { readInt16LE } from './bytes.js';\nimport { counted, grouped } from '../format/list.js';\n// Crossing into convert/ as header.ts already does for `typeable`: the check belongs to the\n// call rather than to the file, and there is one of it.\nimport { OptionError, assertInputPath, describeValue } from '../convert/options.js';\n// A path comes out of the filesystem and nobody vets it; see printable's own comment.\nimport { printable } from '../format/unprintable.js';\n\n/**\n * How far `readOrigin` looks for a record that states its own start time.\n *\n * Enough that one or two unreadable timekeeping entries at the top of a file cost nothing,\n * few enough that `--info` stays a header read rather than a scan.\n */\nconst RECORDS_SEARCHED_FOR_ORIGIN = 16;\n\n/** Default read budget per batch. Large enough to amortise syscalls, small enough to stay cheap. */\nexport const DEFAULT_CHUNK_BYTES = 8 * 1024 * 1024;\n\nexport interface RecordBatch {\n /** Index of the first record in this batch, relative to the whole file. */\n firstRecordIndex: number;\n recordCount: number;\n /**\n * Raw record bytes, `recordCount * header.recordBytes` long.\n *\n * The buffer is reused between iterations. Copy anything you need to keep past\n * the current loop turn.\n */\n data: Uint8Array;\n}\n\nexport interface ReadRecordsOptions {\n /** First record to read, inclusive. Defaults to 0. */\n startRecord?: number;\n /** Last record to read, exclusive. Defaults to the file's record count. */\n endRecord?: number;\n chunkBytes?: number;\n}\n\nexport class EdfFile {\n readonly path: string;\n readonly fileSize: number;\n /**\n * Last-modified time when this file was opened, in milliseconds, for the same reason as\n * `fileSize`.\n *\n * Kept as the raw number rather than a Date because `new Date(ms).getTime()` truncates to\n * whole milliseconds: comparing that against a later `fstat`, which carries the\n * filesystem's sub-millisecond precision, reported every undisturbed conversion as one\n * whose input had changed underneath it.\n */\n readonly modifiedAtOpenMs: number;\n readonly header: EdfHeader;\n /** Records actually present in the file, which may differ from the header's claim. */\n readonly recordCount: number;\n readonly trailingBytes: number;\n readonly diagnostics: Diagnostic[];\n\n #handle: FileHandle;\n #closed = false;\n /** The last answer `changedSinceOpen` computed, so it survives the file being closed. */\n #changed: boolean | null = null;\n\n private constructor(init: {\n path: string;\n fileSize: number;\n modifiedAtOpenMs: number;\n header: EdfHeader;\n recordCount: number;\n trailingBytes: number;\n diagnostics: Diagnostic[];\n handle: FileHandle;\n }) {\n this.path = init.path;\n this.fileSize = init.fileSize;\n this.modifiedAtOpenMs = init.modifiedAtOpenMs;\n this.header = init.header;\n this.recordCount = init.recordCount;\n this.trailingBytes = init.trailingBytes;\n this.diagnostics = init.diagnostics;\n this.#handle = init.handle;\n }\n\n /**\n * SHA-256 of the bytes this conversion actually read.\n *\n * Hashed through the open descriptor, over exactly the `fileSize` bytes that were there\n * when the file was opened — the same number every record count and window in the output\n * was derived from. Re-opening the path to hash it afterwards described whatever was at\n * that name by then: a recording still being written grew from 2,000 records to 3,000\n * mid-conversion and metadata.json recorded `data_records: 2000` beside the checksum and\n * byte count of the 3,000-record file, which is provenance for bytes nobody converted.\n * Replacing the file at that path did the same thing more completely.\n */\n async sha256(): Promise<string> {\n this.#assertOpen();\n const hash = createHash('sha256');\n const buffer = Buffer.alloc(Math.min(this.fileSize, 4 * 1024 * 1024) || 1);\n for (let at = 0; at < this.fileSize; ) {\n const want = Math.min(buffer.length, this.fileSize - at);\n const { bytesRead } = await this.#handle.read(buffer, 0, want, at);\n if (bytesRead <= 0) {\n throw new EdfError(\n 'UNREADABLE',\n // Both figures grouped, like the shortfall message this file raises one function\n // over — `Expected 8,386,560 bytes of data at record 0 but only 2,899,456 bytes\n // were available` — and for the reason 0.8.5 gives: the sentence exists to put one\n // against the other, and at nine digits that is work the separators do.\n `Expected ${grouped(this.fileSize)} bytes to checksum but the file ended at ` +\n `${grouped(at)}; it appears to have changed size while it was being read.`,\n 'Make sure the recording is not still being written to, then try again.',\n );\n }\n hash.update(buffer.subarray(0, bytesRead));\n at += bytesRead;\n }\n return hash.digest('hex');\n }\n\n /**\n * Whether the file has changed since it was opened, by size or by modification time.\n *\n * Checked through the descriptor, so it answers for the bytes that were read rather than\n * for whatever now answers to the same name. A recording still being written is the\n * ordinary cause, and the conversion is still correct for the data it saw — it is the\n * claim that the output describes the file as it now stands that stops being true.\n */\n async changedSinceOpen(): Promise<boolean> {\n /*\n A closed file remembers its last answer rather than inventing a new one.\n\n Returning false once closed asserted \"it did not change\", which is not something a\n closed descriptor can know — and `convert()` closes the file before it returns, so\n `result.file.changedSinceOpen()` denied the very change the INPUT_CHANGED diagnostic\n in the same result object had just reported. One object, two answers.\n\n `convert()` always asks before closing, so the cached answer is the true one. A caller\n who closed the file without ever asking gets an error, which is the same treatment\n every other method on a closed file gets.\n */\n if (this.#closed) {\n if (this.#changed !== null) return this.#changed;\n throw new EdfError(\n 'UNREADABLE',\n `\"${this.path}\" is closed, and whether it changed while it was open was never checked.`,\n 'Ask before closing the file. A ConvertResult carries the answer already, since ' +\n 'convert() checks it on the way out.',\n );\n }\n const now = await this.#handle.stat().catch(() => null);\n if (now === null) return this.#changed ?? false;\n this.#changed = now.size !== this.fileSize || now.mtimeMs !== this.modifiedAtOpenMs;\n return this.#changed;\n }\n\n /*\n A sentence, and advice under it, like the destination-side twin.\n\n `Cannot read \"rec.edf\": no such file` was the one diagnostic this tool prints that does not\n end in a full stop — 68 of its 69 do — and the only member of its family with nothing\n indented under it. `Cannot create \"out\": part of the path does not exist.` has carried\n advice under it since the destination errors were given sentences — one line for every\n cause until 0.8.12, and the cause's own since — the mid-conversion UNREADABLE beside it\n carries one too, and this is the form a mistyped path actually reaches.\n */\n static readonly #UNREADABLE_HINT =\n 'Check the path is spelled the way it is on disk and that you have permission to read it.';\n\n static async open(path: string): Promise<EdfFile> {\n /*\n A path, checked before `fs` is asked about it.\n\n `assertInputPath` was written for exactly this and applied one level up. Its docstring\n names the function it was describing — \"`EdfFile.open` hands whatever it is given to\n `fs`, and the refusal comes back as an `EdfError` coded `UNREADABLE`, hinted 'Check the\n path is spelled the way it is on disk and that you have permission to read it' — advice\n about a path, over a value that is not one, filed as a problem with the recording rather\n than with the call\" — and then went into `convert`, leaving `EdfFile.open` itself, which\n is exported from the package root and is how the api page says to read a header without\n converting anything, doing the thing being described:\n\n EdfFile.open({ path: 'a.edf' })\n EdfError[UNREADABLE]: Cannot read \"[object Object]\": The \"path\" argument must be of\n type string or an instance of Buffer or URL. Received an instance of Object.\n\n Node's own argument-type text, under a hint about spelling and permissions, over a\n value that has neither. `EdfFile.open(['a.edf', 'b.edf'])` was worse: it answered\n `Cannot read \"a.edf,b.edf\"`, quoting a path the caller never wrote, because `String` of\n an array joins it with commas.\n\n The same `OptionError` `convert` raises for the same mistake, so one mistake has one\n answer whichever entry point it arrives at.\n */\n assertInputPath(path);\n const info = await stat(path).catch((cause: unknown) => {\n throw new EdfError(\n 'UNREADABLE',\n `Cannot read \"${printable(path)}\": ${describe(cause)}.`,\n EdfFile.#UNREADABLE_HINT,\n );\n });\n /*\n Both of these said what was wrong and nothing about what to do, which is the gap\n `#UNREADABLE_HINT` two lines up was added to close for the third member of this family.\n\n Neither is reached from the command line, and that is the point: a folder there is\n expanded to the recordings inside it and a socket is filtered out by the walk, so the\n caller who arrives here is holding a path in code and has no walk behind them.\n */\n if (info.isDirectory()) {\n throw new EdfError(\n 'UNREADABLE',\n `\"${printable(path)}\" is a directory, not an EDF file.`,\n 'Name a recording inside it. The command line expands a folder to the recordings it ' +\n 'holds; this takes one file.',\n );\n }\n if (!info.isFile()) {\n throw new EdfError(\n 'UNREADABLE',\n `\"${printable(path)}\" is not a regular file.`,\n 'A pipe, socket or device cannot be read as a recording: the parser seeks to a byte ' +\n 'offset inside the file, which only a real file supports.',\n );\n }\n\n /*\n Opening is a second chance to be refused, and it was the one that got through.\n\n `stat` needs the parent directory searchable and says nothing about the file's own mode,\n so a recording with no read permission passes it and fails here — the commonest\n permission failure there is. Unwrapped, it escaped as Node's own error: the CLI printed\n `error: EACCES: permission denied, open '...'` where every neighbouring failure prints\n the tool's sentence, and the library threw a plain Error whose `code` was the errno.\n\n api.md says `UNREADABLE` \"covers a missing file, a directory passed where a file was\n expected, a permission failure, and a file that changed size while being read. Branch on\n `code`, never on the message text.\" A consumer doing exactly that fell through to its\n generic handler.\n */\n const handle = await open(path, 'r').catch((cause: unknown) => {\n throw new EdfError(\n 'UNREADABLE',\n `Cannot read \"${printable(path)}\": ${describe(cause)}.`,\n EdfFile.#UNREADABLE_HINT,\n );\n });\n try {\n const fixed = Buffer.alloc(Math.min(FIXED_HEADER_BYTES, info.size));\n if (fixed.length > 0) {\n const bytesRead = await readFully(handle, fixed, 0, fixed.length, 0);\n if (bytesRead < fixed.length) throw changedWhileReading(0, fixed.length, bytesRead);\n }\n\n // The signal count decides how much more header there is to read. Read by the header\n // parser itself, so the two cannot disagree about which files are readable: this used\n // to have its own Number(), which tolerated the NUL padding sloppy writers emit but\n // not the comma decimal separator that COMMA_DECIMAL exists to accept.\n let headerBuffer = fixed;\n if (fixed.length === FIXED_HEADER_BYTES) {\n const ns = peekSignalCount(fixed);\n if (ns !== null) {\n const total = FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES;\n if (total <= info.size) {\n headerBuffer = Buffer.alloc(total);\n const bytesRead = await readFully(handle, headerBuffer, 0, total, 0);\n if (bytesRead < total) throw changedWhileReading(0, total, bytesRead);\n }\n }\n }\n\n const { header, recordCount, trailingBytes, diagnostics } = parseHeader(\n headerBuffer,\n info.size,\n );\n\n return new EdfFile({\n path,\n fileSize: info.size,\n modifiedAtOpenMs: info.mtimeMs,\n header,\n recordCount,\n trailingBytes,\n diagnostics,\n handle,\n });\n } catch (error) {\n await handle.close().catch(() => {});\n throw error;\n }\n }\n\n /** Signal channels, excluding the EDF+ annotations channel. */\n get dataSignals(): EdfSignal[] {\n return this.header.signals.filter((s) => !s.isAnnotations);\n }\n\n /**\n * The annotation channel a record's start time is read from.\n *\n * EDF+ puts the timekeeping TAL first in the first annotation channel, and this was read as\n * `annotationSignals[0]` — the first one declared, whether or not it can hold anything. A\n * writer that declares an annotation channel and gives it zero samples per record leaves a\n * slot of zero bytes, so nothing was read from it, and the timekeeping in the channel after\n * it went unread: a three-record EDF+D reported \"3 of 3 data records carry no readable\n * timekeeping annotation\" about three that were perfectly readable, and timed the file from\n * zero.\n *\n * A channel with no room carries nothing, so it is not the one the TAL is in.\n */\n get timekeepingSignal(): EdfSignal | undefined {\n return this.annotationSignals.find((signal) => signal.samplesPerRecord > 0);\n }\n\n get annotationSignals(): EdfSignal[] {\n return this.header.signals.filter((s) => s.isAnnotations);\n }\n\n /** Total recording duration in seconds, based on records actually present. */\n get durationSeconds(): number {\n return this.recordCount * this.header.recordDuration;\n }\n\n /** Read a half-open range of records in batches. */\n async *readRecords(options: ReadRecordsOptions = {}): AsyncGenerator<RecordBatch> {\n this.#assertOpen();\n\n /*\n Record bounds have to be whole records.\n\n A fractional `startRecord` was carried straight into `position = headerBytes +\n record * recordBytes`, so reading from 1.5 began half a record in and every sample\n after it was decoded from the wrong offset: on the two-channel test fixture it\n returned channel 2's values under channel 1's signal, with no error. Clamping\n silently would be no better, since a caller asking for record 1.5 has a bug the\n library should name rather than paper over.\n */\n for (const [name, value] of [\n ['startRecord', options.startRecord],\n ['endRecord', options.endRecord],\n ] as const) {\n if (value !== undefined && !Number.isInteger(value)) {\n /*\n An `OptionError`, because it is the call that is wrong and not the recording.\n\n This raised an `EdfError` coded `BAD_HEADER_FIELD` — a code the reference defines\n as \"a field that should contain a number doesn't\", about the file's header — for a\n number the *caller* passed. A script branching on that code to report a corrupt\n recording blamed the recording for its own bug, and `chunkBytes` below did the same\n under `UNREADABLE`, which means the file could not be read.\n\n The same class `EdfFile.open` raises for a path that is not one and `parseHeader`\n for a byte count that is not one, both settled in this same series, and for the\n reason `assertInputPath` gives.\n */\n throw new OptionError(\n /*\n Through `describeValue`, like every other refusal that quotes a rejected value.\n\n Its rule is \"numbers bare, everything else quoted so its type is visible\", and\n these two were the sites that never used it — so a refusal *for not being a\n number* showed the value as one: `readRecords({ startRecord: '1' })` came back\n `startRecord must be a whole record index, got 1.`, where 1 is a whole record\n index and the caller is left looking for what else could be wrong. An array came\n back `got .`, a hole where the value should be, and an object came back\n `got [object Object]` — the string `assertInputPath`'s own docstring names as the\n reason it exists.\n */\n `readRecords: ${name} must be a whole record index, got ${describeValue(value)}. ` +\n 'Record boundaries are the unit the file can be read in; a fractional index ' +\n 'would decode samples from the middle of a record.',\n );\n }\n }\n\n const start = Math.max(0, options.startRecord ?? 0);\n const end = Math.min(this.recordCount, options.endRecord ?? this.recordCount);\n if (start >= end) return;\n\n const { recordBytes } = this.header;\n /*\n Checked rather than handed to Buffer.alloc.\n\n `chunkBytes: NaN` came back as `RangeError: The value of \"size\" is out of range` from\n inside Node, with no mention of the option that caused it — while a fractional\n `startRecord` two lines up gets a typed EdfError naming the field. Every other option\n here is checked; this one reached the allocator.\n */\n const budget = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;\n if (!Number.isFinite(budget) || budget < 1) {\n // `OptionError` for the same reason as the record bounds above; see there.\n throw new OptionError(\n `chunkBytes must be a positive number of bytes, got ${describeValue(options.chunkBytes)}. ` +\n 'It is a ceiling on how much of the file is held at once; one record is read ' +\n 'whatever it says.',\n );\n }\n /*\n The budget is a ceiling, not an amount to reserve.\n\n `Math.floor(budget / recordBytes)` is how many records would fit in it, and the buffer\n was that many — whether or not the file had that many. A 848-byte fixture read with a\n 512 MB budget allocated 536,870,880 bytes for its two records, and every ordinary read\n of a small file reserved the full 8 MB default. Nothing was wrong with the data; the\n memory just had nothing to do with it.\n\n Bounded by what is actually going to be read, so a batch of five hundred short\n recordings costs five hundred short buffers rather than five hundred 8 MB ones.\n */\n const perChunk = Math.max(1, Math.min(Math.floor(budget / recordBytes), end - start));\n const buffer = Buffer.alloc(perChunk * recordBytes);\n\n for (let record = start; record < end; record += perChunk) {\n const count = Math.min(perChunk, end - record);\n const bytes = count * recordBytes;\n const position = this.header.headerBytes + record * recordBytes;\n\n const bytesRead = await readFully(this.#handle, buffer, 0, bytes, position);\n if (bytesRead < bytes) {\n // The file is shorter than its own size said. Quietly stopping here would\n // hand back a conversion missing its tail with nothing to show for it.\n //\n // Through the shared builder rather than a second copy of its sentence: the two were\n // character-for-character identical, which is how a wording fixed in one of them\n // would have been fixed in only one of them.\n throw changedWhileReading(record, bytes, bytesRead);\n }\n\n yield { firstRecordIndex: record, recordCount: count, data: buffer.subarray(0, bytes) };\n }\n }\n\n /**\n * The channel, confirmed to be one of this recording's.\n *\n * The three methods that take an `EdfSignal` turn its `byteOffsetInRecord` and\n * `samplesPerRecord` into a position in a batch of this file's bytes. Nothing said the\n * channel had to come from this file, and a channel from another one reads as though it\n * did: handing `sampleAt` a `.bdf` channel — three bytes a sample, its own offset — while\n * reading a `.edf` batch returned the first EDF channel's samples, `0 74 147 219 290`,\n * every one of them a real number from the recording and none of them the caller's.\n *\n * Two open files is how it arrives. It is also what a plain object gets: `{}` and `42`\n * both have an undefined offset, which the arithmetic below turns into `NaN` and then\n * into a sample of 0.\n *\n * By identity at its own index, not by scanning the list: `sampleAt` is called once per\n * sample, and the caller already holds these objects — `header.signals[i]`, or the subsets\n * `annotationSignals` and `selectChannels` filter out of it, which are the same references.\n */\n #assertSignalHere(signal: EdfSignal, method: string): void {\n if (this.header.signals[(signal as { index?: number } | null)?.index as number] === signal) {\n return;\n }\n /*\n A channel-shaped argument is placed rather than dumped.\n\n `describeValue` renders an object as its JSON, and a channel is fourteen fields — a\n 458-character refusal, most of it the caller's own data handed back. Its index is the\n part that locates the mistake, and it is the field this check just read. Anything that\n is not object-shaped is quoted the ordinary way, since there it is the value itself\n that is wrong.\n */\n const elsewhere =\n `A channel read out of a different file names a position in that file's records, ` +\n `not this one's.`;\n throw new OptionError(\n signal !== null && typeof signal === 'object'\n ? `${method}: signal is a channel object, but not one of this recording's — ` +\n `header.signals at index ${describeValue((signal as { index?: unknown }).index)} ` +\n `is a different channel. ${elsewhere}`\n : `${method}: signal must be one of this recording's own channels, from ` +\n `header.signals — got ${describeValue(signal)}. ${elsewhere}`,\n );\n }\n\n /**\n * The record, confirmed to be one this batch holds.\n *\n * 0.8.62 put this on `sampleAt`, where it turns a position into a sample. `offsetOf` does\n * the same arithmetic and hands the position back, and `annotationBytes` slices at it, and\n * neither asked anything of it:\n *\n * file.offsetOf(batch, -5, signal) // -1300\n * file.offsetOf(batch, 1.5, signal) // 390, half a record in\n * file.annotationBytes(batch, 99, s) // Uint8Array(0)\n *\n * A negative byte position, a position that decodes the second half of one record against\n * the first half of the next — the failure `readRecords` refuses a fractional `startRecord`\n * for — and an empty slice that reads as \"this record carries no annotations\" for a record\n * that is not in the batch at all.\n *\n * The batch is checked first, because the bound is read off it: `offsetOf` never touched\n * `batch` before this, so a caller who passed the wrong thing got no complaint from it. Its\n * bytes are checked with it, for the reason given where that check sits.\n */\n #assertRecordOffset(batch: RecordBatch, recordOffset: number, method: string): void {\n if (!Number.isInteger((batch as RecordBatch | null)?.recordCount)) {\n throw new OptionError(\n `${method}: batch must be one of the batches readRecords yields, got ` +\n `${describeValue(batch)}.`,\n );\n }\n /*\n And the bytes, which are what the position is a position into.\n\n The count above was the whole of what this asked, and all three methods go on to read\n `batch.data`: `sampleAt` indexes it, `annotationBytes` slices it, and `offsetOf` hands\n back a position for it. A batch-shaped object without any failed differently depending\n on which one was called, and two of those failures were answers:\n\n file.sampleAt({ recordCount: 2, data: [1, 2, 3, 4] }, 0, signal, 0) // 513\n file.sampleAt({ recordCount: 2, data: new Float64Array(64) }, ...) // 0\n file.annotationBytes({ recordCount: 2, data: new Float64Array(8) }, …) // 20 values\n\n 513 is a digital code this recording could have held; 0 is the commonest sample in any\n recording; and the third is a run of numbers that are not the bytes of anything,\n returned as the annotation channel's own. `ArrayBuffer.isView` is true of all of them\n and of a `DataView`, which is why the check is the one 0.8.84 and 0.8.85 settled on for\n the two other places this parser is handed bytes: a view whose elements are one byte.\n */\n const bytes = (batch as { data?: unknown }).data;\n if (!ArrayBuffer.isView(bytes) || (bytes as { BYTES_PER_ELEMENT?: number }).BYTES_PER_ELEMENT !== 1) {\n const carries = 'A batch carries firstRecordIndex, recordCount, and the record bytes themselves.';\n // Named rather than quoted back: a batch is megabytes, and the kind of thing it is is\n // the part that locates the mistake — the same reasoning `#assertSignalHere` gives for\n // placing a channel by its index instead of printing its fourteen fields. The article\n // is worked out because `Array` and `Int16Array` both arrive here and \"a Array\" is not\n // a sentence.\n const kind =\n typeof bytes === 'object' && bytes !== null\n ? ((bytes as object).constructor?.name ?? 'object')\n : null;\n throw new OptionError(\n kind !== null\n ? `${method}: batch.data is ${/^[AEIOU]/u.test(kind) ? 'an' : 'a'} ${kind}, which is ` +\n `not a view of bytes. ${carries}`\n : `${method}: batch.data must be the record bytes, got ${describeValue(bytes)}. ${carries}`,\n );\n }\n if (!Number.isInteger(recordOffset) || recordOffset < 0 || recordOffset >= batch.recordCount) {\n throw new OptionError(\n `${method}: recordOffset must be a record's position within this batch, 0 to ` +\n `${batch.recordCount - 1}, got ${describeValue(recordOffset)}. Absolute record ` +\n `indexes are batch.firstRecordIndex higher.`,\n );\n }\n }\n\n /** Read one sample as its raw digital value. */\n sampleAt(batch: RecordBatch, recordOffset: number, signal: EdfSignal, sampleIndex: number): number {\n this.#assertSignalHere(signal, 'sampleAt');\n /*\n In range, because out of it this invented a number.\n\n The arithmetic below turns four values into a byte position and reads there. Nothing\n stopped that position from landing outside the sample it names. Past the end of the\n buffer, `bytes[position]` is `undefined`, which `| 0` and `<< 8` both turn into 0 — so a\n read past the batch came back as a plausible sample of zero. Inside the buffer but past\n the channel's own samples, it came back as the *next channel's* data: a 256-sample\n channel asked for sample 261 returned 243, which is a real number from the recording\n and belongs to another column.\n\n Both are reachable from the mistake the api page warns about in the sentence that\n describes this method — \"`recordOffset` is the record's position within the batch, from\n 0 to `batch.recordCount - 1`, not its index in the file\". A caller who passes the\n absolute index reads past the batch and gets zeros for every sample of it.\n\n Two integer comparisons each, on a call that then formats a number.\n */\n this.#assertRecordOffset(batch, recordOffset, 'sampleAt');\n if (\n !Number.isInteger(sampleIndex) ||\n sampleIndex < 0 ||\n sampleIndex >= signal.samplesPerRecord\n ) {\n throw new OptionError(\n `sampleIndex must be 0 to ${signal.samplesPerRecord - 1} for this channel, got ` +\n `${describeValue(sampleIndex)}.`,\n );\n }\n const position =\n recordOffset * this.header.recordBytes +\n signal.byteOffsetInRecord +\n sampleIndex * this.header.bytesPerSample;\n\n if (this.header.bytesPerSample === 3) {\n // BDF stores 24-bit little-endian two's complement. Loading the three bytes\n // into the top of a 32-bit word and shifting back down sign-extends them.\n const data = batch.data;\n return (\n ((data[position] as number) << 8) |\n ((data[position + 1] as number) << 16) |\n ((data[position + 2] as number) << 24)\n ) >> 8;\n }\n return readInt16LE(batch.data, position);\n }\n\n /** Byte offset of a signal's samples within a batch. */\n offsetOf(batch: RecordBatch, recordOffset: number, signal: EdfSignal): number {\n this.#assertSignalHere(signal, 'offsetOf');\n this.#assertRecordOffset(batch, recordOffset, 'offsetOf');\n return recordOffset * this.header.recordBytes + signal.byteOffsetInRecord;\n }\n\n /** The annotation channel's raw bytes for one record in a batch. */\n annotationBytes(batch: RecordBatch, recordOffset: number, signal: EdfSignal): Uint8Array {\n // Before delegating, so the refusal names the method the caller called rather than the\n // one underneath it.\n this.#assertSignalHere(signal, 'annotationBytes');\n this.#assertRecordOffset(batch, recordOffset, 'annotationBytes');\n const start = this.offsetOf(batch, recordOffset, signal);\n return batch.data.subarray(start, start + signal.samplesPerRecord * this.header.bytesPerSample);\n }\n\n /**\n * Where this continuous recording begins, from the first record that says.\n *\n * A few records' worth of annotation bytes rather than the whole channel. A continuous\n * recording's origin is the fraction of a second by which its first record follows the\n * header's start time, and `--info` needs that to place a requested window — but it does\n * not need the events, and finding one number by reading every record costs a seek per\n * record across the whole file, which is the scan `--info` was deliberately spared.\n *\n * It reads on past record 0 because a conversion does. This used to stop there, so the\n * moment one timekeeping TAL was unreadable the two disagreed: the conversion took the\n * origin from record 1 and timed the file from 0.5s, while `--info` found nothing at\n * record 0 and reported a recording starting at zero — the same file described two ways by\n * one tool. Records are contiguous, so record `i` beginning at `t` puts the origin at\n * `t - i * duration`, and any one of them settles it.\n *\n * The bound is what keeps this cheap: a file whose first `RECORDS_SEARCHED_FOR_ORIGIN`\n * timekeeping entries are all unreadable reports an origin of zero here, and converting it\n * raises ANNOTATION_DECODE_FAILED for every one of them.\n *\n * That mitigation covers records that could not be read, and not records that said nothing:\n * an empty annotation slot is not a TAL that failed, so nothing is counted and nothing is\n * raised. Twenty records whose only timekeeping entry is in record 16 therefore convert with\n * `time_s` from the origin it states and are reported here as beginning at zero, in silence\n * on both sides — and `--start` and `--end` are read against that same clock. The bound\n * stays, since it is what makes `--info` a header read on a file of any size; what was\n * wrong was the account of what it costs, which every page giving it said was a warning.\n *\n * Returns null when there is nothing to read it from, in which case the origin is zero.\n */\n async readOrigin(): Promise<number | null> {\n return (await this.scanOrigin()).origin;\n }\n\n /**\n * The origin, and what the search saw on the way to it.\n *\n * `--info` takes this route for a continuous recording rather than reading every record,\n * and reported nothing when the timekeeping it read was unreadable: the count was hard-coded\n * to zero at the call site, so a file whose first TAL cannot be parsed raised\n * ANNOTATION_DECODE_FAILED when converted and nothing under `--info`. Its byte-identical\n * EDF+D twin — same bytes but for the reserved field, which has nothing to do with the\n * defect — raised it both ways, because that path reads every record and counts as it goes.\n *\n * The failure was being read and then thrown away. `readOrigin` keeps its shape for callers\n * who only want the number.\n *\n * All three counters, not one. A first-position TAL may carry events after the start time,\n * and when it cannot be parsed those go with it — which is what `malformedTimekeepingWithText`\n * counts and what decides whether the warning says \"No event was lost\" or names the events\n * that were. Counting only the first meant `--info` took the first sentence every time: it\n * announced that a record had lost its position and that nothing else had gone, over a file\n * whose conversion said, correctly, that an event had gone with it. One file, two answers,\n * and the confident one was `--info`, which is the command run first to find out what a\n * conversion will say.\n *\n * `malformed` comes back for the same reason one sentence further on: that hint ends \"and is\n * counted above\", which is only true where the entry warning is printed too.\n *\n * All three are of the records this actually read, which is as far as the first record that\n * states a time — so they are lower bounds on the file, as `malformedTimekeeping` has been\n * since it was returned at all. A conversion reads every record and may count more. What\n * they must not be is inconsistent with each other, which is what a hard-coded zero made\n * them.\n */\n async scanOrigin(): Promise<{\n origin: number | null;\n malformed: number;\n malformedTimekeeping: number;\n malformedTimekeepingWithText: number;\n }> {\n this.#assertOpen();\n\n const counts = { malformed: 0, malformedTimekeeping: 0, malformedTimekeepingWithText: 0 };\n const channel = this.timekeepingSignal;\n if (!channel || this.recordCount === 0) return { origin: null, ...counts };\n\n const { headerBytes, bytesPerSample, recordBytes, recordDuration } = this.header;\n const buffer = Buffer.alloc(channel.samplesPerRecord * bytesPerSample);\n if (buffer.length === 0) return { origin: null, ...counts };\n\n const searched = Math.min(this.recordCount, RECORDS_SEARCHED_FOR_ORIGIN);\n for (let record = 0; record < searched; record++) {\n const offset = headerBytes + record * recordBytes + channel.byteOffsetInRecord;\n const bytesRead = await readFully(this.#handle, buffer, 0, buffer.length, offset);\n if (bytesRead < buffer.length) return { origin: null, ...counts };\n\n const decoded = decodeRecordAnnotations(buffer, record);\n counts.malformed += decoded.malformed;\n counts.malformedTimekeeping += decoded.malformedTimekeeping;\n counts.malformedTimekeepingWithText += decoded.malformedTimekeepingWithText;\n if (decoded.recordStart !== null) {\n return { origin: decoded.recordStart - record * recordDuration, ...counts };\n }\n }\n return { origin: null, ...counts };\n }\n\n /**\n * Read every EDF+ annotation in the file, plus the start time each record declares.\n *\n * Only the annotation channel is read, seeking straight to it inside each record\n * rather than pulling whole records through memory. On a multi-gigabyte recording\n * that is the difference between a few kilobytes of I/O and all of it.\n *\n * The whole file is always scanned, never just the records inside a requested\n * window: writers are not obliged to store an annotation in the record its onset\n * falls in, and some put every annotation in the first record. Reading only the\n * window's records would drop those entirely.\n */\n async readAnnotations(): Promise<{\n annotations: Annotation[];\n recordStarts: (number | null)[];\n malformed: number;\n /** Unreadable TALs in first position, which carry timing rather than an event. */\n malformedTimekeeping: number;\n /** How many of those also carried event text, so events were lost with the position. */\n malformedTimekeepingWithText: number;\n /** Events kept whose stated duration could not be read; see Annotation.duration. */\n unreadableDurations: number;\n /** Events kept whose stated duration read as a number below zero. */\n negativeDurations: number;\n }> {\n this.#assertOpen();\n\n const annotations: Annotation[] = [];\n const recordStarts: (number | null)[] = new Array<number | null>(this.recordCount).fill(null);\n let malformed = 0;\n let malformedTimekeeping = 0;\n let malformedTimekeepingWithText = 0;\n let unreadableDurations = 0;\n let negativeDurations = 0;\n\n const channels = this.annotationSignals;\n if (channels.length === 0) {\n return {\n annotations,\n recordStarts,\n malformed,\n malformedTimekeeping,\n malformedTimekeepingWithText,\n unreadableDurations,\n negativeDurations,\n };\n }\n\n const { headerBytes, recordBytes, bytesPerSample } = this.header;\n const buffers = channels.map((c) => Buffer.alloc(c.samplesPerRecord * bytesPerSample));\n const timekeeping = this.timekeepingSignal;\n\n for (let record = 0; record < this.recordCount; record++) {\n for (const [position, channel] of channels.entries()) {\n const buffer = buffers[position];\n if (!buffer || buffer.length === 0) continue;\n\n const offset = headerBytes + record * recordBytes + channel.byteOffsetInRecord;\n const bytesRead = await readFully(this.#handle, buffer, 0, buffer.length, offset);\n if (bytesRead < buffer.length) {\n throw changedWhileReading(record, buffer.length, bytesRead, 'annotation data');\n }\n\n // Only the timekeeping channel carries the record's start; see timekeepingSignal.\n const decoded = decodeRecordAnnotations(buffer, record, channel === timekeeping);\n if (channel === timekeeping) recordStarts[record] = decoded.recordStart;\n for (const annotation of decoded.annotations) annotations.push(annotation);\n malformed += decoded.malformed;\n malformedTimekeeping += decoded.malformedTimekeeping;\n malformedTimekeepingWithText += decoded.malformedTimekeepingWithText;\n unreadableDurations += decoded.unreadableDurations;\n negativeDurations += decoded.negativeDurations;\n }\n }\n\n annotations.sort((a, b) => a.onset - b.onset || a.recordIndex - b.recordIndex);\n return {\n annotations,\n recordStarts,\n malformed,\n malformedTimekeeping,\n malformedTimekeepingWithText,\n unreadableDurations,\n negativeDurations,\n };\n }\n\n async close(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n await this.#handle.close();\n }\n\n #assertOpen(): void {\n if (this.#closed) {\n throw new EdfError(\n 'UNREADABLE',\n 'This EDF file has already been closed.',\n // The same advice `changedSinceOpen` gives for the same mistake, which is the only\n // other method that has anything to say about a closed file.\n 'Open it again, or keep it open until the last read.',\n );\n }\n }\n}\n\nfunction describe(cause: unknown): string {\n if (cause instanceof Error) {\n const code = (cause as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return 'no such file';\n // EPERM beside EACCES, because everywhere else in this codebase that reads an errno pairs\n // the two, and ENOTDIR because a path that runs through a regular file — `rec.edf/inner`,\n // which a shell completes and a script builds by joining — is otherwise the one input\n // failure that answers in errno text while its output-side twin answers in a sentence.\n if (code === 'EACCES' || code === 'EPERM') return 'permission denied';\n if (code === 'ENOTDIR') return 'part of the path is a file, not a directory';\n return cause.message;\n }\n return String(cause);\n}\n\n/**\n * The most `fs.read` will accept as a length.\n *\n * Node asserts on a length that does not fit in a signed 32-bit integer, and it asserts in\n * C++: `Assertion failed: args[3]->IsInt32()`, forty frames of native stack, SIGABRT. Not an\n * exception — nothing in JavaScript sees it, so no catch block and no `uncaughtException`\n * handler runs, and a library consumer's whole process goes down with it.\n *\n * A round gigabyte rather than the exact limit, so the loop below does whole even reads.\n */\nconst MAX_READ_BYTES = 1024 * 1024 * 1024;\n\n/** Fill a requested region unless EOF is reached; regular-file reads may legally be short. */\nasync function readFully(\n handle: FileHandle,\n buffer: Buffer,\n offset: number,\n length: number,\n position: number,\n): Promise<number> {\n let total = 0;\n while (total < length) {\n /*\n Capped, because one data record can be larger than a single read may be.\n\n A record is read in one call when it exceeds the chunk budget — there is nothing\n smaller to divide it by, since a record is the unit the format is addressed in. EDF's\n samples-per-record field is 8 characters, so eleven channels at 99,999,999 samples make\n a record of 2.2 GB, and a long record duration at ordinary rates gets there too. That\n went to `fs.read` as a single length over 2^31-1 and took the process out with a native\n assertion rather than an error.\n\n Looping was already how a short read is handled, so the cap costs one more iteration\n per gigabyte and nothing else.\n */\n const want = Math.min(length - total, MAX_READ_BYTES);\n const { bytesRead } = await handle.read(buffer, offset + total, want, position + total);\n if (bytesRead === 0) break;\n total += bytesRead;\n }\n return total;\n}\n\nfunction changedWhileReading(\n record: number,\n expected: number,\n actual: number,\n subject = 'data',\n): EdfError {\n return new EdfError(\n 'UNREADABLE',\n `Expected ${grouped(expected)} bytes of ${subject} at record ${record} but only ` +\n `${counted(actual, 'byte')} ${actual === 1 ? 'was' : 'were'} available; the file appears ` +\n `to have changed size while it was being read.`,\n 'Make sure the recording is not still being written to, then try again.',\n );\n}\n"]}
@@ -67,6 +67,7 @@ export declare function timeDecimals(samplingRate: number): number;
67
67
  export declare function formatBytes(bytes: number): string;
68
68
  /** Human-readable duration: 1h 05m 12s. */
69
69
  export declare function formatDuration(seconds: number): string;
70
+ export declare function plainSeconds(seconds: number): string;
70
71
  /**
71
72
  * How many cached offsets a conversion has left to spend.
72
73
  *
@@ -259,7 +259,7 @@ export function formatDuration(seconds) {
259
259
  say what `--start` may be given — and `--start 1e-15s` is refused as an unknown unit "e".
260
260
  */
261
261
  if (total === 0 && seconds > 0)
262
- return `${plain(seconds)}s`;
262
+ return `${plainSeconds(seconds)}s`;
263
263
  const h = Math.floor(total / 3600);
264
264
  const m = Math.floor((total % 3600) / 60);
265
265
  const s = Math.round((total - h * 3600 - m * 60) * 1000) / 1000;
@@ -270,6 +270,39 @@ export function formatDuration(seconds) {
270
270
  return `${m}m ${sText}s`;
271
271
  return `${sText}s`;
272
272
  }
273
+ /**
274
+ * A length of time in a form the sentence it sits in can hold.
275
+ *
276
+ * `plain` expands a double to its full decimal form, which is what the columns of
277
+ * annotations.csv need — one notation down a column, so a `merge` on it matches. Every message
278
+ * that states a length of seconds took the same rendering, and a record duration is eight
279
+ * characters of header, so `1e308` and `1e-320` both fit in one:
280
+ *
281
+ * Duration unknown (2 records of 1000000000000000000000 ... 000s)
282
+ *
283
+ * Three hundred and nine digits, on a line whose other half has just said the total cannot be
284
+ * stated — and three lines under a RATE column rendering the same magnitude as `4.000e-308 Hz`,
285
+ * because `formatRate` already falls back to exponent form when plain decimal stops carrying
286
+ * the number. This is the failure `listed` was written for, one line over: a message is not
287
+ * the place for everything a header is free to ask for.
288
+ *
289
+ * Plain while plain is *typable*, which is the reason the expansion is here at all —
290
+ * `--start 1e-15s` is refused as an unknown unit `e`, so `repeating-fast.edf` reads
291
+ * `0.000000000000001s` and can be acted on. Past the width of a line nothing is typable
292
+ * either way: a reader cannot count three hundred digits any more than they can pass an
293
+ * exponent, so the shortest exact form is the more honest of the two.
294
+ *
295
+ * Here rather than in cli/report.ts, where 0.8.95 wrote it for one half of one line. The
296
+ * other half of that same line expanded a duration of 3e-308 to three hundred and ten
297
+ * characters; so did the refusal naming a record duration that is not positive, and the hint
298
+ * that says how wide a window has to be to hold a sample. All four state a length of seconds
299
+ * out of a header, and the rule is one rule.
300
+ */
301
+ const LONGEST_PLAIN_SECONDS = 30;
302
+ export function plainSeconds(seconds) {
303
+ const expanded = plain(seconds);
304
+ return expanded.length <= LONGEST_PLAIN_SECONDS ? expanded : String(seconds);
305
+ }
273
306
  /** A record can declare a great many samples; two arrays this size is the cost of caching. */
274
307
  const MAX_CACHED_OFFSETS = 1 << 20;
275
308
  export function newOffsetBudget() {
@@ -1 +1 @@
1
- {"version":3,"file":"number.js","sourceRoot":"","sources":["../../src/format/number.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,qFAAqF;AACrF,MAAM,eAAe,GAAG,CAAC,IAAI,EAAE,CAAC;AAyBhC;;;;;;;;GAQG;AACH,MAAM,kBAAkB,GAAG,CAAC,IAAI,EAAE,CAAC;AAEnC,MAAM,UAAU,oBAAoB;IAClC,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC;AAC3C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,KAAK,CAAC,KAAa,EAAE,QAAgB;IACnD,qFAAqF;IACrF,oFAAoF;IACpF,mFAAmF;IACnF,iFAAiF;IACjF,gCAAgC;IAChC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEvC,qFAAqF;IACrF,qFAAqF;IACrF,wFAAwF;IACxF,8EAA8E;IAC9E,EAAE;IACF,qFAAqF;IACrF,6CAA6C;IAC7C,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QACnD,OAAO,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IACnE,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACrC,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IACnD,4CAA4C;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,CAAC,KAAK,EAAE,CAAC,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC,OAAO;YAAE,SAAS;QACnD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,KAAK,CAAC,KAAa;IACjC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3B,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC5B,IAAI,CAAC,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC;IACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAClC,kFAAkF;IAClF,MAAM,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC/E,MAAM,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IACxF,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACjC,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,GAAG,IAAI,KAAK,GAAG,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC;IACjE,IAAI,KAAK,IAAI,MAAM,CAAC,MAAM;QAAE,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;IAC1F,OAAO,GAAG,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;AACnE,CAAC;AAKD,MAAM,UAAU,mBAAmB,CACjC,MAAiB,EACjB,QAAgB,EAChB,SAA4B,oBAAoB,EAAE;IAElD,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IACjC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IAC3D,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IAC5D,MAAM,IAAI,GAAG,IAAI,GAAG,GAAG,GAAG,CAAC,CAAC;IAE5B,IACE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QACtB,IAAI,IAAI,CAAC;QACT,IAAI,GAAG,eAAe;QACtB,IAAI,GAAG,MAAM,CAAC,SAAS,EACvB,CAAC;QACD,OAAO,CAAC,OAAe,EAAU,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,CAAC,SAAS,IAAI,IAAI,CAAC;IAEzB;;;;;;;;;;;;MAYE;IACF,MAAM,KAAK,GAAG,IAAI,KAAK,CAAqB,IAAI,CAAC,CAAC;IAClD,OAAO,CAAC,OAAe,EAAU,EAAE;QACjC,MAAM,IAAI,GAAG,OAAO,GAAG,GAAG,CAAC;QAC3B,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,GAAG,IAAI,EAAE,CAAC;YAC7B,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;YACxB,IAAI,GAAG,KAAK,SAAS;gBAAE,OAAO,GAAG,CAAC;YAClC,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;YAC7C,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;YACnB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;IACzC,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAE7B,MAAM,UAAU,YAAY,CAAC,YAAoB;IAC/C,IAAI,CAAC,CAAC,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;QAAE,OAAO,CAAC,CAAC;IACpE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,iBAAiB,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,IAAI,MAAM,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,GAAG,YAAY,CAAC;YAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC3F,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC5C,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,OAAO,KAAK,IAAI,IAAI,IAAI,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChD,KAAK,IAAI,IAAI,CAAC;QACd,IAAI,EAAE,CAAC;IACT,CAAC;IACD,IAAI,OAAO,GAAG,KAAK,IAAI,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACxF,4FAA4F;IAC5F,0FAA0F;IAC1F,8CAA8C;IAC9C,IAAI,OAAO,KAAK,IAAI,IAAI,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChD,IAAI,EAAE,CAAC;QACP,OAAO,GAAG,CAAC,CAAC;IACd,CAAC;IACD,OAAO,GAAG,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;AACrC,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,yFAAyF;IACzF,sFAAsF;IACtF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAC;IAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;MA0BE;IACF,IAAI,OAAO,GAAG,CAAC,IAAI,OAAO,IAAI,MAAM,CAAC,gBAAgB;QAAE,OAAO,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC;IAEtF,oFAAoF;IACpF,yFAAyF;IACzF,oFAAoF;IACpF,0FAA0F;IAC1F,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAChD;;;;;;;;;;;;;;;;;;;;MAoBE;IACF,IAAI,KAAK,KAAK,CAAC,IAAI,OAAO,GAAG,CAAC;QAAE,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;IAC5D,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IACnC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC1C,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAChE,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACpF,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,KAAK,GAAG,CAAC;IACnE,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC,KAAK,KAAK,GAAG,CAAC;IACpC,OAAO,GAAG,KAAK,GAAG,CAAC;AACrB,CAAC;AAED,8FAA8F;AAC9F,MAAM,kBAAkB,GAAG,CAAC,IAAI,EAAE,CAAC;AAoBnC,MAAM,UAAU,eAAe;IAC7B,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC;AAC3C,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,iBAAiB,CAC/B,gBAAwB,EACxB,IAAY,EACZ,QAAgB,EAChB,SAAuB,eAAe,EAAE;IAExC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAEjD,mFAAmF;IACnF,MAAM,MAAM,GAAG,CAAC,WAAmB,EAAE,MAAc,EAAU,EAAE,CAC7D,KAAK,CAAC,WAAW,GAAG,MAAM,GAAG,IAAI,EAAE,QAAQ,CAAC,CAAC;IAE/C;;;;;;;;;;;;;;;;;;;;;;;;MAwBE;IACF,MAAM,QAAQ,GAAG,CAAC,WAAmB,EAAE,MAAc,EAAU,EAAE;QAC/D,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9B,MAAM,KAAK,GAAG,WAAW,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QACrF,IAAI,KAAK,IAAI,IAAI;YAAE,OAAO,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QACtD,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;IAChE,CAAC,CAAC;IAEF,kEAAkE;IAClE,MAAM,MAAM,GAAG,CAAC,WAAmB,EAAE,MAAc,EAAU,EAAE,CAC7D,MAAM,IAAI,MAAM,IAAI,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,WAAW,IAAI,CAAC;QACxE,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC/B,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IAElC,IAAI,CAAC,MAAM,IAAI,gBAAgB,IAAI,CAAC,IAAI,gBAAgB,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;QAC5E,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,CAAC,SAAS,IAAI,gBAAgB,CAAC;IAErC,qFAAqF;IACrF,uFAAuF;IACvF,6CAA6C;IAC7C,MAAM,WAAW,GAAG,IAAI,YAAY,CAAC,gBAAgB,CAAC,CAAC;IACvD,MAAM,YAAY,GAAa,IAAI,KAAK,CAAS,gBAAgB,CAAC,CAAC;IACnE,KAAK,IAAI,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,gBAAgB,EAAE,MAAM,EAAE,EAAE,CAAC;QACzD,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9B,WAAW,CAAC,MAAM,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;QAC7E,YAAY,CAAC,MAAM,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3D,CAAC;IAED,OAAO,CAAC,WAAmB,EAAE,MAAc,EAAU,EAAE;QACrD;;;;;;UAME;QACF,IACE,MAAM,GAAG,CAAC;YACV,MAAM,IAAI,gBAAgB;YAC1B,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC;YAC9B,WAAW,GAAG,CAAC,EACf,CAAC;YACD,OAAO,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QACrC,CAAC;QACD,MAAM,KAAK,GAAG,WAAW,GAAI,WAAW,CAAC,MAAM,CAAY,CAAC;QAC5D;;;;;;;;;;;;UAYE;QACF,IAAI,KAAK,IAAI,IAAI;YAAE,OAAO,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QACtD,OAAO,GAAG,KAAK,GAAG,YAAY,CAAC,MAAM,CAAW,EAAE,CAAC;IACrD,CAAC,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Number formatting for CSV cells.\n *\n * A one-hour, 23-channel, 256 Hz recording is about 21 million numeric cells, so\n * this is the hottest code in a conversion. Two things keep it cheap:\n *\n * - Every sample in a channel comes from a bounded set of integers (digitalMin to\n * digitalMax, typically 4096 distinct values for a 12-bit ADC). The formatted\n * text for a digital code never changes, so it is computed once and reused.\n * - The cache fills lazily. Real recordings visit only a fraction of the range,\n * and a channel with an implausibly wide range falls back to direct formatting\n * rather than reserving memory it will never use.\n */\n\nimport type { EdfSignal } from '../edf/header.js';\nimport { makeScaler } from '../edf/scale.js';\n\n/** Widest digital range worth caching: 16 bits of pointers is 512 KB per channel. */\nconst MAX_CACHED_SPAN = 1 << 16;\n\n/**\n * How many cached sample slots a conversion has left to spend.\n *\n * MAX_CACHED_SPAN is a bound on one channel, and a bound on one channel is not a bound: a\n * file may declare as many channels as it likes, and each was handed its own cache. A\n * channel declaring the ordinary full 16-bit digital range takes the whole 512 KB, so a\n * 256-channel montage reserved 134 MB of pointers before writing a row — a 7.9 MB\n * recording that needed a 192 MB heap and died with a V8 out-of-memory fatal error under\n * anything smaller. The caches were the live set; nothing else in the conversion came near\n * them. It is the same shape of mistake the offset budget below was made to fix, one level\n * over: there the unbounded count was rate groups, here it is channels.\n *\n * One budget for the whole conversion leaves the ordinary recording exactly as it was and\n * puts a ceiling on the dense montage: the same 256-channel file now holds its caches to\n * 16 MB and converts under a 48 MB heap. Channels ask in the order the groups are written,\n * which is fastest rate first, so the cache goes to the channels with the most cells to\n * format. The ones that miss out fall back to formatting directly, which produces\n * identical text — the output is byte-for-byte what it was.\n */\nexport interface SampleCacheBudget {\n remaining: number;\n}\n\n/**\n * Slots for the whole conversion: 16 MB of pointers if every one is claimed.\n *\n * Enough that a full 32-channel montage declaring the whole 16-bit range keeps every cache\n * it had before, and 512 channels of an ordinary 12-bit ADC do too. Past that the extra\n * channels format directly, which costs about a quarter of the conversion's time on a\n * recording where almost none of them are cached — against a file that did not convert at\n * all under a 128 MB heap.\n */\nconst MAX_CACHED_SAMPLES = 1 << 21;\n\nexport function newSampleCacheBudget(): SampleCacheBudget {\n return { remaining: MAX_CACHED_SAMPLES };\n}\n\n/**\n * Format with a fixed number of decimals, normalising negative zero.\n *\n * Without this, a sample that scales to a very small negative value prints as\n * \"-0.000\", which looks like a distinct measurement but is not.\n */\nexport function fixed(value: number, decimals: number): string {\n // An undefined value becomes an empty cell rather than the text \"NaN\" or \"Infinity\".\n // A channel whose header leaves the digital-to-physical mapping undefined scales to\n // NaN, and an empty field is the CSV convention for \"no value here\" — the same one\n // annotations.csv uses for an absent duration. Readers parse it back as NaN / NA\n // rather than as a measurement.\n if (!Number.isFinite(value)) return '';\n\n // toFixed switches to exponent notation at 1e21, which would put \"1e+21\" in a column\n // whose every other cell is plain fixed-decimal — and a reader parsing the column as\n // decimal text has no reason to expect it. Reachable because EDF's 8-character physical\n // range fields accept exponent form, so a header may legitimately say \"1e30\".\n //\n // Above 2^53 a double carries no fractional part anyway, so the integer expansion is\n // exact rather than an approximation of one.\n if (Math.abs(value) >= 1e21) {\n const whole = BigInt(Math.trunc(value)).toString();\n return decimals > 0 ? `${whole}.${'0'.repeat(decimals)}` : whole;\n }\n\n const text = value.toFixed(decimals);\n if (text.charCodeAt(0) !== 45 /* - */) return text;\n // Cheap check for \"-0\", \"-0.0\", \"-0.000\"...\n for (let i = 1; i < text.length; i++) {\n const c = text.charCodeAt(i);\n if (c === 46 /* . */ || c === 48 /* 0 */) continue;\n return text;\n }\n return text.slice(1);\n}\n\n/**\n * A number as plain decimal text, at any magnitude.\n *\n * `String()` switches to exponent notation twice — above 1e21 and below 1e-6 — and\n * annotations.csv wrote its `onset_s` and `duration_s` through it. An EDF+ TAL states its\n * onset as ordinary decimal text, so a file saying `+0.0000001` came back as `1e-7` in a\n * column whose every other cell is a plain decimal, beside a `time_s` the documentation says\n * it \"joins directly\" with. It does not: pandas reads the column as object rather than\n * float64 once one cell is exponent text, and a `merge` on it matches nothing.\n *\n * `fixed` cannot answer this. It needs a decimal count, and these two columns are documented\n * as carrying \"their natural numeric form ... without padding to a fixed decimal count\" —\n * asking for enough places to hold 1e-7 would rewrite `0.1` as `0.10000000000000000555`.\n * Expanding the notation instead touches only the values that are in it and leaves every\n * other cell byte-for-byte what it was.\n */\nexport function plain(value: number): string {\n const text = String(value);\n const e = text.indexOf('e');\n if (e === -1) return text;\n const negative = text.charCodeAt(0) === 45 /* - */;\n const mantissa = text.slice(negative ? 1 : 0, e);\n const dot = mantissa.indexOf('.');\n // Where the point sits once the exponent is spent, counted in significant digits.\n const point = (dot === -1 ? mantissa.length : dot) + Number(text.slice(e + 1));\n const digits = dot === -1 ? mantissa : mantissa.slice(0, dot) + mantissa.slice(dot + 1);\n const sign = negative ? '-' : '';\n if (point <= 0) return `${sign}0.${'0'.repeat(-point)}${digits}`;\n if (point >= digits.length) return `${sign}${digits}${'0'.repeat(point - digits.length)}`;\n return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`;\n}\n\n/** Maps a raw digital sample to its formatted physical value. */\nexport type SampleFormatter = (digital: number) => string;\n\nexport function makeSampleFormatter(\n signal: EdfSignal,\n decimals: number,\n budget: SampleCacheBudget = newSampleCacheBudget(),\n): SampleFormatter {\n const scale = makeScaler(signal);\n const low = Math.min(signal.digitalMin, signal.digitalMax);\n const high = Math.max(signal.digitalMin, signal.digitalMax);\n const span = high - low + 1;\n\n if (\n !Number.isFinite(span) ||\n span <= 0 ||\n span > MAX_CACHED_SPAN ||\n span > budget.remaining\n ) {\n return (digital: number): string => fixed(scale(digital), decimals);\n }\n budget.remaining -= span;\n\n /*\n The cache covers the channel's declared digital range, not the whole int16 domain.\n\n Allocating 65536 slots regardless of span cost 512 KB of pointers per channel, which a\n dense montage cannot afford: a 400-channel recording needed over 200 MB of cache alone\n and died with a V8 out-of-memory fatal error before writing a row. Sizing to the\n declared span makes the ordinary 12-bit channel 32 KB instead — the same 400 channels\n now fit in about 13 MB. Channels that declare the full range still take the whole 512 KB,\n which is what the budget above is for.\n\n Samples outside the declared range still occur in non-conforming files. They simply\n miss the cache and are formatted directly, which produces identical text.\n */\n const cache = new Array<string | undefined>(span);\n return (digital: number): string => {\n const slot = digital - low;\n if (slot >= 0 && slot < span) {\n const hit = cache[slot];\n if (hit !== undefined) return hit;\n const text = fixed(scale(digital), decimals);\n cache[slot] = text;\n return text;\n }\n return fixed(scale(digital), decimals);\n };\n}\n\n/**\n * Decimals for the time column.\n *\n * The interval between samples is 1/rate, which has a terminating decimal expansion of d\n * places exactly when 10^d divides evenly by the rate. Writing that many places makes sample\n * times exact rather than rounded, so `time_s * rate` comes back as a whole number instead\n * of 8191.999999.\n *\n * The search used to stop at nine places, and the comment here claimed \"every rate in common\n * use clears this — 256 Hz needs 8 places, 512 Hz needs 9\". The next two powers of two do\n * not: 1/1024 needs ten places and 1/2048 needs eleven, and those are the rates a BioSemi\n * ActiveTwo records at by default. Both fell through to the rounding fallback, so the two\n * most common high-rate EEG recordings got exactly the behaviour this function exists to\n * avoid — 0.0009766 for an interval of 0.0009765625.\n *\n * A rate of 2^a * 5^b terminates in max(a, b) places, so fifteen covers every power of two up\n * to 32768 Hz, far past anything that records biosignals. Rates with a repeating expansion\n * (3 Hz, say) still fall back to enough places to keep consecutive samples distinct, and that\n * fallback keeps its own cap.\n *\n * Fifteen and not more, because the test below has to stay exact: 10^16 is past 2^53, where a\n * double can no longer hold every integer, and `Number.isInteger(10 ** 17 / 3)` is true — so a\n * larger bound reports a terminating expansion for rates that have none, and 3 Hz would ask\n * for seventeen decimals of a number that repeats forever.\n *\n * The fallback is bounded by the same fifteen. It used to stop at nine, which defeated the\n * one thing it exists to do: at 3e10 Hz — non-terminating, so no exact expansion to find —\n * the interval is 3.3e-11 and nine places rounded every sample in a record to the same\n * timestamp. A column that cannot tell two samples apart is not keeping them distinct, which\n * is what this branch is for.\n */\nconst MAX_TIME_DECIMALS = 15;\n\nexport function timeDecimals(samplingRate: number): number {\n if (!(samplingRate > 0) || !Number.isFinite(samplingRate)) return 3;\n for (let d = 0; d <= MAX_TIME_DECIMALS; d++) {\n if (Number.isInteger(10 ** d / samplingRate)) return Math.max(3, d);\n }\n return Math.min(MAX_TIME_DECIMALS, Math.max(3, Math.ceil(Math.log10(samplingRate)) + 3));\n}\n\n/** Human-readable byte size for warnings and summaries. */\nexport function formatBytes(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB', 'TB'];\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit++;\n }\n let rounded = value >= 100 || unit === 0 ? Math.round(value) : Number(value.toFixed(1));\n // Rounding can carry into the next unit, and the unit was chosen before it: 1,048,575 bytes\n // is 1023.999 KB, which printed as \"1024 KB\". The same slip formatDuration below fixed by\n // rounding before splitting, one function up.\n if (rounded === 1024 && unit < units.length - 1) {\n unit++;\n rounded = 1;\n }\n return `${rounded} ${units[unit]}`;\n}\n\n/** Human-readable duration: 1h 05m 12s. */\nexport function formatDuration(seconds: number): string {\n // A duration that is not a number cannot be broken into hours and minutes, and saying so\n // beats the alternative: the fallback below rendered these as \"NaNs\" and \"Infinitys\".\n if (!Number.isFinite(seconds)) return 'unknown';\n\n /*\n Past 2^53 the decomposition stops being arithmetic and starts being noise.\n\n `total - h * 3600 - m * 60` cannot be exact once `total` exceeds what a double can hold\n as a whole number, and the error lands in the seconds field, where it shows up as a\n value that cannot exist. A header declaring a record duration of 1e300 printed:\n\n Duration 8.333333333333333e+296h 48m -2880s\n\n Forty-eight minutes and minus forty-eight seconds, under an hours field in exponent\n notation. The seconds are the honest form for a figure this size — nobody reads\n 285 million years as hours — and the record count and duration are printed beside it\n anyway, so a corrupt header stays just as visible.\n\n Written through `fixed` rather than by interpolation, which is the same 1e21 cliff again:\n `${seconds}` switches to exponent notation exactly where `toFixed` does, so the fallback\n put a token back that this tool's own parser refuses. `--start 4000000000000000000000` on\n a recording of three 1e21-second records was answered with\n\n --start \"4000000000000000000000\" is at or past the end of this 3e+21s recording.\n\n a sentence whose whole job is to say what window there is to ask for, ending in a length\n `--start` and `--duration` reject with \"uses an unknown unit \\\"e\\\"\". `formatSeconds` in\n time-range.ts was fixed for this in the other half of that same message, and its comment\n quotes this half as part of what was wrong. `fixed` expands these with BigInt, which is\n exact past 2^53 where a double carries no fraction anyway.\n */\n if (seconds < 0 || seconds >= Number.MAX_SAFE_INTEGER) return `${fixed(seconds, 0)}s`;\n\n // Round to the precision that will actually be printed BEFORE splitting into units.\n // Splitting first left the remainder to be rounded on its own, so 3599.9996 s decomposed\n // as 59 minutes and 59.9996 seconds and then printed as \"59m 60s\" — a duration that\n // cannot exist. Rounding first carries the extra second into the minute where it belongs.\n const total = Math.round(seconds * 1000) / 1000;\n /*\n A duration shorter than the rounding is not a duration of zero.\n\n Three decimals is the right precision for a recording measured in hours, and it collapses\n everything under half a millisecond to \"0s\". `repeating-fast.edf` — two records of 1e-15s,\n six samples, one of this repository's own fixtures — printed\n\n Duration 0s (2 records of 1e-15s)\n\n and refused `--start 0.5` with \"is at or past the end of this 0s recording\". A reader is\n told the file holds nothing, on the line they look at first, about a file that converts to\n six rows; the record duration two columns over already contradicts it. Same shape as the\n other end of this function, which stops decomposing past 2^53 and prints the seconds\n instead, and as the byte size above, which carries 1023.999 KB up rather than printing the\n \"1024 KB\" no unit has. None of the three is a rounding rule; each one refuses to print a\n form the quantity cannot take.\n\n Written through `plain` rather than interpolated, for the reason the comment above gives:\n `${1e-15}` is exponent notation, and the sentence this feeds is one whose whole job is to\n say what `--start` may be given — and `--start 1e-15s` is refused as an unknown unit \"e\".\n */\n if (total === 0 && seconds > 0) return `${plain(seconds)}s`;\n const h = Math.floor(total / 3600);\n const m = Math.floor((total % 3600) / 60);\n const s = Math.round((total - h * 3600 - m * 60) * 1000) / 1000;\n const sText = Number.isInteger(s) ? String(s) : s.toFixed(3).replace(/\\.?0+$/u, '');\n if (h > 0) return `${h}h ${String(m).padStart(2, '0')}m ${sText}s`;\n if (m > 0) return `${m}m ${sText}s`;\n return `${sText}s`;\n}\n\n/** A record can declare a great many samples; two arrays this size is the cost of caching. */\nconst MAX_CACHED_OFFSETS = 1 << 20;\n\n/**\n * How many cached offsets a conversion has left to spend.\n *\n * The cap used to be per rate group, and a file may hold as many rate groups as it has\n * channels. Twelve channels at twelve rates just under the cap — a 25 MB file — took\n * 1.66 GB and 36 seconds, where a 92 MB file at one rate takes 283 MB and finishes in a\n * fraction of that; twenty-four of them never finished at all. A per-group limit is not a\n * limit, since nothing bounds the number of groups.\n *\n * One budget for the whole conversion makes the single-group case identical to what it was\n * and the many-group case bounded. Groups ask in order of rate, fastest first, so the cache\n * goes to the tables with the most rows to write and the ones that miss out are the ones\n * that would have gained least.\n */\nexport interface OffsetBudget {\n remaining: number;\n}\n\nexport function newOffsetBudget(): OffsetBudget {\n return { remaining: MAX_CACHED_OFFSETS };\n}\n\n/**\n * Formats the time column, reusing the part of it that repeats.\n *\n * Every value cell is already cached — a channel has at most `digitalMax - digitalMin + 1`\n * distinct readings, so the same handful of strings serve millions of rows. The time column\n * had no such luck: it rises monotonically, so no two rows share a string and `toFixed` ran\n * once per row. On a ten-million-row conversion that was a third of the total time, more\n * than reading the file and writing the CSV put together.\n *\n * What repeats is the offset within a record. Sample `s` sits at `s / rate` from the start of\n * whichever record holds it, and there are only `samplesPerRecord` such offsets in the whole\n * recording. Splitting each into whole seconds and printed fraction turns the per-row work\n * into one integer addition and a concatenation:\n *\n * record starting at 42s, sample 7 of a 100 Hz record\n * -> 42 + 0 whole seconds, fraction \".070\" -> \"42.070\"\n *\n * The decomposition is only valid when the record starts on a whole, non-negative second,\n * which is what lets the fraction come entirely from the offset. A record starting at 0.5 s would mix the\n * two, so those fall back to formatting the sum directly. Continuous recordings start every\n * record at `index * recordDuration`, so this holds for all of them whose record duration is\n * a whole number of seconds, and for discontinuous files it holds per record depending on\n * where that record actually starts.\n */\nexport function makeTimeFormatter(\n samplesPerRecord: number,\n rate: number,\n decimals: number,\n budget: OffsetBudget = newOffsetBudget(),\n): (recordStart: number, sample: number) => string {\n const usable = rate > 0 && Number.isFinite(rate);\n\n /** The sum, formatted. Right only where the decomposition below does not apply. */\n const summed = (recordStart: number, sample: number): string =>\n fixed(recordStart + sample / rate, decimals);\n\n /*\n The same decomposition the table holds, computed on the spot.\n\n This used to be the sum, and that made the time column two columns depending on how much\n of the budget above was left. `recordStart + sample / rate` cannot carry the fraction once\n the record start is large: at a start of 1e9 and 30 kHz the exact instant is\n 1000000000.0000333333, and the double nearest the sum prints as 1000000000.00003338 —\n the last two places are the addition's rounding, not the recording's clock.\n\n Which of the two a rate group got was decided by the groups ahead of it, since they ask\n from one budget fastest first. So a recording whose fast channel takes the table left its\n slow channel adding doubles, and the same channel asked for on its own got the table:\n\n edf2csv far.edf --out whole -> 1000000000.00003338\n edf2csv far.edf --out one --channels slow -> 1000000000.00003333\n\n Two files from one recording disagreeing about when a sample was taken, in a tool whose\n documentation says `--channels` selects columns and changes nothing else. The narrowing\n sweep asserts exactly that and could not see it: its fixtures are small enough that the\n budget never runs out, so both conversions took the table.\n\n The sum is still the answer where the decomposition has nothing to stand on — a record\n starting on a fraction or before zero, where the whole part and the fraction are not\n separable — and past 1e21, where the whole part stops printing in full.\n */\n const composed = (recordStart: number, sample: number): string => {\n const text = fixed(sample / rate, decimals);\n const dot = text.indexOf('.');\n const whole = recordStart + (dot === -1 ? Number(text) : Number(text.slice(0, dot)));\n if (whole >= 1e21) return summed(recordStart, sample);\n return dot === -1 ? `${whole}` : `${whole}${text.slice(dot)}`;\n };\n\n /** What every row gets when the table is not there to be read. */\n const direct = (recordStart: number, sample: number): string =>\n usable && sample >= 0 && Number.isInteger(recordStart) && recordStart >= 0\n ? composed(recordStart, sample)\n : summed(recordStart, sample);\n\n if (!usable || samplesPerRecord <= 0 || samplesPerRecord > budget.remaining) {\n return direct;\n }\n budget.remaining -= samplesPerRecord;\n\n // Whole seconds and printed fraction of each offset, taken from the formatted offset\n // itself so that an offset which rounds up to the next second (0.9996 at three places)\n // carries that second rather than losing it.\n const wholeOffset = new Float64Array(samplesPerRecord);\n const fractionText: string[] = new Array<string>(samplesPerRecord);\n for (let sample = 0; sample < samplesPerRecord; sample++) {\n const text = fixed(sample / rate, decimals);\n const dot = text.indexOf('.');\n wholeOffset[sample] = dot === -1 ? Number(text) : Number(text.slice(0, dot));\n fractionText[sample] = dot === -1 ? '' : text.slice(dot);\n }\n\n return (recordStart: number, sample: number): string => {\n /*\n Non-negative only. Appending a fraction to a negative whole part moves the time the\n wrong way: a record at -5 s and an offset of half a second is -4.5, but \"-5\" and\n \".500\" concatenate to -5.500. Recording times start at zero, so this is unreachable\n from a well-formed file — an EDF+ timekeeping TAL is free to carry a negative onset\n though, and that is enough reason for the fast path to decline it.\n */\n if (\n sample < 0 ||\n sample >= samplesPerRecord ||\n !Number.isInteger(recordStart) ||\n recordStart < 0\n ) {\n return direct(recordStart, sample);\n }\n const whole = recordStart + (wholeOffset[sample] as number);\n /*\n The same 1e21 cliff `fixed` guards against, arriving through the back door.\n\n `${whole}` is the implicit Number-to-String conversion, which switches to exponent\n notation at 1e21 exactly as `toFixed` does — and then the cached fraction is glued onto\n the end of it, so the cell reads \"1e+21.000\". That is not a number in any notation:\n pandas and R both parse it as NaN, and a column of ordinary decimals ends in a run of\n them. A header may legitimately say `1e21` in its 8-character record-duration field, so\n three records are enough to reach it.\n\n The slow path already expands these with BigInt. One comparison per row keeps that\n correct without giving up the cache for the other twenty million.\n */\n if (whole >= 1e21) return summed(recordStart, sample);\n return `${whole}${fractionText[sample] as string}`;\n };\n}\n"]}
1
+ {"version":3,"file":"number.js","sourceRoot":"","sources":["../../src/format/number.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Number formatting for CSV cells.\n *\n * A one-hour, 23-channel, 256 Hz recording is about 21 million numeric cells, so\n * this is the hottest code in a conversion. Two things keep it cheap:\n *\n * - Every sample in a channel comes from a bounded set of integers (digitalMin to\n * digitalMax, typically 4096 distinct values for a 12-bit ADC). The formatted\n * text for a digital code never changes, so it is computed once and reused.\n * - The cache fills lazily. Real recordings visit only a fraction of the range,\n * and a channel with an implausibly wide range falls back to direct formatting\n * rather than reserving memory it will never use.\n */\n\nimport type { EdfSignal } from '../edf/header.js';\nimport { makeScaler } from '../edf/scale.js';\n\n/** Widest digital range worth caching: 16 bits of pointers is 512 KB per channel. */\nconst MAX_CACHED_SPAN = 1 << 16;\n\n/**\n * How many cached sample slots a conversion has left to spend.\n *\n * MAX_CACHED_SPAN is a bound on one channel, and a bound on one channel is not a bound: a\n * file may declare as many channels as it likes, and each was handed its own cache. A\n * channel declaring the ordinary full 16-bit digital range takes the whole 512 KB, so a\n * 256-channel montage reserved 134 MB of pointers before writing a row — a 7.9 MB\n * recording that needed a 192 MB heap and died with a V8 out-of-memory fatal error under\n * anything smaller. The caches were the live set; nothing else in the conversion came near\n * them. It is the same shape of mistake the offset budget below was made to fix, one level\n * over: there the unbounded count was rate groups, here it is channels.\n *\n * One budget for the whole conversion leaves the ordinary recording exactly as it was and\n * puts a ceiling on the dense montage: the same 256-channel file now holds its caches to\n * 16 MB and converts under a 48 MB heap. Channels ask in the order the groups are written,\n * which is fastest rate first, so the cache goes to the channels with the most cells to\n * format. The ones that miss out fall back to formatting directly, which produces\n * identical text — the output is byte-for-byte what it was.\n */\nexport interface SampleCacheBudget {\n remaining: number;\n}\n\n/**\n * Slots for the whole conversion: 16 MB of pointers if every one is claimed.\n *\n * Enough that a full 32-channel montage declaring the whole 16-bit range keeps every cache\n * it had before, and 512 channels of an ordinary 12-bit ADC do too. Past that the extra\n * channels format directly, which costs about a quarter of the conversion's time on a\n * recording where almost none of them are cached — against a file that did not convert at\n * all under a 128 MB heap.\n */\nconst MAX_CACHED_SAMPLES = 1 << 21;\n\nexport function newSampleCacheBudget(): SampleCacheBudget {\n return { remaining: MAX_CACHED_SAMPLES };\n}\n\n/**\n * Format with a fixed number of decimals, normalising negative zero.\n *\n * Without this, a sample that scales to a very small negative value prints as\n * \"-0.000\", which looks like a distinct measurement but is not.\n */\nexport function fixed(value: number, decimals: number): string {\n // An undefined value becomes an empty cell rather than the text \"NaN\" or \"Infinity\".\n // A channel whose header leaves the digital-to-physical mapping undefined scales to\n // NaN, and an empty field is the CSV convention for \"no value here\" — the same one\n // annotations.csv uses for an absent duration. Readers parse it back as NaN / NA\n // rather than as a measurement.\n if (!Number.isFinite(value)) return '';\n\n // toFixed switches to exponent notation at 1e21, which would put \"1e+21\" in a column\n // whose every other cell is plain fixed-decimal — and a reader parsing the column as\n // decimal text has no reason to expect it. Reachable because EDF's 8-character physical\n // range fields accept exponent form, so a header may legitimately say \"1e30\".\n //\n // Above 2^53 a double carries no fractional part anyway, so the integer expansion is\n // exact rather than an approximation of one.\n if (Math.abs(value) >= 1e21) {\n const whole = BigInt(Math.trunc(value)).toString();\n return decimals > 0 ? `${whole}.${'0'.repeat(decimals)}` : whole;\n }\n\n const text = value.toFixed(decimals);\n if (text.charCodeAt(0) !== 45 /* - */) return text;\n // Cheap check for \"-0\", \"-0.0\", \"-0.000\"...\n for (let i = 1; i < text.length; i++) {\n const c = text.charCodeAt(i);\n if (c === 46 /* . */ || c === 48 /* 0 */) continue;\n return text;\n }\n return text.slice(1);\n}\n\n/**\n * A number as plain decimal text, at any magnitude.\n *\n * `String()` switches to exponent notation twice — above 1e21 and below 1e-6 — and\n * annotations.csv wrote its `onset_s` and `duration_s` through it. An EDF+ TAL states its\n * onset as ordinary decimal text, so a file saying `+0.0000001` came back as `1e-7` in a\n * column whose every other cell is a plain decimal, beside a `time_s` the documentation says\n * it \"joins directly\" with. It does not: pandas reads the column as object rather than\n * float64 once one cell is exponent text, and a `merge` on it matches nothing.\n *\n * `fixed` cannot answer this. It needs a decimal count, and these two columns are documented\n * as carrying \"their natural numeric form ... without padding to a fixed decimal count\" —\n * asking for enough places to hold 1e-7 would rewrite `0.1` as `0.10000000000000000555`.\n * Expanding the notation instead touches only the values that are in it and leaves every\n * other cell byte-for-byte what it was.\n */\nexport function plain(value: number): string {\n const text = String(value);\n const e = text.indexOf('e');\n if (e === -1) return text;\n const negative = text.charCodeAt(0) === 45 /* - */;\n const mantissa = text.slice(negative ? 1 : 0, e);\n const dot = mantissa.indexOf('.');\n // Where the point sits once the exponent is spent, counted in significant digits.\n const point = (dot === -1 ? mantissa.length : dot) + Number(text.slice(e + 1));\n const digits = dot === -1 ? mantissa : mantissa.slice(0, dot) + mantissa.slice(dot + 1);\n const sign = negative ? '-' : '';\n if (point <= 0) return `${sign}0.${'0'.repeat(-point)}${digits}`;\n if (point >= digits.length) return `${sign}${digits}${'0'.repeat(point - digits.length)}`;\n return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`;\n}\n\n/** Maps a raw digital sample to its formatted physical value. */\nexport type SampleFormatter = (digital: number) => string;\n\nexport function makeSampleFormatter(\n signal: EdfSignal,\n decimals: number,\n budget: SampleCacheBudget = newSampleCacheBudget(),\n): SampleFormatter {\n const scale = makeScaler(signal);\n const low = Math.min(signal.digitalMin, signal.digitalMax);\n const high = Math.max(signal.digitalMin, signal.digitalMax);\n const span = high - low + 1;\n\n if (\n !Number.isFinite(span) ||\n span <= 0 ||\n span > MAX_CACHED_SPAN ||\n span > budget.remaining\n ) {\n return (digital: number): string => fixed(scale(digital), decimals);\n }\n budget.remaining -= span;\n\n /*\n The cache covers the channel's declared digital range, not the whole int16 domain.\n\n Allocating 65536 slots regardless of span cost 512 KB of pointers per channel, which a\n dense montage cannot afford: a 400-channel recording needed over 200 MB of cache alone\n and died with a V8 out-of-memory fatal error before writing a row. Sizing to the\n declared span makes the ordinary 12-bit channel 32 KB instead — the same 400 channels\n now fit in about 13 MB. Channels that declare the full range still take the whole 512 KB,\n which is what the budget above is for.\n\n Samples outside the declared range still occur in non-conforming files. They simply\n miss the cache and are formatted directly, which produces identical text.\n */\n const cache = new Array<string | undefined>(span);\n return (digital: number): string => {\n const slot = digital - low;\n if (slot >= 0 && slot < span) {\n const hit = cache[slot];\n if (hit !== undefined) return hit;\n const text = fixed(scale(digital), decimals);\n cache[slot] = text;\n return text;\n }\n return fixed(scale(digital), decimals);\n };\n}\n\n/**\n * Decimals for the time column.\n *\n * The interval between samples is 1/rate, which has a terminating decimal expansion of d\n * places exactly when 10^d divides evenly by the rate. Writing that many places makes sample\n * times exact rather than rounded, so `time_s * rate` comes back as a whole number instead\n * of 8191.999999.\n *\n * The search used to stop at nine places, and the comment here claimed \"every rate in common\n * use clears this — 256 Hz needs 8 places, 512 Hz needs 9\". The next two powers of two do\n * not: 1/1024 needs ten places and 1/2048 needs eleven, and those are the rates a BioSemi\n * ActiveTwo records at by default. Both fell through to the rounding fallback, so the two\n * most common high-rate EEG recordings got exactly the behaviour this function exists to\n * avoid — 0.0009766 for an interval of 0.0009765625.\n *\n * A rate of 2^a * 5^b terminates in max(a, b) places, so fifteen covers every power of two up\n * to 32768 Hz, far past anything that records biosignals. Rates with a repeating expansion\n * (3 Hz, say) still fall back to enough places to keep consecutive samples distinct, and that\n * fallback keeps its own cap.\n *\n * Fifteen and not more, because the test below has to stay exact: 10^16 is past 2^53, where a\n * double can no longer hold every integer, and `Number.isInteger(10 ** 17 / 3)` is true — so a\n * larger bound reports a terminating expansion for rates that have none, and 3 Hz would ask\n * for seventeen decimals of a number that repeats forever.\n *\n * The fallback is bounded by the same fifteen. It used to stop at nine, which defeated the\n * one thing it exists to do: at 3e10 Hz — non-terminating, so no exact expansion to find —\n * the interval is 3.3e-11 and nine places rounded every sample in a record to the same\n * timestamp. A column that cannot tell two samples apart is not keeping them distinct, which\n * is what this branch is for.\n */\nconst MAX_TIME_DECIMALS = 15;\n\nexport function timeDecimals(samplingRate: number): number {\n if (!(samplingRate > 0) || !Number.isFinite(samplingRate)) return 3;\n for (let d = 0; d <= MAX_TIME_DECIMALS; d++) {\n if (Number.isInteger(10 ** d / samplingRate)) return Math.max(3, d);\n }\n return Math.min(MAX_TIME_DECIMALS, Math.max(3, Math.ceil(Math.log10(samplingRate)) + 3));\n}\n\n/** Human-readable byte size for warnings and summaries. */\nexport function formatBytes(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB', 'TB'];\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit++;\n }\n let rounded = value >= 100 || unit === 0 ? Math.round(value) : Number(value.toFixed(1));\n // Rounding can carry into the next unit, and the unit was chosen before it: 1,048,575 bytes\n // is 1023.999 KB, which printed as \"1024 KB\". The same slip formatDuration below fixed by\n // rounding before splitting, one function up.\n if (rounded === 1024 && unit < units.length - 1) {\n unit++;\n rounded = 1;\n }\n return `${rounded} ${units[unit]}`;\n}\n\n/** Human-readable duration: 1h 05m 12s. */\nexport function formatDuration(seconds: number): string {\n // A duration that is not a number cannot be broken into hours and minutes, and saying so\n // beats the alternative: the fallback below rendered these as \"NaNs\" and \"Infinitys\".\n if (!Number.isFinite(seconds)) return 'unknown';\n\n /*\n Past 2^53 the decomposition stops being arithmetic and starts being noise.\n\n `total - h * 3600 - m * 60` cannot be exact once `total` exceeds what a double can hold\n as a whole number, and the error lands in the seconds field, where it shows up as a\n value that cannot exist. A header declaring a record duration of 1e300 printed:\n\n Duration 8.333333333333333e+296h 48m -2880s\n\n Forty-eight minutes and minus forty-eight seconds, under an hours field in exponent\n notation. The seconds are the honest form for a figure this size — nobody reads\n 285 million years as hours — and the record count and duration are printed beside it\n anyway, so a corrupt header stays just as visible.\n\n Written through `fixed` rather than by interpolation, which is the same 1e21 cliff again:\n `${seconds}` switches to exponent notation exactly where `toFixed` does, so the fallback\n put a token back that this tool's own parser refuses. `--start 4000000000000000000000` on\n a recording of three 1e21-second records was answered with\n\n --start \"4000000000000000000000\" is at or past the end of this 3e+21s recording.\n\n a sentence whose whole job is to say what window there is to ask for, ending in a length\n `--start` and `--duration` reject with \"uses an unknown unit \\\"e\\\"\". `formatSeconds` in\n time-range.ts was fixed for this in the other half of that same message, and its comment\n quotes this half as part of what was wrong. `fixed` expands these with BigInt, which is\n exact past 2^53 where a double carries no fraction anyway.\n */\n if (seconds < 0 || seconds >= Number.MAX_SAFE_INTEGER) return `${fixed(seconds, 0)}s`;\n\n // Round to the precision that will actually be printed BEFORE splitting into units.\n // Splitting first left the remainder to be rounded on its own, so 3599.9996 s decomposed\n // as 59 minutes and 59.9996 seconds and then printed as \"59m 60s\" — a duration that\n // cannot exist. Rounding first carries the extra second into the minute where it belongs.\n const total = Math.round(seconds * 1000) / 1000;\n /*\n A duration shorter than the rounding is not a duration of zero.\n\n Three decimals is the right precision for a recording measured in hours, and it collapses\n everything under half a millisecond to \"0s\". `repeating-fast.edf` — two records of 1e-15s,\n six samples, one of this repository's own fixtures — printed\n\n Duration 0s (2 records of 1e-15s)\n\n and refused `--start 0.5` with \"is at or past the end of this 0s recording\". A reader is\n told the file holds nothing, on the line they look at first, about a file that converts to\n six rows; the record duration two columns over already contradicts it. Same shape as the\n other end of this function, which stops decomposing past 2^53 and prints the seconds\n instead, and as the byte size above, which carries 1023.999 KB up rather than printing the\n \"1024 KB\" no unit has. None of the three is a rounding rule; each one refuses to print a\n form the quantity cannot take.\n\n Written through `plain` rather than interpolated, for the reason the comment above gives:\n `${1e-15}` is exponent notation, and the sentence this feeds is one whose whole job is to\n say what `--start` may be given — and `--start 1e-15s` is refused as an unknown unit \"e\".\n */\n if (total === 0 && seconds > 0) return `${plainSeconds(seconds)}s`;\n const h = Math.floor(total / 3600);\n const m = Math.floor((total % 3600) / 60);\n const s = Math.round((total - h * 3600 - m * 60) * 1000) / 1000;\n const sText = Number.isInteger(s) ? String(s) : s.toFixed(3).replace(/\\.?0+$/u, '');\n if (h > 0) return `${h}h ${String(m).padStart(2, '0')}m ${sText}s`;\n if (m > 0) return `${m}m ${sText}s`;\n return `${sText}s`;\n}\n\n/**\n * A length of time in a form the sentence it sits in can hold.\n *\n * `plain` expands a double to its full decimal form, which is what the columns of\n * annotations.csv need — one notation down a column, so a `merge` on it matches. Every message\n * that states a length of seconds took the same rendering, and a record duration is eight\n * characters of header, so `1e308` and `1e-320` both fit in one:\n *\n * Duration unknown (2 records of 1000000000000000000000 ... 000s)\n *\n * Three hundred and nine digits, on a line whose other half has just said the total cannot be\n * stated — and three lines under a RATE column rendering the same magnitude as `4.000e-308 Hz`,\n * because `formatRate` already falls back to exponent form when plain decimal stops carrying\n * the number. This is the failure `listed` was written for, one line over: a message is not\n * the place for everything a header is free to ask for.\n *\n * Plain while plain is *typable*, which is the reason the expansion is here at all —\n * `--start 1e-15s` is refused as an unknown unit `e`, so `repeating-fast.edf` reads\n * `0.000000000000001s` and can be acted on. Past the width of a line nothing is typable\n * either way: a reader cannot count three hundred digits any more than they can pass an\n * exponent, so the shortest exact form is the more honest of the two.\n *\n * Here rather than in cli/report.ts, where 0.8.95 wrote it for one half of one line. The\n * other half of that same line expanded a duration of 3e-308 to three hundred and ten\n * characters; so did the refusal naming a record duration that is not positive, and the hint\n * that says how wide a window has to be to hold a sample. All four state a length of seconds\n * out of a header, and the rule is one rule.\n */\nconst LONGEST_PLAIN_SECONDS = 30;\n\nexport function plainSeconds(seconds: number): string {\n const expanded = plain(seconds);\n return expanded.length <= LONGEST_PLAIN_SECONDS ? expanded : String(seconds);\n}\n\n/** A record can declare a great many samples; two arrays this size is the cost of caching. */\nconst MAX_CACHED_OFFSETS = 1 << 20;\n\n/**\n * How many cached offsets a conversion has left to spend.\n *\n * The cap used to be per rate group, and a file may hold as many rate groups as it has\n * channels. Twelve channels at twelve rates just under the cap — a 25 MB file — took\n * 1.66 GB and 36 seconds, where a 92 MB file at one rate takes 283 MB and finishes in a\n * fraction of that; twenty-four of them never finished at all. A per-group limit is not a\n * limit, since nothing bounds the number of groups.\n *\n * One budget for the whole conversion makes the single-group case identical to what it was\n * and the many-group case bounded. Groups ask in order of rate, fastest first, so the cache\n * goes to the tables with the most rows to write and the ones that miss out are the ones\n * that would have gained least.\n */\nexport interface OffsetBudget {\n remaining: number;\n}\n\nexport function newOffsetBudget(): OffsetBudget {\n return { remaining: MAX_CACHED_OFFSETS };\n}\n\n/**\n * Formats the time column, reusing the part of it that repeats.\n *\n * Every value cell is already cached — a channel has at most `digitalMax - digitalMin + 1`\n * distinct readings, so the same handful of strings serve millions of rows. The time column\n * had no such luck: it rises monotonically, so no two rows share a string and `toFixed` ran\n * once per row. On a ten-million-row conversion that was a third of the total time, more\n * than reading the file and writing the CSV put together.\n *\n * What repeats is the offset within a record. Sample `s` sits at `s / rate` from the start of\n * whichever record holds it, and there are only `samplesPerRecord` such offsets in the whole\n * recording. Splitting each into whole seconds and printed fraction turns the per-row work\n * into one integer addition and a concatenation:\n *\n * record starting at 42s, sample 7 of a 100 Hz record\n * -> 42 + 0 whole seconds, fraction \".070\" -> \"42.070\"\n *\n * The decomposition is only valid when the record starts on a whole, non-negative second,\n * which is what lets the fraction come entirely from the offset. A record starting at 0.5 s would mix the\n * two, so those fall back to formatting the sum directly. Continuous recordings start every\n * record at `index * recordDuration`, so this holds for all of them whose record duration is\n * a whole number of seconds, and for discontinuous files it holds per record depending on\n * where that record actually starts.\n */\nexport function makeTimeFormatter(\n samplesPerRecord: number,\n rate: number,\n decimals: number,\n budget: OffsetBudget = newOffsetBudget(),\n): (recordStart: number, sample: number) => string {\n const usable = rate > 0 && Number.isFinite(rate);\n\n /** The sum, formatted. Right only where the decomposition below does not apply. */\n const summed = (recordStart: number, sample: number): string =>\n fixed(recordStart + sample / rate, decimals);\n\n /*\n The same decomposition the table holds, computed on the spot.\n\n This used to be the sum, and that made the time column two columns depending on how much\n of the budget above was left. `recordStart + sample / rate` cannot carry the fraction once\n the record start is large: at a start of 1e9 and 30 kHz the exact instant is\n 1000000000.0000333333, and the double nearest the sum prints as 1000000000.00003338 —\n the last two places are the addition's rounding, not the recording's clock.\n\n Which of the two a rate group got was decided by the groups ahead of it, since they ask\n from one budget fastest first. So a recording whose fast channel takes the table left its\n slow channel adding doubles, and the same channel asked for on its own got the table:\n\n edf2csv far.edf --out whole -> 1000000000.00003338\n edf2csv far.edf --out one --channels slow -> 1000000000.00003333\n\n Two files from one recording disagreeing about when a sample was taken, in a tool whose\n documentation says `--channels` selects columns and changes nothing else. The narrowing\n sweep asserts exactly that and could not see it: its fixtures are small enough that the\n budget never runs out, so both conversions took the table.\n\n The sum is still the answer where the decomposition has nothing to stand on — a record\n starting on a fraction or before zero, where the whole part and the fraction are not\n separable — and past 1e21, where the whole part stops printing in full.\n */\n const composed = (recordStart: number, sample: number): string => {\n const text = fixed(sample / rate, decimals);\n const dot = text.indexOf('.');\n const whole = recordStart + (dot === -1 ? Number(text) : Number(text.slice(0, dot)));\n if (whole >= 1e21) return summed(recordStart, sample);\n return dot === -1 ? `${whole}` : `${whole}${text.slice(dot)}`;\n };\n\n /** What every row gets when the table is not there to be read. */\n const direct = (recordStart: number, sample: number): string =>\n usable && sample >= 0 && Number.isInteger(recordStart) && recordStart >= 0\n ? composed(recordStart, sample)\n : summed(recordStart, sample);\n\n if (!usable || samplesPerRecord <= 0 || samplesPerRecord > budget.remaining) {\n return direct;\n }\n budget.remaining -= samplesPerRecord;\n\n // Whole seconds and printed fraction of each offset, taken from the formatted offset\n // itself so that an offset which rounds up to the next second (0.9996 at three places)\n // carries that second rather than losing it.\n const wholeOffset = new Float64Array(samplesPerRecord);\n const fractionText: string[] = new Array<string>(samplesPerRecord);\n for (let sample = 0; sample < samplesPerRecord; sample++) {\n const text = fixed(sample / rate, decimals);\n const dot = text.indexOf('.');\n wholeOffset[sample] = dot === -1 ? Number(text) : Number(text.slice(0, dot));\n fractionText[sample] = dot === -1 ? '' : text.slice(dot);\n }\n\n return (recordStart: number, sample: number): string => {\n /*\n Non-negative only. Appending a fraction to a negative whole part moves the time the\n wrong way: a record at -5 s and an offset of half a second is -4.5, but \"-5\" and\n \".500\" concatenate to -5.500. Recording times start at zero, so this is unreachable\n from a well-formed file — an EDF+ timekeeping TAL is free to carry a negative onset\n though, and that is enough reason for the fast path to decline it.\n */\n if (\n sample < 0 ||\n sample >= samplesPerRecord ||\n !Number.isInteger(recordStart) ||\n recordStart < 0\n ) {\n return direct(recordStart, sample);\n }\n const whole = recordStart + (wholeOffset[sample] as number);\n /*\n The same 1e21 cliff `fixed` guards against, arriving through the back door.\n\n `${whole}` is the implicit Number-to-String conversion, which switches to exponent\n notation at 1e21 exactly as `toFixed` does — and then the cached fraction is glued onto\n the end of it, so the cell reads \"1e+21.000\". That is not a number in any notation:\n pandas and R both parse it as NaN, and a column of ordinary decimals ends in a run of\n them. A header may legitimately say `1e21` in its 8-character record-duration field, so\n three records are enough to reach it.\n\n The slow path already expands these with BigInt. One comparison per row keeps that\n correct without giving up the cache for the other twenty million.\n */\n if (whole >= 1e21) return summed(recordStart, sample);\n return `${whole}${fractionText[sample] as string}`;\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edf2csv",
3
- "version": "0.8.96",
3
+ "version": "0.8.98",
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",