initial commit
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user