hypertabular

package module
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 11 Imported by: 0

README

HyperTabular for Go

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.

go get github.com/SkunkWerkx/HyperTabular/go@latest

The module lives in a subdirectory of the repository, so its tags are prefixed (go/vX.Y.Z).

import (
	// Both import paths end in /go, so the package names have to be spelled out.
	hypercast "github.com/SkunkWerkx/HyperCast/go"
	hypertabular "github.com/SkunkWerkx/HyperTabular/go"
)

// The dialect is declared; nothing is sniffed. Open header first, look the columns up by
// name, and bind the plan built from them.
reader, err := hypertabular.OpenDelimitedUnbound("orders.csv", hypertabular.CSV)
if err != nil {
	log.Fatal(err)
}
defer reader.Close()

header := reader.Header() // a hypertabular.Header: a []string with lookups
id, err := header.Ordinal("id") // a missing name is a *NoColumnError that names it
if err != nil {
	log.Fatal(err)
}
customer, _ := header.Ordinal("customer")
total, _ := header.Ordinal("total")
plan := []hypertabular.Column{
	hypertabular.I32(id, hypercast.Invariant),
	hypertabular.Text(customer),
	hypertabular.Exact(total, hypercast.Invariant),
}
if err := reader.Bind(plan); err != nil {
	log.Fatal(err)
}

for {
	batch, err := reader.Read()
	if err == io.EOF {
		break
	}
	if err != nil {
		log.Fatal(err) // a *hypertabular.Failure: the input is not rows of cells
	}
	// A column at a time: typed slices, one element per row, and a verdict beside each…
	ids, customers := batch.I32(0), batch.Text(1)
	for row := 0; row < batch.Rows(); row++ {
		// …or a cell at a time, as the HyperCast door of the same name returns it.
		total, fault := hypertabular.Get[hypercast.Decimal](batch, 2, row)
		if fault != nil {
			fmt.Printf("%d %s: %q is %v\n", ids[row], customers[row], batch.Raw(2, row), fault.Reason)
			continue
		}
		fmt.Println(ids[row], string(customers[row]), total)
	}
}

A plan known up front goes straight to the constructor — OpenDelimited(path, dialect, plan), and likewise NewDelimitedReader and NewDelimitedReaderBytes — and Bind is not called.

Or read row by row, across batches, with Go's range-over-func iterators:

for row, err := range reader.All() {
	if err != nil {
		log.Fatal(err) // how the input ended, if not at its end
	}
	id, fault := hypertabular.Cell[int32](row, 0)
	name := row.TextString(1) // a view of the batch's bytes, not a copy
	…
}

A workbook reads into the same batch, a sheet at a time:

book, err := hypertabular.OpenWorkbook("orders.xlsx") // or NewWorkbook(bytes), NewWorkbookReader(r)
if err != nil {
	log.Fatal(err)
}
sheet, err := book.SheetNamedUnbound("Orders", hypertabular.DefaultSheetOptions)
if err != nil {
	log.Fatal(err)
}
id, err = sheet.Header().Ordinal("id")
…
err = sheet.Bind(plan)

example_test.go holds these as runnable examples, with their output checked by go test.

The shape

The native core owns no memory and reads no files. The reader allocates the buffers — one value slice and one verdict slice per column, the table that locates each cell — once, and the core fills them in one cgo call per batch: the boundary is crossed once per few thousand rows, not once per cell.

  • One batch type. Read() returns a *Batch — Rows(), Columns(), Line(row), Verdicts(column), a typed slice per door (I32, F64, Exact, Timestamp, DateOnly, Text, …), Fault(column, row), Raw(column, row) and the generic Get[T](batch, column, row) (a free function, since a Go method cannot take type parameters) — or io.EOF once there are no more rows. It is a view of the reader's buffers, valid until the next Read(), and the same type for delimited text and for a sheet.
  • Header first, or plan first. OpenDelimitedUnbound, NewDelimitedReaderUnbound, NewDelimitedReaderBytesUnbound, Workbook.SheetUnbound and Workbook.SheetNamedUnbound read the header and leave the plan to Bind(plan), once, before the first Read() — which returns ErrUnbound until then, and a second Bind ErrAlreadyBound. Header() is a Header, a []string underneath, whose Ordinal(name) / OrdinalBytes find the first column of that exact name (case and spaces included) or return a *NoColumnError naming it (errors.Is(err, ErrNoColumn)), and whose Find / FindBytes answer with an ok instead. ColumnCount() is a record's width: the header's, or the first record's once read.
  • Rows, when that is the shape. batch.All() is an iter.Seq[Row] over the batch, and reader.All() / sheet.All() an iter.Seq2[Row, error] that reads batch after batch. A Row — Index(), Line(), Verdict(column), Fault(column), Text(column), TextString(column), Raw(column), and Cell[T](row, column) — is a (batch, index) pair and allocates nothing; like the batch, it is valid until the next read.
  • Text as a string, without the copy. batch.TextString(column, row) and row.TextString(column) are the cell's bytes as a string made with unsafe.String: no allocation, ready for strconv, a map lookup or a switch. The bytes are the reader's and are reused by the next Read(), so a string kept past it changes under you — clone what you keep (strings.Clone, or Get[string], which copies).
  • Cancellation is the source's. A read blocks only in the io.Reader's own Read; a source tied to a context.Context — an http.Request made with one, a connection whose deadline is set — ends with the context's error, and Read() returns it wrapped. Everything after the bytes arrive is in memory and short. A loop over All() stops at a break.
  • HyperCast is the judge. hypercast.Fault, NumFormat, Decimal, Date, CivilDateTime, Duration, UnixPrecision, DateOrder and ExcelEpoch are HyperCast's own types. A text cell means exactly what HyperCast's door would say of the same text; a typed workbook cell is converted by the door directly.
  • A bad value is a verdict; a broken file is an error. A cell that does not cast is a fault in 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 a *hypertabular.Failure, returned after every intact row before it.

Linking

The core is linked in through cgo from the module's own staticlib/{GOOS}_{GOARCH} archives — Linux, macOS, Windows and Android on x86-64 and arm64, and iOS and Mac Catalyst — so a binary carries the core and loads nothing. Building takes a C compiler; CGO_ENABLED=0 or any other target is a compile error rather than a binary that fails at run time. HyperCast's Go module links its own core the same way, and the two archives link side by side.

iOS and Mac Catalyst

Go builds for all three as GOOS=ios, with cgo and the platform's own clang. A Mach-O object says which platform it was built for and the linker refuses a mismatch, so each has its own archive, and since the three are one GOOS/GOARCH pair to Go, build tags choose between them:

Building for Tags Archive
An iOS device none staticlib/ios_arm64
The iOS simulator, Apple silicon iossimulator staticlib/iossimulator_arm64
Mac Catalyst maccatalyst staticlib/maccatalyst_arm64, staticlib/maccatalyst_amd64

gomobile sets maccatalyst itself; nothing sets iossimulator, so a simulator build passes -tags iossimulator by hand. The simulator on an Intel Mac has no archive and is a compile error. CI checks the selection for every platform (.github/scripts/check_go_archives.sh), runs this suite in an iOS simulator through Go's misc/ios/go_ios_exec.go, and links a device build.

