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.
Files changed (188) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +319 -0
  3. package/dist/bytes/latin1.d.ts +43 -0
  4. package/dist/bytes/latin1.d.ts.map +1 -0
  5. package/dist/bytes/latin1.js +84 -0
  6. package/dist/bytes/latin1.js.map +1 -0
  7. package/dist/bytes/numbers.d.ts +45 -0
  8. package/dist/bytes/numbers.d.ts.map +1 -0
  9. package/dist/bytes/numbers.js +87 -0
  10. package/dist/bytes/numbers.js.map +1 -0
  11. package/dist/bytes/view.d.ts +27 -0
  12. package/dist/bytes/view.d.ts.map +1 -0
  13. package/dist/bytes/view.js +53 -0
  14. package/dist/bytes/view.js.map +1 -0
  15. package/dist/constants.d.ts +113 -0
  16. package/dist/constants.d.ts.map +1 -0
  17. package/dist/constants.js +83 -0
  18. package/dist/constants.js.map +1 -0
  19. package/dist/decode/digital.d.ts +49 -0
  20. package/dist/decode/digital.d.ts.map +1 -0
  21. package/dist/decode/digital.js +180 -0
  22. package/dist/decode/digital.js.map +1 -0
  23. package/dist/decode/physical.d.ts +37 -0
  24. package/dist/decode/physical.d.ts.map +1 -0
  25. package/dist/decode/physical.js +178 -0
  26. package/dist/decode/physical.js.map +1 -0
  27. package/dist/diagnostics/codes.d.ts +38 -0
  28. package/dist/diagnostics/codes.d.ts.map +1 -0
  29. package/dist/diagnostics/codes.js +134 -0
  30. package/dist/diagnostics/codes.js.map +1 -0
  31. package/dist/diagnostics/collector.d.ts +76 -0
  32. package/dist/diagnostics/collector.d.ts.map +1 -0
  33. package/dist/diagnostics/collector.js +95 -0
  34. package/dist/diagnostics/collector.js.map +1 -0
  35. package/dist/diagnostics/format.d.ts +21 -0
  36. package/dist/diagnostics/format.d.ts.map +1 -0
  37. package/dist/diagnostics/format.js +125 -0
  38. package/dist/diagnostics/format.js.map +1 -0
  39. package/dist/errors.d.ts +143 -0
  40. package/dist/errors.d.ts.map +1 -0
  41. package/dist/errors.js +135 -0
  42. package/dist/errors.js.map +1 -0
  43. package/dist/header/dates.d.ts +101 -0
  44. package/dist/header/dates.d.ts.map +1 -0
  45. package/dist/header/dates.js +378 -0
  46. package/dist/header/dates.js.map +1 -0
  47. package/dist/header/fields.d.ts +105 -0
  48. package/dist/header/fields.d.ts.map +1 -0
  49. package/dist/header/fields.js +363 -0
  50. package/dist/header/fields.js.map +1 -0
  51. package/dist/header/identification.d.ts +47 -0
  52. package/dist/header/identification.d.ts.map +1 -0
  53. package/dist/header/identification.js +156 -0
  54. package/dist/header/identification.js.map +1 -0
  55. package/dist/header/lookup.d.ts +33 -0
  56. package/dist/header/lookup.d.ts.map +1 -0
  57. package/dist/header/lookup.js +67 -0
  58. package/dist/header/lookup.js.map +1 -0
  59. package/dist/header/parse.d.ts +32 -0
  60. package/dist/header/parse.d.ts.map +1 -0
  61. package/dist/header/parse.js +319 -0
  62. package/dist/header/parse.js.map +1 -0
  63. package/dist/header/scale.d.ts +68 -0
  64. package/dist/header/scale.d.ts.map +1 -0
  65. package/dist/header/scale.js +154 -0
  66. package/dist/header/scale.js.map +1 -0
  67. package/dist/header/signals.d.ts +64 -0
  68. package/dist/header/signals.d.ts.map +1 -0
  69. package/dist/header/signals.js +374 -0
  70. package/dist/header/signals.js.map +1 -0
  71. package/dist/header/variant.d.ts +67 -0
  72. package/dist/header/variant.d.ts.map +1 -0
  73. package/dist/header/variant.js +180 -0
  74. package/dist/header/variant.js.map +1 -0
  75. package/dist/index.d.ts +46 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +59 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/inspect.d.ts +31 -0
  80. package/dist/inspect.d.ts.map +1 -0
  81. package/dist/inspect.js +165 -0
  82. package/dist/inspect.js.map +1 -0
  83. package/dist/io/blob.d.ts +14 -0
  84. package/dist/io/blob.d.ts.map +1 -0
  85. package/dist/io/blob.js +29 -0
  86. package/dist/io/blob.js.map +1 -0
  87. package/dist/io/bytes.d.ts +11 -0
  88. package/dist/io/bytes.d.ts.map +1 -0
  89. package/dist/io/bytes.js +26 -0
  90. package/dist/io/bytes.js.map +1 -0
  91. package/dist/io/cached.d.ts +19 -0
  92. package/dist/io/cached.d.ts.map +1 -0
  93. package/dist/io/cached.js +129 -0
  94. package/dist/io/cached.js.map +1 -0
  95. package/dist/io/http.d.ts +20 -0
  96. package/dist/io/http.d.ts.map +1 -0
  97. package/dist/io/http.js +225 -0
  98. package/dist/io/http.js.map +1 -0
  99. package/dist/io/read.d.ts +42 -0
  100. package/dist/io/read.d.ts.map +1 -0
  101. package/dist/io/read.js +119 -0
  102. package/dist/io/read.js.map +1 -0
  103. package/dist/io/source.d.ts +33 -0
  104. package/dist/io/source.d.ts.map +1 -0
  105. package/dist/io/source.js +72 -0
  106. package/dist/io/source.js.map +1 -0
  107. package/dist/node.d.ts +70 -0
  108. package/dist/node.d.ts.map +1 -0
  109. package/dist/node.js +109 -0
  110. package/dist/node.js.map +1 -0
  111. package/dist/record-index.d.ts +58 -0
  112. package/dist/record-index.d.ts.map +1 -0
  113. package/dist/record-index.js +290 -0
  114. package/dist/record-index.js.map +1 -0
  115. package/dist/recording.d.ts +57 -0
  116. package/dist/recording.d.ts.map +1 -0
  117. package/dist/recording.js +178 -0
  118. package/dist/recording.js.map +1 -0
  119. package/dist/tal/annotations.d.ts +36 -0
  120. package/dist/tal/annotations.d.ts.map +1 -0
  121. package/dist/tal/annotations.js +437 -0
  122. package/dist/tal/annotations.js.map +1 -0
  123. package/dist/tal/grammar.d.ts +111 -0
  124. package/dist/tal/grammar.d.ts.map +1 -0
  125. package/dist/tal/grammar.js +324 -0
  126. package/dist/tal/grammar.js.map +1 -0
  127. package/dist/tal/ticks.d.ts +69 -0
  128. package/dist/tal/ticks.d.ts.map +1 -0
  129. package/dist/tal/ticks.js +137 -0
  130. package/dist/tal/ticks.js.map +1 -0
  131. package/dist/time/segments.d.ts +33 -0
  132. package/dist/time/segments.d.ts.map +1 -0
  133. package/dist/time/segments.js +87 -0
  134. package/dist/time/segments.js.map +1 -0
  135. package/dist/time/timeline.d.ts +62 -0
  136. package/dist/time/timeline.d.ts.map +1 -0
  137. package/dist/time/timeline.js +227 -0
  138. package/dist/time/timeline.js.map +1 -0
  139. package/dist/time/window.d.ts +54 -0
  140. package/dist/time/window.d.ts.map +1 -0
  141. package/dist/time/window.js +232 -0
  142. package/dist/time/window.js.map +1 -0
  143. package/dist/types.d.ts +409 -0
  144. package/dist/types.d.ts.map +1 -0
  145. package/dist/types.js +16 -0
  146. package/dist/types.js.map +1 -0
  147. package/dist/validate.d.ts +53 -0
  148. package/dist/validate.d.ts.map +1 -0
  149. package/dist/validate.js +483 -0
  150. package/dist/validate.js.map +1 -0
  151. package/package.json +77 -0
  152. package/src/bytes/latin1.ts +87 -0
  153. package/src/bytes/numbers.ts +115 -0
  154. package/src/bytes/view.ts +59 -0
  155. package/src/constants.ts +96 -0
  156. package/src/decode/digital.ts +258 -0
  157. package/src/decode/physical.ts +235 -0
  158. package/src/diagnostics/codes.ts +209 -0
  159. package/src/diagnostics/collector.ts +129 -0
  160. package/src/diagnostics/format.ts +143 -0
  161. package/src/errors.ts +208 -0
  162. package/src/header/dates.ts +453 -0
  163. package/src/header/fields.ts +455 -0
  164. package/src/header/identification.ts +196 -0
  165. package/src/header/lookup.ts +81 -0
  166. package/src/header/parse.ts +398 -0
  167. package/src/header/scale.ts +268 -0
  168. package/src/header/signals.ts +550 -0
  169. package/src/header/variant.ts +239 -0
  170. package/src/index.ts +151 -0
  171. package/src/inspect.ts +181 -0
  172. package/src/io/blob.ts +31 -0
  173. package/src/io/bytes.ts +29 -0
  174. package/src/io/cached.ts +136 -0
  175. package/src/io/http.ts +299 -0
  176. package/src/io/read.ts +152 -0
  177. package/src/io/source.ts +87 -0
  178. package/src/node.ts +138 -0
  179. package/src/record-index.ts +389 -0
  180. package/src/recording.ts +234 -0
  181. package/src/tal/annotations.ts +574 -0
  182. package/src/tal/grammar.ts +540 -0
  183. package/src/tal/ticks.ts +172 -0
  184. package/src/time/segments.ts +109 -0
  185. package/src/time/timeline.ts +290 -0
  186. package/src/time/window.ts +304 -0
  187. package/src/types.ts +490 -0
  188. package/src/validate.ts +610 -0
