edfcore 0.2.66 → 0.2.67

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/CHANGELOG.md CHANGED
@@ -6,6 +6,26 @@ alone does not tell you whether you were affected.
6
6
  edfcore is pre-1.0. Patch releases have carried behaviour changes where the old behaviour was a
7
7
  defect; those are called out below.
8
8
 
9
+ ## 0.2.67
10
+
11
+ Three defects in `formatHeader`, found by an adversarial sweep of the modules no earlier pass had
12
+ covered.
13
+
14
+ - **Fixed** the duration line losing a whole second. It computed `recordCount *
15
+ recordDurationSeconds` in float64 and truncated, and a record duration with no exact binary form
16
+ makes that product land just under the true value: 100 records of 0.29 s is exactly 29 s,
17
+ computes as 28.999999999999996, and printed `00:00:28`. It is now computed from
18
+ `recordDurationTicks`, which is exact. A genuine fraction still truncates rather than rounding —
19
+ 7 × 0.7 s is 4.9 and prints `00:00:04`, because rounding would name a time the file never reaches.
20
+ - **Fixed** control characters in a signal label being printed verbatim. EDF pads labels with
21
+ spaces and says nothing about what else may be in them, so a writer can put a newline there and
22
+ the label renders as TWO rows — forging a signal the file does not contain — while a tab shifts
23
+ every column after it. They are replaced with a dot in the rendering only; `signal.raw.label`
24
+ still holds the bytes.
25
+ - **Fixed** the diagnostic severity summary being ordered by arrival, so two files with the same
26
+ diagnostics could summarise them differently. Now error-warning-info, matching
27
+ `formatValidationReport` since 0.2.15 and sharing its counting.
28
+
9
29
  ## 0.2.66
10
30
 
11
31
  - **Fixed** the changelog numbering, which had drifted a second time, and **fixed the cause** so it
@@ -109,5 +109,5 @@ export declare const SIGNAL_FIELD_BLOCK_OFFSETS: {
109
109
  readonly reserved: 224;
110
110
  };
111
111
  /** Published package version. Kept in sync with package.json by a test. */
112
- export declare const VERSION = "0.2.66";
112
+ export declare const VERSION = "0.2.67";
113
113
  //# sourceMappingURL=constants.d.ts.map
package/dist/constants.js CHANGED
@@ -79,5 +79,5 @@ export const SIGNAL_FIELD_BLOCK_OFFSETS = {
79
79
  reserved: 224,
80
80
  };
81
81
  /** Published package version. Kept in sync with package.json by a test. */
82
- export const VERSION = '0.2.66';
82
+ export const VERSION = '0.2.67';
83
83
  //# sourceMappingURL=constants.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"format-header.d.ts","sourceRoot":"","sources":["../src/format-header.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAmB,SAAS,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAwBlF;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,MAAM,CA0DrF"}