Android

GOOS=android with cgo, the NDK's clang as CC, as gomobile arranges and as any cgo package on Android needs:

CC=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang \
  GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build ./...

(x86_64-linux-android21-clang and GOARCH=amd64 for the emulator.) GOOS=android also satisfies Go's linux constraint, so the Linux link lines exclude it and it takes its own archives, staticlib/android_arm64 and staticlib/android_amd64, built for Android against Bionic (API 21+). HyperCast's Go module does the same for its own core, and the two link side by side. Page alignment is the final link's: NDK r28 and later align to the 16 KB pages Android 15 devices may use by default, and an older NDK needs -extldflags=-Wl,-z,max-page-size=16384.

CI checks that both select their own archive (.github/scripts/check_go_archives.sh), cross-compiles this whole suite for android/amd64, and runs it in an x86_64 emulator whose image uses 16 KB pages (.github/scripts/android_build_suite.sh and android_device_test.sh, which run the same way against a local emulator); android/arm64 is linked. ExampleWorkbook is skipped there, since it opens ../corpus by its path and nothing is above the directory the suite is pushed to.

In the browser (TinyGo)

TinyGo 0.42 or later compiles this module to WebAssembly with the core linked in: TinyGo links with wasm-ld and has cgo, so backend_tinygo.go names staticlib/wasm/libhypertabular.a — the core built for wasm32-wasip1 — on its link line. HyperCast's module does the same with its own archive, and the two link side by side.

tinygo build -target=wasm -no-debug -opt=z -o main.wasm .
cp "$(tinygo env TINYGOROOT)/targets/wasm_exec.js" .

Use TinyGo's own wasm_exec.js, not stock Go's. The core imports nothing, so the page loads one module. -target=wasip1 works the same way under a WASI runtime. CI builds internal/tinygosmoke this way on every pull request and runs it in headless Chrome: every export, delimited text and a workbook written in memory, a fault with its span, and the core's version against rust/Cargo.toml. Stock Go compiled to WebAssembly (GOOS=wasip1, GOOS=js) links Go code only, so it is a compile error.

Verifying build provenance

Go has no package registry to attest; go get resolves straight from the go/vX.Y.Z tag. Each committed archive was attested when the forge built it and verified again before it was committed, and can be checked against the build that made it:

gh attestation verify staticlib/linux_amd64/libhypertabular.a --owner SkunkWerkx

License

MIT

Documentation

Overview

Package hypertabular reads delimited text — CSV, TSV, any single-byte ASCII separator — and workbooks — XLSX and ODS — a batch at a time into typed columns, with a HyperCast verdict for every cell.

The native core (libhypertabular) owns no memory and reads no files. It is handed a chunk of input and the buffers to fill, casts each plan column in one native loop, and says how many rows it wrote and how far it got. Everything else is here: a DelimitedReader or a Sheet allocates the column buffers, the table that locates each cell and the arena for the rare text that is not in the input — once, as ordinary Go memory, reused for every batch. The cgo boundary is crossed once per batch, not once per cell.

  • One batch type. Read returns a *Batch — Rows, Columns, Line, Verdicts, a whole column at a time (I32, F64, Text and the rest), and Get for any one cell — for delimited text and for a sheet alike.
  • Nothing is sniffed. The Dialect states the separator, the quoting and the header; SheetOptions states a sheet's header and whether empty rows are skipped; the plan states each column's door and, for numbers, its hypercast.NumFormat.
  • HyperCast is the judge. Fault, CastFailure, NumFormat, Decimal, Date, CivilDateTime, Duration, UnixPrecision, DateOrder and ExcelEpoch are HyperCast's own types, from its own module. A text cell means exactly what the HyperCast door of the same name would say of the same text; a typed workbook cell is converted by the door directly.
  • A bad value is a verdict; a broken file is an error. A cell that does not cast is a fault in 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 a *Failure, returned after every intact row before it.
  • Text is not copied. A Text column's cells are slices of the input itself, or of a workbook's shared strings; only a cell with "" inside, or a typed workbook cell said as text, is written to an arena.

The core is a static library linked in through cgo (backend_static.go), as HyperCast's is: nothing is embedded, extracted or loaded at run time. It builds on Linux, macOS and Windows, on amd64 and arm64, and on iOS and Mac Catalyst, with a C compiler present; and under TinyGo for WebAssembly, browser and WASI, through backend_tinygo.go. Any other build, a build without cgo among them, is a compile error that says so (unsupported.go, unsupported_tinygo.go).

Index

Examples

Constants

View Source
const (
	// MaxRowBytes is the row ceiling: a single record larger than this is a RowTooLong
	// failure.
	MaxRowBytes = 1 << 30

	// DefaultBufferBytes is the initial read buffer for an io.Reader.
	DefaultBufferBytes = 256 * 1024

	// DefaultBatchRows is the rows per batch unless told otherwise.
	DefaultBatchRows = 4096
)

Variables

View Source
var (
	// CSV is comma-separated, quoted, with a header, blank lines skipped.
	CSV = Dialect{Separator: ',', Quoting: true, HasHeader: true, SkipBlankLines: true}
	// TSV is tab-separated, otherwise as CSV.
	TSV = Dialect{Separator: '\t', Quoting: true, HasHeader: true, SkipBlankLines: true}
	// PSV is pipe-separated, otherwise as CSV.
	PSV = Dialect{Separator: '|', Quoting: true, HasHeader: true, SkipBlankLines: true}
)

The declared dialects most files are written in.

View Source
var DefaultSheetOptions = SheetOptions{HasHeader: true, SkipEmptyRows: true, BatchRows: DefaultBatchRows}

DefaultSheetOptions reads a header, skips empty rows, and reads DefaultBatchRows rows a batch.

View Source
var ErrAlreadyBound = errors.New("hypertabular: a plan is already bound")

ErrAlreadyBound is the error Bind returns from a DelimitedReader or a Sheet that has a plan already — one it was opened with, or one bound before.

View Source
var ErrNoColumn = errors.New("hypertabular: no such column")

ErrNoColumn is the error Header.Ordinal returns, inside a *NoColumnError, for a name the header does not have; reach it with errors.Is.

View Source
var ErrNoSheet = errors.New("hypertabular: no such sheet")

ErrNoSheet is the error Workbook.Sheet and Workbook.SheetNamed wrap for a sheet the workbook does not have; reach it with errors.Is.

View Source
var ErrUnbound = errors.New("hypertabular: no plan is bound; call Bind before Read")

ErrUnbound is the error Read returns from a DelimitedReader or a Sheet opened without a plan, until Bind has given it one. It is not the reader's last word: once a plan is bound, Read reads.

Functions

func Cell added in v0.8.0

func Cell[T Value](row Row, column int) (T, *hypercast.Fault)

Cell is the row's cell in column as HyperCast judged it, as Get says of the same cell: Get(row.Batch(), column, row.Index()).

id, fault := hypertabular.Cell[int32](row, 0)

