Skip to content

Add support for writing struct arrays to excel table #2181

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions model2table.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// Copyright 2016 - 2025 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to and
// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.23 or later.

package excelize

import (
"errors"
"fmt"
"reflect"
"strconv"
)

// modelTableRow represents a row in the Excel table.
// It contains an index for the row and a slice of modelTableCol which represents the columns in that row.
type modelTableRow struct {
Index int
Cols []modelTableCol
}

// modelTableCol represents a column in the Excel table.
// It contains the column name, the field name from the struct, and the value to be written.
type modelTableCol struct {
ColName string
FieldName string
Value interface{}
}

// ModelTableOptions defines options for writing models into an Excel table
// It can include options like whether to include a header row.
type ModelTableOptions struct {
HasHeader bool
}

// WriteStructsIntoFile
// writes a list of structs into an excel file
// the struct must have the tag "column" with the name of the column to write
// header is optional and can be specified with the tag "columnHeader"
func WriteStructsIntoFile[T any](f *File, structs []T, o *ModelTableOptions) error {
if f == nil {
return errors.New("nil file")
}

var rows = constructRows(structs)

index := f.GetActiveSheetIndex()
sheetName := f.GetSheetName(index)
if o != nil && o.HasHeader {
writeHeader[T](f, sheetName)
}
writeRows(f, rows, sheetName, o)

return nil
}

// writeHeader writes the header of the columns putting by default the name of the field or eventually the value
// specified by the columnHeader tag
func writeHeader[T any](f *File, sheetName string) error {
var t T
fieldColumnMap := getTagValues(t, "column")
columnAliasMap := getTagValues(t, "columnHeader")
for k, v := range fieldColumnMap {
cellReference := v + strconv.Itoa(1)
headerColName, found := columnAliasMap[k]
var err error
if found {
err = f.SetCellValue(sheetName, cellReference, headerColName)
} else {
err = f.SetCellValue(sheetName, cellReference, k)
}
if err != nil {
return err
}
}
return nil
}

// writeRows writes the rows in the file, the row index changes depending on the presence of the header
func writeRows(f *File, rows []modelTableRow, sheetName string, o *ModelTableOptions) error {
for i, r := range rows {
for _, c := range r.Cols {
var cellReference string
if o != nil && o.HasHeader {
cellReference = c.ColName + strconv.Itoa(i+2)
} else {
cellReference = c.ColName + strconv.Itoa(i+1)
}
cellValue := c.Value
err := f.SetCellValue(sheetName, cellReference, cellValue)
if err != nil {
return fmt.Errorf("error setting cell value at %s: %w", cellReference, err)
}
}
}
return nil
}

// getFieldValue
// given a struct and the name of a field returns the value of that field for that struct
// and a boolean that indicates whether the field has been found or not
func getFieldValue(i interface{}, fieldName string) (interface{}, bool) {
v := reflect.ValueOf(i)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil, false
}
f := v.FieldByName(fieldName)
if !f.IsValid() {
return nil, false
}
return f.Interface(), true
}

// getTagValues
// returns a map of string to string with the field name as key and the value of the requested tag as value
func getTagValues(i interface{}, tagName string) map[string]string {
v := reflect.ValueOf(i)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil
}
t := v.Type()
fieldCount := v.NumField()
tagValues := make(map[string]string)
for i := 0; i < fieldCount; i++ {
field := t.Field(i)
tag := field.Tag.Get(tagName)
if tag != "" {
tagValues[field.Name] = tag
}
}
return tagValues
}

// constructRows constructs the "rows" of the excel by taking the column name
// and retrieving the value of the struct field from the field name
func constructRows[T any](structs []T) []modelTableRow {
var rows []modelTableRow

for i, item := range structs {
fieldColumnMap := getTagValues(item, "column")
var cols []modelTableCol
for fieldName, columnName := range fieldColumnMap {
value, b := getFieldValue(item, fieldName)
if b {
column := constructCol(columnName, fieldName, value)
cols = append(cols, column)
}
}
toAdd := modelTableRow{
Index: i,
Cols: cols,
}
rows = append(rows, toAdd)
}

return rows
}

// constructCol constructs a column of the excel by taking the column name
// and retrieving the value of the struct field from the field name
func constructCol(columnName string, fieldName string, value interface{}) modelTableCol {
var parsed interface{}
kind := reflect.ValueOf(value).Type().Kind()

// checking if the value is a nil pointer
if kind == reflect.Pointer {
if reflect.ValueOf(value).IsNil() {
parsed = ""
} else {
parsed = reflect.ValueOf(value).Elem().Interface()
}
} else if kind == reflect.Struct {
// if the value is a nested struct, take the values to show...
structItemsMap := getTagValues(value, "columnInnerValue")
if len(structItemsMap) > 0 {
parsed = ""
for k, v := range structItemsMap {
fieldValue, found := getFieldValue(value, k)
if found {
//...and format them as a string
parsed = fmt.Sprintf("%s %s %v \n", parsed, v, fieldValue)
}
}
} else {
parsed = value
}
} else {
parsed = value
}

column := modelTableCol{
ColName: columnName,
FieldName: fieldName,
Value: parsed,
}
return column
}
Loading