colprint

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jan 8, 2026 License: MIT Imports: 5 Imported by: 0

README

colprint

High-performance, zero-allocation column formatting library for Go.

Go Reference

Features

  • Zero allocations in formatting hot path
  • Type-safe using Go generics
  • Fast - single syscall per row with line buffering
  • Flexible field selection and collections
  • Custom formatters for complex types
  • Suitable for streaming millions of rows

Installation

go get github.com/arozenfe/colprint

Quick Start

package main

import (
    "os"
    "github.com/arozenfe/colprint"
)

type Person struct {
    Name string
    Age  int
    City string
}

func main() {
    // 1. Create registry and register fields
    reg := colprint.NewRegistry[Person]()
    
    reg.Field("name", "Name", "Person's name").
        Width(15).
        String(func(p *Person) string { return p.Name }).
        Register()
    
    reg.Field("age", "Age", "Age in years").
        Width(5).
        Int(func(p *Person) int { return p.Age }).
        Register()
    
    reg.Field("city", "City", "City of residence").
        Width(12).
        String(func(p *Person) string { return p.City }).
        Register()
    
    // 2. Compile a field specification
    prog, _ := colprint.Compile(reg, "name,age,city")
    
    // 3. Format rows (zero allocations in this loop)
    line := make([]byte, 0, 256)
    tmp := make([]byte, 0, 64)
    
    people := []Person{
        {"Alice", 30, "New York"},
        {"Bob", 25, "Los Angeles"},
        {"Charlie", 35, "Chicago"},
    }
    
    prog.WriteHeader(os.Stdout, &line)
    prog.WriteUnderline(os.Stdout, &line)
    
    for i := range people {
        prog.WriteRow(os.Stdout, &people[i], &tmp, &line)
    }
}

Output:

Name           Age   City        
-------------- ----- ------------
Alice          30    New York    
Bob            25    Los Angeles 
Charlie        35    Chicago     

Field Collections

Group related fields into named collections:

reg.DefineCollection("basic", "Basic info", "name", "age")
reg.DefineCollection("location", "Location info", "city", "country")
reg.SetDefaults("basic", "name,age")

// Use @collection syntax in specs
prog, _ := colprint.Compile(reg, "@basic,city")
prog, _ := colprint.Compile(reg, "@default")  // Uses default fields

Custom Separators

// No spacing between columns
prog, _ := colprint.CompileWithOptions(reg, "name,age,city", colprint.Options{
    Separator: "",
})

// CSV output
prog, _ := colprint.CompileWithOptions(reg, "name,age,city", colprint.Options{
    Separator: ",",
    NoHeader:  false,
})

Field Width Override

// Override width in specification
prog, _ := colprint.Compile(reg, "name:20,age:8,city:15")

Custom Formatters

reg.Field("elapsed", "Elapsed", "Time elapsed").
    Width(10).
    Custom(func(buf []byte, p *Person) []byte {
        // Custom formatting logic
        elapsed := time.Since(p.StartTime)
        return append(buf, elapsed.String()...)
    }).
    Register()

Performance

Designed for maximum performance:

  • All field resolution happens once during Compile()
  • Formatting uses typed closures (no interface dispatch)
  • Buffers are reused across rows (caller-provided)
  • No reflection in hot path

Expected performance: 1M+ rows/sec for typical workloads.

Options

type Options struct {
    Separator      string  // Column separator (default: "  ")
    NoHeader       bool    // Skip header line
    NoUnderline    bool    // Skip header underline
    NoPadding      bool    // No padding on any column
    PadLastColumn  bool    // Pad last column to width (default: false)
}

Documentation

Full documentation available at pkg.go.dev.

See .github/DESIGN.md for design rationale and implementation details.

License

MIT License - see LICENSE file for details.

Contributing

Contributions welcome! Please see .github/DEVELOPMENT.md for development status and roadmap.

Documentation

Overview

Package colprint provides high-performance, zero-allocation column formatting for tabular data output in Go.

