edfcore 0.6.33 → 0.6.35

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.33";
112
+ export declare const VERSION = "0.6.35";
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.33';
82
+ export const VERSION = '0.6.35';
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,43 @@ 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.35
10
+
11
+ - **Added** the census that closes the class four releases in a row have been fixing. A sample rate
12
+ of `68.96551724137932` in a nine-character column (0.6.23), a signal index of 1000 in three
13
+ (0.6.24), a negative or hundred-hour clock in twelve (0.6.25), an observed range and sample count
14
+ that were not padded at all (0.6.34) — the same mistake in four formatters, each found by looking
15
+ at output rather than by anything failing.
16
+ - `every-column-knows-its-width.test.ts` lists every `padStart` and `padEnd` in `src/` that lays
17
+ out a column, with one of three reasons its width is safe: MEASURED from the rows being printed,
18
+ bounded by the FILE FORMAT (a label is 16 bytes, `kind` is one of two words, a rate is rounded to
19
+ fit), or bounded by an ARGUMENT no test can build.
20
+ - Two are in that last group and are named rather than assumed: the per-code diagnostic count and
21
+ the event count, with the arithmetic written out — seven digits is 9,999,999 of one code, and the
22
+ record count field is eight characters, so a per-record code on the largest file the format can
23
+ describe would need eight. They are not measured because no test can construct the report that
24
+ would show it, and saying so is better than a floor nobody can justify.
25
+ - A new width fails until it says which of the three it is. Zero-padding is excluded by pattern: a
26
+ clock digit spelling itself is not a place in a table.
27
+ - No behaviour changed.
28
+
29
+ ## 0.6.34
30
+
31
+ - **Fixed** the observed-ranges block in `edfcore validate` falling where the numbers left it. The
32
+ range went straight after the padded label and the sample count straight after that, so both
33
+ columns moved per row. On the PhysioNet polysomnogram that is seven rows and three offsets:
34
+ `-2048..1819 over 7,950,000 samples`, `-54..1905 over 79,500 samples`, `136..980 over 79,500
35
+ samples`.
36
+ - That block exists so a reader can compare channels down a column — it is called "observed sample
37
+ ranges" — and it was the one output where comparing meant re-finding the number on every line.
38
+ - Same defect as the sample rate (0.6.23), the signal index (0.6.24) and the event clock (0.6.25),
39
+ in the fourth and last formatter that lays out a table: a value wider than the space it was
40
+ given. Same fix: measure the rows first. The count is right-aligned, because it is a number and
41
+ magnitudes compare down a column; the range is left-aligned, because `min..max` has no single
42
+ place to hang.
43
+ - A single-signal file has one row, so its widths are its own and its report is byte for byte what
44
+ it was.
45
+
9
46
  ## 0.6.33
10
47
 
11
48
  - **Added** a sixteenth shape to the `AWKWARD` matrix: BDF+D, 24-bit samples and a discontinuity in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edfcore",
3
- "version": "0.6.33",
3
+ "version": "0.6.35",
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.33';
96
+ export const VERSION = '0.6.35';
@@ -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
  }