47 changes: 45 additions & 2 deletions lib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestColumnNameToNumber_Error(t *testing.T) {
}
}
_, err := ColumnNameToNumber("XFE")
assert.EqualError(t, err, "column number exceeds maximum limit")
assert.EqualError(t, err, ErrColumnNumber.Error())
}

func TestColumnNumberToName_OK(t *testing.T) {
Expand All @@ -98,7 +98,7 @@ func TestColumnNumberToName_Error(t *testing.T) {
}

_, err = ColumnNumberToName(TotalColumns + 1)
assert.EqualError(t, err, "column number exceeds maximum limit")
assert.EqualError(t, err, ErrColumnNumber.Error())
}

func TestSplitCellName_OK(t *testing.T) {
Expand Down Expand Up @@ -228,3 +228,46 @@ func TestStack(t *testing.T) {
assert.Equal(t, s.Peek(), nil)
assert.Equal(t, s.Pop(), nil)
}

func TestGenXMLNamespace(t *testing.T) {
assert.Equal(t, genXMLNamespace([]xml.Attr{
{Name: xml.Name{Space: NameSpaceXML, Local: "space"}, Value: "preserve"},
}), `xml:space="preserve">`)
}

func TestBstrUnmarshal(t *testing.T) {
bstrs := map[string]string{
"*": "*",
"*_x0000_": "*",
"*_x0008_": "*",
"_x0008_*": "*",
"*_x0008_*": "**",
"*_x4F60__x597D_": "*你好",
"*_xG000_": "*_xG000_",
"*_xG05F_x0001_*": "*_xG05F*",
"*_x005F__x0008_*": "*_x005F_*",
"*_x005F_x0001_*": "*_x0001_*",
"*_x005f_x005F__x0008_*": "*_x005F_*",
"*_x005F_x005F_xG05F_x0006_*": "*_x005F_xG05F*",
"*_x005F_x005F_x005F_x0006_*": "*_x005F_x0006_*",
"_x005F__x0008_******": "_x005F_******",
"******_x005F__x0008_": "******_x005F_",
"******_x005F__x0008_******": "******_x005F_******",
}
for bstr, expected := range bstrs {
assert.Equal(t, expected, bstrUnmarshal(bstr))
}
}

func TestBstrMarshal(t *testing.T) {
bstrs := map[string]string{
"*_xG05F_*": "*_xG05F_*",
"*_x0008_*": "*_x005F_x0008_*",
"*_x005F_*": "*_x005F_x005F_*",
"*_x005F_xG006_*": "*_x005F_x005F_xG006_*",
"*_x005F_x0006_*": "*_x005F_x005F_x005F_x0006_*",
}
for bstr, expected := range bstrs {
assert.Equal(t, expected, bstrMarshal(bstr))
}
}
30 changes: 22 additions & 8 deletions merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestMergeCell(t *testing.T) {
assert.NoError(t, f.SetCellValue("Sheet1", "G11", "set value in merged cell"))
assert.NoError(t, f.SetCellInt("Sheet1", "H11", 100))
assert.NoError(t, f.SetCellValue("Sheet1", "I11", float64(0.5)))
assert.NoError(t, f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External"))
assert.NoError(t, f.SetCellHyperLink("Sheet1", "J11", "https://github.com/xuri/excelize", "External"))
assert.NoError(t, f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)"))
value, err := f.GetCellValue("Sheet1", "H11")
assert.Equal(t, "0.5", value)
Expand Down Expand Up @@ -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"`)
}

Expand Down Expand Up @@ -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"`)

}
96 changes: 63 additions & 33 deletions picture.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"bytes"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"image"
"io"
Expand Down Expand Up @@ -55,7 +54,7 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) {
// _ "image/jpeg"
// _ "image/png"
//
// "github.com/360EntSecGroup-Skylar/excelize/v2"
// "github.com/xuri/excelize/v2"
// )
//
// func main() {
Expand All @@ -69,7 +68,7 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) {
// fmt.Println(err)
// }
// // Insert a picture offset in the cell with external hyperlink, printing and positioning support.
// if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`); err != nil {
// if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/xuri/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`); err != nil {
// fmt.Println(err)
// }
// if err := f.SaveAs("Book1.xlsx"); err != nil {
Expand All @@ -93,7 +92,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error {
}
ext, ok := supportImageTypes[path.Ext(picture)]
if !ok {
return errors.New("unsupported image extension")
return ErrImgExt
}
file, _ := ioutil.ReadFile(picture)
_, name := filepath.Split(picture)
Expand All @@ -111,7 +110,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error {
// _ "image/jpeg"
// "io/ioutil"
//
// "github.com/360EntSecGroup-Skylar/excelize/v2"
// "github.com/xuri/excelize/v2"
// )
//
// func main() {
Expand All @@ -134,7 +133,7 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string,
var hyperlinkType string
ext, ok := supportImageTypes[extension]
if !ok {
return errors.New("unsupported image extension")
return ErrImgExt
}
formatSet, err := parseFormatPictureSet(format)
if err != nil {
Expand Down Expand Up @@ -185,12 +184,14 @@ func (f *File) deleteSheetRelationships(sheet, rID string) {
if sheetRels == nil {
sheetRels = &xlsxRelationships{}
}
sheetRels.Lock()
defer sheetRels.Unlock()
for k, v := range sheetRels.Relationships {
if v.ID == rID {
sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...)
}
}
f.Relationships[rels] = sheetRels
f.Relationships.Store(rels, sheetRels)
}

// addSheetLegacyDrawing provides a function to add legacy drawing element to
Expand Down Expand Up @@ -224,16 +225,18 @@ 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++
}
}
for rel := range f.Drawings {
if strings.Contains(rel, "xl/drawings/drawing") {
return true
})
f.Drawings.Range(func(rel, value interface{}) bool {
if strings.Contains(rel.(string), "xl/drawings/drawing") {
c2++
}
}
return true
})
if c1 < c2 {
return c2
}
Expand Down Expand Up @@ -296,20 +299,23 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he
FLocksWithSheet: formatSet.FLocksWithSheet,
FPrintsWithSheet: formatSet.FPrintsWithSheet,
}
content.Lock()
defer content.Unlock()
content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
f.Drawings[drawingXML] = content
f.Drawings.Store(drawingXML, content)
return err
}

// countMedia provides a function to get media files count storage in the
// 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
}

Expand All @@ -318,16 +324,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
}