1
+ {"version":3,"file":"format-header.d.ts","sourceRoot":"","sources":["../src/format-header.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAmB,SAAS,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AA0ClF;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAsErF"}
@@ -11,6 +11,8 @@
11
11
  * identification unless asked — a header carries a name and a birth date, and a summary that
12
12
  * lands in a chat log or an issue tracker should not carry them by default.
13
13
  */
14
+ import { TICKS_PER_SECOND } from './constants.js';
15
+ import { summarizeDiagnostics } from './diagnostics/summary.js';
14
16
  function formatDate(date) {
15
17
  if (date === undefined)
16
18
  return 'unknown';
@@ -18,16 +20,33 @@ function formatDate(date) {
18
20
  const day = String(date.day).padStart(2, '0');
19
21
  return `${date.year}-${month}-${day}`;
20
22
  }
21
- function formatDuration(seconds) {
22
- if (!Number.isFinite(seconds) || seconds < 0)
23
+ /**
24
+ * `hh:mm:ss` from an exact tick count.
25
+ *
26
+ * Ticks, not `recordCount * recordDurationSeconds`. That product is float64, and a record duration
27
+ * with no exact binary representation makes it land just under the true value: 100 records of
28
+ * 0.29 s is exactly 29 s and computes as 28.999999999999996, which floors to 28. The header line
29
+ * then reports a recording a whole second shorter than it is (fixed in 0.2.67).
30
+ */
31
+ function formatDurationTicks(ticks) {
32
+ if (ticks < 0n)
23
33
  return 'unknown';
24
- const whole = Math.floor(seconds);
34
+ const whole = Number(ticks / TICKS_PER_SECOND);
25
35
  const hours = Math.floor(whole / 3600);
26
36
  const minutes = Math.floor((whole % 3600) / 60);
27
37
  const rest = whole % 60;
28
38
  const pad = (n) => String(n).padStart(2, '0');
29
39
  return `${pad(hours)}:${pad(minutes)}:${pad(rest)}`;
30
40
  }
41
+ /** Control characters become a dot, so one field can never become two rows or shift a column. */
42
+ function printable(text) {
43
+ let out = '';
44
+ for (const character of text) {
45
+ const code = character.codePointAt(0) ?? 0;
46
+ out += code < 0x20 || code === 0x7f ? '.' : character;
47
+ }
48
+ return out;
49
+ }
31
50
  function formatRate(signal) {
32
51
  // undefined is the honest answer for a zero record duration, which is legal EDF.
33
52
  return signal.sampleRateHz === undefined ? '—' : `${signal.sampleRateHz} Hz`;
@@ -48,7 +67,7 @@ export function formatHeader(header, options) {
48
67
  ` (local, no timezone)`);
49
68
  lines.push(`record ${header.recordDurationSeconds} s · ${header.recordByteLength} bytes · ` +
50
69
  `${header.bytesPerSample} bytes/sample`);
51
- lines.push(`duration ${formatDuration(header.recordCount * header.recordDurationSeconds)} ` +
70
+ lines.push(`duration ${formatDurationTicks(header.recordDurationTicks * BigInt(header.recordCount))} ` +
52
71
  `(${header.recordCount} × ${header.recordDurationSeconds} s)`);
53
72
  if (header.recordCountSource === 'sourceByteLength') {
54
73
  // Worth saying out loud: the count came from the file size, not from the header field.
@@ -62,7 +81,11 @@ export function formatHeader(header, options) {
62
81
  lines.push(' # label kind rate range');
63
82
  for (const signal of header.signals) {
64
83
  const index = String(signal.index).padStart(3);
65
- const label = signal.label.slice(0, 20).padEnd(21);
84
+ // Control characters are replaced, not printed. A label holding a newline would otherwise
85
+ // render as two rows and forge a signal the file does not contain; a tab would shift every
86
+ // column after it. EDF pads labels with spaces and says nothing about what else may be in
87
+ // them, so a writer can put anything there and a reader must not be steered by it.
88
+ const label = printable(signal.label).slice(0, 20).padEnd(21);
66
89
  const kind = signal.kind.padEnd(12);
67
90
  const rate = formatRate(signal).padEnd(9);
68
91
  const range = signal.kind === 'annotations'
@@ -74,11 +97,17 @@ export function formatHeader(header, options) {
74
97
  }
75
98
  if (header.diagnostics.length > 0) {
76
99
  lines.push('');
77
- const counts = new Map();
78
- for (const diagnostic of header.diagnostics) {
79
- counts.set(diagnostic.severity, (counts.get(diagnostic.severity) ?? 0) + 1);
80
- }
81
- const summary = [...counts].map(([severity, count]) => `${count} ${severity}`).join(', ');
100
+ // Fixed error-warning-info order, matching `formatValidationReport` since 0.2.15. Ordering by
101
+ // arrival meant two files with the same diagnostics could summarise them differently.
102
+ const counted = summarizeDiagnostics(header.diagnostics);
103
+ const summary = [
104
+ [counted.errors, 'error'],
105
+ [counted.warnings, 'warning'],
106
+ [counted.infos, 'info'],
107
+ ]
108
+ .filter(([count]) => count > 0)
109
+ .map(([count, severity]) => `${count} ${severity}`)
110
+ .join(', ');
82
111
  lines.push(`${header.diagnostics.length} diagnostic(s): ${summary}`);
83
112
  lines.push('Call formatDiagnostics(header.diagnostics) for the detail.');
84
113
  }
@@ -1 +1 @@
1
- {"version":3,"file":"format-header.js","sourceRoot":"","sources":["../src/format-header.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"format-header.js","sourceRoot":"","sources":["../src/format-header.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edfcore",
3
- "version": "0.2.66",
3
+ "version": "0.2.67",
4
4
  "description": "Modern, typed, zero-dependency reader for EDF, EDF+, BDF and BDF+ biosignal files. Works in browsers and Node with true random access.",
5
5
  "keywords": [
6
6
  "edf",
package/src/constants.ts CHANGED
@@ -93,4 +93,4 @@ export const SIGNAL_FIELD_BLOCK_OFFSETS = {
93
93
  } as const;
94
94
 
95
95
  /** Published package version. Kept in sync with package.json by a test. */
96
- export const VERSION = '0.2.66';
96
+ export const VERSION = '0.2.67';
@@ -12,6 +12,8 @@
12
12
  * lands in a chat log or an issue tracker should not carry them by default.
13
13
  */
14
14
 
15
+ import { TICKS_PER_SECOND } from './constants.js';
16
+ import { summarizeDiagnostics } from './diagnostics/summary.js';
15
17
  import type { EdfCalendarDate, EdfHeader, FormatHeaderOptions } from './types.js';
16
18
 
17
19
  function formatDate(date: EdfCalendarDate | undefined): string {
@@ -21,9 +23,17 @@ function formatDate(date: EdfCalendarDate | undefined): string {
21
23
  return `${date.year}-${month}-${day}`;
22
24
  }
23
25
 
24
- function formatDuration(seconds: number): string {
25
- if (!Number.isFinite(seconds) || seconds < 0) return 'unknown';
26
- const whole = Math.floor(seconds);
26
+ /**
27
+ * `hh:mm:ss` from an exact tick count.
28
+ *
29
+ * Ticks, not `recordCount * recordDurationSeconds`. That product is float64, and a record duration
30
+ * with no exact binary representation makes it land just under the true value: 100 records of
31
+ * 0.29 s is exactly 29 s and computes as 28.999999999999996, which floors to 28. The header line
32
+ * then reports a recording a whole second shorter than it is (fixed in 0.2.67).
33
+ */
34
+ function formatDurationTicks(ticks: bigint): string {
35
+ if (ticks < 0n) return 'unknown';
36
+ const whole = Number(ticks / TICKS_PER_SECOND);
27
37
  const hours = Math.floor(whole / 3600);
28
38
  const minutes = Math.floor((whole % 3600) / 60);
29
39
  const rest = whole % 60;
@@ -31,6 +41,16 @@ function formatDuration(seconds: number): string {
31
41
  return `${pad(hours)}:${pad(minutes)}:${pad(rest)}`;
32
42
  }
33
43
 
44
+ /** Control characters become a dot, so one field can never become two rows or shift a column. */
45
+ function printable(text: string): string {
46
+ let out = '';
47
+ for (const character of text) {
48
+ const code = character.codePointAt(0) ?? 0;
49
+ out += code < 0x20 || code === 0x7f ? '.' : character;
50
+ }
51
+ return out;
52
+ }
53
+
34
54
  function formatRate(signal: EdfHeader['signals'][number]): string {
35
55
  // undefined is the honest answer for a zero record duration, which is legal EDF.
36
56
  return signal.sampleRateHz === undefined ? '—' : `${signal.sampleRateHz} Hz`;
@@ -60,7 +80,7 @@ export function formatHeader(header: EdfHeader, options?: FormatHeaderOptions):
60
80
  `${header.bytesPerSample} bytes/sample`,
61
81
  );
62
82
  lines.push(
63
- `duration ${formatDuration(header.recordCount * header.recordDurationSeconds)} ` +
83
+ `duration ${formatDurationTicks(header.recordDurationTicks * BigInt(header.recordCount))} ` +
64
84
  `(${header.recordCount} × ${header.recordDurationSeconds} s)`,
65
85
  );
66
86
  if (header.recordCountSource === 'sourceByteLength') {
@@ -77,7 +97,11 @@ export function formatHeader(header: EdfHeader, options?: FormatHeaderOptions):
77
97
  lines.push(' # label kind rate range');
78
98
  for (const signal of header.signals) {
79
99
  const index = String(signal.index).padStart(3);
80
- const label = signal.label.slice(0, 20).padEnd(21);
100
+ // Control characters are replaced, not printed. A label holding a newline would otherwise
101
+ // render as two rows and forge a signal the file does not contain; a tab would shift every
102
+ // column after it. EDF pads labels with spaces and says nothing about what else may be in
103
+ // them, so a writer can put anything there and a reader must not be steered by it.
104
+ const label = printable(signal.label).slice(0, 20).padEnd(21);
81
105
  const kind = signal.kind.padEnd(12);
82
106
  const rate = formatRate(signal).padEnd(9);
83
107
  const range =
@@ -91,11 +115,19 @@ export function formatHeader(header: EdfHeader, options?: FormatHeaderOptions):
91
115
 
92
116
  if (header.diagnostics.length > 0) {
93
117
  lines.push('');
94
- const counts = new Map<string, number>();
95
- for (const diagnostic of header.diagnostics) {
96
- counts.set(diagnostic.severity, (counts.get(diagnostic.severity) ?? 0) + 1);
97
- }
98
- const summary = [...counts].map(([severity, count]) => `${count} ${severity}`).join(', ');
118
+ // Fixed error-warning-info order, matching `formatValidationReport` since 0.2.15. Ordering by
119
+ // arrival meant two files with the same diagnostics could summarise them differently.
120
+ const counted = summarizeDiagnostics(header.diagnostics);
121
+ const summary = (
122
+ [
123
+ [counted.errors, 'error'],
124
+ [counted.warnings, 'warning'],
125
+ [counted.infos, 'info'],
126
+ ] as const
127
+ )
128
+ .filter(([count]) => count > 0)
129
+ .map(([count, severity]) => `${count} ${severity}`)
130
+ .join(', ');
99
131
  lines.push(`${header.diagnostics.length} diagnostic(s): ${summary}`);
100
132
  lines.push('Call formatDiagnostics(header.diagnostics) for the detail.');
101
133
  }