@@ -0,0 +1,378 @@
1
+ /**
2
+ * Every date rule in EDF, in one place.
3
+ *
4
+ * Layer 2. Sole owner of: the `dd.mm.yy` header startdate, the `hh.mm.ss` clock, the
5
+ * `dd-MMM-yyyy` subfield date used by the patient birthdate and the recording-identification
6
+ * `Startdate`, the 1985-2084 two-digit-year rule, the literal `'yy'` post-2084 escape, and the
7
+ * resolution between the two dates a file can carry.
8
+ *
9
+ * A JavaScript `Date` is never constructed. EDF stores local time at the patient with no
10
+ * timezone, so a `Date` would silently apply the reader's zone — worst exactly at DST
11
+ * boundaries — and `lib: ["ES2022"]` gives us no reason to reach for one anyway. Month lengths
12
+ * and leap years are arithmetic, and `'31.02.99'` must never quietly become 3 March.
13
+ *
14
+ * Separators and stray spaces are tolerated (`' 2. 8.51'` is in the EDF FAQ). A tolerated
15
+ * deviation is reported through `conformant` on the parse result rather than as a diagnostic:
16
+ * the vocabulary in `diagnostics/codes.ts` has no code for a date that parsed but was written
17
+ * oddly, and inventing one is not this module's call.
18
+ */
19
+ import { trimEdfField } from '../bytes/latin1.js';
20
+ import { HEADER_FIELDS } from '../constants.js';
21
+ const MONTH_ABBREVIATIONS = [
22
+ 'JAN',
23
+ 'FEB',
24
+ 'MAR',
25
+ 'APR',
26
+ 'MAY',
27
+ 'JUN',
28
+ 'JUL',
29
+ 'AUG',
30
+ 'SEP',
31
+ 'OCT',
32
+ 'NOV',
33
+ 'DEC',
34
+ ];
35
+ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
36
+ const MONTHS_PER_YEAR = 12;
37
+ const FEBRUARY = 2;
38
+ const LEAP_FEBRUARY_DAYS = 29;
39
+ const MAX_HOUR = 23;
40
+ const MAX_MINUTE = 59;
41
+ /** No leap seconds: EDF has no field that could express one, and none is expected. */
42
+ const MAX_SECOND = 59;
43
+ const SECONDS_PER_MINUTE = 60;
44
+ const SECONDS_PER_HOUR = 3600;
45
+ /** 85..99 mean 1985..1999; 00..84 mean 2000..2084. The escape past 2084 is a literal `'yy'`. */
46
+ const TWO_DIGIT_YEAR_PIVOT = 85;
47
+ const TWENTIETH_CENTURY = 1900;
48
+ const TWENTY_FIRST_CENTURY = 2000;
49
+ const DIGIT_ZERO = 0x30;
50
+ const DIGIT_NINE = 0x39;
51
+ /** `'.'`, `':'`, `'-'`, `'/'` and stray spaces all occur in the wild. */
52
+ const DATE_FIELD_SEPARATORS = /[\s.:/-]+/;
53
+ /** Exactly `dd.mm.yy`, including the post-2084 escape in the year position. */
54
+ const CONFORMANT_HEADER_DATE = /^\d{2}\.\d{2}\.(?:\d{2}|yy)$/;
55
+ /** Exactly `hh.mm.ss`. */
56
+ const CONFORMANT_HEADER_TIME = /^\d{2}\.\d{2}\.\d{2}$/;
57
+ /** Exactly `dd-MMM-yyyy`: zero-padded day, uppercase English month, four-digit year. */
58
+ const CONFORMANT_SUBFIELD_DATE = /^\d{2}-[A-Z]{3}-\d{4}$/;
59
+ const YEAR_ESCAPE = /^yy$/i;
60
+ const MIDNIGHT = { hour: 0, minute: 0, second: 0 };
61
+ export function isLeapYear(year) {
62
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
63
+ }
64
+ export function daysInMonth(year, month) {
65
+ if (month === FEBRUARY && isLeapYear(year))
66
+ return LEAP_FEBRUARY_DAYS;
67
+ return DAYS_IN_MONTH[month - 1] ?? 0;
68
+ }
69
+ /** The longest that month can ever be, for validating a day whose year is not yet known. */
70
+ function maxDaysInMonth(month) {
71
+ if (month === FEBRUARY)
72
+ return LEAP_FEBRUARY_DAYS;
73
+ return DAYS_IN_MONTH[month - 1] ?? 0;
74
+ }
75
+ export function isValidCalendarDate(date) {
76
+ if (!Number.isInteger(date.year) || date.year < 1 || date.year > 9999)
77
+ return false;
78
+ if (!Number.isInteger(date.month) || date.month < 1 || date.month > MONTHS_PER_YEAR)
79
+ return false;
80
+ if (!Number.isInteger(date.day) || date.day < 1)
81
+ return false;
82
+ return date.day <= daysInMonth(date.year, date.month);
83
+ }
84
+ export function calendarDatesEqual(a, b) {
85
+ return a.year === b.year && a.month === b.month && a.day === b.day;
86
+ }
87
+ /** `'1951-08-02'`. Used in diagnostic messages and by `formatStartTimeNaive`. */
88
+ export function formatCalendarDate(date) {
89
+ return `${pad(date.year, 4)}-${pad(date.month, 2)}-${pad(date.day, 2)}`;
90
+ }
91
+ /** `'09:00:00'`. */
92
+ export function formatClockTime(clock) {
93
+ return `${pad(clock.hour, 2)}:${pad(clock.minute, 2)}:${pad(clock.second, 2)}`;
94
+ }
95
+ function pad(value, width) {
96
+ return String(value).padStart(width, '0');
97
+ }
98
+ /** All digits, `maxLength` of them at most, no sign and no separators. */
99
+ function parseDigits(text, maxLength) {
100
+ if (text.length === 0 || text.length > maxLength)
101
+ return undefined;
102
+ for (let i = 0; i < text.length; i++) {
103
+ const code = text.charCodeAt(i);
104
+ if (code < DIGIT_ZERO || code > DIGIT_NINE)
105
+ return undefined;
106
+ }
107
+ return Number(text);
108
+ }
109
+ /**
110
+ * Split a date or time field into exactly three parts, tolerating any of the separators real
111
+ * writers emit and any amount of stray space. Returns `undefined` when there are not three.
112
+ */
113
+ function threeFields(text) {
114
+ const parts = trimEdfField(text)
115
+ .split(DATE_FIELD_SEPARATORS)
116
+ .filter((part) => part.length > 0);
117
+ if (parts.length !== 3)
118
+ return undefined;
119
+ const first = parts[0];
120
+ const second = parts[1];
121
+ const third = parts[2];
122
+ if (first === undefined || second === undefined || third === undefined)
123
+ return undefined;
124
+ return [first, second, third];
125
+ }
126
+ export function resolveTwoDigitYear(twoDigitYear) {
127
+ return twoDigitYear >= TWO_DIGIT_YEAR_PIVOT
128
+ ? TWENTIETH_CENTURY + twoDigitYear
129
+ : TWENTY_FIRST_CENTURY + twoDigitYear;
130
+ }
131
+ function unparseableDate(raw, conformant) {
132
+ return {
133
+ status: 'unparseable',
134
+ date: undefined,
135
+ day: undefined,
136
+ month: undefined,
137
+ clippedYear: false,
138
+ conformant,
139
+ raw,
140
+ };
141
+ }
142
+ /**
143
+ * The `dd.mm.yy` startdate at offset 168.
144
+ *
145
+ * A four-digit year is accepted where it fits (`'2.8.1951'`), because a writer that spells the
146
+ * year out is unambiguous and the two-digit rule would be a downgrade. Three digits are
147
+ * corruption, not a convention, and are refused.
148
+ */
149
+ export function parseHeaderStartDate(raw) {
150
+ const conformant = CONFORMANT_HEADER_DATE.test(raw);
151
+ const fields = threeFields(raw);
152
+ if (fields === undefined)
153
+ return unparseableDate(raw, conformant);
154
+ const [dayText, monthText, yearText] = fields;
155
+ const day = parseDigits(dayText, 2);
156
+ const month = parseDigits(monthText, 2);
157
+ if (day === undefined || month === undefined)
158
+ return unparseableDate(raw, conformant);
159
+ if (month < 1 || month > MONTHS_PER_YEAR)
160
+ return unparseableDate(raw, conformant);
161
+ if (YEAR_ESCAPE.test(yearText)) {
162
+ // The year is genuinely unknown here, so the day can only be checked against the longest
163
+ // that month ever gets; the real year re-checks it in `resolveStartTime`.
164
+ if (day < 1 || day > maxDaysInMonth(month))
165
+ return unparseableDate(raw, conformant);
166
+ return {
167
+ status: 'yearEscape',
168
+ date: undefined,
169
+ day,
170
+ month,
171
+ clippedYear: false,
172
+ conformant,
173
+ raw,
174
+ };
175
+ }
176
+ const yearDigits = parseDigits(yearText, 4);
177
+ if (yearDigits === undefined)
178
+ return unparseableDate(raw, conformant);
179
+ if (yearText.length === 3)
180
+ return unparseableDate(raw, conformant);
181
+ const clippedYear = yearText.length <= 2;
182
+ const year = clippedYear ? resolveTwoDigitYear(yearDigits) : yearDigits;
183
+ const date = { year, month, day };
184
+ if (!isValidCalendarDate(date))
185
+ return unparseableDate(raw, conformant);
186
+ return { status: 'parsed', date, day, month, clippedYear, conformant, raw };
187
+ }
188
+ /** The `hh.mm.ss` starttime at offset 176. Whole seconds only — EDF has no finer field. */
189
+ export function parseHeaderStartTime(raw) {
190
+ const conformant = CONFORMANT_HEADER_TIME.test(raw);
191
+ const fields = threeFields(raw);
192
+ if (fields === undefined)
193
+ return { clock: undefined, conformant, raw };
194
+ const [hourText, minuteText, secondText] = fields;
195
+ const hour = parseDigits(hourText, 2);
196
+ const minute = parseDigits(minuteText, 2);
197
+ const second = parseDigits(secondText, 2);
198
+ if (hour === undefined || minute === undefined || second === undefined) {
199
+ return { clock: undefined, conformant, raw };
200
+ }
201
+ if (hour > MAX_HOUR || minute > MAX_MINUTE || second > MAX_SECOND) {
202
+ return { clock: undefined, conformant, raw };
203
+ }
204
+ return { clock: { hour, minute, second }, conformant, raw };
205
+ }
206
+ /**
207
+ * The `dd-MMM-yyyy` subfield date — `'02-AUG-1951'` — used by the patient birthdate and the
208
+ * recording-identification `Startdate`.
209
+ *
210
+ * The month is accepted case-insensitively and a two-digit numeric month is accepted too, both
211
+ * marked non-conformant. The year must be four digits: this field exists precisely to be
212
+ * unambiguous, so applying the two-digit rule to it would throw away the one thing it is for.
213
+ */
214
+ export function parseSubfieldDate(text) {
215
+ const fields = threeFields(text);
216
+ if (fields === undefined)
217
+ return { date: undefined, conformant: false };
218
+ const [dayText, monthText, yearText] = fields;
219
+ const day = parseDigits(dayText, 2);
220
+ const year = parseDigits(yearText, 4);
221
+ if (day === undefined || year === undefined || yearText.length !== 4) {
222
+ return { date: undefined, conformant: false };
223
+ }
224
+ const namedMonth = MONTH_ABBREVIATIONS.indexOf(monthText.toUpperCase()) + 1;
225
+ const month = namedMonth > 0 ? namedMonth : parseDigits(monthText, 2);
226
+ if (month === undefined || month < 1 || month > MONTHS_PER_YEAR) {
227
+ return { date: undefined, conformant: false };
228
+ }
229
+ const date = { year, month, day };
230
+ if (!isValidCalendarDate(date))
231
+ return { date: undefined, conformant: false };
232
+ return { date, conformant: CONFORMANT_SUBFIELD_DATE.test(text) && namedMonth > 0 };
233
+ }
234
+ /**
235
+ * Build the whole `EdfStartTime`, including the cross-field resolution.
236
+ *
237
+ * The recording-identification date wins when both exist, because its year is unambiguous —
238
+ * but a disagreement is always reported and both dates stay on the result, so no winner is
239
+ * picked silently. `dateSource` says which one `resolvedDate` came from.
240
+ */
241
+ export function resolveStartTime(input, sink) {
242
+ const dateParse = parseHeaderStartDate(input.rawStartDate);
243
+ const timeParse = parseHeaderStartTime(input.rawStartTime);
244
+ const recordingIdDate = input.recordingIdDate;
245
+ const dateOffset = HEADER_FIELDS.startDate.offset;
246
+ if (dateParse.status === 'unparseable') {
247
+ sink.report({
248
+ code: 'DATE_UNPARSEABLE',
249
+ message: `startdate field (8 bytes at offset ${dateOffset}) is ` +
250
+ `${JSON.stringify(dateParse.raw)}, which is not a real date in dd.mm.yy form: the ` +
251
+ 'day must exist in that month of that year, and the month must be 01..12. EDF ' +
252
+ 'specification, header record bytes 168-175. Next: header.raw.startDate keeps the ' +
253
+ 'text verbatim, and startTime.recordingIdDate is used instead when the EDF+ ' +
254
+ 'recording identification carries a Startdate.',
255
+ field: 'startDate',
256
+ byteOffset: dateOffset,
257
+ byteLength: HEADER_FIELDS.startDate.length,
258
+ raw: dateParse.raw,
259
+ expected: 'dd.mm.yy',
260
+ actual: trimEdfField(dateParse.raw),
261
+ specReference: 'EDF specification, header record bytes 168-175',
262
+ });
263
+ }
264
+ if (dateParse.clippedYear && dateParse.date !== undefined) {
265
+ sink.report({
266
+ code: 'DATE_CLIPPED_TO_1985_2084',
267
+ message: `startdate field (8 bytes at offset ${dateOffset}) is ` +
268
+ `${JSON.stringify(dateParse.raw)}: its two-digit year was resolved to ` +
269
+ `${dateParse.date.year} by the EDF+ rule that 85..99 mean 1985..1999 and 00..84 mean ` +
270
+ '2000..2084, so the field cannot express a year outside that span. EDF+ additional ' +
271
+ 'specification 2 (1985 is the clipping date). Next: for an unambiguous year read ' +
272
+ 'startTime.recordingIdDate, which the EDF+ recording identification spells out in ' +
273
+ 'four digits.',
274
+ field: 'startDate',
275
+ byteOffset: dateOffset,
276
+ byteLength: HEADER_FIELDS.startDate.length,
277
+ raw: dateParse.raw,
278
+ expected: '1985..2084',
279
+ actual: String(dateParse.date.year),
280
+ specReference: 'EDF+ additional specification 2 (startdate and starttime)',
281
+ });
282
+ }
283
+ if (dateParse.status === 'yearEscape' && recordingIdDate === undefined) {
284
+ sink.report({
285
+ code: 'DATE_UNPARSEABLE',
286
+ message: `startdate field (8 bytes at offset ${dateOffset}) is ` +
287
+ `${JSON.stringify(dateParse.raw)}: the literal "yy" is the EDF+ escape for a ` +
288
+ 'recording after 2084, and it requires the year to be given by the Startdate ' +
289
+ 'subfield of the recording identification, which this file does not carry. EDF+ ' +
290
+ 'additional specifications 2 and 4. Next: the recording has no resolvable date; ' +
291
+ 'startTime.clock is still valid.',
292
+ field: 'startDate',
293
+ byteOffset: dateOffset,
294
+ byteLength: HEADER_FIELDS.startDate.length,
295
+ raw: dateParse.raw,
296
+ expected: 'a Startdate subfield in the local recording identification',
297
+ actual: trimEdfField(dateParse.raw),
298
+ specReference: 'EDF+ additional specifications 2 and 4',
299
+ });
300
+ }
301
+ const headerDate = dateParse.date;
302
+ const disagrees = recordingIdDate !== undefined &&
303
+ ((headerDate !== undefined && !calendarDatesEqual(headerDate, recordingIdDate)) ||
304
+ // Under the escape the header still states a day and a month, and they must match.
305
+ (dateParse.status === 'yearEscape' &&
306
+ (dateParse.day !== recordingIdDate.day || dateParse.month !== recordingIdDate.month)));
307
+ if (disagrees && recordingIdDate !== undefined) {
308
+ const headerText = headerDate === undefined
309
+ ? `day ${String(dateParse.day)}, month ${String(dateParse.month)}`
310
+ : formatCalendarDate(headerDate);
311
+ sink.report({
312
+ code: 'DATE_FIELDS_DISAGREE',
313
+ message: `startdate field (8 bytes at offset ${dateOffset}) is ` +
314
+ `${JSON.stringify(dateParse.raw)}, i.e. ${headerText}, but the Startdate subfield of ` +
315
+ `the local recording identification says ${formatCalendarDate(recordingIdDate)}. ` +
316
+ 'EDF+ additional specifications 2 and 4 both define the start of the recording, so ' +
317
+ 'the two must name the same day; when only the year differs the usual cause is that ' +
318
+ 'the dd.mm.yy field cannot express a year outside 1985..2084. Next: both are ' +
319
+ 'exposed as ' +
320
+ 'startTime.headerDate and startTime.recordingIdDate and edfcore picks no silent ' +
321
+ 'winner; resolvedDate is the four-digit recording-identification date, and ' +
322
+ 'dateSource says so.',
323
+ field: 'startDate',
324
+ byteOffset: dateOffset,
325
+ byteLength: HEADER_FIELDS.startDate.length,
326
+ raw: dateParse.raw,
327
+ expected: formatCalendarDate(recordingIdDate),
328
+ actual: headerText,
329
+ specReference: 'EDF+ additional specifications 2 and 4',
330
+ });
331
+ }
332
+ if (timeParse.clock === undefined) {
333
+ sink.report({
334
+ code: 'DATE_UNPARSEABLE',
335
+ message: `starttime field (8 bytes at offset ${HEADER_FIELDS.startTime.offset}) is ` +
336
+ `${JSON.stringify(timeParse.raw)}, which is not a clock time in hh.mm.ss form with ` +
337
+ 'hour 00..23, minute 00..59 and second 00..59. EDF specification, header record ' +
338
+ 'bytes 176-183. Next: startTime.clock reports 00:00:00 because the type admits no ' +
339
+ 'absent clock; header.raw.startTime keeps the text verbatim, and every sample offset ' +
340
+ 'is relative to the recording start regardless.',
341
+ field: 'startTime',
342
+ byteOffset: HEADER_FIELDS.startTime.offset,
343
+ byteLength: HEADER_FIELDS.startTime.length,
344
+ raw: timeParse.raw,
345
+ expected: 'hh.mm.ss',
346
+ actual: trimEdfField(timeParse.raw),
347
+ specReference: 'EDF specification, header record bytes 176-183',
348
+ });
349
+ }
350
+ const clock = timeParse.clock ?? MIDNIGHT;
351
+ const resolvedDate = recordingIdDate ?? headerDate;
352
+ const dateSource = recordingIdDate !== undefined
353
+ ? 'recordingIdField'
354
+ : headerDate !== undefined
355
+ ? 'headerField'
356
+ : 'none';
357
+ return {
358
+ headerDate,
359
+ recordingIdDate,
360
+ resolvedDate,
361
+ dateSource,
362
+ clock,
363
+ secondsSinceMidnight: clock.hour * SECONDS_PER_HOUR + clock.minute * SECONDS_PER_MINUTE + clock.second,
364
+ };
365
+ }
366
+ /**
367
+ * `'1951-08-02T09:00:00.000'` — no zone designator, because EDF has no zone.
368
+ *
369
+ * The milliseconds are always `.000`: the header stores whole seconds, and the sub-second start
370
+ * of an EDF+ recording lives in record 0's timekeeping TAL, not here.
371
+ */
372
+ export function formatStartTimeNaive(startTime) {
373
+ const date = startTime.resolvedDate;
374
+ if (date === undefined)
375
+ return undefined;
376
+ return `${formatCalendarDate(date)}T${formatClockTime(startTime.clock)}.000`;
377
+ }
378
+ //# sourceMappingURL=dates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dates.js","sourceRoot":"","sources":["../../src/header/dates.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,105 @@
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 { type EdfNumberParse } from '../bytes/numbers.js';
14
+ import { HEADER_FIELDS } from '../constants.js';
15
+ import { type DiagnosticSink } from '../diagnostics/collector.js';
16
+ import type { EdfRawHeaderFields } from '../types.js';
17
+ /** The ten fixed fields. Same keys as `EdfRawHeaderFields`, by construction. */
18
+ export type FixedHeaderFieldName = keyof typeof HEADER_FIELDS;
19
+ /** `'EDF specification, header record bytes 236-243'`. */
20
+ export declare function fixedFieldSpecReference(field: FixedHeaderFieldName): string;
21
+ /** `'number of data records (8 bytes at offset 236)'`. */
22
+ export declare function describeFixedField(field: FixedHeaderFieldName): string;
23
+ /** Every fixed field as the text it holds, padding included and nothing interpreted. */
24
+ export declare function readRawHeaderFields(headerBytes: Uint8Array): EdfRawHeaderFields;
25
+ /**
26
+ * Report `NON_ASCII_HEADER_FIELD` for every fixed text field carrying a byte outside ASCII
27
+ * 32..126.
28
+ *
29
+ * A warning, never more: the field still decodes truthfully, because edfcore reads header text
30
+ * as ISO-8859-1 — real equipment writes accented patient names and a bare 0xB5 for micro.
31
+ */
32
+ export declare function reportNonAsciiHeaderFields(headerBytes: Uint8Array, sink: DiagnosticSink): void;
33
+ /**
34
+ * Everything a numeric-field diagnostic needs that the parse result does not carry.
35
+ *
36
+ * Per-signal fields use this too — `header/signals.ts` supplies its own offsets and wording, so
37
+ * the comma/malformed/justification decision exists in exactly one place.
38
+ */
39
+ export interface NumericFieldContext {
40
+ /** Goes on the diagnostic's `field`. */
41
+ readonly field: string;
42
+ /** `'number of data records (8 bytes at offset 236)'` — completes the first sentence. */
43
+ readonly description: string;
44
+ readonly byteOffset: number;
45
+ readonly byteLength: number;
46
+ /** What the grammar wanted: `'a whole number of data records, or -1 when unknown'`. */
47
+ readonly expected: string;
48
+ readonly specReference: string;
49
+ /** The actionable next step, without a trailing full stop. */
50
+ readonly nextStep: string;
51
+ readonly signalIndex?: number;
52
+ }
53
+ /**
54
+ * The value of a field that edfcore cannot proceed without.
55
+ *
56
+ * A comma decimal or a failed grammar throws: these fields decide where bytes are and what they
57
+ * mean, so a wrong reading would be worse than no reading.
58
+ */
59
+ export declare function requireNumericField(parse: EdfNumberParse, context: NumericFieldContext, sink: DiagnosticSink): number;
60
+ /**
61
+ * The value of a field edfcore can survive without, or `NaN` when it could not be read.
62
+ *
63
+ * Only for the two fields with an authoritative alternative source — the declared header size,
64
+ * which always loses to the computed one, and the record count, which is recoverable from the
65
+ * source length. The caller reports what it did instead; a comma decimal is still fatal.
66
+ */
67
+ export declare function readNumericField(parse: EdfNumberParse, context: NumericFieldContext, sink: DiagnosticSink): number;
68
+ /**
69
+ * The signal count at offset 252, validated to 1..9999.
70
+ *
71
+ * Fatal on anything else, including a comma, and deliberately reported as
72
+ * `SIGNAL_COUNT_INVALID` rather than as the generic numeric codes: every per-signal field
73
+ * address is `256 + ns * blockOffset + i * width`, so an unusable ns makes every byte after the
74
+ * fixed header unlocatable. It is validated before any ns-sized allocation for the same reason.
75
+ */
76
+ export declare function parseSignalCount(raw: string, sink: DiagnosticSink): number;
77
+ /**
78
+ * The declared header size at offset 184, checked against the computed `256 * (ns + 1)`.
79
+ *
80
+ * The computed value ALWAYS wins, which is what makes this a warning rather than a fatal
81
+ * numeric field: a value edfcore never uses cannot make it read the wrong bytes. An unreadable
82
+ * field is reported the same way and returned as `NaN`, so `header.declaredHeaderByteLength`
83
+ * never claims a size the file did not state.
84
+ */
85
+ export declare function checkDeclaredHeaderByteLength(raw: string, computedHeaderByteLength: number, sink: DiagnosticSink): number;
86
+ /**
87
+ * The record duration at offset 244, in seconds.
88
+ *
89
+ * May legitimately be 0 — an EDF+ file whose records carry only annotations does exactly that —
90
+ * and `ZERO_RECORD_DURATION` says so without refusing the file. A NEGATIVE duration is refused:
91
+ * it is not a tolerable oddity but a value that would make every record onset, sample time and
92
+ * sample rate derived from it run backwards. So is a POSITIVE duration that no 100 ns tick count
93
+ * can express, in either direction — see the comment on that check.
94
+ */
95
+ export declare function parseRecordDuration(raw: string, sink: DiagnosticSink): number;
96
+ /**
97
+ * The declared record count at offset 236, verbatim.
98
+ *
99
+ * `-1` is the sanctioned "the writer never closed the file" value and is returned as `-1`; an
100
+ * unreadable field is returned as `NaN`. Both mean the same thing to the caller — the count has
101
+ * to come from the source length instead — and `header/parse.ts` reports which happened, since
102
+ * only it knows the source length that resolves it.
103
+ */
104
+ export declare function parseDeclaredRecordCount(raw: string, sink: DiagnosticSink): number;
105
+ //# sourceMappingURL=fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../../src/header/fields.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,KAAK,cAAc,EAAmC,MAAM,qBAAqB,CAAC;AAE3F,OAAO,EAAwB,aAAa,EAAoB,MAAM,iBAAiB,CAAC;AACxF,OAAO,EAAuB,KAAK,cAAc,EAAc,MAAM,6BAA6B,CAAC;AAEnG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,gFAAgF;AAChF,MAAM,MAAM,oBAAoB,GAAG,MAAM,OAAO,aAAa,CAAC;AAuC9D,0DAA0D;AAC1D,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAG3E;AAED,0DAA0D;AAC1D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAGtE;AAED,wFAAwF;AACxF,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,UAAU,GAAG,kBAAkB,CAe/E;AA6BD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,GAAG,IAAI,CAyB9F;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,yFAAyF;IACzF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,uFAAuF;IACvF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAuED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,cAAc,GACnB,MAAM,CAKR;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,cAAc,GACnB,MAAM,CAKR;AAmBD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,MAAM,CAsB1E;AAED;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC3C,GAAG,EAAE,MAAM,EACX,wBAAwB,EAAE,MAAM,EAChC,IAAI,EAAE,cAAc,GACnB,MAAM,CAqBR;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,MAAM,CAmE7E;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,MAAM,CAOlF"}