edfcore 0.2.62 → 0.2.63

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,21 @@ 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.62
10
+
11
+ - **Added** `formatAnnotations`, the third formatter beside `formatHeader` and
12
+ `formatDiagnostics`, and the one a hypnogram or an event list actually needs.
13
+ - The clock is built from `onsetTicksFromFirstRecord` by integer division, never from the float
14
+ seconds. An event list is exactly where someone reads a number off the screen and types it into
15
+ something else, and a millisecond field derived from a float64 that came out of a division by
16
+ 10,000,000 can be off by one. A test makes the two fields disagree deliberately, so only a
17
+ formatter reading the exact one passes.
18
+ - Hours are not wrapped at 24 — a 30-hour recording is real, and `30:12` is more useful than
19
+ `06:12` on day two. Times truncate to the millisecond rather than rounding, so the printed
20
+ instant is never later than the event. A NEGATIVE onset prints as one: EDF+ measures onsets from
21
+ the header start time, a recording may begin after its first annotation, and clamping to zero
22
+ would silently move it.
23
+
9
24
  ## 0.2.61
10
25
 
11
26
  - **Deprecated** `sampleIndexAt`, `sampleStartTicks` and `sampleStartSeconds`, which are renamed
@@ -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.62";
112
+ export declare const VERSION = "0.2.63";
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.62';
82
+ export const VERSION = '0.2.63';
83
83
  //# sourceMappingURL=constants.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Annotations as text.