Expand All @@ -336,6 +348,8 @@ func (f *File) addMedia(file []byte, ext string) string {
func (f *File) setContentTypePartImageExtensions() {
var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false, "tiff": false}
content := f.contentTypesReader()
content.Lock()
defer content.Unlock()
for _, v := range content.Defaults {
_, ok := imageTypes[v.Extension]
if ok {
Expand All @@ -357,6 +371,8 @@ func (f *File) setContentTypePartImageExtensions() {
func (f *File) setContentTypePartVMLExtensions() {
vml := false
content := f.contentTypesReader()
content.Lock()
defer content.Unlock()
for _, v := range content.Defaults {
if v.Extension == "vml" {
vml = true
Expand Down Expand Up @@ -402,6 +418,8 @@ func (f *File) addContentTypePart(index int, contentType string) {
s()
}
content := f.contentTypesReader()
content.Lock()
defer content.Unlock()
for _, v := range content.Overrides {
if v.PartName == partNames[contentType] {
return
Expand All @@ -426,6 +444,8 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string {
if sheetRels == nil {
sheetRels = &xlsxRelationships{}
}
sheetRels.Lock()
defer sheetRels.Unlock()
for _, v := range sheetRels.Relationships {
if v.ID == rID {
return v.Target
Expand Down Expand Up @@ -468,8 +488,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(
Expand Down Expand Up @@ -532,7 +551,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
}
}
Expand All @@ -550,13 +572,18 @@ func (f *File) getPictureFromWsDr(row, col int, drawingRelationships string, wsD
anchor *xdrCellAnchor
drawRel *xlsxRelationship
)
wsDr.Lock()
defer wsDr.Unlock()
for _, anchor = range wsDr.TwoCellAnchor {
if anchor.From != nil && anchor.Pic != nil {
if anchor.From.Col == col && anchor.From.Row == row {
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
}
}
Expand All @@ -571,6 +598,8 @@ func (f *File) getPictureFromWsDr(row, col int, drawingRelationships string, wsD
// relationship ID.
func (f *File) getDrawingRelationships(rels, rID string) *xlsxRelationship {
if drawingRels := f.relsReader(rels); drawingRels != nil {
drawingRels.Lock()
defer drawingRels.Unlock()
for _, v := range drawingRels.Relationships {
if v.ID == rID {
return &v
Expand All @@ -583,12 +612,13 @@ func (f *File) getDrawingRelationships(rels, rID string) *xlsxRelationship {
// drawingsWriter provides a function to save xl/drawings/drawing%d.xml after
// serialize structure.
func (f *File) drawingsWriter() {
for path, d := range f.Drawings {
f.Drawings.Range(func(path, d interface{}) bool {
if d != nil {
v, _ := xml.Marshal(d)
f.saveFileList(path, v)
v, _ := xml.Marshal(d.(*xlsxWsDr))
f.saveFileList(path.(string), v)
}
}
return true
})
}

// drawingResize calculate the height and width after resizing.
Expand Down Expand Up @@ -619,10 +649,10 @@ func (f *File) drawingResize(sheet string, cell string, width, height float64, f
if inMergeCell {
cellWidth, cellHeight = 0, 0
c, r = rng[0], rng[1]
for col := rng[0] - 1; col < rng[2]; col++ {
for col := rng[0]; col <= rng[2]; col++ {
cellWidth += f.getColWidth(sheet, col)
}
for row := rng[1] - 1; row < rng[3]; row++ {
for row := rng[1]; row <= rng[3]; row++ {
cellHeight += f.getRowHeight(sheet, row)
}
}
Expand Down
19 changes: 11 additions & 8 deletions picture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestAddPicture(t *testing.T) {
`{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`))
// Test add picture to worksheet with offset, external hyperlink and positioning.
assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"),
`{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`))
`{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/xuri/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`))

file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.png"))
assert.NoError(t, err)
Expand Down Expand Up @@ -82,10 +82,10 @@ func TestAddPictureErrors(t *testing.T) {

// Test add picture to worksheet with unsupported file type.
err = xlsx.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), "")
assert.EqualError(t, err, "unsupported image extension")
assert.EqualError(t, err, ErrImgExt.Error())

err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", "jpg", make([]byte, 1))
assert.EqualError(t, err, "unsupported image extension")
assert.EqualError(t, err, ErrImgExt.Error())

// Test add picture to worksheet with invalid file data.
err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", ".jpg", make([]byte, 1))
Expand Down Expand Up @@ -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")
}
Expand All @@ -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")
}
Expand Down Expand Up @@ -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"`)
}
63 changes: 41 additions & 22 deletions pivotTable.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ package excelize

import (
"encoding/xml"
"errors"
"fmt"
"strconv"
"strings"
)

// PivotTableOption directly maps the format settings of the pivot table.
type PivotTableOption struct {
pivotTableSheetName string
DataRange string
PivotTableRange string
Rows []PivotTableField
Expand Down Expand Up @@ -82,7 +82,7 @@ type PivotTableField struct {
// "fmt"
// "math/rand"
//
// "github.com/360EntSecGroup-Skylar/excelize/v2"
// "github.com/xuri/excelize/v2"
// )
//
// func main() {
Expand Down Expand Up @@ -162,16 +162,21 @@ 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")
}
dataSheetName, _, err := f.adjustRange(opt.DataRange)
if err != nil {
return nil, "", fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error())
return nil, "", ErrParameterRequired
}
pivotTableSheetName, _, err := f.adjustRange(opt.PivotTableRange)
if err != nil {
return nil, "", fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error())
}
opt.pivotTableSheetName = pivotTableSheetName
dataRange := f.getDefinedNameRefTo(opt.DataRange, pivotTableSheetName)
if dataRange == "" {
dataRange = opt.DataRange
}
dataSheetName, _, err := f.adjustRange(dataRange)
if err != nil {
return nil, "", fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error())
}
dataSheet, err := f.workSheetReader(dataSheetName)
if err != nil {
return dataSheet, "", err
Expand All @@ -186,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)
Expand All @@ -199,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.
Expand All @@ -215,8 +220,12 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) {

// getPivotFieldsOrder provides a function to get order list of pivot table
// fields.
func (f *File) getPivotFieldsOrder(dataRange string) ([]string, error) {
func (f *File) getPivotFieldsOrder(opt *PivotTableOption) ([]string, error) {
order := []string{}
dataRange := f.getDefinedNameRefTo(opt.DataRange, opt.pivotTableSheetName)
if dataRange == "" {
dataRange = opt.DataRange
}
dataSheet, coordinates, err := f.adjustRange(dataRange)
if err != nil {
return order, fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error())
Expand All @@ -235,12 +244,18 @@ func (f *File) getPivotFieldsOrder(dataRange string) ([]string, error) {
// addPivotCache provides a function to create a pivot cache by given properties.
func (f *File) addPivotCache(pivotCacheID int, pivotCacheXML string, opt *PivotTableOption, ws *xlsxWorksheet) error {
// validate data range
dataSheet, coordinates, err := f.adjustRange(opt.DataRange)
definedNameRef := true
dataRange := f.getDefinedNameRefTo(opt.DataRange, opt.pivotTableSheetName)
if dataRange == "" {
definedNameRef = false
dataRange = opt.DataRange
}
dataSheet, coordinates, err := f.adjustRange(dataRange)
if err != nil {
return fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error())
}
// data range has been checked
order, _ := f.getPivotFieldsOrder(opt.DataRange)
order, _ := f.getPivotFieldsOrder(opt)
hcell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
vcell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
pc := xlsxPivotCacheDefinition{
Expand All @@ -258,7 +273,9 @@ func (f *File) addPivotCache(pivotCacheID int, pivotCacheXML string, opt *PivotT
},
CacheFields: &xlsxCacheFields{},
}

if definedNameRef {
pc.CacheSource.WorksheetSource = &xlsxWorksheetSource{Name: opt.DataRange}
}
for _, name := range order {
defaultRowsSubtotal, rowOk := f.getPivotTableFieldNameDefaultSubtotal(name, opt.Rows)
defaultColumnsSubtotal, colOk := f.getPivotTableFieldNameDefaultSubtotal(name, opt.Columns)
Expand Down Expand Up @@ -509,7 +526,7 @@ func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOp
// addPivotFields create pivot fields based on the column order of the first
// row in the data region by given pivot table definition and option.
func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error {
order, err := f.getPivotFieldsOrder(opt.DataRange)
order, err := f.getPivotFieldsOrder(opt)
if err != nil {
return err
}
Expand Down Expand Up @@ -582,31 +599,33 @@ 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
}

// countPivotCache provides a function to get drawing files count storage in
// 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
}

// getPivotFieldsIndex convert the column of the first row in the data region
// to a sequential index by given fields and pivot option.
func (f *File) getPivotFieldsIndex(fields []PivotTableField, opt *PivotTableOption) ([]int, error) {
pivotFieldsIndex := []int{}
orders, err := f.getPivotFieldsOrder(opt.DataRange)
orders, err := f.getPivotFieldsOrder(opt)
if err != nil {
return pivotFieldsIndex, err
}
Expand Down
19 changes: 14 additions & 5 deletions pivotTable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,16 @@ func TestAddPivotTable(t *testing.T) {
ShowColHeaders: true,
ShowLastColumn: true,
}))
//Test Pivot table with many data, many rows, many cols
// Create pivot table with many data, many rows, many cols and defined name
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: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet2!$A$56:$AG$90",
DataRange: "dataRange",
PivotTableRange: "Sheet2!$A$57:$AJ$91",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Sum of Sales"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}},
Expand Down Expand Up @@ -222,8 +228,11 @@ func TestAddPivotTable(t *testing.T) {
// Test adjust range with invalid range
_, _, err := f.adjustRange("")
assert.EqualError(t, err, "parameter is required")
// Test adjust range with incorrect range
_, _, err = f.adjustRange("sheet1!")
assert.EqualError(t, err, "parameter is invalid")
// Test get pivot fields order with empty data range
_, err = f.getPivotFieldsOrder("")
_, err = f.getPivotFieldsOrder(&PivotTableOption{})
assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`)
// Test add pivot cache with empty data range
assert.EqualError(t, f.addPivotCache(0, "", &PivotTableOption{}, nil), "parameter 'DataRange' parsing error: parameter is required")
Expand Down Expand Up @@ -288,7 +297,7 @@ func TestAddPivotColFields(t *testing.T) {
func TestGetPivotFieldsOrder(t *testing.T) {
f := NewFile()
// Test get pivot fields order with not exist worksheet
_, err := f.getPivotFieldsOrder("SheetN!$A$1:$E$31")
_, err := f.getPivotFieldsOrder(&PivotTableOption{DataRange: "SheetN!$A$1:$E$31"})
assert.EqualError(t, err, "sheet SheetN is not exist")
}

Expand Down
37 changes: 25 additions & 12 deletions rows.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ package excelize
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"log"
Expand Down Expand Up @@ -44,15 +43,19 @@ func (f *File) GetRows(sheet string) ([][]string, error) {
if err != nil {
return nil, err
}
results := make([][]string, 0, 64)
results, cur, max := make([][]string, 0, 64), 0, 0
for rows.Next() {
cur++
row, err := rows.Columns()
if err != nil {
break
}
results = append(results, row)
if len(row) > 0 {
max = cur
}
}
return results, nil
return results[:max], nil
}

// Rows defines an iterator to a sheet.
Expand Down Expand Up @@ -162,7 +165,9 @@ func rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement) {
}
blank := rowIterator.cellCol - len(rowIterator.columns)
val, _ := colCell.getValueFrom(rowIterator.rows.f, rowIterator.d)
rowIterator.columns = append(appendSpace(blank, rowIterator.columns), val)
if val != "" || colCell.F != nil {
rowIterator.columns = append(appendSpace(blank, rowIterator.columns), val)
}
}
}

Expand Down Expand Up @@ -190,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 (
Expand Down Expand Up @@ -245,7 +253,7 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error {
return newInvalidRowNumberError(row)
}
if height > MaxRowHeight {
return errors.New("the height of the row must be smaller than or equal to 409 points")
return ErrMaxRowHeight
}
ws, err := f.workSheetReader(sheet)
if err != nil {
Expand All @@ -261,12 +269,14 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error {
}

// getRowHeight provides a function to get row height in pixels by given sheet
// name and row index.
// name and row number.
func (f *File) getRowHeight(sheet string, row int) int {
ws, _ := f.workSheetReader(sheet)
ws.Lock()
defer ws.Unlock()
for i := range ws.SheetData.Row {
v := &ws.SheetData.Row[i]
if v.R == row+1 && v.Ht != 0 {
if v.R == row && v.Ht != 0 {
return int(convertRowHeightToPixels(v.Ht))
}
}
Expand All @@ -275,7 +285,7 @@ func (f *File) getRowHeight(sheet string, row int) int {
}

// GetRowHeight provides a function to get row height by given worksheet name
// and row index. For example, get the height of the first row in Sheet1:
// and row number. For example, get the height of the first row in Sheet1:
//
// height, err := f.GetRowHeight("Sheet1", 1)
//
Expand Down Expand Up @@ -317,6 +327,9 @@ func (f *File) sharedStringsReader() *xlsxSST {
Decode(&sharedStrings); err != nil && err != io.EOF {
log.Printf("xml decode error: %s", err)
}
if sharedStrings.Count == 0 {
sharedStrings.Count = len(sharedStrings.SI)
}
if sharedStrings.UniqueCount == 0 {
sharedStrings.UniqueCount = sharedStrings.Count
}
Expand Down Expand Up @@ -365,7 +378,7 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) {
return f.formattedValue(c.S, c.V), nil
default:
isNum, precision := isNumeric(c.V)
if isNum && precision > 15 {
if isNum && precision > 10 {
val, _ := roundPrecision(c.V)
if val != c.V {
return f.formattedValue(c.S, val), nil
Expand Down Expand Up @@ -436,7 +449,7 @@ func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) error {
return newInvalidRowNumberError(row)
}
if level > 7 || level < 1 {
return errors.New("invalid outline level")
return ErrOutlineLevel
}
ws, err := f.workSheetReader(sheet)
if err != nil {
Expand Down
58 changes: 35 additions & 23 deletions rows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -109,7 +111,7 @@ func TestRowHeight(t *testing.T) {
assert.Equal(t, 111.0, height)

// Test set row height overflow max row height limit.
assert.EqualError(t, f.SetRowHeight(sheet1, 4, MaxRowHeight+1), "the height of the row must be smaller than or equal to 409 points")
assert.EqualError(t, f.SetRowHeight(sheet1, 4, MaxRowHeight+1), ErrMaxRowHeight.Error())

// Test get row height that rows index over exists rows.
height, err = f.GetRowHeight(sheet1, 5)
Expand Down Expand Up @@ -138,12 +140,18 @@ func TestRowHeight(t *testing.T) {
// Test set row height with custom default row height with prepare XML.
assert.NoError(t, f.SetCellValue(sheet1, "A10", "A10"))

f.NewSheet("Sheet2")
assert.NoError(t, f.SetCellValue("Sheet2", "A2", true))
height, err = f.GetRowHeight("Sheet2", 1)
assert.NoError(t, err)
assert.Equal(t, 15.0, height)

err = f.SaveAs(filepath.Join("test", "TestRowHeight.xlsx"))
if !assert.NoError(t, err) {
t.FailNow()
}

convertColWidthToPixels(0)
assert.Equal(t, 0.0, convertColWidthToPixels(0))
}

func TestColumns(t *testing.T) {
Expand Down Expand Up @@ -181,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())
Expand Down Expand Up @@ -224,7 +232,7 @@ func TestRemoveRow(t *testing.T) {
)
fillCells(f, sheet1, colCount, rowCount)

assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External"))
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))

assert.EqualError(t, f.RemoveRow(sheet1, -1), "invalid row number -1")

Expand Down Expand Up @@ -285,7 +293,7 @@ func TestInsertRow(t *testing.T) {
)
fillCells(f, sheet1, colCount, rowCount)

assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External"))
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))

assert.EqualError(t, f.InsertRow(sheet1, -1), "invalid row number -1")

Expand Down Expand Up @@ -843,22 +851,24 @@ func TestGetValueFromInlineStr(t *testing.T) {
}

func TestGetValueFromNumber(t *testing.T) {
c := &xlsxC{T: "n", V: "2.2200000000000002"}
c := &xlsxC{T: "n"}
f := NewFile()
d := &xlsxSST{}
val, err := c.getValueFrom(f, d)
assert.NoError(t, err)
assert.Equal(t, "2.22", val)

c = &xlsxC{T: "n", V: "2.220000ddsf0000000002-r"}
val, err = c.getValueFrom(f, d)
assert.NoError(t, err)
assert.Equal(t, "2.220000ddsf0000000002-r", val)

c = &xlsxC{T: "n", V: "2.2."}
val, err = c.getValueFrom(f, d)
assert.NoError(t, err)
assert.Equal(t, "2.2.", val)
for input, expected := range map[string]string{
"2.2.": "2.2.",
"1.1000000000000001": "1.1",
"2.2200000000000002": "2.22",
"28.552": "28.552",
"27.399000000000001": "27.399",
"26.245999999999999": "26.246",
"2422.3000000000002": "2422.3",
"2.220000ddsf0000000002-r": "2.220000ddsf0000000002-r",
} {
c.V = input
val, err := c.getValueFrom(f, d)
assert.NoError(t, err)
assert.Equal(t, expected, val)
}
}

func TestErrSheetNotExistError(t *testing.T) {
Expand All @@ -868,12 +878,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 "-"`)
}

Expand Down
2 changes: 1 addition & 1 deletion shape.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format
FPrintsWithSheet: formatSet.Format.FPrintsWithSheet,
}
content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
f.Drawings[drawingXML] = content
f.Drawings.Store(drawingXML, content)
return err
}

Expand Down
193 changes: 101 additions & 92 deletions sheet.go

Large diffs are not rendered by default.

17 changes: 11 additions & 6 deletions sheet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ func ExampleFile_GetPageLayout() {

func TestNewSheet(t *testing.T) {
f := NewFile()
sheetID := f.NewSheet("Sheet2")
f.NewSheet("Sheet2")
sheetID := f.NewSheet("sheet2")
f.SetActiveSheet(sheetID)
// delete original sheet
f.DeleteSheet(f.GetSheetName(f.GetSheetIndex("Sheet1")))
Expand Down Expand Up @@ -347,9 +348,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)
Expand All @@ -365,14 +370,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())
}

Expand Down
8 changes: 6 additions & 2 deletions sheetpr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion sparkline.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 3 additions & 1 deletion sparkline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
139 changes: 115 additions & 24 deletions stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,25 @@ import (

// StreamWriter defined the type of stream writer.
type StreamWriter struct {
File *File
Sheet string
SheetID int
worksheet *xlsxWorksheet
rawData bufferedWriter
tableParts string
File *File
Sheet string
SheetID int
sheetWritten bool
cols string
worksheet *xlsxWorksheet
rawData bufferedWriter
mergeCellsCount int
mergeCells string
tableParts string
}

// NewStreamWriter return stream writer struct by given worksheet name for
// generate new worksheet with large amounts of data. Note that after set
// rows, you must call the 'Flush' method to end the streaming writing
// process and ensure that the order of line numbers is ascending, the common
// API and stream API can't be work mixed to writing data on the worksheets.
// For example, set data for worksheet of size 102400 rows x 50 columns with
// API and stream API can't be work mixed to writing data on the worksheets,
// you can't get cell value when in-memory chunks data over 16MB. For
// example, set data for worksheet of size 102400 rows x 50 columns with
// numbers and style:
//
// file := excelize.NewFile()
Expand All @@ -51,7 +56,8 @@ type StreamWriter struct {
// if err != nil {
// fmt.Println(err)
// }
// if err := streamWriter.SetRow("A1", []interface{}{excelize.Cell{StyleID: styleID, Value: "Data"}}); err != nil {
// if err := streamWriter.SetRow("A1", []interface{}{excelize.Cell{StyleID: styleID, Value: "Data"}},
// excelize.RowOpts{Height: 45, Hidden: false}); err != nil {
// fmt.Println(err)
// }
// for rowID := 2; rowID <= 102400; rowID++ {
Expand Down Expand Up @@ -94,22 +100,21 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) {
return nil, err
}

sheetXML := fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID)
sheetPath := f.sheetMap[trimSheetName(sheet)]
if f.streams == nil {
f.streams = make(map[string]*StreamWriter)
}
f.streams[sheetXML] = sw
f.streams[sheetPath] = sw

_, _ = sw.rawData.WriteString(XMLHeader + `<worksheet` + templateNamespaceIDMap)
bulkAppendFields(&sw.rawData, sw.worksheet, 2, 6)
_, _ = sw.rawData.WriteString(`<sheetData>`)
bulkAppendFields(&sw.rawData, sw.worksheet, 2, 5)
return sw, err
}

// AddTable creates an Excel table for the StreamWriter using the given
// coordinate area and format set. For example, create a table of A1:D5:
//
// err := sw.AddTable("A1", "D5", ``)
// err := sw.AddTable("A1", "D5", "")
//
// Create a table of F2:H6 with format set:
//
Expand Down Expand Up @@ -284,19 +289,35 @@ type Cell struct {
Value interface{}
}

// RowOpts define the options for set row.
type RowOpts struct {
Height float64
Hidden bool
}

// SetRow writes an array to stream rows by giving a worksheet name, starting
// coordinate and a pointer to an array of values. Note that you must call the
// 'Flush' method to end the streaming writing process.
//
// As a special case, if Cell is used as a value, then the Cell.StyleID will be
// applied to that cell.
func (sw *StreamWriter) SetRow(axis string, values []interface{}) error {
func (sw *StreamWriter) SetRow(axis string, values []interface{}, opts ...RowOpts) error {
col, row, err := CellNameToCoordinates(axis)
if err != nil {
return err
}

fmt.Fprintf(&sw.rawData, `<row r="%d">`, row)
if !sw.sheetWritten {
if len(sw.cols) > 0 {
_, _ = sw.rawData.WriteString("<cols>" + sw.cols + "</cols>")
}
_, _ = sw.rawData.WriteString(`<sheetData>`)
sw.sheetWritten = true
}
attrs, err := marshalRowAttrs(opts...)
if err != nil {
return err
}
fmt.Fprintf(&sw.rawData, `<row r="%d"%s>`, row, attrs)
for i, val := range values {
axis, err := CoordinatesToCellName(col+i, row)
if err != nil {
Expand All @@ -322,6 +343,68 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error {
return sw.rawData.Sync()
}

// marshalRowAttrs prepare attributes of the row by given options.
func marshalRowAttrs(opts ...RowOpts) (attrs string, err error) {
var opt *RowOpts
for _, o := range opts {
opt = &o
}
if opt == nil {
return
}
if opt.Height > MaxRowHeight {
err = ErrMaxRowHeight
return
}
if opt.Height > 0 {
attrs += fmt.Sprintf(` ht="%v" customHeight="true"`, opt.Height)
}
if opt.Hidden {
attrs += ` hidden="true"`
}
return
}

// SetColWidth provides a function to set the width of a single column or
// multiple columns for the StreamWriter. Note that you must call
// the 'SetColWidth' function before the 'SetRow' function. For example set
// the width column B:C as 20:
//
// err := streamWriter.SetColWidth(2, 3, 20)
//
func (sw *StreamWriter) SetColWidth(min, max int, width float64) error {
if sw.sheetWritten {
return ErrStreamSetColWidth
}
if min > TotalColumns || max > TotalColumns {
return ErrColumnNumber
}
if min < 1 || max < 1 {
return ErrColumnNumber
}
if width > MaxColumnWidth {
return ErrColumnWidth
}
if min > max {
min, max = max, min
}
sw.cols += fmt.Sprintf(`<col min="%d" max="%d" width="%f" customWidth="1"/>`, min, max, width)
return nil
}

// MergeCell provides a function to merge cells by a given coordinate area for
// the StreamWriter. Don't create a merged cell that overlaps with another
// existing merged cell.
func (sw *StreamWriter) MergeCell(hcell, vcell string) error {
_, err := areaRangeToCoordinates(hcell, vcell)
if err != nil {
return err
}
sw.mergeCellsCount++
sw.mergeCells += fmt.Sprintf(`<mergeCell ref="%s:%s"/>`, hcell, vcell)
return nil
}

// setCellFormula provides a function to set formula of a cell.
func setCellFormula(c *xlsxC, formula string) {
if formula != "" {
Expand Down Expand Up @@ -412,19 +495,28 @@ func writeCell(buf *bufferedWriter, c xlsxC) {

// Flush ending the streaming writing process.
func (sw *StreamWriter) Flush() error {
if !sw.sheetWritten {
_, _ = sw.rawData.WriteString(`<sheetData>`)
sw.sheetWritten = true
}
_, _ = sw.rawData.WriteString(`</sheetData>`)
bulkAppendFields(&sw.rawData, sw.worksheet, 8, 38)
bulkAppendFields(&sw.rawData, sw.worksheet, 8, 15)
if sw.mergeCellsCount > 0 {
sw.mergeCells = fmt.Sprintf(`<mergeCells count="%d">%s</mergeCells>`, sw.mergeCellsCount, sw.mergeCells)
}
_, _ = sw.rawData.WriteString(sw.mergeCells)
bulkAppendFields(&sw.rawData, sw.worksheet, 17, 38)
_, _ = sw.rawData.WriteString(sw.tableParts)
bulkAppendFields(&sw.rawData, sw.worksheet, 40, 40)
_, _ = sw.rawData.WriteString(`</worksheet>`)
if err := sw.rawData.Flush(); err != nil {
return err
}

sheetXML := fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID)
delete(sw.File.Sheet, sheetXML)
delete(sw.File.checked, sheetXML)
delete(sw.File.XLSX, sheetXML)
sheetPath := sw.File.sheetMap[trimSheetName(sw.Sheet)]
sw.File.Sheet.Delete(sheetPath)
delete(sw.File.checked, sheetPath)
sw.File.Pkg.Delete(sheetPath)

return nil
}
Expand Down Expand Up @@ -480,8 +572,7 @@ func (bw *bufferedWriter) Reader() (io.Reader, error) {
// buffer has grown large enough. Any error will be returned.
func (bw *bufferedWriter) Sync() (err error) {
// Try to use local storage
const chunk = 1 << 24
if bw.buf.Len() < chunk {
if bw.buf.Len() < StreamChunkSize {
return nil
}
if bw.tmp == nil {
Expand Down
53 changes: 47 additions & 6 deletions stream_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestStreamWriter(t *testing.T) {

// Test max characters in a cell.
row := make([]interface{}, 1)
row[0] = strings.Repeat("c", 32769)
row[0] = strings.Repeat("c", TotalCellChars+2)
assert.NoError(t, streamWriter.SetRow("A1", row))

// Test leading and ending space(s) character characters in a cell.
Expand All @@ -55,9 +55,11 @@ func TestStreamWriter(t *testing.T) {
// Test set cell with style.
styleID, err := file.NewStyle(`{"font":{"color":"#777777"}}`)
assert.NoError(t, err)
assert.NoError(t, streamWriter.SetRow("A4", []interface{}{Cell{StyleID: styleID}, Cell{Formula: "SUM(A10,B10)"}}))
assert.NoError(t, streamWriter.SetRow("A4", []interface{}{Cell{StyleID: styleID}, Cell{Formula: "SUM(A10,B10)"}}), RowOpts{Height: 45})
assert.NoError(t, streamWriter.SetRow("A5", []interface{}{&Cell{StyleID: styleID, Value: "cell"}, &Cell{Formula: "SUM(A10,B10)"}}))
assert.EqualError(t, streamWriter.SetRow("A6", []interface{}{time.Now()}), "only UTC time expected")
assert.NoError(t, streamWriter.SetRow("A6", []interface{}{time.Now()}))
assert.NoError(t, streamWriter.SetRow("A7", nil, RowOpts{Hidden: true}))
assert.EqualError(t, streamWriter.SetRow("A7", nil, RowOpts{Height: MaxRowHeight + 1}), ErrMaxRowHeight.Error())

for rowID := 10; rowID <= 51200; rowID++ {
row := make([]interface{}, 50)
Expand All @@ -72,6 +74,9 @@ func TestStreamWriter(t *testing.T) {
// Save spreadsheet by the given path.
assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx")))

// Test set cell column overflow.
assert.EqualError(t, streamWriter.SetRow("XFD1", []interface{}{"A", "B", "C"}), ErrColumnNumber.Error())

// Test close temporary file error.
file = NewFile()
streamWriter, err = file.NewStreamWriter("Sheet1")
Expand All @@ -96,10 +101,32 @@ 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")

// Test read cell.
file = NewFile()
streamWriter, err = file.NewStreamWriter("Sheet1")
assert.NoError(t, err)
assert.NoError(t, streamWriter.SetRow("A1", []interface{}{Cell{StyleID: styleID, Value: "Data"}}))
assert.NoError(t, streamWriter.Flush())
cellValue, err := file.GetCellValue("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "Data", cellValue)
}

func TestStreamSetColWidth(t *testing.T) {
file := NewFile()
streamWriter, err := file.NewStreamWriter("Sheet1")
assert.NoError(t, err)
assert.NoError(t, streamWriter.SetColWidth(3, 2, 20))
assert.EqualError(t, streamWriter.SetColWidth(0, 3, 20), ErrColumnNumber.Error())
assert.EqualError(t, streamWriter.SetColWidth(TotalColumns+1, 3, 20), ErrColumnNumber.Error())
assert.EqualError(t, streamWriter.SetColWidth(1, 3, MaxColumnWidth+1), ErrColumnWidth.Error())
assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"}))
assert.EqualError(t, streamWriter.SetColWidth(2, 3, 20), ErrStreamSetColWidth.Error())
}

func TestStreamTable(t *testing.T) {
Expand All @@ -120,7 +147,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)
Expand All @@ -134,6 +163,18 @@ func TestStreamTable(t *testing.T) {
assert.EqualError(t, streamWriter.AddTable("A1", "B", `{}`), `cannot convert cell "B" to coordinates: invalid cell name "B"`)
}

func TestStreamMergeCells(t *testing.T) {
file := NewFile()
streamWriter, err := file.NewStreamWriter("Sheet1")
assert.NoError(t, err)
assert.NoError(t, streamWriter.MergeCell("A1", "D1"))
// Test merge cells with illegal cell coordinates.
assert.EqualError(t, streamWriter.MergeCell("A", "D1"), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
assert.NoError(t, streamWriter.Flush())
// Save spreadsheet by the given path.
assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamMergeCells.xlsx")))
}

func TestNewStreamWriter(t *testing.T) {
// Test error exceptions
file := NewFile()
Expand Down
64 changes: 35 additions & 29 deletions styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"bytes"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"log"
Expand Down Expand Up @@ -46,9 +45,9 @@ var builtInNumFmt = map[int]string{
17: "mmm-yy",
18: "h:mm am/pm",
19: "h:mm:ss am/pm",
20: "h:mm",
21: "h:mm:ss",
22: "m/d/yy h:mm",
20: "hh:mm",
21: "hh:mm:ss",
22: "m/d/yy hh:mm",
37: "#,##0 ;(#,##0)",
38: "#,##0 ;[red](#,##0)",
39: "#,##0.00;(#,##0.00)",
Expand Down Expand Up @@ -934,8 +933,6 @@ func formatToE(v string, format string) string {
return fmt.Sprintf("%.e", f)
}

var dateTimeFormatsCache = map[string]string{}

// parseTime provides a function to returns a string parsed using time.Time.
// Replace Excel placeholders with Go time placeholders. For example, replace
// yyyy with 2006. These are in a specific order, due to the fact that m is
Expand Down Expand Up @@ -963,11 +960,6 @@ func parseTime(v string, format string) string {
return v
}

goFmt, found := dateTimeFormatsCache[format]
if found {
return val.Format(goFmt)
}

goFmt = format

if strings.Contains(goFmt, "[") {
Expand Down Expand Up @@ -1004,6 +996,7 @@ func parseTime(v string, format string) string {
{"mm", "01"},
{"am/pm", "pm"},
{"m/", "1/"},
{"m", "1"},
{"%%%%", "January"},
{"&&&&", "Monday"},
}
Expand All @@ -1013,19 +1006,29 @@ func parseTime(v string, format string) string {
{"\\ ", " "},
{"\\.", "."},
{"\\", ""},
{"\"", ""},
}
// It is the presence of the "am/pm" indicator that determines if this is
// a 12 hour or 24 hours time format, not the number of 'h' characters.
var padding bool
if val.Hour() == 0 && !strings.Contains(format, "hh") && !strings.Contains(format, "HH") {
padding = true
}
if is12HourTime(format) {
goFmt = strings.Replace(goFmt, "hh", "3", 1)
goFmt = strings.Replace(goFmt, "h", "3", 1)
goFmt = strings.Replace(goFmt, "HH", "3", 1)
goFmt = strings.Replace(goFmt, "H", "3", 1)
} else {
goFmt = strings.Replace(goFmt, "hh", "15", 1)
goFmt = strings.Replace(goFmt, "h", "3", 1)
goFmt = strings.Replace(goFmt, "HH", "15", 1)
goFmt = strings.Replace(goFmt, "H", "3", 1)
if 0 < val.Hour() && val.Hour() < 12 {
goFmt = strings.Replace(goFmt, "h", "3", 1)
goFmt = strings.Replace(goFmt, "H", "3", 1)
} else {
goFmt = strings.Replace(goFmt, "h", "15", 1)
goFmt = strings.Replace(goFmt, "H", "15", 1)
}
}

for _, repl := range replacements {
Expand All @@ -1045,10 +1048,11 @@ func parseTime(v string, format string) string {
goFmt = strings.Replace(goFmt, "[3]", "3", 1)
goFmt = strings.Replace(goFmt, "[15]", "15", 1)
}

dateTimeFormatsCache[format] = goFmt

return val.Format(goFmt)
s := val.Format(goFmt)
if padding {
s = strings.Replace(s, "00:", "0:", 1)
}
return s
}

// is12HourTime checks whether an Excel time format string is a 12 hours form.
Expand Down Expand Up @@ -1101,14 +1105,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
Expand Down Expand Up @@ -1988,6 +1992,8 @@ func (f *File) NewStyle(style interface{}) (int, error) {
fs.DecimalPlaces = 2
}
s := f.stylesReader()
s.Lock()
defer s.Unlock()
// check given style already exist.
if cellXfsID = f.getStyleID(s, fs); cellXfsID != -1 {
return cellXfsID, err
Expand Down Expand Up @@ -2187,17 +2193,16 @@ func (f *File) newFont(style *Style) *xlsxFont {
Family: &attrValInt{Val: intPtr(2)},
}
if style.Font.Bold {
fnt.B = &style.Font.Bold
fnt.B = &attrValBool{Val: &style.Font.Bold}
}
if style.Font.Italic {
fnt.I = &style.Font.Italic
fnt.I = &attrValBool{Val: &style.Font.Italic}
}
if *fnt.Name.Val == "" {
*fnt.Name.Val = f.GetDefaultFont()
}
if style.Font.Strike {
strike := true
fnt.Strike = &strike
fnt.Strike = &attrValBool{Val: &style.Font.Strike}
}
val, ok := fontUnderlineType[style.Font.Underline]
if ok {
Expand Down Expand Up @@ -2692,7 +2697,8 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error {
}
prepareSheetXML(ws, vcol, vrow)
makeContiguousColumns(ws, hrow, vrow, vcol)

ws.Lock()
defer ws.Unlock()
for r := hrowIdx; r <= vrowIdx; r++ {
for k := hcolIdx; k <= vcolIdx; k++ {
ws.SheetData.Row[r].C[k].S = styleID
Expand Down Expand Up @@ -3126,11 +3132,11 @@ func ThemeColor(baseColor string, tint float64) string {
if tint == 0 {
return "FF" + baseColor
}
r, _ := strconv.ParseInt(baseColor[0:2], 16, 64)
g, _ := strconv.ParseInt(baseColor[2:4], 16, 64)
b, _ := strconv.ParseInt(baseColor[4:6], 16, 64)
r, _ := strconv.ParseUint(baseColor[0:2], 16, 64)
g, _ := strconv.ParseUint(baseColor[2:4], 16, 64)
b, _ := strconv.ParseUint(baseColor[4:6], 16, 64)
var h, s, l float64
if r >= 0 && r <= math.MaxUint8 && g >= 0 && g <= math.MaxUint8 && b >= 0 && b <= math.MaxUint8 {
if r <= math.MaxUint8 && g <= math.MaxUint8 && b <= math.MaxUint8 {
h, s, l = RGBToHSL(uint8(r), uint8(g), uint8(b))
}
if tint < 0 {
Expand Down
14 changes: 11 additions & 3 deletions styles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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())
}

Expand All @@ -293,6 +293,14 @@ func TestParseTime(t *testing.T) {
assert.Equal(t, "2019-03-04 05:05:42", parseTime("43528.2123", "YYYY-MM-DD hh:mm:ss"))
assert.Equal(t, "2019-03-04 05:05:42", parseTime("43528.2123", "YYYY-MM-DD hh:mm:ss;YYYY-MM-DD hh:mm:ss"))
assert.Equal(t, "3/4/2019 5:5:42", parseTime("43528.2123", "M/D/YYYY h:m:s"))
assert.Equal(t, "3/4/2019 0:5:42", parseTime("43528.003958333335", "m/d/yyyy h:m:s"))
assert.Equal(t, "3/4/2019 0:05:42", parseTime("43528.003958333335", "M/D/YYYY h:mm:s"))
assert.Equal(t, "0:05", parseTime("43528.003958333335", "h:mm"))
assert.Equal(t, "0:0", parseTime("6.9444444444444444E-5", "h:m"))
assert.Equal(t, "0:00", parseTime("6.9444444444444444E-5", "h:mm"))
assert.Equal(t, "0:0", parseTime("6.9444444444444444E-5", "h:m"))
assert.Equal(t, "12:1", parseTime("0.50070601851851848", "h:m"))
assert.Equal(t, "23:30", parseTime("0.97952546296296295", "h:m"))
assert.Equal(t, "March", parseTime("43528", "mmmm"))
assert.Equal(t, "Monday", parseTime("43528", "dddd"))
}
Expand Down
9 changes: 5 additions & 4 deletions table.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func parseFormatTableSet(formatSet string) (*formatTable, error) {
// name, coordinate area and format set. For example, create a table of A1:D5
// on Sheet1:
//
// err := f.AddTable("Sheet1", "A1", "D5", ``)
// err := f.AddTable("Sheet1", "A1", "D5", "")
//
// Create a table of F2:H6 on Sheet2 with format set:
//
Expand Down Expand Up @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions xmlChart.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ type cPageMargins struct {

// formatChartAxis directly maps the format settings of the chart axis.
type formatChartAxis struct {
None bool `json:"none"`
Crossing string `json:"crossing"`
MajorGridlines bool `json:"major_grid_lines"`
MinorGridlines bool `json:"minor_grid_lines"`
Expand Down
4 changes: 2 additions & 2 deletions xmlComments.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import "encoding/xml"
// something special about the cell.
type xlsxComments struct {
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main comments"`
Authors []xlsxAuthor `xml:"authors"`
Authors xlsxAuthor `xml:"authors"`
CommentList xlsxCommentList `xml:"commentList"`
}

Expand All @@ -33,7 +33,7 @@ type xlsxComments struct {
// have an author. The maximum length of the author string is an implementation
// detail, but a good guideline is 255 chars.
type xlsxAuthor struct {
Author string `xml:"author"`
Author []string `xml:"author"`
}

// xlsxCommentList (List of Comments) directly maps the xlsxCommentList element.
Expand Down
6 changes: 5 additions & 1 deletion xmlContentTypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@

package excelize

import "encoding/xml"
import (
"encoding/xml"
"sync"
)

// xlsxTypes directly maps the types element of content types for relationship
// parts, it takes a Multipurpose Internet Mail Extension (MIME) media type as a
// value.
type xlsxTypes struct {
sync.Mutex
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/content-types Types"`
Overrides []xlsxOverride `xml:"Override"`
Defaults []xlsxDefault `xml:"Default"`
Expand Down
7 changes: 6 additions & 1 deletion xmlDrawing.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@

package excelize

import "encoding/xml"
import (
"encoding/xml"
"sync"
)

// Source relationship and namespace list, associated prefixes and schema in which it was
// introduced.
Expand Down Expand Up @@ -91,6 +94,7 @@ const (

// Excel specifications and limits
const (
StreamChunkSize = 1 << 24
MaxFontFamilyLength = 31
MaxFontSize = 409
MaxFileNameLength = 207
Expand Down Expand Up @@ -302,6 +306,7 @@ type xlsxPoint2D struct {
// xlsxWsDr directly maps the root element for a part of this content type shall
// wsDr.
type xlsxWsDr struct {
sync.Mutex
XMLName xml.Name `xml:"xdr:wsDr"`
AbsoluteAnchor []*xdrCellAnchor `xml:"xdr:absoluteAnchor"`
OneCellAnchor []*xdrCellAnchor `xml:"xdr:oneCellAnchor"`
Expand Down
4 changes: 2 additions & 2 deletions xmlSharedStrings.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ func (x xlsxSI) String() string {
rows.WriteString(s.T.Val)
}
}
return rows.String()
return bstrUnmarshal(rows.String())
}
if x.T != nil {
return x.T.Val
return bstrUnmarshal(x.T.Val)
}
return ""
}
Expand Down
20 changes: 12 additions & 8 deletions xmlStyles.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@

package excelize

import "encoding/xml"
import (
"encoding/xml"
"sync"
)

// xlsxStyleSheet is the root element of the Styles part.
type xlsxStyleSheet struct {
sync.Mutex
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main styleSheet"`
NumFmts *xlsxNumFmts `xml:"numFmts,omitempty"`
Fonts *xlsxFonts `xml:"fonts,omitempty"`
Expand Down Expand Up @@ -83,13 +87,13 @@ type xlsxFonts struct {
// xlsxFont directly maps the font element. This element defines the
// properties for one of the fonts used in this workbook.
type xlsxFont struct {
B *bool `xml:"b,omitempty"`
I *bool `xml:"i,omitempty"`
Strike *bool `xml:"strike,omitempty"`
Outline *bool `xml:"outline,omitempty"`
Shadow *bool `xml:"shadow,omitempty"`
Condense *bool `xml:"condense,omitempty"`
Extend *bool `xml:"extend,omitempty"`
B *attrValBool `xml:"b,omitempty"`
I *attrValBool `xml:"i,omitempty"`
Strike *attrValBool `xml:"strike,omitempty"`
Outline *attrValBool `xml:"outline,omitempty"`
Shadow *attrValBool `xml:"shadow,omitempty"`
Condense *attrValBool `xml:"condense,omitempty"`
Extend *attrValBool `xml:"extend,omitempty"`
U *attrValString `xml:"u"`
Sz *attrValFloat `xml:"sz"`
Color *xlsxColor `xml:"color"`
Expand Down
6 changes: 5 additions & 1 deletion xmlWorkbook.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@

package excelize

import "encoding/xml"
import (
"encoding/xml"
"sync"
)

// xlsxRelationships describe references from parts to other internal resources in the package or to external resources.
type xlsxRelationships struct {
sync.Mutex
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/relationships Relationships"`
Relationships []xlsxRelationship `xml:"Relationship"`
}
Expand Down
33 changes: 17 additions & 16 deletions xmlWorksheet.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,23 +108,23 @@ type xlsxPageSetUp struct {
XMLName xml.Name `xml:"pageSetup"`
BlackAndWhite bool `xml:"blackAndWhite,attr,omitempty"`
CellComments string `xml:"cellComments,attr,omitempty"`
Copies uint `xml:"copies,attr,omitempty"`
Copies int `xml:"copies,attr,omitempty"`
Draft bool `xml:"draft,attr,omitempty"`
Errors string `xml:"errors,attr,omitempty"`
FirstPageNumber uint `xml:"firstPageNumber,attr,omitempty"`
FirstPageNumber string `xml:"firstPageNumber,attr,omitempty"`
FitToHeight int `xml:"fitToHeight,attr,omitempty"`
FitToWidth int `xml:"fitToWidth,attr,omitempty"`
HorizontalDPI uint `xml:"horizontalDpi,attr,omitempty"`
HorizontalDPI int `xml:"horizontalDpi,attr,omitempty"`
RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"`
Orientation string `xml:"orientation,attr,omitempty"`
PageOrder string `xml:"pageOrder,attr,omitempty"`
PaperHeight string `xml:"paperHeight,attr,omitempty"`
PaperSize int `xml:"paperSize,attr,omitempty"`
PaperWidth string `xml:"paperWidth,attr,omitempty"`
Scale uint `xml:"scale,attr,omitempty"`
Scale int `xml:"scale,attr,omitempty"`
UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"`
UsePrinterDefaults bool `xml:"usePrinterDefaults,attr,omitempty"`
VerticalDPI uint `xml:"verticalDpi,attr,omitempty"`
VerticalDPI int `xml:"verticalDpi,attr,omitempty"`
}

// xlsxPrintOptions directly maps the printOptions element in the namespace
Expand Down Expand Up @@ -248,9 +248,9 @@ type xlsxSheetPr struct {
// adjust the direction of grouper controls.
type xlsxOutlinePr struct {
ApplyStyles *bool `xml:"applyStyles,attr"`
SummaryBelow bool `xml:"summaryBelow,attr,omitempty"`
SummaryRight bool `xml:"summaryRight,attr,omitempty"`
ShowOutlineSymbols bool `xml:"showOutlineSymbols,attr,omitempty"`
SummaryBelow bool `xml:"summaryBelow,attr"`
SummaryRight bool `xml:"summaryRight,attr"`
ShowOutlineSymbols bool `xml:"showOutlineSymbols,attr"`
}

// xlsxPageSetUpPr expresses page setup properties of the worksheet.
Expand Down Expand Up @@ -528,26 +528,27 @@ type xlsxPhoneticPr struct {
// applied to a particular cell or range.
type xlsxConditionalFormatting struct {
XMLName xml.Name `xml:"conditionalFormatting"`
Pivot bool `xml:"pivot,attr,omitempty"`
SQRef string `xml:"sqref,attr,omitempty"`
CfRule []*xlsxCfRule `xml:"cfRule"`
}

// xlsxCfRule (Conditional Formatting Rule) represents a description of a
// conditional formatting rule.
type xlsxCfRule struct {
AboveAverage *bool `xml:"aboveAverage,attr"`
Bottom bool `xml:"bottom,attr,omitempty"`
Type string `xml:"type,attr,omitempty"`
DxfID *int `xml:"dxfId,attr"`
EqualAverage bool `xml:"equalAverage,attr,omitempty"`
Operator string `xml:"operator,attr,omitempty"`
Percent bool `xml:"percent,attr,omitempty"`
Priority int `xml:"priority,attr,omitempty"`
Rank int `xml:"rank,attr,omitempty"`
StdDev int `xml:"stdDev,attr,omitempty"`
StopIfTrue bool `xml:"stopIfTrue,attr,omitempty"`
AboveAverage *bool `xml:"aboveAverage,attr"`
Percent bool `xml:"percent,attr,omitempty"`
Bottom bool `xml:"bottom,attr,omitempty"`
Operator string `xml:"operator,attr,omitempty"`
Text string `xml:"text,attr,omitempty"`
TimePeriod string `xml:"timePeriod,attr,omitempty"`
Type string `xml:"type,attr,omitempty"`
Rank int `xml:"rank,attr,omitempty"`
StdDev int `xml:"stdDev,attr,omitempty"`
EqualAverage bool `xml:"equalAverage,attr,omitempty"`
Formula []string `xml:"formula,omitempty"`
ColorScale *xlsxColorScale `xml:"colorScale"`
DataBar *xlsxDataBar `xml:"dataBar"`
Expand Down