Cell is a function rather than a method because Go methods cannot take type parameters.

func Get

func Get[T Value](b *Batch, column, row int) (T, *hypercast.Fault)

Get is the cell at (column, row) as HyperCast judged it: its value and a nil fault, or T's zero value and the fault — the same pair the HyperCast door of the same name returns for the same text. T is the Go type of the column's door (see Value); asking for another panics, as the column accessors do. A string is the one T that allocates, and a fault.

Get is a function rather than a method because Go methods cannot take type parameters.

func NativeVersion

func NativeVersion() string

NativeVersion reports the linked core's own version as "major.minor.patch" — read from the core itself, not from this module — so a deployment can confirm which build of the archive went into the binary. The core is linked in, so there is nothing to load and the probe cannot fail.

Example
package main

import (
	"fmt"

	hypertabular "github.com/SkunkWerkx/HyperTabular/go"
)

func main() {
	// The linked core's own version, read from the core.
	fmt.Println(hypertabular.NativeVersion() != "")
}
Output:
true

Types

type Batch

type Batch struct {
	// contains filtered or unexported fields
}

Batch is the rows one Read delivered, as typed columns: the same type for delimited text and for a sheet of a workbook.

A column is handed out whole — I32, F64, Text and the rest, one element per row, with Verdicts beside them — or a cell at a time through Get, or a row at a time through Row and All. Everything it hands out points into its reader's own buffers (and, for text in memory, into that text itself) and is valid until the reader's next Read: views, not copies, so do not write through them. The reader hands out the same *Batch every time, with the next rows in it.

func (*Batch) All added in v0.8.0

func (b *Batch) All() iter.Seq[Row]

All is the batch's rows, in order:

for row := range batch.All() { … }

func (*Batch) Bool

func (b *Batch) Bool(column int) []bool

Bool is a Bool column's values, one per row. Here and for every column accessor below, the value of a row whose verdict is not OK is the type's zero value, and asking a column for a type its door does not write panics.

func (*Batch) Columns

func (b *Batch) Columns() []Column

Columns is the plan the batch was read through: column i of the batch is Columns()[i]. Do not modify it.

func (*Batch) DateOnly

func (b *Batch) DateOnly(column int) []hypercast.Date

DateOnly is the values of a DateOnly or DateOnlyOrdered column, converted as Timestamp's are.

func (*Batch) DateTime

func (b *Batch) DateTime(column int) []hypercast.CivilDateTime

DateTime is a DateTime column's values: wall clocks with no zone — the text named none and none is invented. Converted as Timestamp's are.

func (*Batch) Exact

func (b *Batch) Exact(column int) []hypercast.Decimal

Exact is an Exact column's values: HyperCast's exact Decimal, which is the layout the core writes.

func (*Batch) F32

func (b *Batch) F32(column int) []float32

F32 is an F32 column's values.

func (*Batch) F64

func (b *Batch) F64(column int) []float64

F64 is an F64 column's values.

func (*Batch) Fault

func (b *Batch) Fault(column, row int) *hypercast.Fault

Fault is the verdict of the cell at (column, row) as HyperCast's own union: nil for a cell that cast, otherwise the fault — the one allocation this makes.

func (*Batch) I8

func (b *Batch) I8(column int) []int8

I8 is an I8 column's values.

func (*Batch) I16

func (b *Batch) I16(column int) []int16

I16 is an I16 column's values.

func (*Batch) I32

func (b *Batch) I32(column int) []int32

I32 is an I32 column's values.

func (*Batch) I64

func (b *Batch) I64(column int) []int64

I64 is an I64 column's values.

func (*Batch) Line

func (b *Batch) Line(row int) int

Line is where row came from: for delimited text the 1-based line its record starts on, for a sheet its 1-based row number.

func (*Batch) Raw

func (b *Batch) Raw(column, row int) []byte

Raw is the text the cell at (column, row) was cast from, whatever its door and whatever its verdict — what a fault's span indexes, and what to show for a value that did not cast. For a workbook this is a text cell's own text, and what a typed cell was said as when it failed its door (or went through the text door); a typed cell that cast has none, and neither has an empty cell.

A slice of the input itself, except for a quoted delimited cell with an escaped quote in it, which is unescaped — as the core cast it — into a scratch buffer that the next call to Raw reuses. Valid until the next Raw or Read.

func (*Batch) Row added in v0.8.0

func (b *Batch) Row(index int) Row

Row is the row at index of the batch in hand. An index outside the batch panics.

func (*Batch) Rows

func (b *Batch) Rows() int

Rows is how many rows the batch holds: never zero for a batch Read returned, and zero once the reader has moved on.

func (*Batch) Span

func (b *Batch) Span(column int) []hypercast.Duration

Span is a Span column's values: HyperCast's seconds-and-nanos Duration, which is the layout the core writes.

func (*Batch) Text

func (b *Batch) Text(column int) [][]byte

Text is a Text column's cells: each cell's bytes, untrimmed — quotes resolved for delimited text, and a typed workbook cell said the canonical way (42, true, 2024-01-31T10:30:00, PT1H30M) — and nil for a cell with no bytes at all, which is the one way text fails. Nothing is copied: a cell is a slice of the input itself (the reader's buffer, or the caller's own memory under NewDelimitedReaderBytes) or of a workbook's shared strings, and only a cell that had an escaped quote in it, or a typed workbook cell, is a slice of the reader's arena instead.

func (*Batch) TextString added in v0.8.0

func (b *Batch) TextString(column, row int) string

TextString is the Text cell at (column, row) as a string — without a copy. The string is a view of the same bytes Text hands out, made with unsafe.String: nothing is allocated, which is what lets a cell go straight to strconv, a map lookup or a switch with no garbage per cell. The empty string is a cell with no bytes, which is the one way text fails.

The bytes behind the string belong to the reader, not to the string: they are reused by the next Read, and a string kept past it says whatever the reader has put there since — which a Go string never otherwise does. Use it within the batch, and copy what is kept (strings.Clone, or Get[string], which allocates its own). Under NewDelimitedReaderBytes the bytes are the caller's own, and must not change while the string is in use.

Asking a column whose door is not DoorText panics, as Text does.

func (*Batch) TimeOfDay

func (b *Batch) TimeOfDay(column int) []time.Duration

TimeOfDay is a TimeOfDay column's values: nanoseconds since midnight.

func (*Batch) Timestamp

func (b *Batch) Timestamp(column int) []time.Time

Timestamp is the values of a Timestamp, Unix or ExcelSerial column: UTC instants at full nanosecond fidelity, as HyperCast's own doors return them. They are converted from what the core wrote the first time a batch's column is asked for, into a slice the reader keeps.

func (*Batch) U8

func (b *Batch) U8(column int) []uint8

U8 is a U8 column's values.

func (*Batch) U16

func (b *Batch) U16(column int) []uint16

U16 is a U16 column's values.

func (*Batch) U32

func (b *Batch) U32(column int) []uint32

U32 is a U32 column's values.

func (*Batch) U64

func (b *Batch) U64(column int) []uint64

U64 is a U64 column's values.

