feat: Migrate Excel plugin to built-in functionality

Move Excel plugin from external plugin to built-in functionality, including:
- Core Excel file operations (open, close, save)
- Sheet manipulation and cell operations
- Row and column iterators
- Process handlers for Excel operations
- Comprehensive test coverage

This change internalizes Excel manipulation capabilities:
- Read/write operations on cells, rows and columns
- Sheet management and styling
- Cell merging and formula handling
- Coordinate conversion utilities
This commit is contained in:
Max 2025-03-21 20:56:35 +08:00
parent 211e523759
commit f0da60f467
8 changed files with 1291 additions and 0 deletions

104
excel/each.go Normal file
View file

@ -0,0 +1,104 @@
package excel
import (
"fmt"
"sync"
"time"
"github.com/google/uuid"
"github.com/xuri/excelize/v2"
)
// Cols defines an iterator to a sheet
type Cols struct {
id string
*excelize.Cols
create int64
}
// Rows defines an iterator to a sheet
type Rows struct {
id string
*excelize.Rows
create int64
}
var openCols = sync.Map{}
var openRows = sync.Map{}
// OpenRow each row of the sheet
func (excel *Excel) OpenRow(sheet string) (string, error) {
id := uuid.NewString()
rows, err := excel.Rows(sheet)
if err != nil {
return "", err
}
openRows.Store(id, &Rows{id: id, Rows: rows, create: time.Now().Unix()})
return id, nil
}
// NextRow next row of the sheet
func NextRow(id string) ([]string, error) {
value, ok := openRows.Load(id)
if !ok {
return nil, fmt.Errorf("rows %s not found", id)
}
if value.(*Rows).Next() {
row, err := value.(*Rows).Columns()
// fmt.Printf("DEBUG: %#v %v %v\n", row, err, row == nil)
if err != nil {
return nil, err
}
if row == nil {
return []string{}, nil
}
return row, nil
}
return nil, nil
}
// CloseRow done the sheet
func CloseRow(id string) {
openRows.Delete(id)
}
// OpenColumn each cols of the sheet
func (excel *Excel) OpenColumn(sheet string) (string, error) {
id := uuid.NewString()
cols, err := excel.Cols(sheet)
if err != nil {
return "", err
}
openCols.Store(id, &Cols{id: id, Cols: cols, create: time.Now().Unix()})
return id, nil
}
// NextColumn next col of the sheet
func NextColumn(id string) ([]string, error) {
value, ok := openCols.Load(id)
if !ok {
return nil, fmt.Errorf("cols %s not found", id)
}
if value.(*Cols).Next() {
col, err := value.(*Cols).Rows()
if err != nil {
return nil, err
}
if col == nil {
return []string{}, nil
}
return col, nil
}
return nil, nil
}
// CloseColumn done the sheet
func CloseColumn(id string) {
openCols.Delete(id)
}

64
excel/each_test.go Normal file
View file

@ -0,0 +1,64 @@
package excel
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestEachCols(t *testing.T) {
files := testFiles(t)
h1, err := Open(files["test-01"], false)
if err != nil {
t.Fatal(err)
}
defer Close(h1)
xls, err := Get(h1)
if err != nil {
t.Fatal(err)
}
id, err := xls.OpenColumn("供销存管理表格")
if err != nil {
t.Fatal(err)
}
defer CloseColumn(id)
res := []string{}
for col, err := NextColumn(id); err == nil && col != nil; col, err = NextColumn(id) {
res = append(res, col...)
}
assert.Contains(t, strings.Join(res, ""), "供销存管理表格产品查询")
assert.Contains(t, strings.Join(res, ""), "刘大大")
}
func TestEachRows(t *testing.T) {
files := testFiles(t)
h1, err := Open(files["test-01"], false)
if err != nil {
t.Fatal(err)
}
defer Close(h1)
xls, err := Get(h1)
if err != nil {
t.Fatal(err)
}
id, err := xls.OpenRow("供销存管理表格")
if err != nil {
t.Fatal(err)
}
defer CloseRow(id)
res := []string{}
for row, err := NextRow(id); err == nil && row != nil; row, err = NextRow(id) {
res = append(res, row...)
}
assert.Contains(t, strings.Join(res, ""), "供销存管理表格产品查询")
assert.Contains(t, strings.Join(res, ""), "刘大大")
}

