HyperTabular 0.8.0
dotnet add package HyperTabular --version 0.8.0
NuGet\Install-Package HyperTabular -Version 0.8.0
<PackageReference Include="HyperTabular" Version="0.8.0" />
<PackageVersion Include="HyperTabular" Version="0.8.0" />
<PackageReference Include="HyperTabular" />
paket add HyperTabular --version 0.8.0
#r "nuget: HyperTabular, 0.8.0"
#:package HyperTabular@0.8.0
#addin nuget:?package=HyperTabular&version=0.8.0
#tool nuget:?package=HyperTabular&version=0.8.0
HyperTabular for .NET
Delimited text — CSV, TSV, any single-byte ASCII separator — and workbooks — XLSX and ODS —
read a batch at a time into typed columns, with a
HyperCast Verdict for every cell.
using System.Text;
using HyperCast;
using HyperTabular;
// Open, read the header, find each column by name, then declare the plan.
using var reader = DelimitedReader.Open("orders.csv", Dialect.Csv);
var header = reader.Header!;
reader.Bind([
Column.Int32(header.Ordinal("id")),
Column.Text(header.Ordinal("customer")),
Column.Double(header.Ordinal("score")),
]);
while (reader.Read() is { } batch)
{
// A column at a time, as the core wrote it…
ReadOnlySpan<int> ids = batch.Values<int>(0);
ReadOnlySpan<CellVerdict> verdicts = batch.Verdicts(0);
// …or a row at a time, each cell HyperCast's union.
foreach (var row in batch)
{
var line = row.Get<double>(2) switch
{
Success<double> score => $"{row.GetString(1)}: {score.Value}",
Fault fault => $"line {row.Line}: {fault.Reason} in \"{Encoding.UTF8.GetString(row.Raw(2))}\"",
};
}
}
// A workbook reads into the same batch, from a path, memory or a stream.
using var book = Workbook.Open("orders.xlsx");
var sheet = book.Sheet("Orders", SheetOptions.Default);
sheet.Bind([Column.Int32(sheet.Header!.Ordinal("id")), Column.Text(sheet.Header.Ordinal("customer"))]);
foreach (var row in sheet.Rows()) { /* … */ }
A plan whose positions are known can still be given up front —
DelimitedReader.Open(path, Dialect.Csv, plan), book.Sheet(0, SheetOptions.Default, plan) —
and is bound before the header is read.
The shape
The native core (libhypertabular) owns no memory and reads no files. DelimitedReader
and Sheet allocate the buffers — the input, one value array and one verdict array per
column, the table that locates each cell — once, pinned, and reuse them for every batch. The
core fills them in one native call per batch: the boundary is crossed once per few thousand
rows, not once per cell.
- Header first. A
DelimitedReaderopened without a plan — from aStream,ReadOnlyMemory<byte>or a path — and a sheet started without one (book.Sheet(index or name, options)) read the header straight away.Headeris the list of names it always was, plusOrdinal(name)(exact, case-sensitive, untrimmed, first match;KeyNotFoundExceptionnaming the column when it is missing),TryOrdinal, andOrdinal(ReadOnlySpan<byte>)to match the raw UTF-8.Bind(plan[, batchRows])then declares the plan, once (InvalidOperationExceptionon a second call, and onRead()before it — not final: bind and read).IsBoundsays which;ColumnCountis the header's width, or the first record's once read. - One batch type.
Read()returns aBatch—Rows,Columns,Line(row),Verdicts(column),Values<T>(column)for a whole primitive column,Get<T>(column, row)for any one cell,TryGetText/GetStringfor text, andRawfor the text a cell was cast from — ornullwhen there are no more rows. It is a view of the reader's buffers, valid until the nextRead(), and the same type for delimited text and for a sheet. - Rows, when that is the shape.
foreach (var row in batch)visits eachRow— aref structview of (batch, index) withIndex,Line,Get<T>(column),Verdict(column),TryGetText,GetString,GetChars,TryGetCharsandRaw— allocating nothing;batch.Row(i)picks one.reader.Rows()andsheet.Rows()go on across batches, reading as each runs out; a row is valid for its own iteration only. - Nothing is sniffed. The
Dialectstates the separator, the quoting and the header;SheetOptionsstates a sheet's header and whether empty rows are skipped; the plan states each column's door and, for numbers, itsNumFormat. - HyperCast is the judge.
Verdict<T>,Fault,NumFormat,UnixPrecision,DateOrderandExcelEpochare HyperCast's own types, from its package. A text cell means exactly whatCastwould say of the same text; a typed workbook cell is converted by the door directly (a stored2.5through the decimal door is2.5). - A bad value is a verdict; a broken file is an exception. A cell that does not cast
is a
Faultin its column and the read goes on. A record of the wrong width, input that ends inside a quoted cell, a workbook whose container or parts cannot be read, is aTabularException, raised after every intact row before it. - Text is zero-copy.
TryGetTexthands back a span into the reader's own buffer — the input, or a workbook's shared strings; only a cell with""inside, or a typed workbook cell said as text, is written to an arena. - UTF-16 without strings.
GetChars(column, row)decodes a text cell into aReadOnlySpan<char>in an arena the batch keeps — made on first use, only ever appended to while the batch is current, started over at the nextRead()— for the BCL'sReadOnlySpan<char>parsers (Enum.Parse<T>,ISpanParsable<T>); an empty cell is an empty span.TryGetChars(column, row, destination, out written)decodes into the caller's buffer instead. Invalid UTF-8 is replaced exactly asGetStringreplaces it. - Workbooks from streams.
new Workbook(stream, leaveOpen)/Workbook.Open(stream)read an embedded resource or a response to its end into pinned memory the workbook owns — exactly the remaining length of a seekable stream, more thanint.MaxValuerefused asTabularFailure.TooLargebefore anything is allocated — and dispose the stream as soon as it has been read, unless asked to leave it open. - Async, with cancellation.
DelimitedReader.OpenAsync(stream, dialect, …)awaits the header andReadAsync(cancellationToken)awaits each refill (Stream.ReadAsync); the native fill between them is synchronous, and a reader of memory completes synchronously. The token is observed before anything is done and at every refill; a cancelled read throwsOperationCanceledExceptionand loses nothing — what had been read stays buffered, and the next read goes on from there.Workbook.OpenAsync(stream, leaveOpen, token)awaits a container; its sheets then read synchronously, from memory.DelimitedReaderisIAsyncDisposable, disposing its stream asynchronously unless it was left open. This is what a browser's streams (anHttpClientresponse, a picked file), which cannot be read synchronously, need. - Native AOT. Source-generated
LibraryImportonly. An AOT publish links the core in from the package's static archive: one executable, nothing loaded at start. - iOS and Mac Catalyst. A
net11.0-iosornet11.0-maccatalystapp (MAUI included) needs nothing but the package reference: itsbuildTransitivetargets link the core in from static archives forios-arm64,iossimulator-arm64,maccatalyst-arm64andmaccatalyst-x64, since neither platform loads a library. - Android. A
net11.0-androidapp (MAUI included) needs nothing but the package reference either: the package carries the core forandroid-arm64andandroid-x64(API 21+), loaded out of the APK on CoreCLR and linked in under Native AOT. See Android.
Tabular.IsAvailable and Tabular.NativeVersion answer whether the native library
resolved, without the first read being what finds out.
Android
A .NET for Android or MAUI app (net11.0-android, API 24 and later, .NET 11's floor)
references the package and writes nothing else. On CoreCLR, .NET 11's Android runtime (Mono
is no longer supported there), the SDK takes runtimes/android-arm64/native/libhypertabular.so
and its x64 twin out of the package and stores each in the APK under lib/arm64-v8a/ and
lib/x86_64/, and the ordinary "hypertabular" import opens it, exactly as on Linux. The
libraries are cross-built with the NDK for API level 21, below any app that can reference
them, and their segments are aligned to 16 KB: Android 15 devices may use 16 KB pages, a
library aligned for 4 KB does not load on one, and Google Play requires the alignment of
every new app. A Native AOT publish (PublishAot, -r android-arm64) links
staticlibs/android-{rid}/libhypertabular.a into the app's own native library instead, as it
does on every other RID, and the shared one is left out of the APK. HyperCast's package does
the same for its own core, and the two sit side by side. android-arm64 covers effectively
every Android device in use and android-x64 the emulator; these are the two RIDs .NET for
Android builds by default. The 32-bit android-arm and android-x86 are not in the package,
so an app that adds them gets Tabular.IsAvailable == false on those ABIs.
HyperTabular.AndroidSmokeTest is the Native AOT smoke test's SmokeTest.Run() again,
started from an Activity, and unlike the other smoke tests it takes HyperTabular as a
package, from a local folder CI packs it into, because the package's layout is what Android
needs proven. CI's test-android job builds it four ways from that run's libraries: CoreCLR
and Native AOT, for each RID. The x64 pair runs in an x86_64 emulator whose image uses 16 KB
pages, so a library aligned for 4 KB would fail to load there. Nothing hosted runs arm64
Android, so the arm64 pair is inspected instead: each CoreCLR APK must carry
libhypertabular.so for its ABI, each Native AOT APK must not carry it at all, and all four
must pass zipalign -P 16.
Two .NET 11 RC1 workload behaviors shape that job, and an app on RC1 may meet them too. The
android workload's build tasks require JDK 21 (XA0030 on newer). And the workload RC1
resolves was built against a runtime newer than RC1 on nuget.org, so a Native AOT publish
fails to restore Microsoft.NETCore.App.Runtime.NativeAOT.android-* by exact version until
the .NET 11 daily feed (https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet11/nuget/v3/index.json)
is added as a source; the job adds it.
WebAssembly (Blazor)
One compiled assembly covers browser-wasm too: every native entry point is declared three
times — "hypertabular" for platforms that load a library, "*" for the statically linked
wasm module and "__Internal" for iOS and Mac Catalyst — picked at the call site by
OperatingSystem.IsBrowser() and OperatingSystem.IsIOS(). CI builds the
wasm32-unknown-emscripten archive on every pull request, the release pack stages it under
runtimes/browser-wasm/nativeassets/, and the package's build/net11.0/HyperTabular.targets
wires it into a Blazor WebAssembly project with no configuration: a NativeFileReference
hands the archive to the linker beside HyperCast's (the two share no symbols), an
EmccExportedFunction per export makes each resolvable through "*", and the
exception-handling translation .NET 11 needs is applied.
No export takes more than twelve integer arguments, because Mono's interpreter, which runs
.NET in the browser, passes no more to a native function; the work buffers travel in one
struct instead. HyperTabular.WasmSmokeTest proves the chain in headless Chrome on every
pull request: a Blazor app that reads delimited text and a workbook through the public API
and renders PASS or FAIL into the page. One of the files it reads is one it serves,
fetched with HttpClient as a streamed response — a stream the browser only reads
asynchronously — and read through OpenAsync and ReadAsync.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net11.0 is compatible. |
-
net11.0
- HyperCast (>= 0.8.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.