edf2csv 0.9.5 → 0.9.7

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