102
excel/excel.go Normal file
View file

@ -0,0 +1,102 @@
package excel
import (
"fmt"
"os"
"path/filepath"
"sync"
"time"
"github.com/google/uuid"
"github.com/xuri/excelize/v2"
"github.com/yaoapp/yao/config"
)
// Excel the excel file
type Excel struct {
id string
path string
create int64
*excelize.File
}
// openFiles the open files
var openFiles = sync.Map{}
// Open open the excel file
func Open(path string, writable bool) (string, error) {
excel := &Excel{path: path}
// GET DATA ROOT
root := config.Conf.DataRoot
path, err := filepath.Abs(filepath.Join(root, path))
if err != nil {
return "", err
}
if writable {
// if the file not exists, create it
if _, err := os.Stat(path); os.IsNotExist(err) {
create := excelize.NewFile()
err := create.SaveAs(path)
if err != nil {
return "", err
}
create.Close()
}
excelFile, err := excelize.OpenFile(path)
if err != nil {
return "", err
}
id := uuid.NewString()
excel.File = excelFile
excel.id = id
excel.create = time.Now().Unix()
openFiles.Store(id, excel)
return id, nil
}
file, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("open file %s failed: %w", path, err)
}
excelFile, err := excelize.OpenReader(file)
if err != nil {
return "", err
}
id := uuid.NewString()
excel.File = excelFile
excel.id = id
excel.create = time.Now().Unix()
openFiles.Store(id, excel)
return id, nil
}
// Close close the excel file
func Close(handler string) error {
excel, ok := openFiles.Load(handler)
if !ok {
return fmt.Errorf("file not found")
}
err := excel.(*Excel).Close()
if err != nil {
return err
}
openFiles.Delete(handler)
return nil
}
// Get get the excel file
func Get(handler string) (*Excel, error) {
excel, ok := openFiles.Load(handler)
if !ok {
return nil, fmt.Errorf("%s not found", handler)
}
return excel.(*Excel), nil
}

252
excel/excel_test.go Normal file
View file