func (*Batch) Uuid

func (b *Batch) Uuid(column int) []uuid.UUID

Uuid is a Uuid column's values.

func (*Batch) Verdicts

func (b *Batch) Verdicts(column int) []CellVerdict

Verdicts is a column's verdicts, one per row, as the core wrote them.

type CellVerdict

type CellVerdict struct {
	// contains filtered or unexported fields
}

CellVerdict is one cell's verdict as the core writes it into a column's verdict array: HyperCast's reason code, with zero for a cell that cast, and the offending span within the cell's own text. DelimitedReader.Verdicts hands a column's verdicts out as a slice of these, for code that wants to scan a batch without a *hypercast.Fault per cell.

func (CellVerdict) Fault

func (v CellVerdict) Fault() *hypercast.Fault

Fault is the verdict as HyperCast's own union: nil for a cell that cast, otherwise the *hypercast.Fault the HyperCast door of the same name returns for the same text. The fault is the one allocation this makes.

func (CellVerdict) Length

func (v CellVerdict) Length() int

Length is the byte length of the offending span.

func (CellVerdict) OK

func (v CellVerdict) OK() bool

OK reports whether the cell cast.

func (CellVerdict) Offset

func (v CellVerdict) Offset() int

Offset is the byte offset of the offending span within the cell's text.

func (CellVerdict) Reason

func (v CellVerdict) Reason() hypercast.CastFailure

Reason is why the cell did not cast — zero, which is no CastFailure, when it did.

type Column

type Column struct {
	// contains filtered or unexported fields
}

Column is one output column of a plan: which source column it reads, the door it casts through, and — for the numeric doors — the notation. A plan is a projection: a forty-column file can be read into five typed columns, in any order, and a source column can be read through more than one door.

Build one with the constructor named after its door; the zero Column is not a column, and a reader refuses a plan that holds one.

func Bool

func Bool(ordinal int) Column

Bool is a bool column: HyperCast's boolean lexicon.

func DateOnly

func DateOnly(ordinal int) Column

DateOnly is a strict yyyy-MM-dd date column.

func DateOnlyOrdered

func DateOnlyOrdered(ordinal int, order hypercast.DateOrder) Column

DateOnlyOrdered is a separated-date column under the declared field order.

func DateTime

func DateTime(ordinal int, order hypercast.DateOrder) Column

DateTime is a zone-less civil date-time column under the declared field order.

func Exact

func Exact(ordinal int, format hypercast.NumFormat) Column

Exact is an exact hypercast.Decimal column under the declared notation.

func ExcelSerial

func ExcelSerial(ordinal int, epoch hypercast.ExcelEpoch) Column

ExcelSerial is an Excel date-serial column under the declared date system.

func F32

func F32(ordinal int, format hypercast.NumFormat) Column

F32 is a float32 column under the declared notation.

func F64

func F64(ordinal int, format hypercast.NumFormat) Column

F64 is a float64 column under the declared notation.

func I8

func I8(ordinal int, format hypercast.NumFormat) Column

I8 is an int8 column under the declared notation.

func I16

func I16(ordinal int, format hypercast.NumFormat) Column

I16 is an int16 column under the declared notation.

func I32

func I32(ordinal int, format hypercast.NumFormat) Column

I32 is an int32 column under the declared notation.

func I64

func I64(ordinal int, format hypercast.NumFormat) Column

I64 is an int64 column under the declared notation.

func Span

func Span(ordinal int) Column

Span is a duration column.

func Text

func Text(ordinal int) Column

Text is a text column: the cell's bytes themselves, untrimmed, quotes resolved.

func TimeOfDay

func TimeOfDay(ordinal int) Column

TimeOfDay is a 24-hour time-of-day column.

func Timestamp

func Timestamp(ordinal int) Column

Timestamp is an RFC 3339 instant column.

func U8

func U8(ordinal int, format hypercast.NumFormat) Column

U8 is a uint8 column under the declared notation.

func U16

func U16(ordinal int, format hypercast.NumFormat) Column

U16 is a uint16 column under the declared notation.

func U32

func U32(ordinal int, format hypercast.NumFormat) Column

U32 is a uint32 column under the declared notation.

func U64

func U64(ordinal int, format hypercast.NumFormat) Column

U64 is a uint64 column under the declared notation.

func Unix

func Unix(ordinal int, precision hypercast.UnixPrecision) Column

Unix is a Unix-epoch column at the declared precision — never guessed from magnitude.

func Uuid

func Uuid(ordinal int) Column

Uuid is a uuid.UUID column.

func (Column) Door

func (c Column) Door() Door

Door is the door the column is cast through.

func (Column) Format

func (c Column) Format() hypercast.NumFormat

Format is the numeric notation a numeric door reads; the zero NumFormat for the others.

func (Column) Ordinal

func (c Column) Ordinal() int

Ordinal is the zero-based ordinal of the source column. A column past a record's last cell reads as empty.

type DelimitedReader

type DelimitedReader struct {
	// contains filtered or unexported fields
}

DelimitedReader reads delimited text — CSV, TSV, any single-byte ASCII separator — a batch at a time into typed columns, every cell a HyperCast verdict.

Build one over an io.Reader (NewDelimitedReader), over text already in memory (NewDelimitedReaderBytes) or over a file (OpenDelimited), with the Dialect and the plan of columns declared — or header first, with the Unbound variant of each: the header is read, its names looked up (Header().Ordinal), and the plan built from them handed to Bind. Read returns the next *Batch, whose column accessors — I32, F64, Text and the rest, with Verdicts beside them — hand it out as slices, one element per row, and Get a cell at a time.

A value that does not cast is that cell's verdict, and the read goes on. Input that is not rows of cells at all — a record of the wrong width, a quote never closed — is a *Failure, returned by Read after every intact row before it has been delivered.

A DelimitedReader is not safe for concurrent use.

Example
package main

import (
	"fmt"
	"io"
	"log"
	"strings"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
	hypertabular "github.com/SkunkWerkx/HyperTabular/go"
)

func main() {
	const orders = "id,customer,total,placed\n" +
		"1,alice,\"1,234.50\",2026-01-07\n" +
		"2,\"bob \"\"the builder\"\"\",12x4,2026-02-30\n"

	// The dialect and the plan are declared; nothing is sniffed.
	plan := []hypertabular.Column{
		hypertabular.I32(0, hypercast.Invariant),
		hypertabular.Text(1),
		hypertabular.Exact(2, hypercast.Invariant),
		hypertabular.DateOnly(3),
	}
	reader, err := hypertabular.NewDelimitedReader(strings.NewReader(orders), hypertabular.CSV, plan)
	if err != nil {
		log.Fatal(err)
	}
	defer reader.Close()
	fmt.Println(reader.Header())

	for {
		batch, err := reader.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatal(err)
		}
		// A column at a time: typed slices, one element per row, and a verdict beside each…
		ids, customers, placed := batch.I32(0), batch.Text(1), batch.DateOnly(3)
		for row := 0; row < batch.Rows(); row++ {
			fmt.Printf("%d %s:", ids[row], customers[row])
			// …or a cell at a time, as the HyperCast door of the same name returns it.
			if total, fault := hypertabular.Get[hypercast.Decimal](batch, 2, row); fault != nil {
				// A value that did not cast is a verdict, and its text is still to hand.
				fmt.Printf(" total %q is %v at byte %d;", batch.Raw(2, row), fault.Reason, fault.Offset)
			} else {
				fmt.Printf(" total %v;", total)
			}
			if verdict := batch.Verdicts(3)[row]; !verdict.OK() {
				fmt.Printf(" placed %q is %v\n", batch.Raw(3, row), verdict.Reason())
			} else {
				fmt.Printf(" placed %d-%02d-%02d\n", placed[row].Year, placed[row].Month, placed[row].Day)
			}
		}
	}
}
Output:
[id customer total placed]
1 alice: total 1234.5; placed 2026-01-07
2 bob "the builder": total "12x4" is malformed at byte 2; placed "2026-02-30" is out of range