3
+ *
4
+ * Layer 7, and pure. `formatHeader` does this for the header and `formatDiagnostics` for problems;
5
+ * this is the third, and the one a hypnogram or an event list actually needs.
6
+ *
7
+ * The clock is built from `onsetTicksFromFirstRecord` by integer division, never from the float
8
+ * seconds. That is not fussiness: `onsetSecondsFromFirstRecord` is a float64 produced by dividing
9
+ * an exact tick count by 10,000,000, so an onset written `+1.0000001` can print a millisecond
10
+ * field that is off by one — and an event list is exactly where someone reads a number off the
11
+ * screen and types it into something else.
12
+ *
13
+ * A NEGATIVE onset is legal and prints as one. EDF+ measures onsets from the header start time
14
+ * and a recording may begin after its first annotation, so `-00:00:01.500` is a real thing a real
15
+ * file says. Clamping it to zero would silently move an event.
16
+ */
17
+ import type { EdfAnnotation } from './types.js';
18
+ export interface FormatAnnotationsOptions {
19
+ /** Rows to print. Defaults to every annotation; the count of the rest is always stated. */
20
+ readonly maxItems?: number;
21
+ /** Include the `description@@channel` label EDF+ allows. Off by default: most files have none. */
22
+ readonly includeChannel?: boolean;
23
+ }
24
+ /**
25
+ * A multi-line listing, one annotation per line, in the order given.
26
+ *
27
+ * Returns `''` for an empty list rather than a blank line, so it concatenates into a larger
28
+ * report cleanly — the same rule `formatDiagnostics` follows.
29
+ *
30
+ * The order is the caller's. `readAnnotations` already returns them sorted by onset, and
31
+ * re-sorting here would quietly discard a deliberate `filterAnnotationsByText` ordering.
32
+ */
33
+ export declare function formatAnnotations(annotations: readonly EdfAnnotation[], options?: FormatAnnotationsOptions): string;
34
+ //# sourceMappingURL=format-annotations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-annotations.d.ts","sourceRoot":"","sources":["../src/format-annotations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,wBAAwB;IACvC,2FAA2F;IAC3F,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,kGAAkG;IAClG,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;CACnC;AAqCD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,SAAS,aAAa,EAAE,EACrC,OAAO,CAAC,EAAE,wBAAwB,GACjC,MAAM,CA4BR"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Annotations as text.
3
+ *
4
+ * Layer 7, and pure. `formatHeader` does this for the header and `formatDiagnostics` for problems;
5
+ * this is the third, and the one a hypnogram or an event list actually needs.
6
+ *
7
+ * The clock is built from `onsetTicksFromFirstRecord` by integer division, never from the float
8
+ * seconds. That is not fussiness: `onsetSecondsFromFirstRecord` is a float64 produced by dividing
9
+ * an exact tick count by 10,000,000, so an onset written `+1.0000001` can print a millisecond
10
+ * field that is off by one — and an event list is exactly where someone reads a number off the
11
+ * screen and types it into something else.
12
+ *
13
+ * A NEGATIVE onset is legal and prints as one. EDF+ measures onsets from the header start time
14
+ * and a recording may begin after its first annotation, so `-00:00:01.500` is a real thing a real
15
+ * file says. Clamping it to zero would silently move an event.
16
+ */
17
+ import { TICKS_PER_SECOND } from './constants.js';
18
+ const TICKS_PER_MILLISECOND = TICKS_PER_SECOND / 1000n;
19
+ /**
20
+ * `hh:mm:ss.mmm`, from exact ticks.
21
+ *
22
+ * Truncates to the millisecond rather than rounding, so the printed time never names an instant
23
+ * later than the event. Hours are not wrapped at 24 — a 30-hour recording is a real thing and
24
+ * `30:12:00.000` is more useful than `06:12:00.000` on day two.
25
+ */
26
+ function clock(ticks) {
27
+ const negative = ticks < 0n;
28
+ const absolute = negative ? -ticks : ticks;
29
+ const totalMilliseconds = absolute / TICKS_PER_MILLISECOND;
30
+ const milliseconds = totalMilliseconds % 1000n;
31
+ const totalSeconds = totalMilliseconds / 1000n;
32
+ const seconds = totalSeconds % 60n;
33
+ const totalMinutes = totalSeconds / 60n;
34
+ const minutes = totalMinutes % 60n;
35
+ const hours = totalMinutes / 60n;
36
+ const pad = (value, width) => String(value).padStart(width, '0');
37
+ return (`${negative ? '-' : ''}${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)}` +
38
+ `.${pad(milliseconds, 3)}`);
39
+ }
40
+ /** A duration in seconds, or blank. `undefined` and an explicit `0` are the same instant. */
41
+ function duration(annotation) {
42
+ const ticks = annotation.durationTicks;
43
+ if (ticks === undefined || ticks === 0n)
44
+ return '';
45
+ return `${clock(ticks)}`;
46
+ }
47
+ /**
48
+ * A multi-line listing, one annotation per line, in the order given.
49
+ *
50
+ * Returns `''` for an empty list rather than a blank line, so it concatenates into a larger
51
+ * report cleanly — the same rule `formatDiagnostics` follows.
52
+ *
53
+ * The order is the caller's. `readAnnotations` already returns them sorted by onset, and
54
+ * re-sorting here would quietly discard a deliberate `filterAnnotationsByText` ordering.
55
+ */
56
+ export function formatAnnotations(annotations, options) {
57
+ if (annotations.length === 0)
58
+ return '';
59
+ const limit = options?.maxItems === undefined || !Number.isFinite(options.maxItems)
60
+ ? annotations.length
61
+ : Math.max(0, Math.min(annotations.length, Math.floor(options.maxItems)));
62
+ const rows = [];
63
+ for (let i = 0; i < limit; i += 1) {
64
+ const annotation = annotations[i];
65
+ if (annotation === undefined)
66
+ continue;
67
+ const parts = [
68
+ clock(annotation.onsetTicksFromFirstRecord),
69
+ duration(annotation).padEnd(12),
70
+ annotation.text,
71
+ ];
72
+ if (options?.includeChannel === true && annotation.channelLabel !== undefined) {
73
+ parts.push(`@@${annotation.channelLabel}`);
74
+ }
75
+ rows.push(parts.join(' ').trimEnd());
76
+ }
77
+ const hidden = annotations.length - limit;
78
+ // Always stated. A truncated listing that does not say so reads as a complete one.
79
+ if (hidden > 0)
80
+ rows.push(`... and ${hidden} more`);
81
+ return rows.join('\n');
82
+ }
83
+ //# sourceMappingURL=format-annotations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-annotations.js","sourceRoot":"","sources":["../src/format-annotations.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -47,6 +47,8 @@ export { annotationsAt, countAnnotationsByText, filterAnnotationsByText, filterA
47
47
  export { decodeStatusWord, getStatusSignal, readTriggers } from './biosemi.js';
