edf2csv 0.9.12 → 0.9.14

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.
@@ -222,7 +222,23 @@ function assertFinite(value, optionName, input, allowNegative) {
222
222
  if (!Number.isFinite(value)) {
223
223
  throw new TimeRangeError(`${optionName} "${input}" is further from zero than a number of seconds can hold.`);
224
224
  }
225
- if (!allowNegative && value < 0) {
225
+ /*
226
+ Read as `=== true`, which is how every other flag in this package is read.
227
+
228
+ A truthiness test takes a value that is not a boolean as the opposite of what it says, and
229
+ this is exported: `parseTimeSpec` is the function the api page tells other people to point
230
+ their own users at, so its third argument arrives from a config file, a query string or a
231
+ wrapper that did not coerce, exactly as `assertOptions` describes for the six flags —
232
+
233
+ parseTimeSpec('-5s', '--duration', 'false') // -5
234
+ parseTimeSpec('-5s', '--duration', 'no') // -5
235
+ parseTimeSpec('-5s', '--duration', {}) // -5
236
+
237
+ — and the value it lets through is a negative length of time, which is the one thing this
238
+ line exists to stop. `--duration -5s` from the command line is refused, because the CLI
239
+ passes a real boolean.
240
+ */
241
+ if (allowNegative !== true && value < 0) {
226
242
  throw new TimeRangeError(`${optionName} "${input}" is not a valid non-negative time.`);
227
243
  }
228
244
  return value;
@@ -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, 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"]}
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 /*\n Read as `=== true`, which is how every other flag in this package is read.\n\n A truthiness test takes a value that is not a boolean as the opposite of what it says, and\n this is exported: `parseTimeSpec` is the function the api page tells other people to point\n their own users at, so its third argument arrives from a config file, a query string or a\n wrapper that did not coerce, exactly as `assertOptions` describes for the six flags —\n\n parseTimeSpec('-5s', '--duration', 'false') // -5\n parseTimeSpec('-5s', '--duration', 'no') // -5\n parseTimeSpec('-5s', '--duration', {}) // -5\n\n — and the value it lets through is a negative length of time, which is the one thing this\n line exists to stop. `--duration -5s` from the command line is refused, because the CLI\n passes a real boolean.\n */\n if (allowNegative !== true && 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"]}
@@ -107,8 +107,19 @@ export function decodeRecordAnnotations(bytes, recordIndex, carriesTimekeeping =
107
107
  A whole non-negative number, like the record bounds `readRecords` and `sampleAt` take:
108
108
  records are counted from zero, and this one is a position in the file rather than in a
109
109
  batch, so there is no upper bound to hold it to here.
110
+
111
+ Safe, though, which is a bound and not a guess. `Number.isInteger` is true of 1e300 and of
112
+ 2^53 + 2, and neither is a position any file has: past 2^53 a double stops being able to
113
+ tell one whole number from the next, so the value cannot name a record even in principle.
114
+ It was written onto every event and into `record_index`, where a join reads it —
115
+
116
+ decodeRecordAnnotations(bytes, 1e300).annotations[0].recordIndex // 1e+300
117
+
118
+ — and `1e+300` is not even a number of the form that column holds, since every other row
119
+ of it is plain digits. A file of 2^53 records at the format's smallest record duration is
120
+ longer than the age of the universe; a value past it came from arithmetic, not from a file.
110
121
  */
111
- if (!Number.isInteger(recordIndex) || recordIndex < 0) {
122
+ if (!Number.isSafeInteger(recordIndex) || recordIndex < 0) {
112
123
  throw new OptionError(`recordIndex must be the record's whole-number position in the file, got ` +
113
124
  `${describeValue(recordIndex)}. It is written onto every annotation this returns.`);
114
125
  }
@@ -1 +1 @@
1
- {"version":3,"file":"annotations.js","sourceRoot":"","sources":["../../src/edf/annotations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * EDF+ annotation (TAL) decoding.\n *\n * The annotations channel stores UTF-8 text in place of samples. Its bytes are a\n * run of Time-stamped Annotation Lists, each terminated by a NUL, with the rest\n * of the channel NUL-padded:\n *\n * +<onset>[<0x15><duration>]<0x14><text><0x14>...<0x00>\n *\n * The first TAL of every data record carries that record's start time; that is how an EDF+D\n * file states where each record actually sits in time. It may carry event text after that,\n * which the specification allows and writers use, so an unreadable one costs a position and\n * whatever events went with it — see `malformedTimekeepingWithText`.\n *\n * +1.25<0x15>0.5<0x14>Seizure onset<0x14><0x00>\n */\n\nimport { decodeText } from './bytes.js';\n// Crossing into convert/ as header.ts and reader.ts already do: the check belongs to the call.\nimport { OptionError, describeValue } from '../convert/options.js';\n\nconst SEP_TEXT = 0x14; // separates onset/duration from text, and text from text\nconst SEP_DURATION = 0x15; // separates onset from duration\nconst TAL_END = 0x00;\n\nconst TEXT_SEP_CHAR = String.fromCharCode(SEP_TEXT);\nconst DURATION_SEP_CHAR = String.fromCharCode(SEP_DURATION);\n\n/**\n * What EDF+ allows a TAL duration to look like, which is less than `Number()` allows.\n *\n * The header parser's DECIMAL_FIELD makes this argument at length and lists where the same\n * mistake had already been fixed: `#0x2` reaching channel 2 through `--channels`,\n * `--decimals 0o5` writing five places, `--jobs 0x10` running sixteen, a physical maximum of\n * `0x64` setting a whole calibration to 100. The annotation duration is the one field it did\n * not reach. `+1<0x15>0x10<0x14>Seizure` was exported with a duration_s of 16 — sixteen\n * seconds no writer wrote, in a column the documentation defines as the length the file\n * stated, exit 0 and no diagnostic. `0b11` is three the same way.\n *\n * A duration this cannot read is already a thing the parser knows how to say: the field is\n * left empty, counted, and reported. Sending these there costs nothing and states the truth,\n * which is that the file wrote something that is not a number of seconds.\n */\nconst DECIMAL_DURATION = /^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$/u;\n\nexport interface Annotation {\n /** Seconds from the start of the recording. */\n onset: number;\n /**\n * Seconds, or null when the TAL stated no duration that could be read.\n *\n * Null covers two cases the file distinguishes and this field does not: a TAL that omitted\n * the duration, and a TAL that stated one which is not a number. They are told apart by\n * `unreadableDurations`, which is what raises the warning; the value itself has nowhere\n * honest to put \"the file said `abc`\".\n */\n duration: number | null;\n text: string;\n /** Index of the data record this annotation was stored in. */\n recordIndex: number;\n /**\n * True when the file stated a duration that could not be read.\n *\n * `duration` is null either way, which is the ambiguity the counts beside it exist to\n * flag — and those counts were of the whole file while `annotations.csv` is filtered to\n * the requested window. A conversion of one second of a recording warned that \"1\n * annotation states a duration that is not a number, so its duration_s cell is empty\"\n * about an event two seconds outside it, and failed `--strict` for it. Carrying the fact\n * on the event lets the count be taken where the window has already been applied.\n */\n durationUnreadable?: boolean;\n}\n\nexport interface DecodedRecordAnnotations {\n /** Record start time in seconds, from the leading timekeeping TAL. */\n recordStart: number | null;\n annotations: Annotation[];\n /** Non-empty chunks that were not valid TALs, so the caller can report them. */\n malformed: number;\n /** Unreadable TALs in first position, which carry a record's start time, not an event. */\n malformedTimekeeping: number;\n /**\n * How many of those also carried event text, and so lost events as well as a position.\n *\n * A TAL in first position holds the record's start time, and may hold events after it — the\n * specification allows both in the one entry, and writers use it. When such a TAL cannot be\n * parsed, both are gone, and counting it only as lost timekeeping let the warning beside it\n * say \"No event was lost\" over a conversion that had just dropped four of them.\n *\n * Counted rather than inferred, because the sentence has to be right in the ordinary case\n * too: a bare timekeeping TAL really does lose no event, and that is nearly all of them.\n */\n malformedTimekeepingWithText: number;\n /**\n * Events kept whose stated duration could not be read.\n *\n * Counted apart again, for the same reason the two above are: the entry was exported and\n * nothing about it is missing except the one field, so calling it an entry that \"could not\n * be exported\" describes a loss that did not happen and hides the one that did.\n */\n unreadableDurations: number;\n /**\n * Events kept whose stated duration is a readable number below zero.\n *\n * Separate from the count above because the value survives: it is written to the CSV as\n * the file gave it, and what is wrong with it is arithmetic rather than parsing.\n */\n negativeDurations: number;\n}\n\n/**\n * Decode one data record's annotation bytes.\n *\n * Malformed TALs are skipped rather than thrown, because a single bad annotation\n * should not cost the user an entire conversion. The count of skipped chunks is\n * returned so the caller can tell the user rather than losing them in silence.\n */\nexport function decodeRecordAnnotations(\n bytes: Uint8Array,\n recordIndex: number,\n carriesTimekeeping = true,\n): DecodedRecordAnnotations {\n /*\n The bytes, checked before anything is read out of them.\n\n The api page offers this function on its own — \"For decoding annotation bytes yourself,\n `decodeRecordAnnotations(bytes, recordIndex)` handles one record's worth of the channel\" —\n so it is reached by a caller holding a slice they cut themselves. Hand it anything but a\n typed array and the loop below reported the shape of its own local:\n\n decodeRecordAnnotations(Buffer.from(text).toString(), 0)\n TypeError: bytes.subarray is not a function\n\n which names a variable inside this function and a method the caller never called. The\n same `OptionError` the two path-taking exports raise, for the reason given there: the call\n is what is wrong, not the recording.\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 both of those reach past this check\n into the loop below — which reads `bytes[i]` and calls `bytes.subarray`. A typed array of\n wider elements has both, so it does not fail; it decodes the wrong thing and says nothing:\n\n decodeRecordAnnotations(new Float64Array(2), 0)\n { recordStart: null, annotations: [], malformed: 0, ... }\n\n Sixteen bytes of annotation channel, read as two doubles, reported as a record with\n nothing malformed in it — which is what this function says about a record that really is\n empty. A `DataView` has no index accessor at all, so every byte reads `undefined`.\n\n `BYTES_PER_ELEMENT === 1` is the question being asked: true of `Uint8Array`, `Int8Array`,\n `Uint8ClampedArray` and the `Buffer` `annotationBytes` hands this, undefined on a\n `DataView`. The same check `parseHeader` makes of its own bytes.\n */\n if (!ArrayBuffer.isView(bytes) || (bytes as { BYTES_PER_ELEMENT?: number }).BYTES_PER_ELEMENT !== 1) {\n // A view is named rather than dumped, for the reason 0.8.84 gives: a hundred doubles\n // handed back is the caller's own data, not a description of what is wrong with it.\n throw new OptionError(\n ArrayBuffer.isView(bytes)\n ? `bytes is a ${(bytes as object).constructor?.name ?? 'view'}, which is a view of an ` +\n `ArrayBuffer but not of bytes. One record's annotation channel is a Uint8Array or a ` +\n `Buffer, one byte an element — annotationBytes returns one.`\n : `bytes must be one record's annotation channel, got ${describeValue(bytes)}.`,\n );\n }\n /*\n And the second argument, which this function does not read — it writes it.\n\n `recordIndex` is copied onto every `Annotation` this call produces, and `Annotation`\n declares it a number. Nothing checked that it was one, so whatever was passed came back\n out in the event list:\n\n decodeRecordAnnotations(bytes, 'x').annotations[0].recordIndex // 'x'\n decodeRecordAnnotations(bytes).annotations[0].recordIndex // absent\n\n The second is the ordinary mistake, since the page shows this called as\n `decodeRecordAnnotations(bytes, recordIndex)` beside `annotationBytes(batch, recordOffset,\n signal)` and the two take their record different ways. It produces events with the field\n missing altogether, which `record_index` in annotations.csv is written from, and which a\n caller joining events back to records reads as a record of `undefined`.\n\n A whole non-negative number, like the record bounds `readRecords` and `sampleAt` take:\n records are counted from zero, and this one is a position in the file rather than in a\n batch, so there is no upper bound to hold it to here.\n */\n if (!Number.isInteger(recordIndex) || recordIndex < 0) {\n throw new OptionError(\n `recordIndex must be the record's whole-number position in the file, got ` +\n `${describeValue(recordIndex)}. It is written onto every annotation this returns.`,\n );\n }\n const annotations: Annotation[] = [];\n let recordStart: number | null = null;\n let isFirstTal = true;\n let malformed = 0;\n let malformedTimekeeping = 0;\n let malformedTimekeepingWithText = 0;\n let unreadableDurations = 0;\n let negativeDurations = 0;\n\n let start = 0;\n for (let i = 0; i <= bytes.length; i++) {\n if (i !== bytes.length && bytes[i] !== TAL_END) continue;\n\n if (i > start) {\n const chunk = bytes.subarray(start, i);\n /*\n Padding is not a lost annotation.\n\n The spec pads the slot with NUL, which the loop above already skips because it is what\n separates one TAL from the next. Writers pad with spaces instead, and a run of spaces\n after the last TAL is a non-empty chunk — so a file holding one perfectly readable\n event, exported in full, was told \"2 annotation entries were unreadable and could not\n be exported\", one per record. Nothing was lost. Under --strict that is a failed run\n over the whitespace at the end of a slot.\n\n Only whitespace. A chunk of anything else that does not parse is a real loss and is\n still counted, which is the case this warning exists for.\n */\n if (chunk.every(isPaddingByte)) {\n start = i + 1;\n continue;\n }\n const parsed = parseTal(chunk, recordIndex);\n\n // The timekeeping TAL is the one in first POSITION, whether or not it decodes.\n // Clearing this flag only on a successful parse meant that an unreadable first TAL\n // promoted the next ordinary annotation to timekeeping, and its onset silently\n // became the record's start time — shifting every sample in that record. Leaving\n // recordStart null instead is what the caller already handles, with a fallback\n // timestamp and an ANNOTATION_DECODE_FAILED warning naming the record.\n /*\n Only one annotation channel carries a record's start time.\n\n This flagged the first TAL of *every* annotation channel as timekeeping. In a second\n channel the first TAL is an ordinary event — so when one failed to parse, the event\n was dropped and counted as a lost timekeeping entry, which produced the warning\n \"3 data records carry a timekeeping annotation that could not be read\" followed by\n \"No event was lost\". Three events had been lost, and the timekeeping in that file was\n perfectly readable. Both sentences false, about the same three records.\n */\n const isTimekeeping = isFirstTal && carriesTimekeeping;\n isFirstTal = false;\n\n if (parsed) {\n if (isTimekeeping) recordStart = parsed.onset;\n for (const annotation of parsed.annotations) annotations.push(annotation);\n unreadableDurations += parsed.unreadableDurations;\n negativeDurations += parsed.negativeDurations;\n } else {\n /*\n Counted apart from the events, because losing one is a different loss.\n\n A timekeeping TAL is never exported — it says where the record sits, not what\n happened — so counting it among the entries that \"could not be exported\" both\n overstated what was lost from annotations.csv and said nothing about the thing that\n actually went missing, which is a record's position in time. A file with one\n unreadable timekeeping TAL and three perfectly good events reported \"1 annotation\n entry was unreadable and could not be exported\" while exporting all three.\n\n The other direction is just as wrong. A first-position TAL may carry events after\n the start time, and when one of those cannot be parsed the events go with it — so\n counting it only as lost timekeeping produced the opposite false sentence: \"No event\n was lost\", printed over a run whose annotations.csv had gone from six rows to two.\n It is one entry that could not be exported and one record with no position, and it\n is counted as both.\n */\n if (isTimekeeping) {\n malformedTimekeeping++;\n if (carriesAnnotationText(chunk)) {\n malformedTimekeepingWithText++;\n malformed++;\n }\n } else malformed++;\n }\n }\n start = i + 1;\n }\n\n return {\n recordStart,\n annotations,\n malformed,\n malformedTimekeeping,\n malformedTimekeepingWithText,\n unreadableDurations,\n negativeDurations,\n };\n}\n\ninterface ParsedTal {\n onset: number;\n annotations: Annotation[];\n /** How many of those annotations carry a duration the file stated and this could not read. */\n unreadableDurations: number;\n /** How many carry a duration that read as a number below zero. */\n negativeDurations: number;\n}\n\n/**\n * Whether a TAL that could not be parsed still carried event text.\n *\n * A TAL is `onset[<0x15>duration]<0x14>text<0x14>...`, so anything other than padding after\n * the first 0x14 is a description the file meant to export. Read from the raw chunk, since by\n * the time this is asked the parse has already failed and there is no structure to consult.\n */\nfunction carriesAnnotationText(chunk: Uint8Array): boolean {\n let afterSeparator = false;\n for (const byte of chunk) {\n if (byte === 0x14) {\n afterSeparator = true;\n continue;\n }\n if (afterSeparator && !isPaddingByte(byte)) return true;\n }\n return false;\n}\n\n/** Space, tab, CR, LF or NUL — what a writer fills the rest of the slot with. */\nfunction isPaddingByte(byte: number): boolean {\n return byte === 0x20 || byte === 0x09 || byte === 0x0d || byte === 0x0a || byte === 0x00;\n}\n\nfunction parseTal(chunk: Uint8Array, recordIndex: number): ParsedTal | null {\n // The onset must be explicitly signed; anything else is not a TAL.\n const first = chunk[0];\n if (first !== 0x2b /* + */ && first !== 0x2d /* - */) return null;\n\n const text = decodeText(chunk);\n const parts = text.split(TEXT_SEP_CHAR);\n const head = parts[0] ?? '';\n\n let onsetText = head;\n let durationText: string | null = null;\n const durationSep = head.indexOf(DURATION_SEP_CHAR);\n if (durationSep >= 0) {\n onsetText = head.slice(0, durationSep);\n durationText = head.slice(durationSep + 1);\n }\n\n const onset = Number(onsetText);\n if (!Number.isFinite(onset)) return null;\n\n /*\n A duration the file stated and this could not read is not the same as no duration.\n\n Both came out as `null` and so as an empty `duration_s` cell, which the documentation\n defines as meaning the file gave no duration — so an event whose duration was written as\n `abc` was exported as an event with no duration, indistinguishable from one beside it\n that genuinely had none, and nothing anywhere said a field had been dropped. The onset is\n already held to this standard: one that is not a number costs the whole TAL and is\n reported. A duration is one field of an otherwise readable event, so the event is kept —\n but it is counted, and the run says so.\n */\n /*\n Fill in the field is an absent duration, not a zero.\n\n `Number` reads a run of whitespace as 0 — the rule that makes `Number('')` zero, one step\n along — so a TAL whose duration field held nothing but the writer's padding was exported\n with a `duration_s` of `0`, byte-identical to the event beside it whose file really did\n say `0`. An instantaneous event is a claim about the recording, and no writer made it;\n inventing it is the one thing this tool does not do, and it did so in silence, exit 0.\n\n The empty field this condition already declines is the same field without the fill in it,\n so padding takes the same answer: the file stated no duration. `trim` empties exactly the\n strings `Number` would otherwise have swallowed into a zero, so ` 2.5 ` still reads as\n 2.5 and `abc` is still counted as unreadable below.\n */\n let duration: number | null = null;\n let durationUnreadable = false;\n if (durationText !== null && durationText.trim() !== '') {\n const stated = durationText.trim();\n const d = DECIMAL_DURATION.test(stated) ? Number(stated) : NaN;\n if (Number.isFinite(d)) duration = d;\n else durationUnreadable = true;\n }\n\n /*\n A duration is a length of time, and a length below zero is not one.\n\n The value is kept and written as the file gave it — inventing a zero, or dropping it to\n an empty cell, would put a number in annotations.csv that no writer wrote, which is the\n one thing this tool does not do. But it is reported, because everything downstream\n quietly does the wrong thing with it: the recipe this documentation gives for the samples\n an event covers is `onset_s + duration_s`, which for a duration of -3 ends three seconds\n before the event starts and selects nothing at all, with no error anywhere.\n */\n const durationNegative = duration !== null && duration < 0;\n\n const annotations: Annotation[] = [];\n for (const raw of parts.slice(1)) {\n /*\n A trailing separator yields an empty segment; a timekeeping TAL is all empty.\n\n Whitespace counts as empty here, which it did not, and the padding at the end of the\n slot became an event. The chunk loop above already refuses to call a run of spaces a\n lost annotation — but it only sees chunks between NULs, and a writer that leaves its\n last TAL unterminated puts the fill *inside* the chunk, after the final 0x14. Split on\n that separator it is a text segment like any other, and \" \" is not \"\".\n\n A file holding two events exported four rows: `0.5,,Lights off,0` and `0.5,, ,0`,\n twice, sharing the real event's onset, with annotations_written and the run summary\n agreeing with the inflated number and nothing warned. An event whose description is\n genuinely nothing but spaces cannot be told from fill, and inventing rows out of fill\n is the worse of the two answers.\n */\n if ([...raw].every((c) => isPaddingByte(c.charCodeAt(0)))) continue;\n annotations.push({\n onset,\n duration,\n text: raw,\n recordIndex,\n ...(durationUnreadable ? { durationUnreadable: true } : {}),\n });\n }\n\n // Per event rather than per TAL: one TAL may carry several texts, and each becomes a row\n // of annotations.csv with the same cell in it.\n return {\n onset,\n annotations,\n unreadableDurations: durationUnreadable ? annotations.length : 0,\n negativeDurations: durationNegative ? annotations.length : 0,\n };\n}\n\nexport { SEP_TEXT, SEP_DURATION, TAL_END };\n"]}
1
+ {"version":3,"file":"annotations.js","sourceRoot":"","sources":["../../src/edf/annotations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * EDF+ annotation (TAL) decoding.\n *\n * The annotations channel stores UTF-8 text in place of samples. Its bytes are a\n * run of Time-stamped Annotation Lists, each terminated by a NUL, with the rest\n * of the channel NUL-padded:\n *\n * +<onset>[<0x15><duration>]<0x14><text><0x14>...<0x00>\n *\n * The first TAL of every data record carries that record's start time; that is how an EDF+D\n * file states where each record actually sits in time. It may carry event text after that,\n * which the specification allows and writers use, so an unreadable one costs a position and\n * whatever events went with it — see `malformedTimekeepingWithText`.\n *\n * +1.25<0x15>0.5<0x14>Seizure onset<0x14><0x00>\n */\n\nimport { decodeText } from './bytes.js';\n// Crossing into convert/ as header.ts and reader.ts already do: the check belongs to the call.\nimport { OptionError, describeValue } from '../convert/options.js';\n\nconst SEP_TEXT = 0x14; // separates onset/duration from text, and text from text\nconst SEP_DURATION = 0x15; // separates onset from duration\nconst TAL_END = 0x00;\n\nconst TEXT_SEP_CHAR = String.fromCharCode(SEP_TEXT);\nconst DURATION_SEP_CHAR = String.fromCharCode(SEP_DURATION);\n\n/**\n * What EDF+ allows a TAL duration to look like, which is less than `Number()` allows.\n *\n * The header parser's DECIMAL_FIELD makes this argument at length and lists where the same\n * mistake had already been fixed: `#0x2` reaching channel 2 through `--channels`,\n * `--decimals 0o5` writing five places, `--jobs 0x10` running sixteen, a physical maximum of\n * `0x64` setting a whole calibration to 100. The annotation duration is the one field it did\n * not reach. `+1<0x15>0x10<0x14>Seizure` was exported with a duration_s of 16 — sixteen\n * seconds no writer wrote, in a column the documentation defines as the length the file\n * stated, exit 0 and no diagnostic. `0b11` is three the same way.\n *\n * A duration this cannot read is already a thing the parser knows how to say: the field is\n * left empty, counted, and reported. Sending these there costs nothing and states the truth,\n * which is that the file wrote something that is not a number of seconds.\n */\nconst DECIMAL_DURATION = /^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$/u;\n\nexport interface Annotation {\n /** Seconds from the start of the recording. */\n onset: number;\n /**\n * Seconds, or null when the TAL stated no duration that could be read.\n *\n * Null covers two cases the file distinguishes and this field does not: a TAL that omitted\n * the duration, and a TAL that stated one which is not a number. They are told apart by\n * `unreadableDurations`, which is what raises the warning; the value itself has nowhere\n * honest to put \"the file said `abc`\".\n */\n duration: number | null;\n text: string;\n /** Index of the data record this annotation was stored in. */\n recordIndex: number;\n /**\n * True when the file stated a duration that could not be read.\n *\n * `duration` is null either way, which is the ambiguity the counts beside it exist to\n * flag — and those counts were of the whole file while `annotations.csv` is filtered to\n * the requested window. A conversion of one second of a recording warned that \"1\n * annotation states a duration that is not a number, so its duration_s cell is empty\"\n * about an event two seconds outside it, and failed `--strict` for it. Carrying the fact\n * on the event lets the count be taken where the window has already been applied.\n */\n durationUnreadable?: boolean;\n}\n\nexport interface DecodedRecordAnnotations {\n /** Record start time in seconds, from the leading timekeeping TAL. */\n recordStart: number | null;\n annotations: Annotation[];\n /** Non-empty chunks that were not valid TALs, so the caller can report them. */\n malformed: number;\n /** Unreadable TALs in first position, which carry a record's start time, not an event. */\n malformedTimekeeping: number;\n /**\n * How many of those also carried event text, and so lost events as well as a position.\n *\n * A TAL in first position holds the record's start time, and may hold events after it — the\n * specification allows both in the one entry, and writers use it. When such a TAL cannot be\n * parsed, both are gone, and counting it only as lost timekeeping let the warning beside it\n * say \"No event was lost\" over a conversion that had just dropped four of them.\n *\n * Counted rather than inferred, because the sentence has to be right in the ordinary case\n * too: a bare timekeeping TAL really does lose no event, and that is nearly all of them.\n */\n malformedTimekeepingWithText: number;\n /**\n * Events kept whose stated duration could not be read.\n *\n * Counted apart again, for the same reason the two above are: the entry was exported and\n * nothing about it is missing except the one field, so calling it an entry that \"could not\n * be exported\" describes a loss that did not happen and hides the one that did.\n */\n unreadableDurations: number;\n /**\n * Events kept whose stated duration is a readable number below zero.\n *\n * Separate from the count above because the value survives: it is written to the CSV as\n * the file gave it, and what is wrong with it is arithmetic rather than parsing.\n */\n negativeDurations: number;\n}\n\n/**\n * Decode one data record's annotation bytes.\n *\n * Malformed TALs are skipped rather than thrown, because a single bad annotation\n * should not cost the user an entire conversion. The count of skipped chunks is\n * returned so the caller can tell the user rather than losing them in silence.\n */\nexport function decodeRecordAnnotations(\n bytes: Uint8Array,\n recordIndex: number,\n carriesTimekeeping = true,\n): DecodedRecordAnnotations {\n /*\n The bytes, checked before anything is read out of them.\n\n The api page offers this function on its own — \"For decoding annotation bytes yourself,\n `decodeRecordAnnotations(bytes, recordIndex)` handles one record's worth of the channel\" —\n so it is reached by a caller holding a slice they cut themselves. Hand it anything but a\n typed array and the loop below reported the shape of its own local:\n\n decodeRecordAnnotations(Buffer.from(text).toString(), 0)\n TypeError: bytes.subarray is not a function\n\n which names a variable inside this function and a method the caller never called. The\n same `OptionError` the two path-taking exports raise, for the reason given there: the call\n is what is wrong, not the recording.\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 both of those reach past this check\n into the loop below — which reads `bytes[i]` and calls `bytes.subarray`. A typed array of\n wider elements has both, so it does not fail; it decodes the wrong thing and says nothing:\n\n decodeRecordAnnotations(new Float64Array(2), 0)\n { recordStart: null, annotations: [], malformed: 0, ... }\n\n Sixteen bytes of annotation channel, read as two doubles, reported as a record with\n nothing malformed in it — which is what this function says about a record that really is\n empty. A `DataView` has no index accessor at all, so every byte reads `undefined`.\n\n `BYTES_PER_ELEMENT === 1` is the question being asked: true of `Uint8Array`, `Int8Array`,\n `Uint8ClampedArray` and the `Buffer` `annotationBytes` hands this, undefined on a\n `DataView`. The same check `parseHeader` makes of its own bytes.\n */\n if (!ArrayBuffer.isView(bytes) || (bytes as { BYTES_PER_ELEMENT?: number }).BYTES_PER_ELEMENT !== 1) {\n // A view is named rather than dumped, for the reason 0.8.84 gives: a hundred doubles\n // handed back is the caller's own data, not a description of what is wrong with it.\n throw new OptionError(\n ArrayBuffer.isView(bytes)\n ? `bytes is a ${(bytes as object).constructor?.name ?? 'view'}, which is a view of an ` +\n `ArrayBuffer but not of bytes. One record's annotation channel is a Uint8Array or a ` +\n `Buffer, one byte an element — annotationBytes returns one.`\n : `bytes must be one record's annotation channel, got ${describeValue(bytes)}.`,\n );\n }\n /*\n And the second argument, which this function does not read — it writes it.\n\n `recordIndex` is copied onto every `Annotation` this call produces, and `Annotation`\n declares it a number. Nothing checked that it was one, so whatever was passed came back\n out in the event list:\n\n decodeRecordAnnotations(bytes, 'x').annotations[0].recordIndex // 'x'\n decodeRecordAnnotations(bytes).annotations[0].recordIndex // absent\n\n The second is the ordinary mistake, since the page shows this called as\n `decodeRecordAnnotations(bytes, recordIndex)` beside `annotationBytes(batch, recordOffset,\n signal)` and the two take their record different ways. It produces events with the field\n missing altogether, which `record_index` in annotations.csv is written from, and which a\n caller joining events back to records reads as a record of `undefined`.\n\n A whole non-negative number, like the record bounds `readRecords` and `sampleAt` take:\n records are counted from zero, and this one is a position in the file rather than in a\n batch, so there is no upper bound to hold it to here.\n\n Safe, though, which is a bound and not a guess. `Number.isInteger` is true of 1e300 and of\n 2^53 + 2, and neither is a position any file has: past 2^53 a double stops being able to\n tell one whole number from the next, so the value cannot name a record even in principle.\n It was written onto every event and into `record_index`, where a join reads it —\n\n decodeRecordAnnotations(bytes, 1e300).annotations[0].recordIndex // 1e+300\n\n — and `1e+300` is not even a number of the form that column holds, since every other row\n of it is plain digits. A file of 2^53 records at the format's smallest record duration is\n longer than the age of the universe; a value past it came from arithmetic, not from a file.\n */\n if (!Number.isSafeInteger(recordIndex) || recordIndex < 0) {\n throw new OptionError(\n `recordIndex must be the record's whole-number position in the file, got ` +\n `${describeValue(recordIndex)}. It is written onto every annotation this returns.`,\n );\n }\n const annotations: Annotation[] = [];\n let recordStart: number | null = null;\n let isFirstTal = true;\n let malformed = 0;\n let malformedTimekeeping = 0;\n let malformedTimekeepingWithText = 0;\n let unreadableDurations = 0;\n let negativeDurations = 0;\n\n let start = 0;\n for (let i = 0; i <= bytes.length; i++) {\n if (i !== bytes.length && bytes[i] !== TAL_END) continue;\n\n if (i > start) {\n const chunk = bytes.subarray(start, i);\n /*\n Padding is not a lost annotation.\n\n The spec pads the slot with NUL, which the loop above already skips because it is what\n separates one TAL from the next. Writers pad with spaces instead, and a run of spaces\n after the last TAL is a non-empty chunk — so a file holding one perfectly readable\n event, exported in full, was told \"2 annotation entries were unreadable and could not\n be exported\", one per record. Nothing was lost. Under --strict that is a failed run\n over the whitespace at the end of a slot.\n\n Only whitespace. A chunk of anything else that does not parse is a real loss and is\n still counted, which is the case this warning exists for.\n */\n if (chunk.every(isPaddingByte)) {\n start = i + 1;\n continue;\n }\n const parsed = parseTal(chunk, recordIndex);\n\n // The timekeeping TAL is the one in first POSITION, whether or not it decodes.\n // Clearing this flag only on a successful parse meant that an unreadable first TAL\n // promoted the next ordinary annotation to timekeeping, and its onset silently\n // became the record's start time — shifting every sample in that record. Leaving\n // recordStart null instead is what the caller already handles, with a fallback\n // timestamp and an ANNOTATION_DECODE_FAILED warning naming the record.\n /*\n Only one annotation channel carries a record's start time.\n\n This flagged the first TAL of *every* annotation channel as timekeeping. In a second\n channel the first TAL is an ordinary event — so when one failed to parse, the event\n was dropped and counted as a lost timekeeping entry, which produced the warning\n \"3 data records carry a timekeeping annotation that could not be read\" followed by\n \"No event was lost\". Three events had been lost, and the timekeeping in that file was\n perfectly readable. Both sentences false, about the same three records.\n */\n const isTimekeeping = isFirstTal && carriesTimekeeping;\n isFirstTal = false;\n\n if (parsed) {\n if (isTimekeeping) recordStart = parsed.onset;\n for (const annotation of parsed.annotations) annotations.push(annotation);\n unreadableDurations += parsed.unreadableDurations;\n negativeDurations += parsed.negativeDurations;\n } else {\n /*\n Counted apart from the events, because losing one is a different loss.\n\n A timekeeping TAL is never exported — it says where the record sits, not what\n happened — so counting it among the entries that \"could not be exported\" both\n overstated what was lost from annotations.csv and said nothing about the thing that\n actually went missing, which is a record's position in time. A file with one\n unreadable timekeeping TAL and three perfectly good events reported \"1 annotation\n entry was unreadable and could not be exported\" while exporting all three.\n\n The other direction is just as wrong. A first-position TAL may carry events after\n the start time, and when one of those cannot be parsed the events go with it — so\n counting it only as lost timekeeping produced the opposite false sentence: \"No event\n was lost\", printed over a run whose annotations.csv had gone from six rows to two.\n It is one entry that could not be exported and one record with no position, and it\n is counted as both.\n */\n if (isTimekeeping) {\n malformedTimekeeping++;\n if (carriesAnnotationText(chunk)) {\n malformedTimekeepingWithText++;\n malformed++;\n }\n } else malformed++;\n }\n }\n start = i + 1;\n }\n\n return {\n recordStart,\n annotations,\n malformed,\n malformedTimekeeping,\n malformedTimekeepingWithText,\n unreadableDurations,\n negativeDurations,\n };\n}\n\ninterface ParsedTal {\n onset: number;\n annotations: Annotation[];\n /** How many of those annotations carry a duration the file stated and this could not read. */\n unreadableDurations: number;\n /** How many carry a duration that read as a number below zero. */\n negativeDurations: number;\n}\n\n/**\n * Whether a TAL that could not be parsed still carried event text.\n *\n * A TAL is `onset[<0x15>duration]<0x14>text<0x14>...`, so anything other than padding after\n * the first 0x14 is a description the file meant to export. Read from the raw chunk, since by\n * the time this is asked the parse has already failed and there is no structure to consult.\n */\nfunction carriesAnnotationText(chunk: Uint8Array): boolean {\n let afterSeparator = false;\n for (const byte of chunk) {\n if (byte === 0x14) {\n afterSeparator = true;\n continue;\n }\n if (afterSeparator && !isPaddingByte(byte)) return true;\n }\n return false;\n}\n\n/** Space, tab, CR, LF or NUL — what a writer fills the rest of the slot with. */\nfunction isPaddingByte(byte: number): boolean {\n return byte === 0x20 || byte === 0x09 || byte === 0x0d || byte === 0x0a || byte === 0x00;\n}\n\nfunction parseTal(chunk: Uint8Array, recordIndex: number): ParsedTal | null {\n // The onset must be explicitly signed; anything else is not a TAL.\n const first = chunk[0];\n if (first !== 0x2b /* + */ && first !== 0x2d /* - */) return null;\n\n const text = decodeText(chunk);\n const parts = text.split(TEXT_SEP_CHAR);\n const head = parts[0] ?? '';\n\n let onsetText = head;\n let durationText: string | null = null;\n const durationSep = head.indexOf(DURATION_SEP_CHAR);\n if (durationSep >= 0) {\n onsetText = head.slice(0, durationSep);\n durationText = head.slice(durationSep + 1);\n }\n\n const onset = Number(onsetText);\n if (!Number.isFinite(onset)) return null;\n\n /*\n A duration the file stated and this could not read is not the same as no duration.\n\n Both came out as `null` and so as an empty `duration_s` cell, which the documentation\n defines as meaning the file gave no duration — so an event whose duration was written as\n `abc` was exported as an event with no duration, indistinguishable from one beside it\n that genuinely had none, and nothing anywhere said a field had been dropped. The onset is\n already held to this standard: one that is not a number costs the whole TAL and is\n reported. A duration is one field of an otherwise readable event, so the event is kept —\n but it is counted, and the run says so.\n */\n /*\n Fill in the field is an absent duration, not a zero.\n\n `Number` reads a run of whitespace as 0 — the rule that makes `Number('')` zero, one step\n along — so a TAL whose duration field held nothing but the writer's padding was exported\n with a `duration_s` of `0`, byte-identical to the event beside it whose file really did\n say `0`. An instantaneous event is a claim about the recording, and no writer made it;\n inventing it is the one thing this tool does not do, and it did so in silence, exit 0.\n\n The empty field this condition already declines is the same field without the fill in it,\n so padding takes the same answer: the file stated no duration. `trim` empties exactly the\n strings `Number` would otherwise have swallowed into a zero, so ` 2.5 ` still reads as\n 2.5 and `abc` is still counted as unreadable below.\n */\n let duration: number | null = null;\n let durationUnreadable = false;\n if (durationText !== null && durationText.trim() !== '') {\n const stated = durationText.trim();\n const d = DECIMAL_DURATION.test(stated) ? Number(stated) : NaN;\n if (Number.isFinite(d)) duration = d;\n else durationUnreadable = true;\n }\n\n /*\n A duration is a length of time, and a length below zero is not one.\n\n The value is kept and written as the file gave it — inventing a zero, or dropping it to\n an empty cell, would put a number in annotations.csv that no writer wrote, which is the\n one thing this tool does not do. But it is reported, because everything downstream\n quietly does the wrong thing with it: the recipe this documentation gives for the samples\n an event covers is `onset_s + duration_s`, which for a duration of -3 ends three seconds\n before the event starts and selects nothing at all, with no error anywhere.\n */\n const durationNegative = duration !== null && duration < 0;\n\n const annotations: Annotation[] = [];\n for (const raw of parts.slice(1)) {\n /*\n A trailing separator yields an empty segment; a timekeeping TAL is all empty.\n\n Whitespace counts as empty here, which it did not, and the padding at the end of the\n slot became an event. The chunk loop above already refuses to call a run of spaces a\n lost annotation — but it only sees chunks between NULs, and a writer that leaves its\n last TAL unterminated puts the fill *inside* the chunk, after the final 0x14. Split on\n that separator it is a text segment like any other, and \" \" is not \"\".\n\n A file holding two events exported four rows: `0.5,,Lights off,0` and `0.5,, ,0`,\n twice, sharing the real event's onset, with annotations_written and the run summary\n agreeing with the inflated number and nothing warned. An event whose description is\n genuinely nothing but spaces cannot be told from fill, and inventing rows out of fill\n is the worse of the two answers.\n */\n if ([...raw].every((c) => isPaddingByte(c.charCodeAt(0)))) continue;\n annotations.push({\n onset,\n duration,\n text: raw,\n recordIndex,\n ...(durationUnreadable ? { durationUnreadable: true } : {}),\n });\n }\n\n // Per event rather than per TAL: one TAL may carry several texts, and each becomes a row\n // of annotations.csv with the same cell in it.\n return {\n onset,\n annotations,\n unreadableDurations: durationUnreadable ? annotations.length : 0,\n negativeDurations: durationNegative ? annotations.length : 0,\n };\n}\n\nexport { SEP_TEXT, SEP_DURATION, TAL_END };\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edf2csv",
3
- "version": "0.9.12",
3
+ "version": "0.9.14",
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",