func NewDelimitedReader

func NewDelimitedReader(source io.Reader, dialect Dialect, plan []Column, opts ...Option) (*DelimitedReader, error)

NewDelimitedReader reads UTF-8 delimited text from an io.Reader, forward only, through a buffer of its own. When the dialect declares a header it is read here, so a header that is structurally broken — or a source that fails before the header is complete — is this function's error. The reader does not close source.

func NewDelimitedReaderBytes

func NewDelimitedReaderBytes(utf8 []byte, dialect Dialect, plan []Column, opts ...Option) (*DelimitedReader, error)

NewDelimitedReaderBytes reads UTF-8 delimited text already in memory, in place — nothing is copied, so the text must not change while the reader is in use. When the dialect declares a header it is read here, so a header that is structurally broken is this function's error.

func NewDelimitedReaderBytesUnbound added in v0.8.0

func NewDelimitedReaderBytesUnbound(utf8 []byte, dialect Dialect, opts ...Option) (*DelimitedReader, error)

NewDelimitedReaderBytesUnbound is NewDelimitedReaderBytes without a plan: the header is read, and the plan bound later with Bind.

func NewDelimitedReaderUnbound added in v0.8.0

func NewDelimitedReaderUnbound(source io.Reader, dialect Dialect, opts ...Option) (*DelimitedReader, error)

NewDelimitedReaderUnbound is NewDelimitedReader without a plan: the header is read — when the dialect declares one — and the plan is bound later, with Bind, once the header has said where each column is. Read returns ErrUnbound until then.

func OpenDelimited

func OpenDelimited(path string, dialect Dialect, plan []Column, opts ...Option) (*DelimitedReader, error)

OpenDelimited opens a file of UTF-8 delimited text. The reader owns the file: Close closes it.

func OpenDelimitedUnbound added in v0.8.0

func OpenDelimitedUnbound(path string, dialect Dialect, opts ...Option) (*DelimitedReader, error)

OpenDelimitedUnbound is OpenDelimited without a plan: the header is read, and the plan bound later with Bind.

func (*DelimitedReader) All added in v0.8.0

func (r *DelimitedReader) All() iter.Seq2[Row, error]

All reads the rest of the input, batch by batch, and yields every row of it in order — each with a nil error — and then, if the input ended on anything but io.EOF, a zero Row and that error:

for row, err := range reader.All() {
	if err != nil {
		return err
	}
	…
}

Each row is valid for its own iteration only: the batch it is in is read over when the next is. Breaking out of the loop stops the reading there; a later All goes on with the next batch, so what was left of the batch in hand is not yielded again.

func (*DelimitedReader) Bind added in v0.8.0

func (r *DelimitedReader) Bind(plan []Column) error

Bind declares the plan a reader opened without one reads through: column i of every batch is plan[i]. It is called once, before the first Read — typically with ordinals the header has given:

header := reader.Header()
id, err := header.Ordinal("id")
…
err = reader.Bind([]hypertabular.Column{hypertabular.I32(id, hypercast.Invariant)})

A reader with a plan already — one it was opened with, or one bound before — returns ErrAlreadyBound. A plan the reader cannot honour is this method's error, as it would have been the constructor's, and leaves the reader unbound. The rows a batch are the BatchRows option the reader was opened with.

Example
package main

import (
	"errors"
	"fmt"
	"log"
	"strings"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
	hypertabular "github.com/SkunkWerkx/HyperTabular/go"
)

func main() {
	// The publisher controls the column order; the names are the contract. Open without a
	// plan, look the names up in the header, and bind the plan built from them.
	const countries = "Country,M49 Code,ISO-alpha2 Code\n" +
		"Algeria,012,DZ\n" +
		"Argentina,032,AR\n"
	reader, err := hypertabular.NewDelimitedReaderUnbound(strings.NewReader(countries), hypertabular.CSV)
	if err != nil {
		log.Fatal(err)
	}
	defer reader.Close()

	header := reader.Header()
	m49, err := header.Ordinal("M49 Code")
	if err != nil {
		log.Fatal(err)
	}
	iso, err := header.Ordinal("ISO-alpha2 Code")
	if err != nil {
		log.Fatal(err)
	}
	if err := reader.Bind([]hypertabular.Column{
		hypertabular.U16(m49, hypercast.Invariant),
		hypertabular.Text(iso),
	}); err != nil {
		log.Fatal(err)
	}
	// A name the header lacks is an error that says which.
	_, err = header.Ordinal("Region Code")
	fmt.Println(err, errors.Is(err, hypertabular.ErrNoColumn))

	// Row by row, across batches: each row is valid for its own iteration.
	for row, err := range reader.All() {
		if err != nil {
			log.Fatal(err)
		}
		code, _ := hypertabular.Cell[uint16](row, 0)
		// TextString is a view, not a copy: kept past the row, it would need strings.Clone.
		fmt.Println(row.Line(), row.TextString(1), code)
	}
}
Output:
hypertabular: the header has no column named "Region Code" true
2 DZ 12
3 AR 32

func (*DelimitedReader) Close

func (r *DelimitedReader) Close() error

Close releases the reader: the batch in hand is over, and a file opened by OpenDelimited is closed. A reader built over an io.Reader or over memory holds nothing that needs releasing — every buffer is ordinary Go memory — and does not close its source.

func (*DelimitedReader) ColumnCount added in v0.8.0

func (r *DelimitedReader) ColumnCount() int

ColumnCount is how many cells a record has: the header's count once it has been read, otherwise the first record's once that has been — and zero while neither is known (a headerless input before its first Read, or an input with no record at all).

func (*DelimitedReader) Dialect

func (r *DelimitedReader) Dialect() Dialect

Dialect is the dialect the text is read in.

func (*DelimitedReader) Header

func (r *DelimitedReader) Header() Header

Header is the header's names, when the dialect declares one: nil when it does not, and empty but not nil for an input with no record. It is read when the reader is built, so it is there to build a plan from before Bind.

func (*DelimitedReader) IsBound added in v0.8.0

func (r *DelimitedReader) IsBound() bool

IsBound says whether the reader has a plan: always, for a reader opened with one.