48
48
  export { mergeChunks } from './chunks.js';
49
49
  export { envelopeOfSamples, readEnvelope, readEnvelopeAtResolution, toPhysicalEnvelope, } from './envelope.js';
50
+ export type { FormatAnnotationsOptions } from './format-annotations.js';
51
+ export { formatAnnotations } from './format-annotations.js';
50
52
  export { formatHeader } from './format-header.js';
51
53
  export { inspectEdf } from './inspect.js';
52
54
  export { openEdf, readAnnotations, readRecords, readWindow } from './recording.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -59,6 +59,7 @@ export { annotationsAt, countAnnotationsByText, filterAnnotationsByText, filterA
59
59
  export { decodeStatusWord, getStatusSignal, readTriggers } from './biosemi.js';
60
60
  export { mergeChunks } from './chunks.js';
61
61
  export { envelopeOfSamples, readEnvelope, readEnvelopeAtResolution, toPhysicalEnvelope, } from './envelope.js';
62
+ export { formatAnnotations } from './format-annotations.js';
62
63
  export { formatHeader } from './format-header.js';
63
64
  export { inspectEdf } from './inspect.js';
64
65
  export { openEdf, readAnnotations, readRecords, readWindow } from './recording.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA2EH,OAAO,EACL,wBAAwB,EACxB,cAAc,EACd,uBAAuB,EACvB,QAAQ,EACR,cAAc,EACd,aAAa,EACb,eAAe,EACf,cAAc,EACd,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,gCAAgC,EAChC,gBAAgB,EAChB,OAAO,GACR,MAAM,gBAAgB,CAAC;AAExB,8EAA8E;AAC9E,2EAA2E;AAC3E,2CAA2C;AAC3C,8EAA8E;AAE9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EACL,uBAAuB,EACvB,WAAW,EACX,SAAS,EACT,iBAAiB,EACjB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEnE,8EAA8E;AAC9E,yEAAyE;AACzE,2EAA2E;AAC3E,6CAA6C;AAC7C,8EAA8E;AAE9E,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,8EAA8E;AAC9E,uCAAuC;AACvC,8EAA8E;AAE9E,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,KAAK,EACL,SAAS,GACV,MAAM,mBAAmB,CAAC;AAE3B,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,QAAQ,EACR,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.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.62",
