retable

package module
v0.0.0-...-31d2b3c Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2025 License: MIT Imports: 16 Imported by: 1

README

go-retable

Go Reference Go Report Card

A powerful Go library for working with tabular data using reflection. go-retable provides a unified interface for reading, transforming, and writing tables from various sources and formats including CSV, Excel, HTML, and SQL.

Features

  • Unified Table Interface: Work with all tabular data through a single View interface
  • Multiple Format Support: Read/write CSV, Excel (XLSX), HTML tables, and SQL result sets
  • Type-Safe Conversions: Convert between struct slices, string slices, and generic values
  • Smart Type Conversion: Intelligent value assignment across different Go types
  • Zero-Copy Transformations: Efficient view wrappers for filtering, mapping, and combining data
  • Flexible Formatting: Customizable cell formatters with type-based routing
  • Struct Tag Support: Map struct fields to columns using tags
  • Format Auto-Detection: Automatic detection of CSV encoding, separator, and line endings

Installation

go get github.com/domonda/go-retable

Quick Start

Working with Struct Slices
package main

import (
    "fmt"
    "github.com/domonda/go-retable"
)

type Person struct {
    Name string `col:"Full Name"`
    Age  int    `col:"Age"`
    City string `col:"City"`
}

func main() {
    people := []Person{
        {"Alice Smith", 30, "New York"},
        {"Bob Jones", 25, "London"},
        {"Carol White", 35, "Tokyo"},
    }

    // Create a view from struct slice
    view := retable.NewStructRowsView("People", people, nil, nil)

    // Print the table
    retable.PrintlnView(view)
    // Output:
    // | Full Name   | Age | City     |
    // |-------------|-----|----------|
    // | Alice Smith | 30  | New York |
    // | Bob Jones   | 25  | London   |
    // | Carol White | 35  | Tokyo    |
}
Reading and Writing CSV
import (
    "github.com/domonda/go-retable/csvtable"
)

// Read CSV file
data, format, err := csvtable.ParseFile("data.csv")
if err != nil {
    log.Fatal(err)
}

// Auto-detected format
fmt.Printf("Detected: %s encoding, '%s' separator\n",
    format.Encoding, format.Separator)

// Write CSV with custom formatting
writer := csvtable.NewWriter[[]Person]().
    WithHeaderRow(true).
    WithDelimiter(";").
    WithPadding(csvtable.PadRight)

err = writer.Write(context.Background(), file, people, "People")
Working with Excel Files
import (
    "github.com/domonda/go-retable/exceltable"
)

// Read all sheets from Excel file
sheets, err := exceltable.ReadLocalFile("data.xlsx", false)
if err != nil {
    log.Fatal(err)
}

for _, sheet := range sheets {
    fmt.Printf("Sheet: %s (%d rows, %d cols)\n",
        sheet.Title(),
        sheet.NumRows(),
        len(sheet.Columns()))
}

// Read first sheet only
firstSheet, err := exceltable.ReadLocalFileFirstSheet("data.xlsx", false)
Generating HTML Tables
import (
    "github.com/domonda/go-retable/htmltable"
)

writer := htmltable.NewWriter[[]Person]().
    WithHeaderRow(true).
    WithTableClass("table table-striped")

err := writer.Write(context.Background(), os.Stdout, people, "People")
// Outputs:
// <table class="table table-striped">
//   <thead><tr><th>Full Name</th><th>Age</th><th>City</th></tr></thead>
//   <tbody>
//     <tr><td>Alice Smith</td><td>30</td><td>New York</td></tr>
//     ...
//   </tbody>
// </table>
Converting Between Types
// Convert View back to struct slice
type Employee struct {
    Name     string
    Age      int
    Position string
}

employees, err := retable.ViewToStructSlice[Employee](
    view,
    nil,  // Use default field naming
    nil,  // No custom scanner
    nil,  // No custom formatter
    nil,  // No validation
    "Name", "Age", // Required columns
)

Core Concepts

View Interface

The View interface is the heart of go-retable. It represents any tabular data with rows and columns:

type View interface {
    Title() string      // Table name/title
    Columns() []string  // Column names
    NumRows() int       // Number of data rows
    Cell(row, col int) any // Get cell value
}

All table operations work with Views, making the library highly composable.

View Implementations

Built-in View Types:

  • StringsView - Backed by [][]string (CSV, text data)
  • StructRowsView - Backed by struct slices using reflection
  • AnyValuesView - Backed by [][]any (mixed types, SQL results)
  • ReflectValuesView - Backed by [][]reflect.Value (advanced reflection)

Example:

// From strings
data := [][]string{
    {"Alice", "30"},
    {"Bob", "25"},
}
view := retable.NewStringsView("People", data, []string{"Name", "Age"})

// From structs
people := []Person{{"Alice", 30}, {"Bob", 25}}
view := retable.NewStructRowsView("People", people, nil, nil)
View Wrappers (Decorators)

Transform Views without copying data:

// Filter rows and columns
filtered := retable.FilteredView{
    View:          view,
    RowOffset:     10,
    RowLimit:      20,
    ColumnMapping: []int{0, 2, 3}, // Select specific columns
}

// Dereference pointers automatically
deref := retable.DerefView(pointerView)

// Add computed columns
withTotal := retable.ExtraColsReflectValueFuncView(
    view,
    []string{"Total"},
    func(row int) []reflect.Value {
        price := view.Cell(row, 1).(float64)
        qty := view.Cell(row, 2).(int)
        return []reflect.Value{reflect.ValueOf(price * float64(qty))}
    },
)

// Concatenate views horizontally (like SQL JOIN)
joined := retable.ExtraColsView{view1, view2, view3}

// Concatenate views vertically (like SQL UNION)
combined := retable.ExtraRowView{viewA, viewB, viewC}
Cell Formatters

Customize how values are formatted:

// Type-based formatter
formatter := retable.NewReflectTypeCellFormatter().
    WithKindFormatter(reflect.Float64,
        retable.PrintfCellFormatter("%.2f", false)).
    WithTypeFormatter(reflect.TypeOf(time.Time{}),
        retable.LayoutFormatter("2006-01-02"))

// Use in CSV writer
writer := csvtable.NewWriter[[]Product]().
    WithTypeFormatter(formatter)

// Column-specific formatter
writer.WithColumnFormatter(2, // Column index
    retable.PrintfCellFormatter("$%.2f", false))
Struct Field Naming

Control how struct fields map to columns:

type Product struct {
    SKU         string  `csv:"Product Code"`
    Name        string  `csv:"Product Name"`
    Price       float64 `csv:"Unit Price"`
    InternalID  int     `csv:"-"` // Ignored
}

// Use custom naming
naming := &retable.StructFieldNaming{
    Tag:    "csv",
    Ignore: "-",
}

view := retable.NewStructRowsView("Products", products, nil, naming)
// Columns: ["Product Code", "Product Name", "Unit Price"]

Advanced Features

Smart Type Assignment

SmartAssign intelligently converts between different Go types:

var dest int
src := "42"

err := retable.SmartAssign(
    reflect.ValueOf(&dest).Elem(),
    reflect.ValueOf(src),
    nil, // scanner
    nil, // formatter
)
// dest is now 42

Supports:

  • Direct type conversions
  • String parsing (numbers, bools, times, durations)
  • Interface unwrapping (TextMarshaler, Stringer)
  • Pointer dereferencing
  • Null-like value handling
  • Custom formatters and scanners
SQL Integration

Query in-memory Views using SQL:

import "github.com/domonda/go-retable/sqltable"

// Create virtual database
view := retable.NewStructRowsView("users", users, nil, nil)
db := sqltable.NewViewDB("users", view)
defer db.Close()

// Use standard database/sql
rows, err := db.Query("SELECT name, age FROM users WHERE age > 25")
defer rows.Close()

for rows.Next() {
    var name string
    var age int
    rows.Scan(&name, &age)
    fmt.Printf("%s: %d\n", name, age)
}
Format Detection

Automatically detect CSV format:

import "github.com/domonda/go-retable/csvtable"

config := csvtable.FormatDetectionConfig{
    DetectEncoding:  true,
    DetectSeparator: true,
    DetectNewline:   true,
}

data, format, err := csvtable.ParseFile("unknown.csv")
// Detects: UTF-8/UTF-16LE/ISO-8859-1/Windows-1252/Macintosh
// Detects: , or ; or \t separators
// Detects: \n or \r\n or \n\r line endings

Subpackages

csvtable

CSV reading and writing with format detection:

  • Auto-detect encoding, separator, line endings
  • RFC 4180 compliant parsing
  • Multi-line field support
  • Configurable padding and quoting
  • Row modification utilities
exceltable

Excel file reading using excelize:

  • Read XLSX files from filesystem or io.Reader
  • Multiple sheet support
  • Raw or formatted cell values
  • Automatic empty row/column cleanup
htmltable

HTML table generation:

  • Template-based output
  • Custom CSS classes
  • Column and type-based formatters
  • Automatic HTML escaping
  • Raw HTML output support
sqltable

Virtual SQL driver for in-memory Views:

  • Query Views with SQL syntax
  • Standard database/sql interface
  • Column selection and filtering
  • No actual database required

Utility Functions

// Pretty-print any table data
retable.PrintlnTable(data)

// Get struct field types including embedded fields
fields := retable.StructFieldTypes(reflect.TypeOf(MyStruct{}))

// Convert PascalCase to spaced names
title := retable.SpacePascalCase("UserID")  // "User ID"

// Calculate column widths for alignment
widths := retable.StringColumnWidths([][]string{...})

Examples

Example 1: CSV to Excel Conversion
// Read CSV
csvData, _, err := csvtable.ParseFile("input.csv")
check(err)

csvView := retable.NewStringsView("Data", csvData, nil)

// Convert to structs for processing
type Record struct {
    ID   int
    Name string
    Date time.Time
}

records, err := retable.ViewToStructSlice[Record](csvView, nil, nil, nil, nil)
check(err)

// Process data...
for i := range records {
    records[i].Name = strings.ToUpper(records[i].Name)
}

// Write to Excel (via another library or export as HTML/CSV)
Example 2: Data Validation Pipeline
type User struct {
    Email string
    Age   int
}

func (u User) Validate() error {
    if !strings.Contains(u.Email, "@") {
        return fmt.Errorf("invalid email: %s", u.Email)
    }
    if u.Age < 18 || u.Age > 120 {
        return fmt.Errorf("invalid age: %d", u.Age)
    }
    return nil
}

// Read and validate
users, err := retable.ViewToStructSlice[User](
    csvView,
    nil, // naming
    nil, // scanner
    nil, // formatter
    retable.CallValidateMethod, // validation
    "Email", "Age", // required columns
)
// Returns error if any user fails validation
Example 3: Report Generation
// Load data from multiple sources
salesView := loadSalesData()
inventoryView := loadInventoryData()

// Join data (add inventory info to sales)
joined := retable.ExtraColsView{salesView, inventoryView}

// Add computed columns
withMargin := retable.ExtraColsReflectValueFuncView(
    joined,
    []string{"Margin %"},
    func(row int) []reflect.Value {
        cost := joined.Cell(row, 2).(float64)
        price := joined.Cell(row, 3).(float64)
        margin := ((price - cost) / price) * 100
        return []reflect.Value{reflect.ValueOf(margin)}
    },
)

// Format as HTML report
writer := htmltable.NewWriter[retable.View]().
    WithHeaderRow(true).
    WithTableClass("report-table").
    WithColumnFormatter(4, retable.PrintfCellFormatter("%.1f%%", false))

writer.Write(ctx, reportFile, withMargin, "Sales Report")

Design Philosophy

In-Memory Architecture

go-retable is designed around a fundamental principle: tables are completely loaded into memory before being wrapped as Views. This design decision prioritizes simplicity and performance over streaming capabilities.

Key implications:

  • No context cancellation: View methods don't accept context.Context parameters since data is already in memory
  • No error handling in reads: Cell() and other read methods don't return errors - the data is guaranteed to be available
  • Simple API: The absence of error propagation makes the API cleaner and easier to use
  • Better performance: Random access to any cell is O(1) without I/O overhead
  • Composability: Views can be freely composed, transformed, and reused without side effects

Trade-offs:

This approach makes go-retable not suitable for gigantic tables like those commonly found in large SQL databases (millions+ rows). For such use cases, consider streaming solutions that process data row-by-row.

Ideal use cases:

  • CSV files (typically < 100K rows)
  • Excel spreadsheets (< 1M rows)
  • Report generation and data transformation
  • Configuration and reference data
  • API responses and data exports
  • Data validation pipelines

When to use streaming instead:

  • Processing SQL tables with millions of rows
  • ETL pipelines for large datasets
  • Real-time data processing
  • Memory-constrained environments

Performance Considerations

  • Views are lightweight: Most views are just wrappers around existing data
  • Zero-copy transformations: View decorators don't duplicate data
  • Caching: StructRowsView caches reflected values for efficiency
  • Reflection overhead: Type-based operations use reflection; consider caching for tight loops
  • Memory footprint: Entire table loaded in memory - typical CSV/Excel files fit comfortably, but be mindful of very large datasets

Thread Safety

  • Views are generally not thread-safe for concurrent modifications to underlying data
  • Immutable operations: Reading from Views is safe if underlying data doesn't change
  • Writers use immutable builder pattern: Safe to share writer configurations

Best Practices

  1. Use struct tags for explicit column mapping: col:"Column Name"
  2. Validate data using ViewToStructSlice with validation functions
  3. Choose the right View type:
    • StringsView for CSV/text data
    • StructRowsView for typed data
    • AnyValuesView for mixed types
  4. Compose View wrappers for complex transformations
  5. Reuse formatters rather than creating new ones per cell
  6. Use type-based formatters for consistency across columns

Contributing

Contributions are welcome! Please feel free to submit issues or pull requests.

License

MIT License

  • excelize - Excel file library (used by exceltable)
  • charset - Character encoding support

Documentation

Overview

Package retable provides utilities for working with tabular data in Go, including formatting structs and slices as tables with customizable column naming, cell formatting, and output rendering.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// DefaultStructFieldNaming provides the default StructFieldNaming configuration
	// for converting struct fields to table columns.
	//
	// Configuration:
	//   - Uses "col" as the struct tag to read column titles
	//   - Ignores fields tagged with "-"
	//   - Uses SpacePascalCase to convert untagged field names to titles
	//
	// This is the recommended configuration for most use cases and implements the Viewer interface.
	//
	// Example:
	//
	//	type Person struct {
	//	    FirstName string `col:"First Name"`
	//	    LastName  string `col:"Last Name"`
	//	    Age       int    `col:"Age"`
	//	    password  string `col:"-"` // ignored (also unexported)
	//	}
	DefaultStructFieldNaming = StructFieldNaming{
		Tag:      "col",
		Ignore:   "-",
		Untagged: SpacePascalCase,
	}

	// DefaultStructFieldNamingIgnoreUntagged provides a StructFieldNaming configuration
	// that only includes explicitly tagged fields.
	//
	// Configuration:
	//   - Uses "col" as the struct tag to read column titles
	//   - Ignores fields tagged with "-"
	//   - Ignores all untagged fields (by treating them as "-")
	//
	// This is useful when you want strict control over which fields are included in the table,
	// requiring explicit opt-in via struct tags. Implements the Viewer interface.
	//
	// Example:
	//
	//	type Person struct {
	//	    FirstName string `col:"First Name"` // included
	//	    LastName  string `col:"Last Name"`  // included
	//	    Age       int                       // excluded (no tag)
	//	    internal  string `col:"-"`          // excluded (tagged with "-")
	//	}
	DefaultStructFieldNamingIgnoreUntagged = StructFieldNaming{
		Tag:      "col",
		Ignore:   "-",
		Untagged: UseTitle("-"),
	}

	// SelectViewer is a function variable that selects the most appropriate Viewer implementation
	// for a given table type.
	//
	// Default behavior:
	//   - Returns StringsViewer for [][]string tables
	//   - Returns DefaultStructFieldNaming (as Viewer) for all other types
	//
	// This variable can be reassigned to customize viewer selection logic.
	// The function should return an error if the table type is not supported.
	//
	// Example custom selector:
	//
	//	SelectViewer = func(table any) (Viewer, error) {
	//	    switch table.(type) {
	//	    case [][]string:
	//	        return new(StringsViewer), nil
	//	    case []MyCustomType:
	//	        return &MyCustomViewer{}, nil
	//	    default:
	//	        return &DefaultStructFieldNaming, nil
	//	    }
	//	}
	SelectViewer = func(table any) (Viewer, error) {
		if _, ok := table.([][]string); ok {
			return new(StringsViewer), nil
		}
		return &DefaultStructFieldNaming, nil
	}
)

Functions

func CallValidateMethod

func CallValidateMethod(v reflect.Value) error

CallValidateMethod is a validation function that can be passed to ViewToStructSlice. It checks if the value implements either Validate() or Valid() methods and calls them.

Validation Method Support:

  1. Validate() error: If the value implements this method, it is called. Any non-nil error returned is propagated as a validation failure.

  2. Valid() bool: If the value implements this method, it is called. If it returns false, an error is created describing the validation failure.

The method checks are performed on v.Interface(), so they work with any type that implements these methods, including pointer receivers.

Parameters:

  • v: The reflect.Value to validate. Can be invalid or nil, in which case the function returns nil without error.

Returns:

  • error: nil if validation passes, v is invalid, or v doesn't implement validation methods. Non-nil error if validation fails.

Example:

type Email string

func (e Email) Validate() error {
    if !strings.Contains(string(e), "@") {
        return fmt.Errorf("invalid email: %s", e)
    }
    return nil
}

type Person struct {
    Email Email `db:"email"`
    Age   int   `db:"age"`
}

// CallValidateMethod will call Email.Validate() for the Email field
people, err := ViewToStructSlice[Person](
    view,
    naming,
    nil, nil,
    CallValidateMethod,
)

// Example with Valid() bool method
type Age int

func (a Age) Valid() bool {
    return a >= 0 && a <= 150
}

type PersonWithAge struct {
    Age Age `db:"age"`
}

// CallValidateMethod will call Age.Valid() for the Age field
people, err := ViewToStructSlice[PersonWithAge](
    view,
    naming,
    nil, nil,
    CallValidateMethod,
)

func FormatTableAsStrings

func FormatTableAsStrings(ctx context.Context, table any, formatter CellFormatter, options ...Option) (rows [][]string, err error)

FormatTableAsStrings converts any table data into a 2D string slice.

This function automatically selects an appropriate Viewer for the table type, creates a View, and then formats it as strings using the provided formatter and options.

Parameters:

  • ctx: Context for cancellation and timeout control during formatting
  • table: The table data to format (e.g., [][]string, []Person, etc.)
  • formatter: Optional CellFormatter to customize cell rendering (can be nil for default formatting)
  • options: Optional formatting options (e.g., OptionAddHeaderRow)

Returns a 2D string slice where each inner slice represents a row, and an error if viewer selection, view creation, or formatting fails.

Example:

type Person struct {
    Name string
    Age  int
}
people := []Person{{Name: "John", Age: 30}, {Name: "Alice", Age: 25}}
rows, err := FormatTableAsStrings(ctx, people, nil, OptionAddHeaderRow)
if err != nil {
    log.Fatal(err)
}
// rows[0] = ["Name", "Age"]       // header row
// rows[1] = ["John", "30"]        // data row
// rows[2] = ["Alice", "25"]       // data row

func FormatViewAsStrings

func FormatViewAsStrings(ctx context.Context, view View, formatter CellFormatter, options ...Option) (rows [][]string, err error)

FormatViewAsStrings converts a View into a 2D string slice.

This function iterates through all rows and columns in the view, formatting each cell using the provided CellFormatter. If no formatter is provided, TryFormattersOrSprint is used as the default.

Parameters:

  • ctx: Context for cancellation and timeout control during formatting
  • view: The View to format, providing access to rows, columns, and cell data
  • formatter: Optional CellFormatter to customize cell rendering (can be nil for default formatting)
  • options: Optional formatting options (e.g., OptionAddHeaderRow to include column titles)

When OptionAddHeaderRow is set, the column titles from view.Columns() are added as the first row, also passed through the formatter for consistent formatting.

Returns a 2D string slice where each inner slice represents a row, and an error if any cell formatting fails.

Example:

rows, err := FormatViewAsStrings(ctx, myView, nil, OptionAddHeaderRow)
if err != nil {
    log.Fatal(err)
}
// Process the formatted rows
for _, row := range rows {
    fmt.Println(strings.Join(row, " | "))
}

func FprintlnTable

func FprintlnTable(w io.Writer, title string, table any) error

FprintlnTable formats and writes any table data as a human-readable table to the provided io.Writer.

The function automatically selects an appropriate Viewer for the table type, creates a View with the given title, and then formats it using FprintlnView.

Supported table types include [][]string, struct slices, and any type with a registered Viewer.

Parameters:

  • w: The io.Writer to write the formatted table to
  • title: The title to display above the table (can be empty)
  • table: The table data to format (e.g., [][]string, []Person, etc.)

Returns an error if the table type is not supported, if view creation fails, or if writing fails.

Example:

people := []Person{
    {Name: "John", Age: 30},
    {Name: "Alice", Age: 25},
}
err := FprintlnTable(os.Stdout, "People", people)

