/** * Turning a request into a concrete conversion plan. * * The plan is where the tool's central promise is enforced: channels recorded at * different sampling rates are never merged into one table. A single wide CSV can * only hold mixed rates by inventing samples for the slow channels — MNE, for * instance, expands three genuine 1 Hz temperature readings into 768 interpolated * values without warning. Instead each distinct rate gets its own file, so every * number in every output file is a number that was actually recorded. */ import type { Diagnostic } from '../edf/errors.js'; import type { EdfSignal } from '../edf/header.js'; import type { ResolvedRange } from './time-range.js'; export interface PlannedChannel { signal: EdfSignal; column: string; decimals: number; } export interface RateGroup { /** Sampling rate in Hz shared by every channel in this group. */ rate: number; samplesPerRecord: number; fileName: string; timeDecimals: number; channels: PlannedChannel[]; /** * How many rows this group's table gets, under whatever window was asked for. * * A rate group is what gets a file, and the same window can hold samples of one rate and * none of another — so this is the number that says whether a file comes out with rows in * it, which `EMPTY_RATE_WINDOW` is raised from and which the sentences about a channel's * cells are true or false of. Set once the window is resolved, since it depends on it. */ rows: number; } export interface PlanInput { signals: readonly EdfSignal[]; recordDuration: number; recordCount: number; hasAnnotationChannel: boolean; /** * True start time of each data record, supplied for discontinuous files. The * requested time window is resolved against these rather than against * `recordCount * recordDuration`, which for a file with gaps is the amount of * data rather than the span of time it covers. */ recordStarts?: Float64Array | null | undefined; } export interface PlanOptions { channels?: readonly string[] | undefined; start?: number | undefined; /** The `--start` value exactly as typed, for error messages. */ startText?: string | undefined; duration?: number | undefined; /** The `--duration` value exactly as typed, for error messages. */ durationText?: string | undefined; end?: number | undefined; /** The `--end` value exactly as typed, for error messages. */ endText?: string | undefined; annotationsOnly?: boolean | undefined; /** Force a fixed number of decimals instead of deriving it per channel. */ decimals?: number | undefined; /** Compress each CSV with gzip, giving every one of them a `.gz` name. */ gzip?: boolean | undefined; /** Start each CSV with a UTF-8 byte order mark, so Excel reads it as UTF-8. */ bom?: boolean | undefined; /** * How the samples are arranged in the CSV. * * `'wide'`, the default, gives one column per channel and one file per sampling rate. * `'long'` gives one file, three columns — `time_s`, `channel`, `value` — and one row per * sample. See ConversionPlan.layout for why that is the only way to put channels recorded * at different rates in one table without inventing samples. */ layout?: 'wide' | 'long' | undefined; /** * Whether the one table this produces goes to stdout rather than to a directory. * * Read only by the warnings that name where their rows land. Nothing about the plan itself * changes — `--stdout` is refused unless the recording makes exactly one table — but three * of those warnings named `signals.csv`, a file such a run never writes. */ toStdout?: boolean | undefined; } export interface ConversionPlan { groups: RateGroup[]; /** * How the samples are arranged. `'wide'` is a column per channel and a file per rate; * `'long'` is `time_s,channel,value`, one row per sample, all rates in one file. * * The wide layout has to split a mixed-rate recording across files: a 100 Hz channel and * a 1 Hz channel share no rows, and putting them in one wide table means either 99 empty * cells out of every hundred or inventing the samples that would fill them. In the long * layout each sample carries its own time, so nothing has to line up and nothing is * invented — which also makes it the one layout `--stdout` can stream for such a file. */ layout: 'wide' | 'long'; /** * Whether the CSVs will be compressed. * * Recorded rather than inferred from the group file names. Under `--annotations-only` * there are no groups to read it off, and `--info` named `annotations.csv` for a run that * wrote `annotations.csv.gz`. */ gzip: boolean; range: ResolvedRange; columnNames: Map; writeSignals: boolean; diagnostics: Diagnostic[]; estimate: OutputEstimate; } export interface OutputEstimate { /** Total data rows across every signal file. */ rows: number; /** * Approximate size of the signal CSVs as CSV text, which under `gzip` is not their size on * disk: what is counted here is what the compressor is handed, and the file holds what it * produces. `--info` writes "before compression" beside this number for that reason, and * `infoJson` calls it a character count. */ bytes: number; /** True when any single file would exceed Excel's row limit. */ exceedsSpreadsheetLimit: boolean; } /** Excel and most spreadsheet tools stop at 1,048,576 rows including the header. */ export declare const SPREADSHEET_ROW_LIMIT = 1048576; export declare function buildPlan(input: PlanInput, options?: PlanOptions): ConversionPlan; /** * A file's diagnostics with the header's mixed-rate warning removed. * * `buildPlan` raises that warning for the channels actually being converted, so keeping both * would either duplicate it or contradict it. The header parser's copy stays where it is, for * callers reading a header without planning a conversion. */ export declare function withoutFileRateWarning(diagnostics: readonly Diagnostic[]): Diagnostic[]; /** * One rate's slug, rendered on its own. * * The names a conversion writes come from `formatRates` over the whole set, which widens the * precision until rates that differ read as differing — so on a recording carrying both, * 1e-6 Hz and 1.25e-6 Hz are `0_000001hz` and `0_00000125hz`. This renders one rate with no * set to separate it from, and both of those come back `0_000001hz`. * * Which is right for the question it is asked, and was worth saying: a caller reaching for the * exported slug function to predict a filename got a name the tool does not write, on exactly * the rates the reference warns `formatRate` collapses. The two now spell a rendered rate the * same way, through the line above, so only the rendering differs and nothing can drift. */ export declare function rateSlug(rate: number): string; /** * The name a CSV is written under, which `--gzip` changes. * * `plan.gzip` is recorded rather than inferred because "`--info` named `annotations.csv` for a * run that wrote `annotations.csv.gz`" — and the warnings *about* those files went on doing it * after the file list stopped. A run that says "annotations.csv holds its header and no rows" * three lines above a summary listing `annotations.csv.gz` names two files and writes one. * * Written once here because three places already computed it by hand. */ export declare function outputCsvName(base: string, gzip: boolean): string;