edf2csv 0.7.260 → 0.8.1
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/dist/cli/report.js +4 -32
- package/dist/cli/report.js.map +1 -1
- package/dist/cli.js +21 -5
- package/dist/cli.js.map +1 -1
- package/dist/convert/run.d.ts +31 -0
- package/dist/convert/run.js +67 -1
- package/dist/convert/run.js.map +1 -1
- package/dist/edf/header.d.ts +24 -0
- package/dist/edf/header.js +1 -1
- package/dist/edf/header.js.map +1 -1
- package/dist/format/unprintable.d.ts +11 -0
- package/dist/format/unprintable.js +45 -0
- package/dist/format/unprintable.js.map +1 -0
- package/package.json +1 -1
package/dist/convert/run.d.ts
CHANGED
|
@@ -106,6 +106,37 @@ export declare function durationDiagnostics(annotations: readonly Annotation[],
|
|
|
106
106
|
from: number;
|
|
107
107
|
to: number;
|
|
108
108
|
}): Diagnostic[];
|
|
109
|
+
/**
|
|
110
|
+
* What the descriptions in the events that will actually be written look like.
|
|
111
|
+
*
|
|
112
|
+
* EDF's four free-text header fields have had two warnings about where they land since they
|
|
113
|
+
* were written: `FORMULA_LABEL` for text a spreadsheet runs instead of reading, and
|
|
114
|
+
* `NONPRINTABLE_LABEL` for bytes that drive a terminal. Both say the same thing about the
|
|
115
|
+
* remedy — the text is written exactly as the file has it, because rewriting it would mean
|
|
116
|
+
* the CSV no longer says what the recording says — and both exist so that the tool is not
|
|
117
|
+
* silent about where it goes.
|
|
118
|
+
*
|
|
119
|
+
* `annotations.csv`'s `description` column is the same kind of text, out of the same file,
|
|
120
|
+
* into the same spreadsheet, and nothing was said about it at all. An event described
|
|
121
|
+
* `=HYPERLINK("http://…","Sleep stage W")` was written verbatim, exit 0, no warning, and
|
|
122
|
+
* opens as a live link nobody in the reading chain wrote; one carrying `\x1b[31m` turns the
|
|
123
|
+
* terminal red on `cat annotations.csv`. It is the more likely of the two to happen by
|
|
124
|
+
* accident, since a description is typed by a person at a scoring station while a channel
|
|
125
|
+
* label is written once by the recorder.
|
|
126
|
+
*
|
|
127
|
+
* It is also the only free text in the output that can carry a character above U+00FF —
|
|
128
|
+
* header text is decoded latin1, so every byte of it becomes a code point below U+0100, and
|
|
129
|
+
* a bidirectional override cannot reach a label. It can reach a description, which is UTF-8.
|
|
130
|
+
*
|
|
131
|
+
* Counted rather than raised per event, unlike the header's four fields: a night's scoring is
|
|
132
|
+
* thousands of events, and a warning each is not a report. The count is of the rows that
|
|
133
|
+
* reach `annotations.csv`, after the same window filter the writer applies, for the reason
|
|
134
|
+
* `durationDiagnostics` beside it gives.
|
|
135
|
+
*/
|
|
136
|
+
export declare function descriptionDiagnostics(annotations: readonly Annotation[], window: {
|
|
137
|
+
from: number;
|
|
138
|
+
to: number;
|
|
139
|
+
}): Diagnostic[];
|
|
109
140
|
/**
|
|
110
141
|
* The window annotations are filtered by — the bounds as asked for, not as snapped to records.
|
|
111
142
|
*
|
package/dist/convert/run.js
CHANGED
|
@@ -11,10 +11,11 @@ import { finished } from 'node:stream/promises';
|
|
|
11
11
|
import { createGzip, gzipSync } from 'node:zlib';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { EdfFile } from '../edf/reader.js';
|
|
14
|
-
import { describeFormat, formatRates, formatWallClock } from '../edf/header.js';
|
|
14
|
+
import { describeFormat, formatRates, formatWallClock, startsFormula } from '../edf/header.js';
|
|
15
15
|
import { EdfError } from '../edf/errors.js';
|
|
16
16
|
import { BufferedLineWriter, DEFAULT_FLUSH_THRESHOLD, UTF8_BOM, csvRow, escapeCsvField, } from '../format/csv.js';
|
|
17
17
|
import { counted, listed } from '../format/list.js';
|
|
18
|
+
import { escapeCharacter, unprintableIn } from '../format/unprintable.js';
|
|
18
19
|
import { makeSampleFormatter, makeTimeFormatter, newOffsetBudget, newSampleCacheBudget, plain, } from '../format/number.js';
|
|
19
20
|
import { TIME_COLUMN } from './channels.js';
|
|
20
21
|
import { assertInputPath } from './options.js';
|
|
@@ -147,6 +148,7 @@ export async function convert(inputPath, options = {}) {
|
|
|
147
148
|
// Reported against the rows that will be written, not against the file; see
|
|
148
149
|
// durationDiagnostics.
|
|
149
150
|
plan.diagnostics.push(...durationDiagnostics(annotationData.annotations, window));
|
|
151
|
+
plan.diagnostics.push(...descriptionDiagnostics(annotationData.annotations, window));
|
|
150
152
|
const result = await writeAnnotationsCsv(outputDir, annotationData.annotations, window, options.gzip === true, options.bom === true);
|
|
151
153
|
written.push(result);
|
|
152
154
|
annotationsWritten = result.rows;
|
|
@@ -1048,6 +1050,70 @@ export function durationDiagnostics(annotations, window) {
|
|
|
1048
1050
|
}
|
|
1049
1051
|
return diagnostics;
|
|
1050
1052
|
}
|
|
1053
|
+
/**
|
|
1054
|
+
* What the descriptions in the events that will actually be written look like.
|
|
1055
|
+
*
|
|
1056
|
+
* EDF's four free-text header fields have had two warnings about where they land since they
|
|
1057
|
+
* were written: `FORMULA_LABEL` for text a spreadsheet runs instead of reading, and
|
|
1058
|
+
* `NONPRINTABLE_LABEL` for bytes that drive a terminal. Both say the same thing about the
|
|
1059
|
+
* remedy — the text is written exactly as the file has it, because rewriting it would mean
|
|
1060
|
+
* the CSV no longer says what the recording says — and both exist so that the tool is not
|
|
1061
|
+
* silent about where it goes.
|
|
1062
|
+
*
|
|
1063
|
+
* `annotations.csv`'s `description` column is the same kind of text, out of the same file,
|
|
1064
|
+
* into the same spreadsheet, and nothing was said about it at all. An event described
|
|
1065
|
+
* `=HYPERLINK("http://…","Sleep stage W")` was written verbatim, exit 0, no warning, and
|
|
1066
|
+
* opens as a live link nobody in the reading chain wrote; one carrying `\x1b[31m` turns the
|
|
1067
|
+
* terminal red on `cat annotations.csv`. It is the more likely of the two to happen by
|
|
1068
|
+
* accident, since a description is typed by a person at a scoring station while a channel
|
|
1069
|
+
* label is written once by the recorder.
|
|
1070
|
+
*
|
|
1071
|
+
* It is also the only free text in the output that can carry a character above U+00FF —
|
|
1072
|
+
* header text is decoded latin1, so every byte of it becomes a code point below U+0100, and
|
|
1073
|
+
* a bidirectional override cannot reach a label. It can reach a description, which is UTF-8.
|
|
1074
|
+
*
|
|
1075
|
+
* Counted rather than raised per event, unlike the header's four fields: a night's scoring is
|
|
1076
|
+
* thousands of events, and a warning each is not a report. The count is of the rows that
|
|
1077
|
+
* reach `annotations.csv`, after the same window filter the writer applies, for the reason
|
|
1078
|
+
* `durationDiagnostics` beside it gives.
|
|
1079
|
+
*/
|
|
1080
|
+
export function descriptionDiagnostics(annotations, window) {
|
|
1081
|
+
const written = annotations.filter((a) => a.onset >= window.from && a.onset < window.to);
|
|
1082
|
+
const diagnostics = [];
|
|
1083
|
+
const formulaic = written.filter((a) => startsFormula(a.text));
|
|
1084
|
+
if (formulaic.length > 0) {
|
|
1085
|
+
const one = formulaic.length === 1;
|
|
1086
|
+
const shown = [...new Set(formulaic.map((a) => a.text[0]))].join(', ');
|
|
1087
|
+
diagnostics.push({
|
|
1088
|
+
code: 'FORMULA_LABEL',
|
|
1089
|
+
severity: 'warning',
|
|
1090
|
+
message: `${counted(formulaic.length, 'annotation')} ${one ? 'has a description' : 'have descriptions'} ` +
|
|
1091
|
+
`starting with ${shown}, which Excel, LibreOffice and Google Sheets read as the start ` +
|
|
1092
|
+
`of a formula rather than as text.`,
|
|
1093
|
+
hint: 'The text is written to annotations.csv exactly as the file has it, so the cell is ' +
|
|
1094
|
+
'what the recording says. Open the CSV with pandas or R, or import it into the ' +
|
|
1095
|
+
'spreadsheet as text, if you do not want it evaluated.',
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
const marked = written.filter((a) => unprintableIn(a.text).length > 0);
|
|
1099
|
+
if (marked.length > 0) {
|
|
1100
|
+
const one = marked.length === 1;
|
|
1101
|
+
const shown = [...new Set(marked.flatMap((a) => unprintableIn(a.text)))]
|
|
1102
|
+
.map(escapeCharacter)
|
|
1103
|
+
.join(', ');
|
|
1104
|
+
diagnostics.push({
|
|
1105
|
+
code: 'NONPRINTABLE_LABEL',
|
|
1106
|
+
severity: 'warning',
|
|
1107
|
+
message: `${counted(marked.length, 'annotation')} ${one ? 'has a description' : 'have descriptions'} ` +
|
|
1108
|
+
`carrying text a terminal does not print as itself (${shown}), written to ` +
|
|
1109
|
+
`annotations.csv exactly as the file has ${one ? 'it' : 'them'}.`,
|
|
1110
|
+
hint: 'A control byte can drive the terminal and a bidirectional override reverses what ' +
|
|
1111
|
+
'follows it, so read the file with pandas or R rather than with cat. The cell is ' +
|
|
1112
|
+
'what the recording says either way.',
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
return diagnostics;
|
|
1116
|
+
}
|
|
1051
1117
|
/**
|
|
1052
1118
|
* The window annotations are filtered by — the bounds as asked for, not as snapped to records.
|
|
1053
1119
|
*
|