edfcore 0.6.32 → 0.6.34

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.
@@ -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.6.32";
112
+ export declare const VERSION = "0.6.34";
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.6.32';
82
+ export const VERSION = '0.6.34';
83
83
  //# sourceMappingURL=constants.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"format-report.d.ts","sourceRoot":"","sources":["../src/format-report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAKnF;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,gBAAgB,EACxB,OAAO,CAAC,EAAE,mBAAmB,GAC5B,MAAM,CA+ER;AAED,qFAAqF;AACrF,YAAY,EAAE,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"format-report.d.ts","sourceRoot":"","sources":["../src/format-report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAKnF;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,gBAAgB,EACxB,OAAO,CAAC,EAAE,mBAAmB,GAC5B,MAAM,CAuGR;AAED,qFAAqF;AACrF,YAAY,EAAE,SAAS,EAAE,CAAC"}
@@ -63,18 +63,41 @@ export function formatValidationReport(report, options) {
63
63
  if (report.signalStats.length > 0) {
64
64
  lines.push('');
65
65
  lines.push('observed sample ranges:');
66
- for (const stats of report.signalStats) {
66
+ /*
67
+ * The range and the count are sized from the rows, not left to fall where they land.
68
+ *
69
+ * An observed range is as wide as the numbers in it, and those differ per channel: on the
70
+ * PhysioNet polysomnogram the seven rows read `-2048..1819`, `-54..1905` and `136..980`, so
71
+ * the word `over` sat at three different columns in one block and the sample counts under it
72
+ * at three more. It is the same defect as the sample rate (0.6.23), the signal index (0.6.24)
73
+ * and the event clock (0.6.25) — a value wider than the space it was given — in the fourth
74
+ * formatter, and the same fix: measure the rows first.
75
+ *
76
+ * The count is right-aligned because it is a number and the eye compares magnitudes down a
77
+ * column. A single-signal file has one row, so its widths are its own and its output does not
78
+ * move.
79
+ */
80
+ const rows = report.signalStats.map((stats) => {
67
81
  const signal = header?.signals[stats.signalIndex];
68
82
  // Through `printable` for the reason `formatHeader` does it: a label is arbitrary bytes
69
83
  // from the file, and one holding a newline would open a row naming a signal that does not
70
84
  // exist — in a conformance report, which is read precisely because the file is suspect.
71
85
  const name = signal === undefined ? `signal ${stats.signalIndex}` : printable(signal.label);
72
- const overflow = stats.outOfDigitalRangeCount > 0
73
- ? ` ${stats.outOfDigitalRangeCount} outside the declared range`
74
- : '';
75
- lines.push(` ${name.slice(0, 20).padEnd(21)}${stats.observedDigitalMin}..${stats.observedDigitalMax}` +
76
- ` over ${stats.sampleCount.toLocaleString('en-US')} ` +
77
- `${plural('sample', stats.sampleCount)}${overflow}`);
86
+ return {
87
+ name: name.slice(0, 20).padEnd(21),
88
+ range: `${stats.observedDigitalMin}..${stats.observedDigitalMax}`,
89
+ count: stats.sampleCount.toLocaleString('en-US'),
90
+ noun: plural('sample', stats.sampleCount),
91
+ overflow: stats.outOfDigitalRangeCount > 0
92
+ ? ` ${stats.outOfDigitalRangeCount} outside the declared range`
93
+ : '',
94
+ };
95
+ });
96
+ const rangeWidth = Math.max(...rows.map((row) => row.range.length));
97
+ const countWidth = Math.max(...rows.map((row) => row.count.length));
98
+ for (const row of rows) {
99
+ lines.push(` ${row.name}${row.range.padEnd(rangeWidth)} over ${row.count.padStart(countWidth)} ` +
100
+ `${row.noun}${row.overflow}`);
78
101
  }
79
102
  }
80
103
  if (report.diagnostics.length > 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"format-report.js","sourceRoot":"","sources":["../src/format-report.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"format-report.js","sourceRoot":"","sources":["../src/format-report.ts"],"names":[],"mappings":"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"}
package/docs/CHANGELOG.md CHANGED
@@ -6,6 +6,38 @@ 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.6.34
10
+
11
+ - **Fixed** the observed-ranges block in `edfcore validate` falling where the numbers left it. The
12
+ range went straight after the padded label and the sample count straight after that, so both
13
+ columns moved per row. On the PhysioNet polysomnogram that is seven rows and three offsets:
14
+ `-2048..1819 over 7,950,000 samples`, `-54..1905 over 79,500 samples`, `136..980 over 79,500
15
+ samples`.
16
+ - That block exists so a reader can compare channels down a column — it is called "observed sample
17
+ ranges" — and it was the one output where comparing meant re-finding the number on every line.
18
+ - Same defect as the sample rate (0.6.23), the signal index (0.6.24) and the event clock (0.6.25),
19
+ in the fourth and last formatter that lays out a table: a value wider than the space it was
20
+ given. Same fix: measure the rows first. The count is right-aligned, because it is a number and
21
+ magnitudes compare down a column; the range is left-aligned, because `min..max` has no single
22
+ place to hang.
23
+ - A single-signal file has one row, so its widths are its own and its report is byte for byte what
24
+ it was.
25
+
26
+ ## 0.6.33
27
+
28
+ - **Added** a sixteenth shape to the `AWKWARD` matrix: BDF+D, 24-bit samples and a discontinuity in
29
+ one file. The matrix held both halves and never together — every BDF shape in it ran end to end,
30
+ and every discontinuous shape was 16-bit EDF — so a three-byte stride and a chunk boundary that
31
+ is not the nominal grid had never met.
32
+ - They meet in one arithmetic. A read across a gap resolves a record range per contiguous run, and
33
+ each run's bytes are addressed as `headerByteLength + record * recordByteLength +
34
+ signal.recordByteOffset`, where every term is a multiple of `bytesPerSample` — 3 here and 2
35
+ everywhere else in the matrix — and a sign extension that is right for a 16-bit sample is wrong
36
+ for a 24-bit one. An off-by-one in either would show on this file and on no other.
37
+ - Thirty sweeps run over the matrix and all of them pass over it. `twenty-four-bits-across-a-gap.
38
+ test.ts` checks the crossing is real rather than nominal: two segments, a seven-second hole, and
39
+ the same samples through `readWindow` across the join as through the whole record range.
40
+
9
41
  ## 0.6.32
10
42
 
11
43
  - **Added** a fifteenth shape to the `AWKWARD` matrix: a file with two annotation channels of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edfcore",
3
- "version": "0.6.32",
3
+ "version": "0.6.34",
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.6.32';
96
+ export const VERSION = '0.6.34';
@@ -80,20 +80,44 @@ export function formatValidationReport(
80
80
  if (report.signalStats.length > 0) {
81
81
  lines.push('');
82
82
  lines.push('observed sample ranges:');
83
- for (const stats of report.signalStats) {
83
+ /*
84
+ * The range and the count are sized from the rows, not left to fall where they land.
85
+ *
86
+ * An observed range is as wide as the numbers in it, and those differ per channel: on the
87
+ * PhysioNet polysomnogram the seven rows read `-2048..1819`, `-54..1905` and `136..980`, so
88
+ * the word `over` sat at three different columns in one block and the sample counts under it
89
+ * at three more. It is the same defect as the sample rate (0.6.23), the signal index (0.6.24)
90
+ * and the event clock (0.6.25) — a value wider than the space it was given — in the fourth
91
+ * formatter, and the same fix: measure the rows first.
92
+ *
93
+ * The count is right-aligned because it is a number and the eye compares magnitudes down a
94
+ * column. A single-signal file has one row, so its widths are its own and its output does not
95
+ * move.
96
+ */
97
+ const rows = report.signalStats.map((stats) => {
84
98
  const signal = header?.signals[stats.signalIndex];
85
99
  // Through `printable` for the reason `formatHeader` does it: a label is arbitrary bytes
86
100
  // from the file, and one holding a newline would open a row naming a signal that does not
87
101
  // exist — in a conformance report, which is read precisely because the file is suspect.
88
102
  const name = signal === undefined ? `signal ${stats.signalIndex}` : printable(signal.label);
89
- const overflow =
90
- stats.outOfDigitalRangeCount > 0
91
- ? ` ${stats.outOfDigitalRangeCount} outside the declared range`
92
- : '';
103
+ return {
104
+ name: name.slice(0, 20).padEnd(21),
105
+ range: `${stats.observedDigitalMin}..${stats.observedDigitalMax}`,
106
+ count: stats.sampleCount.toLocaleString('en-US'),
107
+ noun: plural('sample', stats.sampleCount),
108
+ overflow:
109
+ stats.outOfDigitalRangeCount > 0
110
+ ? ` ${stats.outOfDigitalRangeCount} outside the declared range`
111
+ : '',
112
+ };
113
+ });
114
+ const rangeWidth = Math.max(...rows.map((row) => row.range.length));
115
+ const countWidth = Math.max(...rows.map((row) => row.count.length));
116
+
117
+ for (const row of rows) {
93
118
  lines.push(
94
- ` ${name.slice(0, 20).padEnd(21)}${stats.observedDigitalMin}..${stats.observedDigitalMax}` +
95
- ` over ${stats.sampleCount.toLocaleString('en-US')} ` +
96
- `${plural('sample', stats.sampleCount)}${overflow}`,
119
+ ` ${row.name}${row.range.padEnd(rangeWidth)} over ${row.count.padStart(countWidth)} ` +
120
+ `${row.noun}${row.overflow}`,
97
121
  );
98
122
  }
99
123
  }