Overview

colprint allows you to define fields for your struct types, compile field specifications into optimized write programs, and format millions of rows with zero allocations in the hot path.

Key Features

  • Type-safe using Go generics
  • Zero allocations in formatting hot path
  • Single syscall per row with line buffering
  • Support for collections and default field sets
  • Custom formatters for complex types
  • Suitable for streaming and batch processing

Basic Usage

// 1. Define your type
type Person struct {
    Name string
    Age  int
}

// 2. Create registry and register fields
reg := colprint.NewRegistry[Person]()
reg.Field("name", "Name", "Person's name").
    Width(15).
    String(func(p *Person) string { return p.Name }).
    Register()

reg.Field("age", "Age", "Age in years").
    Width(5).
    Int(func(p *Person) int { return p.Age }).
    Register()

// 3. Compile a field specification
prog, _ := colprint.Compile(reg, "name,age")

// 4. Format rows (zero allocations)
line := make([]byte, 0, 256)
tmp := make([]byte, 0, 64)

prog.WriteHeader(os.Stdout, &line)
for i := range people {
    prog.WriteRow(os.Stdout, &people[i], &tmp, &line)
}

Collections

Group related fields into named collections:

reg.DefineCollection("basic", "name,age", "name", "age", "email")
reg.SetDefaults("basic", "name,age")

// Use @collection syntax in specs
prog, _ := colprint.Compile(reg, "@basic,custom_field")

Performance

The library is designed for maximum performance:

  • All field resolution happens once during Compile()
  • Formatting uses typed closures (no interface dispatch)
  • Buffers are reused across rows (caller-provided)
  • No reflection in hot path

Expected performance: 1M+ rows/sec for typical workloads.

Current Limitations

This initial version supports left-alignment only. Future versions will add right-alignment support for numeric fields.

Example (Basic)

Example_basic demonstrates simple field registration and formatting.

package main

import (
	"os"

	"github.com/example/colprint"
)

// Person is our example domain type.
type Person struct {
	Name   string
	Age    int
	Height float64
	Kids   int
}

func main() {
	// Create a registry for Person type
	reg := colprint.NewRegistry[Person]()

	// Register fields using fluent API
	reg.Field("name", "Name", "Person's full name").
		Width(12).
		String(func(p *Person) string { return p.Name }).
		Register()

	reg.Field("age", "Age", "Age in years").
		Width(4).
		Int(func(p *Person) int { return p.Age }).
		Register()

	// Compile a program
	prog, _ := colprint.Compile(reg, "name,age")

	// Create reusable buffers
	line := make([]byte, 0, 128)
	tmp := make([]byte, 0, 32)

	// Write header
	prog.WriteHeader(os.Stdout, &line)
	prog.WriteUnderline(os.Stdout, &line)

	// Write rows
	people := []Person{
		{Name: "Alice", Age: 30},
		{Name: "Bob", Age: 25},
	}

	for i := range people {
		prog.WriteRow(os.Stdout, &people[i], &tmp, &line)
	}

}
Output:
Name          Age
----          ---
Alice         30
Bob           25
Example (Collections)

Example_collections shows how to use field collections.

package main

import (
	"os"

	"github.com/example/colprint"
)

// Person is our example domain type.
type Person struct {
	Name   string
	Age    int
	Height float64
	Kids   int
}