3
+ "version": "0.2.63",
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.62';
96
+ export const VERSION = '0.2.63';
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Annotations as text.
3
+ *
4
+ * Layer 7, and pure. `formatHeader` does this for the header and `formatDiagnostics` for problems;
5
+ * this is the third, and the one a hypnogram or an event list actually needs.
6
+ *
7
+ * The clock is built from `onsetTicksFromFirstRecord` by integer division, never from the float
8
+ * seconds. That is not fussiness: `onsetSecondsFromFirstRecord` is a float64 produced by dividing
9
+ * an exact tick count by 10,000,000, so an onset written `+1.0000001` can print a millisecond
10
+ * field that is off by one — and an event list is exactly where someone reads a number off the
11
+ * screen and types it into something else.
12
+ *
13
+ * A NEGATIVE onset is legal and prints as one. EDF+ measures onsets from the header start time
14
+ * and a recording may begin after its first annotation, so `-00:00:01.500` is a real thing a real
15
+ * file says. Clamping it to zero would silently move an event.
16
+ */
17
+
18
+ import { TICKS_PER_SECOND } from './constants.js';
19
+ import type { EdfAnnotation } from './types.js';
20
+
21
+ export interface FormatAnnotationsOptions {
22
+ /** Rows to print. Defaults to every annotation; the count of the rest is always stated. */
23
+ readonly maxItems?: number;
24
+ /** Include the `description@@channel` label EDF+ allows. Off by default: most files have none. */
25
+ readonly includeChannel?: boolean;
26
+ }
27
+
28
+ const TICKS_PER_MILLISECOND = TICKS_PER_SECOND / 1000n;
29
+
30
+ /**
31
+ * `hh:mm:ss.mmm`, from exact ticks.
32
+ *
33
+ * Truncates to the millisecond rather than rounding, so the printed time never names an instant
34
+ * later than the event. Hours are not wrapped at 24 — a 30-hour recording is a real thing and
35
+ * `30:12:00.000` is more useful than `06:12:00.000` on day two.
36
+ */
37
+ function clock(ticks: bigint): string {
38
+ const negative = ticks < 0n;
39
+ const absolute = negative ? -ticks : ticks;
40
+
41
+ const totalMilliseconds = absolute / TICKS_PER_MILLISECOND;
42
+ const milliseconds = totalMilliseconds % 1000n;
43
+ const totalSeconds = totalMilliseconds / 1000n;
44
+ const seconds = totalSeconds % 60n;
45
+ const totalMinutes = totalSeconds / 60n;
46
+ const minutes = totalMinutes % 60n;
47
+ const hours = totalMinutes / 60n;
48
+
49
+ const pad = (value: bigint, width: number): string => String(value).padStart(width, '0');
50
+ return (
51
+ `${negative ? '-' : ''}${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)}` +
52
+ `.${pad(milliseconds, 3)}`
53
+ );
54
+ }
55
+
56
+ /** A duration in seconds, or blank. `undefined` and an explicit `0` are the same instant. */
57
+ function duration(annotation: EdfAnnotation): string {
58
+ const ticks = annotation.durationTicks;
59
+ if (ticks === undefined || ticks === 0n) return '';
60
+ return `${clock(ticks)}`;
61
+ }
62
+
63
+ /**
64
+ * A multi-line listing, one annotation per line, in the order given.
65
+ *
66
+ * Returns `''` for an empty list rather than a blank line, so it concatenates into a larger
67
+ * report cleanly — the same rule `formatDiagnostics` follows.
68
+ *
69
+ * The order is the caller's. `readAnnotations` already returns them sorted by onset, and
70
+ * re-sorting here would quietly discard a deliberate `filterAnnotationsByText` ordering.
71
+ */
72
+ export function formatAnnotations(
73
+ annotations: readonly EdfAnnotation[],
74
+ options?: FormatAnnotationsOptions,
75
+ ): string {
76
+ if (annotations.length === 0) return '';
77
+
78
+ const limit =
79
+ options?.maxItems === undefined || !Number.isFinite(options.maxItems)
80
+ ? annotations.length
81
+ : Math.max(0, Math.min(annotations.length, Math.floor(options.maxItems)));
82
+
83
+ const rows: string[] = [];
84
+ for (let i = 0; i < limit; i += 1) {
85
+ const annotation = annotations[i];
86
+ if (annotation === undefined) continue;
87
+ const parts = [
88
+ clock(annotation.onsetTicksFromFirstRecord),
89
+ duration(annotation).padEnd(12),
90
+ annotation.text,
91
+ ];
92
+ if (options?.includeChannel === true && annotation.channelLabel !== undefined) {
93
+ parts.push(`@@${annotation.channelLabel}`);
94
+ }
95
+ rows.push(parts.join(' ').trimEnd());
96
+ }
97
+
98
+ const hidden = annotations.length - limit;
99
+ // Always stated. A truncated listing that does not say so reads as a complete one.
100
+ if (hidden > 0) rows.push(`... and ${hidden} more`);
101
+
102
+ return rows.join('\n');
103
+ }
package/src/index.ts CHANGED
@@ -186,6 +186,8 @@ export {
186
186
  readEnvelopeAtResolution,
187
187
  toPhysicalEnvelope,
188
188
  } from './envelope.js';
189
+ export type { FormatAnnotationsOptions } from './format-annotations.js';
190
+ export { formatAnnotations } from './format-annotations.js';
189
191
  export { formatHeader } from './format-header.js';
190
192
  export { inspectEdf } from './inspect.js';
191
193
  export { openEdf, readAnnotations, readRecords, readWindow } from './recording.js';