func FprintlnView

func FprintlnView(w io.Writer, view View) error

FprintlnView formats and writes a View as a human-readable table to the provided io.Writer.

The table is formatted with pipe-separated columns, aligned based on the maximum width of each column. If the view has a title, it is printed first on a separate line. A header row is automatically added using the view's column names.

The output format looks like:

Title:
| Column1 | Column2 | Column3 |
| value1  | value2  | value3  |

Returns an error if formatting fails or if writing to the io.Writer fails.

Example:

var buf bytes.Buffer
err := FprintlnView(&buf, myView)
if err != nil {
    log.Fatal(err)
}
fmt.Print(buf.String())

func HasOption

func HasOption(options []Option, option Option) bool

HasOption checks whether any Option in the slice has the specified option flag set. Returns true if at least one Option in the slice contains the given option.

This is a convenience function for checking options in a slice of Options, which is commonly used when passing variadic option parameters.

Example:

options := []Option{OptionAddHeaderRow}
if HasOption(options, OptionAddHeaderRow) {
    // Header row option is present in the slice
}

// With variadic parameters:
func formatTable(data any, options ...Option) {
    if HasOption(options, OptionAddHeaderRow) {
        // Add header row
    }
}

func IndexedStructFieldAnyValues

func IndexedStructFieldAnyValues(structValue reflect.Value, numVals int, indices []int) []any

IndexedStructFieldAnyValues returns a reordered subset of exported struct field values as []any, including the inlined fields of any anonymously embedded structs.

The indices parameter maps from the flattened field list to the desired output positions. A negative index value (-1) indicates that the field should be skipped. The numVals parameter specifies the size of the output slice.

This is similar to IndexedStructFieldReflectValues but returns values as any interfaces.

Panics if the length of indices does not match the number of fields in the struct.

Example:

type Person struct {
    Name string
    Age  int
    City string
}
p := Person{Name: "John", Age: 30, City: "NYC"}
// Reorder to [City, Name] and skip Age
values := IndexedStructFieldAnyValues(reflect.ValueOf(p), 2, []int{1, -1, 0})
// Returns: []any{NYC, John}

func IndexedStructFieldReflectValues

func IndexedStructFieldReflectValues(structValue reflect.Value, numVals int, indices []int) []reflect.Value

IndexedStructFieldReflectValues returns a reordered subset of reflect.Value for exported struct fields, including the inlined fields of any anonymously embedded structs.

The indices parameter maps from the flattened field list to the desired output positions. A negative index value (-1) indicates that the field should be skipped. The numVals parameter specifies the size of the output slice.

Panics if the length of indices does not match the number of fields in the struct.

Example:

type Person struct {
    Name string
    Age  int
    City string
}
p := Person{Name: "John", Age: 30, City: "NYC"}
// Reorder to [City, Name] and skip Age
values := IndexedStructFieldReflectValues(reflect.ValueOf(p), 2, []int{1, -1, 0})
// Returns reflect.Values at positions: [0]=City, [1]=Name

func IsNullLike

func IsNullLike(val reflect.Value) bool

IsNullLike returns true if the passed reflect.Value should be treated as null-like.

A value is considered null-like if it fulfills any of the following conditions:

  • Is not valid (zero value of reflect.Value)
  • Is nil (for pointer, interface, slice, map, channel, func, or unsafe pointer types)
  • Is of type struct{} (empty struct)
  • Implements IsNull() bool method which returns true
  • Implements IsZero() bool method which returns true
  • Implements driver.Valuer interface which returns (nil, nil)

This function is useful for determining whether a value should be rendered as NULL or empty in table output, supporting various nullable type conventions used in Go.

Example:

var ptr *string
IsNullLike(reflect.ValueOf(ptr))  // true (nil pointer)

type Nullable struct{}
func (n Nullable) IsNull() bool { return true }
IsNullLike(reflect.ValueOf(Nullable{}))  // true

IsNullLike(reflect.ValueOf(42))  // false

func IsStringRowEmpty

func IsStringRowEmpty(row []string) bool

IsStringRowEmpty returns true if all cells in the row are empty strings or if the length of the row is zero.

This is useful for filtering out empty rows from table data before rendering or processing.

Example:

IsStringRowEmpty([]string{})              // true
IsStringRowEmpty([]string{"", "", ""})    // true
IsStringRowEmpty([]string{"", "data", ""}) // false

func MustStructFieldIndex

func MustStructFieldIndex(structPtr, fieldPtr any) int

MustStructFieldIndex returns the index of the struct field pointed to by fieldPtr within the struct pointed to by structPtr.

This is the panic-on-error version of StructFieldIndex. The returned index counts exported struct fields including the inlined fields of any anonymously embedded structs, using the same ordering as StructFieldTypes.

Panics if either parameter is invalid or if the field cannot be found.

Example:

type Person struct {
    Name string
    Age  int
}
p := Person{Name: "John", Age: 30}
index := MustStructFieldIndex(&p, &p.Age)
// Returns: 1

func ParseTime

func ParseTime(str string) (t time.Time, format string, err error)

ParseTime is a standalone function that parses a time string and returns both the parsed time and the format that successfully parsed it.

This is useful when you need to know which format was used, for example to maintain consistent formatting when round-tripping time values, or for validation purposes.

The function tries all formats in the package-level timeFormats list in order.

Parameters:

  • str: The string to parse

Returns:

  • t: The parsed time.Time value
  • format: The layout string that successfully parsed the time
  • err: Error if no format could parse the string

Example:

t, format, err := ParseTime("2024-03-15T14:30:00Z")
// t = time value, format = "2006-01-02T15:04:05Z07:00" (RFC3339), err = nil

t, format, err := ParseTime("2024-03-15")
// t = time value, format = "2006-01-02" (DateOnly), err = nil

// Use the format to maintain consistency
formatted := t.Format(format) // Returns string in same format as input

func PrintlnTable

func PrintlnTable(title string, table any) error

PrintlnTable formats and prints any table data as a human-readable table to standard output.

This is a convenience wrapper around FprintlnTable that writes to os.Stdout. See FprintlnTable for details on supported table types.

Parameters:

  • title: The title to display above the table (can be empty)
  • table: The table data to format

Returns any error that occurred during formatting or writing.

Example:

people := []Person{
    {Name: "John", Age: 30},
    {Name: "Alice", Age: 25},
}
err := PrintlnTable("People", people)
Example
type Row struct {
	A string
	B int
	C *time.Time
}
t := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)

PrintlnTable("ExamplePrintlnTable", []Row{
	{A: "1", B: -1, C: &t},
	{A: "", B: 2222222222, C: nil},
	{A: "Last row", B: 0, C: nil},
})
Output:

ExamplePrintlnTable:
| A        | B          | C                             |
| 1        | -1         | 2024-01-02 03:04:05 +0000 UTC |
|          | 2222222222 |                               |
| Last row | 0          |                               |

func PrintlnView

func PrintlnView(view View) error

PrintlnView formats and prints a View as a human-readable table to standard output.

This is a convenience wrapper around FprintlnView that writes to os.Stdout. See FprintlnView for format details.

Returns any error that occurred during formatting or writing.

Example:

err := PrintlnView(myView)
if err != nil {
    log.Fatal(err)
}
Example
PrintlnView(&StringsView{
	Tit:  "ExamplePrintlnView",
	Cols: []string{"A", "B", "C"},
	Rows: [][]string{
		{"1", "2222222222", "3"},
		{"", "", "3333"},
		{"Last row"},
	},
})
Output:

ExamplePrintlnView:
| A        | B          | C    |
| 1        | 2222222222 | 3    |
|          |            | 3333 |
| Last row |            |      |

func RemoveEmptyStringColumns

func RemoveEmptyStringColumns(rows [][]string) (numCols int)

RemoveEmptyStringColumns removes all columns that contain only empty strings from the input rows and returns the new number of columns.

The function modifies the input rows slice in-place by removing columns that are empty across all rows. It processes columns from right to left to maintain correct indexing during removal.

Returns the number of columns remaining after removal.

Example:

rows := [][]string{
    {"Name", "",   "Age", ""},
    {"John", "",   "30",  ""},
    {"Alice", "",  "25",  ""},
}
numCols := RemoveEmptyStringColumns(rows)
// Modifies rows to: [["Name", "Age"], ["John", "30"], ["Alice", "25"]]
// Returns: 2

func RemoveEmptyStringRows

func RemoveEmptyStringRows(rows [][]string) [][]string

RemoveEmptyStringRows removes all rows that contain only empty strings from the input slice and returns the modified slice.

The function modifies the input slice in-place by removing empty rows. Rows are checked using IsStringRowEmpty and removed in reverse order to maintain correct indexing during removal.

Example:

rows := [][]string{
    {"Name", "Age"},
    {"", ""},        // Will be removed
    {"John", "30"},
    {"", ""},        // Will be removed
}
result := RemoveEmptyStringRows(rows)
// Returns: [["Name", "Age"], ["John", "30"]]

func SmartAssign

func SmartAssign(dst, src reflect.Value, dstScanner Scanner, srcFormatter Formatter) (err error)

SmartAssign performs intelligent type conversion when assigning src to dst. It attempts multiple conversion strategies in order of preference, making it suitable for converting between different types in data mapping scenarios.

Type Conversion Strategies (in order):

  1. Null handling: If src implements IsNull() bool and returns true, dst is set to its zero value.

  2. Direct conversion: If src type is convertible to dst type using reflect.Value.Convert, the conversion is performed directly.

  3. Nil pointer handling: If src is a nil pointer, dst is set to its zero value.

  4. Custom formatting: If dst is a string type and srcFormatter is provided, srcFormatter.Format is used to convert src to string.

  5. TextMarshaler: If src implements encoding.TextMarshaler, its MarshalText method is used to get a text representation for further conversion.

  6. Stringer: If src implements fmt.Stringer, its String method is used to get a string representation for further conversion.

  7. Time parsing: If src is a string and dst is time.Time or *time.Time, ParseTime is used to convert the string to a time value.

  8. Pointer dereferencing: If src is a non-nil pointer, SmartAssign is recursively called with the dereferenced value.

  9. Empty struct handling: If src is an empty struct (struct{}), dst is set to its zero value.

  10. Boolean conversions: - bool to numeric types: true becomes 1, false becomes 0 - bool to string: "true" or "false" - numeric types to bool: non-zero becomes true, zero becomes false - string to bool: parsed using strconv.ParseBool

  11. String to numeric conversions: - String to int/uint: parsed using strconv.ParseInt/ParseUint - String to float: parsed using strconv.ParseFloat

  12. Fallback string conversion: Any type can be converted to string using fmt.Sprint as a last resort.

  13. Pointer allocation: If dst is a pointer type and previous strategies failed, a new instance is created and SmartAssign is recursively called to assign to the dereferenced pointer.

Parameters:

  • dst: The destination reflect.Value to assign to. Must be valid and settable.
  • src: The source reflect.Value to assign from. Must be valid.
  • dstScanner: Optional Scanner for custom string-to-type conversions (can be nil).
  • srcFormatter: Optional Formatter for custom type-to-string conversions (can be nil).

Returns:

  • error: nil on success, or an error describing why the assignment failed. Returns errors.ErrUnsupported if no conversion strategy could handle the type combination.

Example:

// Convert string to int
var result int
dst := reflect.ValueOf(&result).Elem()
src := reflect.ValueOf("42")
err := SmartAssign(dst, src, nil, nil)
// result == 42

// Convert bool to string
var str string
dst = reflect.ValueOf(&str).Elem()
src = reflect.ValueOf(true)
err = SmartAssign(dst, src, nil, nil)
// str == "true"

// Convert with custom formatter
formatter := FormatterFunc(func(v reflect.Value) (string, error) {
    return fmt.Sprintf("#%v", v.Interface()), nil
})
var output string
dst = reflect.ValueOf(&output).Elem()
src = reflect.ValueOf(42)
err = SmartAssign(dst, src, nil, formatter)
// output == "#42"

func SpaceGoCase

func SpaceGoCase(name string) string

SpaceGoCase inserts spaces before upper case characters within Go-style names, with special handling for acronyms and abbreviations.

The function processes each character and:

  • Inserts a space before uppercase letters following lowercase letters ("CamelCase" -> "Camel Case")
  • Detects acronyms by recognizing consecutive uppercase letters
  • Inserts a space before the last uppercase letter in an acronym when followed by lowercase ("HTTPServer" -> "HTTP Server")
  • Replaces underscore '_' characters with spaces
  • Avoids duplicate spaces
  • Trims leading and trailing spaces from the result

This is more sophisticated than SpacePascalCase as it properly handles Go naming conventions where acronyms like HTTP, API, URL are kept in uppercase. Can be used as the UntaggedTitle function in StructFieldNaming.

Example:

SpaceGoCase("FirstName")      // Returns: "First Name"
SpaceGoCase("HTTPServer")     // Returns: "HTTP Server"
SpaceGoCase("APIKey")         // Returns: "API Key"
SpaceGoCase("URLPath")        // Returns: "URL Path"
SpaceGoCase("user_name")      // Returns: "user name"

func SpacePascalCase

func SpacePascalCase(name string) string

SpacePascalCase inserts spaces before upper case characters within PascalCase names.

The function processes each character in the input string and:

  • Inserts a space before each uppercase letter that follows a lowercase letter or non-space
  • Replaces underscore '_' characters with spaces
  • Avoids duplicate spaces
  • Trims leading and trailing spaces from the result

This is particularly useful for converting struct field names to human-readable column titles. Can be used as the UntaggedTitle function in StructFieldNaming.

Example:

SpacePascalCase("FirstName")      // Returns: "First Name"
SpacePascalCase("HTTPServer")     // Returns: "H T T P Server"
SpacePascalCase("user_name")      // Returns: "user name"
SpacePascalCase("API_Key")        // Returns: "A P I Key"

func SprintlnTable

func SprintlnTable(w io.Writer, title string, table any) (string, error)

SprintlnTable formats any table data as a human-readable table and returns it as a string.

This is a convenience wrapper around FprintlnTable that writes to a strings.Builder and returns the resulting string. See FprintlnTable for details on supported table types.

Parameters:

  • w: Ignored parameter (kept for signature compatibility)
  • title: The title to display above the table (can be empty)
  • table: The table data to format

Returns the formatted table string and any error that occurred.

Example:

people := []Person{{Name: "John", Age: 30}}
str, err := SprintlnTable(nil, "People", people)
if err != nil {
    log.Fatal(err)
}
fmt.Print(str)

func SprintlnView

func SprintlnView(w io.Writer, view View) (string, error)

SprintlnView formats a View as a human-readable table and returns it as a string.

This is a convenience wrapper around FprintlnView that writes to a strings.Builder and returns the resulting string. See FprintlnView for format details.

Returns the formatted table string and any error that occurred during formatting.

Example:

str, err := SprintlnView(myView)
if err != nil {
    log.Fatal(err)
}
fmt.Print(str)

func StringColumnWidths

func StringColumnWidths(rows [][]string, maxCols int) []int

StringColumnWidths returns the maximum width of each column in a string table, measured as the count of UTF-8 runes (not bytes).

The maxCols parameter limits the number of columns to consider:

  • If maxCols is -1, all columns across all rows are considered
  • If maxCols is positive, only the first maxCols columns are processed

This is useful for formatting tables with aligned columns, ensuring each column is wide enough to accommodate its widest value.

Returns a slice where each element represents the maximum width of that column. Returns nil if maxCols is 0.

Example:

rows := [][]string{
    {"Name", "Age", "City"},
    {"John", "30", "New York"},
    {"Alice", "25", "SF"},
}
widths := StringColumnWidths(rows, -1)
// Returns: [5, 3, 8] (lengths of "Alice", "Age", "New York")

func StructFieldAnyValues

func StructFieldAnyValues(structValue reflect.Value) []any

StructFieldAnyValues returns the values of exported struct fields as []any, including the inlined fields of any anonymously embedded structs. If structValue is a pointer, it automatically dereferences to the underlying struct value.

This is similar to StructFieldReflectValues but returns the values as any interfaces instead of reflect.Value, making it easier to work with in non-reflection contexts.

The function recursively processes embedded structs, flattening their field values into the result. Only exported fields are included. The order matches StructFieldTypes.

Example:

type Person struct {
    Name string
    Age  int
}
p := Person{Name: "John", Age: 30}
values := StructFieldAnyValues(reflect.ValueOf(p))
// Returns: []any{"John", 30}

func StructFieldIndex

func StructFieldIndex(structPtr, fieldPtr any) (int, error)

StructFieldIndex returns the index of the struct field pointed to by fieldPtr within the struct pointed to by structPtr.

The returned index counts exported struct fields including the inlined fields of any anonymously embedded structs, using the same ordering as StructFieldTypes.

Both structPtr and fieldPtr must be non-nil pointers. Returns an error if either parameter is nil, not a pointer, or if the field cannot be found within the struct.

Example:

type Person struct {
    Name string
    Age  int
}
p := Person{Name: "John", Age: 30}
index, err := StructFieldIndex(&p, &p.Age)
// Returns: index=1, err=nil

func StructFieldReflectValues

func StructFieldReflectValues(structValue reflect.Value) []reflect.Value

StructFieldReflectValues returns the reflect.Value of exported struct fields, including the inlined fields of any anonymously embedded structs. If structValue is a pointer, it automatically dereferences to the underlying struct value.

The function recursively processes embedded structs, flattening their field values into the result. Only exported fields (those starting with an uppercase letter) are included. The order of fields matches the order returned by StructFieldTypes.

Example:

type Address struct {
    Street string
    City   string
}
type Person struct {
    Name string
    Address
}
p := Person{Name: "John", Address: Address{Street: "Main St", City: "NYC"}}
values := StructFieldReflectValues(reflect.ValueOf(p))
// Returns reflect.Values for: ["John", "Main St", "NYC"]

func StructFieldTypes

func StructFieldTypes(structType reflect.Type) (fields []reflect.StructField)

StructFieldTypes returns the exported fields of a struct type, including the inlined fields of any anonymously embedded structs. If structType is a pointer, it automatically dereferences to the underlying struct type.

The function recursively processes embedded structs, flattening their fields into the result. Only exported fields (those starting with an uppercase letter) are included.

Example:

type Address struct {
    Street string
    City   string
}
type Person struct {
    Name string
    Address // embedded struct
    age  int // unexported, will be excluded
}
fields := StructFieldTypes(reflect.TypeOf(Person{}))
// Returns: [Name, Street, City]

func UseTitle

func UseTitle(columnTitle string) func(fieldName string) (columnTitle string)

UseTitle returns a function that always returns the same fixed columnTitle, ignoring the input fieldName parameter.

This is useful when you want to provide a constant column title regardless of the field name, or as a way to ignore certain fields by returning a special marker like "-". Can be used as the UntaggedTitle function in StructFieldNaming.

Example:

// Create a function that always returns "Fixed Title"
titleFunc := UseTitle("Fixed Title")
title := titleFunc("AnyFieldName") // Returns: "Fixed Title"

// Common pattern to ignore untagged fields
naming := StructFieldNaming{
    Tag:      "col",
    Ignore:   "-",
    Untagged: UseTitle("-"), // Ignore all untagged fields
}

func ViewToStructSlice

func ViewToStructSlice[T any](view View, naming *StructFieldNaming, dstScanner Scanner, srcFormatter Formatter, validate func(reflect.Value) error, requiredCols ...string) ([]T, error)

ViewToStructSlice converts a View into a strongly-typed slice of structs. It maps each row in the View to a struct instance by matching column names to struct field names using the provided naming conventions.

Type Parameter:

  • T: The target struct type. Can be either a struct type (MyStruct) or a pointer to struct (*MyStruct). The function will handle both cases appropriately.

Column to Field Mapping:

The function uses StructFieldNaming to determine how View column names map to struct field names. For each column in the View, it attempts to find a corresponding struct field and assigns the value using SmartAssign, which performs intelligent type conversions.

If a column has no corresponding struct field, it is silently skipped. This allows Views to contain extra columns that aren't needed in the struct.

Required Columns:

The requiredCols parameter specifies column names that MUST exist both in the View and as struct fields. If any required column is missing from either the View or the struct definition, an error is returned immediately before processing any rows.

Type Conversion:

SmartAssign is used for each field assignment, enabling automatic conversion between compatible types:

  • String to numeric types (int, float, etc.)
  • Numeric to string
  • Boolean to numeric (true=1, false=0)
  • Custom conversions via dstScanner and srcFormatter

Validation:

After each successful field assignment, if a validate function is provided, it is called with the struct field value. If validation fails, processing stops and the error is returned. This allows for field-level validation during the conversion process.

Use CallValidateMethod as the validate function to automatically invoke Validate() or Valid() methods on struct field values that implement them.

Parameters:

  • view: The source View to convert. Must not be nil.
  • naming: Defines how column names map to struct field names. Must not be nil.
  • dstScanner: Optional Scanner for custom string-to-type conversions (can be nil).
  • srcFormatter: Optional Formatter for custom type-to-string conversions (can be nil).
  • validate: Optional function called after each field assignment for validation (can be nil).
  • requiredCols: Column names that must exist in both View and struct.