func main() {
	reg := colprint.NewRegistry[Person]()

	// Register fields
	reg.Field("name", "Name", "Person's name").
		Width(10).
		String(func(p *Person) string { return p.Name }).
		Register()

	reg.Field("age", "Age", "Age in years").
		Width(4).
		Int(func(p *Person) int { return p.Age }).
		Register()

	reg.Field("kids", "Kids", "Number of children").
		Width(5).
		Int(func(p *Person) int { return p.Kids }).
		Register()

	reg.Field("height", "Height", "Height in cm").
		Width(8).
		Float(1, func(p *Person) float64 { return p.Height }).
		Register()

	// Define collections
	reg.DefineCollection("basic", "name,age", "name", "age")
	reg.DefineCollection("family", "kids", "kids")

	// Use collections with @ prefix
	prog, _ := colprint.Compile(reg, "@basic,@family")

	line := make([]byte, 0, 128)
	tmp := make([]byte, 0, 32)

	prog.WriteHeader(os.Stdout, &line)

	person := Person{Name: "Carol", Age: 45, Kids: 2}
	prog.WriteRow(os.Stdout, &person, &tmp, &line)

}
Output:
Name        Age   Kids
Carol       45    2
Example (Csv)

Example_csv demonstrates CSV-style output with custom separator.

package main

import (
	"os"

	"github.com/example/colprint"
)

// Person is our example domain type.
type Person struct {
	Name   string
	Age    int
	Height float64
	Kids   int
}

func main() {
	reg := colprint.NewRegistry[Person]()

	// For CSV output, width determines max field size
	reg.Field("name", "Name", "Person's name").
		Width(5).
		String(func(p *Person) string { return p.Name }).
		Register()

	reg.Field("age", "Age", "Age in years").
		Width(3).
		Int(func(p *Person) int { return p.Age }).
		Register()

	// For CSV output, use NoPadding option
	opts := colprint.Options{
		Separator: ",",
		NoPadding: true,
	}
	prog, _ := colprint.CompileWithOptions(reg, "name,age", opts)

	line := make([]byte, 0, 128)
	tmp := make([]byte, 0, 32)

	prog.WriteHeader(os.Stdout, &line)

	people := []Person{
		{Name: "Alice", Age: 30},
		{Name: "Bob", Age: 25},
	}

	for i := range people {
		prog.WriteRow(os.Stdout, &people[i], &tmp, &line)
	}

}
Output:
Name,Age
Alice,30
Bob,25
Example (Custom)

Example_custom demonstrates custom formatters for complex types.

package main

import (
	"math"
	"os"
	"strconv"

	"github.com/example/colprint"
)

// Person is our example domain type.
type Person struct {
	Name   string
	Age    int
	Height float64
	Kids   int
}

func main() {
	reg := colprint.NewRegistry[Person]()

	reg.Field("name", "Name", "Person's name").
		Width(10).
		String(func(p *Person) string { return p.Name }).
		Register()

	// Custom formatter: convert cm to feet/inches
	reg.Field("height", "Height", "Height in imperial units").
		Width(10).
		Custom(func(dst []byte, p *Person) []byte {
			totalIn := p.Height / 2.54
			feet := int(math.Floor(totalIn / 12))
			inches := int(math.Round(totalIn)) % 12
			dst = strconv.AppendInt(dst, int64(feet), 10)
			dst = append(dst, '\'')
			dst = strconv.AppendInt(dst, int64(inches), 10)
			dst = append(dst, '"')
			return dst
		}).
		Register()

	prog, _ := colprint.Compile(reg, "name,height")

	line := make([]byte, 0, 128)
	tmp := make([]byte, 0, 32)

	prog.WriteHeader(os.Stdout, &line)

	person := Person{Name: "Bob", Height: 175.0}
	prog.WriteRow(os.Stdout, &person, &tmp, &line)

}
Output:
Name        Height
Bob         5'9"
Example (Help)

Example_help demonstrates the help functionality.

package main

import (
	"bytes"
	"fmt"

	"github.com/example/colprint"
)

// Person is our example domain type.
type Person struct {
	Name   string
	Age    int
	Height float64
	Kids   int
}

func main() {
	reg := colprint.NewRegistry[Person]()

	reg.Field("name", "Name", "Person's full name").
		Width(12).
		Category("Basic").
		String(func(p *Person) string { return p.Name }).
		Register()

	reg.Field("age", "Age", "Age in years").
		Width(4).
		Category("Basic").
		Int(func(p *Person) int { return p.Age }).
		Register()

	reg.Field("height", "Height", "Height in centimeters").
		Width(8).
		Category("Physical").
		Float(1, func(p *Person) float64 { return p.Height }).
		Register()

	// Print help
	var buf bytes.Buffer
	reg.PrintHelp(&buf, "")

	// Show output
	fmt.Print(buf.String())

}
Output:

