From 6e4b7cd59e2932927e99a841ffcb4b9fbcdf05ef Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 29 Mar 2025 17:02:18 +0800 Subject: [PATCH] feat: Add sheet management operations to Excel module - Implement new process handlers for creating, reading, updating, copying, and deleting sheets in Excel files. - Enhance the Open function to automatically create directories for file paths if they do not exist. - Update README.md to include detailed documentation and examples for new sheet operations, improving user guidance. --- excel/README.md | 119 +++++++++++++++++++++++++++++++ excel/excel.go | 10 +++ excel/process.go | 126 +++++++++++++++++++++++++++++++++ excel/process_test.go | 158 ++++++++++++++++++++++++++++++++++++++++++ excel/sheet.go | 130 ++++++++++++++++++++++++++++++++++ excel/sheet_test.go | 123 ++++++++++++++++++++++++++++++++ 6 files changed, 666 insertions(+) create mode 100644 excel/sheet.go create mode 100644 excel/sheet_test.go diff --git a/excel/README.md b/excel/README.md index 6a2097b5..e5624beb 100644 --- a/excel/README.md +++ b/excel/README.md @@ -49,6 +49,125 @@ const h2: string = Process("excel.Open", "file.xlsx"); // Process("excel.Close", h); ``` +### Sheet Operations + +#### Create a new sheet + +```typescript +/** + * Creates a new sheet in the workbook + * @param handle - Handle ID from excel.open + * @param name - Name for the new sheet + * @returns number - Index of the new sheet + */ +const idx: number = Process("excel.sheet.create", h, "NewSheet"); +``` + +#### List all sheets + +```typescript +/** + * Lists all sheets in the workbook + * @param handle - Handle ID from excel.open + * @returns string[] - Array of sheet names + */ +const sheets: string[] = Process("excel.sheet.list", h); +// Example output: ["Sheet1", "Sheet2", "NewSheet"] +``` + +#### Read sheet data + +```typescript +/** + * Reads all data from a sheet + * @param handle - Handle ID from excel.open + * @param name - Sheet name + * @returns any[][] - Two-dimensional array of cell values + */ +const data: any[][] = Process("excel.sheet.read", h, "Sheet1"); +``` + +#### Update sheet data + +```typescript +/** + * Updates data in a sheet. Creates the sheet if it doesn't exist. + * @param handle - Handle ID from excel.open + * @param name - Sheet name + * @param data - Two-dimensional array of values to write + * @returns null + */ +const data = [ + ["Header1", "Header2", "Header3"], + [1, "Data1", true], + [2, "Data2", false], +]; +Process("excel.sheet.update", h, "Sheet1", data); +``` + +#### Copy a sheet + +```typescript +/** + * Copies a sheet with all its content and formatting + * @param handle - Handle ID from excel.open + * @param source - Source sheet name + * @param target - Target sheet name (must not exist) + * @returns null + */ +Process("excel.sheet.copy", h, "Sheet1", "Sheet1Copy"); +``` + +#### Delete a sheet + +```typescript +/** + * Deletes a sheet from the workbook + * @param handle - Handle ID from excel.open + * @param name - Sheet name to delete + * @returns null + */ +Process("excel.sheet.delete", h, "Sheet1Copy"); +``` + +### Example: Sheet Operations Workflow + +```typescript +// Open Excel file in writable mode +const h: string = Process("excel.Open", "file.xlsx", true); + +// Create a new sheet +const idx: number = Process("excel.sheet.create", h, "DataSheet"); + +// Write some data to the new sheet +const data = [ + ["Name", "Age", "Active"], + ["John", 30, true], + ["Jane", 25, false], +]; +Process("excel.sheet.update", h, "DataSheet", data); + +// Make a backup copy of the sheet +Process("excel.sheet.copy", h, "DataSheet", "DataSheet_Backup"); + +// List all sheets to verify +const sheets: string[] = Process("excel.sheet.list", h); +console.log("Available sheets:", sheets); + +// Read data from the backup sheet +const backupData: any[][] = Process("excel.sheet.read", h, "DataSheet_Backup"); +console.log("Backup data:", backupData); + +// Delete the backup sheet when no longer needed +Process("excel.sheet.delete", h, "DataSheet_Backup"); + +// Save changes +Process("excel.Save", h); + +// IMPORTANT: Always close the handle when done +Process("excel.Close", h); +``` + #### Get all sheets in the workbook ```typescript diff --git a/excel/excel.go b/excel/excel.go index b342fe8c..5c5847eb 100644 --- a/excel/excel.go +++ b/excel/excel.go @@ -39,6 +39,16 @@ func Open(path string, writable bool) (string, error) { // if the file not exists, create it if _, err := os.Stat(absPath); os.IsNotExist(err) { + + // Auto create dir + dir := filepath.Dir(absPath) + if _, err := os.Stat(dir); os.IsNotExist(err) { + err := os.MkdirAll(dir, 0644) + if err != nil { + return "", err + } + } + create := excelize.NewFile() err := create.SaveAs(absPath) if err != nil { diff --git a/excel/process.go b/excel/process.go index f6bdf37f..5a56495d 100644 --- a/excel/process.go +++ b/excel/process.go @@ -13,6 +13,13 @@ func init() { "save": processSave, "sheets": processSheets, + "sheet.create": processCreateSheet, + "sheet.read": processReadSheet, + "sheet.update": processUpdateSheet, + "sheet.delete": processDeleteSheet, + "sheet.copy": processCopySheet, + "sheet.list": processListSheets, + "read.cell": processReadCell, "read.row": processReadRow, "read.column": processReadColumn, @@ -623,3 +630,122 @@ func processNextColumn(process *process.Process) interface{} { return col } + +// processCreateSheet process the excel.sheet.create +func processCreateSheet(process *process.Process) interface{} { + process.ValidateArgNums(2) + handle := process.ArgsString(0) + name := process.ArgsString(1) + + xls, err := Get(handle) + if err != nil { + exception.New("excel.sheet.create %s error: %s", 500, handle, err.Error()).Throw() + } + + idx, err := xls.CreateSheet(name) + if err != nil { + exception.New("excel.sheet.create %s:%s error: %s", 500, handle, name, err.Error()).Throw() + } + return idx +} + +// processReadSheet process the excel.sheet.read +func processReadSheet(process *process.Process) interface{} { + process.ValidateArgNums(2) + handle := process.ArgsString(0) + name := process.ArgsString(1) + + xls, err := Get(handle) + if err != nil { + exception.New("excel.sheet.read %s error: %s", 500, handle, err.Error()).Throw() + } + + data, err := xls.ReadSheet(name) + if err != nil { + exception.New("excel.sheet.read %s:%s error: %s", 500, handle, name, err.Error()).Throw() + } + return data +} + +// processUpdateSheet process the excel.sheet.update +func processUpdateSheet(process *process.Process) interface{} { + process.ValidateArgNums(3) + handle := process.ArgsString(0) + name := process.ArgsString(1) + data := process.Args[2] + + xls, err := Get(handle) + if err != nil { + exception.New("excel.sheet.update %s error: %s", 500, handle, err.Error()).Throw() + } + + // Convert data to [][]interface{} + var sheetData [][]interface{} + if arr, ok := data.([]interface{}); ok { + for _, row := range arr { + if rowArr, ok := row.([]interface{}); ok { + sheetData = append(sheetData, rowArr) + } else { + sheetData = append(sheetData, []interface{}{row}) + } + } + } else { + sheetData = [][]interface{}{{data}} + } + + err = xls.UpdateSheet(name, sheetData) + if err != nil { + exception.New("excel.sheet.update %s:%s error: %s", 500, handle, name, err.Error()).Throw() + } + return nil +} + +// processDeleteSheet process the excel.sheet.delete +func processDeleteSheet(process *process.Process) interface{} { + process.ValidateArgNums(2) + handle := process.ArgsString(0) + name := process.ArgsString(1) + + xls, err := Get(handle) + if err != nil { + exception.New("excel.sheet.delete %s error: %s", 500, handle, err.Error()).Throw() + } + + err = xls.DeleteSheet(name) + if err != nil { + exception.New("excel.sheet.delete %s:%s error: %s", 500, handle, name, err.Error()).Throw() + } + return nil +} + +// processCopySheet process the excel.sheet.copy +func processCopySheet(process *process.Process) interface{} { + process.ValidateArgNums(3) + handle := process.ArgsString(0) + source := process.ArgsString(1) + target := process.ArgsString(2) + + xls, err := Get(handle) + if err != nil { + exception.New("excel.sheet.copy %s error: %s", 500, handle, err.Error()).Throw() + } + + err = xls.CopySheet(source, target) + if err != nil { + exception.New("excel.sheet.copy %s:%s:%s error: %s", 500, handle, source, target, err.Error()).Throw() + } + return nil +} + +// processListSheets process the excel.sheet.list +func processListSheets(process *process.Process) interface{} { + process.ValidateArgNums(1) + handle := process.ArgsString(0) + + xls, err := Get(handle) + if err != nil { + exception.New("excel.sheet.list %s error: %s", 500, handle, err.Error()).Throw() + } + + return xls.ListSheets() +} diff --git a/excel/process_test.go b/excel/process_test.go index 0b4a10d4..479827dc 100644 --- a/excel/process_test.go +++ b/excel/process_test.go @@ -758,3 +758,161 @@ func TestProcessIterators(t *testing.T) { _, err = p.Exec() assert.NoError(t, err) } + +func TestProcessSheetOperations(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-sheet-ops.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 file in write mode + p, err := process.Of("excel.open", newFile, false) + if err != nil { + t.Fatal(err) + } + + handle, err := p.Exec() + if err != nil { + t.Fatal(err) + } + + // Test sheet.create + t.Run("CreateSheet", func(t *testing.T) { + p, err := process.Of("excel.sheet.create", handle, "TestSheet1") + assert.NoError(t, err) + + idx, err := p.Exec() + assert.NoError(t, err) + assert.Greater(t, idx.(int), 0) + + // Try to create a sheet with the same name (should fail) + p, err = process.Of("excel.sheet.create", handle, "TestSheet1") + assert.NoError(t, err) + + _, err = p.Exec() + assert.Error(t, err) + }) + + // Test sheet.list + t.Run("ListSheets", func(t *testing.T) { + p, err := process.Of("excel.sheet.list", handle) + assert.NoError(t, err) + + sheets, err := p.Exec() + assert.NoError(t, err) + sheetList := sheets.([]string) + assert.Contains(t, sheetList, "TestSheet1") + }) + + // Test sheet.update and sheet.read + t.Run("UpdateAndReadSheet", func(t *testing.T) { + testData := [][]interface{}{ + {"Header1", "Header2"}, + {1, "Data1"}, + {2, "Data2"}, + } + + p, err := process.Of("excel.sheet.update", handle, "TestSheet1", testData) + assert.NoError(t, err) + + _, err = p.Exec() + assert.NoError(t, err) + + // Read and verify + p, err = process.Of("excel.sheet.read", handle, "TestSheet1") + assert.NoError(t, err) + + data, err := p.Exec() + assert.NoError(t, err) + assert.NotNil(t, data) + + // Try to read non-existent sheet + p, err = process.Of("excel.sheet.read", handle, "NonExistentSheet") + assert.NoError(t, err) + + _, err = p.Exec() + assert.Error(t, err) + }) + + // Test sheet.copy + t.Run("CopySheet", func(t *testing.T) { + p, err := process.Of("excel.sheet.copy", handle, "TestSheet1", "CopiedSheet") + assert.NoError(t, err) + + _, err = p.Exec() + assert.NoError(t, err) + + // Verify the copy exists + p, err = process.Of("excel.sheet.list", handle) + assert.NoError(t, err) + + sheets, err := p.Exec() + assert.NoError(t, err) + sheetList := sheets.([]string) + assert.Contains(t, sheetList, "CopiedSheet") + + // Try to copy to existing sheet name (should fail) + p, err = process.Of("excel.sheet.copy", handle, "TestSheet1", "CopiedSheet") + assert.NoError(t, err) + + _, err = p.Exec() + assert.Error(t, err) + }) + + // Test sheet.delete + t.Run("DeleteSheet", func(t *testing.T) { + p, err := process.Of("excel.sheet.delete", handle, "CopiedSheet") + assert.NoError(t, err) + + _, err = p.Exec() + assert.NoError(t, err) + + // Verify the sheet is deleted + p, err = process.Of("excel.sheet.list", handle) + assert.NoError(t, err) + + sheets, err := p.Exec() + assert.NoError(t, err) + sheetList := sheets.([]string) + assert.NotContains(t, sheetList, "CopiedSheet") + + // Try to delete non-existent sheet + p, err = process.Of("excel.sheet.delete", handle, "NonExistentSheet") + assert.NoError(t, err) + + _, err = p.Exec() + assert.Error(t, err) + }) + + // Save and close + p, err = process.Of("excel.save", handle) + if err != nil { + t.Fatal(err) + } + + _, err = p.Exec() + assert.NoError(t, err) + + p, err = process.Of("excel.close", handle) + if err != nil { + t.Fatal(err) + } + + _, err = p.Exec() + assert.NoError(t, err) +} diff --git a/excel/sheet.go b/excel/sheet.go new file mode 100644 index 00000000..0e94449c --- /dev/null +++ b/excel/sheet.go @@ -0,0 +1,130 @@ +package excel + +import ( + "fmt" + + "github.com/xuri/excelize/v2" +) + +// New creates a new Excel workbook +func New() (*Excel, error) { + f := excelize.NewFile() + return &Excel{ + File: f, + id: "", + path: "", + create: 0, + abs: "", + }, nil +} + +// CreateSheet creates a new sheet with the given name +// Returns the index of the new sheet and any error encountered +func (excel *Excel) CreateSheet(name string) (int, error) { + // Check if sheet already exists + if idx, _ := excel.GetSheetIndex(name); idx != -1 { + return 0, fmt.Errorf("sheet %s already exists", name) + } + + return excel.NewSheet(name) +} + +// ReadSheet reads all data from a sheet +// Returns the data as a 2D array of interfaces and any error encountered +func (excel *Excel) ReadSheet(name string) ([][]interface{}, error) { + // Check if sheet exists + if idx, _ := excel.GetSheetIndex(name); idx == -1 { + return nil, fmt.Errorf("sheet %s does not exist", name) + } + + rows, err := excel.GetRows(name) + if err != nil { + return nil, err + } + + // Convert [][]string to [][]interface{} + result := make([][]interface{}, len(rows)) + for i, row := range rows { + result[i] = make([]interface{}, len(row)) + for j, cell := range row { + result[i][j] = cell + } + } + return result, nil +} + +// UpdateSheet updates an existing sheet with new data +// If the sheet doesn't exist, it will be created +func (excel *Excel) UpdateSheet(name string, data [][]interface{}) error { + // Ensure sheet exists + _, err := excel.SetSheet(name) + if err != nil { + return err + } + + // Clear existing content by deleting the sheet + err = excel.DeleteSheet(name) + if err != nil { + return err + } + + // Create new sheet with same name + _, err = excel.NewSheet(name) + if err != nil { + return err + } + + // Write new data + return excel.WriteAll(name, "A1", data) +} + +// DeleteSheet removes a sheet by name +func (excel *Excel) DeleteSheet(name string) error { + // Check if sheet exists + if idx, _ := excel.GetSheetIndex(name); idx == -1 { + return fmt.Errorf("sheet %s does not exist", name) + } + + return excel.File.DeleteSheet(name) +} + +// ListSheets returns a list of all sheet names in the workbook +func (excel *Excel) ListSheets() []string { + return excel.GetSheetList() +} + +// CopySheet copies a sheet to a new name +func (excel *Excel) CopySheet(source, destination string) error { + // Check if source exists + if idx, _ := excel.GetSheetIndex(source); idx == -1 { + return fmt.Errorf("source sheet %s does not exist", source) + } + + // Check if destination already exists + if idx, _ := excel.GetSheetIndex(destination); idx != -1 { + return fmt.Errorf("destination sheet %s already exists", destination) + } + + // Create new sheet + _, err := excel.NewSheet(destination) + if err != nil { + return err + } + + // Copy content + rows, err := excel.GetRows(source) + if err != nil { + return err + } + + // Convert [][]string to [][]interface{} + data := make([][]interface{}, len(rows)) + for i, row := range rows { + data[i] = make([]interface{}, len(row)) + for j, cell := range row { + data[i][j] = cell + } + } + + return excel.WriteAll(destination, "A1", data) +} diff --git a/excel/sheet_test.go b/excel/sheet_test.go new file mode 100644 index 00000000..5e9338b9 --- /dev/null +++ b/excel/sheet_test.go @@ -0,0 +1,123 @@ +package excel + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSheetOperations(t *testing.T) { + // Get test files and open the test file + files := testFiles(t) + handler, err := Open(files["test-01"], true) // Open in writable mode + if err != nil { + t.Fatal(err) + } + defer Close(handler) + + excel, err := Get(handler) + if err != nil { + t.Fatal(err) + } + + // Test CreateSheet + t.Run("CreateSheet", func(t *testing.T) { + // Create a new sheet + idx, err := excel.CreateSheet("TestSheet1") + assert.NoError(t, err) + assert.Greater(t, idx, 0) + + // Try to create a sheet with the same name (should fail) + _, err = excel.CreateSheet("TestSheet1") + assert.Error(t, err) + }) + + // Test ReadSheet + t.Run("ReadSheet", func(t *testing.T) { + // Create test data + testData := [][]interface{}{ + {"Header1", "Header2"}, + {1, "Data1"}, + {2, "Data2"}, + } + + // Write test data + err := excel.WriteAll("TestSheet1", "A1", testData) + assert.NoError(t, err) + + // Read the data back + data, err := excel.ReadSheet("TestSheet1") + assert.NoError(t, err) + assert.Equal(t, len(testData), len(data)) + + // Try to read non-existent sheet + _, err = excel.ReadSheet("NonExistentSheet") + assert.Error(t, err) + }) + + // Test UpdateSheet + t.Run("UpdateSheet", func(t *testing.T) { + newData := [][]interface{}{ + {"NewHeader1", "NewHeader2"}, + {3, "NewData1"}, + {4, "NewData2"}, + } + + // Update existing sheet + err := excel.UpdateSheet("TestSheet1", newData) + assert.NoError(t, err) + + // Read back and verify + data, err := excel.ReadSheet("TestSheet1") + assert.NoError(t, err) + assert.Equal(t, len(newData), len(data)) + + // Update non-existent sheet (should create new) + err = excel.UpdateSheet("NewSheet", newData) + assert.NoError(t, err) + }) + + // Test ListSheets + t.Run("ListSheets", func(t *testing.T) { + sheets := excel.ListSheets() + assert.Contains(t, sheets, "TestSheet1") + assert.Contains(t, sheets, "NewSheet") + }) + + // Test CopySheet + t.Run("CopySheet", func(t *testing.T) { + // Copy existing sheet + err := excel.CopySheet("TestSheet1", "CopiedSheet") + assert.NoError(t, err) + + // Verify the copy + originalData, err := excel.ReadSheet("TestSheet1") + assert.NoError(t, err) + copiedData, err := excel.ReadSheet("CopiedSheet") + assert.NoError(t, err) + assert.Equal(t, originalData, copiedData) + + // Try to copy to existing sheet name (should fail) + err = excel.CopySheet("TestSheet1", "CopiedSheet") + assert.Error(t, err) + + // Try to copy non-existent sheet (should fail) + err = excel.CopySheet("NonExistentSheet", "NewSheet2") + assert.Error(t, err) + }) + + // Test DeleteSheet + t.Run("DeleteSheet", func(t *testing.T) { + // Delete existing sheet + err := excel.DeleteSheet("CopiedSheet") + assert.NoError(t, err) + + // Verify sheet is deleted + sheets := excel.ListSheets() + assert.NotContains(t, sheets, "CopiedSheet") + + // Try to delete non-existent sheet + err = excel.DeleteSheet("NonExistentSheet") + assert.Error(t, err) + }) +}