func (*DelimitedReader) Plan

func (r *DelimitedReader) Plan() []Column

Plan is the plan the text is read through: column i of every batch is Plan()[i]; nil until one is bound. Do not modify it.

func (*DelimitedReader) Read

func (r *DelimitedReader) Read() (*Batch, error)

Read reads the next batch: up to BatchRows whole rows, as many as one call of the core found — fewer when the input, or what the read buffer holds of it, ran out first. At the end of the input it returns nil and io.EOF. Each batch is one crossing into the core, and the batch is valid until the next Read.

When the input is structurally broken it returns nil and a *Failure — after every intact row before the break has been delivered by earlier calls — and the same *Failure on every later call. An error from the underlying io.Reader ends the input the same way. After Close it returns os.ErrClosed. A reader opened without a plan returns ErrUnbound until Bind has given it one.

func (*DelimitedReader) Records

func (r *DelimitedReader) Records() int64

Records is the number of records finished so far — the header and skipped blank lines included.

type Dialect

type Dialect struct {
	// Separator is the single-byte separator: tab, or any printable ASCII byte except '"'.
	Separator byte
	// Quoting says whether '"' quotes cells (RFC 4180, "" for a literal quote). Off, a quote
	// is an ordinary byte.
	Quoting bool
	// HasHeader says whether the first record is a header, exposed through
	// DelimitedReader.Header and never delivered as a row.
	HasHeader bool
	// SkipBlankLines says whether a completely empty line is skipped rather than read as a
	// one-cell row.
	SkipBlankLines bool
}

Dialect is how the text is delimited, declared by the caller. Nothing is sniffed: the separator is stated, quoting is stated, the header is stated — the same stance HyperCast's NumFormat takes for numeric notation.

The zero Dialect is not a dialect (it has no separator). Start from CSV, TSV or PSV and change what differs.

type Door

type Door uint32

Door is the door a column is cast through: HyperCast's, plus DoorText for the bytes themselves. The values are the native core's codes, verbatim; zero is never a door.

const (
	// DoorBool is hypercast.Bool: a bool.
	DoorBool Door = 1
	// DoorI8 is hypercast.I8: an int8.
	DoorI8 Door = 2
	// DoorI16 is hypercast.I16: an int16.
	DoorI16 Door = 3
	// DoorI32 is hypercast.I32: an int32.
	DoorI32 Door = 4
	// DoorI64 is hypercast.I64: an int64.
	DoorI64 Door = 5
	// DoorU8 is hypercast.U8: a uint8.
	DoorU8 Door = 6
	// DoorU16 is hypercast.U16: a uint16.
	DoorU16 Door = 7
	// DoorU32 is hypercast.U32: a uint32.
	DoorU32 Door = 8
	// DoorU64 is hypercast.U64: a uint64.
	DoorU64 Door = 9
	// DoorF32 is hypercast.F32: a float32.
	DoorF32 Door = 10
	// DoorF64 is hypercast.F64: a float64.
	DoorF64 Door = 11
	// DoorUuid is hypercast.Uuid: a uuid.UUID.
	DoorUuid Door = 12
	// DoorTimestamp is hypercast.Timestamp: an RFC 3339 instant, as a UTC time.Time.
	DoorTimestamp Door = 13
	// DoorUnix is hypercast.Unix: a Unix-epoch integer at a declared precision, as a UTC
	// time.Time.
	DoorUnix Door = 14
	// DoorDateOnly is hypercast.DateOnly: a strict yyyy-MM-dd date, as a hypercast.Date.
	DoorDateOnly Door = 15
	// DoorTimeOfDay is hypercast.TimeOfDay: a 24-hour time of day, as a time.Duration since
	// midnight.
	DoorTimeOfDay Door = 16
	// DoorSpan is hypercast.Span: a duration, as a hypercast.Duration.
	DoorSpan Door = 17
	// DoorText is the cell's bytes themselves — no cast.
	DoorText Door = 18
	// DoorExact is hypercast.Exact: an exact hypercast.Decimal; no float is ever formed.
	DoorExact Door = 19
	// DoorDateOnlyOrdered is hypercast.DateOnlyOrdered: a separated calendar date under a
	// declared field order, as a hypercast.Date.
	DoorDateOnlyOrdered Door = 20
	// DoorDateTime is hypercast.DateTime: a zone-less civil date-time under a declared field
	// order, as a hypercast.CivilDateTime.
	DoorDateTime Door = 21
	// DoorExcelSerial is hypercast.ExcelSerial: an Excel date serial under a declared date
	// system, as a UTC time.Time.
	DoorExcelSerial Door = 22
)

The twenty-two doors, named as HyperCast's Go module names the functions behind them.

func (Door) String

func (d Door) String() string

String names the door as its constructor is named — "I32", "DateOnlyOrdered", "Text".

type Failure

type Failure struct {
	// Kind is what is wrong.
	Kind FailureKind
	// Record is the zero-based index of the offending record — the header and skipped blank
	// lines included.
	Record int64
	// Line is the one-based line the offending record starts on.
	Line int
	// Byte is the absolute byte offset of the offending record's start.
	Byte int64
	// Expected is the number of cells in the first record, for ColumnCount.
	Expected int
	// Found is the number of cells in this record, for ColumnCount.
	Found int
}

Failure is a structural failure: the input is not rows of cells — a record of the wrong width, input that ends inside a quoted cell, a workbook whose container or parts cannot be read. Never a cell's verdict: a value that does not cast is a fault in its column, and the read goes on. A structural failure ends the input, after every intact row before it has been delivered. It is the error DelimitedReader.Read and Sheet.Read return then, and on every call after, and the error NewWorkbook returns for a workbook it cannot open; reach it with errors.As.

For a workbook, Record is the part the failure is in, Line the sheet row, and Byte the offset within the part's inflated bytes.

Example
package main

import (
	"errors"
	"fmt"
	"log"

	hypercast "github.com/SkunkWerkx/HyperCast/go"
	hypertabular "github.com/SkunkWerkx/HyperTabular/go"
)

func main() {
	// A record of the wrong width is not a cell's verdict: the input is not rows of cells.
	// It is returned after every intact row before it.
	const broken = "a,b\n1,2\n3\n4,5\n"
	plan := []hypertabular.Column{hypertabular.I32(0, hypercast.Invariant)}
	reader, err := hypertabular.NewDelimitedReaderBytes([]byte(broken), hypertabular.CSV, plan)
	if err != nil {
		log.Fatal(err)
	}
	for {
		batch, err := reader.Read()
		if err != nil {
			var failure *hypertabular.Failure
			if errors.As(err, &failure) {
				fmt.Println(failure.Kind, "at line", failure.Line, "— expected", failure.Expected, "cells, found", failure.Found)
				fmt.Println(failure)
			}
			break
		}
		fmt.Println(batch.Rows(), "row:", batch.I32(0))
	}
}
Output:
1 row: [1]
column count at line 3 — expected 2 cells, found 1
hypertabular: record 2 (line 3, byte 8) has 1 cells; the first record had 2

func (*Failure) Error

func (f *Failure) Error() string