@ -0,0 +1,252 @@
package excel
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/test"
)
func TestOpenClose(t *testing.T) {
files := testFiles(t)
h1, err := Open(files["test-01"], false)
if err != nil {
t.Fatal(err)
}
if _, ok := openFiles.Load(h1); !ok {
t.Fatal("open file failed")
}
h2, err := Open(files["test-02"], true)
if err != nil {
t.Fatal(err)
}
if _, ok := openFiles.Load(h2); !ok {
t.Fatal("open file failed")
}
h3, err := Open(files["test-03"], false)
if err != nil {
t.Fatal(err)
}
if _, ok := openFiles.Load(h3); !ok {
t.Fatal("open file failed")
}
_, err = Open(files["test-04"], false)
assert.Error(t, err)
err = Close(h1)
if err != nil {
t.Fatal(err)
}
if _, ok := openFiles.Load(h1); ok {
t.Fatal("close file failed")
}
}
func TestGetSheetList(t *testing.T) {
files := testFiles(t)
h1, err := Open(files["test-01"], false)
if err != nil {
t.Fatal(err)
}
defer Close(h1)
xls, err := Get(h1)
if err != nil {
t.Fatal(err)
}
sheets := xls.GetSheetList()
assert.Equal(t, []string{"供销存管理表格", "使用说明"}, sheets)
_, err = Get("not found")
assert.Error(t, err)
}
func TestOpenInvalidFile(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
// Create an invalid excel file in the data root
root := "excel"
invalidFile := filepath.Join(root, "invalid.xlsx")
// Ensure cleanup after test
defer func() {
if err := os.Remove(filepath.Join(config.Conf.DataRoot, invalidFile)); err != nil {
t.Logf("Failed to cleanup test file: %v", err)
}
}()
err := os.WriteFile(filepath.Join(config.Conf.DataRoot, invalidFile), []byte("invalid content"), 0644)
if err != nil {
t.Fatal(err)
}
_, err = Open(invalidFile, false)
assert.Error(t, err, "should fail to open invalid excel file")
}
func TestCloseErrors(t *testing.T) {
// Test closing non-existent handler
err := Close("non-existent-handler")
assert.Error(t, err, "should fail to close non-existent file")
// Test double close
files := testFiles(t)
h1, err := Open(files["test-01"], false)
if err != nil {
t.Fatal(err)
}
// First close
err = Close(h1)
assert.NoError(t, err)
// Second close should fail
err = Close(h1)
assert.Error(t, err, "should fail on second close")
}
func TestOpenWithInvalidPath(t *testing.T) {
// Test with invalid path
_, err := Open("../invalid/path/file.xlsx", false)
assert.Error(t, err, "should fail with invalid path")
// Test with path trying to escape data root
_, err = Open("../../../../etc/file.xlsx", false)
assert.Error(t, err, "should fail with path trying to escape data root")
}
func TestWrite(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
// Create a new writable file for testing
root := "excel"
testFile := filepath.Join(root, "write-test.xlsx")
// Ensure cleanup after test
defer func() {
if err := os.Remove(filepath.Join(config.Conf.DataRoot, testFile)); err != nil {
t.Logf("Failed to cleanup test file: %v", err)
}
}()
h1, err := Open(testFile, true)
if err != nil {
t.Fatal(err)
}
defer Close(h1)
xls, err := Get(h1)
if err != nil {
t.Fatal(err)
}
// Test WriteCell
err = xls.WriteCell("Sheet1", "A1", "Hello")
assert.NoError(t, err)
err = xls.WriteCell("Sheet1", "B1", 123)
assert.NoError(t, err)
err = xls.WriteCell("Sheet1", "C1", true)
assert.NoError(t, err)
// Test WriteRow
row := []interface{}{"Row1", 456, false}
err = xls.WriteRow("Sheet1", "A2", row)
assert.NoError(t, err)
// Test WriteColumn
col := []interface{}{"Col1", 789, true}
err = xls.WriteColumn("Sheet1", "D1", col)
assert.NoError(t, err)
// Test WriteAll
data := [][]interface{}{
{"Name", "Age", "City"},
{"John", 30, "New York"},
{"Alice", 25, "London"},
}
err = xls.WriteAll("Sheet2", "A1", data)
assert.NoError(t, err)
// Test error cases
// Invalid cell reference
err = xls.WriteCell("Sheet1", "invalid", "test")
assert.Error(t, err)
// Save the file to verify changes
err = xls.SaveAs(filepath.Join(config.Conf.DataRoot, testFile))
assert.NoError(t, err)
// Verify written data
val, err := xls.GetCellValue("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "Hello", val)
val, err = xls.GetCellValue("Sheet2", "A1")
assert.NoError(t, err)
assert.Equal(t, "Name", val)
}
func TestSetSheet(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
root := "excel"
testFile := filepath.Join(root, "sheet-test.xlsx")
// Ensure cleanup after test
defer func() {
if err := os.Remove(filepath.Join(config.Conf.DataRoot, testFile)); err != nil {
t.Logf("Failed to cleanup test file: %v", err)
}
}()
h1, err := Open(testFile, true)
if err != nil {
t.Fatal(err)
}
defer Close(h1)
xls, err := Get(h1)
if err != nil {
t.Fatal(err)
}
// Test creating new sheet
idx, err := xls.SetSheet("NewSheet")
assert.NoError(t, err)
assert.Greater(t, idx, 0)
// Test getting existing sheet
idx2, err := xls.SetSheet("NewSheet")
assert.NoError(t, err)
assert.Equal(t, idx, idx2)
// Verify sheet exists
sheets := xls.GetSheetList()
assert.Contains(t, sheets, "NewSheet")
}
func testFiles(t *testing.T) map[string]string {
test.Prepare(t, config.Conf)
defer test.Clean()
// test data root path
root := "excel"
return map[string]string{
"test-01": filepath.Join(root, "test-01.xlsx"),
"test-02": filepath.Join(root, "test-02.xlsx"),
"test-03": filepath.Join(root, "test-03.xlsx"),
}
}

261
excel/process.go Normal file
View file

@ -0,0 +1,261 @@
package excel
import (
"github.com/xuri/excelize/v2"
"github.com/yaoapp/gou/process"
"github.com/yaoapp/kun/exception"
)
func init() {
process.RegisterGroup("excel", map[string]process.Handler{
"open": processOpen,
"close": processClose,
"save": processSave,
"sheets": processSheets,
"read.cell": processReadCell,
"write.cell": processWriteCell,
"set.style": processSetStyle,
"set.formula": processSetFormula,
"set.link": processSetLink,
"set.mergecell": processMergeCell,
"set.unmergecell": processUnmergeCell,
"convert.columnnametonumber": processColumnNameToNumber,
"convert.columnnumbertoname": processColumnNumberToName,
"convert.cellnametocoordinates": processCellNameToCoordinates,
"convert.coordinatestocellname": processCoordinatesToCellName,
})
}
// processOpen process the excel.open <file> <readonly>
func processOpen(process *process.Process) interface{} {
process.ValidateArgNums(1)
file := process.ArgsString(0)
readonly := false
if len(process.Args) > 1 {
readonly = process.ArgsBool(1)
}
handle, err := Open(file, readonly)
if err != nil {
exception.New("excel.open %s error: %s", 500, file, err.Error()).Throw()
}
return handle
}
// processClose process the excel.close <handle>
func processClose(process *process.Process) interface{} {
process.ValidateArgNums(1)
handle := process.ArgsString(0)
err := Close(handle)
if err != nil {
exception.New("excel.close %s error: %s", 500, handle, err.Error()).Throw()
}
return nil
}
// processSave process the excel.save <handle>
func processSave(process *process.Process) interface{} {
process.ValidateArgNums(1)
handle := process.ArgsString(0)
xls, err := Get(handle)
if err != nil {
exception.New("excel.save %s error: %s", 500, handle, err.Error()).Throw()
}
err = xls.Save()
if err != nil {
exception.New("excel.save %s error: %s", 500, handle, err.Error()).Throw()
}
return nil
}
// processSheets process the excel.sheets <handle>
func processSheets(process *process.Process) interface{} {
process.ValidateArgNums(1)
handle := process.ArgsString(0)
xls, err := Get(handle)
if err != nil {
exception.New("excel.sheets %s error: %s", 500, handle, err.Error()).Throw()
}
return xls.GetSheetList()
}
// processReadCell process the excel.read.cell <handle> <sheet> <cell>
func processReadCell(process *process.Process) interface{} {
process.ValidateArgNums(3)
handle := process.ArgsString(0)
sheet := process.ArgsString(1)
cell := process.ArgsString(2)
xls, err := Get(handle)
if err != nil {
exception.New("excel.read.cell %s error: %s", 500, handle, err.Error()).Throw()
}
value, err := xls.GetCellValue(sheet, cell)
if err != nil {
exception.New("excel.read.cell %s:%s:%s error: %s", 500, handle, sheet, cell, err.Error()).Throw()
}
return value
}
// processWriteCell process the excel.write.cell <handle> <sheet> <cell> <value>
func processWriteCell(process *process.Process) interface{} {
process.ValidateArgNums(4)
handle := process.ArgsString(0)
sheet := process.ArgsString(1)
cell := process.ArgsString(2)
value := process.Args[3]
xls, err := Get(handle)
if err != nil {
exception.New("excel.write.cell %s error: %s", 500, handle, err.Error()).Throw()
}
err = xls.SetCellValue(sheet, cell, value)
if err != nil {
exception.New("excel.write.cell %s:%s:%s error: %s", 500, handle, sheet, cell, err.Error()).Throw()
}
return nil
}
// processSetStyle process the excel.set.style <handle> <sheet> <cell> <style>
func processSetStyle(process *process.Process) interface{} {
process.ValidateArgNums(4)
handle := process.ArgsString(0)
sheet := process.ArgsString(1)
cell := process.ArgsString(2)
styleID := process.ArgsInt(3)
xls, err := Get(handle)
if err != nil {
exception.New("excel.set.style %s error: %s", 500, handle, err.Error()).Throw()
}
err = xls.SetCellStyle(sheet, cell, cell, styleID)
if err != nil {
exception.New("excel.set.style %s:%s:%s error: %s", 500, handle, sheet, cell, err.Error()).Throw()
}
return nil
}
// processSetFormula process the excel.set.formula <handle> <sheet> <cell> <formula>
func processSetFormula(process *process.Process) interface{} {
process.ValidateArgNums(4)
handle := process.ArgsString(0)
sheet := process.ArgsString(1)
cell := process.ArgsString(2)
formula := process.ArgsString(3)
xls, err := Get(handle)
if err != nil {
exception.New("excel.set.formula %s error: %s", 500, handle, err.Error()).Throw()
}
err = xls.SetCellFormula(sheet, cell, formula)
if err != nil {
exception.New("excel.set.formula %s:%s:%s error: %s", 500, handle, sheet, cell, err.Error()).Throw()
}
return nil
}
// processSetLink process the excel.set.link <handle> <sheet> <cell> <link> <text>
func processSetLink(process *process.Process) interface{} {
process.ValidateArgNums(5)
handle := process.ArgsString(0)
sheet := process.ArgsString(1)
cell := process.ArgsString(2)
link := process.ArgsString(3)
text := process.ArgsString(4)
xls, err := Get(handle)
if err != nil {
exception.New("excel.set.link %s error: %s", 500, handle, err.Error()).Throw()
}
err = xls.SetCellHyperLink(sheet, cell, link, text)
if err != nil {
exception.New("excel.set.link %s:%s:%s error: %s", 500, handle, sheet, cell, err.Error()).Throw()
}
return nil
}
// processMergeCell process the excel.set.mergecell <handle> <sheet> <start> <end>
func processMergeCell(process *process.Process) interface{} {
process.ValidateArgNums(4)
handle := process.ArgsString(0)
sheet := process.ArgsString(1)
start := process.ArgsString(2)
end := process.ArgsString(3)
xls, err := Get(handle)
if err != nil {
exception.New("excel.set.mergecell %s error: %s", 500, handle, err.Error()).Throw()
}
err = xls.MergeCell(sheet, start, end)
if err != nil {
exception.New("excel.set.mergecell %s:%s:%s:%s error: %s", 500, handle, sheet, start, end, err.Error()).Throw()
}
return nil
}
// processUnmergeCell process the excel.set.unmergecell <handle> <sheet> <start> <end>
func processUnmergeCell(process *process.Process) interface{} {
process.ValidateArgNums(4)
handle := process.ArgsString(0)
sheet := process.ArgsString(1)
start := process.ArgsString(2)
end := process.ArgsString(3)
xls, err := Get(handle)
if err != nil {
exception.New("excel.set.unmergecell %s error: %s", 500, handle, err.Error()).Throw()
}
err = xls.UnmergeCell(sheet, start, end)
if err != nil {
exception.New("excel.set.unmergecell %s:%s:%s:%s error: %s", 500, handle, sheet, start, end, err.Error()).Throw()
}
return nil
}
// processColumnNameToNumber process the excel.convert.columnnametonumber <name>
func processColumnNameToNumber(process *process.Process) interface{} {
process.ValidateArgNums(1)
name := process.ArgsString(0)
number, err := excelize.ColumnNameToNumber(name)
if err != nil {
exception.New("excel.convert.columnnametonumber %s error: %s", 500, name, err.Error()).Throw()
}
return number
}
// processColumnNumberToName process the excel.convert.columnnumbertoname <number>
func processColumnNumberToName(process *process.Process) interface{} {
process.ValidateArgNums(1)
number := process.ArgsInt(0)
name, err := excelize.ColumnNumberToName(number)
if err != nil {
exception.New("excel.convert.columnnumbertoname %d error: %s", 500, number, err.Error()).Throw()
}
return name
}
// processCellNameToCoordinates process the excel.convert.cellnametocoordinates <cell>
func processCellNameToCoordinates(process *process.Process) interface{} {
process.ValidateArgNums(1)
cell := process.ArgsString(0)
x, y, err := excelize.CellNameToCoordinates(cell)
if err != nil {
exception.New("excel.convert.cellnametocoordinates %s error: %s", 500, cell, err.Error()).Throw()
}
return []int{x, y}
}
// processCoordinatesToCellName process the excel.convert.coordinatestocellname <col> <row>
func processCoordinatesToCellName(process *process.Process) interface{} {
process.ValidateArgNums(2)
col := process.ArgsInt(0)
row := process.ArgsInt(1)
cell, err := excelize.CoordinatesToCellName(col, row)
if err != nil {
exception.New("excel.convert.coordinatestocellname %d,%d error: %s", 500, col, row, err.Error()).Throw()
}
return cell
}

432
excel/process_test.go Normal file
View file

@ -0,0 +1,432 @@
package excel
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/gou/process"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/test"
)
func TestProcessOpen(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
files := testFiles(t)
// Test opening file in read-only mode
p, err := process.Of("excel.open", files["test-01"], true)
if err != nil {
t.Fatal(err)
}
handle, err := p.Exec()
if err != nil {
t.Fatal(err)
}
assert.NotEmpty(t, handle)
// Test opening file in write mode
p, err = process.Of("excel.open", files["test-01"], false)
if err != nil {
t.Fatal(err)
}
handle2, err := p.Exec()
if err != nil {
t.Fatal(err)
}
assert.NotEmpty(t, handle2)
}
func TestProcessSheets(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
files := testFiles(t)
// Open file first
p, err := process.Of("excel.open", files["test-01"], true)
if err != nil {
t.Fatal(err)
}
handle, err := p.Exec()
if err != nil {
t.Fatal(err)
}
// Get sheets
p, err = process.Of("excel.sheets", handle)
if err != nil {
t.Fatal(err)
}
sheets, err := p.Exec()
if err != nil {
t.Fatal(err)
}
sheetList := sheets.([]string)
assert.Equal(t, []string{"供销存管理表格", "使用说明"}, sheetList)
}
func TestProcessReadCell(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
files := testFiles(t)
// Open file first
p, err := process.Of("excel.open", files["test-01"], true)
if err != nil {
t.Fatal(err)
}
handle, err := p.Exec()
if err != nil {
t.Fatal(err)
}
// Get sheets first to verify we have the right sheet
p, err = process.Of("excel.sheets", handle)
if err != nil {
t.Fatal(err)
}
sheets, err := p.Exec()
if err != nil {
t.Fatal(err)
}
sheetList := sheets.([]string)
if len(sheetList) == 0 {
t.Fatal("no sheets found")
}
// Read cell from the first sheet
p, err = process.Of("excel.read.cell", handle, sheetList[0], "B2")
if err != nil {
t.Fatal(err)
}
value, err := p.Exec()
if err != nil {
t.Fatal(err)
}
// Print the value for debugging
assert.NotEmpty(t, value)
// Try reading from a non-existent cell
p, err = process.Of("excel.read.cell", handle, sheetList[0], "ZZ999")
if err != nil {
t.Fatal(err)
}
value, err = p.Exec()
if err != nil {
t.Fatal(err)
}
assert.Empty(t, value) // Non-existent cell should return empty string
}
func TestProcessClose(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
files := testFiles(t)
// Open file first
p, err := process.Of("excel.open", files["test-01"], true)
if err != nil {
t.Fatal(err)
}
handle, err := p.Exec()
if err != nil {
t.Fatal(err)
}
// Close file
p, err = process.Of("excel.close", handle)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.NoError(t, err)
// Try to use closed handle
p, err = process.Of("excel.sheets", handle)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.Error(t, err)
}
func TestProcessConvert(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
// Test ColumnNameToNumber
p, err := process.Of("excel.convert.columnnametonumber", "AK")
if err != nil {
t.Fatal(err)
}
number, err := p.Exec()
if err != nil {
t.Fatal(err)
}
assert.Equal(t, 37, number)
// Test ColumnNumberToName
p, err = process.Of("excel.convert.columnnumbertoname", 37)
if err != nil {
t.Fatal(err)
}
name, err := p.Exec()
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "AK", name)
// Test CellNameToCoordinates
p, err = process.Of("excel.convert.cellnametocoordinates", "A1")
if err != nil {
t.Fatal(err)
}
coords, err := p.Exec()
if err != nil {
t.Fatal(err)
}
coordsArr := coords.([]int)
assert.Equal(t, []int{1, 1}, coordsArr)
// Test CoordinatesToCellName
p, err = process.Of("excel.convert.coordinatestocellname", 1, 1)
if err != nil {
t.Fatal(err)
}
cell, err := p.Exec()
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "A1", cell)
}
func TestProcessSave(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
files := testFiles(t)
// Create a new test file path
dataRoot := config.Conf.DataRoot
newFile := filepath.Join(filepath.Dir(files["test-01"]), "test-save.xlsx")
// Copy test-01.xlsx to new file
content, err := os.ReadFile(filepath.Join(dataRoot, files["test-01"]))
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(dataRoot, newFile), content, 0644)
if err != nil {
t.Fatal(err)
}
defer os.Remove(filepath.Join(dataRoot, newFile)) // Clean up after test
// Open new file in write mode
p, err := process.Of("excel.open", newFile, true)
if err != nil {
t.Fatal(err)
}
handle, err := p.Exec()
if err != nil {
t.Fatal(err)
}
// Write something to the file
p, err = process.Of("excel.write.cell", handle, "供销存管理表格", "A1", "Test Save")
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
if err != nil {
t.Fatal(err)
}
// Save the file
p, err = process.Of("excel.save", handle)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.NoError(t, err)
// Close the file before reading
p, err = process.Of("excel.close", handle)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.NoError(t, err)
// Verify the file exists and has been modified
savedContent, err := os.ReadFile(filepath.Join(dataRoot, newFile))
if err != nil {
t.Fatal(err)
}
assert.NotEqual(t, content, savedContent, "New file should be modified")
// Verify original file is unchanged
originalContent, err := os.ReadFile(filepath.Join(dataRoot, files["test-01"]))
if err != nil {
t.Fatal(err)
}
assert.Equal(t, content, originalContent, "Original file should not be modified")
// Try to save with invalid handle
p, err = process.Of("excel.save", "invalid-handle")
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.Error(t, err)
}
func TestProcessWriteCell(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
files := testFiles(t)
// Open file in write mode
p, err := process.Of("excel.open", files["test-01"], false)
if err != nil {
t.Fatal(err)
}
handle, err := p.Exec()
if err != nil {
t.Fatal(err)
}
// Write string value
p, err = process.Of("excel.write.cell", handle, "供销存管理表格", "A1", "Test Write")
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.NoError(t, err)
// Write number value
p, err = process.Of("excel.write.cell", handle, "供销存管理表格", "B1", 123.45)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.NoError(t, err)
// Verify written values
p, err = process.Of("excel.read.cell", handle, "供销存管理表格", "A1")
if err != nil {
t.Fatal(err)
}
value, err := p.Exec()
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "Test Write", value)
// Test with invalid handle
p, err = process.Of("excel.write.cell", "invalid-handle", "供销存管理表格", "A1", "Test")
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.Error(t, err)
// Test with invalid sheet name
p, err = process.Of("excel.write.cell", handle, "InvalidSheet", "A1", "Test")
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.Error(t, err)
}
func TestProcessSetStyle(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
files := testFiles(t)
// Open file in write mode
p, err := process.Of("excel.open", files["test-01"], false)
if err != nil {
t.Fatal(err)
}
handle, err := p.Exec()
if err != nil {
t.Fatal(err)
}
// Set style
p, err = process.Of("excel.set.style", handle, "供销存管理表格", "A1", 1)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.NoError(t, err)
// Test with invalid handle
p, err = process.Of("excel.set.style", "invalid-handle", "供销存管理表格", "A1", 1)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.Error(t, err)
// Test with invalid sheet name
p, err = process.Of("excel.set.style", handle, "InvalidSheet", "A1", 1)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.Error(t, err)
// Test with invalid style ID
p, err = process.Of("excel.set.style", handle, "供销存管理表格", "A1", -1)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.Error(t, err)
}