Basic:
  Field  Display  Description
  age    Age      Age in years
  name   Name     Person's full name

Physical:
  Field   Display  Description
  height  Height   Height in centimeters
Example (Streaming)

Example_streaming shows handling of streaming data.

package main

import (
	"bytes"
	"fmt"

	"github.com/example/colprint"
)

// Person is our example domain type.
type Person struct {
	Name   string
	Age    int
	Height float64
	Kids   int
}

func main() {
	reg := colprint.NewRegistry[Person]()

	reg.Field("name", "Name", "Person's name").
		Width(10).
		String(func(p *Person) string { return p.Name }).
		Register()

	reg.Field("age", "Age", "Age in years").
		Width(4).
		Int(func(p *Person) int { return p.Age }).
		Register()

	prog, _ := colprint.Compile(reg, "name,age")

	// Simulate streaming data
	stream := make(chan Person, 3)
	go func() {
		stream <- Person{Name: "Alice", Age: 30}
		stream <- Person{Name: "Bob", Age: 25}
		stream <- Person{Name: "Carol", Age: 35}
		close(stream)
	}()

	// Print header once
	var buf bytes.Buffer
	line := make([]byte, 0, 128)
	tmp := make([]byte, 0, 32)

	prog.WriteHeader(&buf, &line)

	// Process stream
	for person := range stream {
		prog.WriteRow(&buf, &person, &tmp, &line)
	}

	fmt.Print(buf.String())

}
Output:
Name        Age
Alice       30
Bob         25
Carol       35
Example (WidthOverride)

Example_widthOverride shows how to override field widths.

package main

import (
	"os"

	"github.com/example/colprint"
)

// Person is our example domain type.
type Person struct {
	Name   string
	Age    int
	Height float64
	Kids   int
}

func main() {
	reg := colprint.NewRegistry[Person]()

	reg.Field("name", "Name", "Person's name").
		Width(10).
		String(func(p *Person) string { return p.Name }).
		Register()

	reg.Field("age", "Age", "Age in years").
		Width(3).
		Int(func(p *Person) int { return p.Age }).
		Register()

	// Override name width to 20
	prog, _ := colprint.Compile(reg, "name:20,age")

	line := make([]byte, 0, 128)
	tmp := make([]byte, 0, 32)

	prog.WriteHeader(os.Stdout, &line)

	person := Person{Name: "Alexandria", Age: 33}
	prog.WriteRow(os.Stdout, &person, &tmp, &line)

}
Output:
Name                  Age
Alexandria            33

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func InheritFieldsFrom added in v0.2.0

func InheritFieldsFrom[T any, S any](dest *Registry[T], source *Registry[S], mapper func(*T) *S)

InheritFieldsFrom copies all fields from a source registry to the destination registry, transforming the field accessors using the provided mapper function.

This is useful when type T contains or embeds type S, and you want to reuse all field definitions from S's registry for T.

Example:

type TreeNode struct {
    Proc  // embedded
    cutime float32
}

procReg := CreateProcRegistry()
treeReg := colprint.NewRegistry[TreeNode]()
colprint.InheritFieldsFrom(treeReg, procReg, func(n *TreeNode) *Proc { return &n.Proc })
// Now treeReg has all Proc fields, add TreeNode-specific fields
treeReg.Field("cutime", "CUtime", "Cumulative user time").Width(10)...

Types

type Field

type Field[T any] struct {
	// Name is the unique identifier for this field
	Name string

	// Display is the text shown in the column header
	Display string

	// Description provides help text for this field
	Description string

	// Width is the column width in characters
	Width int

	// Kind indicates the data type (String, Int, Float, Custom)
	Kind Kind

	// Precision specifies decimal places for Float fields
	Precision int

	// Value extractors - only one should be set based on Kind
	GetString func(*T) string
	GetInt    func(*T) int
	GetFloat  func(*T) float64
	GetCustom func(dst []byte, v *T) []byte
}

