Streams, cancellation, progress and cultures¶
Who closes the stream¶
One rule at every entry point: a stream handed over is closed — when the cursor or run is disposed, and also when the call fails — unless the caller asked for it to stay open.
| Entry point | Closes the stream | Keep it open with |
|---|---|---|
new CsvCursor(stream, …), new XlsxCursor(stream, …) |
on Dispose, or on a failed open |
leaveOpen: true |
TabularFile.Open(stream, …) |
on Dispose, or on a failed open |
TabularOpenOptions.LeaveOpen |
TabularImporter.Import(stream, …) |
on Dispose of the run, or when the call throws (a refused plan included) |
ImportOptions.Open.LeaveOpen |
TabularImporter.Import(cursor, …), TabularAnalyzer.Analyze, TabularExtractor.Start |
never — the cursor is the caller's | — |
TabularOpenOptions also carries the csv, xlsx and ods cursor options and the archive's bounds, so
ceilings can be changed without giving up format detection. The files inside an archive are read with
the same csv, xlsx and ods options as files on their own.
Cancellation¶
ReadRow takes a CancellationToken, and the analyzer and importer hand theirs down rather than
only checking between rows. That distinction is the whole of it: the expensive things happen inside
a single read — a shared string table of a million entries loads on the first cell that refers to
one — and a check between rows never runs while that is happening. A timeout at the request level
cannot reach a thread that is inside such a call.
The token is checked on a stride rather than per character, because the check is cheap but not free and a row is normally over in a few hundred characters.
Every operation that reads takes a token, last parameter, as the BCL's do: Open, Analyze,
Start and Import for the work they do up front, and ExtractionSession.ReadRow,
ImportRun.Rows, InChunks and All for the reading. The token a run was started with keeps
applying to every read of it, so either one stops the run — a plain foreach over the run, which
cannot pass a token, is stopped by the run's.
Progress¶
Analysis of a multi-million-row file takes seconds to tens of seconds, and a screen waiting on it wants to say how far it has got:
IProgress<AnalysisProgress> progress = new Progress<AnalysisProgress>(p =>
Console.WriteLine($"{p.SheetName}: {p.RowsRead:N0} rows, {p.Fraction:P0}"));
FileProfile profile = new TabularAnalyzer().Analyze(cursor, progress, cancellationToken);
The fraction is taken from how much of the file the reader has consumed — a csv's stream position
against its length, a workbook's worksheet bytes against their total, which the package directory
states up front — so nothing reads the file twice to have a denominator. It is exactly 1 in the final
report, which has IsComplete set; where the stream has no length it is null until then.
Reports go out when the fraction has moved by AnalysisOptions.ProgressStep (1% by default) and
at least ProgressInterval data rows (10,000) have passed since the last one. A five-million-row
file reports about a hundred times; a file of twenty thousand rows, read in milliseconds, once or
twice. With no length to measure, the row interval alone decides. Progress<T> posts each report to
the context it was created on; an IProgress<T> of your own is called on the analysing thread.
A console progress bar with Ctrl+C cancellation, runnable:
samples/TriasDev.Tabular.Samples.Progress.
Cultures¶
Every text value is tried under each culture in AnalysisOptions.Cultures, by default
["", "de-DE", "en-US"] — invariant, German and US conventions — and the ranked hypotheses name the
culture that read a column — the empty string for the invariant one, the same spelling
MappingPlan.Culture takes, so a hypothesis's culture goes into a plan as it is. The default leans towards the files this library was first written for;
a caller whose files come from elsewhere should list its own (["", "fr-FR"]). An unknown name is
refused when the analyzer is created.
Under invariant globalization (InvariantGlobalization / DOTNET_SYSTEM_GLOBALIZATION_INVARIANT,
common in slim container images) only the invariant culture exists. Analysis then leaves the named
cultures out instead of failing — the profile's ParseCounts show which cultures were used — and a
mapping that names one is refused by the validator and the precheck as mapping.unknown-culture.
German amounts such as 1.234,50 cannot be read as numbers in that mode.