feat: Add row reading and dimension retrieval functionalities for Excel sheets

- Implemented `excel.sheet.rows` to read specified rows from a sheet with pagination support.
- Added `excel.sheet.dimension` to retrieve the number of rows and columns in a sheet.
- Enhanced the Excel processing logic to handle new functionalities and updated related tests.
- Updated README with examples for the new features.
This commit is contained in:
Max 2025-05-15 12:56:28 +08:00
parent aca3014167
commit 1cce634d24
5 changed files with 730 additions and 25 deletions

View file

@ -142,6 +142,37 @@ Process("excel.sheet.delete", h, "Sheet1Copy");
const exists: boolean = Process("excel.sheet.exists", h, "Sheet1");
```
#### Read sheet rows with pagination
```typescript
/**
* Reads rows from a sheet with pagination support
* @param handle - Handle ID from excel.open
* @param name - Sheet name
* @param start - Starting row index (0-based)
* @param size - Number of rows to read
* @returns string[][] - Two-dimensional array of cell values
*/
const rows: string[][] = Process("excel.sheet.rows", h, "Sheet1", 0, 10); // Read first 10 rows
```
#### Get sheet dimensions
```typescript
/**
* Gets the dimensions (number of rows and columns) of a sheet
* @param handle - Handle ID from excel.open
* @param name - Sheet name
* @returns {rows: number, cols: number} - Object containing row and column counts
*/
const dim: { rows: number; cols: number } = Process(
"excel.sheet.dimension",
h,
"Sheet1"
);
console.log(`Sheet has ${dim.rows} rows and ${dim.cols} columns`);
```
### Example: Sheet Operations Workflow
```typescript

View file

@ -13,13 +13,15 @@ func init() {
"save": processSave,
"sheets": processSheets,
"sheet.create": processCreateSheet,
"sheet.read": processReadSheet,
"sheet.update": processUpdateSheet,
"sheet.delete": processDeleteSheet,
"sheet.copy": processCopySheet,
"sheet.list": processListSheets,
"sheet.exists": processSheetExists,
"sheet.create": processCreateSheet,
"sheet.read": processReadSheet,
"sheet.update": processUpdateSheet,
"sheet.delete": processDeleteSheet,
"sheet.copy": processCopySheet,
"sheet.list": processListSheets,
"sheet.exists": processSheetExists,
"sheet.rows": processReadSheetRows,
"sheet.dimension": processGetSheetDimension,
"read.cell": processReadCell,
"read.row": processReadRow,
@ -736,3 +738,45 @@ func processSheetExists(process *process.Process) interface{} {
return xls.SheetExists(name)
}
// processReadSheetRows process the excel.sheet.rows <handle> <name> <start> <size>
func processReadSheetRows(process *process.Process) interface{} {
process.ValidateArgNums(4)
handle := process.ArgsString(0)
name := process.ArgsString(1)
start := process.ArgsInt(2)
size := process.ArgsInt(3)
xls, err := Get(handle)
if err != nil {
exception.New("excel.sheet.rows %s error: %s", 500, handle, err.Error()).Throw()
}
data, err := xls.ReadSheetRows(name, start, size)
if err != nil {
exception.New("excel.sheet.rows %s:%s error: %s", 500, handle, name, err.Error()).Throw()
}
return data
}
// processGetSheetDimension process the excel.sheet.dimension <handle> <name>
func processGetSheetDimension(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.dimension %s error: %s", 500, handle, err.Error()).Throw()
}
rows, cols, err := xls.GetSheetDimension(name)
if err != nil {
exception.New("excel.sheet.dimension %s:%s error: %s", 500, handle, name, err.Error()).Throw()
}
return map[string]int{
"rows": rows,
"cols": cols,
}
}

View file