Returns:

  • []T: A slice of structs with length equal to view.NumRows(). Each struct represents one row from the View.
  • error: An error if:
  • T is not a struct or pointer to struct
  • Any required column is missing from View or struct
  • Type conversion fails for any field
  • Validation fails for any field

Example:

// Define a struct to hold row data
type Person struct {
    Name string `db:"name"`
    Age  int    `db:"age"`
}

// Create a View with data
view := NewStringsView("people",
    [][]string{
        {"Alice", "30"},
        {"Bob", "25"},
    },
    "name", "age")

// Convert to slice of structs
naming := &StructFieldNaming{Tag: "db"}
people, err := ViewToStructSlice[Person](view, naming, nil, nil, nil)
// people[0] == Person{Name: "Alice", Age: 30}
// people[1] == Person{Name: "Bob", Age: 25}

// With required columns and validation
people, err = ViewToStructSlice[Person](
    view,
    naming,
    nil, nil,
    CallValidateMethod, // Validate each field
    "name", "age",      // Both columns are required
)

// Using pointer type
people, err := ViewToStructSlice[*Person](view, naming, nil, nil, nil)
// people[0] == &Person{Name: "Alice", Age: 30}

Types

type AnyValuesView

type AnyValuesView struct {
	// Tit is the title of this view, returned by the Title() method.
	Tit string

	// Cols contains the column names defining both the column headers
	// and the number of columns in this view.
	Cols []string

	// Rows contains the data rows, where each row is a slice of any-typed values.
	// Each cell can hold a value of any type.
	Rows [][]any
}

AnyValuesView is a View implementation that stores cells as values of any type.

Unlike StringsView which is restricted to string values, AnyValuesView can hold heterogeneous data types within the same table. Each cell can contain values of different types (int, string, bool, struct, etc.), making it ideal for mixed-type data or when converting from other view types.

The underlying data structure uses [][]any, where each row is a slice of interface{} (any) values. This provides maximum flexibility at the cost of type safety and some performance overhead due to interface boxing.

Performance characteristics:

  • Direct memory access: O(1) for cell retrieval
  • Interface boxing overhead for value storage and retrieval
  • Higher memory usage than StringsView due to interface overhead
  • Type assertions required when accessing specific types

When to use AnyValuesView:

  • Working with mixed-type tabular data (numbers, strings, booleans, etc.)
  • Caching data from another View with NewAnyValuesViewFrom
  • Need to store arbitrary Go values in table cells
  • Converting between different view types
  • Building dynamic tables where cell types aren't known at compile time

Example usage:

view := &retable.AnyValuesView{
    Tit:  "Mixed Data",
    Cols: []string{"ID", "Name", "Active", "Score"},
    Rows: [][]any{
        {1, "Alice", true, 95.5},
        {2, "Bob", false, 87.3},
    },
}
// Type assertion needed for specific types
if score, ok := view.Cell(0, 3).(float64); ok {
    fmt.Printf("Score: %.1f\n", score)
}

Thread safety: Not thread-safe. External synchronization required for concurrent access.

func NewAnyValuesViewFrom

func NewAnyValuesViewFrom(source View) *AnyValuesView

NewAnyValuesViewFrom creates an AnyValuesView by reading and caching all cells from the source View.

This function materializes all data from the source view into memory as an AnyValuesView. This is useful for:

  • Caching data from views with expensive Cell() operations
  • Converting computed or dynamic views into static data
  • Creating a snapshot of view data at a point in time
  • Ensuring consistent data access when the source might change

The function calls source.Cell() for every cell in the view, so for large views this can be memory-intensive. The resulting view is completely independent of the source.

Parameters:

  • source: The View to read data from

Returns a new AnyValuesView containing all data from the source.

Example:

// Cache an expensive computed view
expensiveView := retable.NewComputedView(...)
cached := retable.NewAnyValuesViewFrom(expensiveView)
// Now cached can be accessed multiple times without recomputation

Time complexity: O(rows * cols) for copying all cells Space complexity: O(rows * cols) for storing all values

func (*AnyValuesView) Cell

func (view *AnyValuesView) Cell(row, col int) any

Cell returns the value at the specified row and column indices.

The returned value can be of any type depending on what was stored in that cell. Callers typically need to use type assertions or type switches to work with the specific types.

Parameters:

  • row: Zero-based row index (0 to NumRows()-1)
  • col: Zero-based column index (0 to len(Columns())-1)

Returns:

  • The cell value (of any type) if indices are valid
  • nil if row or col indices are out of bounds

Example with type assertion:

value := view.Cell(0, 2)
switch v := value.(type) {
case int:
    fmt.Printf("Integer: %d\n", v)
case string:
    fmt.Printf("String: %s\n", v)
case bool:
    fmt.Printf("Boolean: %t\n", v)
}

Time complexity: O(1)

func (*AnyValuesView) Columns

func (view *AnyValuesView) Columns() []string

Columns returns the column names of this view.

func (*AnyValuesView) NumRows

func (view *AnyValuesView) NumRows() int

NumRows returns the number of data rows in this view.

func (*AnyValuesView) Title

func (view *AnyValuesView) Title() string

Title returns the title of this view.

type CellFormatter

type CellFormatter interface {
	// FormatCell formats the view cell at a row/col position as string.
	//
	// Returns errors.ErrUnsupported if this formatter doesn't support the cell's type,
	// signaling that alternative formatters should be tried. Other errors indicate
	// actual formatting failures.
	//
	// The raw result indicates whether the returned string is in the raw format of
	// the target table format and can be used as-is (true), or if it needs to be
	// sanitized according to the output format (false).
	//
	// Parameters:
	//   - ctx: Context for cancellation and timeout control
	//   - view: The table view containing the cell to format
	//   - row: Zero-based row index
	//   - col: Zero-based column index
	//
	// Returns:
	//   - str: The formatted string representation
	//   - raw: Whether the string is format-specific raw output
	//   - err: Error if formatting failed, or errors.ErrUnsupported if unsupported
	FormatCell(ctx context.Context, view View, row, col int) (str string, raw bool, err error)
}

CellFormatter is the primary interface for formatting table cell values as strings. This is the higher-level interface compared to Formatter, as it operates on cells within a table View context and supports format-specific "raw" output.

The CellFormatter design supports a sophisticated formatting pipeline where multiple formatters can be tried in sequence until one successfully formats a value. Formatters signal their inability to handle a value by returning errors.ErrUnsupported, which allows the caller to try alternative formatters.

The "raw" result concept is crucial for output format optimization: when raw is true, the string can be used directly in the output format without escaping or sanitization (e.g., HTML tags in HTML output, CSV-formatted values in CSV output). When raw is false, the string should be sanitized according to the output format (e.g., HTML-escaped).

Design pattern:

// Try formatters in priority order
str, raw, err := formatter1.FormatCell(ctx, view, row, col)
if errors.Is(err, errors.ErrUnsupported) {
    str, raw, err = formatter2.FormatCell(ctx, view, row, col)
}
if err != nil {
    // handle error or use fallback
}

Example usage:

formatter := PrintfCellFormatter("%.2f")
str, raw, err := formatter.FormatCell(ctx, view, 0, 0)
if err != nil {
    // handle error
}
if !raw {
    str = html.EscapeString(str) // sanitize for HTML output
}

func CellFormatterFromFormatter

func CellFormatterFromFormatter(f Formatter, rawResult bool) CellFormatter

CellFormatterFromFormatter adapts a simple Formatter to work as a CellFormatter. This bridge function allows reflection-based value formatters to be used in the table cell formatting system.

The rawResult parameter determines whether the formatted strings should be marked as "raw" (true) or requiring sanitization (false) in the table output format.

This adapter extracts the reflect.Value from the specified cell position and passes it to the underlying Formatter.

