forked from p30928647/excelize
- Support concurrency iterate rows and columns
- Rename exported field `File.XLSX` to `File.Pkg` - Exported error message
This commit is contained in:
parent
0e02329bed
commit
544ef18a8c
|
@ -73,20 +73,10 @@ func TestAdjustAutoFilter(t *testing.T) {
|
|||
func TestAdjustHelper(t *testing.T) {
|
||||
f := NewFile()
|
||||
f.NewSheet("Sheet2")
|
||||
f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{
|
||||
MergeCells: &xlsxMergeCells{
|
||||
Cells: []*xlsxMergeCell{
|
||||
{
|
||||
Ref: "A:B1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
f.Sheet["xl/worksheets/sheet2.xml"] = &xlsxWorksheet{
|
||||
AutoFilter: &xlsxAutoFilter{
|
||||
Ref: "A1:B",
|
||||
},
|
||||
}
|
||||
f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{
|
||||
MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:B1"}}}})
|
||||
f.Sheet.Store("xl/worksheets/sheet2.xml", &xlsxWorksheet{
|
||||
AutoFilter: &xlsxAutoFilter{Ref: "A1:B"}})
|
||||
// testing adjustHelper with illegal cell coordinates.
|
||||
assert.EqualError(t, f.adjustHelper("Sheet1", rows, 0, 0), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
|
||||
assert.EqualError(t, f.adjustHelper("Sheet2", rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`)
|
||||
|
|
|
@ -54,7 +54,7 @@ func (f *File) deleteCalcChain(index int, axis string) {
|
|||
}
|
||||
if len(calc.C) == 0 {
|
||||
f.CalcChain = nil
|
||||
delete(f.XLSX, "xl/calcChain.xml")
|
||||
f.Pkg.Delete("xl/calcChain.xml")
|
||||
content := f.contentTypesReader()
|
||||
for k, v := range content.Overrides {
|
||||
if v.PartName == "/xl/calcChain.xml" {
|
||||
|
|
|
@ -5,7 +5,7 @@ import "testing"
|
|||
func TestCalcChainReader(t *testing.T) {
|
||||
f := NewFile()
|
||||
f.CalcChain = nil
|
||||
f.XLSX["xl/calcChain.xml"] = MacintoshCyrillicCharset
|
||||
f.Pkg.Store("xl/calcChain.xml", MacintoshCyrillicCharset)
|
||||
f.calcChainReader()
|
||||
}
|
||||
|
||||
|
|
12
cell.go
12
cell.go
|
@ -13,7 +13,6 @@ package excelize
|
|||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
@ -187,6 +186,8 @@ func (f *File) SetCellInt(sheet, axis string, value int) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
cellData.S = f.prepareCellStyle(ws, col, cellData.S)
|
||||
cellData.T, cellData.V = setCellInt(value)
|
||||
return err
|
||||
|
@ -262,6 +263,8 @@ func (f *File) SetCellStr(sheet, axis, value string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
cellData.S = f.prepareCellStyle(ws, col, cellData.S)
|
||||
cellData.T, cellData.V = f.setCellString(value)
|
||||
return err
|
||||
|
@ -742,7 +745,7 @@ func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error {
|
|||
// Make sure 'slice' is a Ptr to Slice
|
||||
v := reflect.ValueOf(slice)
|
||||
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Slice {
|
||||
return errors.New("pointer to slice expected")
|
||||
return ErrParameterInvalid
|
||||
}
|
||||
v = v.Elem()
|
||||
|
||||
|
@ -762,8 +765,6 @@ func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error {
|
|||
|
||||
// getCellInfo does common preparation for all SetCell* methods.
|
||||
func (f *File) prepareCell(ws *xlsxWorksheet, sheet, cell string) (*xlsxC, int, int, error) {
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
var err error
|
||||
cell, err = f.mergeCellsParser(ws, cell)
|
||||
if err != nil {
|
||||
|
@ -775,7 +776,8 @@ func (f *File) prepareCell(ws *xlsxWorksheet, sheet, cell string) (*xlsxC, int,
|
|||
}
|
||||
|
||||
prepareSheetXML(ws, col, row)
|
||||
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
return &ws.SheetData.Row[row-1].C[col-1], col, row, err
|
||||
}
|
||||
|
||||
|
|
42
cell_test.go
42
cell_test.go
|
@ -30,6 +30,20 @@ func TestConcurrency(t *testing.T) {
|
|||
assert.Equal(t, "", name)
|
||||
assert.Nil(t, raw)
|
||||
assert.NoError(t, err)
|
||||
// Concurrency iterate rows
|
||||
rows, err := f.Rows("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
for rows.Next() {
|
||||
_, err := rows.Columns()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
// Concurrency iterate columns
|
||||
cols, err := f.Cols("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
for rows.Next() {
|
||||
_, err := cols.Rows()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}(i, t)
|
||||
|
@ -149,8 +163,8 @@ func TestGetCellValue(t *testing.T) {
|
|||
// Test get cell value without r attribute of the row.
|
||||
f := NewFile()
|
||||
sheetData := `<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>%s</sheetData></worksheet>`
|
||||
delete(f.Sheet, "xl/worksheets/sheet1.xml")
|
||||
f.XLSX["xl/worksheets/sheet1.xml"] = []byte(fmt.Sprintf(sheetData, `<row r="3"><c t="str"><v>A3</v></c></row><row><c t="str"><v>A4</v></c><c t="str"><v>B4</v></c></row><row r="7"><c t="str"><v>A7</v></c><c t="str"><v>B7</v></c></row><row><c t="str"><v>A8</v></c><c t="str"><v>B8</v></c></row>`))
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="3"><c t="str"><v>A3</v></c></row><row><c t="str"><v>A4</v></c><c t="str"><v>B4</v></c></row><row r="7"><c t="str"><v>A7</v></c><c t="str"><v>B7</v></c></row><row><c t="str"><v>A8</v></c><c t="str"><v>B8</v></c></row>`)))
|
||||
f.checked = nil
|
||||
cells := []string{"A3", "A4", "B4", "A7", "B7"}
|
||||
rows, err := f.GetRows("Sheet1")
|
||||
|
@ -164,20 +178,20 @@ func TestGetCellValue(t *testing.T) {
|
|||
cols, err := f.GetCols("Sheet1")
|
||||
assert.Equal(t, [][]string{{"", "", "A3", "A4", "", "", "A7", "A8"}, {"", "", "", "B4", "", "", "B7", "B8"}}, cols)
|
||||
assert.NoError(t, err)
|
||||
delete(f.Sheet, "xl/worksheets/sheet1.xml")
|
||||
f.XLSX["xl/worksheets/sheet1.xml"] = []byte(fmt.Sprintf(sheetData, `<row r="2"><c r="A2" t="str"><v>A2</v></c></row><row r="2"><c r="B2" t="str"><v>B2</v></c></row>`))
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="2"><c r="A2" t="str"><v>A2</v></c></row><row r="2"><c r="B2" t="str"><v>B2</v></c></row>`)))
|
||||
f.checked = nil
|
||||
cell, err := f.GetCellValue("Sheet1", "A2")
|
||||
assert.Equal(t, "A2", cell)
|
||||
assert.NoError(t, err)
|
||||
delete(f.Sheet, "xl/worksheets/sheet1.xml")
|
||||
f.XLSX["xl/worksheets/sheet1.xml"] = []byte(fmt.Sprintf(sheetData, `<row r="2"><c r="A2" t="str"><v>A2</v></c></row><row r="2"><c r="B2" t="str"><v>B2</v></c></row>`))
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="2"><c r="A2" t="str"><v>A2</v></c></row><row r="2"><c r="B2" t="str"><v>B2</v></c></row>`)))
|
||||
f.checked = nil
|
||||
rows, err = f.GetRows("Sheet1")
|
||||
assert.Equal(t, [][]string{nil, {"A2", "B2"}}, rows)
|
||||
assert.NoError(t, err)
|
||||
delete(f.Sheet, "xl/worksheets/sheet1.xml")
|
||||
f.XLSX["xl/worksheets/sheet1.xml"] = []byte(fmt.Sprintf(sheetData, `<row r="1"><c r="A1" t="str"><v>A1</v></c></row><row r="1"><c r="B1" t="str"><v>B1</v></c></row>`))
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="1"><c r="A1" t="str"><v>A1</v></c></row><row r="1"><c r="B1" t="str"><v>B1</v></c></row>`)))
|
||||
f.checked = nil
|
||||
rows, err = f.GetRows("Sheet1")
|
||||
assert.Equal(t, [][]string{{"A1", "B1"}}, rows)
|
||||
|
@ -264,17 +278,23 @@ func TestGetCellRichText(t *testing.T) {
|
|||
assert.True(t, reflect.DeepEqual(runsSource[1].Font, runs[1].Font), "should get the same font")
|
||||
|
||||
// Test get cell rich text when string item index overflow
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].SheetData.Row[0].C[0].V = "2"
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "2"
|
||||
runs, err = f.GetCellRichText("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(runs))
|
||||
// Test get cell rich text when string item index is negative
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].SheetData.Row[0].C[0].V = "-1"
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "-1"
|
||||
runs, err = f.GetCellRichText("Sheet1", "A1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(runs))
|
||||
// Test get cell rich text on invalid string item index
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].SheetData.Row[0].C[0].V = "x"
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "x"
|
||||
_, err = f.GetCellRichText("Sheet1", "A1")
|
||||
assert.EqualError(t, err, "strconv.Atoi: parsing \"x\": invalid syntax")
|
||||
// Test set cell rich text on not exists worksheet
|
||||
|
|
14
chart.go
14
chart.go
|
@ -14,7 +14,6 @@ package excelize
|
|||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -945,7 +944,7 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error {
|
|||
sheetID++
|
||||
path := "xl/chartsheets/sheet" + strconv.Itoa(sheetID) + ".xml"
|
||||
f.sheetMap[trimSheetName(sheet)] = path
|
||||
f.Sheet[path] = nil
|
||||
f.Sheet.Store(path, nil)
|
||||
drawingID := f.countDrawings() + 1
|
||||
chartID := f.countCharts() + 1
|
||||
drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
|
||||
|
@ -981,12 +980,12 @@ func (f *File) getFormatChart(format string, combo []string) (*formatChart, []*f
|
|||
return formatSet, comboCharts, err
|
||||
}
|
||||
if _, ok := chartValAxNumFmtFormatCode[comboChart.Type]; !ok {
|
||||
return formatSet, comboCharts, errors.New("unsupported chart type " + comboChart.Type)
|
||||
return formatSet, comboCharts, newUnsupportChartType(comboChart.Type)
|
||||
}
|
||||
comboCharts = append(comboCharts, comboChart)
|
||||
}
|
||||
if _, ok := chartValAxNumFmtFormatCode[formatSet.Type]; !ok {
|
||||
return formatSet, comboCharts, errors.New("unsupported chart type " + formatSet.Type)
|
||||
return formatSet, comboCharts, newUnsupportChartType(formatSet.Type)
|
||||
}
|
||||
return formatSet, comboCharts, err
|
||||
}
|
||||
|
@ -1015,11 +1014,12 @@ func (f *File) DeleteChart(sheet, cell string) (err error) {
|
|||
// folder xl/charts.
|
||||
func (f *File) countCharts() int {
|
||||
count := 0
|
||||
for k := range f.XLSX {
|
||||
if strings.Contains(k, "xl/charts/chart") {
|
||||
f.Pkg.Range(func(k, v interface{}) bool {
|
||||
if strings.Contains(k.(string), "xl/charts/chart") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
|
|
|
@ -65,10 +65,10 @@ func TestChartSize(t *testing.T) {
|
|||
anchor decodeTwoCellAnchor
|
||||
)
|
||||
|
||||
content, ok := newFile.XLSX["xl/drawings/drawing1.xml"]
|
||||
content, ok := newFile.Pkg.Load("xl/drawings/drawing1.xml")
|
||||
assert.True(t, ok, "Can't open the chart")
|
||||
|
||||
err = xml.Unmarshal([]byte(content), &workdir)
|
||||
err = xml.Unmarshal(content.([]byte), &workdir)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
@ -340,11 +340,15 @@ func TestChartWithLogarithmicBase(t *testing.T) {
|
|||
type xmlChartContent []byte
|
||||
xmlCharts := make([]xmlChartContent, expectedChartsCount)
|
||||
expectedChartsLogBase := []float64{0, 10.5, 0, 2, 0, 1000}
|
||||
var ok bool
|
||||
|
||||
var (
|
||||
drawingML interface{}
|
||||
ok bool
|
||||
)
|
||||
for i := 0; i < expectedChartsCount; i++ {
|
||||
chartPath := fmt.Sprintf("xl/charts/chart%d.xml", i+1)
|
||||
xmlCharts[i], ok = newFile.XLSX[chartPath]
|
||||
if drawingML, ok = newFile.Pkg.Load(chartPath); ok {
|
||||
xmlCharts[i] = drawingML.([]byte)
|
||||
}
|
||||
assert.True(t, ok, "Can't open the %s", chartPath)
|
||||
|
||||
err = xml.Unmarshal([]byte(xmlCharts[i]), &chartSpaces[i])
|
||||
|
|
7
col.go
7
col.go
|
@ -199,8 +199,11 @@ func (f *File) Cols(sheet string) (*Cols, error) {
|
|||
if !ok {
|
||||
return nil, ErrSheetNotExist{sheet}
|
||||
}
|
||||
if f.Sheet[name] != nil {
|
||||
output, _ := xml.Marshal(f.Sheet[name])
|
||||
if ws, ok := f.Sheet.Load(name); ok && ws != nil {
|
||||
worksheet := ws.(*xlsxWorksheet)
|
||||
worksheet.Lock()
|
||||
defer worksheet.Unlock()
|
||||
output, _ := xml.Marshal(worksheet)
|
||||
f.saveFileList(name, f.replaceNameSpaceBytes(name, output))
|
||||
}
|
||||
var colIterator columnXMLIterator
|
||||
|
|
18
col_test.go
18
col_test.go
|
@ -48,11 +48,11 @@ func TestCols(t *testing.T) {
|
|||
_, err = f.Rows("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{
|
||||
f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{
|
||||
Dimension: &xlsxDimension{
|
||||
Ref: "C2:C4",
|
||||
},
|
||||
}
|
||||
})
|
||||
_, err = f.Rows("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
@ -110,15 +110,15 @@ func TestGetColsError(t *testing.T) {
|
|||
assert.EqualError(t, err, "sheet SheetN is not exist")
|
||||
|
||||
f = NewFile()
|
||||
delete(f.Sheet, "xl/worksheets/sheet1.xml")
|
||||
f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`<worksheet><sheetData><row r="A"><c r="2" t="str"><v>B</v></c></row></sheetData></worksheet>`)
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="A"><c r="2" t="str"><v>B</v></c></row></sheetData></worksheet>`))
|
||||
f.checked = nil
|
||||
_, err = f.GetCols("Sheet1")
|
||||
assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`)
|
||||
|
||||
f = NewFile()
|
||||
delete(f.Sheet, "xl/worksheets/sheet1.xml")
|
||||
f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`<worksheet><sheetData><row r="2"><c r="A" t="str"><v>B</v></c></row></sheetData></worksheet>`)
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="2"><c r="A" t="str"><v>B</v></c></row></sheetData></worksheet>`))
|
||||
f.checked = nil
|
||||
_, err = f.GetCols("Sheet1")
|
||||
assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)
|
||||
|
@ -142,14 +142,14 @@ func TestColsRows(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 1))
|
||||
f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{
|
||||
f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{
|
||||
Dimension: &xlsxDimension{
|
||||
Ref: "A1:A1",
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
f = NewFile()
|
||||
f.XLSX["xl/worksheets/sheet1.xml"] = nil
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", nil)
|
||||
_, err = f.Cols("Sheet1")
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
|
|
22
comment.go
22
comment.go
|
@ -299,11 +299,12 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) {
|
|||
// the folder xl.
|
||||
func (f *File) countComments() int {
|
||||
c1, c2 := 0, 0
|
||||
for k := range f.XLSX {
|
||||
if strings.Contains(k, "xl/comments") {
|
||||
f.Pkg.Range(func(k, v interface{}) bool {
|
||||
if strings.Contains(k.(string), "xl/comments") {
|
||||
c1++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
for rel := range f.Comments {
|
||||
if strings.Contains(rel, "xl/comments") {
|
||||
c2++
|
||||
|
@ -321,10 +322,10 @@ func (f *File) decodeVMLDrawingReader(path string) *decodeVmlDrawing {
|
|||
var err error
|
||||
|
||||
if f.DecodeVMLDrawing[path] == nil {
|
||||
c, ok := f.XLSX[path]
|
||||
if ok {
|
||||
c, ok := f.Pkg.Load(path)
|
||||
if ok && c != nil {
|
||||
f.DecodeVMLDrawing[path] = new(decodeVmlDrawing)
|
||||
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c))).
|
||||
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c.([]byte)))).
|
||||
Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF {
|
||||
log.Printf("xml decode error: %s", err)
|
||||
}
|
||||
|
@ -339,7 +340,7 @@ func (f *File) vmlDrawingWriter() {
|
|||
for path, vml := range f.VMLDrawing {
|
||||
if vml != nil {
|
||||
v, _ := xml.Marshal(vml)
|
||||
f.XLSX[path] = v
|
||||
f.Pkg.Store(path, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -348,12 +349,11 @@ func (f *File) vmlDrawingWriter() {
|
|||
// after deserialization of xl/comments%d.xml.
|
||||
func (f *File) commentsReader(path string) *xlsxComments {
|
||||
var err error
|
||||
|
||||
if f.Comments[path] == nil {
|
||||
content, ok := f.XLSX[path]
|
||||
if ok {
|
||||
content, ok := f.Pkg.Load(path)
|
||||
if ok && content != nil {
|
||||
f.Comments[path] = new(xlsxComments)
|
||||
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content))).
|
||||
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
|
||||
Decode(f.Comments[path]); err != nil && err != io.EOF {
|
||||
log.Printf("xml decode error: %s", err)
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ func TestAddComments(t *testing.T) {
|
|||
}
|
||||
|
||||
f.Comments["xl/comments2.xml"] = nil
|
||||
f.XLSX["xl/comments2.xml"] = []byte(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><comments xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><authors><author>Excelize: </author></authors><commentList><comment ref="B7" authorId="0"><text><t>Excelize: </t></text></comment></commentList></comments>`)
|
||||
f.Pkg.Store("xl/comments2.xml", []byte(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><comments xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><authors><author>Excelize: </author></authors><commentList><comment ref="B7" authorId="0"><text><t>Excelize: </t></text></comment></commentList></comments>`))
|
||||
comments := f.GetComments()
|
||||
assert.EqualValues(t, 2, len(comments["Sheet1"]))
|
||||
assert.EqualValues(t, 1, len(comments["Sheet2"]))
|
||||
|
@ -46,14 +46,14 @@ func TestAddComments(t *testing.T) {
|
|||
func TestDecodeVMLDrawingReader(t *testing.T) {
|
||||
f := NewFile()
|
||||
path := "xl/drawings/vmlDrawing1.xml"
|
||||
f.XLSX[path] = MacintoshCyrillicCharset
|
||||
f.Pkg.Store(path, MacintoshCyrillicCharset)
|
||||
f.decodeVMLDrawingReader(path)
|
||||
}
|
||||
|
||||
func TestCommentsReader(t *testing.T) {
|
||||
f := NewFile()
|
||||
path := "xl/comments1.xml"
|
||||
f.XLSX[path] = MacintoshCyrillicCharset
|
||||
f.Pkg.Store(path, MacintoshCyrillicCharset)
|
||||
f.commentsReader(path)
|
||||
}
|
||||
|
||||
|
|
11
crypt.go
11
crypt.go
|
@ -21,7 +21,6 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"hash"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
|
@ -145,7 +144,7 @@ func Decrypt(raw []byte, opt *Options) (packageBuf []byte, err error) {
|
|||
case "standard":
|
||||
return standardDecrypt(encryptionInfoBuf, encryptedPackageBuf, opt)
|
||||
default:
|
||||
err = errors.New("unsupport encryption mechanism")
|
||||
err = ErrUnsupportEncryptMechanism
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -265,7 +264,7 @@ func Encrypt(raw []byte, opt *Options) (packageBuf []byte, err error) {
|
|||
}
|
||||
// TODO: Create a new CFB.
|
||||
_, _ = encryptedPackage, encryptionInfoBuffer
|
||||
err = errors.New("not support encryption currently")
|
||||
err = ErrEncrypt
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -293,7 +292,7 @@ func extractPart(doc *mscfb.Reader) (encryptionInfoBuf, encryptedPackageBuf []by
|
|||
// encryptionMechanism parse password-protected documents created mechanism.
|
||||
func encryptionMechanism(buffer []byte) (mechanism string, err error) {
|
||||
if len(buffer) < 4 {
|
||||
err = errors.New("unknown encryption mechanism")
|
||||
err = ErrUnknownEncryptMechanism
|
||||
return
|
||||
}
|
||||
versionMajor, versionMinor := binary.LittleEndian.Uint16(buffer[0:2]), binary.LittleEndian.Uint16(buffer[2:4])
|
||||
|
@ -306,7 +305,7 @@ func encryptionMechanism(buffer []byte) (mechanism string, err error) {
|
|||
} else if (versionMajor == 3 || versionMajor == 4) && versionMinor == 3 {
|
||||
mechanism = "extensible"
|
||||
}
|
||||
err = errors.New("unsupport encryption mechanism")
|
||||
err = ErrUnsupportEncryptMechanism
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -470,7 +469,6 @@ func convertPasswdToKey(passwd string, blockKey []byte, encryption Encryption) (
|
|||
if len(key) < keyBytes {
|
||||
tmp := make([]byte, 0x36)
|
||||
key = append(key, tmp...)
|
||||
key = tmp
|
||||
} else if len(key) > keyBytes {
|
||||
key = key[:keyBytes]
|
||||
}
|
||||
|
@ -599,7 +597,6 @@ func createIV(blockKey interface{}, encryption Encryption) ([]byte, error) {
|
|||
if len(iv) < encryptedKey.BlockSize {
|
||||
tmp := make([]byte, 0x36)
|
||||
iv = append(iv, tmp...)
|
||||
iv = tmp
|
||||
} else if len(iv) > encryptedKey.BlockSize {
|
||||
iv = iv[0:encryptedKey.BlockSize]
|
||||
}
|
||||
|
|
|
@ -42,12 +42,12 @@ func TestSetDocProps(t *testing.T) {
|
|||
Version: "1.0.0",
|
||||
}))
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx")))
|
||||
f.XLSX["docProps/core.xml"] = nil
|
||||
f.Pkg.Store("docProps/core.xml", nil)
|
||||
assert.NoError(t, f.SetDocProps(&DocProperties{}))
|
||||
|
||||
// Test unsupported charset
|
||||
f = NewFile()
|
||||
f.XLSX["docProps/core.xml"] = MacintoshCyrillicCharset
|
||||
f.Pkg.Store("docProps/core.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualError(t, f.SetDocProps(&DocProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
||||
|
@ -59,13 +59,13 @@ func TestGetDocProps(t *testing.T) {
|
|||
props, err := f.GetDocProps()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, props.Creator, "Microsoft Office User")
|
||||
f.XLSX["docProps/core.xml"] = nil
|
||||
f.Pkg.Store("docProps/core.xml", nil)
|
||||
_, err = f.GetDocProps()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test unsupported charset
|
||||
f = NewFile()
|
||||
f.XLSX["docProps/core.xml"] = MacintoshCyrillicCharset
|
||||
f.Pkg.Store("docProps/core.xml", MacintoshCyrillicCharset)
|
||||
_, err = f.GetDocProps()
|
||||
assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
|
|
@ -1151,7 +1151,7 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) {
|
|||
content := xlsxWsDr{}
|
||||
content.A = NameSpaceDrawingML.Value
|
||||
content.Xdr = NameSpaceDrawingMLSpreadSheet.Value
|
||||
if _, ok = f.XLSX[path]; ok { // Append Model
|
||||
if _, ok = f.Pkg.Load(path); ok { // Append Model
|
||||
decodeWsDr := decodeWsDr{}
|
||||
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))).
|
||||
Decode(&decodeWsDr); err != nil && err != io.EOF {
|
||||
|
|
|
@ -19,10 +19,10 @@ import (
|
|||
func TestDrawingParser(t *testing.T) {
|
||||
f := File{
|
||||
Drawings: sync.Map{},
|
||||
XLSX: map[string][]byte{
|
||||
"charset": MacintoshCyrillicCharset,
|
||||
"wsDr": []byte(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"><xdr:oneCellAnchor><xdr:graphicFrame/></xdr:oneCellAnchor></xdr:wsDr>`)},
|
||||
Pkg: sync.Map{},
|
||||
}
|
||||
f.Pkg.Store("charset", MacintoshCyrillicCharset)
|
||||
f.Pkg.Store("wsDr", []byte(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"><xdr:oneCellAnchor><xdr:graphicFrame/></xdr:oneCellAnchor></xdr:wsDr>`))
|
||||
// Test with one cell anchor
|
||||
f.drawingParser("wsDr")
|
||||
// Test with unsupported charset
|
||||
|
|
34
errors.go
34
errors.go
|
@ -32,6 +32,10 @@ func newInvalidExcelDateError(dateValue float64) error {
|
|||
return fmt.Errorf("invalid date value %f, negative values are not supported supported", dateValue)
|
||||
}
|
||||
|
||||
func newUnsupportChartType(chartType string) error {
|
||||
return fmt.Errorf("unsupported chart type %s", chartType)
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrStreamSetColWidth defined the error message on set column width in
|
||||
// stream writing mode.
|
||||
|
@ -71,4 +75,34 @@ var (
|
|||
// ErrMaxFileNameLength defined the error message on receive the file name
|
||||
// length overflow.
|
||||
ErrMaxFileNameLength = errors.New("file name length exceeds maximum limit")
|
||||
// ErrEncrypt defined the error message on encryption spreadsheet.
|
||||
ErrEncrypt = errors.New("not support encryption currently")
|
||||
// ErrUnknownEncryptMechanism defined the error message on unsupport
|
||||
// encryption mechanism.
|
||||
ErrUnknownEncryptMechanism = errors.New("unknown encryption mechanism")
|
||||
// ErrUnsupportEncryptMechanism defined the error message on unsupport
|
||||
// encryption mechanism.
|
||||
ErrUnsupportEncryptMechanism = errors.New("unsupport encryption mechanism")
|
||||
// ErrParameterRequired defined the error message on receive the empty
|
||||
// parameter.
|
||||
ErrParameterRequired = errors.New("parameter is required")
|
||||
// ErrParameterInvalid defined the error message on receive the invalid
|
||||
// parameter.
|
||||
ErrParameterInvalid = errors.New("parameter is invalid")
|
||||
// ErrDefinedNameScope defined the error message on not found defined name
|
||||
// in the given scope.
|
||||
ErrDefinedNameScope = errors.New("no defined name on the scope")
|
||||
// ErrDefinedNameduplicate defined the error message on the same name
|
||||
// already exists on the scope.
|
||||
ErrDefinedNameduplicate = errors.New("the same name already exists on the scope")
|
||||
// ErrFontLength defined the error message on the length of the font
|
||||
// family name overflow.
|
||||
ErrFontLength = errors.New("the length of the font family name must be smaller than or equal to 31")
|
||||
// ErrFontSize defined the error message on the size of the font is invalid.
|
||||
ErrFontSize = errors.New("font size must be between 1 and 409 points")
|
||||
// ErrSheetIdx defined the error message on receive the invalid worksheet
|
||||
// index.
|
||||
ErrSheetIdx = errors.New("invalid worksheet index")
|
||||
// ErrGroupSheets defined the error message on group sheets.
|
||||
ErrGroupSheets = errors.New("group worksheet must contain an active worksheet")
|
||||
)
|
||||
|
|
71
excelize.go
71
excelize.go
|
@ -43,7 +43,7 @@ type File struct {
|
|||
Path string
|
||||
SharedStrings *xlsxSST
|
||||
sharedStringsMap map[string]int
|
||||
Sheet map[string]*xlsxWorksheet
|
||||
Sheet sync.Map // map[string]*xlsxWorksheet
|
||||
SheetCount int
|
||||
Styles *xlsxStyleSheet
|
||||
Theme *xlsxTheme
|
||||
|
@ -51,7 +51,7 @@ type File struct {
|
|||
VMLDrawing map[string]*vmlDrawing
|
||||
WorkBook *xlsxWorkbook
|
||||
Relationships sync.Map
|
||||
XLSX map[string][]byte
|
||||
Pkg sync.Map
|
||||
CharsetReader charsetTranscoderFn
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ func newFile() *File {
|
|||
Comments: make(map[string]*xlsxComments),
|
||||
Drawings: sync.Map{},
|
||||
sharedStringsMap: make(map[string]int),
|
||||
Sheet: make(map[string]*xlsxWorksheet),
|
||||
Sheet: sync.Map{},
|
||||
DecodeVMLDrawing: make(map[string]*decodeVmlDrawing),
|
||||
VMLDrawing: make(map[string]*vmlDrawing),
|
||||
Relationships: sync.Map{},
|
||||
|
@ -129,7 +129,10 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.SheetCount, f.XLSX = sheetCount, file
|
||||
f.SheetCount = sheetCount
|
||||
for k, v := range file {
|
||||
f.Pkg.Store(k, v)
|
||||
}
|
||||
f.CalcChain = f.calcChainReader()
|
||||
f.sheetMap = f.getSheetMap()
|
||||
f.Styles = f.stylesReader()
|
||||
|
@ -172,40 +175,40 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) {
|
|||
name string
|
||||
ok bool
|
||||
)
|
||||
|
||||
if name, ok = f.sheetMap[trimSheetName(sheet)]; !ok {
|
||||
err = fmt.Errorf("sheet %s is not exist", sheet)
|
||||
return
|
||||
}
|
||||
if ws = f.Sheet[name]; f.Sheet[name] == nil {
|
||||
if strings.HasPrefix(name, "xl/chartsheets") {
|
||||
err = fmt.Errorf("sheet %s is chart sheet", sheet)
|
||||
return
|
||||
}
|
||||
ws = new(xlsxWorksheet)
|
||||
if _, ok := f.xmlAttr[name]; !ok {
|
||||
d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name))))
|
||||
f.xmlAttr[name] = append(f.xmlAttr[name], getRootElement(d)...)
|
||||
}
|
||||
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))).
|
||||
Decode(ws); err != nil && err != io.EOF {
|
||||
err = fmt.Errorf("xml decode error: %s", err)
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
if f.checked == nil {
|
||||
f.checked = make(map[string]bool)
|
||||
}
|
||||
if ok = f.checked[name]; !ok {
|
||||
checkSheet(ws)
|
||||
if err = checkRow(ws); err != nil {
|
||||
return
|
||||
}
|
||||
f.checked[name] = true
|
||||
}
|
||||
f.Sheet[name] = ws
|
||||
if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil {
|
||||
ws = worksheet.(*xlsxWorksheet)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(name, "xl/chartsheets") {
|
||||
err = fmt.Errorf("sheet %s is chart sheet", sheet)
|
||||
return
|
||||
}
|
||||
ws = new(xlsxWorksheet)
|
||||
if _, ok := f.xmlAttr[name]; !ok {
|
||||
d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name))))
|
||||
f.xmlAttr[name] = append(f.xmlAttr[name], getRootElement(d)...)
|
||||
}
|
||||
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))).
|
||||
Decode(ws); err != nil && err != io.EOF {
|
||||
err = fmt.Errorf("xml decode error: %s", err)
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
if f.checked == nil {
|
||||
f.checked = make(map[string]bool)
|
||||
}
|
||||
if ok = f.checked[name]; !ok {
|
||||
checkSheet(ws)
|
||||
if err = checkRow(ws); err != nil {
|
||||
return
|
||||
}
|
||||
f.checked[name] = true
|
||||
}
|
||||
f.Sheet.Store(name, ws)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -375,7 +378,7 @@ func (f *File) AddVBAProject(bin string) error {
|
|||
})
|
||||
}
|
||||
file, _ := ioutil.ReadFile(bin)
|
||||
f.XLSX["xl/vbaProject.bin"] = file
|
||||
f.Pkg.Store("xl/vbaProject.bin", file)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -343,13 +343,17 @@ func TestSetCellHyperLink(t *testing.T) {
|
|||
f = NewFile()
|
||||
_, err = f.workSheetReader("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].Hyperlinks = &xlsxHyperlinks{Hyperlink: make([]xlsxHyperlink, 65530)}
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).Hyperlinks = &xlsxHyperlinks{Hyperlink: make([]xlsxHyperlink, 65530)}
|
||||
assert.EqualError(t, f.SetCellHyperLink("Sheet1", "A65531", "https://github.com/360EntSecGroup-Skylar/excelize", "External"), ErrTotalSheetHyperlinks.Error())
|
||||
|
||||
f = NewFile()
|
||||
_, err = f.workSheetReader("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
|
||||
err = f.SetCellHyperLink("Sheet1", "A1", "https://github.com/360EntSecGroup-Skylar/excelize", "External")
|
||||
assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)
|
||||
}
|
||||
|
@ -376,7 +380,9 @@ func TestGetCellHyperLink(t *testing.T) {
|
|||
f = NewFile()
|
||||
_, err = f.workSheetReader("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].Hyperlinks = &xlsxHyperlinks{
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).Hyperlinks = &xlsxHyperlinks{
|
||||
Hyperlink: []xlsxHyperlink{{Ref: "A1"}},
|
||||
}
|
||||
link, target, err = f.GetCellHyperLink("Sheet1", "A1")
|
||||
|
@ -384,7 +390,9 @@ func TestGetCellHyperLink(t *testing.T) {
|
|||
assert.Equal(t, link, true)
|
||||
assert.Equal(t, target, "")
|
||||
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
|
||||
link, target, err = f.GetCellHyperLink("Sheet1", "A1")
|
||||
assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)
|
||||
assert.Equal(t, link, false)
|
||||
|
@ -1112,8 +1120,8 @@ func TestSetSheetRow(t *testing.T) {
|
|||
assert.EqualError(t, f.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}),
|
||||
`cannot convert cell "" to coordinates: invalid cell name ""`)
|
||||
|
||||
assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", []interface{}{}), `pointer to slice expected`)
|
||||
assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", &f), `pointer to slice expected`)
|
||||
assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", []interface{}{}), ErrParameterInvalid.Error())
|
||||
assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", &f), ErrParameterInvalid.Error())
|
||||
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetRow.xlsx")))
|
||||
}
|
||||
|
||||
|
@ -1198,7 +1206,7 @@ func TestContentTypesReader(t *testing.T) {
|
|||
// Test unsupported charset.
|
||||
f := NewFile()
|
||||
f.ContentTypes = nil
|
||||
f.XLSX["[Content_Types].xml"] = MacintoshCyrillicCharset
|
||||
f.Pkg.Store("[Content_Types].xml", MacintoshCyrillicCharset)
|
||||
f.contentTypesReader()
|
||||
}
|
||||
|
||||
|
@ -1206,22 +1214,22 @@ func TestWorkbookReader(t *testing.T) {
|
|||
// Test unsupported charset.
|
||||
f := NewFile()
|
||||
f.WorkBook = nil
|
||||
f.XLSX["xl/workbook.xml"] = MacintoshCyrillicCharset
|
||||
f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset)
|
||||
f.workbookReader()
|
||||
}
|
||||
|
||||
func TestWorkSheetReader(t *testing.T) {
|
||||
// Test unsupported charset.
|
||||
f := NewFile()
|
||||
delete(f.Sheet, "xl/worksheets/sheet1.xml")
|
||||
f.XLSX["xl/worksheets/sheet1.xml"] = MacintoshCyrillicCharset
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
|
||||
_, err := f.workSheetReader("Sheet1")
|
||||
assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
// Test on no checked worksheet.
|
||||
f = NewFile()
|
||||
delete(f.Sheet, "xl/worksheets/sheet1.xml")
|
||||
f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData/></worksheet>`)
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData/></worksheet>`))
|
||||
f.checked = nil
|
||||
_, err = f.workSheetReader("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
|
@ -1232,7 +1240,7 @@ func TestRelsReader(t *testing.T) {
|
|||
f := NewFile()
|
||||
rels := "xl/_rels/workbook.xml.rels"
|
||||
f.Relationships.Store(rels, nil)
|
||||
f.XLSX[rels] = MacintoshCyrillicCharset
|
||||
f.Pkg.Store(rels, MacintoshCyrillicCharset)
|
||||
f.relsReader(rels)
|
||||
}
|
||||
|
||||
|
|
48
file.go
48
file.go
|
@ -26,18 +26,17 @@ import (
|
|||
// f := NewFile()
|
||||
//
|
||||
func NewFile() *File {
|
||||
file := make(map[string][]byte)
|
||||
file["_rels/.rels"] = []byte(XMLHeader + templateRels)
|
||||
file["docProps/app.xml"] = []byte(XMLHeader + templateDocpropsApp)
|
||||
file["docProps/core.xml"] = []byte(XMLHeader + templateDocpropsCore)
|
||||
file["xl/_rels/workbook.xml.rels"] = []byte(XMLHeader + templateWorkbookRels)
|
||||
file["xl/theme/theme1.xml"] = []byte(XMLHeader + templateTheme)
|
||||
file["xl/worksheets/sheet1.xml"] = []byte(XMLHeader + templateSheet)
|
||||
file["xl/styles.xml"] = []byte(XMLHeader + templateStyles)
|
||||
file["xl/workbook.xml"] = []byte(XMLHeader + templateWorkbook)
|
||||
file["[Content_Types].xml"] = []byte(XMLHeader + templateContentTypes)
|
||||
f := newFile()
|
||||
f.SheetCount, f.XLSX = 1, file
|
||||
f.Pkg.Store("_rels/.rels", []byte(XMLHeader+templateRels))
|
||||
f.Pkg.Store("docProps/app.xml", []byte(XMLHeader+templateDocpropsApp))
|
||||
f.Pkg.Store("docProps/core.xml", []byte(XMLHeader+templateDocpropsCore))
|
||||
f.Pkg.Store("xl/_rels/workbook.xml.rels", []byte(XMLHeader+templateWorkbookRels))
|
||||
f.Pkg.Store("xl/theme/theme1.xml", []byte(XMLHeader+templateTheme))
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(XMLHeader+templateSheet))
|
||||
f.Pkg.Store("xl/styles.xml", []byte(XMLHeader+templateStyles))
|
||||
f.Pkg.Store("xl/workbook.xml", []byte(XMLHeader+templateWorkbook))
|
||||
f.Pkg.Store("[Content_Types].xml", []byte(XMLHeader+templateContentTypes))
|
||||
f.SheetCount = 1
|
||||
f.CalcChain = f.calcChainReader()
|
||||
f.Comments = make(map[string]*xlsxComments)
|
||||
f.ContentTypes = f.contentTypesReader()
|
||||
|
@ -48,8 +47,9 @@ func NewFile() *File {
|
|||
f.WorkBook = f.workbookReader()
|
||||
f.Relationships = sync.Map{}
|
||||
f.Relationships.Store("xl/_rels/workbook.xml.rels", f.relsReader("xl/_rels/workbook.xml.rels"))
|
||||
f.Sheet["xl/worksheets/sheet1.xml"], _ = f.workSheetReader("Sheet1")
|
||||
f.sheetMap["Sheet1"] = "xl/worksheets/sheet1.xml"
|
||||
ws, _ := f.workSheetReader("Sheet1")
|
||||
f.Sheet.Store("xl/worksheets/sheet1.xml", ws)
|
||||
f.Theme = f.themeReader()
|
||||
return f
|
||||
}
|
||||
|
@ -165,20 +165,22 @@ func (f *File) writeToZip(zw *zip.Writer) error {
|
|||
}
|
||||
stream.rawData.Close()
|
||||
}
|
||||
|
||||
for path, content := range f.XLSX {
|
||||
if _, ok := f.streams[path]; ok {
|
||||
continue
|
||||
}
|
||||
fi, err := zw.Create(path)
|
||||
var err error
|
||||
f.Pkg.Range(func(path, content interface{}) bool {
|
||||
if err != nil {
|
||||
return err
|
||||
return false
|
||||
}
|
||||
_, err = fi.Write(content)
|
||||
if _, ok := f.streams[path.(string)]; ok {
|
||||
return true
|
||||
}
|
||||
var fi io.Writer
|
||||
fi, err = zw.Create(path.(string))
|
||||
if err != nil {
|
||||
return err
|
||||
return false
|
||||
}
|
||||
}
|
||||
_, err = fi.Write(content.([]byte))
|
||||
return true
|
||||
})
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
|
15
file_test.go
15
file_test.go
|
@ -5,6 +5,7 @@ import (
|
|||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -38,19 +39,19 @@ func TestWriteTo(t *testing.T) {
|
|||
{
|
||||
f := File{}
|
||||
buf := bytes.Buffer{}
|
||||
f.XLSX = make(map[string][]byte)
|
||||
f.XLSX["/d/"] = []byte("s")
|
||||
f.Pkg = sync.Map{}
|
||||
f.Pkg.Store("/d/", []byte("s"))
|
||||
_, err := f.WriteTo(bufio.NewWriter(&buf))
|
||||
assert.EqualError(t, err, "zip: write to directory")
|
||||
delete(f.XLSX, "/d/")
|
||||
f.Pkg.Delete("/d/")
|
||||
}
|
||||
// Test file path overflow
|
||||
{
|
||||
f := File{}
|
||||
buf := bytes.Buffer{}
|
||||
f.XLSX = make(map[string][]byte)
|
||||
f.Pkg = sync.Map{}
|
||||
const maxUint16 = 1<<16 - 1
|
||||
f.XLSX[strings.Repeat("s", maxUint16+1)] = nil
|
||||
f.Pkg.Store(strings.Repeat("s", maxUint16+1), nil)
|
||||
_, err := f.WriteTo(bufio.NewWriter(&buf))
|
||||
assert.EqualError(t, err, "zip: FileHeader.Name too long")
|
||||
}
|
||||
|
@ -58,8 +59,8 @@ func TestWriteTo(t *testing.T) {
|
|||
{
|
||||
f := File{}
|
||||
buf := bytes.Buffer{}
|
||||
f.XLSX = make(map[string][]byte)
|
||||
f.XLSX["s"] = nil
|
||||
f.Pkg = sync.Map{}
|
||||
f.Pkg.Store("s", nil)
|
||||
f.streams = make(map[string]*StreamWriter)
|
||||
file, _ := os.Open("123")
|
||||
f.streams["s"] = &StreamWriter{rawData: bufferedWriter{tmp: file}}
|
||||
|
|
6
lib.go
6
lib.go
|
@ -51,8 +51,8 @@ func ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {
|
|||
|
||||
// readXML provides a function to read XML content as string.
|
||||
func (f *File) readXML(name string) []byte {
|
||||
if content, ok := f.XLSX[name]; ok {
|
||||
return content
|
||||
if content, _ := f.Pkg.Load(name); content != nil {
|
||||
return content.([]byte)
|
||||
}
|
||||
if content, ok := f.streams[name]; ok {
|
||||
return content.rawData.buf.Bytes()
|
||||
|
@ -66,7 +66,7 @@ func (f *File) saveFileList(name string, content []byte) {
|
|||
newContent := make([]byte, 0, len(XMLHeader)+len(content))
|
||||
newContent = append(newContent, []byte(XMLHeader)...)
|
||||
newContent = append(newContent, content...)
|
||||
f.XLSX[name] = newContent
|
||||
f.Pkg.Store(name, newContent)
|
||||
}
|
||||
|
||||
// Read file content as string in a archive file.
|
||||
|
|
|
@ -71,13 +71,19 @@ func TestMergeCell(t *testing.T) {
|
|||
|
||||
f = NewFile()
|
||||
assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3"))
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}}
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}}
|
||||
assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3"))
|
||||
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}
|
||||
assert.EqualError(t, f.MergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`)
|
||||
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
|
||||
assert.EqualError(t, f.MergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
|
||||
}
|
||||
|
||||
|
@ -154,16 +160,24 @@ func TestUnmergeCell(t *testing.T) {
|
|||
// Test unmerged area on not exists worksheet.
|
||||
assert.EqualError(t, f.UnmergeCell("SheetN", "A1", "A1"), "sheet SheetN is not exist")
|
||||
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = nil
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).MergeCells = nil
|
||||
assert.NoError(t, f.UnmergeCell("Sheet1", "H7", "B15"))
|
||||
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}}
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}}
|
||||
assert.NoError(t, f.UnmergeCell("Sheet1", "H15", "B7"))
|
||||
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}
|
||||
assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`)
|
||||
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
|
||||
assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
|
||||
|
||||
}
|
||||
|
|
45
picture.go
45
picture.go
|
@ -223,11 +223,12 @@ func (f *File) addSheetPicture(sheet string, rID int) {
|
|||
// folder xl/drawings.
|
||||
func (f *File) countDrawings() int {
|
||||
c1, c2 := 0, 0
|
||||
for k := range f.XLSX {
|
||||
if strings.Contains(k, "xl/drawings/drawing") {
|
||||
f.Pkg.Range(func(k, v interface{}) bool {
|
||||
if strings.Contains(k.(string), "xl/drawings/drawing") {
|
||||
c1++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
f.Drawings.Range(func(rel, value interface{}) bool {
|
||||
if strings.Contains(rel.(string), "xl/drawings/drawing") {
|
||||
c2++
|
||||
|
@ -305,11 +306,12 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he
|
|||
// folder xl/media/image.
|
||||
func (f *File) countMedia() int {
|
||||
count := 0
|
||||
for k := range f.XLSX {
|
||||
if strings.Contains(k, "xl/media/image") {
|
||||
f.Pkg.Range(func(k, v interface{}) bool {
|
||||
if strings.Contains(k.(string), "xl/media/image") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
|
@ -318,16 +320,22 @@ func (f *File) countMedia() int {
|
|||
// and drawings that use it will reference the same image.
|
||||
func (f *File) addMedia(file []byte, ext string) string {
|
||||
count := f.countMedia()
|
||||
for name, existing := range f.XLSX {
|
||||
if !strings.HasPrefix(name, "xl/media/image") {
|
||||
continue
|
||||
var name string
|
||||
f.Pkg.Range(func(k, existing interface{}) bool {
|
||||
if !strings.HasPrefix(k.(string), "xl/media/image") {
|
||||
return true
|
||||
}
|
||||
if bytes.Equal(file, existing) {
|
||||
return name
|
||||
if bytes.Equal(file, existing.([]byte)) {
|
||||
name = k.(string)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if name != "" {
|
||||
return name
|
||||
}
|
||||
media := "xl/media/image" + strconv.Itoa(count+1) + ext
|
||||
f.XLSX[media] = file
|
||||
f.Pkg.Store(media, file)
|
||||
return media
|
||||
}
|
||||
|
||||
|
@ -468,8 +476,7 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) {
|
|||
}
|
||||
target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
|
||||
drawingXML := strings.Replace(target, "..", "xl", -1)
|
||||
_, ok := f.XLSX[drawingXML]
|
||||
if !ok {
|
||||
if _, ok := f.Pkg.Load(drawingXML); !ok {
|
||||
return "", nil, err
|
||||
}
|
||||
drawingRelationships := strings.Replace(
|
||||
|
@ -532,7 +539,10 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string)
|
|||
if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row {
|
||||
drawRel = f.getDrawingRelationships(drawingRelationships, deTwoCellAnchor.Pic.BlipFill.Blip.Embed)
|
||||
if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok {
|
||||
ret, buf = filepath.Base(drawRel.Target), f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)]
|
||||
ret = filepath.Base(drawRel.Target)
|
||||
if buffer, _ := f.Pkg.Load(strings.Replace(drawRel.Target, "..", "xl", -1)); buffer != nil {
|
||||
buf = buffer.([]byte)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -556,7 +566,10 @@ func (f *File) getPictureFromWsDr(row, col int, drawingRelationships string, wsD
|
|||
if drawRel = f.getDrawingRelationships(drawingRelationships,
|
||||
anchor.Pic.BlipFill.Blip.Embed); drawRel != nil {
|
||||
if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok {
|
||||
ret, buf = filepath.Base(drawRel.Target), f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)]
|
||||
ret = filepath.Base(drawRel.Target)
|
||||
if buffer, _ := f.Pkg.Load(strings.Replace(drawRel.Target, "..", "xl", -1)); buffer != nil {
|
||||
buf = buffer.([]byte)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -155,7 +155,7 @@ func TestGetPicture(t *testing.T) {
|
|||
assert.Empty(t, raw)
|
||||
f, err = prepareTestBook1()
|
||||
assert.NoError(t, err)
|
||||
f.XLSX["xl/drawings/drawing1.xml"] = MacintoshCyrillicCharset
|
||||
f.Pkg.Store("xl/drawings/drawing1.xml", MacintoshCyrillicCharset)
|
||||
_, _, err = f.getPicture(20, 5, "xl/drawings/drawing1.xml", "xl/drawings/_rels/drawing2.xml.rels")
|
||||
assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8")
|
||||
}
|
||||
|
@ -173,11 +173,12 @@ func TestAddPictureFromBytes(t *testing.T) {
|
|||
assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 1), "", "logo", ".png", imgFile))
|
||||
assert.NoError(t, f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 50), "", "logo", ".png", imgFile))
|
||||
imageCount := 0
|
||||
for fileName := range f.XLSX {
|
||||
if strings.Contains(fileName, "media/image") {
|
||||
f.Pkg.Range(func(fileName, v interface{}) bool {
|
||||
if strings.Contains(fileName.(string), "media/image") {
|
||||
imageCount++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
assert.Equal(t, 1, imageCount, "Duplicate image should only be stored once.")
|
||||
assert.EqualError(t, f.AddPictureFromBytes("SheetN", fmt.Sprint("A", 1), "", "logo", ".png", imgFile), "sheet SheetN is not exist")
|
||||
}
|
||||
|
@ -205,6 +206,8 @@ func TestDrawingResize(t *testing.T) {
|
|||
// Test calculate drawing resize with invalid coordinates.
|
||||
_, _, _, _, err = f.drawingResize("Sheet1", "", 1, 1, nil)
|
||||
assert.EqualError(t, err, `cannot convert cell "" to coordinates: invalid cell name ""`)
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
|
||||
assert.EqualError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ package excelize
|
|||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -163,7 +162,7 @@ func (f *File) AddPivotTable(opt *PivotTableOption) error {
|
|||
// properties.
|
||||
func (f *File) parseFormatPivotTableSet(opt *PivotTableOption) (*xlsxWorksheet, string, error) {
|
||||
if opt == nil {
|
||||
return nil, "", errors.New("parameter is required")
|
||||
return nil, "", ErrParameterRequired
|
||||
}
|
||||
pivotTableSheetName, _, err := f.adjustRange(opt.PivotTableRange)
|
||||
if err != nil {
|
||||
|
@ -192,11 +191,11 @@ func (f *File) parseFormatPivotTableSet(opt *PivotTableOption) (*xlsxWorksheet,
|
|||
// adjustRange adjust range, for example: adjust Sheet1!$E$31:$A$1 to Sheet1!$A$1:$E$31
|
||||
func (f *File) adjustRange(rangeStr string) (string, []int, error) {
|
||||
if len(rangeStr) < 1 {
|
||||
return "", []int{}, errors.New("parameter is required")
|
||||
return "", []int{}, ErrParameterRequired
|
||||
}
|
||||
rng := strings.Split(rangeStr, "!")
|
||||
if len(rng) != 2 {
|
||||
return "", []int{}, errors.New("parameter is invalid")
|
||||
return "", []int{}, ErrParameterInvalid
|
||||
}
|
||||
trimRng := strings.Replace(rng[1], "$", "", -1)
|
||||
coordinates, err := f.areaRefToCoordinates(trimRng)
|
||||
|
@ -205,7 +204,7 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) {
|
|||
}
|
||||
x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3]
|
||||
if x1 == x2 && y1 == y2 {
|
||||
return rng[0], []int{}, errors.New("parameter is invalid")
|
||||
return rng[0], []int{}, ErrParameterInvalid
|
||||
}
|
||||
|
||||
// Correct the coordinate area, such correct C1:B3 to B1:C3.
|
||||
|
@ -600,11 +599,12 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio
|
|||
// the folder xl/pivotTables.
|
||||
func (f *File) countPivotTables() int {
|
||||
count := 0
|
||||
for k := range f.XLSX {
|
||||
if strings.Contains(k, "xl/pivotTables/pivotTable") {
|
||||
f.Pkg.Range(func(k, v interface{}) bool {
|
||||
if strings.Contains(k.(string), "xl/pivotTables/pivotTable") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
|
@ -612,11 +612,12 @@ func (f *File) countPivotTables() int {
|
|||
// the folder xl/pivotCache.
|
||||
func (f *File) countPivotCache() int {
|
||||
count := 0
|
||||
for k := range f.XLSX {
|
||||
if strings.Contains(k, "xl/pivotCache/pivotCacheDefinition") {
|
||||
f.Pkg.Range(func(k, v interface{}) bool {
|
||||
if strings.Contains(k.(string), "xl/pivotCache/pivotCacheDefinition") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
|
|
|
@ -137,12 +137,12 @@ func TestAddPivotTable(t *testing.T) {
|
|||
ShowLastColumn: true,
|
||||
}))
|
||||
// Create pivot table with many data, many rows, many cols and defined name
|
||||
f.SetDefinedName(&DefinedName{
|
||||
assert.NoError(t, f.SetDefinedName(&DefinedName{
|
||||
Name: "dataRange",
|
||||
RefersTo: "Sheet1!$A$1:$E$31",
|
||||
Comment: "Pivot Table Data Range",
|
||||
Scope: "Sheet2",
|
||||
})
|
||||
}))
|
||||
assert.NoError(t, f.AddPivotTable(&PivotTableOption{
|
||||
DataRange: "dataRange",
|
||||
PivotTableRange: "Sheet2!$A$57:$AJ$91",
|
||||
|
|
7
rows.go
7
rows.go
|
@ -195,9 +195,12 @@ func (f *File) Rows(sheet string) (*Rows, error) {
|
|||
if !ok {
|
||||
return nil, ErrSheetNotExist{sheet}
|
||||
}
|
||||
if f.Sheet[name] != nil {
|
||||
if ws, ok := f.Sheet.Load(name); ok && ws != nil {
|
||||
worksheet := ws.(*xlsxWorksheet)
|
||||
worksheet.Lock()
|
||||
defer worksheet.Unlock()
|
||||
// flush data
|
||||
output, _ := xml.Marshal(f.Sheet[name])
|
||||
output, _ := xml.Marshal(worksheet)
|
||||
f.saveFileList(name, f.replaceNameSpaceBytes(name, output))
|
||||
}
|
||||
var (
|
||||
|
|
14
rows_test.go
14
rows_test.go
|
@ -43,11 +43,13 @@ func TestRows(t *testing.T) {
|
|||
}
|
||||
|
||||
f = NewFile()
|
||||
f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`<worksheet><sheetData><row r="1"><c r="A1" t="s"><v>1</v></c></row><row r="A"><c r="2" t="str"><v>B</v></c></row></sheetData></worksheet>`)
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="1"><c r="A1" t="s"><v>1</v></c></row><row r="A"><c r="2" t="str"><v>B</v></c></row></sheetData></worksheet>`))
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
delete(f.checked, "xl/worksheets/sheet1.xml")
|
||||
_, err = f.Rows("Sheet1")
|
||||
assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`)
|
||||
|
||||
f.XLSX["xl/worksheets/sheet1.xml"] = nil
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", nil)
|
||||
_, err = f.Rows("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
@ -187,7 +189,7 @@ func TestColumns(t *testing.T) {
|
|||
|
||||
func TestSharedStringsReader(t *testing.T) {
|
||||
f := NewFile()
|
||||
f.XLSX["xl/sharedStrings.xml"] = MacintoshCyrillicCharset
|
||||
f.Pkg.Store("xl/sharedStrings.xml", MacintoshCyrillicCharset)
|
||||
f.sharedStringsReader()
|
||||
si := xlsxSI{}
|
||||
assert.EqualValues(t, "", si.String())
|
||||
|
@ -874,12 +876,14 @@ func TestErrSheetNotExistError(t *testing.T) {
|
|||
|
||||
func TestCheckRow(t *testing.T) {
|
||||
f := NewFile()
|
||||
f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ><sheetData><row r="2"><c><v>1</v></c><c r="F2"><v>2</v></c><c><v>3</v></c><c><v>4</v></c><c r="M2"><v>5</v></c></row></sheetData></worksheet>`)
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ><sheetData><row r="2"><c><v>1</v></c><c r="F2"><v>2</v></c><c><v>3</v></c><c><v>4</v></c><c r="M2"><v>5</v></c></row></sheetData></worksheet>`))
|
||||
_, err := f.GetRows("Sheet1")
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, f.SetCellValue("Sheet1", "A1", false))
|
||||
f = NewFile()
|
||||
f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ><sheetData><row r="2"><c><v>1</v></c><c r="-"><v>2</v></c><c><v>3</v></c><c><v>4</v></c><c r="M2"><v>5</v></c></row></sheetData></worksheet>`)
|
||||
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ><sheetData><row r="2"><c><v>1</v></c><c r="-"><v>2</v></c><c><v>3</v></c><c><v>4</v></c><c r="M2"><v>5</v></c></row></sheetData></worksheet>`))
|
||||
f.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
delete(f.checked, "xl/worksheets/sheet1.xml")
|
||||
assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), `cannot convert cell "-" to coordinates: invalid cell name "-"`)
|
||||
}
|
||||
|
||||
|
|
55
sheet.go
55
sheet.go
|
@ -15,7 +15,6 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -152,25 +151,27 @@ func (f *File) workSheetWriter() {
|
|||
var arr []byte
|
||||
buffer := bytes.NewBuffer(arr)
|
||||
encoder := xml.NewEncoder(buffer)
|
||||
for p, sheet := range f.Sheet {
|
||||
if sheet != nil {
|
||||
f.Sheet.Range(func(p, ws interface{}) bool {
|
||||
if ws != nil {
|
||||
sheet := ws.(*xlsxWorksheet)
|
||||
for k, v := range sheet.SheetData.Row {
|
||||
f.Sheet[p].SheetData.Row[k].C = trimCell(v.C)
|
||||
sheet.SheetData.Row[k].C = trimCell(v.C)
|
||||
}
|
||||
if sheet.SheetPr != nil || sheet.Drawing != nil || sheet.Hyperlinks != nil || sheet.Picture != nil || sheet.TableParts != nil {
|
||||
f.addNameSpaces(p, SourceRelationship)
|
||||
f.addNameSpaces(p.(string), SourceRelationship)
|
||||
}
|
||||
// reusing buffer
|
||||
_ = encoder.Encode(sheet)
|
||||
f.saveFileList(p, replaceRelationshipsBytes(f.replaceNameSpaceBytes(p, buffer.Bytes())))
|
||||
ok := f.checked[p]
|
||||
f.saveFileList(p.(string), replaceRelationshipsBytes(f.replaceNameSpaceBytes(p.(string), buffer.Bytes())))
|
||||
ok := f.checked[p.(string)]
|
||||
if ok {
|
||||
delete(f.Sheet, p)
|
||||
f.checked[p] = false
|
||||
f.Sheet.Delete(p.(string))
|
||||
f.checked[p.(string)] = false
|
||||
}
|
||||
buffer.Reset()
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// trimCell provides a function to trim blank cells which created by fillColumns.
|
||||
|
@ -213,7 +214,7 @@ func (f *File) setSheet(index int, name string) {
|
|||
}
|
||||
path := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml"
|
||||
f.sheetMap[trimSheetName(name)] = path
|
||||
f.Sheet[path] = &ws
|
||||
f.Sheet.Store(path, &ws)
|
||||
f.xmlAttr[path] = []xml.Attr{NameSpaceSpreadSheet}
|
||||
}
|
||||
|
||||
|
@ -448,7 +449,7 @@ func (f *File) getSheetMap() map[string]string {
|
|||
if strings.HasPrefix(rel.Target, "/") {
|
||||
path = filepath.ToSlash(strings.TrimPrefix(strings.Replace(filepath.Clean(rel.Target), "\\", "/", -1), "/"))
|
||||
}
|
||||
if _, ok := f.XLSX[path]; ok {
|
||||
if _, ok := f.Pkg.Load(path); ok {
|
||||
maps[v.Name] = path
|
||||
}
|
||||
}
|
||||
|
@ -524,10 +525,10 @@ func (f *File) DeleteSheet(name string) {
|
|||
f.deleteSheetFromContentTypes(target)
|
||||
f.deleteCalcChain(sheet.SheetID, "")
|
||||
delete(f.sheetMap, sheetName)
|
||||
delete(f.XLSX, sheetXML)
|
||||
delete(f.XLSX, rels)
|
||||
f.Pkg.Delete(sheetXML)
|
||||
f.Pkg.Delete(rels)
|
||||
f.Relationships.Delete(rels)
|
||||
delete(f.Sheet, sheetXML)
|
||||
f.Sheet.Delete(sheetXML)
|
||||
delete(f.xmlAttr, sheetXML)
|
||||
f.SheetCount--
|
||||
}
|
||||
|
@ -573,7 +574,7 @@ func (f *File) deleteSheetFromContentTypes(target string) {
|
|||
//
|
||||
func (f *File) CopySheet(from, to int) error {
|
||||
if from < 0 || to < 0 || from == to || f.GetSheetName(from) == "" || f.GetSheetName(to) == "" {
|
||||
return errors.New("invalid worksheet index")
|
||||
return ErrSheetIdx
|
||||
}
|
||||
return f.copySheet(from, to)
|
||||
}
|
||||
|
@ -595,12 +596,11 @@ func (f *File) copySheet(from, to int) error {
|
|||
worksheet.Drawing = nil
|
||||
worksheet.TableParts = nil
|
||||
worksheet.PageSetUp = nil
|
||||
f.Sheet[path] = worksheet
|
||||
f.Sheet.Store(path, worksheet)
|
||||
toRels := "xl/worksheets/_rels/sheet" + toSheetID + ".xml.rels"
|
||||
fromRels := "xl/worksheets/_rels/sheet" + strconv.Itoa(f.getSheetID(fromSheet)) + ".xml.rels"
|
||||
_, ok := f.XLSX[fromRels]
|
||||
if ok {
|
||||
f.XLSX[toRels] = f.XLSX[fromRels]
|
||||
if rels, ok := f.Pkg.Load(fromRels); ok && rels != nil {
|
||||
f.Pkg.Store(toRels, rels.([]byte))
|
||||
}
|
||||
fromSheetXMLPath := f.sheetMap[trimSheetName(fromSheet)]
|
||||
fromSheetAttr := f.xmlAttr[fromSheetXMLPath]
|
||||
|
@ -824,9 +824,9 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) {
|
|||
if !ok {
|
||||
return result, ErrSheetNotExist{sheet}
|
||||
}
|
||||
if f.Sheet[name] != nil {
|
||||
if ws, ok := f.Sheet.Load(name); ok && ws != nil {
|
||||
// flush data
|
||||
output, _ := xml.Marshal(f.Sheet[name])
|
||||
output, _ := xml.Marshal(ws.(*xlsxWorksheet))
|
||||
f.saveFileList(name, f.replaceNameSpaceBytes(name, output))
|
||||
}
|
||||
return f.searchSheet(name, value, regSearch)
|
||||
|
@ -1483,7 +1483,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error {
|
|||
scope = f.GetSheetName(*dn.LocalSheetID)
|
||||
}
|
||||
if scope == definedName.Scope && dn.Name == definedName.Name {
|
||||
return errors.New("the same name already exists on the scope")
|
||||
return ErrDefinedNameduplicate
|
||||
}
|
||||
}
|
||||
wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d)
|
||||
|
@ -1518,7 +1518,7 @@ func (f *File) DeleteDefinedName(definedName *DefinedName) error {
|
|||
}
|
||||
}
|
||||
}
|
||||
return errors.New("no defined name on the scope")
|
||||
return ErrDefinedNameScope
|
||||
}
|
||||
|
||||
// GetDefinedName provides a function to get the defined names of the workbook
|
||||
|
@ -1558,7 +1558,7 @@ func (f *File) GroupSheets(sheets []string) error {
|
|||
}
|
||||
}
|
||||
if !inActiveSheet {
|
||||
return errors.New("group worksheet must contain an active worksheet")
|
||||
return ErrGroupSheets
|
||||
}
|
||||
// check worksheet exists
|
||||
wss := []*xlsxWorksheet{}
|
||||
|
@ -1714,8 +1714,7 @@ func (f *File) relsReader(path string) *xlsxRelationships {
|
|||
var err error
|
||||
rels, _ := f.Relationships.Load(path)
|
||||
if rels == nil {
|
||||
_, ok := f.XLSX[path]
|
||||
if ok {
|
||||
if _, ok := f.Pkg.Load(path); ok {
|
||||
c := xlsxRelationships{}
|
||||
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))).
|
||||
Decode(&c); err != nil && err != io.EOF {
|
||||
|
@ -1734,6 +1733,8 @@ func (f *File) relsReader(path string) *xlsxRelationships {
|
|||
// row to accept data. Missing rows are backfilled and given their row number
|
||||
// Uses the last populated row as a hint for the size of the next row to add
|
||||
func prepareSheetXML(ws *xlsxWorksheet, col int, row int) {
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
rowCount := len(ws.SheetData.Row)
|
||||
sizeHint := 0
|
||||
var ht float64
|
||||
|
|
|
@ -347,9 +347,13 @@ func TestSetActiveSheet(t *testing.T) {
|
|||
f.WorkBook.BookViews = nil
|
||||
f.SetActiveSheet(1)
|
||||
f.WorkBook.BookViews = &xlsxBookViews{WorkBookView: []xlsxWorkBookView{}}
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{}}
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{}}
|
||||
f.SetActiveSheet(1)
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = nil
|
||||
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetViews = nil
|
||||
f.SetActiveSheet(1)
|
||||
f = NewFile()
|
||||
f.SetActiveSheet(-1)
|
||||
|
@ -365,14 +369,14 @@ func TestSetSheetName(t *testing.T) {
|
|||
|
||||
func TestGetWorkbookPath(t *testing.T) {
|
||||
f := NewFile()
|
||||
delete(f.XLSX, "_rels/.rels")
|
||||
f.Pkg.Delete("_rels/.rels")
|
||||
assert.Equal(t, "", f.getWorkbookPath())
|
||||
}
|
||||
|
||||
func TestGetWorkbookRelsPath(t *testing.T) {
|
||||
f := NewFile()
|
||||
delete(f.XLSX, "xl/_rels/.rels")
|
||||
f.XLSX["_rels/.rels"] = []byte(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument" Target="/workbook.xml"/></Relationships>`)
|
||||
f.Pkg.Delete("xl/_rels/.rels")
|
||||
f.Pkg.Store("_rels/.rels", []byte(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument" Target="/workbook.xml"/></Relationships>`))
|
||||
assert.Equal(t, "_rels/workbook.xml.rels", f.getWorkbookRelsPath())
|
||||
}
|
||||
|
||||
|
|
|
@ -443,7 +443,9 @@ func TestSheetFormatPrOptions(t *testing.T) {
|
|||
func TestSetSheetFormatPr(t *testing.T) {
|
||||
f := NewFile()
|
||||
assert.NoError(t, f.GetSheetFormatPr("Sheet1"))
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].SheetFormatPr = nil
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetFormatPr = nil
|
||||
assert.NoError(t, f.SetSheetFormatPr("Sheet1", BaseColWidth(1.0)))
|
||||
// Test set formatting properties on not exists worksheet.
|
||||
assert.EqualError(t, f.SetSheetFormatPr("SheetN"), "sheet SheetN is not exist")
|
||||
|
@ -452,7 +454,9 @@ func TestSetSheetFormatPr(t *testing.T) {
|
|||
func TestGetSheetFormatPr(t *testing.T) {
|
||||
f := NewFile()
|
||||
assert.NoError(t, f.GetSheetFormatPr("Sheet1"))
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].SheetFormatPr = nil
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).SheetFormatPr = nil
|
||||
var (
|
||||
baseColWidth BaseColWidth
|
||||
defaultColWidth DefaultColWidth
|
||||
|
|
|
@ -467,7 +467,7 @@ func (f *File) parseFormatAddSparklineSet(sheet string, opt *SparklineOption) (*
|
|||
return ws, err
|
||||
}
|
||||
if opt == nil {
|
||||
return ws, errors.New("parameter is required")
|
||||
return ws, ErrParameterRequired
|
||||
}
|
||||
if len(opt.Location) < 1 {
|
||||
return ws, errors.New("parameter 'Location' is required")
|
||||
|
|
|
@ -253,7 +253,9 @@ func TestAddSparkline(t *testing.T) {
|
|||
Style: -1,
|
||||
}), `parameter 'Style' must betweent 0-35`)
|
||||
|
||||
f.Sheet["xl/worksheets/sheet1.xml"].ExtLst.Ext = `<extLst>
|
||||
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
|
||||
assert.True(t, ok)
|
||||
ws.(*xlsxWorksheet).ExtLst.Ext = `<extLst>
|
||||
<ext x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" uri="{05C60535-1F16-4fd2-B633-F4F36F0B64E0}">
|
||||
<x14:sparklineGroups
|
||||
xmlns:xm="http://schemas.microsoft.com/office/excel/2006/main">
|
||||
|
|
|
@ -301,7 +301,7 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error {
|
|||
}
|
||||
if !sw.sheetWritten {
|
||||
if len(sw.cols) > 0 {
|
||||
sw.rawData.WriteString("<cols>" + sw.cols + "</cols>")
|
||||
_, _ = sw.rawData.WriteString("<cols>" + sw.cols + "</cols>")
|
||||
}
|
||||
_, _ = sw.rawData.WriteString(`<sheetData>`)
|
||||
sw.sheetWritten = true
|
||||
|
@ -481,9 +481,9 @@ func (sw *StreamWriter) Flush() error {
|
|||
}
|
||||
|
||||
sheetPath := sw.File.sheetMap[trimSheetName(sw.Sheet)]
|
||||
delete(sw.File.Sheet, sheetPath)
|
||||
sw.File.Sheet.Delete(sheetPath)
|
||||
delete(sw.File.checked, sheetPath)
|
||||
delete(sw.File.XLSX, sheetPath)
|
||||
sw.File.Pkg.Delete(sheetPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -99,8 +99,8 @@ func TestStreamWriter(t *testing.T) {
|
|||
|
||||
// Test unsupported charset
|
||||
file = NewFile()
|
||||
delete(file.Sheet, "xl/worksheets/sheet1.xml")
|
||||
file.XLSX["xl/worksheets/sheet1.xml"] = MacintoshCyrillicCharset
|
||||
file.Sheet.Delete("xl/worksheets/sheet1.xml")
|
||||
file.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
|
||||
_, err = file.NewStreamWriter("Sheet1")
|
||||
assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8")
|
||||
|
||||
|
@ -145,7 +145,9 @@ func TestStreamTable(t *testing.T) {
|
|||
|
||||
// Verify the table has names.
|
||||
var table xlsxTable
|
||||
assert.NoError(t, xml.Unmarshal(file.XLSX["xl/tables/table1.xml"], &table))
|
||||
val, ok := file.Pkg.Load("xl/tables/table1.xml")
|
||||
assert.True(t, ok)
|
||||
assert.NoError(t, xml.Unmarshal(val.([]byte), &table))
|
||||
assert.Equal(t, "A", table.TableColumns.TableColumn[0].Name)
|
||||
assert.Equal(t, "B", table.TableColumns.TableColumn[1].Name)
|
||||
assert.Equal(t, "C", table.TableColumns.TableColumn[2].Name)
|
||||
|
|
|
@ -15,7 +15,6 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
@ -1104,14 +1103,14 @@ func parseFormatStyleSet(style interface{}) (*Style, error) {
|
|||
case *Style:
|
||||
fs = *v
|
||||
default:
|
||||
err = errors.New("invalid parameter type")
|
||||
err = ErrParameterInvalid
|
||||
}
|
||||
if fs.Font != nil {
|
||||
if len(fs.Font.Family) > MaxFontFamilyLength {
|
||||
return &fs, errors.New("the length of the font family name must be smaller than or equal to 31")
|
||||
return &fs, ErrFontLength
|
||||
}
|
||||
if fs.Font.Size > MaxFontSize {
|
||||
return &fs, errors.New("font size must be between 1 and 409 points")
|
||||
return &fs, ErrFontSize
|
||||
}
|
||||
}
|
||||
return &fs, err
|
||||
|
|
|
@ -201,7 +201,7 @@ func TestNewStyle(t *testing.T) {
|
|||
_, err = f.NewStyle(&Style{})
|
||||
assert.NoError(t, err)
|
||||
_, err = f.NewStyle(Style{})
|
||||
assert.EqualError(t, err, "invalid parameter type")
|
||||
assert.EqualError(t, err, ErrParameterInvalid.Error())
|
||||
|
||||
_, err = f.NewStyle(&Style{Font: &Font{Family: strings.Repeat("s", MaxFontFamilyLength+1)}})
|
||||
assert.EqualError(t, err, "the length of the font family name must be smaller than or equal to 31")
|
||||
|
@ -261,14 +261,14 @@ func TestStylesReader(t *testing.T) {
|
|||
f := NewFile()
|
||||
// Test read styles with unsupported charset.
|
||||
f.Styles = nil
|
||||
f.XLSX["xl/styles.xml"] = MacintoshCyrillicCharset
|
||||
f.Pkg.Store("xl/styles.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualValues(t, new(xlsxStyleSheet), f.stylesReader())
|
||||
}
|
||||
|
||||
func TestThemeReader(t *testing.T) {
|
||||
f := NewFile()
|
||||
// Test read theme with unsupported charset.
|
||||
f.XLSX["xl/theme/theme1.xml"] = MacintoshCyrillicCharset
|
||||
f.Pkg.Store("xl/theme/theme1.xml", MacintoshCyrillicCharset)
|
||||
assert.EqualValues(t, new(xlsxTheme), f.themeReader())
|
||||
}
|
||||
|
||||
|
|
7
table.go
7
table.go
|
@ -105,11 +105,12 @@ func (f *File) AddTable(sheet, hcell, vcell, format string) error {
|
|||
// folder xl/tables.
|
||||
func (f *File) countTables() int {
|
||||
count := 0
|
||||
for k := range f.XLSX {
|
||||
if strings.Contains(k, "xl/tables/table") {
|
||||
f.Pkg.Range(func(k, v interface{}) bool {
|
||||
if strings.Contains(k.(string), "xl/tables/table") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue