156 lines
3.5 KiB
Go
156 lines
3.5 KiB
Go
package csv
|
|
|
|
import (
|
|
"context"
|
|
"encoding/csv"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/evcc-io/evcc/util/locale"
|
|
"github.com/fatih/structs"
|
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
"golang.org/x/text/language"
|
|
"golang.org/x/text/message"
|
|
"golang.org/x/text/number"
|
|
)
|
|
|
|
type Config struct {
|
|
I18nPrefix string // e.g., "sessions.csv" or "config.hems.csv"
|
|
}
|
|
|
|
func formatValue(mp *message.Printer, value any, digits int) string {
|
|
if rv := reflect.ValueOf(value); rv.Kind() == reflect.Pointer && rv.IsNil() {
|
|
return ""
|
|
}
|
|
|
|
switch v := value.(type) {
|
|
case float64:
|
|
return mp.Sprint(number.Decimal(v, number.NoSeparator(), number.MaxFractionDigits(digits)))
|
|
case *float64:
|
|
return mp.Sprint(number.Decimal(*v, number.NoSeparator(), number.MaxFractionDigits(digits)))
|
|
case time.Time:
|
|
if v.IsZero() {
|
|
return ""
|
|
}
|
|
return v.Local().Format("2006-01-02 15:04:05")
|
|
default:
|
|
return fmt.Sprintf("%v", value)
|
|
}
|
|
}
|
|
|
|
func writeHeader(ctx context.Context, ww *csv.Writer, structType any, i18nPrefix string) error {
|
|
localizer := locale.Localizer
|
|
if val, ok := ctx.Value(locale.Locale).(string); ok && val != "" {
|
|
localizer = i18n.NewLocalizer(locale.Bundle, val, locale.Language)
|
|
}
|
|
|
|
var row []string
|
|
for _, f := range structs.Fields(structType) {
|
|
csvTag := f.Tag("csv")
|
|
if csvTag == "-" {
|
|
continue
|
|
}
|
|
|
|
caption, err := localizer.Localize(&locale.Config{
|
|
MessageID: i18nPrefix + "." + strings.ToLower(f.Name()),
|
|
})
|
|
if err != nil {
|
|
if csvTag != "" {
|
|
caption = csvTag
|
|
} else {
|
|
caption = f.Name()
|
|
}
|
|
}
|
|
|
|
row = append(row, caption)
|
|
}
|
|
|
|
return ww.Write(row)
|
|
}
|
|
|
|
func writeRow(ww *csv.Writer, mp *message.Printer, structVal any) error {
|
|
var row []string
|
|
for _, f := range structs.Fields(structVal) {
|
|
if f.Tag("csv") == "-" {
|
|
continue
|
|
}
|
|
|
|
digits := 4
|
|
if format := f.Tag("format"); format == "int" {
|
|
digits = 0
|
|
}
|
|
|
|
val := formatValue(mp, f.Value(), digits)
|
|
row = append(row, val)
|
|
}
|
|
|
|
return ww.Write(row)
|
|
}
|
|
|
|
// NewLocalizedWriter writes a UTF-8 BOM and returns a locale-configured
|
|
// csv.Writer (German uses ';') and matching number printer.
|
|
func NewLocalizedWriter(ctx context.Context, w io.Writer) (*csv.Writer, *message.Printer, error) {
|
|
if _, err := w.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
lang := locale.Language
|
|
if v, ok := ctx.Value(locale.Locale).(string); ok && v != "" {
|
|
lang = v
|
|
}
|
|
if lang == "" {
|
|
lang = "en"
|
|
}
|
|
|
|
tag, err := language.Parse(lang)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
ww := csv.NewWriter(w)
|
|
if b, _ := tag.Base(); b.String() == language.German.String() {
|
|
ww.Comma = ';'
|
|
}
|
|
|
|
return ww, message.NewPrinter(tag), nil
|
|
}
|
|
|
|
// WriteStructSlice writes a slice of structs to CSV format with localized headers
|
|
func WriteStructSlice(ctx context.Context, w io.Writer, slice any, cfg Config) error {
|
|
ww, mp, err := NewLocalizedWriter(ctx, w)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sliceVal := reflect.ValueOf(slice)
|
|
if sliceVal.Kind() == reflect.Pointer {
|
|
sliceVal = sliceVal.Elem()
|
|
}
|
|
if sliceVal.Kind() != reflect.Slice {
|
|
return fmt.Errorf("expected slice, got %T", slice)
|
|
}
|
|
|
|
var structType any
|
|
if sliceVal.Len() > 0 {
|
|
structType = sliceVal.Index(0).Interface()
|
|
} else {
|
|
structType = reflect.New(sliceVal.Type().Elem()).Elem().Interface()
|
|
}
|
|
|
|
if err := writeHeader(ctx, ww, structType, cfg.I18nPrefix); err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := 0; i < sliceVal.Len(); i++ {
|
|
row := sliceVal.Index(i).Interface()
|
|
if err := writeRow(ww, mp, row); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
ww.Flush()
|
|
return ww.Error()
|
|
}
|