Field describes how to extract and format a field from type T.

Fields are created using the Registry.Field() builder pattern, not by constructing this struct directly.

type FieldBuilder

type FieldBuilder[T any] struct {
	// contains filtered or unexported fields
}

FieldBuilder provides a fluent API for field construction.

func (*FieldBuilder[T]) Custom

func (b *FieldBuilder[T]) Custom(fn func(dst []byte, v *T) []byte) *FieldBuilder[T]

Custom configures this field with a custom formatter.

The formatter appends formatted bytes to dst and returns the result. This allows for complex formatting logic (e.g., timestamps, byte counts).

Example:

Custom(func(dst []byte, p *Person) []byte {
    return append(dst, formatTimestamp(p.Created)...)
})

func (*FieldBuilder[T]) Float

func (b *FieldBuilder[T]) Float(precision int, fn func(*T) float64) *FieldBuilder[T]

Float configures this field as a floating-point type.

The precision parameter specifies the number of decimal places (e.g., 2 for "3.14").

func (*FieldBuilder[T]) Int

func (b *FieldBuilder[T]) Int(fn func(*T) int) *FieldBuilder[T]

Int configures this field as an integer type.

The provided function extracts the int value from the object.

func (*FieldBuilder[T]) Register

func (b *FieldBuilder[T]) Register()

Register adds this field to the registry.

This is the final step in the builder chain.

func (*FieldBuilder[T]) String

func (b *FieldBuilder[T]) String(fn func(*T) string) *FieldBuilder[T]

String configures this field as a string type.

The provided function extracts the string value from the object.

func (*FieldBuilder[T]) Width

func (b *FieldBuilder[T]) Width(w int) *FieldBuilder[T]

Width sets the column width in characters.

type Kind

type Kind int

Kind represents the data type of a field.

const (
	// KindString indicates a string field.
	KindString Kind = iota + 1
	// KindInt indicates an integer field.
	KindInt
	// KindFloat indicates a floating-point field.
	KindFloat
	// KindCustom indicates a custom formatter function.
	KindCustom
)

type Options

type Options struct {
	// Separator is inserted between columns (default: "  ")
	Separator string

	// NoPadding disables all column padding (useful for CSV)
	NoPadding bool

	// PadLastColumn pads the last column to its width (default: false)
	// Setting to false avoids trailing spaces
	PadLastColumn bool

	// NoHeader skips header line generation
	NoHeader bool

	// NoUnderline skips underline generation
	NoUnderline bool
}

Options configures program compilation.

type Program

type Program[T any] struct {
	// contains filtered or unexported fields
}

Program is a compiled, optimized formatting plan for type T.

Programs are created by Compile() and can be reused for formatting millions of rows with zero allocations.

func Compile

func Compile[T any](reg *Registry[T], spec string) (*Program[T], error)

Compile creates an optimized formatting program from a field specification.

The spec is a comma-separated list of field names, with optional features:

  • Field width override: "name:20" sets width to 20
  • Default expansion: "@default" expands to collection's default fields
  • Collection expansion: "@collection_name" expands to collection fields

Examples:

Compile(reg, "name,age,email")
Compile(reg, "name:20,age:5,email:30")
Compile(reg, "@default,extra_field")
Compile(reg, "@basic,@perf")

Returns an error if any field name is invalid or a collection doesn't exist.

func CompileWithOptions

func CompileWithOptions[T any](reg *Registry[T], spec string, opts Options) (*Program[T], error)

CompileWithOptions creates a program with custom options.

func (*Program[T]) FormatRow

func (p *Program[T]) FormatRow(v *T, tmp, line *[]byte) string

FormatRow formats a row and returns it as a string.

