edfcore 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +319 -0
- package/dist/bytes/latin1.d.ts +43 -0
- package/dist/bytes/latin1.d.ts.map +1 -0
- package/dist/bytes/latin1.js +84 -0
- package/dist/bytes/latin1.js.map +1 -0
- package/dist/bytes/numbers.d.ts +45 -0
- package/dist/bytes/numbers.d.ts.map +1 -0
- package/dist/bytes/numbers.js +87 -0
- package/dist/bytes/numbers.js.map +1 -0
- package/dist/bytes/view.d.ts +27 -0
- package/dist/bytes/view.d.ts.map +1 -0
- package/dist/bytes/view.js +53 -0
- package/dist/bytes/view.js.map +1 -0
- package/dist/constants.d.ts +113 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +83 -0
- package/dist/constants.js.map +1 -0
- package/dist/decode/digital.d.ts +49 -0
- package/dist/decode/digital.d.ts.map +1 -0
- package/dist/decode/digital.js +180 -0
- package/dist/decode/digital.js.map +1 -0
- package/dist/decode/physical.d.ts +37 -0
- package/dist/decode/physical.d.ts.map +1 -0
- package/dist/decode/physical.js +178 -0
- package/dist/decode/physical.js.map +1 -0
- package/dist/diagnostics/codes.d.ts +38 -0
- package/dist/diagnostics/codes.d.ts.map +1 -0
- package/dist/diagnostics/codes.js +134 -0
- package/dist/diagnostics/codes.js.map +1 -0
- package/dist/diagnostics/collector.d.ts +76 -0
- package/dist/diagnostics/collector.d.ts.map +1 -0
- package/dist/diagnostics/collector.js +95 -0
- package/dist/diagnostics/collector.js.map +1 -0
- package/dist/diagnostics/format.d.ts +21 -0
- package/dist/diagnostics/format.d.ts.map +1 -0
- package/dist/diagnostics/format.js +125 -0
- package/dist/diagnostics/format.js.map +1 -0
- package/dist/errors.d.ts +143 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +135 -0
- package/dist/errors.js.map +1 -0
- package/dist/header/dates.d.ts +101 -0
- package/dist/header/dates.d.ts.map +1 -0
- package/dist/header/dates.js +378 -0
- package/dist/header/dates.js.map +1 -0
- package/dist/header/fields.d.ts +105 -0
- package/dist/header/fields.d.ts.map +1 -0
- package/dist/header/fields.js +363 -0
- package/dist/header/fields.js.map +1 -0
- package/dist/header/identification.d.ts +47 -0
- package/dist/header/identification.d.ts.map +1 -0
- package/dist/header/identification.js +156 -0
- package/dist/header/identification.js.map +1 -0
- package/dist/header/lookup.d.ts +33 -0
- package/dist/header/lookup.d.ts.map +1 -0
- package/dist/header/lookup.js +67 -0
- package/dist/header/lookup.js.map +1 -0
- package/dist/header/parse.d.ts +32 -0
- package/dist/header/parse.d.ts.map +1 -0
- package/dist/header/parse.js +319 -0
- package/dist/header/parse.js.map +1 -0
- package/dist/header/scale.d.ts +68 -0
- package/dist/header/scale.d.ts.map +1 -0
- package/dist/header/scale.js +154 -0
- package/dist/header/scale.js.map +1 -0
- package/dist/header/signals.d.ts +64 -0
- package/dist/header/signals.d.ts.map +1 -0
- package/dist/header/signals.js +374 -0
- package/dist/header/signals.js.map +1 -0
- package/dist/header/variant.d.ts +67 -0
- package/dist/header/variant.d.ts.map +1 -0
- package/dist/header/variant.js +180 -0
- package/dist/header/variant.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect.d.ts +31 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +165 -0
- package/dist/inspect.js.map +1 -0
- package/dist/io/blob.d.ts +14 -0
- package/dist/io/blob.d.ts.map +1 -0
- package/dist/io/blob.js +29 -0
- package/dist/io/blob.js.map +1 -0
- package/dist/io/bytes.d.ts +11 -0
- package/dist/io/bytes.d.ts.map +1 -0
- package/dist/io/bytes.js +26 -0
- package/dist/io/bytes.js.map +1 -0
- package/dist/io/cached.d.ts +19 -0
- package/dist/io/cached.d.ts.map +1 -0
- package/dist/io/cached.js +129 -0
- package/dist/io/cached.js.map +1 -0
- package/dist/io/http.d.ts +20 -0
- package/dist/io/http.d.ts.map +1 -0
- package/dist/io/http.js +225 -0
- package/dist/io/http.js.map +1 -0
- package/dist/io/read.d.ts +42 -0
- package/dist/io/read.d.ts.map +1 -0
- package/dist/io/read.js +119 -0
- package/dist/io/read.js.map +1 -0
- package/dist/io/source.d.ts +33 -0
- package/dist/io/source.d.ts.map +1 -0
- package/dist/io/source.js +72 -0
- package/dist/io/source.js.map +1 -0
- package/dist/node.d.ts +70 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +109 -0
- package/dist/node.js.map +1 -0
- package/dist/record-index.d.ts +58 -0
- package/dist/record-index.d.ts.map +1 -0
- package/dist/record-index.js +290 -0
- package/dist/record-index.js.map +1 -0
- package/dist/recording.d.ts +57 -0
- package/dist/recording.d.ts.map +1 -0
- package/dist/recording.js +178 -0
- package/dist/recording.js.map +1 -0
- package/dist/tal/annotations.d.ts +36 -0
- package/dist/tal/annotations.d.ts.map +1 -0
- package/dist/tal/annotations.js +437 -0
- package/dist/tal/annotations.js.map +1 -0
- package/dist/tal/grammar.d.ts +111 -0
- package/dist/tal/grammar.d.ts.map +1 -0
- package/dist/tal/grammar.js +324 -0
- package/dist/tal/grammar.js.map +1 -0
- package/dist/tal/ticks.d.ts +69 -0
- package/dist/tal/ticks.d.ts.map +1 -0
- package/dist/tal/ticks.js +137 -0
- package/dist/tal/ticks.js.map +1 -0
- package/dist/time/segments.d.ts +33 -0
- package/dist/time/segments.d.ts.map +1 -0
- package/dist/time/segments.js +87 -0
- package/dist/time/segments.js.map +1 -0
- package/dist/time/timeline.d.ts +62 -0
- package/dist/time/timeline.d.ts.map +1 -0
- package/dist/time/timeline.js +227 -0
- package/dist/time/timeline.js.map +1 -0
- package/dist/time/window.d.ts +54 -0
- package/dist/time/window.d.ts.map +1 -0
- package/dist/time/window.js +232 -0
- package/dist/time/window.js.map +1 -0
- package/dist/types.d.ts +409 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +53 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +483 -0
- package/dist/validate.js.map +1 -0
- package/package.json +77 -0
- package/src/bytes/latin1.ts +87 -0
- package/src/bytes/numbers.ts +115 -0
- package/src/bytes/view.ts +59 -0
- package/src/constants.ts +96 -0
- package/src/decode/digital.ts +258 -0
- package/src/decode/physical.ts +235 -0
- package/src/diagnostics/codes.ts +209 -0
- package/src/diagnostics/collector.ts +129 -0
- package/src/diagnostics/format.ts +143 -0
- package/src/errors.ts +208 -0
- package/src/header/dates.ts +453 -0
- package/src/header/fields.ts +455 -0
- package/src/header/identification.ts +196 -0
- package/src/header/lookup.ts +81 -0
- package/src/header/parse.ts +398 -0
- package/src/header/scale.ts +268 -0
- package/src/header/signals.ts +550 -0
- package/src/header/variant.ts +239 -0
- package/src/index.ts +151 -0
- package/src/inspect.ts +181 -0
- package/src/io/blob.ts +31 -0
- package/src/io/bytes.ts +29 -0
- package/src/io/cached.ts +136 -0
- package/src/io/http.ts +299 -0
- package/src/io/read.ts +152 -0
- package/src/io/source.ts +87 -0
- package/src/node.ts +138 -0
- package/src/record-index.ts +389 -0
- package/src/recording.ts +234 -0
- package/src/tal/annotations.ts +574 -0
- package/src/tal/grammar.ts +540 -0
- package/src/tal/ticks.ts +172 -0
- package/src/time/segments.ts +109 -0
- package/src/time/timeline.ts +290 -0
- package/src/time/window.ts +304 -0
- package/src/types.ts +490 -0
- package/src/validate.ts +610 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ten fixed header fields.
|
|
3
|
+
*
|
|
4
|
+
* Layer 2. Sole owner of the 256-byte fixed header: where each field lives, what its bytes say
|
|
5
|
+
* verbatim, and which diagnostic a field that fails its grammar deserves. It owns no ORDER —
|
|
6
|
+
* `header/parse.ts` decides that and calls the functions here one field at a time, because the
|
|
7
|
+
* fixed header has to be read in dependency order rather than in file order (the signal count
|
|
8
|
+
* has to be trusted before the header-size field is worth reading at all).
|
|
9
|
+
*
|
|
10
|
+
* Raw first, always: a numeric field that failed to parse is exactly the case where the caller
|
|
11
|
+
* needs the bytes as written, so every field is exposed as text before it is interpreted.
|
|
12
|
+
*/
|
|
13
|
+
import { hasNonPrintableAscii } from '../bytes/latin1.js';
|
|
14
|
+
import { parseEdfInteger, parseEdfNumber } from '../bytes/numbers.js';
|
|
15
|
+
import { readAsciiField, sliceBytes } from '../bytes/view.js';
|
|
16
|
+
import { EDF_MAX_SIGNAL_COUNT, HEADER_FIELDS, TICKS_PER_SECOND } from '../constants.js';
|
|
17
|
+
import { fatalError } from '../diagnostics/collector.js';
|
|
18
|
+
const CHAR_NUL = 0x00;
|
|
19
|
+
const CHAR_SPACE = 0x20;
|
|
20
|
+
/** Enough bytes to recognise the problem in a message without pasting an 80-byte field. */
|
|
21
|
+
const RAW_EVIDENCE_MAX_BYTES = 16;
|
|
22
|
+
/** 10^7 is far below 2^53, so this conversion is exact. */
|
|
23
|
+
const TICKS_PER_SECOND_FLOAT = Number(TICKS_PER_SECOND);
|
|
24
|
+
const FIELD_DESCRIPTIONS = {
|
|
25
|
+
version: 'version of this data format',
|
|
26
|
+
patientId: 'local patient identification',
|
|
27
|
+
recordingId: 'local recording identification',
|
|
28
|
+
startDate: 'startdate of the recording',
|
|
29
|
+
startTime: 'starttime of the recording',
|
|
30
|
+
headerByteLength: 'number of bytes in the header record',
|
|
31
|
+
reserved: 'reserved field',
|
|
32
|
+
recordCount: 'number of data records',
|
|
33
|
+
recordDuration: 'duration of a data record in seconds',
|
|
34
|
+
signalCount: 'number of signals',
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The text fields whose bytes are checked against the spec's printable-ASCII alphabet.
|
|
38
|
+
*
|
|
39
|
+
* `version` is excluded because BDF's is 0xFF followed by `'BIOSEMI'` by definition. The numeric
|
|
40
|
+
* fields are excluded because a byte outside the alphabet there fails the grammar instead, which
|
|
41
|
+
* is a stronger statement than "unusual bytes".
|
|
42
|
+
*/
|
|
43
|
+
const ASCII_CHECKED_FIELDS = [
|
|
44
|
+
'patientId',
|
|
45
|
+
'recordingId',
|
|
46
|
+
'startDate',
|
|
47
|
+
'startTime',
|
|
48
|
+
'reserved',
|
|
49
|
+
];
|
|
50
|
+
/** `'EDF specification, header record bytes 236-243'`. */
|
|
51
|
+
export function fixedFieldSpecReference(field) {
|
|
52
|
+
const { offset, length } = HEADER_FIELDS[field];
|
|
53
|
+
return `EDF specification, header record bytes ${offset}-${offset + length - 1}`;
|
|
54
|
+
}
|
|
55
|
+
/** `'number of data records (8 bytes at offset 236)'`. */
|
|
56
|
+
export function describeFixedField(field) {
|
|
57
|
+
const { offset, length } = HEADER_FIELDS[field];
|
|
58
|
+
return `${FIELD_DESCRIPTIONS[field]} (${length} bytes at offset ${offset})`;
|
|
59
|
+
}
|
|
60
|
+
/** Every fixed field as the text it holds, padding included and nothing interpreted. */
|
|
61
|
+
export function readRawHeaderFields(headerBytes) {
|
|
62
|
+
const read = (field) => readAsciiField(headerBytes, HEADER_FIELDS[field].offset, HEADER_FIELDS[field].length);
|
|
63
|
+
return {
|
|
64
|
+
version: read('version'),
|
|
65
|
+
patientId: read('patientId'),
|
|
66
|
+
recordingId: read('recordingId'),
|
|
67
|
+
startDate: read('startDate'),
|
|
68
|
+
startTime: read('startTime'),
|
|
69
|
+
headerByteLength: read('headerByteLength'),
|
|
70
|
+
reserved: read('reserved'),
|
|
71
|
+
recordCount: read('recordCount'),
|
|
72
|
+
recordDuration: read('recordDuration'),
|
|
73
|
+
signalCount: read('signalCount'),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function isPadding(code) {
|
|
77
|
+
return code === CHAR_SPACE || code === CHAR_NUL;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The bounds of a field's content inside its own bytes, stripping the same padding
|
|
81
|
+
* `trimEdfField` does.
|
|
82
|
+
*
|
|
83
|
+
* Padding is excluded on purpose: `NON_ASCII_HEADER_FIELD` is about what the field SAYS, and a
|
|
84
|
+
* writer that pads with NUL instead of space has produced the same value. That deviation is
|
|
85
|
+
* already reported for numeric fields, as `NUMERIC_FIELD_NOT_LEFT_JUSTIFIED`.
|
|
86
|
+
*/
|
|
87
|
+
function contentBounds(raw) {
|
|
88
|
+
let start = 0;
|
|
89
|
+
let end = raw.length;
|
|
90
|
+
while (end > start && isPadding(raw.charCodeAt(end - 1)))
|
|
91
|
+
end--;
|
|
92
|
+
while (start < end && isPadding(raw.charCodeAt(start)))
|
|
93
|
+
start++;
|
|
94
|
+
return { start, end };
|
|
95
|
+
}
|
|
96
|
+
function hexBytes(bytes) {
|
|
97
|
+
const shown = bytes.subarray(0, RAW_EVIDENCE_MAX_BYTES);
|
|
98
|
+
const parts = [];
|
|
99
|
+
for (const byte of shown)
|
|
100
|
+
parts.push(`0x${byte.toString(16).padStart(2, '0')}`);
|
|
101
|
+
return parts.join(' ') + (bytes.length > shown.length ? ' ...' : '');
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Report `NON_ASCII_HEADER_FIELD` for every fixed text field carrying a byte outside ASCII
|
|
105
|
+
* 32..126.
|
|
106
|
+
*
|
|
107
|
+
* A warning, never more: the field still decodes truthfully, because edfcore reads header text
|
|
108
|
+
* as ISO-8859-1 — real equipment writes accented patient names and a bare 0xB5 for micro.
|
|
109
|
+
*/
|
|
110
|
+
export function reportNonAsciiHeaderFields(headerBytes, sink) {
|
|
111
|
+
for (const field of ASCII_CHECKED_FIELDS) {
|
|
112
|
+
const { offset, length } = HEADER_FIELDS[field];
|
|
113
|
+
const raw = readAsciiField(headerBytes, offset, length);
|
|
114
|
+
const { start, end } = contentBounds(raw);
|
|
115
|
+
const content = sliceBytes(headerBytes, offset + start, end - start);
|
|
116
|
+
if (!hasNonPrintableAscii(content))
|
|
117
|
+
continue;
|
|
118
|
+
sink.report({
|
|
119
|
+
code: 'NON_ASCII_HEADER_FIELD',
|
|
120
|
+
message: `${describeFixedField(field)} is ${JSON.stringify(raw)} and carries bytes outside ` +
|
|
121
|
+
`printable ASCII 32..126: ${hexBytes(content)}. The EDF specification restricts header ` +
|
|
122
|
+
'text to ASCII 32..126. Next: nothing is lost — edfcore decodes header text as ' +
|
|
123
|
+
'ISO-8859-1 (never TextDecoder, whose "latin1" label decodes 0x80 differently in Node ' +
|
|
124
|
+
'and in a browser), so read the field as Latin-1 rather than as UTF-8.',
|
|
125
|
+
field,
|
|
126
|
+
byteOffset: offset,
|
|
127
|
+
byteLength: length,
|
|
128
|
+
rawBytes: content,
|
|
129
|
+
raw,
|
|
130
|
+
expected: 'ASCII 32..126',
|
|
131
|
+
actual: hexBytes(content),
|
|
132
|
+
specReference: fixedFieldSpecReference(field),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function contextInit(context, raw) {
|
|
137
|
+
return {
|
|
138
|
+
field: context.field,
|
|
139
|
+
byteOffset: context.byteOffset,
|
|
140
|
+
byteLength: context.byteLength,
|
|
141
|
+
raw,
|
|
142
|
+
expected: context.expected,
|
|
143
|
+
specReference: context.specReference,
|
|
144
|
+
...(context.signalIndex === undefined ? {} : { signalIndex: context.signalIndex }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* `'0,5'` is a half and `'1,024'` is one thousand and twenty-four, and nothing in the bytes says
|
|
149
|
+
* which was meant — so this is fatal with no opt-in, at every call site.
|
|
150
|
+
*/
|
|
151
|
+
function commaError(parse, context) {
|
|
152
|
+
return fatalError({
|
|
153
|
+
code: 'COMMA_DECIMAL_SEPARATOR',
|
|
154
|
+
message: `${context.description} is ${JSON.stringify(parse.raw)}, which uses a comma as the ` +
|
|
155
|
+
'decimal separator. "0,5" (a half) and "1,024" (one thousand and twenty-four) are ' +
|
|
156
|
+
'indistinguishable, and substituting "." in the second silently turns 1024 into 1.024, ' +
|
|
157
|
+
`so edfcore refuses to guess. ${context.specReference}: the field is an ASCII number ` +
|
|
158
|
+
'with "." as the decimal separator. Next: rewrite the field in the source file — the ' +
|
|
159
|
+
'docs carry a preprocessing recipe — because no reading of these bytes is safe.',
|
|
160
|
+
...contextInit(context, parse.raw),
|
|
161
|
+
actual: parse.raw,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function invalidError(parse, context) {
|
|
165
|
+
const blank = parse.problem === 'empty' ? ' (blank)' : '';
|
|
166
|
+
return fatalError({
|
|
167
|
+
code: 'NUMERIC_FIELD_INVALID',
|
|
168
|
+
message: `${context.description} is ${JSON.stringify(parse.raw)}${blank}, which is not ` +
|
|
169
|
+
`${context.expected}. ${context.specReference}. Next: ${context.nextStep}.`,
|
|
170
|
+
...contextInit(context, parse.raw),
|
|
171
|
+
actual: parse.raw,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The value is trustworthy, the layout is not: leading padding means the writer right-justified
|
|
176
|
+
* the field, and a NUL in the padding means it used the wrong pad byte.
|
|
177
|
+
*/
|
|
178
|
+
function reportLayout(parse, context, sink) {
|
|
179
|
+
if (parse.problem !== 'not-left-justified')
|
|
180
|
+
return;
|
|
181
|
+
sink.report({
|
|
182
|
+
code: 'NUMERIC_FIELD_NOT_LEFT_JUSTIFIED',
|
|
183
|
+
message: `${context.description} is ${JSON.stringify(parse.raw)}: the value ${parse.value} was ` +
|
|
184
|
+
'read from it, but the field is right-justified or NUL-padded, where EDF requires the ' +
|
|
185
|
+
`digits to start at the first byte and the rest of the field to be spaces. ` +
|
|
186
|
+
`${context.specReference}. Next: nothing to do — the value is used as read, and ` +
|
|
187
|
+
'header.raw keeps the bytes as written.',
|
|
188
|
+
...contextInit(context, parse.raw),
|
|
189
|
+
actual: parse.raw,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The value of a field that edfcore cannot proceed without.
|
|
194
|
+
*
|
|
195
|
+
* A comma decimal or a failed grammar throws: these fields decide where bytes are and what they
|
|
196
|
+
* mean, so a wrong reading would be worse than no reading.
|
|
197
|
+
*/
|
|
198
|
+
export function requireNumericField(parse, context, sink) {
|
|
199
|
+
if (parse.problem === 'comma-decimal')
|
|
200
|
+
throw commaError(parse, context);
|
|
201
|
+
if (!parse.ok)
|
|
202
|
+
throw invalidError(parse, context);
|
|
203
|
+
reportLayout(parse, context, sink);
|
|
204
|
+
return parse.value;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The value of a field edfcore can survive without, or `NaN` when it could not be read.
|
|
208
|
+
*
|
|
209
|
+
* Only for the two fields with an authoritative alternative source — the declared header size,
|
|
210
|
+
* which always loses to the computed one, and the record count, which is recoverable from the
|
|
211
|
+
* source length. The caller reports what it did instead; a comma decimal is still fatal.
|
|
212
|
+
*/
|
|
213
|
+
export function readNumericField(parse, context, sink) {
|
|
214
|
+
if (parse.problem === 'comma-decimal')
|
|
215
|
+
throw commaError(parse, context);
|
|
216
|
+
if (!parse.ok)
|
|
217
|
+
return Number.NaN;
|
|
218
|
+
reportLayout(parse, context, sink);
|
|
219
|
+
return parse.value;
|
|
220
|
+
}
|
|
221
|
+
function fixedContext(field, expected, nextStep) {
|
|
222
|
+
const { offset, length } = HEADER_FIELDS[field];
|
|
223
|
+
return {
|
|
224
|
+
field,
|
|
225
|
+
description: describeFixedField(field),
|
|
226
|
+
byteOffset: offset,
|
|
227
|
+
byteLength: length,
|
|
228
|
+
expected,
|
|
229
|
+
specReference: fixedFieldSpecReference(field),
|
|
230
|
+
nextStep,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The signal count at offset 252, validated to 1..9999.
|
|
235
|
+
*
|
|
236
|
+
* Fatal on anything else, including a comma, and deliberately reported as
|
|
237
|
+
* `SIGNAL_COUNT_INVALID` rather than as the generic numeric codes: every per-signal field
|
|
238
|
+
* address is `256 + ns * blockOffset + i * width`, so an unusable ns makes every byte after the
|
|
239
|
+
* fixed header unlocatable. It is validated before any ns-sized allocation for the same reason.
|
|
240
|
+
*/
|
|
241
|
+
export function parseSignalCount(raw, sink) {
|
|
242
|
+
const context = fixedContext('signalCount', 'a whole number of signals in 1..9999', 'inspect the first 256 bytes in a hex viewer; nothing after them can be located without ' +
|
|
243
|
+
'this field');
|
|
244
|
+
const parse = parseEdfInteger(raw);
|
|
245
|
+
if (parse.ok && parse.value >= 1 && parse.value <= EDF_MAX_SIGNAL_COUNT) {
|
|
246
|
+
reportLayout(parse, context, sink);
|
|
247
|
+
return parse.value;
|
|
248
|
+
}
|
|
249
|
+
throw fatalError({
|
|
250
|
+
code: 'SIGNAL_COUNT_INVALID',
|
|
251
|
+
message: `${context.description} is ${JSON.stringify(raw)}, which is not ${context.expected}. ` +
|
|
252
|
+
'Every per-signal field address is 256 + ns * blockOffset + i * width and the whole ' +
|
|
253
|
+
`header is 256 * (ns + 1) bytes, so an unusable ns leaves no byte after the fixed ` +
|
|
254
|
+
`header locatable. ${context.specReference}. Next: ${context.nextStep}.`,
|
|
255
|
+
...contextInit(context, raw),
|
|
256
|
+
actual: raw,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The declared header size at offset 184, checked against the computed `256 * (ns + 1)`.
|
|
261
|
+
*
|
|
262
|
+
* The computed value ALWAYS wins, which is what makes this a warning rather than a fatal
|
|
263
|
+
* numeric field: a value edfcore never uses cannot make it read the wrong bytes. An unreadable
|
|
264
|
+
* field is reported the same way and returned as `NaN`, so `header.declaredHeaderByteLength`
|
|
265
|
+
* never claims a size the file did not state.
|
|
266
|
+
*/
|
|
267
|
+
export function checkDeclaredHeaderByteLength(raw, computedHeaderByteLength, sink) {
|
|
268
|
+
const context = fixedContext('headerByteLength', 'a whole number of bytes equal to 256 * (number of signals + 1)', 'nothing to do — edfcore uses the computed size');
|
|
269
|
+
const declared = readNumericField(parseEdfInteger(raw), context, sink);
|
|
270
|
+
if (declared === computedHeaderByteLength)
|
|
271
|
+
return declared;
|
|
272
|
+
sink.report({
|
|
273
|
+
code: 'HEADER_SIZE_MISMATCH',
|
|
274
|
+
message: `${context.description} is ${JSON.stringify(raw)}, but the header of a file with this ` +
|
|
275
|
+
`many signals is exactly ${computedHeaderByteLength} bytes (256 * (ns + 1)). ` +
|
|
276
|
+
`${context.specReference}. Next: the computed size wins, so the first data record is ` +
|
|
277
|
+
`read at byte ${computedHeaderByteLength}; header.declaredHeaderByteLength keeps what ` +
|
|
278
|
+
'the file claimed.',
|
|
279
|
+
...contextInit(context, raw),
|
|
280
|
+
expected: String(computedHeaderByteLength),
|
|
281
|
+
actual: Number.isNaN(declared) ? raw : String(declared),
|
|
282
|
+
});
|
|
283
|
+
return declared;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* The record duration at offset 244, in seconds.
|
|
287
|
+
*
|
|
288
|
+
* May legitimately be 0 — an EDF+ file whose records carry only annotations does exactly that —
|
|
289
|
+
* and `ZERO_RECORD_DURATION` says so without refusing the file. A NEGATIVE duration is refused:
|
|
290
|
+
* it is not a tolerable oddity but a value that would make every record onset, sample time and
|
|
291
|
+
* sample rate derived from it run backwards. So is a POSITIVE duration that no 100 ns tick count
|
|
292
|
+
* can express, in either direction — see the comment on that check.
|
|
293
|
+
*/
|
|
294
|
+
export function parseRecordDuration(raw, sink) {
|
|
295
|
+
const context = fixedContext('recordDuration', 'a number of seconds, written in ASCII digits with "." as the decimal separator, such as ' +
|
|
296
|
+
'"1", "0.1" or "30"', 'header.raw.recordDuration keeps the bytes as written; every sample time in the file is a ' +
|
|
297
|
+
'multiple of this value, and edfcore will not assume one second');
|
|
298
|
+
const seconds = requireNumericField(parseEdfNumber(raw), context, sink);
|
|
299
|
+
if (seconds < 0) {
|
|
300
|
+
throw fatalError({
|
|
301
|
+
code: 'NUMERIC_FIELD_INVALID',
|
|
302
|
+
message: `${context.description} is ${JSON.stringify(raw)}, i.e. ${seconds} seconds. A data ` +
|
|
303
|
+
'record cannot last a negative time, and every record onset, sample time and sample ' +
|
|
304
|
+
`rate derived from it would run backwards. ${context.specReference}. Next: ` +
|
|
305
|
+
`${context.nextStep}.`,
|
|
306
|
+
...contextInit(context, raw),
|
|
307
|
+
expected: 'a duration of 0 seconds or more',
|
|
308
|
+
actual: String(seconds),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// edfcore measures time in exact 100 ns ticks, so a POSITIVE duration that no tick count can
|
|
312
|
+
// express is not a duration it can honour, and the two failures look nothing alike from the
|
|
313
|
+
// outside. Too large and `seconds * 10^7` overflows float64 before it can become a bigint, which
|
|
314
|
+
// surfaced as a bare `RangeError` about BigInt() — an error about a number, thrown at a caller
|
|
315
|
+
// who passed the right arguments, for a defect that is entirely the file's. Too small and it
|
|
316
|
+
// rounds to zero ticks while `recordDurationSeconds` keeps claiming a positive length, which
|
|
317
|
+
// leaves `recordDurationTicks` at 0 next to a non-zero `recordDurationSeconds` and makes every
|
|
318
|
+
// `sampleRateHz` in the file Infinity. Zero itself stays legal and is reported just below.
|
|
319
|
+
const ticks = seconds * TICKS_PER_SECOND_FLOAT;
|
|
320
|
+
if (seconds > 0 && (!Number.isFinite(ticks) || Math.round(ticks) === 0)) {
|
|
321
|
+
throw fatalError({
|
|
322
|
+
code: 'NUMERIC_FIELD_INVALID',
|
|
323
|
+
message: `${context.description} is ${JSON.stringify(raw)}, i.e. ${seconds} seconds, which is ` +
|
|
324
|
+
(Number.isFinite(ticks)
|
|
325
|
+
? 'shorter than the 100 ns tick edfcore measures time in, so every data record would ' +
|
|
326
|
+
'occupy no time at all while the field claims otherwise'
|
|
327
|
+
: 'so large that its length in 100 ns ticks is not a number at all') +
|
|
328
|
+
`. ${context.specReference}. Next: ${context.nextStep}.`,
|
|
329
|
+
...contextInit(context, raw),
|
|
330
|
+
expected: 'a duration of 0 seconds, or of at least 100 ns',
|
|
331
|
+
actual: String(seconds),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (seconds === 0) {
|
|
335
|
+
sink.report({
|
|
336
|
+
code: 'ZERO_RECORD_DURATION',
|
|
337
|
+
message: `${context.description} is ${JSON.stringify(raw)}. Zero is legal — EDF+ uses it for a ` +
|
|
338
|
+
'recording whose data records carry annotations and nothing else — but it makes every ' +
|
|
339
|
+
'sample rate undefined, because samplesPerRecord / 0 is not a rate. EDF+ additional ' +
|
|
340
|
+
'specification 1. Next: signal.sampleRateHz is undefined for every signal in this ' +
|
|
341
|
+
'file; index samples by signal.samplesPerRecord and never divide by ' +
|
|
342
|
+
'header.recordDurationSeconds.',
|
|
343
|
+
...contextInit(context, raw),
|
|
344
|
+
expected: 'a positive number of seconds, or 0 for an annotations-only recording',
|
|
345
|
+
actual: '0',
|
|
346
|
+
specReference: 'EDF+ additional specification 1 (annotations-only recordings)',
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return seconds;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* The declared record count at offset 236, verbatim.
|
|
353
|
+
*
|
|
354
|
+
* `-1` is the sanctioned "the writer never closed the file" value and is returned as `-1`; an
|
|
355
|
+
* unreadable field is returned as `NaN`. Both mean the same thing to the caller — the count has
|
|
356
|
+
* to come from the source length instead — and `header/parse.ts` reports which happened, since
|
|
357
|
+
* only it knows the source length that resolves it.
|
|
358
|
+
*/
|
|
359
|
+
export function parseDeclaredRecordCount(raw, sink) {
|
|
360
|
+
const context = fixedContext('recordCount', 'a whole number of data records, or -1 when the writer did not know it', 'the count is recovered from the source length instead');
|
|
361
|
+
return readNumericField(parseEdfInteger(raw), context, sink);
|
|
362
|
+
}
|
|
363
|
+
//# sourceMappingURL=fields.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fields.js","sourceRoot":"","sources":["../../src/header/fields.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two EDF+ identification grammars.
|
|
3
|
+
*
|
|
4
|
+
* Layer 2. Sole owner of the subfield structure of the 80-byte local patient identification
|
|
5
|
+
* (offset 8) and the 80-byte local recording identification (offset 88).
|
|
6
|
+
*
|
|
7
|
+
* Three rules run through both:
|
|
8
|
+
*
|
|
9
|
+
* - `'X'` means unknown in ANY subfield and becomes `undefined`, never the string `'X'`.
|
|
10
|
+
* - The raw text is ALWAYS preserved, whatever the grammar says, and subfields beyond the ones
|
|
11
|
+
* the spec names go to `extraSubfields` — EDF+ says the field must *start with* its
|
|
12
|
+
* subfields, so trailing extras are legal and are not a non-conformance.
|
|
13
|
+
* - Underscores are exposed verbatim. EDF+ says a space inside a subfield must be replaced by
|
|
14
|
+
* another character and suggests an underscore, but it mandates neither the character nor a
|
|
15
|
+
* way back: `'Mac_Donald'` and a genuine underscore are indistinguishable, so substituting
|
|
16
|
+
* here would corrupt one to display the other. `raw` and the subfields both keep what the
|
|
17
|
+
* file wrote.
|
|
18
|
+
*
|
|
19
|
+
* Plain EDF puts free text in these fields, which is normal and not a defect. The diagnostic
|
|
20
|
+
* therefore fires only when the file carries an EDF+/BDF+ marker; `conformant` is set honestly
|
|
21
|
+
* either way.
|
|
22
|
+
*/
|
|
23
|
+
import type { DiagnosticSink } from '../diagnostics/collector.js';
|
|
24
|
+
import type { EdfPatientId, EdfRecordingId } from '../types.js';
|
|
25
|
+
export interface IdentificationOptions {
|
|
26
|
+
/**
|
|
27
|
+
* The reserved field carries an EDF+/BDF+ marker, so the subfield grammar is required rather
|
|
28
|
+
* than merely conventional. Only then is a deviation worth a diagnostic.
|
|
29
|
+
*/
|
|
30
|
+
readonly edfPlus: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The local patient identification: code, sex, birthdate, name, then anything else the writer
|
|
34
|
+
* appended.
|
|
35
|
+
*
|
|
36
|
+
* `raw` on the result is the text exactly as it was passed in, padding included.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parsePatientId(raw: string, options: IdentificationOptions, sink: DiagnosticSink): EdfPatientId;
|
|
39
|
+
/**
|
|
40
|
+
* The local recording identification: the literal `Startdate`, the start date, and the
|
|
41
|
+
* investigation, technician and equipment codes.
|
|
42
|
+
*
|
|
43
|
+
* This is the only field in an EDF file that can carry a four-digit year, which is what makes
|
|
44
|
+
* it the authority `dates.ts` prefers and the only way past 2084.
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseRecordingId(raw: string, options: IdentificationOptions, sink: DiagnosticSink): EdfRecordingId;
|
|
47
|
+
//# sourceMappingURL=identification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identification.d.ts","sourceRoot":"","sources":["../../src/header/identification.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EAAmB,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAejF,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAoCD;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,qBAAqB,EAC9B,IAAI,EAAE,cAAc,GACnB,YAAY,CA0Cd;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,qBAAqB,EAC9B,IAAI,EAAE,cAAc,GACnB,cAAc,CA+ChB"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two EDF+ identification grammars.
|
|
3
|
+
*
|
|
4
|
+
* Layer 2. Sole owner of the subfield structure of the 80-byte local patient identification
|
|
5
|
+
* (offset 8) and the 80-byte local recording identification (offset 88).
|
|
6
|
+
*
|
|
7
|
+
* Three rules run through both:
|
|
8
|
+
*
|
|
9
|
+
* - `'X'` means unknown in ANY subfield and becomes `undefined`, never the string `'X'`.
|
|
10
|
+
* - The raw text is ALWAYS preserved, whatever the grammar says, and subfields beyond the ones
|
|
11
|
+
* the spec names go to `extraSubfields` — EDF+ says the field must *start with* its
|
|
12
|
+
* subfields, so trailing extras are legal and are not a non-conformance.
|
|
13
|
+
* - Underscores are exposed verbatim. EDF+ says a space inside a subfield must be replaced by
|
|
14
|
+
* another character and suggests an underscore, but it mandates neither the character nor a
|
|
15
|
+
* way back: `'Mac_Donald'` and a genuine underscore are indistinguishable, so substituting
|
|
16
|
+
* here would corrupt one to display the other. `raw` and the subfields both keep what the
|
|
17
|
+
* file wrote.
|
|
18
|
+
*
|
|
19
|
+
* Plain EDF puts free text in these fields, which is normal and not a defect. The diagnostic
|
|
20
|
+
* therefore fires only when the file carries an EDF+/BDF+ marker; `conformant` is set honestly
|
|
21
|
+
* either way.
|
|
22
|
+
*/
|
|
23
|
+
import { trimEdfField } from '../bytes/latin1.js';
|
|
24
|
+
import { HEADER_FIELDS } from '../constants.js';
|
|
25
|
+
import { parseSubfieldDate } from './dates.js';
|
|
26
|
+
/** The EDF+ placeholder for an unknown subfield, in every position. */
|
|
27
|
+
const UNKNOWN_SUBFIELD = 'X';
|
|
28
|
+
const RECORDING_ID_PREFIX = 'Startdate';
|
|
29
|
+
const PATIENT_SUBFIELD_COUNT = 4;
|
|
30
|
+
const RECORDING_SUBFIELD_COUNT = 5;
|
|
31
|
+
const PATIENT_GRAMMAR = 'code sex(F|M) birthdate(dd-MMM-yyyy) name';
|
|
32
|
+
const RECORDING_GRAMMAR = 'Startdate startdate(dd-MMM-yyyy) investigationCode technicianCode equipmentCode';
|
|
33
|
+
/** Subfields are separated by spaces and may not contain one. Runs are tolerated. */
|
|
34
|
+
function splitSubfields(raw) {
|
|
35
|
+
const text = trimEdfField(raw);
|
|
36
|
+
if (text.length === 0)
|
|
37
|
+
return [];
|
|
38
|
+
return text.split(/\s+/).filter((part) => part.length > 0);
|
|
39
|
+
}
|
|
40
|
+
/** `'X'` is the spec's "unknown", and an absent subfield is unknown too. */
|
|
41
|
+
function subfieldValue(subfields, index) {
|
|
42
|
+
const value = subfields[index];
|
|
43
|
+
if (value === undefined || value === UNKNOWN_SUBFIELD)
|
|
44
|
+
return undefined;
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
function parseSex(subfield) {
|
|
48
|
+
if (subfield === 'F' || subfield === 'M')
|
|
49
|
+
return subfield;
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A subfield date, or `undefined` for `'X'`/absent/unparseable.
|
|
54
|
+
*
|
|
55
|
+
* `conformant` is false when the text is present but is not exactly `dd-MMM-yyyy` with an
|
|
56
|
+
* uppercase English month — the date is still exposed when it can be read at all.
|
|
57
|
+
*/
|
|
58
|
+
function parseIdentificationDate(subfield) {
|
|
59
|
+
if (subfield === undefined)
|
|
60
|
+
return { date: undefined, conformant: true };
|
|
61
|
+
const parsed = parseSubfieldDate(subfield);
|
|
62
|
+
return { date: parsed.date, conformant: parsed.conformant };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The local patient identification: code, sex, birthdate, name, then anything else the writer
|
|
66
|
+
* appended.
|
|
67
|
+
*
|
|
68
|
+
* `raw` on the result is the text exactly as it was passed in, padding included.
|
|
69
|
+
*/
|
|
70
|
+
export function parsePatientId(raw, options, sink) {
|
|
71
|
+
const subfields = splitSubfields(raw);
|
|
72
|
+
const sexText = subfields[1];
|
|
73
|
+
const sex = parseSex(sexText);
|
|
74
|
+
const birth = parseIdentificationDate(subfieldValue(subfields, 2));
|
|
75
|
+
const sexConformant = sexText === undefined ? false : sex !== undefined || sexText === UNKNOWN_SUBFIELD;
|
|
76
|
+
const conformant = subfields.length >= PATIENT_SUBFIELD_COUNT && sexConformant && birth.conformant;
|
|
77
|
+
if (!conformant && options.edfPlus) {
|
|
78
|
+
const offset = HEADER_FIELDS.patientId.offset;
|
|
79
|
+
sink.report({
|
|
80
|
+
code: 'PATIENT_ID_NONCONFORMANT',
|
|
81
|
+
message: `local patient identification (80 bytes at offset ${offset}) is ` +
|
|
82
|
+
`${JSON.stringify(trimEdfField(raw))}, which does not follow the EDF+ grammar ` +
|
|
83
|
+
`"${PATIENT_GRAMMAR}": four space-separated subfields, sex written F or M, ` +
|
|
84
|
+
'birthdate written dd-MMM-yyyy such as 02-AUG-1951, X for anything unknown, and no ' +
|
|
85
|
+
`space inside a subfield. Found ${subfields.length} subfield(s). EDF+ additional ` +
|
|
86
|
+
'specification 3. Next: header.patient.raw keeps the text verbatim and every subfield edfcore ' +
|
|
87
|
+
'could read is still exposed; the rest are undefined.',
|
|
88
|
+
field: 'patientId',
|
|
89
|
+
byteOffset: offset,
|
|
90
|
+
byteLength: HEADER_FIELDS.patientId.length,
|
|
91
|
+
raw,
|
|
92
|
+
expected: PATIENT_GRAMMAR,
|
|
93
|
+
actual: trimEdfField(raw),
|
|
94
|
+
specReference: 'EDF+ additional specification 3 (local patient identification)',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
raw,
|
|
99
|
+
conformant,
|
|
100
|
+
code: subfieldValue(subfields, 0),
|
|
101
|
+
sex,
|
|
102
|
+
birthDate: birth.date,
|
|
103
|
+
name: subfieldValue(subfields, 3),
|
|
104
|
+
extraSubfields: Object.freeze(subfields.slice(PATIENT_SUBFIELD_COUNT)),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The local recording identification: the literal `Startdate`, the start date, and the
|
|
109
|
+
* investigation, technician and equipment codes.
|
|
110
|
+
*
|
|
111
|
+
* This is the only field in an EDF file that can carry a four-digit year, which is what makes
|
|
112
|
+
* it the authority `dates.ts` prefers and the only way past 2084.
|
|
113
|
+
*/
|
|
114
|
+
export function parseRecordingId(raw, options, sink) {
|
|
115
|
+
const subfields = splitSubfields(raw);
|
|
116
|
+
const prefix = subfields[0];
|
|
117
|
+
const startDate = parseIdentificationDate(subfieldValue(subfields, 1));
|
|
118
|
+
// Case-insensitive acceptance, non-conformance noted: the spec spells it 'Startdate'.
|
|
119
|
+
const prefixPresent = prefix !== undefined && prefix.toUpperCase() === RECORDING_ID_PREFIX.toUpperCase();
|
|
120
|
+
const prefixConformant = prefix === RECORDING_ID_PREFIX;
|
|
121
|
+
const conformant = subfields.length >= RECORDING_SUBFIELD_COUNT && prefixConformant && startDate.conformant;
|
|
122
|
+
if (!conformant && options.edfPlus) {
|
|
123
|
+
const offset = HEADER_FIELDS.recordingId.offset;
|
|
124
|
+
sink.report({
|
|
125
|
+
code: 'RECORDING_ID_NONCONFORMANT',
|
|
126
|
+
message: `local recording identification (80 bytes at offset ${offset}) is ` +
|
|
127
|
+
`${JSON.stringify(trimEdfField(raw))}, which does not follow the EDF+ grammar ` +
|
|
128
|
+
`"${RECORDING_GRAMMAR}": five space-separated subfields starting with the literal ` +
|
|
129
|
+
'text Startdate, then the start date written dd-MMM-yyyy such as 02-AUG-1951, then ' +
|
|
130
|
+
'the investigation, technician and equipment codes, with X for anything unknown. ' +
|
|
131
|
+
`Found ${subfields.length} subfield(s). EDF+ additional specification 4. Next: ` +
|
|
132
|
+
'header.recording.raw keeps the text verbatim; a Startdate that could not be read ' +
|
|
133
|
+
'leaves startTime.recordingIdDate undefined, so the two-digit header year is all ' +
|
|
134
|
+
'that remains.',
|
|
135
|
+
field: 'recordingId',
|
|
136
|
+
byteOffset: offset,
|
|
137
|
+
byteLength: HEADER_FIELDS.recordingId.length,
|
|
138
|
+
raw,
|
|
139
|
+
expected: RECORDING_GRAMMAR,
|
|
140
|
+
actual: trimEdfField(raw),
|
|
141
|
+
specReference: 'EDF+ additional specification 4 (local recording identification)',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
raw,
|
|
146
|
+
conformant,
|
|
147
|
+
// A date sitting behind a missing or misspelt 'Startdate' keyword is not a Startdate; the
|
|
148
|
+
// subfield is only meaningful in the position the grammar gives it.
|
|
149
|
+
startDate: prefixPresent ? startDate.date : undefined,
|
|
150
|
+
investigationCode: subfieldValue(subfields, 2),
|
|
151
|
+
technicianCode: subfieldValue(subfields, 3),
|
|
152
|
+
equipmentCode: subfieldValue(subfields, 4),
|
|
153
|
+
extraSubfields: Object.freeze(subfields.slice(RECORDING_SUBFIELD_COUNT)),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=identification.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identification.js","sourceRoot":"","sources":["../../src/header/identification.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding a signal by name.
|
|
3
|
+
*
|
|
4
|
+
* Layer 2. Two functions and a predicate, and the reason they exist rather than being left to
|
|
5
|
+
* the caller: deleting name lookup does not delete `signals.find(s => s.label === label)`, which
|
|
6
|
+
* silently returns the first of a duplicate pair. CHB-MIT ships `'T8-P8'` twice, and picking one
|
|
7
|
+
* of them by accident is how the wrong channel ends up in a paper.
|
|
8
|
+
*
|
|
9
|
+
* Matching is exact on the TRIMMED label and is case-sensitive. Nothing else is normalised: EDF
|
|
10
|
+
* labels are electrode names, `'Fp1'` and `'FP1'` are written by different systems, and edfcore
|
|
11
|
+
* has no montage vocabulary to decide they are the same thing.
|
|
12
|
+
*/
|
|
13
|
+
import type { EdfHeader, EdfSignal } from '../types.js';
|
|
14
|
+
/**
|
|
15
|
+
* True for the two reserved annotation labels, matched on the trimmed text and case-sensitively.
|
|
16
|
+
*
|
|
17
|
+
* Both are accepted for either family: the label identifies the channel's ROLE, and a BDF+ file
|
|
18
|
+
* written by an EDF+ library carries `'EDF Annotations'` — refusing to recognise it there would
|
|
19
|
+
* expose a text channel as if it held samples.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isAnnotationLabel(label: string): boolean;
|
|
22
|
+
/** Every signal with this label, in signal order. Empty when none matches. */
|
|
23
|
+
export declare function findSignals(header: EdfHeader, label: string): readonly EdfSignal[];
|
|
24
|
+
/**
|
|
25
|
+
* One signal, by index or by label.
|
|
26
|
+
*
|
|
27
|
+
* A numeric selector indexes `header.signals` directly. A label that matches nothing throws
|
|
28
|
+
* `EdfChannelNotFoundError` carrying every available label; a label that matches more than one
|
|
29
|
+
* signal throws `EdfAmbiguousChannelError` carrying the indices, because there is no answer
|
|
30
|
+
* edfcore could return that would not be a guess.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getSignal(header: EdfHeader, selector: number | string): EdfSignal;
|
|
33
|
+
//# sourceMappingURL=lookup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lookup.d.ts","sourceRoot":"","sources":["../../src/header/lookup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAGxD;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,SAAS,EAAE,CAGlF;AAMD;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAgCjF"}
|