Error describes the failure and where it is.

type FailureKind

type FailureKind int

FailureKind is why an input could not be read as rows at all. The values are the native core's codes, verbatim.

const (
	// UnclosedQuote means the input ended inside a quoted cell.
	UnclosedQuote FailureKind = 1
	// ColumnCount means a record's cell count disagrees with the first record's.
	ColumnCount FailureKind = 2
	// RowTooLong means a single record is larger than MaxRowBytes.
	RowTooLong FailureKind = 3
	// NotAZip means a workbook's container is not a zip file.
	NotAZip FailureKind = 16
	// Container means the zip's own structure is broken.
	Container FailureKind = 17
	// Encrypted means the workbook is encrypted.
	Encrypted FailureKind = 18
	// Method means a part is compressed by a method other than stored or deflate.
	Method FailureKind = 19
	// MissingPart means a part the workbook cannot be read without is not there.
	MissingPart FailureKind = 20
	// XML means a part's XML ends inside a construct.
	XML FailureKind = 21
	// Deflate means a part's bytes are not a deflate stream, or stop before the stream does.
	Deflate FailureKind = 22
	// NotAWorkbook means the zip is neither an XLSX nor an ODS workbook.
	NotAWorkbook FailureKind = 23
	// SharedString means a cell names a shared string the table does not have.
	SharedString FailureKind = 24
	// TooLarge means more text than can be addressed: over 4 GiB in a batch or in the
	// shared strings, or 2 GiB in a cell.
	TooLarge FailureKind = 25
)

func (FailureKind) String

func (k FailureKind) String() string

String names the kind in lower case — "unclosed quote", "column count", "not a zip".

type Header []string

Header is a header record's names, in column order: what DelimitedReader.Header and Sheet.Header return. It is a []string underneath, so it is ranged over, indexed, measured and handed to anything that takes a []string as before; what it adds is the lookup a plan is built from — the ordinal of a column by its name.

Do not modify it: it is the reader's own.

func (Header) Find added in v0.8.0

func (h Header) Find(name string) (int, bool)

Find is the ordinal of the first column named name, and whether there is one. The match is exact — byte for byte, case and spaces included, nothing trimmed or folded — and the first of two columns with the same name is the one found.

func (Header) FindBytes added in v0.8.0

func (h Header) FindBytes(name []byte) (int, bool)

FindBytes is Find for a name held as UTF-8 bytes; it allocates nothing.

func (Header) Ordinal added in v0.8.0

func (h Header) Ordinal(name string) (int, error)

Ordinal is the ordinal of the first column named name, matched as Find matches it — what a plan built from names reads best with:

id, err := header.Ordinal("id")

A name the header does not have is a *NoColumnError that names it, and errors.Is matches it to ErrNoColumn.

func (Header) OrdinalBytes added in v0.8.0

func (h Header) OrdinalBytes(name []byte) (int, error)

OrdinalBytes is Ordinal for a name held as UTF-8 bytes.

type NoColumnError added in v0.8.0

type NoColumnError struct {
	// Name is the name that was looked for.
	Name string
}

NoColumnError is the error Header.Ordinal returns for a name the header does not have: it says which. errors.Is matches it to ErrNoColumn.

func (*NoColumnError) Error added in v0.8.0

func (e *NoColumnError) Error() string

Error says which column was not found.

func (*NoColumnError) Is added in v0.8.0

func (e *NoColumnError) Is(target error) bool

Is reports whether target is ErrNoColumn, for errors.Is.

type Option

type Option func(*options)

Option adjusts how a DelimitedReader is built.

func BatchRows

func BatchRows(rows int) Option

BatchRows sets the rows per batch — the capacity of every column buffer. It must be positive; the default is DefaultBatchRows.

func BufferBytes

func BufferBytes(bytes int) Option

BufferBytes sets the initial read buffer for an io.Reader or a file; it doubles whenever a record does not fit, up to MaxRowBytes. It must be positive; the default is DefaultBufferBytes. Text already in memory is read in place and has no buffer.

type Row added in v0.8.0

type Row struct {
	// contains filtered or unexported fields
}

Row is one row of a Batch: the batch read across rather than down, for code that builds a value from each record's columns. It is a (batch, index) pair, so it costs nothing to make or to copy, and — like everything a batch hands out — it is valid until the reader's next Read; do not keep it past that.

func (Row) Batch added in v0.8.0

func (r Row) Batch() *Batch

Batch is the batch the row is in.

func (Row) Fault added in v0.8.0

func (r Row) Fault(column int) *hypercast.Fault

Fault is the verdict of the row's cell in column as HyperCast's own union, as Batch.Fault says.

func (Row) Index added in v0.8.0

func (r Row) Index() int

Index is the row's place in its batch, from zero.

func (Row) Line added in v0.8.0

func (r Row) Line() int

Line is where the row came from, as Batch.Line says: for delimited text the 1-based line its record starts on, for a sheet its 1-based row number.

func (Row) Raw added in v0.8.0

func (r Row) Raw(column int) []byte

Raw is the text the row's cell in column was cast from, as Batch.Raw says.

func (Row) Text added in v0.8.0

func (r Row) Text(column int) []byte

Text is the row's cell in a Text column, as Batch.Text says: nil for a cell with no bytes.

func (Row) TextString added in v0.8.0

func (r Row) TextString(column int) string

TextString is the row's cell in a Text column as a string with no copy, as Batch.TextString says — valid only until the reader's next Read.

func (Row) Verdict added in v0.8.0

func (r Row) Verdict(column int) CellVerdict

Verdict is the verdict of the row's cell in column: Batch.Verdicts(column)[r.Index()].

type Sheet

type Sheet struct {
	// contains filtered or unexported fields
}

Sheet is a forward-only read of one sheet of a Workbook, a batch at a time, through a plan — into the same Batch delimited text is read into.

A sheet has its own buffers and its own copy of the workbook's read state, so several can be read at once. A typed workbook cell is converted directly by its door — a stored 42.0 never passes through text to become an int32 — and a text cell goes through the door as delimited text would. A Sheet is not safe for concurrent use.

func (*Sheet) All added in v0.8.0

func (s *Sheet) All() iter.Seq2[Row, error]

All reads the rest of the sheet, batch by batch, and yields every row of it in order, as DelimitedReader.All does.

func (*Sheet) Bind added in v0.8.0

func (s *Sheet) Bind(plan []Column) error

Bind declares the plan a sheet opened without one (Workbook.SheetUnbound, Workbook.SheetNamedUnbound) reads through — once, before the first Read: DelimitedReader.Bind, for a sheet. A sheet with a plan already returns ErrAlreadyBound; a plan that cannot be honoured is this method's error and leaves the sheet unbound. The rows a batch are the options' BatchRows.

func (*Sheet) Header

func (s *Sheet) Header() Header

Header is the header row's names — a typed cell said the way the text door says it — or nil when the options declare no header. A header with no names is a sheet with no rows. It is read when the sheet is opened, so it is there to build a plan from before Bind.

func (*Sheet) IsBound added in v0.8.0

func (s *Sheet) IsBound() bool