This is less efficient than WriteRow as it allocates a string. Prefer WriteRow for high-volume output.

func (*Program[T]) HeaderString

func (p *Program[T]) HeaderString() string

HeaderString returns the header as a string.

func (*Program[T]) WriteHeader

func (p *Program[T]) WriteHeader(w io.Writer, line *[]byte) error

WriteHeader writes the column headers to w.

The line buffer is used for temporary storage and reused across calls. It should have adequate capacity (typically 256 bytes).

func (*Program[T]) WriteRow

func (p *Program[T]) WriteRow(w io.Writer, v *T, tmp, line *[]byte) error

WriteRow formats and writes a single row to w.

This is the hot path - designed for zero allocations and maximum speed. Buffers tmp and line are reused across calls and should have adequate capacity (typically 64 and 256 bytes respectively).

The tmp buffer is used for formatting individual values. The line buffer accumulates the complete row before writing.

func (*Program[T]) WriteUnderline

func (p *Program[T]) WriteUnderline(w io.Writer, line *[]byte) error

WriteUnderline writes the header underline to w.

The underline uses dashes under text and spaces elsewhere.

type Registry

type Registry[T any] struct {
	// contains filtered or unexported fields
}

Registry stores field definitions for type T.

Create a registry with NewRegistry, then use the Field() method to register fields via a fluent builder API.

Registries can be hierarchical - use AddRegistry to nest sub-registries with their own names for organized help output.

func NewRegistry

func NewRegistry[T any]() *Registry[T]

NewRegistry creates a new unnamed field registry for type T.

func NewRegistryWithName added in v0.2.0

func NewRegistryWithName[T any](name string) *Registry[T]

NewRegistryWithName creates a named field registry for type T.

Named registries are useful for organizing related fields into sections when building hierarchical registries with AddRegistry().

func (*Registry[T]) AddRegistry added in v0.2.0

func (r *Registry[T]) AddRegistry(sub *Registry[T])

AddRegistry adds a sub-registry to this registry.

This enables hierarchical organization of fields. Sub-registries with names will appear as separate sections in help output.

Example:

procFields := colprint.NewRegistryWithName[TreeNode]("Process Fields")
colprint.InheritFieldsFrom(procFields, procReg, mapper)
reg.AddRegistry(procFields)

func (*Registry[T]) DefineCollection

func (r *Registry[T]) DefineCollection(name, defaultSpec string, fields ...string)

DefineCollection creates a named collection of fields.

The defaultSpec is the comma-separated list of fields used when this collection is referenced with @collection_name. The fields list contains all fields that belong to this collection (for help display).

Example:

reg.DefineCollection("basic", "name,age", "name", "age", "email", "phone")

func (*Registry[T]) Field

func (r *Registry[T]) Field(name, display, description string) *FieldBuilder[T]

Field starts building a new field definition.

Use the returned FieldBuilder to configure the field, then call Register() to add it to the registry.

Example:

reg.Field("age", "Age", "Age in years").
    Width(5).
    Int(func(p *Person) int { return p.Age }).
    Register()

func (*Registry[T]) ListCollections

func (r *Registry[T]) ListCollections() []string

ListCollections returns all collection names in alphabetical order.

func (*Registry[T]) ListFields

func (r *Registry[T]) ListFields(sorted bool) []string

ListFields returns all registered field names.

By default, fields are returned in insertion order. If sorted is true, they are returned in alphabetical order instead.

func (*Registry[T]) PrintHelp

func (r *Registry[T]) PrintHelp(w io.Writer, collection string)

PrintHelp writes formatted help for all fields to w.

If collection is non-empty, only fields in that collection are shown. For hierarchical registries, sub-registries are shown as separate sections.

func (*Registry[T]) SetDefaults

func (r *Registry[T]) SetDefaults(collectionName, spec string)

SetDefaults sets the default field specification for a collection.

When @default is used in a spec, it expands to this value.

Jump to

Keyboard shortcuts

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