246 lines
4.8 KiB
Go
246 lines
4.8 KiB
Go
package excel
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/xuri/excelize/v2"
|
|
)
|
|
|
|
type Table struct {
|
|
Name string
|
|
Columns []string
|
|
Rows []map[string]any
|
|
}
|
|
|
|
type Engine struct {
|
|
mu sync.Mutex
|
|
filePath string
|
|
Tables map[string]*Table
|
|
}
|
|
|
|
func (e *Engine) Lock() { e.mu.Lock() }
|
|
func (e *Engine) Unlock() { e.mu.Unlock() }
|
|
|
|
func Open(filePath string) (*Engine, error) {
|
|
f, err := excelize.OpenFile(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open xlsx: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
e := &Engine{
|
|
filePath: filePath,
|
|
Tables: make(map[string]*Table),
|
|
}
|
|
|
|
sheets := f.GetSheetList()
|
|
for _, sheet := range sheets {
|
|
rows, err := f.GetRows(sheet)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read sheet %q: %w", sheet, err)
|
|
}
|
|
if len(rows) < 2 {
|
|
continue
|
|
}
|
|
|
|
t := &Table{
|
|
Name: sheet,
|
|
Columns: rows[0],
|
|
Rows: make([]map[string]any, 0, len(rows)-1),
|
|
}
|
|
|
|
for _, row := range rows[1:] {
|
|
rec := make(map[string]any, len(t.Columns))
|
|
for i, col := range t.Columns {
|
|
if i < len(row) {
|
|
rec[col] = inferValue(row[i])
|
|
} else {
|
|
rec[col] = nil
|
|
}
|
|
}
|
|
t.Rows = append(t.Rows, rec)
|
|
}
|
|
|
|
e.Tables[sheet] = t
|
|
}
|
|
|
|
if len(e.Tables) == 0 {
|
|
return nil, fmt.Errorf("no tables found in %s", filePath)
|
|
}
|
|
|
|
return e, nil
|
|
}
|
|
|
|
func (e *Engine) ListTables() []string {
|
|
names := make([]string, 0, len(e.Tables))
|
|
for n := range e.Tables {
|
|
names = append(names, n)
|
|
}
|
|
return names
|
|
}
|
|
|
|
func (e *Engine) GetTable(name string) *Table {
|
|
return e.Tables[name]
|
|
}
|
|
|
|
func (e *Engine) IDColumn(table string) string {
|
|
t, ok := e.Tables[table]
|
|
if !ok || len(t.Columns) == 0 {
|
|
return ""
|
|
}
|
|
return t.Columns[0]
|
|
}
|
|
|
|
func (e *Engine) CreateSheet(name string) error {
|
|
if _, exists := e.Tables[name]; exists {
|
|
return fmt.Errorf("sheet %q already exists", name)
|
|
}
|
|
e.Tables[name] = &Table{
|
|
Name: name,
|
|
Columns: []string{"ID"},
|
|
Rows: []map[string]any{},
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *Engine) DropSheet(name string) error {
|
|
if _, exists := e.Tables[name]; !exists {
|
|
return fmt.Errorf("sheet %q not found", name)
|
|
}
|
|
delete(e.Tables, name)
|
|
return nil
|
|
}
|
|
|
|
func (e *Engine) CreateTable(sheetName string, columns []string, rows []map[string]any) error {
|
|
t, exists := e.Tables[sheetName]
|
|
if !exists {
|
|
return fmt.Errorf("sheet %q not found", sheetName)
|
|
}
|
|
t.Columns = columns
|
|
t.Rows = rows
|
|
return nil
|
|
}
|
|
|
|
func (e *Engine) DropTable(name string) error {
|
|
t, exists := e.Tables[name]
|
|
if !exists {
|
|
return fmt.Errorf("table %q not found", name)
|
|
}
|
|
t.Columns = nil
|
|
t.Rows = nil
|
|
return nil
|
|
}
|
|
|
|
func (e *Engine) Flush() error {
|
|
f, err := excelize.OpenFile(e.filePath)
|
|
if err != nil {
|
|
return fmt.Errorf("open for flush: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
// Merge: import any sheets from the file that our in-memory state doesn't know about.
|
|
// This preserves external changes (e.g. edits made in Excel) across Flush calls.
|
|
for _, sheet := range f.GetSheetList() {
|
|
if _, exists := e.Tables[sheet]; exists {
|
|
continue
|
|
}
|
|
rows, err := f.GetRows(sheet)
|
|
if err != nil || len(rows) < 2 {
|
|
continue
|
|
}
|
|
t := &Table{
|
|
Name: sheet,
|
|
Columns: rows[0],
|
|
Rows: make([]map[string]any, 0, len(rows)-1),
|
|
}
|
|
for _, row := range rows[1:] {
|
|
rec := make(map[string]any, len(t.Columns))
|
|
for i, col := range t.Columns {
|
|
if i < len(row) {
|
|
rec[col] = inferValue(row[i])
|
|
} else {
|
|
rec[col] = nil
|
|
}
|
|
}
|
|
t.Rows = append(t.Rows, rec)
|
|
}
|
|
e.Tables[sheet] = t
|
|
}
|
|
|
|
existing := f.GetSheetList()
|
|
keep := make(map[string]bool)
|
|
for name := range e.Tables {
|
|
keep[name] = true
|
|
}
|
|
|
|
for _, s := range existing {
|
|
if !keep[s] {
|
|
f.DeleteSheet(s)
|
|
}
|
|
}
|
|
|
|
for _, t := range e.Tables {
|
|
if !sheetExists(f, t.Name) {
|
|
f.NewSheet(t.Name)
|
|
}
|
|
rows, err := f.GetRows(t.Name)
|
|
if err != nil {
|
|
rows = nil
|
|
}
|
|
for i := range rows {
|
|
for j := range rows[i] {
|
|
cell, _ := excelize.CoordinatesToCellName(j+1, i+1)
|
|
_ = f.SetCellValue(t.Name, cell, "")
|
|
}
|
|
}
|
|
|
|
for j, col := range t.Columns {
|
|
cell, _ := excelize.CoordinatesToCellName(j+1, 1)
|
|
_ = f.SetCellStr(t.Name, cell, col)
|
|
}
|
|
|
|
for i, row := range t.Rows {
|
|
for j, col := range t.Columns {
|
|
val, ok := row[col]
|
|
if !ok || val == nil {
|
|
continue
|
|
}
|
|
cell, _ := excelize.CoordinatesToCellName(j+1, i+2)
|
|
switch v := val.(type) {
|
|
case string:
|
|
_ = f.SetCellStr(t.Name, cell, v)
|
|
case float64:
|
|
_ = f.SetCellFloat(t.Name, cell, v, 2, 64)
|
|
case int:
|
|
_ = f.SetCellInt(t.Name, cell, int64(v))
|
|
default:
|
|
_ = f.SetCellStr(t.Name, cell, fmt.Sprintf("%v", v))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return f.Save()
|
|
}
|
|
|
|
func sheetExists(f *excelize.File, name string) bool {
|
|
for _, s := range f.GetSheetList() {
|
|
if s == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func inferValue(s string) any {
|
|
if i, err := strconv.Atoi(s); err == nil {
|
|
return i
|
|
}
|
|
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
|
return f
|
|
}
|
|
return s
|
|
}
|