IsBound says whether the sheet has a plan: always, for a sheet opened with one.

func (*Sheet) Options

func (s *Sheet) Options() SheetOptions

Options is the options the sheet is read with.

func (*Sheet) Plan

func (s *Sheet) Plan() []Column

Plan is the plan the sheet is read through: column i of every batch is Plan()[i]; nil until one is bound. Do not modify it.

func (*Sheet) Read

func (s *Sheet) Read() (*Batch, error)

Read reads the next batch: up to the options' BatchRows rows. At the end of the sheet it returns nil and io.EOF. The batch is valid until the next Read.

When the sheet is structurally broken it returns nil and a *Failure — after every intact row before the break has been delivered — and the same *Failure on every later call. A sheet opened without a plan returns ErrUnbound until Bind has given it one.

type SheetInfo

type SheetInfo struct {
	// Name is the sheet's name.
	Name string
	// Hidden says whether the workbook hides the sheet.
	Hidden bool
	// contains filtered or unexported fields
}

SheetInfo is one sheet of a workbook, as Workbook.Sheets lists it.

type SheetOptions

type SheetOptions struct {
	// HasHeader says whether the sheet's first row is a header, exposed through
	// Sheet.Header and never delivered as a row.
	HasHeader bool
	// SkipEmptyRows says whether a row with no cells is skipped rather than delivered as a
	// row of empty cells.
	SkipEmptyRows bool
	// BatchRows is the rows per batch — the capacity of every column buffer. It must be
	// positive.
	BatchRows int
}

SheetOptions is how a sheet is read.

type Value

Value is every type Get reads a cell as: the Go type of each door, which is the type of the column accessor of the same name — bool, the integers, the floats, hypercast.Decimal (Exact), uuid.UUID, time.Time (Timestamp, Unix, ExcelSerial), hypercast.Date (DateOnly, DateOnlyOrdered), hypercast.CivilDateTime (DateTime), time.Duration (TimeOfDay) and hypercast.Duration (Span) — and, for a Text column, []byte or string.

type Workbook

type Workbook struct {
	// contains filtered or unexported fields
}

Workbook is an XLSX or ODS workbook held in memory, its sheets listed and its shared strings and styles loaded: what a Sheet reads from.

A workbook is read in place and holds nothing that needs releasing — every buffer is ordinary Go memory. Its sheets may be read at once, each with buffers of its own, but neither a Workbook nor a Sheet is safe for concurrent use.

Example
package main

import (
	"fmt"
	"log"

	hypertabular "github.com/SkunkWerkx/HyperTabular/go"
)

func main() {
	// A workbook reads into the same batch, a sheet at a time.
	book, err := hypertabular.OpenWorkbook("../corpus/workbook/basic.xlsx")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(book.Format(), book.Sheets()[0].Name)
	// Header first: the sheet is opened, its header read, and the plan bound by name.
	sheet, err := book.SheetUnbound(0, hypertabular.DefaultSheetOptions)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(sheet.Header()[:2])
	id, _ := sheet.Header().Ordinal("id")
	named, _ := sheet.Header().Ordinal("name")
	if err := sheet.Bind([]hypertabular.Column{hypertabular.Text(id), hypertabular.Text(named)}); err != nil {
		log.Fatal(err)
	}
	batch, err := sheet.Read()
	if err != nil {
		log.Fatal(err)
	}
	name, _ := hypertabular.Get[string](batch, 1, 0)
	fmt.Println("row", batch.Line(0), name)
}
Output:
XLSX Data
[id name]
row 2 alice

func NewWorkbook

func NewWorkbook(container []byte) (*Workbook, error)

NewWorkbook opens the workbook in container, in place — nothing is copied, so the bytes must not change while the workbook or any of its sheets is in use. A container that is not a workbook this module can read is a *Failure.

func NewWorkbookReader added in v0.8.0

func NewWorkbookReader(source io.Reader) (*Workbook, error)

NewWorkbookReader reads source to its end into memory the workbook owns, and opens it — for a workbook that is not a file or a []byte already: an embedded file, an HTTP response body, an archive member. The format is told from the bytes. The workbook does not close source; it is done with it when this function returns.

func OpenWorkbook

func OpenWorkbook(path string) (*Workbook, error)

OpenWorkbook reads the file at path into memory and opens it.

func (*Workbook) DateSystem

func (w *Workbook) DateSystem() hypercast.ExcelEpoch

DateSystem is the date system the workbook's serials count in: what a date-formatted number is read by.

func (*Workbook) Format

func (w *Workbook) Format() WorkbookFormat

Format is which kind of workbook this is.

func (*Workbook) Sheet

func (w *Workbook) Sheet(index int, options SheetOptions, plan []Column) (*Sheet, error)

Sheet starts a read of the sheet at index of Sheets through plan. When the options declare a header it is read here, so a header that is structurally broken is this method's error. An index the workbook has no sheet at is an error that wraps ErrNoSheet.

func (*Workbook) SheetNamed

func (w *Workbook) SheetNamed(name string, options SheetOptions, plan []Column) (*Sheet, error)

SheetNamed starts a read of the first sheet named name, as Sheet does. A name the workbook has no sheet by is an error that wraps ErrNoSheet.

func (*Workbook) SheetNamedUnbound added in v0.8.0

func (w *Workbook) SheetNamedUnbound(name string, options SheetOptions) (*Sheet, error)

SheetNamedUnbound starts a read of the first sheet named name without a plan, as SheetUnbound does.

func (*Workbook) SheetUnbound added in v0.8.0

func (w *Workbook) SheetUnbound(index int, options SheetOptions) (*Sheet, error)

SheetUnbound starts a read of the sheet at index of Sheets without a plan: the header is read — when the options declare one — and the plan is bound later, with Sheet.Bind, once the header has said where each column is. Sheet.Read returns ErrUnbound until then.

func (*Workbook) Sheets

func (w *Workbook) Sheets() []SheetInfo

Sheets is the workbook's sheets, in its own order. Sheets that hold no cells (chart sheets, macro sheets) are not among them. Do not modify it.

type WorkbookFormat

type WorkbookFormat int

WorkbookFormat is which kind of workbook a Workbook is.

const (
	// XLSX is Office Open XML: .xlsx, .xlsm.
	XLSX WorkbookFormat = 1
	// ODS is OpenDocument: .ods.
	ODS WorkbookFormat = 2
)

func (WorkbookFormat) String

func (f WorkbookFormat) String() string

String names the format: "XLSX" or "ODS".

Directories

Path Synopsis
internal
tinygosmoke command
Command tinygosmoke is the check CI runs in headless Chrome: this module compiled by TinyGo for the browser (`tinygo build -target=wasm`), with the core linked in from staticlib/wasm and HyperCast's beside it, loaded by index.html through TinyGo's own wasm_exec.js.
Command tinygosmoke is the check CI runs in headless Chrome: this module compiled by TinyGo for the browser (`tinygo build -target=wasm`), with the core linked in from staticlib/wasm and HyperCast's beside it, loaded by index.html through TinyGo's own wasm_exec.js.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL