report

package module
v0.3.4 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: MIT Imports: 23 Imported by: 0

README

report

Release Test Go Reference Go Report Card codecov

Excel report generator for Go. Accepts any tabular data source, auto-sizes columns, and supports per-report customisation via embedded Starlark scripts.

Requires Go 1.26+

go get github.com/abagile/tokyo3-report

Design

Generate(outputPath, worksheets...) ──► .xlsx file
                    │
                    └── WorksheetConf[]
                          ├── Rows       (data source: StructRows or SqlxRows)
                          ├── Script     (optional Starlark, controls layout & style)
                          ├── Parsers    (optional named field-value parsers)
                          └── Translator (optional, localises headers and enum values)

Generate either creates a new workbook or opens an existing one. Each worksheet with a RowsReader replaces its named sheet with fresh data; a nil reader preserves an existing named sheet while applying script-defined layout. All row data is streamed into one staged workbook, whose column widths, and the styles of sheets without a RowsReader, are patched in before the final atomic replacement. If the target file does not exist it is created from scratch; if it exists, unrelated sheets are left untouched.


Quick start

type Order struct {
    Region  string
    Product string
    Amount  float64
}

rows := []any{
    Order{"North", "Widget A", 1200.00},
    Order{"North", "Widget B",  450.00},
    Order{"South", "Widget A",  980.00},
}

err := report.Generate(
    "orders.xlsx",
    report.WorksheetConf{
        SheetName: "Orders",
        Rows:      report.NewStructRows(rows),
    },
)

Produces a sheet with auto-titled headers (Region, Product, Amount) and auto-sized columns.

See the example/ directory for a complete runnable program.


WorksheetConf

Field Type Description
SheetName string Name of the worksheet to write or style.
Rows RowsReader Optional data source; nil preserves an existing worksheet.
Script string Starlark script for layout customisation (see below).
Translator Translator Optional interface for header and value localisation.
Parsers map[string]Parser Optional named parsers referenced by column metadata.

RowsReader

RowsReader is the interface that decouples Generate from any particular data source.

type RowsReader interface {
    Read()            error
    Headers()         ([]string, error)
    Values()          ([]any, error)
    Next()            bool
    Err()             error
}

A reader that also implements io.Closer is closed by Generate once the worksheet is written, including when generation fails partway through.

Two implementations are provided:

StructRows

Wraps an in-memory []any of structs. Struct field names become column headers.

reader := report.NewStructRows([]any{
    MyStruct{...},
    MyStruct{...},
})

Pointer fields are dereferenced; nil pointers render as an empty string.

SqlxRows

Streams directly from a live *sqlx.DB query.

reader := report.NewSqlxRows("SELECT region, product, amount FROM orders", db)

NUMERIC database columns are automatically converted to float64. Close releases the result set; Generate calls it, so only direct users of the reader need to.


Translator

Implement Translator to localise header names and enum-coded values.

type Translator interface {
    Header(key string) string              // raw field/column name → display header
    Lookup(key string, code any) string    // lookup key + raw code → display value
}

Header is called once per header with the raw field or SQL column name. Lookup is called per cell for columns that set lookup in the script (see below).

type MyTranslator struct{}

func (MyTranslator) Header(key string) string {
    return map[string]string{
        "region":  "Region",
        "product": "Product Name",
        "amount":  "Amount (USD)",
    }[key]
}

func (MyTranslator) Lookup(key string, code any) string {
    if key == "status" {
        return map[any]string{1: "Active", 2: "Closed"}[code]
    }
    return fmt.Sprintf("%v", code)
}

When no Translator is set, headers are produced by flect.Titleize (order_id → Order Id).


Parsers

A Parser converts a raw field value before it is written to Excel:

type Parser func(any) (any, error)

Register parsers by name in WorksheetConf, then reference those names from the script's col metadata:

worksheet := report.WorksheetConf{
    Parsers: map[string]report.Parser{
        "trim": func(value any) (any, error) {
            return strings.TrimSpace(value.(string)), nil
        },
    },
}

An unknown parser name or a parser error stops report generation. When a column specifies both parser and lookup, the parser runs first and the lookup receives the parsed value.


Starlark script

The Script field accepts a Starlark program. Starlark is a deterministic, sandboxed Python dialect — no I/O, no imports, no side-effects outside the declared variables below.

The script may define any combination of the four top-level variables. Unrecognised variables are silently ignored.

groupFields
groupFields = ["Region", "Category"]

A list of raw column names. Whenever the value in any listed column changes compared to the previous row, a blank separator row is inserted above the new group. Multiple fields are checked in order.

width
width = {
    "A":   14,
    "B:D": 22,
}

Explicit column widths. Keys are column letters or letter ranges ("B:D"). Values are numbers (integer or float). Overrides auto-sizing for those columns. All other columns remain auto-sized.

Column widths are clamped to the range [10, 100].

style
style = {
    "1":       '{"font":{"bold":true}}',    # entire row 1 (header)
    "A":       '{"font":{"color":"FF0000"}}', # entire column A
    "A:C":     '{"fill":{"type":"pattern","color":["FFFFCC"],"pattern":1}}', # columns A–C
    "B2":      '{"font":{"italic":true}}',  # single cell
    "B2:D10":  '{"font":{"italic":true}}',  # cell range
}

Each key selects a target; each value is the JSON serialisation of an excelize Style. Target type is inferred from the key format:

Key pattern Example Target
Column letter(s) "A", "A:C" Whole column, plus every cell already in it
Integer "1", "3" Whole row, plus every cell already in it
Cell reference "B2" Single cell
Cell range "B2:D10" Every cell in the rectangle

Styles of a sheet with a RowsReader are applied while its rows are streamed. Sheets without one are patched directly in the staged worksheet XML. Neither path loads sheet data into memory.

Precedence (first match wins): cell reference → cell range → row → column. A style replaces the target's existing style rather than merging with it, so a column or row style also overrides the automatic date format on time.Time cells in that column or row.

Row, cell and range targets create what they need. Rows and cells that hold no data are written as empty styled elements so the style is visible in Excel, exactly as SetCellStyle would do. The cost is therefore proportional to the targeted area, not to the amount of data: "B2:D10" adds at most 27 cells, but "A1:B1048576" adds two million of them. To style a whole column, use a column target ("A", "A:B"), which is a single <col> definition, instead of a range that spans every row.

A style that resolves to no formatting clears the target. '{}', and any style JSON excelize maps to its default style ID 0, removes the style from the target instead of adding one.

col

Per-column settings keyed by the raw field/column name (before any Translator.Header rename).

col = {
    "amount": {
        "width": 18,
        "style": '{"NumFmt":4}',        # #,##0.00
    },
    "status": {
        "parser": "normalize_status",    # resolves WorksheetConf.Parsers entry
        "lookup": "order_status",        # receives the parsed value
    },
}
Sub-key Type Description
width number Column width (same semantics as width dict above).
style string JSON style applied to the entire column.
parser string Runs the named WorksheetConf.Parsers entry. Unknown names and parser errors stop generation.
lookup string Passes the parsed (or raw) value through Translator.Lookup(key, value). No-op when no Translator is set.

col style and width entries are merged into the style and width dicts after the script runs, so they can coexist with direct style/width entries.

Style JSON reference

Style values are JSON representations of excelize.Style. Common fields:

{
  "font":    { "bold": true, "italic": true, "size": 11, "color": "FF0000" },
  "fill":    { "type": "pattern", "pattern": 1, "color": ["FFFFCC"] },
  "alignment": { "horizontal": "center", "WrapText": true },
  "NumFmt": 4    // built-in number format ID (4 = #,##0.00)
}

Common NumFmt IDs: 3 = #,##0, 4 = #,##0.00, 9 = 0%, 10 = 0.00%.


Auto column sizing

Column widths are calculated automatically from content width, where ASCII characters count as 1 unit and CJK/multibyte characters count as 2 units, multiplied by a 1.123 scale factor. The result is clamped to [10, 100]. Script-defined widths always override auto-sizing.

time.Time values receive Excel's built-in short-date format (format ID 14, e.g. 01/15/24) to prevent Excel from auto-converting dates to MMM-YY display format on open.

Documentation

Index

Constants

View Source
const (
	DefaultSheetName          = "Sheet1"
	OpenXMLShortDateFmtDateId = 14
)

Variables

This section is empty.

Functions

func Generate

func Generate(outputPath string, worksheets ...WorksheetConf) (err error)

Types

type Parser added in v0.2.0

type Parser func(any) (any, error)

Parser converts a raw field value before any configured lookup is applied.

type RowsReader

type RowsReader interface {
	Read() error
	Headers() ([]string, error)
	Values() ([]any, error)
	Next() bool
	Err() error
}

type SqlxRows

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

func NewSqlxRows

func NewSqlxRows(query string, db *sqlx.DB) *SqlxRows

func (*SqlxRows) Close added in v0.3.4

func (r *SqlxRows) Close() error

Close releases the underlying result set. Iterating to the end closes it already, so this only matters when reading stops early. It is safe to call more than once, and Err still reports any error seen while iterating.

func (*SqlxRows) Err

func (r *SqlxRows) Err() error

func (*SqlxRows) Headers

func (r *SqlxRows) Headers() ([]string, error)

func (*SqlxRows) Next

func (r *SqlxRows) Next() bool

func (*SqlxRows) Read

func (r *SqlxRows) Read() error

func (*SqlxRows) Values

func (r *SqlxRows) Values() ([]any, error)

type StructRows

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

func NewStructRows

func NewStructRows(rows []any) *StructRows

func (*StructRows) Err

func (r *StructRows) Err() error

func (*StructRows) Headers

func (r *StructRows) Headers() ([]string, error)

func (*StructRows) Next

func (r *StructRows) Next() bool

func (*StructRows) Read

func (r *StructRows) Read() error

func (*StructRows) Values

func (r *StructRows) Values() ([]any, error)

type Translator

type Translator interface {
	Header(key string) string
	Lookup(key string, code any) string
}

Translator supplies display strings for header names and enum-mapped values. A nil Translator is valid; headers fall back to flect.Titleize and values pass through unchanged.

type WorksheetConf added in v0.3.0

type WorksheetConf struct {
	SheetName  string
	Rows       RowsReader
	Script     string
	Translator Translator
	Parsers    map[string]Parser
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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