Parameters:

  • f: The Formatter to adapt
  • rawResult: Whether the formatted output is considered raw (doesn't need escaping)

Returns:

  • A CellFormatter that delegates to the provided Formatter

Example:

formatter := SprintFormatter{}
cellFormatter := CellFormatterFromFormatter(formatter, false)
// cellFormatter can now be used in table rendering

func SprintCellFormatter

func SprintCellFormatter(rawResult bool) CellFormatter

SprintCellFormatter returns a universal CellFormatter that formats any cell value using fmt.Sprint, which uses the value's String() method if available, or falls back to Go's default formatting.

This formatter never returns errors.ErrUnsupported and accepts all value types, making it an ideal fallback formatter at the end of a formatting chain.

The rawResult parameter determines whether the formatted output should be marked as raw (true) or requiring sanitization (false). This allows the caller to control the raw flag based on the output format requirements.

Parameters:

  • rawResult: Whether formatted strings should be marked as raw output

Returns:

  • A CellFormatter that uses fmt.Sprint for all values

Example usage:

// Non-raw formatter (strings need sanitization)
formatter := SprintCellFormatter(false)
str, raw, _ := formatter.FormatCell(ctx, view, 0, 0)
// str contains fmt.Sprint output, raw == false

// Raw formatter (strings are pre-formatted)
rawFormatter := SprintCellFormatter(true)
str, raw, _ := rawFormatter.FormatCell(ctx, view, 0, 0)
// str contains fmt.Sprint output, raw == true

func TryFormattersOrSprint

func TryFormattersOrSprint(formatters ...CellFormatter) CellFormatter

TryFormattersOrSprint creates a composite CellFormatter that tries multiple formatters in sequence until one succeeds, with fmt.Sprint as the ultimate fallback.

This is the primary function for building flexible formatting chains. It implements a "chain of responsibility" pattern where each formatter is tried in order until one successfully formats the value (returns no error or a non-ErrUnsupported error).

Behavior:

  • Tries each formatter in the order provided
  • Continues to next formatter only if current returns errors.ErrUnsupported
  • Returns immediately on success or non-ErrUnsupported errors
  • Falls back to fmt.Sprint if all formatters return ErrUnsupported
  • Returns empty string for nil values
  • Ignores nil formatters in the list
  • Fallback results are always marked as non-raw (raw=false)

This function is essential for type-safe formatting where you want to try specialized formatters first and fall back to generic formatting if the value type doesn't match.

Parameters:

  • formatters: Variable number of CellFormatters to try in order

Returns:

  • A composite CellFormatter that implements the fallback chain

Example usage:

formatter := TryFormattersOrSprint(
    PrintfCellFormatter("%.2f"),      // Try formatting as float with 2 decimals
    LayoutFormatter("2006-01-02"),    // Try formatting as date
    // Falls back to fmt.Sprint for other types
)
str, raw, err := formatter.FormatCell(ctx, view, 0, 0)
// Uses first matching formatter, or fmt.Sprint if none match

type CellFormatterFunc

type CellFormatterFunc func(ctx context.Context, view View, row, col int) (str string, raw bool, err error)

CellFormatterFunc is a function type that implements the CellFormatter interface, allowing plain functions to be used as CellFormatters.

This adapter type follows the common Go pattern of defining a function type that implements an interface (similar to http.HandlerFunc), making it easy to create inline formatters without defining separate types.

Example:

formatter := CellFormatterFunc(func(ctx context.Context, view View, row, col int) (string, bool, error) {
    val := view.Cell(row, col)
    if num, ok := val.(int); ok {
        return fmt.Sprintf("#%05d", num), false, nil
    }
    return "", false, errors.ErrUnsupported
})

func ReflectCellFormatterFunc

func ReflectCellFormatterFunc(function any, rawResult bool) (formatter CellFormatterFunc, valType reflect.Type, err error)

ReflectCellFormatterFunc converts an arbitrary function into a CellFormatterFunc using reflection, enabling type-safe cell formatting with minimal boilerplate.

This powerful function allows you to write formatters as simple, strongly-typed functions that work with specific value types, and automatically adapts them to work with the generic CellFormatter interface. This provides both type safety and ergonomic API.

Function signature requirements:

Arguments (0-2 parameters):

  • Optional first parameter: context.Context (if present, must be first)
  • Optional value parameter: Any type (becomes the valType return value)

Results (1-2 return values):

  • First result: Must be string (the formatted output)
  • Optional second result: Must be error (formatting error)

The rawResult parameter determines the raw flag returned by the generated formatter.

Parameters:

  • function: The function to convert (validated via reflection)
  • rawResult: Whether the formatter should mark output as raw

Returns:

  • formatter: The generated CellFormatterFunc
  • valType: The reflect.Type of the value parameter (for type registration)
  • err: Error if function signature is invalid

Example usage:

// Simple type-safe formatter
formatter, typ, err := ReflectCellFormatterFunc(
    func(t time.Time) string {
        return t.Format("2006-01-02")
    },
    false,
)
// typ == reflect.TypeOf(time.Time{})
// formatter is a CellFormatterFunc that only works with time.Time

// With context and error handling
formatter, typ, err := ReflectCellFormatterFunc(
    func(ctx context.Context, val CustomType) (string, error) {
        if err := ctx.Err(); err != nil {
            return "", err
        }
        return val.Format(), nil
    },
    false,
)

// No arguments (formats any value the same way)
formatter, _, err := ReflectCellFormatterFunc(
    func() string { return "constant" },
    true,
)

func (CellFormatterFunc) FormatCell

func (f CellFormatterFunc) FormatCell(ctx context.Context, view View, row, col int) (str string, raw bool, err error)

FormatCell implements the CellFormatter interface by calling the function itself.

type ExtraColsView

type ExtraColsView []View

ExtraColsView is a zero-copy decorator that horizontally concatenates multiple Views, combining their columns into a single unified View. This is the columnar equivalent of a SQL JOIN operation, allowing you to merge data from different sources side-by-side.

Key Features

  • Horizontal concatenation: Columns from all views appear sequentially
  • Zero-copy: No data duplication, references original Views
  • Automatic row padding: Shorter views treated as having nil cells for extra rows
  • Preserves types: Each cell maintains its original type from source View

Use Cases

  • Joining related data: Combine user profiles with their statistics
  • Adding computed columns: Append calculated values to existing data
  • Merging partial data: Combine views from different data sources
  • Building composite reports: Unite data from multiple queries

Row Count Behavior

The resulting view has as many rows as the longest input View. Shorter views are implicitly padded with nil values:

View1: 10 rows, 2 columns
View2: 5 rows, 3 columns
ExtraColsView: 10 rows, 5 columns (View2 rows 5-9 are all nil)

Column Order

Columns appear in the order of views in the slice:

ExtraColsView{view1, view2, view3}
Columns: [view1.col0, view1.col1, view2.col0, view2.col1, view3.col0]

Performance Characteristics

  • No data copying: Only stores View references
  • Linear column lookup: O(n) where n is number of views
  • Constant row lookup: O(1) after finding the right view
  • Memory efficient: Only overhead is the slice of View references

Example: Basic Column Concatenation

// View1: Name and Age
people := NewStringsView("People", [][]string{
    {"Alice", "30"},
    {"Bob", "25"},
}, []string{"Name", "Age"})

// View2: City and Country
locations := NewStringsView("", [][]string{
    {"NYC", "USA"},
    {"London", "UK"},
}, []string{"City", "Country"})

// Combine them
combined := ExtraColsView{people, locations}
// Columns: ["Name", "Age", "City", "Country"]
// Row 0: ["Alice", "30", "NYC", "USA"]
// Row 1: ["Bob", "25", "London", "UK"]

Example: Unequal Row Counts

view1 := NewStringsView("", [][]string{
    {"A1"}, {"A2"}, {"A3"},
}, []string{"Col1"})

view2 := NewStringsView("", [][]string{
    {"B1"},
}, []string{"Col2"})

combined := ExtraColsView{view1, view2}
// 3 rows total (max of 3 and 1)
combined.Cell(0, 0) -> "A1"
combined.Cell(0, 1) -> "B1"
combined.Cell(1, 0) -> "A2"
combined.Cell(1, 1) -> nil  // view2 has no row 1
combined.Cell(2, 0) -> "A3"
combined.Cell(2, 1) -> nil  // view2 has no row 2

Example: Adding Computed Columns

// Original data
baseView := NewStructRowsView("Sales", salesData, nil, nil)

// Add computed columns via a custom view
computedView := ExtraColsAnyValueFuncView(nil, []string{"Total", "Tax"},
    func(row, col int) any {
        // Calculate based on baseView data
        if col == 0 { return calculateTotal(row) }
        return calculateTax(row)
    })

// Combine
enriched := ExtraColsView{baseView, computedView}
// Now has original columns plus Total and Tax

Example: Multi-Source Join

users := loadUsersView()      // id, name, email
profiles := loadProfilesView() // bio, avatar
stats := loadStatsView()       // post_count, follower_count

fullProfile := ExtraColsView{users, profiles, stats}
// Columns: id, name, email, bio, avatar, post_count, follower_count

Edge Cases

  • Empty ExtraColsView{} results in 0 rows, 0 columns, empty title
  • Single view ExtraColsView{view} behaves identically to view
  • Views with 0 rows contribute 0 to final row count
  • Negative row/col indices return nil
  • Column index beyond total column count returns nil

Composition

ExtraColsView can be nested and combined with other decorators:

base := NewStringsView(...)
extra1 := ExtraColsView{base, computedView1}
extra2 := ExtraColsView{extra1, computedView2}
filtered := &FilteredView{Source: extra2, RowLimit: 10}
// First 10 rows with all computed columns

Title Behavior

The title is taken from the first view in the slice. If you need a custom title, wrap the result with ViewWithTitle.

func (ExtraColsView) Cell

func (e ExtraColsView) Cell(row, col int) any

Cell returns the value at the specified row and column position.

The column parameter is translated to the appropriate source view:

  1. Iterates through views to find which one contains the requested column
  2. Translates col to the local column index within that view
  3. Returns view.Cell(row, localCol)

Returns nil if:

  • row or col are negative
  • col >= total number of columns
  • row >= NumRows() of the responsible view (implicit nil padding)
  • The underlying view cell is nil

Example:

view1 has columns ["A", "B"] (indices 0-1)
view2 has columns ["C", "D", "E"] (indices 2-4 in combined view)

combined := ExtraColsView{view1, view2}

combined.Cell(0, 0) -> view1.Cell(0, 0)  // Column "A"
combined.Cell(0, 1) -> view1.Cell(0, 1)  // Column "B"
combined.Cell(0, 2) -> view2.Cell(0, 0)  // Column "C"
combined.Cell(0, 3) -> view2.Cell(0, 1)  // Column "D"
combined.Cell(0, 4) -> view2.Cell(0, 2)  // Column "E"
combined.Cell(0, 5) -> nil               // Out of bounds

Performance: O(n) where n is the number of views, as it must iterate to find the correct view. For performance-critical code with many views, consider caching column offsets.

func (ExtraColsView) Columns

func (e ExtraColsView) Columns() []string

Columns returns all column names from all Views concatenated in order.

The resulting slice contains columns from each view sequentially:

[view0.col0, view0.col1, ..., view1.col0, view1.col1, ..., viewN.colM]

Returns an empty slice if ExtraColsView is empty.

Example:

view1.Columns() -> ["A", "B"]
view2.Columns() -> ["C", "D", "E"]
ExtraColsView{view1, view2}.Columns() -> ["A", "B", "C", "D", "E"]

func (ExtraColsView) NumRows

func (e ExtraColsView) NumRows() int

NumRows returns the maximum row count across all Views.

The combined view has as many rows as its longest component View. Shorter views are implicitly treated as having nil cells for additional rows.

Returns 0 if ExtraColsView is empty or all views have 0 rows.

Example:

view1.NumRows() -> 10
view2.NumRows() -> 5
view3.NumRows() -> 15
ExtraColsView{view1, view2, view3}.NumRows() -> 15

func (ExtraColsView) Title

func (e ExtraColsView) Title() string

Title returns the title of the first View in the slice. Returns an empty string if the slice is empty.

The title is not a combination of all view titles - only the first is used. To set a custom title, use ViewWithTitle to wrap the ExtraColsView.

type ExtraRowView

type ExtraRowView []View

ExtraRowView is a zero-copy decorator that vertically concatenates multiple Views, stacking their rows into a single unified View. This is the row equivalent of a SQL UNION operation, allowing you to combine data from different sources with the same column structure.

Key Features

  • Vertical concatenation: Rows from all views appear sequentially
  • Zero-copy: No data duplication, references original Views
  • Column alignment: All views must have compatible column structures
  • Preserves types: Each cell maintains its original type from source View

Use Cases

  • Combining result sets: Merge data from multiple queries
  • Appending batches: Stack incrementally loaded data
  • Unifying data sources: Combine test + prod data for reports
  • Building composite datasets: Unite similar data from different time periods

Column Structure Requirements

ExtraRowView uses the column structure from the first view in the slice. All subsequent views should have the same column count and compatible types. The implementation does NOT validate column compatibility - it's the caller's responsibility to ensure views have matching structures.

Column names are taken only from the first view:

View1: ["Name", "Age"]  <- columns used
View2: ["Name", "Age"]
ExtraRowView columns: ["Name", "Age"]

If views have different columns, cells are accessed by position only:

View1: ["A", "B"]
View2: ["X", "Y"]
ExtraRowView: columns ["A", "B"], but view2.Cell(0, 0) returns "X" data

Row Count Behavior

The resulting view has the sum of all input Views' row counts:

View1: 10 rows
View2: 5 rows
View3: 15 rows
ExtraRowView: 30 rows total

Performance Characteristics

  • No data copying: Only stores View references
  • Linear row lookup: O(n) where n is number of views
  • Constant column lookup: O(1)
  • Memory efficient: Only overhead is the slice of View references

Example: Basic Row Concatenation

// Historical data
past := NewStringsView("", [][]string{
    {"Alice", "30"},
    {"Bob", "25"},
}, []string{"Name", "Age"})

// Recent data
recent := NewStringsView("", [][]string{
    {"Charlie", "35"},
    {"Diana", "28"},
}, []string{"Name", "Age"})

// Combine them
all := ExtraRowView{past, recent}
// 4 rows total:
// Row 0: ["Alice", "30"]   (from past)
// Row 1: ["Bob", "25"]     (from past)
// Row 2: ["Charlie", "35"] (from recent)
// Row 3: ["Diana", "28"]   (from recent)

Example: Combining Multiple Data Sources

usData := loadUSCustomers()      // 100 rows
euData := loadEUCustomers()      // 75 rows
asiaData := loadAsiaCustomers()  // 50 rows

allCustomers := ExtraRowView{usData, euData, asiaData}
// 225 rows total, all with same columns

Example: Appending Summary Rows

dataRows := loadDataView()

// Create a summary row view
summaryData := [][]any{
    {"TOTAL", sumValues(), avgValues()},
}
summary := NewAnyValuesView("", summaryData, dataRows.Columns())

// Append summary at the bottom
withSummary := ExtraRowView{dataRows, summary}
// Last row contains the totals

Example: Combining Filtered Subsets

allData := loadDataView()

// Split into categories
categoryA := &FilteredView{
    Source: allData,
    RowOffset: 0,
    RowLimit: 10,
}
categoryB := &FilteredView{
    Source: allData,
    RowOffset: 50,
    RowLimit: 10,
}

// Recombine selected rows
selected := ExtraRowView{categoryA, categoryB}
// 20 rows: first 10 from categoryA, then 10 from categoryB

Example: Multi-Batch Loading

var batches []View
for batch := range loadIncrementally() {
    batches = append(batches, batch)
}
allData := ExtraRowView(batches)
// All batches combined into single view

Row Index Translation

Cell access translates row indices to the appropriate source view:

view1 has 10 rows (indices 0-9)
view2 has 5 rows (indices 10-14 in combined view)
view3 has 8 rows (indices 15-22 in combined view)

combined := ExtraRowView{view1, view2, view3}

combined.Cell(5, 0)  -> view1.Cell(5, 0)   // Within view1
combined.Cell(12, 0) -> view2.Cell(2, 0)   // Within view2 (12-10=2)
combined.Cell(20, 0) -> view3.Cell(5, 0)   // Within view3 (20-15=5)

Edge Cases

  • Empty ExtraRowView{} results in 0 rows, 0 columns, empty title
  • Single view ExtraRowView{view} behaves identically to view
  • Views with 0 rows contribute nothing to final result
  • Negative row/col indices return nil
  • Column index beyond first view's column count returns nil
  • Row index beyond total row count returns nil

Column Mismatch Behavior

If views have different column counts, no error occurs:

  • Columns() returns the first view's columns only

  • Accessing a column index >= a view's column count returns nil for that view's rows

    view1 := NewStringsView("", [][]string{{"A", "B"}}, []string{"Col1", "Col2"}) view2 := NewStringsView("", [][]string{{"X"}}, []string{"Col1"})

    combined := ExtraRowView{view1, view2} combined.Columns() -> ["Col1", "Col2"] combined.Cell(0, 0) -> "A" combined.Cell(0, 1) -> "B" combined.Cell(1, 0) -> "X" combined.Cell(1, 1) -> nil // view2 has no column 1

Composition

ExtraRowView can be nested and combined with other decorators:

batch1 := loadBatch1()
batch2 := loadBatch2()
combined := ExtraRowView{batch1, batch2}

// Filter the combined result
filtered := &FilteredView{
    Source: combined,
    RowLimit: 100,
}

// Add computed columns to the combination
enriched := ExtraColsAnyValueFuncView(filtered, []string{"Score"}, scoreFunc)

Title Behavior

The title is taken from the first view in the slice. If you need a custom title, wrap the result with ViewWithTitle.

func (ExtraRowView) Cell

func (e ExtraRowView) Cell(row, col int) any

Cell returns the value at the specified row and column position.

The row parameter is translated to the appropriate source view:

  1. Iterates through views to find which one contains the requested row
  2. Translates row to the local row index within that view
  3. Returns view.Cell(localRow, col)

Returns nil if:

  • row or col are negative
  • row >= total number of rows
  • col >= number of columns (from first view)
  • The underlying view cell is nil

Example:

view1 has 2 rows (indices 0-1 in combined view)
view2 has 3 rows (indices 2-4 in combined view)

combined := ExtraRowView{view1, view2}

combined.Cell(0, 0) -> view1.Cell(0, 0)  // First row of view1
combined.Cell(1, 0) -> view1.Cell(1, 0)  // Second row of view1
combined.Cell(2, 0) -> view2.Cell(0, 0)  // First row of view2
combined.Cell(3, 0) -> view2.Cell(1, 0)  // Second row of view2
combined.Cell(4, 0) -> view2.Cell(2, 0)  // Third row of view2
combined.Cell(5, 0) -> nil               // Out of bounds

Performance: O(n) where n is the number of views, as it must iterate to find the correct view. For performance-critical code with many views, consider pre-computing row offsets.

func (ExtraRowView) Columns

func (e ExtraRowView) Columns() []string

Columns returns the column names from the first View in the slice. Returns nil if the slice is empty.

All views in ExtraRowView should have the same column structure, but this is not enforced. Only the first view's columns are used to define the structure of the combined view.

Example:

view1.Columns() -> ["A", "B", "C"]
view2.Columns() -> ["A", "B", "C"]
ExtraRowView{view1, view2}.Columns() -> ["A", "B", "C"]

func (ExtraRowView) NumRows

func (e ExtraRowView) NumRows() int

NumRows returns the total row count across all Views.

The combined view has the sum of row counts from all component Views. Views with 0 rows contribute nothing to the total.

Returns 0 if ExtraRowView is empty or all views have 0 rows.

Example:

view1.NumRows() -> 10
view2.NumRows() -> 5
view3.NumRows() -> 15
ExtraRowView{view1, view2, view3}.NumRows() -> 30

func (ExtraRowView) Title

func (e ExtraRowView) Title() string

Title returns the title of the first View in the slice. Returns an empty string if the slice is empty.

The title is not a combination of all view titles - only the first is used. To set a custom title, use ViewWithTitle to wrap the ExtraRowView.

type FilteredView

type FilteredView struct {
	// Source is the underlying View to filter and transform.
	// All Cell() calls are delegated to this View after applying
	// row offset and column mapping transformations.
	Source View

	// RowOffset is the index of the first row from Source to include.
	// Rows 0 to RowOffset-1 are skipped.
	// Negative values are treated as 0.
	// If RowOffset >= Source.NumRows(), the view will have 0 rows.
	//
	// Example:
	//   RowOffset: 0  -> starts at first row (no offset)
	//   RowOffset: 10 -> skips first 10 rows, starts at row 10
	RowOffset int

	// RowLimit caps the maximum number of rows in this view.
	// If RowLimit is 0 or negative, no limit is applied (all rows shown).
	// If RowLimit > 0, at most RowLimit rows are shown.
	// The actual row count may be less if Source has fewer rows available.
	//
	// Example:
	//   RowLimit: 0  -> no limit, show all rows after RowOffset
	//   RowLimit: 10 -> show at most 10 rows
	RowLimit int

	// ColumnMapping controls which columns are visible and in what order.
	//
	// If nil:
	//   - All columns from Source are included
	//   - Columns appear in their original order
	//   - NumCols() returns len(Source.Columns())
	//
	// If not nil:
	//   - Only mapped columns are included
	//   - len(ColumnMapping) determines NumCols()
	//   - Each element is an index into Source's columns
	//   - Columns can be reordered or duplicated
	//
	// Example:
	//   ColumnMapping: nil        -> [0, 1, 2, 3] (all columns)
	//   ColumnMapping: []int{0}   -> [0] (only first column)
	//   ColumnMapping: []int{2,0} -> [2, 0] (columns 2 and 0, reordered)
	//   ColumnMapping: []int{1,1} -> [1, 1] (column 1 duplicated)
	//
	// WARNING: Invalid indices (negative or >= Source.NumCols()) will cause
	// panics when accessing cells or column names.
	ColumnMapping []int
}

FilteredView is a decorator that provides zero-copy filtering, slicing, and column remapping of an underlying View. It implements pagination, column selection, and column reordering without duplicating the underlying data.

Key Features

  • Row slicing: Skip rows (RowOffset) and limit results (RowLimit)
  • Column filtering: Select specific columns via ColumnMapping
  • Column reordering: Rearrange columns by mapping indices
  • Zero-copy: No data duplication, just index manipulation

Use Cases

  • Pagination: Implement offset/limit patterns for large datasets
  • Column selection: Export only specific columns (e.g., for CSV or API responses)
  • Column reordering: Change column display order without modifying source data
  • Data projection: Create different views of the same underlying data

Performance Characteristics

FilteredView is extremely lightweight:

  • No data copying: Source data is never duplicated
  • Constant overhead: Cell access adds only index arithmetic
  • Memory efficient: Only stores integers for offset, limit, and mapping

Example: Pagination

// Original view with 1000 rows
source := NewStringsView("Products", productData, columns)

// Get page 3 (rows 20-29, assuming page size of 10)
page3 := &FilteredView{
    Source:    source,
    RowOffset: 20,
    RowLimit:  10,
}
fmt.Println(page3.NumRows()) // 10

Example: Column Selection

// Select only columns 0, 2, and 4 from source
filtered := &FilteredView{
    Source:        source,
    ColumnMapping: []int{0, 2, 4},
}
// Accessing column 0 of filtered reads column 0 of source
// Accessing column 1 of filtered reads column 2 of source
// Accessing column 2 of filtered reads column 4 of source

Example: Column Reordering

// Reverse column order (assuming 3 columns)
reversed := &FilteredView{
    Source:        source,
    ColumnMapping: []int{2, 1, 0},
}

Example: Combined Operations

// Select specific columns AND paginate
view := &FilteredView{
    Source:        source,
    RowOffset:     100,
    RowLimit:      25,
    ColumnMapping: []int{0, 3, 5}, // Only columns 0, 3, 5
}

Field Details

  • Source: The underlying View to filter/transform (required)
  • RowOffset: Number of rows to skip from the beginning (0 = no offset)
  • RowLimit: Maximum number of rows to include (0 = no limit, show all remaining)
  • ColumnMapping: nil = all columns; []int = column indices to include/reorder

Edge Cases

  • Negative RowOffset is treated as 0
  • RowOffset beyond source length results in 0 rows
  • Out-of-bounds cell access returns nil
  • Invalid column mapping indices will panic on Cell() access

Composition

FilteredView can wrap other FilteredViews for complex transformations:

base := NewStringsView(...)
step1 := &FilteredView{Source: base, ColumnMapping: []int{0, 2, 4, 6}}
step2 := &FilteredView{Source: step1, RowOffset: 10, RowLimit: 5}
// step2 shows rows 10-14 of base, with columns 0, 2, 4, 6

func (*FilteredView) Cell

func (view *FilteredView) Cell(row, col int) any

Cell returns the value at the specified row and column position in this filtered view.

The row and col parameters are relative to this view's coordinate space (after filtering), not the Source view's coordinates.

Returns nil if:

  • row or col are negative
  • row >= NumRows()
  • col >= NumCols()
  • The underlying Source cell is nil

The method translates coordinates before accessing Source:

  • Row: adds RowOffset to translate to Source's row space
  • Col: maps through ColumnMapping if set, otherwise uses col directly

Example:

source := NewStringsView("Data", [][]string{
    {"A0", "B0", "C0"},  // row 0
    {"A1", "B1", "C1"},  // row 1
    {"A2", "B2", "C2"},  // row 2
    {"A3", "B3", "C3"},  // row 3
}, []string{"A", "B", "C"})

filtered := &FilteredView{
    Source:        source,
    RowOffset:     1,           // skip first row
    RowLimit:      2,           // show 2 rows
    ColumnMapping: []int{2, 0}, // columns C, A
}

filtered.Cell(0, 0) -> "C1"  (row 1, col 2 in source)
filtered.Cell(0, 1) -> "A1"  (row 1, col 0 in source)
filtered.Cell(1, 0) -> "C2"  (row 2, col 2 in source)
filtered.Cell(2, 0) -> nil   (row 2 is out of bounds in filtered view)

func (*FilteredView) Columns

func (view *FilteredView) Columns() []string

Columns returns the column names for this filtered view.

If ColumnMapping is nil, returns all columns from Source in their original order. If ColumnMapping is set, returns only the mapped columns in the specified order.

The returned slice is newly allocated and safe to modify.

Example:

source.Columns() -> ["A", "B", "C", "D"]
view := &FilteredView{Source: source, ColumnMapping: []int{3, 1}}
view.Columns() -> ["D", "B"]

func (*FilteredView) NumCols

func (view *FilteredView) NumCols() int

NumCols returns the number of columns in this filtered view.

If ColumnMapping is nil, returns the number of columns in Source. If ColumnMapping is set, returns len(ColumnMapping).

This method is more efficient than len(view.Columns()) as it avoids allocating the column name slice.

func (*FilteredView) NumRows

func (view *FilteredView) NumRows() int

NumRows returns the number of rows visible in this filtered view after applying RowOffset and RowLimit.

The calculation:

  1. Start with Source.NumRows()
  2. Subtract RowOffset (treated as 0 if negative)
  3. Apply RowLimit if > 0
  4. Return 0 if result would be negative

Examples:

Source has 100 rows:
  RowOffset: 0,  RowLimit: 0   -> 100 rows
  RowOffset: 10, RowLimit: 0   -> 90 rows
  RowOffset: 10, RowLimit: 20  -> 20 rows
  RowOffset: 95, RowLimit: 20  -> 5 rows (limited by available data)
  RowOffset: 200, RowLimit: 0  -> 0 rows (offset beyond data)

func (*FilteredView) Title

func (view *FilteredView) Title() string

Title returns the title from the underlying Source view. FilteredView does not modify the title.

type Formatter

type Formatter interface {
	// Format converts a reflect.Value to its string representation.
	// Returns errors.ErrUnsupported if the formatter doesn't support the value's type.
	Format(reflect.Value) (string, error)
}

Formatter is a reflection-based value formatter that converts a reflect.Value to a string. This is the simpler, lower-level interface compared to CellFormatter, as it operates directly on reflect.Value without requiring a View context or cell coordinates.

Formatter is useful when you need to format individual values that aren't necessarily part of a table structure, or when building custom CellFormatter implementations.

Example usage:

formatter := FormatterFunc(func(v reflect.Value) (string, error) {
    if v.Kind() == reflect.Int {
        return fmt.Sprintf("#%d", v.Int()), nil
    }
    return "", errors.ErrUnsupported
})
str, err := formatter.Format(reflect.ValueOf(42))
// str == "#42"

func FormatterFromCellFormatter

func FormatterFromCellFormatter(f CellFormatter) Formatter

FormatterFromCellFormatter adapts a CellFormatter to work as a simple Formatter. This is the reverse bridge of CellFormatterFromFormatter, allowing table cell formatters to be used for formatting individual reflect.Values.

The adapter creates a minimal single-value View containing just the provided reflect.Value at position (0, 0) and calls the CellFormatter on it. The context used is context.Background() since there's no external context available.

The "raw" result from the CellFormatter is discarded, as the simpler Formatter interface doesn't have this concept.

Parameters:

  • f: The CellFormatter to adapt

Returns:

  • A Formatter that delegates to the provided CellFormatter

Example:

cellFormatter := PrintfCellFormatter("%d%%")
formatter := FormatterFromCellFormatter(cellFormatter)
str, _ := formatter.Format(reflect.ValueOf(95))
// str == "95%"

type FormatterFunc

type FormatterFunc func(reflect.Value) (string, error)

FormatterFunc is a function type that implements the Formatter interface, allowing plain functions to be used as Formatters.

This adapter type follows the common Go pattern of defining a function type that implements an interface (similar to http.HandlerFunc).

Example:

var formatter Formatter = FormatterFunc(func(v reflect.Value) (string, error) {
    return fmt.Sprintf("value: %v", v.Interface()), nil
})

func (FormatterFunc) Format

func (f FormatterFunc) Format(v reflect.Value) (string, error)

Format implements the Formatter interface by calling the function itself.

type HeaderView

type HeaderView struct {
	// Tit is the title of this view.
	Tit string

	// Cols contains the column names, which are also used as the data row.
	Cols []string
}

HeaderView is a specialized View that contains only a header row.

This view type displays column names as both the column headers and the single data row, making it useful for showing just table structure or for combining multiple views where you need header information displayed.

The view always has exactly one row (NumRows() returns 1), and that row contains the column names as values.

When to use HeaderView:

  • Displaying table structure/schema information
  • Creating view combinations where headers need to be visible
  • Testing or debugging column configurations
  • Generating table metadata displays

Example:

view := &retable.HeaderView{
    Tit:  "User Schema",
    Cols: []string{"ID", "Name", "Email", "Active"},
}
// Row 0 will contain: "ID", "Name", "Email", "Active"

func NewHeaderView

func NewHeaderView(cols ...string) *HeaderView

NewHeaderView creates a View containing only a header row.

The header row displays the column names as data, creating a single-row view where the column names are also the cell values in that row. This is useful for displaying just the header information or combining with other views.

All column names have leading and trailing whitespace trimmed automatically.

Parameters:

  • cols: Variable number of column names

Returns a new HeaderView with one row containing the column names.

Example:

view := retable.NewHeaderView("ID", "Name", "Email")
fmt.Println(view.NumRows())    // Output: 1
fmt.Println(view.Cell(0, 1))   // Output: Name

func NewHeaderViewFrom

func NewHeaderViewFrom(source View) *HeaderView

NewHeaderViewFrom creates a HeaderView from an existing View's columns.

This function extracts the column names and title from the source View and creates a new HeaderView where the column names appear as both the header and the single data row.

Parameters:

  • source: The View to extract column names from

Returns a new HeaderView with the source's title and columns.

Example:

original := retable.NewStringsView("Products", data, "ID", "Name", "Price")
headerOnly := retable.NewHeaderViewFrom(original)
// headerOnly contains just one row with "ID", "Name", "Price" as values

func (*HeaderView) Cell

func (view *HeaderView) Cell(row, col int) any

Cell returns the column name at the specified column index for row 0.

Since HeaderView has only one row, row must be 0. The returned value is the column name at the given column index.

Parameters:

  • row: Must be 0, otherwise nil is returned
  • col: Zero-based column index (0 to len(Columns())-1)

Returns:

  • The column name as any if row is 0 and col is valid
  • nil if row is not 0 or col is out of bounds

func (*HeaderView) Columns

func (view *HeaderView) Columns() []string

Columns returns the column names of this view.

func (*HeaderView) NumRows

func (view *HeaderView) NumRows() int

NumRows always returns 1 for HeaderView since it contains only the header row.

func (*HeaderView) Title

func (view *HeaderView) Title() string

Title returns the title of this view.

type LayoutFormatter

type LayoutFormatter string

LayoutFormatter formats values that implement the Format(string) string method, such as time.Time, by calling their Format method with the layout string.

This formatter is specifically designed for types like time.Time that use layout strings for formatting. The string value of this type becomes the layout parameter passed to the value's Format method.

If the cell value doesn't implement the required interface, an error is returned (not errors.ErrUnsupported, but a descriptive error about the type mismatch).

Example usage:

// Format time.Time values as ISO dates
formatter := LayoutFormatter("2006-01-02")
str, raw, err := formatter.FormatCell(ctx, view, 0, 0)
// For time.Time value of "2024-03-15 14:30:00"
// str == "2024-03-15", raw == false

// Format as 12-hour time
timeFormatter := LayoutFormatter("3:04 PM")
// For time.Time value of "2024-03-15 14:30:00"
// str == "2:30 PM"

func (LayoutFormatter) FormatCell

func (f LayoutFormatter) FormatCell(ctx context.Context, view View, row, col int) (str string, raw bool, err error)

FormatCell implements CellFormatter by calling the cell value's Format method with the layout string. Returns an error if the value doesn't implement interface{ Format(string) string }.

type Option

type Option int

Option represents formatting options that can be applied when rendering tables. Options are implemented as bit flags and can be combined using bitwise OR.

const (
	// OptionAddHeaderRow adds a header row containing column titles
	// as the first row in the formatted output.
	//
	// When this option is set, the View's Columns() method is called to get column titles,
	// and these are rendered as the first row before any data rows.
	//
	// Example:
	//
	//	rows, err := FormatViewAsStrings(ctx, view, nil, OptionAddHeaderRow)
	//	// First row will contain: ["Column1", "Column2", "Column3"]
	//	// Subsequent rows contain data
	OptionAddHeaderRow Option = 1 << iota
)

func (Option) Has

func (o Option) Has(option Option) bool

Has checks whether this Option has the specified option flag set. Since Option is a bit flag type, multiple options can be combined and this method tests if a specific option is present.

Example:

opts := OptionAddHeaderRow
if opts.Has(OptionAddHeaderRow) {
    // Header row option is enabled
}

func (Option) String

func (o Option) String() string

String returns a human-readable string representation of the Option. Multiple options are joined with "|". If no options are set, returns "no Option".

Example:

opt := OptionAddHeaderRow
fmt.Println(opt.String()) // Output: "AddHeaderRow"

opt = 0
fmt.Println(opt.String()) // Output: "no Option"

type Parser

type Parser interface {
	// ParseInt parses a string into a 64-bit signed integer.
	// Returns an error if the string is not a valid integer.
	ParseInt(string) (int64, error)

	// ParseUint parses a string into a 64-bit unsigned integer.
	// Returns an error if the string is not a valid unsigned integer.
	ParseUint(string) (uint64, error)

	// ParseFloat parses a string into a 64-bit floating point number.
	// May handle locale-specific formatting (e.g., comma vs. period decimals).
	// Returns an error if the string is not a valid float.
	ParseFloat(string) (float64, error)

	// ParseBool parses a string into a boolean value.
	// The strings recognized as true/false are implementation-specific.
	// Returns an error if the string is not recognized as a boolean.
	ParseBool(string) (bool, error)

	// ParseTime parses a string into a time.Time value.
	// May try multiple time formats in sequence.
	// Returns an error if the string doesn't match any recognized format.
	ParseTime(string) (time.Time, error)

	// ParseDuration parses a string into a time.Duration value.
	// Uses Go's standard duration format (e.g., "1h30m", "5s").
	// Returns an error if the string is not a valid duration.
	ParseDuration(string) (time.Duration, error)
}

Parser is the interface for parsing string representations into primitive Go types. This is the counterpart to formatting - it handles the conversion from strings back to typed values.

Parser provides a centralized place to configure parsing behavior, including:

  • Which strings represent boolean true/false values
  • Which strings represent nil/null values
  • Which time formats to try when parsing time values
  • Locale-specific number formatting (e.g., comma vs. period decimal separators)

The Parser interface is used by Scanners to perform the actual string-to-value conversions. By abstracting parsing into an interface, different parsing strategies can be used (strict vs. lenient, different locale conventions, etc.).

Example usage:

parser := NewStringParser()
// Configure custom boolean strings
parser.TrueStrings = []string{"true", "yes", "1", "on"}
parser.FalseStrings = []string{"false", "no", "0", "off"}

// Parse various types
i, _ := parser.ParseInt("42")          // 42
f, _ := parser.ParseFloat("3,14")      // 3.14 (handles comma decimal)
b, _ := parser.ParseBool("yes")        // true
t, _ := parser.ParseTime("2024-03-15") // time.Time value

type PrintfCellFormatter

type PrintfCellFormatter string

PrintfCellFormatter implements CellFormatter by applying a Printf-style format string to cell values. The string value of this type is used as the format string for fmt.Sprintf.

This formatter never returns errors.ErrUnsupported - it will format any value type that fmt.Sprintf can handle, which includes all basic Go types. The formatted result is marked as non-raw (raw=false), meaning it should be sanitized for the output format.

Example usage:

// Format numbers with 2 decimal places
formatter := PrintfCellFormatter("%.2f")
str, raw, _ := formatter.FormatCell(ctx, view, 0, 0)
// For value 3.14159, str == "3.14", raw == false

// Format as percentage
percentFormatter := PrintfCellFormatter("%.0f%%")
str, _, _ := percentFormatter.FormatCell(ctx, view, 0, 0)
// For value 95, str == "95%"

func (PrintfCellFormatter) FormatCell

func (format PrintfCellFormatter) FormatCell(ctx context.Context, view View, row, col int) (str string, raw bool, err error)

FormatCell implements CellFormatter using fmt.Sprintf with the format string. The formatted result is always marked as non-raw (requiring sanitization). Never returns an error.

type PrintfRawCellFormatter

type PrintfRawCellFormatter string

PrintfRawCellFormatter implements CellFormatter by applying a Printf-style format string to cell values, similar to PrintfCellFormatter, but marks the result as raw output.

The key difference from PrintfCellFormatter is that this formatter marks its output as "raw" (raw=true), indicating the formatted string is already in the correct format for the target output and doesn't need sanitization. This is useful when the format string produces output that's already escaped or contains format-specific markup.

Example usage:

// Format as HTML with embedded tags (already properly escaped)
formatter := PrintfRawCellFormatter("<b>%s</b>")
str, raw, _ := formatter.FormatCell(ctx, view, 0, 0)
// For value "Title", str == "<b>Title</b>", raw == true
// The raw=true signals that no further HTML escaping is needed

func (PrintfRawCellFormatter) FormatCell

func (format PrintfRawCellFormatter) FormatCell(ctx context.Context, view View, row, col int) (str string, raw bool, err error)

FormatCell implements CellFormatter using fmt.Sprintf with the format string. The formatted result is always marked as raw (not requiring sanitization). Never returns an error.

type RawCellString

type RawCellString string

RawCellString is a constant string CellFormatter that ignores the cell value entirely and always returns its own string value, marked as raw output.

This formatter is useful for injecting fixed content (like HTML snippets, icons, or format-specific markup) into table cells regardless of the actual cell value. Since the output is marked as raw, it won't be sanitized by the output format.

Example usage:

// Always show an HTML checkbox, regardless of cell value
checkboxFormatter := RawCellString(`<input type="checkbox" checked>`)
str, raw, _ := checkboxFormatter.FormatCell(ctx, view, 0, 0)
// str == `<input type="checkbox" checked>`, raw == true

// Use as a constant marker
marker := RawCellString("✓")

func (RawCellString) FormatCell

func (rawStr RawCellString) FormatCell(ctx context.Context, view View, row, col int) (str string, raw bool, err error)

FormatCell implements CellFormatter by returning the constant string as raw output. The cell value is ignored. Never returns an error.

type RawStringIfTrue

type RawStringIfTrue string

RawStringIfTrue formats boolean cells by returning a specified string for true values and an empty string for false values, with the output marked as raw.

This is similar to StringIfTrue, but marks the output as raw, indicating it doesn't need sanitization. This is useful when the string contains format-specific markup like HTML tags or pre-escaped content. It panics if the cell value is not a bool.

Example usage:

// Show HTML icon for true, nothing for false
formatter := RawStringIfTrue(`<span class="icon-check"></span>`)
str, raw, _ := formatter.FormatCell(ctx, view, 0, 0)
// If cell value is true: str == `<span class="icon-check"></span>`, raw == true
// If cell value is false: str == "", raw == true

func (RawStringIfTrue) FormatCell

func (f RawStringIfTrue) FormatCell(ctx context.Context, view View, row, col int) (str string, raw bool, err error)

FormatCell implements CellFormatter for boolean values with raw output. Returns the string value marked as raw for true, empty string for false. Panics if the cell value is not a bool. Never returns an error.

type ReflectCellView

type ReflectCellView interface {
	View

	// ReflectCell returns the reflect.Value of the cell at the specified position.
	// This provides direct access to the underlying value's reflection metadata.
	//
	// The returned reflect.Value can be:
	//   - A valid value with the cell's actual type and value
	//   - A zero Value (from reflect.Value{}) if row/col are out of bounds
	//   - A reflect.Value containing nil if the cell is nil
	//
	// To check if a returned Value is valid, use:
	//   val := view.ReflectCell(row, col)
	//   if val.IsValid() { /* use val */ }
	//
	// Row and column indices are zero-based, same as Cell().
	ReflectCell(row, col int) reflect.Value
}

ReflectCellView extends the View interface with reflection-based cell access. This interface is useful when working with generic code that needs to inspect or manipulate cell values using Go's reflect package.

Use Cases

  • Type-safe data conversion via SmartAssign
  • Generic formatters that inspect value types
  • Building dynamic struct scanners
  • Implementing custom CellFormatter instances

Automatic Wrapping

Not all Views natively implement ReflectCellView. Use AsReflectCellView() to get a ReflectCellView from any View - it will either return the View directly (if it implements the interface) or wrap it automatically.

Performance Consideration

Views that natively implement ReflectCellView (like ReflectValuesView) are more efficient than wrapped views, as they avoid repeated reflection operations via reflect.ValueOf().

Example Usage

view := NewStringsView("Data", [][]string{{"123", "true"}}, []string{"Number", "Flag"})
reflectView := AsReflectCellView(view)

val := reflectView.ReflectCell(0, 0)  // reflect.Value containing "123"
if val.Kind() == reflect.String {
    fmt.Println(val.String())          // "123"
}

func AsReflectCellView

func AsReflectCellView(view View) ReflectCellView

AsReflectCellView converts any View to a ReflectCellView.

If the input View already implements ReflectCellView, it is returned as-is with no allocation. Otherwise, the View is wrapped in a lightweight adapter that implements ReflectCellView by calling reflect.ValueOf() on Cell() results.

This function never returns nil - it always returns a valid ReflectCellView.

Example Usage

var view View = NewStringsView(...)
reflectView := AsReflectCellView(view)
// reflectView.ReflectCell() is now available

Performance Note

The wrapper created for non-ReflectCellView inputs is very lightweight (just a struct embedding the original View), but calling ReflectCell() will perform reflection on each call. For performance-critical code with heavy reflection usage, prefer Views that natively implement ReflectCellView.

func DerefView

func DerefView(source View) ReflectCellView

DerefView creates a zero-copy decorator that automatically dereferences pointer values in a source View. This is particularly useful when working with Views of pointer types (e.g., []*Person) but you need to access the underlying values.

Use Cases

  • Converting Views of pointer slices ([]*T) to value access
  • Unwrapping optional/nullable values represented as pointers
  • Simplifying code that works with dereferenced values
  • Preparing data for formatters that expect concrete values

How It Works

DerefView wraps any View and calls reflect.Value.Elem() on every cell value, which dereferences pointers, interfaces, and other indirect types. The source View is automatically converted to ReflectCellView for efficient operation.

Performance

  • Zero data copying: Only dereferences on access
  • Reflection overhead: Each Cell() call performs reflection
  • Efficient for sparse access: Only dereferences accessed cells

Panics

DerefView will panic if:

  • A cell value cannot be dereferenced (e.g., non-pointer primitive)
  • A cell contains a nil pointer and you try to access it
  • The reflect.Value.Elem() operation is invalid for the value type

Example: Dereferencing Pointer Structs

type Person struct {
    Name string
    Age  int
}

// Source with pointer values
people := []*Person{
    {Name: "Alice", Age: 30},
    {Name: "Bob", Age: 25},
}
source := NewStructRowsView("People", people, nil, nil)
// source.Cell(0, 0) returns &Person{Name: "Alice", Age: 30}

// Deref view automatically unwraps pointers
deref := DerefView(source)
// deref.Cell(0, 0) returns Person{Name: "Alice", Age: 30}

Example: Unwrapping Interface Values

// View with cells wrapped in interface{}
source := NewAnyValuesView("Data", [][]any{
    {any(&Person{Name: "Alice"})},
})

deref := DerefView(source)
// Dereferences the pointer inside the interface
deref.Cell(0, 0) // Returns Person{Name: "Alice"}

Example: Composition with Other Decorators

// Combine dereferencing with filtering
source := NewStructRowsView("People", peoplePointers, nil, nil)
deref := DerefView(source)
filtered := &FilteredView{
    Source:    deref,
    RowLimit:  10,
}
// filtered provides first 10 dereferenced values

Safety Considerations

To safely use DerefView with potentially nil pointers, check values first:

val := source.Cell(row, col)
if val != nil {
    derefVal := deref.Cell(row, col)
    // Use derefVal
}

Or use reflection to check before dereferencing:

rv := deref.(ReflectCellView).ReflectCell(row, col)
if rv.IsValid() && !rv.IsNil() {
    // Safe to use rv.Interface()
}

Return Type

Returns a ReflectCellView (which embeds View), providing both Cell() and ReflectCell() methods for flexibility in value access.

func ExtraColsAnyValueFuncView

func ExtraColsAnyValueFuncView(left View, columns []string, anyValue func(row, col int) any) ReflectCellView

ExtraColsAnyValueFuncView creates a View that appends dynamically computed columns to a base View. The additional column values are generated on-demand by a user-provided function, enabling lazy evaluation and computed fields without data duplication.

Use Cases

  • Adding calculated fields: totals, percentages, derived metrics
  • Generating display columns: formatted strings, URLs, labels
  • Implementing virtual columns: values computed from other columns
  • Creating composite views: enrich data with external lookups

How It Works

The function receives relative column indices (0-based within the added columns) and row indices, returning the computed value for that position. The left View provides the base columns, and the function provides the extra columns on the right.

Performance

  • Zero base data copying: Left view data is never duplicated
  • Lazy evaluation: Values computed only when accessed
  • Function overhead: Each Cell() call invokes the function
  • Efficient for sparse access: Only computes accessed cells

Parameters

  • left: The base View to extend (can be nil for a pure computed view)
  • columns: Names for the computed columns (determines column count)
  • anyValue: Function that computes cell values (row, col) -> any

Column Indices

The col parameter passed to anyValue is 0-based within the new columns only:

left has 3 columns: ["A", "B", "C"]
Adding 2 columns: ["D", "E"]

Combined view columns: ["A", "B", "C", "D", "E"]
Cell(0, 3) -> anyValue(0, 0)  // col 3 is "D", passed as 0 to anyValue
Cell(0, 4) -> anyValue(0, 1)  // col 4 is "E", passed as 1 to anyValue

Example: Adding Calculated Columns

// Base data: product prices
products := NewStringsView("Products", [][]string{
    {"Widget", "100"},
    {"Gadget", "200"},
}, []string{"Name", "Price"})

// Add computed tax and total columns
withTax := ExtraColsAnyValueFuncView(products, []string{"Tax", "Total"},
    func(row, col int) any {
        priceStr := products.Cell(row, 1).(string)
        price, _ := strconv.ParseFloat(priceStr, 64)
        if col == 0 { // Tax column
            return price * 0.1
        }
        // Total column
        return price * 1.1
    })

// Result columns: ["Name", "Price", "Tax", "Total"]
// Row 0: ["Widget", "100", 10.0, 110.0]
// Row 1: ["Gadget", "200", 20.0, 220.0]

Example: Pure Computed View (No Left View)

// Generate a multiplication table without any base data
table := ExtraColsAnyValueFuncView(nil, []string{"A", "B", "Product"},
    func(row, col int) any {
        if col == 0 {
            return row + 1
        }
        if col == 1 {
            return row + 2
        }
        return (row + 1) * (row + 2)
    })

// When NumRows() is called, returns 0 because left is nil
// Typically you'd wrap this in a FilteredView to set row count

Example: Lookups and External Data

userData := loadUserView() // id, name

withStatus := ExtraColsAnyValueFuncView(userData, []string{"Status", "LastSeen"},
    func(row, col int) any {
        userID := userData.Cell(row, 0).(int)
        if col == 0 {
            return statusCache.Get(userID)
        }
        return lastSeenCache.Get(userID)
    })

Example: Conditional Formatting

scores := loadScoresView() // name, score

withGrade := ExtraColsAnyValueFuncView(scores, []string{"Grade"},
    func(row, col int) any {
        score := scores.Cell(row, 1).(int)
        if score >= 90 { return "A" }
        if score >= 80 { return "B" }
        if score >= 70 { return "C" }
        return "F"
    })

Edge Cases

  • If left is nil, NumRows() returns 0 (function never called)
  • Empty columns slice is valid (adds no columns)
  • Function can return nil for any cell
  • Out-of-bounds access returns nil (function not called)

Composition

Can be chained to add multiple sets of computed columns:

base := loadDataView()
step1 := ExtraColsAnyValueFuncView(base, []string{"A"}, funcA)
step2 := ExtraColsAnyValueFuncView(step1, []string{"B", "C"}, funcBC)
// step2 has base columns plus A, B, C

Return Type

Returns a ReflectCellView, providing both Cell() and ReflectCell() access. This enables efficient integration with reflection-based operations.

func ExtraColsReflectValueFuncView

func ExtraColsReflectValueFuncView(left View, columns []string, reflectValue func(row, col int) reflect.Value) ReflectCellView

ExtraColsReflectValueFuncView creates a View that appends dynamically computed columns using a reflection-based value function. This is similar to ExtraColsAnyValueFuncView but provides reflect.Value access for efficiency when working with reflection.

When To Use

Use this instead of ExtraColsAnyValueFuncView when:

  • You're already working with reflect.Value in your code
  • You need to avoid boxing/unboxing via Interface()
  • You want to return invalid reflect.Value to represent nil
  • Performance-critical reflection operations

Key Difference

The function receives and returns reflect.Value instead of any:

  • More efficient: Avoids Interface() conversions
  • More control: Can return invalid reflect.Values for nil
  • More complex: Requires reflection knowledge

Parameters

  • left: The base View to extend (can be nil)
  • columns: Names for the computed columns
  • reflectValue: Function that computes cell values (row, col) -> reflect.Value

Example: Efficient Reflection-Based Computation

type Product struct {
    Name  string
    Price float64
}

products := NewStructRowsView("Products", productSlice, nil, nil)

withFormatted := ExtraColsReflectValueFuncView(products, []string{"FormattedPrice"},
    func(row, col int) reflect.Value {
        rv := products.(ReflectCellView).ReflectCell(row, 1) // Price field
        price := rv.Float()
        formatted := fmt.Sprintf("$%.2f", price)
        return reflect.ValueOf(formatted)
    })

Example: Returning nil as Invalid Value

withOptional := ExtraColsReflectValueFuncView(base, []string{"Optional"},
    func(row, col int) reflect.Value {
        if shouldBeNil(row) {
            return reflect.Value{} // Invalid value represents nil
        }
        return reflect.ValueOf(computeValue(row))
    })

Example: Type Conversion with Reflection

withConverted := ExtraColsReflectValueFuncView(base, []string{"AsString"},
    func(row, col int) reflect.Value {
        val := base.(ReflectCellView).ReflectCell(row, 0)
        // Use reflection to convert to string
        str := fmt.Sprint(val.Interface())
        return reflect.ValueOf(str)
    })

Invalid Reflect Values

If reflectValue returns an invalid reflect.Value (reflect.Value{}), the Cell() method will return nil. This is the recommended way to represent null/missing values.

Performance Advantages

When implementing complex reflection-based transformations:

// Less efficient: Using ExtraColsAnyValueFuncView
ExtraColsAnyValueFuncView(v, cols, func(r, c int) any {
    rv := v.(ReflectCellView).ReflectCell(r, c)
    // ... reflection operations ...
    return rv.Interface() // Extra boxing
})

// More efficient: Using ExtraColsReflectValueFuncView
ExtraColsReflectValueFuncView(v, cols, func(r, c int) reflect.Value {
    rv := v.(ReflectCellView).ReflectCell(r, c)
    // ... reflection operations ...
    return rv // No boxing
})

Return Type

Returns a ReflectCellView, providing efficient reflection-based access through both Cell() and ReflectCell() methods.

type ReflectTypeCellFormatter

type ReflectTypeCellFormatter struct {
	// Types maps exact reflect.Type to their CellFormatters.
	// This has the highest priority in the matching hierarchy.
	// Example: reflect.TypeOf(time.Time{}) -> LayoutFormatter("2006-01-02")
	Types map[reflect.Type]CellFormatter

	// InterfaceTypes maps interface types to their CellFormatters.
	// Cell types are checked if they implement these interfaces.
	// Example: reflect.TypeOf((*fmt.Stringer)(nil)).Elem() -> custom formatter
	InterfaceTypes map[reflect.Type]CellFormatter

	// Kinds maps reflect.Kind to their CellFormatters.
	// This provides fallback formatting for broad categories of types.
	// Example: reflect.Int -> PrintfCellFormatter("%d")
	Kinds map[reflect.Kind]CellFormatter

	// Default is the fallback formatter used when no type, interface, or kind matches.
	// If nil and no match is found, errors.ErrUnsupported is returned.
	Default CellFormatter
}

ReflectTypeCellFormatter is a sophisticated type-based routing formatter that selects the appropriate CellFormatter based on the reflected type, interface, or kind of a cell value.

This formatter implements a hierarchical matching strategy:

  1. Exact type match (Types map) - highest priority
  2. Interface type match (InterfaceTypes map) - checks if type implements interface
  3. Kind match (Kinds map) - matches reflect.Kind (int, string, struct, etc.)
  4. Pointer dereferencing - if pointer type has no match, checks dereferenced type
  5. Default formatter - fallback for unmatched types

The formatter returns errors.ErrUnsupported if no matching formatter is found and no Default is configured, allowing it to be used in formatter chains.

Design pattern: This type uses an immutable builder pattern where all With* methods return a new instance, allowing for safe concurrent usage and compositional configuration.

Example usage:

formatter := NewReflectTypeCellFormatter().
    WithTypeFormatter(reflect.TypeOf(time.Time{}), LayoutFormatter("2006-01-02")).
    WithKindFormatter(reflect.Int, PrintfCellFormatter("%d")).
    WithDefaultFormatter(SprintCellFormatter(false))

str, raw, err := formatter.FormatCell(ctx, view, 0, 0)
// Automatically routes to the correct formatter based on cell type

func NewReflectTypeCellFormatter

func NewReflectTypeCellFormatter() *ReflectTypeCellFormatter

NewReflectTypeCellFormatter creates a new empty ReflectTypeCellFormatter. Use the With* methods to configure type mappings.

Example:

formatter := NewReflectTypeCellFormatter().
    WithTypeFormatter(reflect.TypeOf(time.Time{}), LayoutFormatter("2006-01-02")).
    WithKindFormatter(reflect.Float64, PrintfCellFormatter("%.2f"))

func (*ReflectTypeCellFormatter) FormatCell

func (f *ReflectTypeCellFormatter) FormatCell(ctx context.Context, view View, row, col int) (str string, raw bool, err error)

FormatCell implements CellFormatter by routing to the appropriate formatter based on the cell value's reflected type.

Matching algorithm:

  1. Check Types map for exact type match
  2. Check InterfaceTypes map for interface implementations
  3. Check Kinds map for reflect.Kind match
  4. If pointer type and not matched, dereference and try steps 1-3 on dereferenced type
  5. Use Default formatter if configured
  6. Return errors.ErrUnsupported if no match found

At each step, if a formatter returns errors.ErrUnsupported, the algorithm continues to the next step. Any other error is returned immediately.

Parameters:

  • ctx: Context for cancellation and timeout
  • view: The table view containing the cell
  • row: Zero-based row index
  • col: Zero-based column index

Returns:

  • str: The formatted string from the matched formatter
  • raw: Whether the output is raw (from the matched formatter)
  • err: Error from formatter, or errors.ErrUnsupported if no match

func (*ReflectTypeCellFormatter) WithDefaultFormatter

func (f *ReflectTypeCellFormatter) WithDefaultFormatter(fmt CellFormatter) *ReflectTypeCellFormatter

WithDefaultFormatter returns a new ReflectTypeCellFormatter with a default formatter set. This creates a copy, leaving the original unchanged.

The default formatter is used as the ultimate fallback when no type, interface, or kind formatters match. If no default is set and no formatters match, errors.ErrUnsupported is returned, allowing the formatter to be used in chains.

Parameters:

  • fmt: The CellFormatter to use as default

Returns:

  • A new ReflectTypeCellFormatter with the default formatter set

Example:

// Use fmt.Sprint as fallback for unmatched types
formatter := base.WithDefaultFormatter(SprintCellFormatter(false))

// Use a custom fallback that logs unmatched types
formatter = base.WithDefaultFormatter(
    CellFormatterFunc(func(ctx context.Context, view View, row, col int) (string, bool, error) {
        val := view.Cell(row, col)
        log.Printf("unmatched type: %T", val)
        return fmt.Sprint(val), false, nil
    }),
)

func (*ReflectTypeCellFormatter) WithInterfaceTypeFormatter

func (f *ReflectTypeCellFormatter) WithInterfaceTypeFormatter(typ reflect.Type, fmt CellFormatter) *ReflectTypeCellFormatter

WithInterfaceTypeFormatter returns a new ReflectTypeCellFormatter with an interface type formatter added. This creates a copy, leaving the original unchanged.

Interface type formatters match any cell value whose type implements the specified interface. This is checked after exact type matching but before kind matching.

Parameters:

  • typ: The interface type to match (use reflect.TypeOf((*InterfaceName)(nil)).Elem())
  • fmt: The CellFormatter to use for types implementing this interface

Returns:

  • A new ReflectTypeCellFormatter with the interface formatter added

Example:

// Format any type implementing fmt.Stringer with its String() method
stringerType := reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
formatter := base.WithInterfaceTypeFormatter(
    stringerType,
    CellFormatterFunc(func(ctx context.Context, view View, row, col int) (string, bool, error) {
        return view.Cell(row, col).(fmt.Stringer).String(), false, nil
    }),
)

// Format any type implementing a custom interface
formatterInterface := reflect.TypeOf((*CustomFormatter)(nil)).Elem()
formatter = formatter.WithInterfaceTypeFormatter(formatterInterface, myFormatter)

func (*ReflectTypeCellFormatter) WithKindFormatter

WithKindFormatter returns a new ReflectTypeCellFormatter with a kind formatter added. This creates a copy, leaving the original unchanged.

Kind formatters provide broad category matching for types with the same reflect.Kind (int, string, struct, etc.). This is checked after exact type and interface matching, providing a coarse-grained fallback before the default formatter.

Parameters:

  • kind: The reflect.Kind to match (e.g., reflect.Int, reflect.String)
  • fmt: The CellFormatter to use for this kind

Returns:

  • A new ReflectTypeCellFormatter with the kind formatter added

Example:

// Format all integer types with zero-padding
formatter := base.WithKindFormatter(
    reflect.Int,
    PrintfCellFormatter("%05d"),
)

// Format all float types with 2 decimal places
formatter = formatter.WithKindFormatter(
    reflect.Float64,
    PrintfCellFormatter("%.2f"),
)

// Format all struct types with JSON
formatter = formatter.WithKindFormatter(
    reflect.Struct,
    customJSONFormatter,
)

func (*ReflectTypeCellFormatter) WithTypeFormatter

WithTypeFormatter returns a new ReflectTypeCellFormatter with an exact type formatter added. This creates a copy of the formatter, leaving the original unchanged (immutable builder pattern).

The type formatter has the highest priority in the matching hierarchy and will be tried before interface, kind, or default formatters.

Parameters:

  • typ: The exact reflect.Type to match
  • fmt: The CellFormatter to use for this type

Returns:

  • A new ReflectTypeCellFormatter with the type formatter added

Example:

// Format time.Time values with custom layout
formatter := base.WithTypeFormatter(
    reflect.TypeOf(time.Time{}),
    LayoutFormatter("2006-01-02"),
)

// Format custom type with specific formatter
formatter = formatter.WithTypeFormatter(
    reflect.TypeOf(MyCustomType{}),
    PrintfCellFormatter("custom: %v"),
)

type ReflectValuesView

type ReflectValuesView struct {
	// Tit is the title of this view, returned by the Title() method.
	Tit string

	// Cols contains the column names defining both the column headers
	// and the number of columns in this view.
	Cols []string

	// Rows contains the data rows, where each row is a slice of reflect.Value.
	// Each cell is stored as a reflect.Value for introspection capabilities.
	Rows [][]reflect.Value
}

ReflectValuesView is a View implementation that stores cells as reflect.Value.

This view type is designed for advanced use cases requiring reflection-based operations on cell data. Each cell is stored as a reflect.Value, providing access to type information and reflection capabilities while maintaining the View interface.

The underlying data structure uses [][]reflect.Value, where each cell can be introspected, compared, and manipulated using Go's reflection package. This is particularly useful for generic operations, type discovery, and dynamic value manipulation.

ReflectValuesView implements both View and ReflectCellView interfaces, providing both standard Cell() and specialized ReflectCell() methods.

Performance characteristics:

  • Direct memory access: O(1) for cell retrieval
  • Reflection overhead for value operations
  • Higher memory usage than AnyValuesView due to reflect.Value storage
  • Efficient for reflection-heavy operations

When to use ReflectValuesView:

  • Need type introspection on cell values
  • Implementing generic algorithms that work with arbitrary types
  • Converting between different representation forms
  • Caching reflection results for performance
  • Building dynamic view transformations
  • Working with code generation or analysis tools

Example usage:

source := retable.NewStringsView("Data", data, "A", "B")
reflected, _ := retable.NewReflectValuesViewFrom(source)

// Access via standard interface
value := reflected.Cell(0, 0)

// Access via reflection interface
reflectVal := reflected.ReflectCell(0, 0)
fmt.Printf("Type: %s, Kind: %s\n", reflectVal.Type(), reflectVal.Kind())

Thread safety: Not thread-safe. External synchronization required for concurrent access.

func NewReflectValuesViewFrom

func NewReflectValuesViewFrom(source View) (*ReflectValuesView, error)

NewReflectValuesViewFrom creates a ReflectValuesView by reading and caching all cells from the source View as reflect.Value instances.

This function materializes all data from the source view into memory, converting each cell to a reflect.Value. If the source implements ReflectCellView, it uses ReflectCell() directly; otherwise, it wraps Cell() results with reflect.ValueOf().

This is useful for:

  • Caching reflection results for repeated introspection
  • Converting any view to a reflection-based representation
  • Preparing data for reflection-heavy operations
  • Type analysis and schema discovery

Parameters:

  • source: The View to read data from

Returns:

  • A new ReflectValuesView containing all data as reflect.Values
  • An error if source is nil

Example:

original := retable.NewStringsView("Data", rows, "A", "B", "C")
reflected, err := retable.NewReflectValuesViewFrom(original)
if err != nil {
    log.Fatal(err)
}
// All cells are now stored as reflect.Value for introspection
for row := 0; row < reflected.NumRows(); row++ {
    for col := range reflected.Columns() {
        val := reflected.ReflectCell(row, col)
        fmt.Printf("Type: %s\n", val.Type())
    }
}

Time complexity: O(rows * cols) for copying all cells Space complexity: O(rows * cols) for storing all reflect.Values

func (*ReflectValuesView) Cell

func (view *ReflectValuesView) Cell(row, col int) any

Cell returns the interface value at the specified row and column indices.

This method extracts the underlying value from the reflect.Value using Interface(). If you need the reflect.Value itself for introspection, use ReflectCell() instead.

Parameters:

  • row: Zero-based row index (0 to NumRows()-1)
  • col: Zero-based column index (0 to len(Columns())-1)

Returns:

  • The cell's underlying value (via reflect.Value.Interface()) if indices are valid
  • nil if row or col indices are out of bounds

Time complexity: O(1)

func (*ReflectValuesView) Columns

func (view *ReflectValuesView) Columns() []string

Columns returns the column names of this view.

func (*ReflectValuesView) NumRows

func (view *ReflectValuesView) NumRows() int

NumRows returns the number of data rows in this view.

func (*ReflectValuesView) ReflectCell

func (view *ReflectValuesView) ReflectCell(row, col int) reflect.Value

ReflectCell returns the reflect.Value at the specified row and column indices.

This method provides direct access to the stored reflect.Value, allowing for type introspection, comparison, and other reflection operations without extracting the underlying interface value.

Parameters:

  • row: Zero-based row index (0 to NumRows()-1)
  • col: Zero-based column index (0 to len(Columns())-1)

Returns:

  • The reflect.Value stored at the cell if indices are valid
  • An invalid reflect.Value (reflect.Value{}) if indices are out of bounds

To check if the returned value is valid, use reflectVal.IsValid().

Example:

val := view.ReflectCell(0, 0)
if val.IsValid() {
    fmt.Printf("Type: %s, Kind: %s\n", val.Type(), val.Kind())
    if val.Kind() == reflect.Int {
        fmt.Printf("Int value: %d\n", val.Int())
    }
}

Time complexity: O(1)

func (*ReflectValuesView) Title

func (view *ReflectValuesView) Title() string

Title returns the title of this view.

type Scanner

type Scanner interface {
	// ScanString parses a string value into the destination reflect.Value.
	//
	// The dest parameter must be settable (obtained from a pointer's Elem()).
	// The scanner should check dest's type and use the appropriate parser method
	// to convert the string.
	//
	// Returns errors.ErrUnsupported if the scanner doesn't support the dest type,
	// allowing scanner chains. Other errors indicate actual parsing failures.
	//
	// Parameters:
	//   - dest: The settable reflect.Value to write the parsed value into
	//   - str: The string to parse
	//   - parser: The Parser to use for primitive type conversions
	//
	// Returns:
	//   - error: Parsing error, or errors.ErrUnsupported if type not supported
	ScanString(dest reflect.Value, str string, parser Parser) error
}

Scanner is the interface for parsing string values into Go values using reflection. This is the inverse operation of formatting - it converts string representations back into typed Go values.

Scanners work in conjunction with Parsers to handle the actual string-to-value conversion. The Scanner is responsible for orchestrating the conversion, while the Parser provides the primitive parsing operations (int, float, bool, time, etc.).

The Scanner interface operates at a lower level than the table system, working directly with reflect.Value. This makes it reusable across different contexts beyond just table cell parsing.

Design pattern: Scanners should check the dest type and call the appropriate Parser method to convert the string into the target type. For complex types, scanners may need to perform additional logic (e.g., splitting strings, handling null values, type conversions).

Example usage:

scanner := ScannerFunc(func(dest reflect.Value, str string, parser Parser) error {
    if dest.Kind() == reflect.Int {
        i, err := parser.ParseInt(str)
        if err != nil {
            return err
        }
        dest.SetInt(i)
        return nil
    }
    return errors.ErrUnsupported
})

var result int
destValue := reflect.ValueOf(&result).Elem()
err := scanner.ScanString(destValue, "42", parser)
// result == 42

type ScannerFunc

type ScannerFunc func(dest reflect.Value, str string, parser Parser) error

ScannerFunc is a function type that implements the Scanner interface, allowing plain functions to be used as Scanners.

This adapter type follows the common Go pattern of defining a function type that implements an interface (similar to http.HandlerFunc), making it easy to create inline scanners without defining separate types.

Example:

var scanner Scanner = ScannerFunc(func(dest reflect.Value, str string, parser Parser) error {
    if dest.Type() == reflect.TypeOf(time.Time{}) {
        t, err := parser.ParseTime(str)
        if err != nil {
            return err
        }
        dest.Set(reflect.ValueOf(t))
        return nil
    }
    return errors.ErrUnsupported
})

func (ScannerFunc) ScanString

func (f ScannerFunc) ScanString(dest reflect.Value, str string, parser Parser) error

ScanString implements the Scanner interface by calling the function itself.

type SingleReflectValueView

type SingleReflectValueView struct {
	// Tit is the title of this view, usually inherited from the source.
	Tit string

	// Col is the name of the single column in this view.
	Col string

	// Val is the reflect.Value stored in the single cell.
	Val reflect.Value
}

SingleReflectValueView is a View implementation containing exactly one cell stored as a reflect.Value.

This specialized view type represents a 1x1 table (single row, single column) where the cell value is stored as a reflect.Value. It implements both View and ReflectCellView interfaces.

SingleReflectValueView is useful for:

  • Extracting and wrapping a single cell from a larger view
  • Representing scalar values as a minimal view
  • Type introspection on individual values
  • Building view hierarchies where leaf nodes are single values

Performance characteristics:

  • Minimal memory footprint (one value + metadata)
  • O(1) access time
  • No allocations for repeated access

Example usage:

// Extract a single cell from another view
source := retable.NewStringsView("Data", rows, "A", "B", "C")
singleCell := retable.NewSingleReflectValueView(source, 0, 1)
fmt.Println(singleCell.Cell(0, 0)) // Value from source row 0, col 1

Thread safety: Immutable after creation, safe for concurrent reads.

func NewSingleReflectValueView

func NewSingleReflectValueView(source View, row, col int) *SingleReflectValueView

NewSingleReflectValueView creates a SingleReflectValueView by extracting a single cell from the source View at the specified row and column.

The function reads one cell value from the source and wraps it in a minimal 1x1 view. The title is inherited from the source, and the column name is taken from the source's column at the specified index.

Parameters:

  • source: The View to extract the cell from
  • row: Zero-based row index in the source view
  • col: Zero-based column index in the source view

Returns a new SingleReflectValueView containing the single cell value. If source is nil or indices are invalid, returns a view with only the title set (and an invalid reflect.Value).

Example:

source := retable.NewStringsView(
    "Products",
    [][]string{
        {"Widget", "9.99"},
        {"Gadget", "19.99"},
    },
    "Name", "Price",
)
priceView := retable.NewSingleReflectValueView(source, 1, 1)
fmt.Println(priceView.Cell(0, 0)) // Output: 19.99

func (*SingleReflectValueView) Cell

func (view *SingleReflectValueView) Cell(row, col int) any

Cell returns the underlying interface value of the single cell.

Parameters:

  • row: Must be 0 (only one row exists)
  • col: Must be 0 (only one column exists)

Returns:

  • The underlying value extracted via Val.Interface() if row and col are both 0
  • nil if row is not 0 or col is not 0

Time complexity: O(1)

func (*SingleReflectValueView) Columns

func (view *SingleReflectValueView) Columns() []string

Columns returns a slice containing the single column name.

func (*SingleReflectValueView) NumRows

func (view *SingleReflectValueView) NumRows() int

NumRows always returns 1 for SingleReflectValueView.

func (*SingleReflectValueView) ReflectCell

func (view *SingleReflectValueView) ReflectCell(row, col int) reflect.Value

ReflectCell returns the reflect.Value of the single cell.

Parameters:

  • row: Must be 0 (only one row exists)
  • col: Must be 0 (only one column exists)

Returns:

  • The reflect.Value stored in Val if row and col are both 0
  • An invalid reflect.Value (reflect.Value{}) if row is not 0 or col is not 0

Time complexity: O(1)

func (*SingleReflectValueView) Title

func (view *SingleReflectValueView) Title() string

Title returns the title of this view.

type SprintFormatter

type SprintFormatter struct{}

SprintFormatter is a universal Formatter that uses fmt.Sprint to format any value. This formatter never returns an error and accepts all value types.

It extracts the actual Go value using reflect.Value.Interface() and formats it using fmt.Sprint, which uses the value's String() method if available, or falls back to the default Go formatting.

Example:

var f Formatter = SprintFormatter{}
str, _ := f.Format(reflect.ValueOf(time.Now()))
// str contains the time formatted by time.Time's String() method

func (SprintFormatter) Format

func (SprintFormatter) Format(v reflect.Value) (string, error)

Format implements Formatter by using fmt.Sprint on the underlying Go value. Never returns an error.

type StringIfTrue

type StringIfTrue string

StringIfTrue formats boolean cells by returning a specified string for true values and an empty string for false values. The output is marked as non-raw.

This formatter is useful for creating checkmarks, labels, or indicators that only appear when a condition is true. It panics if the cell value is not a bool.

Example usage:

// Show checkmark for true, nothing for false
formatter := StringIfTrue("✓")
str, raw, _ := formatter.FormatCell(ctx, view, 0, 0)
// If cell value is true: str == "✓", raw == false
// If cell value is false: str == "", raw == false

// Show "Active" for true, nothing for false
statusFormatter := StringIfTrue("Active")

func (StringIfTrue) FormatCell

func (f StringIfTrue) FormatCell(ctx context.Context, view View, row, col int) (str string, raw bool, err error)

FormatCell implements CellFormatter for boolean values. Returns the string value for true, empty string for false. Panics if the cell value is not a bool. Never returns an error.

type StringParser

type StringParser struct {
	// TrueStrings lists all strings that should be parsed as boolean true.
	// Default includes: "true", "True", "TRUE", "yes", "Yes", "YES", "1"
	TrueStrings []string `json:"trueStrings"`

	// FalseStrings lists all strings that should be parsed as boolean false.
	// Default includes: "false", "False", "FALSE", "no", "No", "NO", "0"
	FalseStrings []string `json:"falseStrings"`

	// NilStrings lists all strings that represent nil/null values.
	// Default includes: "", "nil", "<nil>", "null", "NULL"
	// This is used by higher-level scanning logic to detect null values.
	NilStrings []string `json:"nilStrings"`

	// TimeFormats lists time layout strings to try when parsing time values.
	// Formats are tried in order until one succeeds.
	// Default includes RFC3339, ISO dates, and several common formats.
	TimeFormats []string `json:"timeFormats"`
}

StringParser is a configurable implementation of the Parser interface that handles string-to-value conversions with support for multiple conventions and formats.

Key features:

  • Configurable boolean string representations (e.g., "yes"/"no", "1"/"0")
  • Configurable nil/null string representations
  • Multiple time format attempts for flexible time parsing
  • Locale-aware float parsing (e.g., handling comma decimal separators)

The StringParser uses standard Go parsing functions (strconv, time.Parse) internally, but adds flexibility through configuration and fallback strategies.

Example usage:

parser := NewStringParser()
// Parser comes pre-configured with sensible defaults

// Customize boolean parsing
parser.TrueStrings = append(parser.TrueStrings, "enabled", "on")
parser.FalseStrings = append(parser.FalseStrings, "disabled", "off")

// Add custom time formats
parser.TimeFormats = append([]string{"01/02/2006"}, parser.TimeFormats...)

// Parse with custom configuration
b, _ := parser.ParseBool("enabled") // true
t, _ := parser.ParseTime("03/15/2024") // uses custom format

func NewStringParser

func NewStringParser() *StringParser

NewStringParser creates a new StringParser with sensible default configurations.

Default configurations:

  • TrueStrings: "true", "True", "TRUE", "yes", "Yes", "YES", "1"
  • FalseStrings: "false", "False", "FALSE", "no", "No", "NO", "0"
  • NilStrings: "", "nil", "<nil>", "null", "NULL"
  • TimeFormats: Comprehensive list of common formats (RFC3339, ISO, etc.)

The returned parser can be used as-is or customized by modifying its fields.

Returns:

  • A new StringParser with default configurations

Example:

parser := NewStringParser()
// Use with defaults
value, _ := parser.ParseBool("yes") // true

// Or customize
parser.TrueStrings = append(parser.TrueStrings, "ja", "oui", "si")

func (*StringParser) ParseBool

func (p *StringParser) ParseBool(str string) (bool, error)

ParseBool parses a string into a boolean value based on the configured TrueStrings and FalseStrings lists.

The parser checks if the string exactly matches (case-sensitive) any string in TrueStrings or FalseStrings. If no match is found, an error is returned.

Parameters:

  • str: The string to parse

Returns:

  • bool: The parsed boolean value
  • error: Error if string doesn't match any configured true/false string

Example:

parser := NewStringParser()
b, _ := parser.ParseBool("true")  // true, nil
b, _ := parser.ParseBool("yes")   // true, nil
b, _ := parser.ParseBool("1")     // true, nil
b, _ := parser.ParseBool("false") // false, nil
b, _ := parser.ParseBool("no")    // false, nil
b, err := parser.ParseBool("maybe") // false, error

func (*StringParser) ParseDuration

func (p *StringParser) ParseDuration(str string) (time.Duration, error)

ParseDuration parses a string into a time.Duration value. Uses Go's standard time.ParseDuration which accepts strings like "1h30m", "5s", "100ms".

Valid units: "ns" (nanoseconds), "us" (microseconds), "ms" (milliseconds), "s" (seconds), "m" (minutes), "h" (hours).

Parameters:

  • str: The string to parse (e.g., "1h30m", "5s")

Returns:

  • time.Duration: The parsed duration value
  • error: Error if string is not a valid duration format

Example:

d, _ := parser.ParseDuration("1h30m")    // 1 hour 30 minutes
d, _ := parser.ParseDuration("5s")       // 5 seconds
d, _ := parser.ParseDuration("100ms")    // 100 milliseconds
d, _ := parser.ParseDuration("2h45m30s") // 2 hours 45 minutes 30 seconds
_, err := parser.ParseDuration("invalid") // error

func (*StringParser) ParseFloat

func (p *StringParser) ParseFloat(str string) (float64, error)

ParseFloat parses a string into a 64-bit floating point number with locale awareness. This method tries multiple strategies to handle different number formats:

  1. Standard parsing using strconv.ParseFloat (handles "123.45")
  2. If that fails, tries handling comma as decimal separator ("123,45" -> "123.45")
  3. More strategies could be added for thousand separators, etc.

This flexibility is important for parsing numbers from different locales or user input.

Parameters:

  • str: The string to parse

Returns:

  • float64: The parsed floating point value
  • error: Parsing error if the string is not a valid number in any recognized format

Example:

f, _ := parser.ParseFloat("3.14")    // 3.14 (standard)
f, _ := parser.ParseFloat("3,14")    // 3.14 (comma decimal)
f, _ := parser.ParseFloat("-2.5e10") // -2.5e10 (scientific notation)

func (*StringParser) ParseInt

func (p *StringParser) ParseInt(str string) (int64, error)

ParseInt parses a string into a 64-bit signed integer using base 10. Uses strconv.ParseInt internally.

Parameters:

  • str: The string to parse

Returns:

  • int64: The parsed integer value
  • error: Parsing error if the string is not a valid integer

Example:

i, err := parser.ParseInt("42")    // 42, nil
i, err := parser.ParseInt("-123")  // -123, nil
i, err := parser.ParseInt("abc")   // 0, error

func (*StringParser) ParseTime

func (p *StringParser) ParseTime(str string) (time.Time, error)

ParseTime parses a string into a time.Time value by trying multiple time formats.

The parser tries each format in the TimeFormats slice in order until one succeeds. This allows parsing of various time string formats without needing to know the exact format in advance.

The default TimeFormats include RFC3339, ISO 8601, common date formats, and several others. You can customize the TimeFormats slice to add or prioritize specific formats.

Parameters:

  • str: The string to parse

Returns:

  • time.Time: The parsed time value
  • error: Error if string doesn't match any configured time format

Example:

parser := NewStringParser()
t, _ := parser.ParseTime("2024-03-15T14:30:00Z")    // RFC3339
t, _ := parser.ParseTime("2024-03-15")              // ISO date
t, _ := parser.ParseTime("15.03.2024")              // German date format
t, _ := parser.ParseTime("2024-03-15 14:30:00")     // DateTime format
_, err := parser.ParseTime("not a date")            // error

func (*StringParser) ParseUint

func (p *StringParser) ParseUint(str string) (uint64, error)

ParseUint parses a string into a 64-bit unsigned integer using base 10. Uses strconv.ParseUint internally.

Parameters:

  • str: The string to parse

Returns:

  • uint64: The parsed unsigned integer value
  • error: Parsing error if the string is not a valid unsigned integer

Example:

u, err := parser.ParseUint("42")    // 42, nil
u, err := parser.ParseUint("0")     // 0, nil
u, err := parser.ParseUint("-1")    // 0, error (negative not allowed)

type StringsView

type StringsView struct {
	// Tit is the title of this view, returned by the Title() method.
	Tit string

	// Cols contains the column names defining both the column headers
	// and the number of columns in this view.
	Cols []string

	// Rows contains the data rows, where each row is a slice of strings.
	// Rows can have fewer elements than len(Cols) for sparse data support.
	Rows [][]string
}

StringsView is a View implementation that uses strings as cell values. It is the most straightforward and commonly used view type for tabular string data.

The Cols field defines the column names and determines the number of columns. Each element in Rows represents a row of data, where each row is a slice of strings.

StringsView supports sparse data: a row within Rows can have fewer slice elements than Cols, in which case empty strings ("") are returned as values for missing cells. This allows for efficient storage of tables with many empty cells.

Performance characteristics:

  • Direct memory access: O(1) for cell retrieval
  • Memory efficient for string-only data
  • No type conversion overhead

When to use StringsView:

  • Working with CSV, TSV, or other text-based tabular data
  • All cell values are strings or can be represented as strings
  • Need simple, fast access to string data
  • Memory efficiency is important for large string tables

Example usage:

view := retable.NewStringsView(
    "Products",
    [][]string{
        {"ID", "Name", "Price"},
        {"1", "Widget", "9.99"},
        {"2", "Gadget", "19.99"},
    },
)
fmt.Println(view.Cell(0, 1)) // Output: Widget

Sparse data example:

view := &retable.StringsView{
    Cols: []string{"A", "B", "C"},
    Rows: [][]string{
        {"1", "2", "3"},
        {"4"}, // Missing B and C - will return empty strings
    },
}
fmt.Println(view.Cell(1, 2)) // Output: "" (empty string)

func NewStringsView

func NewStringsView(title string, rows [][]string, cols ...string) *StringsView

NewStringsView creates a new StringsView with flexible column specification.

The function accepts column names in two ways:

  1. Explicit columns: Pass column names via the cols variadic parameter
  2. Header row: If no cols are provided and rows is not empty, the first row is used as column names and removed from the data rows

All column names have leading and trailing whitespace trimmed automatically.

Parameters:

  • title: The title for this view (can be empty string)
  • rows: The data rows, or rows including a header row if cols is empty
  • cols: Optional column names. If empty, first row is used as header

Returns a new StringsView with the specified title, columns, and data rows.

Example with explicit columns:

view := retable.NewStringsView(
    "Users",
    [][]string{
        {"alice", "30"},
        {"bob", "25"},
    },
    "Name", "Age",
)

Example with header row:

view := retable.NewStringsView(
    "Users",
    [][]string{
        {"Name", "Age"}, // This becomes the header
        {"alice", "30"},
        {"bob", "25"},
    },
)

func (*StringsView) Cell

func (view *StringsView) Cell(row, col int) any

Cell returns the value at the specified row and column indices.

For StringsView, Cell returns:

  • The string value at [row][col] if the cell exists
  • An empty string "" if the row exists but has fewer columns (sparse data)
  • nil if row or col indices are out of bounds

Parameters:

  • row: Zero-based row index (0 to NumRows()-1)
  • col: Zero-based column index (0 to len(Columns())-1)

Returns the cell value as any, which will be either string, "", or nil.

Time complexity: O(1)

func (*StringsView) Columns

func (view *StringsView) Columns() []string

Columns returns the column names of this view.

func (*StringsView) NumRows

func (view *StringsView) NumRows() int

NumRows returns the number of data rows in this view.

func (*StringsView) Title

func (view *StringsView) Title() string

Title returns the title of this view.

type StringsViewer

type StringsViewer struct {
	// Cols specifies the column names to use when creating Views.
	// Can be empty or nil, in which case column names will be handled
	// by the underlying View implementation (NewStringsView).
	Cols []string
}

StringsViewer is a Viewer implementation that creates Views from two-dimensional string slices ([][]string).

This is one of the simplest Viewer implementations and is useful when working with raw string data, such as CSV files, database query results converted to strings, or any tabular data represented as strings.

Column Names:

The Cols field can be used to specify column names for the Views. If provided, these names will be used as the column headers in the resulting View. If Cols is empty or nil, NewStringsView will use default column names or the table may be treated as having no headers.

The behavior when Cols is provided:

  • If Cols has fewer entries than columns in the data, remaining columns will use default names
  • If Cols has more entries than columns in the data, extra names are ignored

Example:

// Create a StringsViewer with column names
viewer := StringsViewer{
    Cols: []string{"Name", "Age", "City"},
}

// Use it to create a View from string data
data := [][]string{
    {"Alice", "30", "NYC"},
    {"Bob", "25", "LA"},
}
view, err := viewer.NewView("People", data)

// Create a StringsViewer without predefined columns
viewer := StringsViewer{}
view, err := viewer.NewView("Data", data)

func (StringsViewer) NewView

func (v StringsViewer) NewView(title string, table any) (View, error)

NewView creates a View from a [][]string table.

This method implements the Viewer interface and is the primary way to create Views from string data using StringsViewer.

The table parameter must be of type [][]string where:

  • Each inner slice represents one row of data
  • All rows should have the same number of elements (columns) for best results
  • Empty tables (nil or zero length) are accepted and will create empty Views

Column names are taken from the Cols field of the StringsViewer. See the StringsViewer type documentation for details on column naming.

Parameters:

  • title: The title for the resulting View. Can be empty. This title is returned by the View's Title() method.
  • table: The data to create the View from. Must be of type [][]string.

Returns:

  • View: The created View containing the string data with the specified title.
  • error: nil on success, or an error if table is not of type [][]string.

Example:

viewer := StringsViewer{Cols: []string{"Product", "Price", "Stock"}}

inventory := [][]string{
    {"Widget", "9.99", "100"},
    {"Gadget", "19.99", "50"},
    {"Doohickey", "29.99", "25"},
}

view, err := viewer.NewView("Inventory", inventory)
if err != nil {
    log.Fatal(err)
}

fmt.Println(view.Title())    // "Inventory"
fmt.Println(view.Columns())  // ["Product", "Price", "Stock"]
fmt.Println(view.NumRows())  // 3

// Access data through the View interface
cell := view.Cell(0, 0)  // "Widget"

type StructFieldNaming

type StructFieldNaming struct {
	// Tag is the struct field tag to be used as column title.
	// When non-empty, struct fields are checked for this tag name,
	// and the tag value is used as the column title.
	//
	// Tag values support comma-separated options, where only the first
	// part before the comma is used as the column title:
	//   `json:"name,omitempty"` results in column title "name"
	//
	// If Tag is empty, then every struct field will be treated as untagged.
	Tag string

	// Ignore specifies a column title value that marks fields to be excluded.
	// Any field whose column title matches this value will be ignored and
	// not appear in the resulting table view.
	//
	// Common patterns:
	//   - Use "-" to match the convention `json:"-"` for ignored fields
	//   - Use "" (empty string) to ignore all unexported or empty-titled fields
	Ignore string

	// Untagged is called with the struct field name to generate a column title
	// when the field has no tag matching Tag (or Tag is empty).
	//
	// If Untagged is nil, the raw struct field name is used as the column title.
	//
	// Common functions to use:
	//   - SpacePascalCase: "FirstName" -> "First Name"
	//   - SpaceGoCase: "HTTPServer" -> "HTTP Server"
	//   - Custom function for domain-specific naming conventions
	Untagged func(fieldName string) (column string)
}

StructFieldNaming defines how struct fields are mapped to column titles as used by View. It provides flexible control over which struct fields become columns and what their column titles are through struct tags and custom naming functions.

The mapping process follows these rules:

  • Only exported (public) struct fields are considered for column mapping
  • Anonymous embedded struct fields are recursively processed, with their fields inlined
  • If a Tag is specified, the struct tag value is used as the column title
  • For fields without tags (or if Tag is empty), the Untagged function determines the column title
  • Fields matching the Ignore value are excluded from the table

Example usage with struct tags:

type Person struct {
    FirstName string `csv:"first_name"`
    LastName  string `csv:"last_name"`
    Age       int    `csv:"age"`
    Internal  string `csv:"-"` // ignored when Ignore is set to "-"
}

naming := &StructFieldNaming{
    Tag:    "csv",
    Ignore: "-",
}
columns := naming.Columns(&Person{}) // ["first_name", "last_name", "age"]

Example with custom naming function:

naming := &StructFieldNaming{
    Untagged: SpacePascalCase, // "FirstName" becomes "First Name"
}

A nil *StructFieldNaming is a valid value and is equivalent to the zero value, which will use all exported struct fields with their field name as column title.

StructFieldNaming implements the Viewer interface, allowing it to create Views from slices or arrays of structs.

func (*StructFieldNaming) ColumnStructFieldValue

func (n *StructFieldNaming) ColumnStructFieldValue(structVal reflect.Value, column string) reflect.Value

ColumnStructFieldValue returns the reflect.Value of the struct field that is mapped to the given column title.

The function searches through all exported struct fields (including those from anonymously embedded structs) to find the field whose column title matches the given column parameter.

Anonymous embedded structs are recursively searched, allowing fields from nested structs to be accessed by their column title.

Returns an invalid reflect.Value if:

  • The column is ignored (empty or matches Ignore)
  • No struct field maps to the given column title
  • structVal is not a struct or pointer to struct

Panics if structVal is not a struct or pointer to struct type.

This method is safe to call with a nil receiver.

Example:

type Person struct {
    Name string `csv:"full_name"`
    Age  int    `csv:"age"`
}
naming := &StructFieldNaming{Tag: "csv"}
person := Person{Name: "John", Age: 30}
val := naming.ColumnStructFieldValue(reflect.ValueOf(person), "full_name")
// val.String() == "John"

func (*StructFieldNaming) Columns

func (n *StructFieldNaming) Columns(strct any) []string

Columns returns the column titles for all non-ignored exported fields of a struct or pointer to a struct.

The returned slice contains column titles in the order they appear in the struct definition, with fields from anonymously embedded structs inlined in their position.

Fields are processed according to the StructFieldNaming rules:

  • Tag determines which struct tag to read for column titles
  • Untagged function transforms field names when no tag is present
  • Ignore value filters out unwanted columns

Panics if strct is not a struct or pointer to struct type.

This method is safe to call with a nil receiver, in which case it returns all exported field names without transformation.

Example:

type User struct {
    ID       int    `db:"user_id"`
    Username string `db:"username"`
    Password string `db:"-"`
}
naming := &StructFieldNaming{Tag: "db", Ignore: "-"}
cols := naming.Columns(&User{}) // ["user_id", "username"]

func (*StructFieldNaming) IsIgnored

func (n *StructFieldNaming) IsIgnored(column string) bool

IsIgnored returns true if the given column title should be ignored and excluded from the table view.

A column is considered ignored if:

  • The column title is an empty string
  • The column title matches the Ignore field value (when n is not nil)

This method is safe to call with a nil receiver.

func (*StructFieldNaming) NewView

func (n *StructFieldNaming) NewView(title string, table any) (View, error)

NewView returns a View for a table made up of a slice or array of structs.

This method implements the Viewer interface for StructFieldNaming, creating a StructRowsView by delegating to StructRowsViewer with this naming configuration.

Parameters:

  • title: The title for the table view
  • table: A slice or array of structs (or pointers to structs)

Returns an error if table is not a slice or array, or if the element type is not a struct or pointer to struct.

Example:

type Product struct {
    Name  string `json:"name"`
    Price float64 `json:"price"`
}
naming := &StructFieldNaming{Tag: "json"}
products := []Product{{"Widget", 9.99}, {"Gadget", 19.99}}
view, err := naming.NewView("Products", products)

func (*StructFieldNaming) String

func (n *StructFieldNaming) String() string

String implements the fmt.Stringer interface for StructFieldNaming.

Valid to call with nil receiver.

func (*StructFieldNaming) StructFieldColumn

func (n *StructFieldNaming) StructFieldColumn(field reflect.StructField) string

StructFieldColumn returns the column title for a struct field using the naming rules defined in the StructFieldNaming configuration.

The function applies the following logic:

  • Returns empty string for unexported or anonymous fields
  • If Tag is set and the field has that tag, uses the tag value (before any comma)
  • Otherwise calls Untagged function if set, or uses the field name

This method is safe to call with a nil receiver, in which case it returns the field name for exported fields and empty string for unexported fields.

func (*StructFieldNaming) WithIgnore

func (n *StructFieldNaming) WithIgnore(ignore string) *StructFieldNaming

WithIgnore returns a copy of the StructFieldNaming with the Ignore field set to the specified value.

This method allows fluent configuration of the naming rules.

Example:

naming := (&StructFieldNaming{Tag: "csv"}).WithIgnore("-")

func (*StructFieldNaming) WithTag

func (n *StructFieldNaming) WithTag(tag string) *StructFieldNaming

WithTag returns a copy of the StructFieldNaming with the Tag field set to the specified value.

This method allows fluent configuration of the naming rules.

Example:

naming := (&StructFieldNaming{}).WithTag("json").WithIgnore("-")

type StructRowsView

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

StructRowsView is a View implementation that provides efficient access to tabular data stored in a slice or array of structs.

StructRowsView is created by StructRowsViewer.NewView and implements the View interface, allowing struct data to be formatted, exported, or displayed as tables.

The view uses reflection to extract field values on demand, with caching to optimize repeated access to the same row. This makes it efficient for scenarios where rows are accessed sequentially or where the same row is queried multiple times.

Field-to-column mapping:

  • If indices is nil, struct fields map 1:1 to columns in declaration order
  • If indices is non-nil, it specifies which struct field index maps to each column
  • The indices slice has one entry per struct field, with values indicating column positions
  • An index value of -1 means that field is excluded from the view

Performance characteristics:

  • Row access is O(1) - direct slice indexing
  • Cell access includes reflection overhead on first access per row
  • Subsequent cell access for the same row uses cached values (O(1))
  • Memory usage scales with the number of structs in the underlying slice

This type is thread-safe for concurrent read access.

func (*StructRowsView) Cell

func (view *StructRowsView) Cell(row, col int) any

Cell returns the value at the specified row and column as an any (interface{}).

This method implements the View interface and uses reflection to extract the struct field value corresponding to the requested cell.

Caching behavior:

  • On first access to a row, all field values are extracted and cached
  • Subsequent Cell calls for the same row use the cached values
  • Accessing a different row invalidates the cache and extracts that row's values

Parameters:

  • row: Row index (0-based)
  • col: Column index (0-based)

Returns nil if row or col is out of bounds.

Performance: O(1) for cached rows, O(n) where n is the number of struct fields for the first access to a new row.

func (*StructRowsView) Columns

func (view *StructRowsView) Columns() []string

Columns returns the column names of the view in display order. This implements the View interface.

func (*StructRowsView) NumRows

func (view *StructRowsView) NumRows() int

NumRows returns the number of rows in the view, which equals the length of the underlying struct slice or array. This implements the View interface.

func (*StructRowsView) ReflectCell

func (view *StructRowsView) ReflectCell(row, col int) reflect.Value

ReflectCell returns the reflect.Value at the specified row and column.

This method is similar to Cell but returns a reflect.Value instead of any, allowing callers to perform type-specific operations or avoid unnecessary Interface() conversions.

Caching behavior:

  • On first access to a row, all field reflect.Values are extracted and cached
  • Subsequent ReflectCell calls for the same row use the cached values
  • Accessing a different row invalidates the cache and extracts that row's values

Parameters:

  • row: Row index (0-based)
  • col: Column index (0-based)

Returns an invalid reflect.Value (reflect.Value{}) if row or col is out of bounds. Use reflect.Value.IsValid() to check if the returned value is valid.

Performance: O(1) for cached rows, O(n) where n is the number of struct fields for the first access to a new row.

Example:

val := view.ReflectCell(0, 0)
if val.IsValid() && val.CanInt() {
    fmt.Println("Integer value:", val.Int())
}

func (*StructRowsView) Title

func (view *StructRowsView) Title() string

Title returns the title of the view. This implements the View interface.

type StructRowsViewer

type StructRowsViewer struct {
	StructFieldNaming

	// MapIndices maps struct field indices to column indices in the output view.
	//
	// The key is the index of a field as returned by StructFieldTypes (exported
	// fields in declaration order, with embedded struct fields inlined).
	// The value is the column index where that field's data should appear.
	//
	// If MapIndices is nil, fields map 1:1 to columns in their natural order.
	//
	// Mapping a field index to -1 excludes that field from the view entirely.
	//
	// All column indices from 0 to N-1 must be mapped exactly once (excluding -1),
	// where N is the number of columns in the output.
	//
	// Example:
	//   MapIndices: map[int]int{
	//       0: 1,  // field 0 -> column 1
	//       1: 0,  // field 1 -> column 0 (swap with field 0)
	//       2: -1, // field 2 -> excluded
	//       3: 2,  // field 3 -> column 2
	//   }
	MapIndices map[int]int
}

StructRowsViewer implements Viewer for tables represented by a slice or array of struct rows, with advanced control over column ordering and field inclusion.

StructRowsViewer extends StructFieldNaming with the ability to rearrange columns and selectively exclude specific struct fields by their index. This is useful when:

  • You need to reorder columns without changing the struct definition
  • You want to exclude specific fields beyond what struct tags provide
  • You need programmatic control over which fields appear in the view

The viewer uses reflection to extract field values from each struct in the slice, mapping them to columns according to the StructFieldNaming rules and MapIndices configuration.

Column mapping process:

  1. All exported struct fields are identified (including embedded struct fields)
  2. StructFieldNaming rules determine the column title for each field
  3. MapIndices (if set) remaps field indices to column positions
  4. Fields mapped to -1 are excluded from the output
  5. A StructRowsView is created to provide access to the data

Example with field reordering:

type Employee struct {
    ID        int
    FirstName string
    LastName  string
    Salary    float64
}

viewer := &StructRowsViewer{
    StructFieldNaming: StructFieldNaming{
        Untagged: SpacePascalCase,
    },
    MapIndices: map[int]int{
        0: 2,  // ID appears in 3rd column
        1: 0,  // FirstName appears in 1st column
        2: 1,  // LastName appears in 2nd column
        3: -1, // Salary is excluded
    },
}
// Columns will be: ["First Name", "Last Name", "ID"]

Example with field exclusion:

viewer := &StructRowsViewer{}
viewer = viewer.WithIgnoreFieldIndex(3) // Ignore field at index 3

StructRowsViewer implements the Viewer interface.

func DefaultStructRowsViewer

func DefaultStructRowsViewer() *StructRowsViewer

DefaultStructRowsViewer returns a new StructRowsViewer configured with DefaultStructFieldNaming.

The returned viewer:

  • Uses "col" struct tag for column titles
  • Ignores fields tagged with "-"
  • Uses SpacePascalCase for untagged field names
  • Has no column index mapping (MapIndices is nil)

This is a convenience function for creating a standard viewer for struct slices.

Example:

type Person struct {
    Name string `col:"Full Name"`
    Age  int
}
viewer := DefaultStructRowsViewer()
view, err := viewer.NewView("People", []Person{{Name: "John", Age: 30}})

func NoTagsStructRowsViewer

func NoTagsStructRowsViewer() *StructRowsViewer

NoTagsStructRowsViewer returns a new StructRowsViewer that ignores struct tags and uses raw field names as column titles.

The returned viewer:

  • Does not read any struct tags
  • Uses the exact struct field names as column titles (e.g., "FirstName", "LastName")
  • Has no column index mapping (MapIndices is nil)

This is useful when you want to quickly display struct data without setting up tags, or when the raw field names are already suitable as column headers.

Example:

type Person struct {
    Name string
    Age  int
}
viewer := NoTagsStructRowsViewer()
view, err := viewer.NewView("People", []Person{{Name: "John", Age: 30}})
// Column titles will be: "Name", "Age"

func (*StructRowsViewer) NewView

func (v *StructRowsViewer) NewView(title string, table any) (View, error)

NewView returns a View for a table made up of a slice or array of structs.

This method implements the Viewer interface for StructRowsViewer. It uses reflection to analyze the struct type, apply the naming configuration, and create a StructRowsView that provides efficient access to the data.

The method performs the following steps:

  1. Validates that table is a slice or array
  2. Extracts the element type and verifies it's a struct
  3. Gets all exported struct fields using StructFieldTypes
  4. Applies StructFieldNaming rules to determine column titles
  5. Applies MapIndices remapping if configured
  6. Creates and returns a StructRowsView with the computed mapping

Parameters:

  • title: The title for the table view
  • table: Must be a slice or array of structs (or pointers to structs)

Returns an error if:

  • table is not a slice or array type
  • the element type is not a struct or pointer to struct

Example:

type Order struct {
    OrderID   int    `csv:"id"`
    Customer  string `csv:"customer"`
    Total     float64 `csv:"total"`
}
viewer := &StructRowsViewer{
    StructFieldNaming: StructFieldNaming{Tag: "csv"},
}
orders := []Order{{1, "Alice", 99.99}, {2, "Bob", 149.99}}
view, err := viewer.NewView("Orders", orders)

func (*StructRowsViewer) String

func (v *StructRowsViewer) String() string

String implements the fmt.Stringer interface for StructRowsViewer.

func (*StructRowsViewer) WithIgnore

func (v *StructRowsViewer) WithIgnore(ignore string) *StructRowsViewer

WithIgnore returns a copy of the StructRowsViewer with the Ignore field set to the specified value.

This method allows fluent configuration of the viewer.

Example:

viewer := (&StructRowsViewer{Tag: "csv"}).WithIgnore("-")

func (*StructRowsViewer) WithIgnoreField

func (v *StructRowsViewer) WithIgnoreField(structPtr, fieldPtr any) *StructRowsViewer

WithIgnoreField returns a copy of the StructRowsViewer with the specified struct field excluded from the view.

This method uses reflection to determine the field index by comparing pointer addresses. It requires pointers to both a struct instance and one of its fields.

Panics if:

  • structPtr is not a pointer to a struct
  • fieldPtr is not a pointer to a field of that struct
  • the field cannot be found in the struct

Example:

type Person struct {
    Name   string
    Age    int
    Secret string
}
var p Person
viewer := (&StructRowsViewer{}).WithIgnoreField(&p, &p.Secret)

func (*StructRowsViewer) WithIgnoreFieldIndex

func (v *StructRowsViewer) WithIgnoreFieldIndex(fieldIndex int) *StructRowsViewer

WithIgnoreFieldIndex returns a copy of the StructRowsViewer with the specified field index mapped to -1, effectively excluding it from the view.

This is a convenience method equivalent to WithMapIndex(fieldIndex, -1).

The fieldIndex is the index of the struct field as returned by StructFieldTypes.

Example:

// Exclude the 3rd field (index 2)
viewer := (&StructRowsViewer{}).WithIgnoreFieldIndex(2)

func (*StructRowsViewer) WithIgnoreFieldIndices

func (v *StructRowsViewer) WithIgnoreFieldIndices(fieldIndices ...int) *StructRowsViewer

WithIgnoreFieldIndices returns a copy of the StructRowsViewer with multiple field indices mapped to -1, effectively excluding them from the view.

This is a convenience method for excluding multiple fields at once.

Example:

// Exclude fields at indices 2, 5, and 7
viewer := (&StructRowsViewer{}).WithIgnoreFieldIndices(2, 5, 7)

func (*StructRowsViewer) WithMapIndex

func (v *StructRowsViewer) WithMapIndex(fieldIndex, columnIndex int) *StructRowsViewer

WithMapIndex returns a copy of the StructRowsViewer with an additional field-to-column mapping added to MapIndices.

The fieldIndex is the index of the struct field (as returned by StructFieldTypes), and columnIndex is the desired column position in the output (or -1 to exclude).

Example:

viewer := (&StructRowsViewer{}).
    WithMapIndex(0, 2).  // field 0 -> column 2
    WithMapIndex(1, 0).  // field 1 -> column 0
    WithMapIndex(2, 1)   // field 2 -> column 1

func (*StructRowsViewer) WithMapIndices

func (v *StructRowsViewer) WithMapIndices(mapIndices map[int]int) *StructRowsViewer

WithMapIndices returns a copy of the StructRowsViewer with the MapIndices field replaced by the provided map.

This replaces any existing mappings with the new map. Use WithMapIndex to add individual mappings incrementally.

Example:

viewer := (&StructRowsViewer{}).WithMapIndices(map[int]int{
    0: 2,  // field 0 -> column 2
    1: 0,  // field 1 -> column 0
    2: 1,  // field 2 -> column 1
    3: -1, // field 3 -> excluded
})

func (*StructRowsViewer) WithTag

func (v *StructRowsViewer) WithTag(tag string) *StructRowsViewer

WithTag returns a copy of the StructRowsViewer with the Tag field set to the specified value.

This method allows fluent configuration of the viewer.

Example:

viewer := (&StructRowsViewer{}).WithTag("json").WithIgnore("-")

type UnsupportedCellFormatter

type UnsupportedCellFormatter struct{}

UnsupportedCellFormatter is a CellFormatter that always returns errors.ErrUnsupported. This is useful as a placeholder or for explicitly marking certain cell types as unsupported in a formatting configuration.

In a formatter chain, this can be used to force trying the next formatter, or to document that a particular cell type intentionally has no formatter.

Example usage:

// Explicitly mark a type as unsupported
var formatter CellFormatter = UnsupportedCellFormatter{}
_, _, err := formatter.FormatCell(ctx, view, 0, 0)
// err == errors.ErrUnsupported

func (UnsupportedCellFormatter) FormatCell

func (UnsupportedCellFormatter) FormatCell(ctx context.Context, view View, row, col int) (str string, raw bool, err error)

FormatCell implements CellFormatter by always returning errors.ErrUnsupported.

type UnsupportedFormatter

type UnsupportedFormatter struct{}

UnsupportedFormatter is a Formatter that always returns errors.ErrUnsupported. This is useful as a placeholder or when you want to explicitly mark certain types as unsupported in a formatting chain.

Example:

// Mark a type as unsupported, forcing fallback to another formatter
var f Formatter = UnsupportedFormatter{}
_, err := f.Format(reflect.ValueOf("anything"))
// err == errors.ErrUnsupported

func (UnsupportedFormatter) Format

Format implements Formatter by always returning errors.ErrUnsupported.

type View

type View interface {
	// Title returns the name/title of this table.
	// The title is used for display purposes and when writing to formats
	// that support named tables (e.g., Excel sheet names, HTML table captions).
	// Returns an empty string if the table has no title.
	Title() string

	// Columns returns the names of all columns in this table.
	// The length of the returned slice defines the number of columns.
	// Individual column names may be empty strings if unnamed.
	// The returned slice should not be modified by callers.
	// Column names are used as headers when writing to CSV, HTML, etc.
	Columns() []string

	// NumRows returns the total number of data rows in this table.
	// This does not include any header row - it's purely the count of data rows.
	// Returns 0 for an empty table.
	NumRows() int

	// Cell returns the value at the specified row and column coordinates.
	// Row and column indices are zero-based.
	//
	// Returns nil in these cases:
	//   - row is negative or >= NumRows()
	//   - col is negative or >= len(Columns())
	//   - the cell actually contains a nil value
	//
	// The returned value type depends on the View implementation and
	// the underlying data source. Common types include:
	//   - string (for text-based sources like CSV)
	//   - numeric types (int, float64, etc.)
	//   - time.Time (for date/time values)
	//   - bool
	//   - custom types (for struct-based Views)
	//
	// Callers should use type assertions or reflection to work with
	// the returned value. For more type-safe access, consider using
	// ViewToStructSlice or SmartAssign functions.
	Cell(row, col int) any
}

View is the central interface in the retable package that represents read-only tabular data in a uniform, type-agnostic way. It provides access to table metadata (title, columns) and cell data through a simple coordinate-based API.

View is designed as an in-memory abstraction - implementations should load all data before wrapping it as a View. This design choice eliminates the need for context parameters and error handling in the core View methods, simplifying the API.

Design Philosophy

The View interface follows these principles:

  • Immutability: Views are read-only; modifications create new Views
  • Simplicity: No error returns from data access methods (data already in memory)
  • Uniformity: All tabular data sources expose the same interface
  • Composability: Views can be wrapped and transformed via decorator types

Coordinate System

Views use zero-based indexing:

  • Columns are numbered 0 to len(Columns())-1
  • Rows are numbered 0 to NumRows()-1

Cell Values

Cell values are returned as any (empty interface) and can be:

  • Go primitives (int, string, bool, float64, etc.)
  • Complex types (time.Time, custom structs, etc.)
  • nil (for missing/null values or out-of-bounds access)

Common Implementations

The package provides several View implementations:

  • StringsView: Backed by [][]string (from CSV, text)
  • StructRowsView: Backed by []StructType (reflection-based)
  • AnyValuesView: Backed by [][]any (from SQL, mixed types)
  • ReflectValuesView: Backed by [][]reflect.Value (advanced)

View Wrappers

Several wrapper types transform Views without copying data:

  • FilteredView: Row/column filtering and remapping
  • DerefView: Automatic pointer dereferencing
  • ExtraColsView: Horizontal concatenation
  • ExtraRowsView: Vertical concatenation
  • ViewWithTitle: Custom title override

Example Usage

// Create a view from structs
type Person struct {
    Name string
    Age  int
}
people := []Person{{"Alice", 30}, {"Bob", 25}}
view := NewStructRowsView("People", people, nil, nil)

// Access data
fmt.Println(view.Title())              // "People"
fmt.Println(view.Columns())            // ["Name", "Age"]
fmt.Println(view.NumRows())            // 2
fmt.Println(view.Cell(0, 0))           // "Alice"
fmt.Println(view.Cell(0, 1))           // 30
fmt.Println(view.Cell(2, 0))           // nil (out of bounds)

func NewStructRowsView

func NewStructRowsView(title string, columns []string, indices []int, rows reflect.Value) View

NewStructRowsView creates a new StructRowsView for the given struct slice data.

This function is typically called by StructRowsViewer.NewView after determining the column names and field-to-column mapping through reflection and naming rules.

Parameters:

  • title: The title for the table view
  • columns: Slice of column names in display order
  • indices: Maps struct field indices to column indices (or nil for 1:1 mapping)
  • rows: reflect.Value containing a slice or array of structs

The indices parameter, when non-nil, must satisfy these constraints:

  • Length equals the number of struct fields (exported, with embedded fields inlined)
  • Each value is either -1 (field excluded) or a valid column index [0, len(columns))
  • Each column index from 0 to len(columns)-1 appears exactly once
  • No column index appears more than once

If indices represents a simple 1:1 mapping where indices[i] == i for all i, it will be optimized to nil internally.

Panics if:

  • rows is not a slice or array
  • indices violates the mapping constraints
  • any column is unmapped
  • any column is mapped more than once

Example:

// For struct: {Field0, Field1, Field2, Field3}
// To create columns: [Field1, Field3, Field0] (Field2 excluded)
indices := []int{2, 0, -1, 1}
view := NewStructRowsView("Data", []string{"Col0", "Col1", "Col2"}, indices, rowsValue)

func SingleCellView

func SingleCellView[T any](title, column string, value T) View

SingleCellView creates a View containing exactly one cell with a typed value.

This generic function creates a minimal 1x1 view (one row, one column) containing a single value of type T. It provides type safety and can be used to wrap scalar values as Views for compatibility with view-based APIs.

The resulting view has one row and one column. It implements both View and ReflectCellView interfaces.

Performance characteristics:

  • Minimal memory footprint (one value + metadata)
  • O(1) cell access
  • No allocations for repeated access

When to use SingleCellView:

  • Wrapping a single scalar value as a View
  • Creating minimal test fixtures
  • Representing single-value results in a tabular format
  • Building view hierarchies where leaf nodes are single values

Parameters:

  • title: The title of the view
  • column: The name of the single column
  • value: The value of type T to store in the single cell

Returns a View with one row and one column containing the provided value.

Example:

// Create a single-cell view with an integer
view := retable.SingleCellView("Count", "Total", 42)
fmt.Println(view.Title())      // Output: Count
fmt.Println(view.NumRows())    // Output: 1
fmt.Println(view.Cell(0, 0))   // Output: 42

// Create a single-cell view with a string
view := retable.SingleCellView("Status", "Message", "Success")
fmt.Println(view.Cell(0, 0))   // Output: Success

Thread safety: Immutable after creation (assuming T is not a reference type that is modified externally), safe for concurrent reads.

func SingleColView

func SingleColView[T any](column string, rows []T) View

SingleColView creates a View containing a single column with typed data.

This generic function provides a type-safe way to create a one-column view from a slice of values. The generic type parameter T determines the type of values in the column, providing compile-time type safety.

The resulting view has one column and len(rows) rows, where each row contains one value from the rows slice. The view implements both View and ReflectCellView interfaces.

Performance characteristics:

  • Zero-copy: directly wraps the provided slice
  • O(1) cell access
  • Memory efficient (no data duplication)
  • Generic type instantiation overhead at compile time only

When to use SingleColView:

  • Working with a single column of homogeneous typed data
  • Converting a slice into a View for compatibility
  • Building columnar data structures
  • Type-safe single-column operations

Parameters:

  • column: The name of the single column
  • rows: Slice of values of type T, one per row

Returns a View with one column containing the provided values.

Example:

// Create a single-column view of integers
numbers := []int{10, 20, 30, 40}
view := retable.SingleColView("Value", numbers)
fmt.Println(view.NumRows())    // Output: 4
fmt.Println(view.Cell(2, 0))   // Output: 30

// Create a single-column view of strings
names := []string{"Alice", "Bob", "Charlie"}
view := retable.SingleColView("Name", names)
fmt.Println(view.Cell(1, 0))   // Output: Bob

Thread safety: Not thread-safe if the underlying rows slice is modified. The view does not copy the data, so concurrent modifications to the slice will be visible through the view.

func ViewWithTitle

func ViewWithTitle(source View, title string) View

ViewWithTitle creates a new View that wraps a source View with a different title.

This function provides a way to change the title of an existing view without modifying the original or copying the data. The returned view delegates all operations to the source view except for Title(), which returns the provided title string.

The wrapper implements both View and ReflectCellView interfaces, providing transparent access to the underlying data while presenting a different title.

Performance characteristics:

  • Zero-copy: no data duplication
  • O(1) overhead for all operations (simple delegation)
  • Minimal memory footprint (wrapper object only)

When to use ViewWithTitle:

  • Need to display the same data with different titles in different contexts
  • Building view pipelines where title changes are required
  • Implementing view transformations that only affect metadata
  • Creating aliases or alternative presentations of existing views

Parameters:

  • source: The View to wrap (will be converted to ReflectCellView)
  • title: The new title to use for the wrapped view

Returns a View that has the specified title but otherwise behaves like source.

Example:

original := retable.NewStringsView("Original Title", data, "A", "B")
renamed := retable.ViewWithTitle(original, "New Title")
fmt.Println(renamed.Title())       // Output: New Title
fmt.Println(renamed.Cell(0, 0))    // Delegates to original
fmt.Println(original.Title())      // Output: Original Title (unchanged)

Thread safety: Safe for concurrent reads if the source view is safe for concurrent reads. The wrapper is immutable after creation.

type Viewer

type Viewer interface {
	// NewView creates a View with the specified title from the given table data.
	//
	// The table parameter should contain tabular data in a format that this
	// Viewer implementation recognizes. Different Viewer implementations accept
	// different types:
	//   - StringsViewer accepts [][]string
	//   - StructRowsViewer accepts []StructType or []*StructType
	//
	// Returns an error if:
	//   - table is not a supported type for this Viewer
	//   - table is nil or empty (implementation-dependent)
	//   - the data structure is invalid or malformed
	//
	// The title parameter becomes the result of the View's Title() method.
	// It can be an empty string if no title is needed.
	NewView(title string, table any) (View, error)
}

Viewer is a factory interface for creating Views from arbitrary table data. Viewers are responsible for recognizing specific data structures (like [][]string or []StructType) and wrapping them as View implementations.

Purpose

The Viewer interface provides a uniform way to create Views from different data representations without needing to know the exact View type to construct. This is especially useful in generic code that works with multiple data formats.

Standard Implementations

  • StringsViewer: Creates views from [][]string (CSV, text data)
  • StructRowsViewer: Creates views from struct slices via reflection

Example Usage

// Using StringsViewer
viewer := StringsViewer{FirstRowIsHeader: true}
data := [][]string{
    {"Name", "Age"},
    {"Alice", "30"},
    {"Bob", "25"},
}
view, err := viewer.NewView("People", data)

// Using StructRowsViewer
type Person struct {
    Name string
    Age  int
}
people := []Person{{"Alice", 30}, {"Bob", 25}}
viewer := NewStructRowsViewer(nil)
view, err := viewer.NewView("People", people)

Custom Viewers

Implement this interface to create Views from custom data structures:

type MyViewer struct{}

func (v MyViewer) NewView(title string, table any) (View, error) {
    myData, ok := table.(MyDataType)
    if !ok {
        return nil, fmt.Errorf("expected MyDataType, got %T", table)
    }
    return myViewFromData(title, myData), nil
}

Directories

Path Synopsis
Package csvtable provides CSV parsing, writing, and format detection functionality with support for various encodings, separators, and RFC 4180 compliance.
Package csvtable provides CSV parsing, writing, and format detection functionality with support for various encodings, separators, and RFC 4180 compliance.
exceltable module
Package htmltable provides functionality for writing tables as HTML.
Package htmltable provides functionality for writing tables as HTML.
Package sqltable provides a virtual SQL database driver that allows querying in-memory retable.View data structures using database/sql and SQL-like queries.
Package sqltable provides a virtual SQL database driver that allows querying in-memory retable.View data structures using database/sql and SQL-like queries.

Jump to

Keyboard shortcuts

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