@ -1,6 +1,7 @@
package excel
import (
"fmt"
"os"
"path/filepath"
"testing"
@ -531,36 +532,38 @@ func TestProcessWriteOperations(t *testing.T) {
t.Fatal(err)
}
// Test write row
p, err = process.Of("excel.write.row", handle, "供销存管理表格", "A1", []interface{}{"Test1", "Test2", "Test3"})
// Create test data
testData := make([]interface{}, 0)
testData = append(testData, []interface{}{"Header1", "Header2", "Header3"})
testData = append(testData, []interface{}{1, "Row1", true})
testData = append(testData, []interface{}{2, "Row2", false})
testData = append(testData, []interface{}{3, "Row3", true})
testData = append(testData, []interface{}{4, "Row4", false})
testData = append(testData, []interface{}{5, "Row5", true})
testData = append(testData, []interface{}{6, "Row6", false})
testData = append(testData, []interface{}{7, "Row7", true})
testData = append(testData, []interface{}{8, "Row8", false})
testData = append(testData, []interface{}{9, "Row9", true})
// Create a new sheet and write test data
p, err = process.Of("excel.sheet.create", handle, "RowTestSheet")
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.NoError(t, err)
// Test write column
p, err = process.Of("excel.write.column", handle, "供销存管理表格", "B1", []interface{}{"Col1", "Col2", "Col3"})
p, err = process.Of("excel.write.all", handle, "RowTestSheet", "A1", testData)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.NoError(t, err)
// Test write all
p, err = process.Of("excel.write.all", handle, "供销存管理表格", "C1", [][]interface{}{
{"All1", "All2", "All3"},
{"All4", "All5", "All6"},
})
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.NoError(t, err)
// Save and close
p, err = process.Of("excel.save", handle)
if err != nil {
@ -916,3 +919,320 @@ func TestProcessSheetOperations(t *testing.T) {
_, err = p.Exec()
assert.NoError(t, err)
}
func TestProcessReadSheetRows(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-read-rows.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)
}
// Create test data
testData := make([]interface{}, 0)
testData = append(testData, []interface{}{"Header1", "Header2", "Header3"})
testData = append(testData, []interface{}{1, "Row1", true})
testData = append(testData, []interface{}{2, "Row2", false})
testData = append(testData, []interface{}{3, "Row3", true})
testData = append(testData, []interface{}{4, "Row4", false})
testData = append(testData, []interface{}{5, "Row5", true})
testData = append(testData, []interface{}{6, "Row6", false})
testData = append(testData, []interface{}{7, "Row7", true})
testData = append(testData, []interface{}{8, "Row8", false})
testData = append(testData, []interface{}{9, "Row9", true})
// Create a new sheet and write test data
p, err = process.Of("excel.sheet.create", handle, "RowTestSheet")
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
if err != nil {
t.Fatal(err)
}
p, err = process.Of("excel.write.all", handle, "RowTestSheet", "A1", testData)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
if err != nil {
t.Fatal(err)
}
// Save and close the file
p, err = process.Of("excel.save", handle)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
if err != nil {
t.Fatal(err)
}
p, err = process.Of("excel.close", handle)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
if err != nil {
t.Fatal(err)
}
// Reopen the file for reading
p, err = process.Of("excel.open", newFile, true)
if err != nil {
t.Fatal(err)
}
handle, err = p.Exec()
if err != nil {
t.Fatal(err)
}
// Test cases for reading rows
t.Run("ReadFromMiddle", func(t *testing.T) {
p, err := process.Of("excel.sheet.rows", handle, "RowTestSheet", 2, 4)
assert.NoError(t, err)
data, err := p.Exec()
assert.NoError(t, err)
rows := data.([][]string)
assert.Equal(t, 4, len(rows))
assert.Equal(t, "2", rows[0][0]) // First row should be row 2
})
t.Run("ReadFromBeginning", func(t *testing.T) {
p, err := process.Of("excel.sheet.rows", handle, "RowTestSheet", 0, 3)
assert.NoError(t, err)
data, err := p.Exec()
assert.NoError(t, err)
rows := data.([][]string)
assert.Equal(t, 3, len(rows))
assert.Equal(t, "Header1", rows[0][0]) // First row should be header
})
t.Run("ReadBeyondAvailable", func(t *testing.T) {
p, err := process.Of("excel.sheet.rows", handle, "RowTestSheet", 8, 5)
assert.NoError(t, err)
data, err := p.Exec()
assert.NoError(t, err)
rows := data.([][]string)
assert.Equal(t, 2, len(rows)) // Only 2 rows remain
})
t.Run("ReadNonExistentSheet", func(t *testing.T) {
p, err := process.Of("excel.sheet.rows", handle, "NonExistentSheet", 0, 5)
assert.NoError(t, err)
_, err = p.Exec()
assert.Error(t, err)
})
t.Run("ReadWithSizeZero", func(t *testing.T) {
p, err := process.Of("excel.sheet.rows", handle, "RowTestSheet", 0, 0)
assert.NoError(t, err)
data, err := p.Exec()
assert.NoError(t, err)
rows := data.([][]string)
assert.Equal(t, 0, len(rows))
})
t.Run("ReadWithNegativeStart", func(t *testing.T) {
p, err := process.Of("excel.sheet.rows", handle, "RowTestSheet", -1, 5)
assert.NoError(t, err)
_, err = p.Exec()
assert.Error(t, err)
})
t.Run("ReadWithNegativeSize", func(t *testing.T) {
p, err := process.Of("excel.sheet.rows", handle, "RowTestSheet", 0, -1)
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)
}
func TestProcessGetSheetDimension(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-dimension.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 1: Empty sheet
t.Run("EmptySheet", func(t *testing.T) {
// Create empty sheet
p, err := process.Of("excel.sheet.create", handle, "EmptySheet")
assert.NoError(t, err)
_, err = p.Exec()
assert.NoError(t, err)
// Get dimensions
p, err = process.Of("excel.sheet.dimension", handle, "EmptySheet")
assert.NoError(t, err)
dim, err := p.Exec()
assert.NoError(t, err)
// Verify dimensions
dimMap := dim.(map[string]int)
assert.Equal(t, 0, dimMap["rows"])
assert.Equal(t, 0, dimMap["cols"])
})
// Test 2: Sheet with data
t.Run("SheetWithData", func(t *testing.T) {
// Create test data
testData := make([]interface{}, 0)
testData = append(testData, []interface{}{"A1", "B1", "C1"})
testData = append(testData, []interface{}{"A2", "B2", "C2"})
testData = append(testData, []interface{}{"A3", "B3", "C3"})
// Create and write to sheet
p, err := process.Of("excel.sheet.create", handle, "DataSheet")
assert.NoError(t, err)
_, err = p.Exec()
assert.NoError(t, err)
p, err = process.Of("excel.write.all", handle, "DataSheet", "A1", testData)
assert.NoError(t, err)
_, err = p.Exec()
assert.NoError(t, err)
// Save to ensure dimensions are updated
p, err = process.Of("excel.save", handle)
assert.NoError(t, err)
_, err = p.Exec()
assert.NoError(t, err)
// Get dimensions
p, err = process.Of("excel.sheet.dimension", handle, "DataSheet")
assert.NoError(t, err)
dim, err := p.Exec()
assert.NoError(t, err)
// Verify dimensions
dimMap := dim.(map[string]int)
assert.Equal(t, 3, dimMap["rows"])
assert.Equal(t, 3, dimMap["cols"])
})
// Test 3: Non-existent sheet
t.Run("NonExistentSheet", func(t *testing.T) {
p, err := process.Of("excel.sheet.dimension", handle, "NonExistentSheet")
assert.NoError(t, err)
_, err = p.Exec()
assert.Error(t, err)
})
// Test 4: Large sheet
t.Run("LargeSheet", func(t *testing.T) {
// Create large test data (100x50)
largeData := make([]interface{}, 0)
for i := 0; i < 100; i++ {
row := make([]interface{}, 50)
for j := 0; j < 50; j++ {
row[j] = fmt.Sprintf("Cell_%d_%d", i, j)
}
largeData = append(largeData, row)
}
// Create and write to sheet
p, err := process.Of("excel.sheet.create", handle, "LargeSheet")
assert.NoError(t, err)
_, err = p.Exec()
assert.NoError(t, err)
p, err = process.Of("excel.write.all", handle, "LargeSheet", "A1", largeData)
assert.NoError(t, err)
_, err = p.Exec()
assert.NoError(t, err)
// Save to ensure dimensions are updated
p, err = process.Of("excel.save", handle)
assert.NoError(t, err)
_, err = p.Exec()
assert.NoError(t, err)
// Get dimensions
p, err = process.Of("excel.sheet.dimension", handle, "LargeSheet")
assert.NoError(t, err)
dim, err := p.Exec()
assert.NoError(t, err)
// Verify dimensions
dimMap := dim.(map[string]int)
assert.Equal(t, 100, dimMap["rows"])
assert.Equal(t, 50, dimMap["cols"])
})
// Clean up
p, err = process.Of("excel.close", handle)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.NoError(t, err)
}

View file

@ -76,6 +76,151 @@ func (excel *Excel) ReadSheet(name string) ([][]interface{}, error) {
return result, nil
}
// GetSheetDimension returns the number of rows and columns in a sheet
func (excel *Excel) GetSheetDimension(name string) (rows int, cols int, err error) {
// Check if sheet exists
if idx, _ := excel.GetSheetIndex(name); idx == -1 {
return 0, 0, fmt.Errorf("sheet %s does not exist", name)
}
rows = 0
cols = 0
ri, err := excel.File.Rows(name)
if err != nil {
return 0, 0, err
}
defer ri.Close()
for ri.Next() {
rows++
row, err := ri.Columns()
if err != nil {
return 0, 0, err
}
if len(row) > cols {
cols = max(cols, len(row))
}
}
return rows, cols, nil
// // Get dimension directly using excelize API
// dimension, err := excel.File.GetSheetDimension(name)
// if err != nil {
// return 0, 0, err
// }
// // If sheet is not set, use row and column count
// if dimension == "A1" {
// rows = 0
// cols = 0
// ri, err := excel.File.Rows(name)
// if err != nil {
// return 0, 0, err
// }
// defer ri.Close()
// for ri.Next() {
// rows++
// row, err := ri.Columns()
// if err != nil {
// return 0, 0, err
// }
// if len(row) > cols {
// cols = max(cols, len(row))
// }
// }
// return rows, cols, nil
// }
// // If sheet is empty
// if dimension == "" || dimension == "A1" {
// return 0, 0, nil
// }
// // For single cell case, add the range suffix
// if !strings.Contains(dimension, ":") {
// dimension = fmt.Sprintf("%s:%s", dimension, dimension)
// }
// // Split dimension into start and end coordinates
// parts := strings.Split(dimension, ":")
// if len(parts) != 2 {
// return 0, 0, fmt.Errorf("invalid dimension format: %s", dimension)
// }
// // Convert end coordinate to row and column numbers
// endCol, endRow, err := excelize.CellNameToCoordinates(parts[1])
// if err != nil {
// return 0, 0, err
// }
// // Convert start coordinate to row and column numbers
// startCol, startRow, err := excelize.CellNameToCoordinates(parts[0])
// if err != nil {
// return 0, 0, err
// }
// // Calculate actual dimensions
// rows = endRow - startRow + 1
// cols = endCol - startCol + 1
// return rows, cols, nil
}
// ReadSheetRows reads all data from a sheet by rows
func (excel *Excel) ReadSheetRows(name string, start int, size int) ([][]string, error) {
// Validate parameters
if start < 0 {
return nil, fmt.Errorf("start position cannot be negative")
}
if size < 0 {
return nil, fmt.Errorf("size cannot be negative")
}
// Check if sheet exists
if idx, _ := excel.GetSheetIndex(name); idx == -1 {
return nil, fmt.Errorf("sheet %s does not exist", name)
}
// If size is 0, return empty slice
if size == 0 {
return [][]string{}, nil
}
// Get rows iterator
rows, err := excel.File.Rows(name)
if err != nil {
return nil, err
}
defer rows.Close()
// Skip to start position
currentRow := -1
for rows.Next() {
currentRow++
if currentRow >= start {
break
}
}
// Read requested number of rows
result := make([][]string, 0, size)
if currentRow == start {
row, err := rows.Columns()
if err != nil {
return nil, err
}
result = append(result, row)
}
for i := 1; i < size && rows.Next(); i++ {
row, err := rows.Columns()
if err != nil {
return nil, err
}
result = append(result, row)
}
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 {

View file

@ -1,6 +1,8 @@
package excel
import (
"fmt"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -131,4 +133,167 @@ func TestSheetOperations(t *testing.T) {
err = excel.DeleteSheet("NonExistentSheet")
assert.Error(t, err)
})
// Test ReadSheetRows
t.Run("ReadSheetRows", func(t *testing.T) {
// Create test data with 10 rows
testData := [][]interface{}{
{"Header1", "Header2", "Header3"},
{1, "Row1", true},
{2, "Row2", false},
{3, "Row3", true},
{4, "Row4", false},
{5, "Row5", true},
{6, "Row6", false},
{7, "Row7", true},
{8, "Row8", false},
{9, "Row9", true},
}
// Expected string data
expectedData := [][]string{
{"Header1", "Header2", "Header3"},
{"1", "Row1", "TRUE"},
{"2", "Row2", "FALSE"},
{"3", "Row3", "TRUE"},
{"4", "Row4", "FALSE"},
{"5", "Row5", "TRUE"},
{"6", "Row6", "FALSE"},
{"7", "Row7", "TRUE"},
{"8", "Row8", "FALSE"},
{"9", "Row9", "TRUE"},
}
// Create a new sheet for testing
_, err := excel.CreateSheet("RowTestSheet")
assert.NoError(t, err)
// Write test data
err = excel.WriteAll("RowTestSheet", "A1", testData)
assert.NoError(t, err)
// Test 1: Read from middle (start at row 2, read 4 rows)
data, err := excel.ReadSheetRows("RowTestSheet", 2, 4)
assert.NoError(t, err)
assert.Equal(t, 4, len(data))
assert.Equal(t, expectedData[2:6], data)
// Test 2: Read from beginning (start at row 0, read 3 rows)
data, err = excel.ReadSheetRows("RowTestSheet", 0, 3)
assert.NoError(t, err)
assert.Equal(t, 3, len(data))
assert.Equal(t, expectedData[0:3], data)
// Test 3: Read beyond available rows (should return remaining rows)
data, err = excel.ReadSheetRows("RowTestSheet", 8, 5)
assert.NoError(t, err)
assert.Equal(t, 2, len(data)) // Only 2 rows remain
assert.Equal(t, expectedData[8:], data)
// Test 4: Read from non-existent sheet
_, err = excel.ReadSheetRows("NonExistentSheet", 0, 5)
assert.Error(t, err)
// Test 5: Read with size 0 (should return empty slice)
data, err = excel.ReadSheetRows("RowTestSheet", 0, 0)
assert.NoError(t, err)
assert.Equal(t, 0, len(data))
// Test 6: Read with negative start (should return error)
_, err = excel.ReadSheetRows("RowTestSheet", -1, 5)
assert.Error(t, err)
// Test 7: Read with negative size (should return error)
_, err = excel.ReadSheetRows("RowTestSheet", 0, -1)
assert.Error(t, err)
})
// Test GetSheetDimension
t.Run("GetSheetDimension", TestGetSheetDimension)
}
// TestGetSheetDimension tests the GetSheetDimension function
func TestGetSheetDimension(t *testing.T) {
// Get test files and open the test file
files := testFiles(t)
filename := filepath.Dir(files["test-01"]) + "/test-dimension.xlsx"
handler, err := Open(filename, true) // Open in writable mode
if err != nil {
t.Fatal(err)
}
defer Close(handler)
excel, err := Get(handler)
if err != nil {
t.Fatal(err)
}
// Clean up existing sheets
sheets := excel.ListSheets()
for _, sheet := range sheets {
if sheet != "Sheet1" { // Keep the default sheet
err = excel.DeleteSheet(sheet)
assert.NoError(t, err)
}
}
// Test 1: Create a large sheet (100x100)
_, err = excel.CreateSheet("LargeSheet")
assert.NoError(t, err)
// Create test data (100x100)
largeData := make([][]interface{}, 100)
for i := 0; i < 100; i++ {
largeData[i] = make([]interface{}, 100)
for j := 0; j < 100; j++ {
largeData[i][j] = fmt.Sprintf("Cell_%d_%d", i, j)
}
}
err = excel.WriteAll("LargeSheet", "A1", largeData)
assert.NoError(t, err)
// Save file to ensure dimensions are updated
err = excel.Save()
assert.NoError(t, err)
// Test large sheet dimensions
rows, cols, err := excel.GetSheetDimension("LargeSheet")
assert.NoError(t, err)
assert.Equal(t, 100, rows)
assert.Equal(t, 100, cols)
// Test 2: Empty sheet
if excel.SheetExists("EmptySheet") {
excel.DeleteSheet("EmptySheet")
}
_, err = excel.CreateSheet("EmptySheet")
assert.NoError(t, err)
rows, cols, err = excel.GetSheetDimension("EmptySheet")
assert.NoError(t, err)
assert.Equal(t, 0, rows)
assert.Equal(t, 0, cols)
// Test 3: Regular sheet with data
testData := [][]interface{}{
{"A1", "B1", "C1"},
{"A2", "B2", "C2"},
{"A3", "B3", "C3"},
}
err = excel.WriteAll("RegularSheet", "A1", testData)
assert.NoError(t, err)
// Save file to ensure dimensions are updated
err = excel.Save()
assert.NoError(t, err)
rows, cols, err = excel.GetSheetDimension("RegularSheet")
assert.NoError(t, err)
assert.Equal(t, 3, rows)
assert.Equal(t, 3, cols)
// Test 4: Non-existent sheet
rows, cols, err = excel.GetSheetDimension("NonExistentSheet")
assert.Error(t, err)
assert.Equal(t, 0, rows)
assert.Equal(t, 0, cols)
}