75
excel/write.go Normal file
View file

@ -0,0 +1,75 @@
package excel
import (
"github.com/xuri/excelize/v2"
)
// WriteCell write the cell
func (excel *Excel) WriteCell(sheet string, cell string, value interface{}) error {
_, err := excel.SetSheet(sheet)
if err != nil {
return err
}
return excel.SetCellValue(sheet, cell, value)
}
// WriteRow write the row
func (excel *Excel) WriteRow(sheet string, cell string, value []interface{}) error {
_, err := excel.SetSheet(sheet)
if err != nil {
return err
}
return excel.SetSheetRow(sheet, cell, &value)
}
// WriteColumn write the column
func (excel *Excel) WriteColumn(sheet string, cell string, value []interface{}) error {
_, err := excel.SetSheet(sheet)
if err != nil {
return err
}
return excel.SetSheetCol(sheet, cell, &value)
}
// WriteAll write all the sheet
func (excel *Excel) WriteAll(sheet string, cell string, rows [][]interface{}) error {
_, err := excel.SetSheet(sheet)
if err != nil {
return err
}
for _, row := range rows {
if err := excel.SetSheetRow(sheet, cell, &row); err != nil {
return err
}
colIndex, rowIndex, err := excelize.CellNameToCoordinates(cell)
cell, err = excelize.CoordinatesToCellName(colIndex, rowIndex+1)
if err != nil {
return err
}
}
return nil
}
// SetSheet set the sheet
func (excel *Excel) SetSheet(name string) (int, error) {
idx, err := excel.GetSheetIndex(name)
if err != nil {
return 0, err
}
if idx == -1 {
idx, err = excel.NewSheet(name)
if err != nil {
return 0, err
}
}
return idx, nil
}

View file

@ -8,6 +8,7 @@ import (
_ "github.com/yaoapp/gou/encoding" _ "github.com/yaoapp/gou/encoding"
_ "github.com/yaoapp/yao/aigc" _ "github.com/yaoapp/yao/aigc"
_ "github.com/yaoapp/yao/crypto" _ "github.com/yaoapp/yao/crypto"
_ "github.com/yaoapp/yao/excel"
_ "github.com/yaoapp/yao/helper" _ "github.com/yaoapp/yao/helper"
_ "github.com/yaoapp/yao/openai" _ "github.com/yaoapp/yao/openai"
_ "github.com/yaoapp/yao/wework" _ "github.com/yaoapp/yao/wework"