1,056 changes: 624 additions & 432 deletions excelize_test.go

Large diffs are not rendered by default.

70 changes: 37 additions & 33 deletions file.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand All @@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.15 or later.
// data. This library needs Go version 1.16 or later.

package excelize

Expand All @@ -30,69 +30,58 @@ func NewFile() *File {
f.Pkg.Store("_rels/.rels", []byte(xml.Header+templateRels))
f.Pkg.Store(defaultXMLPathDocPropsApp, []byte(xml.Header+templateDocpropsApp))
f.Pkg.Store(defaultXMLPathDocPropsCore, []byte(xml.Header+templateDocpropsCore))
f.Pkg.Store("xl/_rels/workbook.xml.rels", []byte(xml.Header+templateWorkbookRels))
f.Pkg.Store(defaultXMLPathWorkbookRels, []byte(xml.Header+templateWorkbookRels))
f.Pkg.Store("xl/theme/theme1.xml", []byte(xml.Header+templateTheme))
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(xml.Header+templateSheet))
f.Pkg.Store(defaultXMLPathStyles, []byte(xml.Header+templateStyles))
f.Pkg.Store(defaultXMLPathWorkbook, []byte(xml.Header+templateWorkbook))
f.Pkg.Store(defaultXMLPathContentTypes, []byte(xml.Header+templateContentTypes))
f.SheetCount = 1
f.CalcChain = f.calcChainReader()
f.CalcChain, _ = f.calcChainReader()
f.Comments = make(map[string]*xlsxComments)
f.ContentTypes = f.contentTypesReader()
f.ContentTypes, _ = f.contentTypesReader()
f.Drawings = sync.Map{}
f.Styles = f.stylesReader()
f.Styles, _ = f.stylesReader()
f.DecodeVMLDrawing = make(map[string]*decodeVmlDrawing)
f.VMLDrawing = make(map[string]*vmlDrawing)
f.WorkBook = f.workbookReader()
f.WorkBook, _ = f.workbookReader()
f.Relationships = sync.Map{}
f.Relationships.Store("xl/_rels/workbook.xml.rels", f.relsReader("xl/_rels/workbook.xml.rels"))
rels, _ := f.relsReader(defaultXMLPathWorkbookRels)
f.Relationships.Store(defaultXMLPathWorkbookRels, rels)
f.sheetMap["Sheet1"] = "xl/worksheets/sheet1.xml"
ws, _ := f.workSheetReader("Sheet1")
f.Sheet.Store("xl/worksheets/sheet1.xml", ws)
f.Theme = f.themeReader()
f.Theme, _ = f.themeReader()
return f
}

// Save provides a function to override the spreadsheet with origin path.
func (f *File) Save() error {
func (f *File) Save(opts ...Options) error {
if f.Path == "" {
return ErrSave
}
if f.options != nil {
return f.SaveAs(f.Path, *f.options)
for i := range opts {
f.options = &opts[i]
}
return f.SaveAs(f.Path)
return f.SaveAs(f.Path, *f.options)
}

// SaveAs provides a function to create or update to a spreadsheet at the
// provided path.
func (f *File) SaveAs(name string, opt ...Options) error {
func (f *File) SaveAs(name string, opts ...Options) error {
if len(name) > MaxFilePathLength {
return ErrMaxFilePathLength
}
f.Path = name
contentType, ok := map[string]string{
".xlam": ContentTypeAddinMacro,
".xlsm": ContentTypeMacro,
".xlsx": ContentTypeSheetML,
".xltm": ContentTypeTemplateMacro,
".xltx": ContentTypeTemplate,
}[filepath.Ext(f.Path)]
if !ok {
if _, ok := supportedContentTypes[filepath.Ext(f.Path)]; !ok {
return ErrWorkbookFileFormat
}
f.setContentTypePartProjectExtensions(contentType)
file, err := os.OpenFile(filepath.Clean(name), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm)
if err != nil {
return err
}
defer file.Close()
f.options = nil
for i := range opt {
f.options = &opt[i]
}
return f.Write(file)
return f.Write(file, opts...)
}

// Close closes and cleanup the open temporary file for the spreadsheet.
Expand All @@ -109,17 +98,32 @@ func (f *File) Close() error {
}
return true
})
for _, stream := range f.streams {
_ = stream.rawData.Close()
}
return err
}

// Write provides a function to write to an io.Writer.
func (f *File) Write(w io.Writer) error {
_, err := f.WriteTo(w)
func (f *File) Write(w io.Writer, opts ...Options) error {
_, err := f.WriteTo(w, opts...)
return err
}

// WriteTo implements io.WriterTo to write the file.
func (f *File) WriteTo(w io.Writer) (int64, error) {
func (f *File) WriteTo(w io.Writer, opts ...Options) (int64, error) {
for i := range opts {
f.options = &opts[i]
}
if len(f.Path) != 0 {
contentType, ok := supportedContentTypes[filepath.Ext(f.Path)]
if !ok {
return 0, ErrWorkbookFileFormat
}
if err := f.setContentTypePartProjectExtensions(contentType); err != nil {
return 0, err
}
}
if f.options != nil && f.options.Password != "" {
buf, err := f.WriteToBuffer()
if err != nil {
Expand Down Expand Up @@ -178,9 +182,10 @@ func (f *File) writeToZip(zw *zip.Writer) error {
f.workBookWriter()
f.workSheetWriter()
f.relsWriter()
f.sharedStringsLoader()
_ = f.sharedStringsLoader()
f.sharedStringsWriter()
f.styleSheetWriter()
f.themeWriter()

for path, stream := range f.streams {
fi, err := zw.Create(path)
Expand All @@ -197,7 +202,6 @@ func (f *File) writeToZip(zw *zip.Writer) error {
if err != nil {
return err
}
_ = stream.rawData.Close()
}
var err error
f.Pkg.Range(func(path, content interface{}) bool {
Expand Down
17 changes: 17 additions & 0 deletions file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"os"
"path/filepath"
"strings"
"sync"
"testing"
Expand Down Expand Up @@ -71,6 +72,22 @@ func TestWriteTo(t *testing.T) {
_, err := f.WriteTo(bufio.NewWriter(&buf))
assert.EqualError(t, err, "zip: FileHeader.Name too long")
}
// Test write with unsupported workbook file format
{
f, buf := File{Pkg: sync.Map{}}, bytes.Buffer{}
f.Pkg.Store("/d", []byte("s"))
f.Path = "Book1.xls"
_, err := f.WriteTo(bufio.NewWriter(&buf))
assert.EqualError(t, err, ErrWorkbookFileFormat.Error())
}
// Test write with unsupported charset content types.
{
f, buf := NewFile(), bytes.Buffer{}
f.ContentTypes, f.Path = nil, filepath.Join("test", "TestWriteTo.xlsx")
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
_, err := f.WriteTo(bufio.NewWriter(&buf))
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
}

func TestClose(t *testing.T) {
Expand Down
17 changes: 8 additions & 9 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
module github.com/xuri/excelize/v2

go 1.15
go 1.16

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/richardlehane/mscfb v1.0.4
github.com/richardlehane/msoleps v1.0.3 // indirect
github.com/stretchr/testify v1.7.1
github.com/stretchr/testify v1.8.0
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9
golang.org/x/net v0.0.0-20220812174116-3211cb980234
golang.org/x/text v0.3.7
gopkg.in/yaml.v3 v3.0.0 // indirect
golang.org/x/crypto v0.5.0
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69
golang.org/x/net v0.5.0
golang.org/x/text v0.6.0
)

require github.com/richardlehane/msoleps v1.0.3 // indirect
46 changes: 32 additions & 14 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,49 @@ github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTK
github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c=
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M=
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 h1:GIAS/yBem/gq2MUqgNIzUHW7cJMmx3TGZOrnyYaNQ6c=
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE=
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E=
golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
138 changes: 49 additions & 89 deletions lib.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand All @@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.15 or later.
// data. This library needs Go version 1.16 or later.

package excelize

Expand All @@ -18,7 +18,7 @@ import (
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"math/big"
"os"
"regexp"
"strconv"
Expand Down Expand Up @@ -72,7 +72,7 @@ func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {
// unzipToTemp unzip the zip entity to the system temporary directory and
// returned the unzipped file path.
func (f *File) unzipToTemp(zipFile *zip.File) (string, error) {
tmp, err := ioutil.TempFile(os.TempDir(), "excelize-")
tmp, err := os.CreateTemp(os.TempDir(), "excelize-")
if err != nil {
return "", err
}
Expand Down Expand Up @@ -110,7 +110,7 @@ func (f *File) readBytes(name string) []byte {
if err != nil {
return content
}
content, _ = ioutil.ReadAll(file)
content, _ = io.ReadAll(file)
f.Pkg.Store(name, content)
_ = file.Close()
return content
Expand Down Expand Up @@ -261,7 +261,7 @@ func CellNameToCoordinates(cell string) (int, int, error) {
// excelize.CoordinatesToCellName(1, 1, true) // returns "$A$1", nil
func CoordinatesToCellName(col, row int, abs ...bool) (string, error) {
if col < 1 || row < 1 {
return "", fmt.Errorf("invalid cell coordinates [%d, %d]", col, row)
return "", fmt.Errorf("invalid cell reference [%d, %d]", col, row)
}
sign := ""
for _, a := range abs {
Expand All @@ -273,19 +273,19 @@ func CoordinatesToCellName(col, row int, abs ...bool) (string, error) {
return sign + colName + sign + strconv.Itoa(row), err
}

// areaRefToCoordinates provides a function to convert area reference to a
// rangeRefToCoordinates provides a function to convert range reference to a
// pair of coordinates.
func areaRefToCoordinates(ref string) ([]int, error) {
func rangeRefToCoordinates(ref string) ([]int, error) {
rng := strings.Split(strings.ReplaceAll(ref, "$", ""), ":")
if len(rng) < 2 {
return nil, ErrParameterInvalid
}
return areaRangeToCoordinates(rng[0], rng[1])
return cellRefsToCoordinates(rng[0], rng[1])
}

// areaRangeToCoordinates provides a function to convert cell range to a
// cellRefsToCoordinates provides a function to convert cell range to a
// pair of coordinates.
func areaRangeToCoordinates(firstCell, lastCell string) ([]int, error) {
func cellRefsToCoordinates(firstCell, lastCell string) ([]int, error) {
coordinates := make([]int, 4)
var err error
coordinates[0], coordinates[1], err = CellNameToCoordinates(firstCell)
Expand All @@ -296,7 +296,7 @@ func areaRangeToCoordinates(firstCell, lastCell string) ([]int, error) {
return coordinates, err
}

// sortCoordinates provides a function to correct the coordinate area, such
// sortCoordinates provides a function to correct the cell range, such
// correct C1:B3 to B1:C3.
func sortCoordinates(coordinates []int) error {
if len(coordinates) != 4 {
Expand All @@ -311,17 +311,17 @@ func sortCoordinates(coordinates []int) error {
return nil
}

// coordinatesToAreaRef provides a function to convert a pair of coordinates
// to area reference.
func (f *File) coordinatesToAreaRef(coordinates []int) (string, error) {
// coordinatesToRangeRef provides a function to convert a pair of coordinates
// to range reference.
func (f *File) coordinatesToRangeRef(coordinates []int, abs ...bool) (string, error) {
if len(coordinates) != 4 {
return "", ErrCoordinates
}
firstCell, err := CoordinatesToCellName(coordinates[0], coordinates[1])
firstCell, err := CoordinatesToCellName(coordinates[0], coordinates[1], abs...)
if err != nil {
return "", err
}
lastCell, err := CoordinatesToCellName(coordinates[2], coordinates[3])
lastCell, err := CoordinatesToCellName(coordinates[2], coordinates[3], abs...)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -349,7 +349,7 @@ func (f *File) getDefinedNameRefTo(definedNameName string, currentSheet string)
return
}

// flatSqref convert reference sequence to cell coordinates list.
// flatSqref convert reference sequence to cell reference list.
func (f *File) flatSqref(sqref string) (cells map[int][][]int, err error) {
var coordinates []int
cells = make(map[int][][]int)
Expand All @@ -364,7 +364,7 @@ func (f *File) flatSqref(sqref string) (cells map[int][][]int, err error) {
}
cells[col] = append(cells[col], []int{col, row})
case 2:
if coordinates, err = areaRefToCoordinates(ref); err != nil {
if coordinates, err = rangeRefToCoordinates(ref); err != nil {
return
}
_ = sortCoordinates(coordinates)
Expand Down Expand Up @@ -421,20 +421,15 @@ func boolPtr(b bool) *bool { return &b }
// intPtr returns a pointer to an int with the given value.
func intPtr(i int) *int { return &i }

// uintPtr returns a pointer to an int with the given value.
func uintPtr(i uint) *uint { return &i }

// float64Ptr returns a pointer to a float64 with the given value.
func float64Ptr(f float64) *float64 { return &f }

// stringPtr returns a pointer to a string with the given value.
func stringPtr(s string) *string { return &s }

// defaultTrue returns true if b is nil, or the pointed value.
func defaultTrue(b *bool) bool {
if b == nil {
return true
}
return *b
}

// MarshalXML convert the boolean data type to literal values 0 or 1 on
// serialization.
func (avb attrValBool) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
Expand Down Expand Up @@ -498,15 +493,6 @@ func (avb *attrValBool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) err
return nil
}

// parseFormatSet provides a method to convert format string to []byte and
// handle empty string.
func parseFormatSet(formatSet string) []byte {
if formatSet != "" {
return []byte(formatSet)
}
return []byte("{}")
}

// namespaceStrictToTransitional provides a method to convert Strict and
// Transitional namespaces.
func namespaceStrictToTransitional(content []byte) []byte {
Expand All @@ -524,14 +510,14 @@ func namespaceStrictToTransitional(content []byte) []byte {
return content
}

// bytesReplace replace old bytes with given new.
func bytesReplace(s, old, new []byte, n int) []byte {
// bytesReplace replace source bytes with given target.
func bytesReplace(s, source, target []byte, n int) []byte {
if n == 0 {
return s
}

if len(old) < len(new) {
return bytes.Replace(s, old, new, n)
if len(source) < len(target) {
return bytes.Replace(s, source, target, n)
}

if n < 0 {
Expand All @@ -540,14 +526,14 @@ func bytesReplace(s, old, new []byte, n int) []byte {

var wid, i, j, w int
for i, j = 0, 0; i < len(s) && j < n; j++ {
wid = bytes.Index(s[i:], old)
wid = bytes.Index(s[i:], source)
if wid < 0 {
break
}

w += copy(s[w:], s[i:i+wid])
w += copy(s[w:], new)
i += wid + len(old)
w += copy(s[w:], target)
i += wid + len(source)
}

w += copy(s[w:], s[i:])
Expand Down Expand Up @@ -631,12 +617,12 @@ func getXMLNamespace(space string, attr []xml.Attr) string {
// replaceNameSpaceBytes provides a function to replace the XML root element
// attribute by the given component part path and XML content.
func (f *File) replaceNameSpaceBytes(path string, contentMarshal []byte) []byte {
oldXmlns := []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
newXmlns := []byte(templateNamespaceIDMap)
sourceXmlns := []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
targetXmlns := []byte(templateNamespaceIDMap)
if attr, ok := f.xmlAttr[path]; ok {
newXmlns = []byte(genXMLNamespace(attr))
targetXmlns = []byte(genXMLNamespace(attr))
}
return bytesReplace(contentMarshal, oldXmlns, bytes.ReplaceAll(newXmlns, []byte(" mc:Ignorable=\"r\""), []byte{}), -1)
return bytesReplace(contentMarshal, sourceXmlns, bytes.ReplaceAll(targetXmlns, []byte(" mc:Ignorable=\"r\""), []byte{}), -1)
}

// addNameSpaces provides a function to add an XML attribute by the given
Expand Down Expand Up @@ -691,39 +677,24 @@ func (f *File) addSheetNameSpace(sheet string, ns xml.Attr) {

// isNumeric determines whether an expression is a valid numeric type and get
// the precision for the numeric.
func isNumeric(s string) (bool, int) {
dot, e, n, p := false, false, false, 0
for i, v := range s {
if v == '.' {
if dot {
return false, 0
}
dot = true
} else if v == 'E' || v == 'e' {
e = true
} else if v < '0' || v > '9' {
if i == 0 && v == '-' {
continue
}
if e && v == '-' {
return true, 0
}
if e && v == '+' {
p = 15
continue
}
return false, 0
} else {
p++
}
n = true
func isNumeric(s string) (bool, int, float64) {
if strings.Contains(s, "_") {
return false, 0, 0
}
var decimal big.Float
_, ok := decimal.SetString(s)
if !ok {
return false, 0, 0
}
return n, p
var noScientificNotation string
flt, _ := decimal.Float64()
noScientificNotation = strconv.FormatFloat(flt, 'f', -1, 64)
return true, len(strings.ReplaceAll(noScientificNotation, ".", "")), flt
}

var (
bstrExp = regexp.MustCompile(`_x[a-zA-Z\d]{4}_`)
bstrEscapeExp = regexp.MustCompile(`x[a-zA-Z\d]{4}_`)
bstrExp = regexp.MustCompile(`_x[a-fA-F\d]{4}_`)
bstrEscapeExp = regexp.MustCompile(`x[a-fA-F\d]{4}_`)
)

// bstrUnmarshal parses the binary basic string, this will trim escaped string
Expand All @@ -749,16 +720,7 @@ func bstrUnmarshal(s string) (result string) {
}
if bstrExp.MatchString(subStr) {
cursor = match[1]
v, err := strconv.Unquote(`"\u` + s[match[0]+2:match[1]-1] + `"`)
if err != nil {
if l > match[1]+6 && bstrEscapeExp.MatchString(s[match[1]:match[1]+6]) {
result += subStr[:6]
cursor = match[1] + 6
continue
}
result += subStr
continue
}
v, _ := strconv.Unquote(`"\u` + s[match[0]+2:match[1]-1] + `"`)
result += v
}
}
Expand Down Expand Up @@ -789,12 +751,10 @@ func bstrMarshal(s string) (result string) {
}
if bstrExp.MatchString(subStr) {
cursor = match[1]
_, err := strconv.Unquote(`"\u` + s[match[0]+2:match[1]-1] + `"`)
if err == nil {
if _, err := strconv.Unquote(`"\u` + s[match[0]+2:match[1]-1] + `"`); err == nil {
result += "_x005F" + subStr
continue
}
result += subStr
}
}
if cursor < l {
Expand Down
21 changes: 11 additions & 10 deletions lib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,15 +217,15 @@ func TestCoordinatesToCellName_Error(t *testing.T) {
}
}

func TestCoordinatesToAreaRef(t *testing.T) {
func TestCoordinatesToRangeRef(t *testing.T) {
f := NewFile()
_, err := f.coordinatesToAreaRef([]int{})
_, err := f.coordinatesToRangeRef([]int{})
assert.EqualError(t, err, ErrCoordinates.Error())
_, err = f.coordinatesToAreaRef([]int{1, -1, 1, 1})
assert.EqualError(t, err, "invalid cell coordinates [1, -1]")
_, err = f.coordinatesToAreaRef([]int{1, 1, 1, -1})
assert.EqualError(t, err, "invalid cell coordinates [1, -1]")
ref, err := f.coordinatesToAreaRef([]int{1, 1, 1, 1})
_, err = f.coordinatesToRangeRef([]int{1, -1, 1, 1})
assert.EqualError(t, err, "invalid cell reference [1, -1]")
_, err = f.coordinatesToRangeRef([]int{1, 1, 1, -1})
assert.EqualError(t, err, "invalid cell reference [1, -1]")
ref, err := f.coordinatesToRangeRef([]int{1, 1, 1, 1})
assert.NoError(t, err)
assert.EqualValues(t, ref, "A1:A1")
}
Expand Down Expand Up @@ -305,18 +305,19 @@ func TestBstrUnmarshal(t *testing.T) {
"*_x0008_*": "*\b*",
"*_x4F60__x597D_": "*你好",
"*_xG000_": "*_xG000_",
"*_xG05F_x0001_*": "*_xG05F*",
"*_xG05F_x0001_*": "*_xG05F\x01*",
"*_x005F__x0008_*": "*_\b*",
"*_x005F_x0001_*": "*_x0001_*",
"*_x005f_x005F__x0008_*": "*_x005F_\b*",
"*_x005F_x005F_xG05F_x0006_*": "*_x005F_xG05F*",
"*_x005F_x005F_xG05F_x0006_*": "*_x005F_xG05F\x06*",
"*_x005F_x005F_x005F_x0006_*": "*_x005F_x0006_*",
"_x005F__x0008_******": "_\b******",
"******_x005F__x0008_": "******_\b",
"******_x005F__x0008_******": "******_\b******",
"_x000x_x005F_x000x_": "_x000x_x000x_",
}
for bstr, expected := range bstrs {
assert.Equal(t, expected, bstrUnmarshal(bstr))
assert.Equal(t, expected, bstrUnmarshal(bstr), bstr)
}
}

Expand Down
54 changes: 32 additions & 22 deletions merge.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand All @@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.15 or later.
// data. This library needs Go version 1.16 or later.

package excelize

Expand All @@ -17,20 +17,24 @@ import "strings"
func (mc *xlsxMergeCell) Rect() ([]int, error) {
var err error
if mc.rect == nil {
mc.rect, err = areaRefToCoordinates(mc.Ref)
mergedCellsRef := mc.Ref
if !strings.Contains(mergedCellsRef, ":") {
mergedCellsRef += ":" + mergedCellsRef
}
mc.rect, err = rangeRefToCoordinates(mergedCellsRef)
}
return mc.rect, err
}

// MergeCell provides a function to merge cells by given coordinate area and
// MergeCell provides a function to merge cells by given range reference and
// sheet name. Merging cells only keeps the upper-left cell value, and
// discards the other values. For example create a merged cell of D3:E9 on
// Sheet1:
//
// err := f.MergeCell("Sheet1", "D3", "E9")
//
// If you create a merged cell that overlaps with another existing merged cell,
// those merged cells that already exist will be removed. The cell coordinates
// those merged cells that already exist will be removed. The cell references
// tuple after merging in the following range will be: A1(x3,y1) D1(x2,y1)
// A8(x3,y4) D8(x2,y4)
//
Expand All @@ -46,11 +50,11 @@ func (mc *xlsxMergeCell) Rect() ([]int, error) {
// |A8(x3,y4) C8(x4,y4)|
// +------------------------+
func (f *File) MergeCell(sheet, hCell, vCell string) error {
rect, err := areaRefToCoordinates(hCell + ":" + vCell)
rect, err := rangeRefToCoordinates(hCell + ":" + vCell)
if err != nil {
return err
}
// Correct the coordinate area, such correct C1:B3 to B1:C3.
// Correct the range reference, such correct C1:B3 to B1:C3.
_ = sortCoordinates(rect)

hCell, _ = CoordinatesToCellName(rect[0], rect[1])
Expand All @@ -60,6 +64,8 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error {
if err != nil {
return err
}
ws.Lock()
defer ws.Unlock()
ref := hCell + ":" + vCell
if ws.MergeCells != nil {
ws.MergeCells.Cells = append(ws.MergeCells.Cells, &xlsxMergeCell{Ref: ref, rect: rect})
Expand All @@ -70,23 +76,25 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error {
return err
}

// UnmergeCell provides a function to unmerge a given coordinate area.
// For example unmerge area D3:E9 on Sheet1:
// UnmergeCell provides a function to unmerge a given range reference.
// For example unmerge range reference D3:E9 on Sheet1:
//
// err := f.UnmergeCell("Sheet1", "D3", "E9")
//
// Attention: overlapped areas will also be unmerged.
func (f *File) UnmergeCell(sheet string, hCell, vCell string) error {
// Attention: overlapped range will also be unmerged.
func (f *File) UnmergeCell(sheet, hCell, vCell string) error {
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
rect1, err := areaRefToCoordinates(hCell + ":" + vCell)
ws.Lock()
defer ws.Unlock()
rect1, err := rangeRefToCoordinates(hCell + ":" + vCell)
if err != nil {
return err
}

// Correct the coordinate area, such correct C1:B3 to B1:C3.
// Correct the range reference, such correct C1:B3 to B1:C3.
_ = sortCoordinates(rect1)

// return nil since no MergeCells in the sheet
Expand All @@ -101,7 +109,11 @@ func (f *File) UnmergeCell(sheet string, hCell, vCell string) error {
if mergeCell == nil {
continue
}
rect2, _ := areaRefToCoordinates(mergeCell.Ref)
mergedCellsRef := mergeCell.Ref
if !strings.Contains(mergedCellsRef, ":") {
mergedCellsRef += ":" + mergedCellsRef
}
rect2, _ := rangeRefToCoordinates(mergedCellsRef)
if isOverlap(rect1, rect2) {
continue
}
Expand Down Expand Up @@ -131,8 +143,8 @@ func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) {
mergeCells = make([]MergeCell, 0, len(ws.MergeCells.Cells))
for i := range ws.MergeCells.Cells {
ref := ws.MergeCells.Cells[i].Ref
axis := strings.Split(ref, ":")[0]
val, _ := f.GetCellValue(sheet, axis)
cell := strings.Split(ref, ":")[0]
val, _ := f.GetCellValue(sheet, cell)
mergeCells = append(mergeCells, []string{ref, val})
}
}
Expand Down Expand Up @@ -268,16 +280,14 @@ func (m *MergeCell) GetCellValue() string {
return (*m)[1]
}

// GetStartAxis returns the top left cell coordinates of merged range, for
// GetStartAxis returns the top left cell reference of merged range, for
// example: "C2".
func (m *MergeCell) GetStartAxis() string {
axis := strings.Split((*m)[0], ":")
return axis[0]
return strings.Split((*m)[0], ":")[0]
}

// GetEndAxis returns the bottom right cell coordinates of merged range, for
// GetEndAxis returns the bottom right cell reference of merged range, for
// example: "D4".
func (m *MergeCell) GetEndAxis() string {
axis := strings.Split((*m)[0], ":")
return axis[1]
return strings.Split((*m)[0], ":")[1]
}
42 changes: 28 additions & 14 deletions merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@ func TestMergeCell(t *testing.T) {
value, err := f.GetCellValue("Sheet1", "H11")
assert.Equal(t, "100", value)
assert.NoError(t, err)
value, err = f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate.
// Merged cell ref is single coordinate
value, err = f.GetCellValue("Sheet2", "A6")
assert.Equal(t, "", value)
assert.NoError(t, err)
value, err = f.GetCellFormula("Sheet1", "G12")
assert.Equal(t, "SUM(Sheet1!B19,Sheet1!C19)", value)
assert.NoError(t, err)

f.NewSheet("Sheet3")
_, err = f.NewSheet("Sheet3")
assert.NoError(t, err)
assert.NoError(t, f.MergeCell("Sheet3", "D11", "F13"))
assert.NoError(t, f.MergeCell("Sheet3", "G10", "K12"))

Expand Down Expand Up @@ -64,9 +66,10 @@ func TestMergeCell(t *testing.T) {
assert.NoError(t, f.MergeCell("Sheet3", "M8", "Q13"))
assert.NoError(t, f.MergeCell("Sheet3", "N10", "O11"))

// Test get merged cells on not exists worksheet.
assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN is not exist")

// Test merge cells on not exists worksheet
assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN does not exist")
// Test merged cells with invalid sheet name
assert.EqualError(t, f.MergeCell("Sheet:1", "N10", "O11"), ErrSheetNameInvalid.Error())
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx")))
assert.NoError(t, f.Close())

Expand Down Expand Up @@ -137,10 +140,12 @@ func TestGetMergeCells(t *testing.T) {
assert.Equal(t, wants[i].start, m.GetStartAxis())
assert.Equal(t, wants[i].end, m.GetEndAxis())
}

// Test get merged cells on not exists worksheet.
// Test get merged cells with invalid sheet name
_, err = f.GetMergeCells("Sheet:1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
// Test get merged cells on not exists worksheet
_, err = f.GetMergeCells("SheetN")
assert.EqualError(t, err, "sheet SheetN is not exist")
assert.EqualError(t, err, "sheet SheetN does not exist")
assert.NoError(t, f.Close())
}

Expand All @@ -158,7 +163,7 @@ func TestUnmergeCell(t *testing.T) {

assert.EqualError(t, f.UnmergeCell("Sheet1", "A", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())

// unmerge the mergecell that contains A1
// Test unmerge the merged cells that contains A1
assert.NoError(t, f.UnmergeCell(sheet1, "A1", "A1"))
if len(sheet.MergeCells.Cells) != mergeCellNum-1 {
t.FailNow()
Expand All @@ -169,8 +174,11 @@ func TestUnmergeCell(t *testing.T) {

f = NewFile()
assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3"))
// Test unmerged area on not exists worksheet.
assert.EqualError(t, f.UnmergeCell("SheetN", "A1", "A1"), "sheet SheetN is not exist")
// Test unmerged range reference on not exists worksheet
assert.EqualError(t, f.UnmergeCell("SheetN", "A1", "A1"), "sheet SheetN does not exist")

// Test unmerge the merged cells with invalid sheet name
assert.EqualError(t, f.UnmergeCell("Sheet:1", "A1", "A1"), ErrSheetNameInvalid.Error())

ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
Expand All @@ -185,7 +193,7 @@ func TestUnmergeCell(t *testing.T) {
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"), ErrParameterInvalid.Error())
assert.NoError(t, f.UnmergeCell("Sheet1", "A2", "B3"))

ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
Expand All @@ -194,6 +202,12 @@ func TestUnmergeCell(t *testing.T) {
}

func TestFlatMergedCells(t *testing.T) {
ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}}
assert.EqualError(t, flatMergedCells(ws, [][]*xlsxMergeCell{}), ErrParameterInvalid.Error())
ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ""}}}}
assert.EqualError(t, flatMergedCells(ws, [][]*xlsxMergeCell{}), "cannot convert cell \"\" to coordinates: invalid cell name \"\"")
}

func TestMergeCellsParser(t *testing.T) {
f := NewFile()
_, err := f.mergeCellsParser(&xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{nil}}}, "A1")
assert.NoError(t, err)
}
22 changes: 12 additions & 10 deletions numfmt.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand All @@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.15 or later.
// data. This library needs Go version 1.16 or later.

package excelize

Expand Down Expand Up @@ -279,7 +279,7 @@ var (
// prepareNumberic split the number into two before and after parts by a
// decimal point.
func (nf *numberFormat) prepareNumberic(value string) {
if nf.isNumeric, _ = isNumeric(value); !nf.isNumeric {
if nf.isNumeric, _, _ = isNumeric(value); !nf.isNumeric {
return
}
}
Expand Down Expand Up @@ -338,13 +338,13 @@ func (nf *numberFormat) positiveHandler() (result string) {
continue
}
if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == strings.Repeat("0", len(token.TValue)) {
if isNum, precision := isNumeric(nf.value); isNum {
if isNum, precision, decimal := isNumeric(nf.value); isNum {
if nf.number < 1 {
nf.result += "0"
continue
}
if precision > 15 {
nf.result += roundPrecision(nf.value, 15)
nf.result += strconv.FormatFloat(decimal, 'f', -1, 64)
} else {
nf.result += fmt.Sprintf("%.f", nf.number)
}
Expand Down Expand Up @@ -696,7 +696,7 @@ func (nf *numberFormat) dateTimesHandler(i int, token nfp.Token) {
nextHours := nf.hoursNext(i)
aps := strings.Split(nf.localAmPm(token.TValue), "/")
nf.ap = aps[0]
if nextHours > 12 {
if nextHours >= 12 {
nf.ap = aps[1]
}
}
Expand Down Expand Up @@ -777,9 +777,11 @@ func (nf *numberFormat) hoursHandler(i int, token nfp.Token) {
ap, ok := nf.apNext(i)
if ok {
nf.ap = ap[0]
if h >= 12 {
nf.ap = ap[1]
}
if h > 12 {
h -= 12
nf.ap = ap[1]
}
}
if nf.ap != "" && nf.hoursNext(i) == -1 && h > 12 {
Expand Down Expand Up @@ -900,13 +902,13 @@ func (nf *numberFormat) negativeHandler() (result string) {
continue
}
if token.TType == nfp.TokenTypeZeroPlaceHolder && token.TValue == strings.Repeat("0", len(token.TValue)) {
if isNum, precision := isNumeric(nf.value); isNum {
if isNum, precision, decimal := isNumeric(nf.value); isNum {
if math.Abs(nf.number) < 1 {
nf.result += "0"
continue
}
if precision > 15 {
nf.result += strings.TrimLeft(roundPrecision(nf.value, 15), "-")
nf.result += strings.TrimLeft(strconv.FormatFloat(decimal, 'f', -1, 64), "-")
} else {
nf.result += fmt.Sprintf("%.f", math.Abs(nf.number))
}
Expand Down Expand Up @@ -939,7 +941,7 @@ func (nf *numberFormat) textHandler() (result string) {
// getValueSectionType returns its applicable number format expression section
// based on the given value.
func (nf *numberFormat) getValueSectionType(value string) (float64, string) {
isNum, _ := isNumeric(value)
isNum, _, _ := isNumeric(value)
if !isNum {
return 0, nfp.TokenSectionText
}
Expand Down
306 changes: 153 additions & 153 deletions numfmt_test.go

Large diffs are not rendered by default.

284 changes: 185 additions & 99 deletions picture.go

Large diffs are not rendered by default.

195 changes: 123 additions & 72 deletions picture_test.go

Large diffs are not rendered by default.

180 changes: 93 additions & 87 deletions pivotTable.go

Large diffs are not rendered by default.

86 changes: 51 additions & 35 deletions pivotTable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestAddPivotTable(t *testing.T) {
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(5000)))
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)]))
}
assert.NoError(t, f.AddPivotTable(&PivotTableOption{
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$G$2:$M$34",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Expand All @@ -41,7 +41,7 @@ func TestAddPivotTable(t *testing.T) {
ShowError: true,
}))
// Use different order of coordinate tests
assert.NoError(t, f.AddPivotTable(&PivotTableOption{
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Expand All @@ -55,7 +55,7 @@ func TestAddPivotTable(t *testing.T) {
ShowLastColumn: true,
}))

assert.NoError(t, f.AddPivotTable(&PivotTableOption{
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$W$2:$AC$34",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Expand All @@ -68,7 +68,7 @@ func TestAddPivotTable(t *testing.T) {
ShowColHeaders: true,
ShowLastColumn: true,
}))
assert.NoError(t, f.AddPivotTable(&PivotTableOption{
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$G$37:$W$50",
Rows: []PivotTableField{{Data: "Month"}},
Expand All @@ -81,7 +81,7 @@ func TestAddPivotTable(t *testing.T) {
ShowColHeaders: true,
ShowLastColumn: true,
}))
assert.NoError(t, f.AddPivotTable(&PivotTableOption{
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$AE$2:$AG$33",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Expand All @@ -94,7 +94,7 @@ func TestAddPivotTable(t *testing.T) {
ShowLastColumn: true,
}))
// Create pivot table with empty subtotal field name and specified style
assert.NoError(t, f.AddPivotTable(&PivotTableOption{
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$AJ$2:$AP1$35",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Expand All @@ -109,8 +109,9 @@ func TestAddPivotTable(t *testing.T) {
ShowLastColumn: true,
PivotTableStyleName: "PivotStyleLight19",
}))
f.NewSheet("Sheet2")
assert.NoError(t, f.AddPivotTable(&PivotTableOption{
_, err := f.NewSheet("Sheet2")
assert.NoError(t, err)
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet2!$A$1:$AR$15",
Rows: []PivotTableField{{Data: "Month"}},
Expand All @@ -123,7 +124,7 @@ func TestAddPivotTable(t *testing.T) {
ShowColHeaders: true,
ShowLastColumn: true,
}))
assert.NoError(t, f.AddPivotTable(&PivotTableOption{
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet2!$A$18:$AR$54",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Type"}},
Expand All @@ -143,7 +144,7 @@ func TestAddPivotTable(t *testing.T) {
Comment: "Pivot Table Data Range",
Scope: "Sheet2",
}))
assert.NoError(t, f.AddPivotTable(&PivotTableOption{
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "dataRange",
PivotTableRange: "Sheet2!$A$57:$AJ$91",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Expand All @@ -160,55 +161,55 @@ func TestAddPivotTable(t *testing.T) {
// Test empty pivot table options
assert.EqualError(t, f.AddPivotTable(nil), ErrParameterRequired.Error())
// Test invalid data range
assert.EqualError(t, f.AddPivotTable(&PivotTableOption{
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1",
PivotTableRange: "Sheet1!$U$34:$O$2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), `parameter 'DataRange' parsing error: parameter is invalid`)
// Test the data range of the worksheet that is not declared
assert.EqualError(t, f.AddPivotTable(&PivotTableOption{
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), `parameter 'DataRange' parsing error: parameter is invalid`)
// Test the worksheet declared in the data range does not exist
assert.EqualError(t, f.AddPivotTable(&PivotTableOption{
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "SheetN!$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), "sheet SheetN is not exist")
}), "sheet SheetN does not exist")
// Test the pivot table range of the worksheet that is not declared
assert.EqualError(t, f.AddPivotTable(&PivotTableOption{
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "$U$34:$O$2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), `parameter 'PivotTableRange' parsing error: parameter is invalid`)
// Test the worksheet declared in the pivot table range does not exist
assert.EqualError(t, f.AddPivotTable(&PivotTableOption{
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "SheetN!$U$34:$O$2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), "sheet SheetN is not exist")
}), "sheet SheetN does not exist")
// Test not exists worksheet in data range
assert.EqualError(t, f.AddPivotTable(&PivotTableOption{
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "SheetN!$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), "sheet SheetN is not exist")
}), "sheet SheetN does not exist")
// Test invalid row number in data range
assert.EqualError(t, f.AddPivotTable(&PivotTableOption{
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$0:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Expand All @@ -217,78 +218,93 @@ func TestAddPivotTable(t *testing.T) {
}), `parameter 'DataRange' parsing error: cannot convert cell "A0" to coordinates: invalid cell name "A0"`)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable1.xlsx")))
// Test with field names that exceed the length limit and invalid subtotal
assert.NoError(t, f.AddPivotTable(&PivotTableOption{
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$G$2:$M$34",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", MaxFieldLength+1)}},
}))

// Test add pivot table with invalid sheet name
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet:1!$A$1:$E$31",
PivotTableRange: "Sheet:1!$G$2:$M$34",
Rows: []PivotTableField{{Data: "Year"}},
}), ErrSheetNameInvalid.Error())
// Test adjust range with invalid range
_, _, err := f.adjustRange("")
_, _, err = f.adjustRange("")
assert.EqualError(t, err, ErrParameterRequired.Error())
// 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(&PivotTableOption{})
_, err = f.getPivotFieldsOrder(&PivotTableOptions{})
assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`)
// Test add pivot cache with empty data range
assert.EqualError(t, f.addPivotCache("", &PivotTableOption{}), "parameter 'DataRange' parsing error: parameter is required")
assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is required")
// Test add pivot cache with invalid data range
assert.EqualError(t, f.addPivotCache("", &PivotTableOption{
assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{
DataRange: "$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), "parameter 'DataRange' parsing error: parameter is invalid")
// Test add pivot table with empty options
assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOption{}), "parameter 'PivotTableRange' parsing error: parameter is required")
assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOptions{}), "parameter 'PivotTableRange' parsing error: parameter is required")
// Test add pivot table with invalid data range
assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOption{}), "parameter 'PivotTableRange' parsing error: parameter is required")
assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOptions{}), "parameter 'PivotTableRange' parsing error: parameter is required")
// Test add pivot fields with empty data range
assert.EqualError(t, f.addPivotFields(nil, &PivotTableOption{
assert.EqualError(t, f.addPivotFields(nil, &PivotTableOptions{
DataRange: "$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), `parameter 'DataRange' parsing error: parameter is invalid`)
// Test get pivot fields index with empty data range
_, err = f.getPivotFieldsIndex([]PivotTableField{}, &PivotTableOption{})
_, err = f.getPivotFieldsIndex([]PivotTableField{}, &PivotTableOptions{})
assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`)
// Test add pivot table with unsupported charset content types.
f = NewFile()
f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$G$2:$M$34",
Rows: []PivotTableField{{Data: "Year"}},
}), "XML syntax error on line 1: invalid UTF-8")
}

func TestAddPivotRowFields(t *testing.T) {
f := NewFile()
// Test invalid data range
assert.EqualError(t, f.addPivotRowFields(&xlsxPivotTableDefinition{}, &PivotTableOption{
assert.EqualError(t, f.addPivotRowFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1",
}), `parameter 'DataRange' parsing error: parameter is invalid`)
}

func TestAddPivotPageFields(t *testing.T) {
f := NewFile()
// Test invalid data range
assert.EqualError(t, f.addPivotPageFields(&xlsxPivotTableDefinition{}, &PivotTableOption{
assert.EqualError(t, f.addPivotPageFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1",
}), `parameter 'DataRange' parsing error: parameter is invalid`)
}

func TestAddPivotDataFields(t *testing.T) {
f := NewFile()
// Test invalid data range
assert.EqualError(t, f.addPivotDataFields(&xlsxPivotTableDefinition{}, &PivotTableOption{
assert.EqualError(t, f.addPivotDataFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1",
}), `parameter 'DataRange' parsing error: parameter is invalid`)
}

func TestAddPivotColFields(t *testing.T) {
f := NewFile()
// Test invalid data range
assert.EqualError(t, f.addPivotColFields(&xlsxPivotTableDefinition{}, &PivotTableOption{
assert.EqualError(t, f.addPivotColFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1",
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
}), `parameter 'DataRange' parsing error: parameter is invalid`)
Expand All @@ -297,8 +313,8 @@ func TestAddPivotColFields(t *testing.T) {
func TestGetPivotFieldsOrder(t *testing.T) {
f := NewFile()
// Test get pivot fields order with not exist worksheet
_, err := f.getPivotFieldsOrder(&PivotTableOption{DataRange: "SheetN!$A$1:$E$31"})
assert.EqualError(t, err, "sheet SheetN is not exist")
_, err := f.getPivotFieldsOrder(&PivotTableOptions{DataRange: "SheetN!$A$1:$E$31"})
assert.EqualError(t, err, "sheet SheetN does not exist")
}

func TestGetPivotTableFieldName(t *testing.T) {
Expand Down
183 changes: 74 additions & 109 deletions rows.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand All @@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.15 or later.
// data. This library needs Go version 1.16 or later.

package excelize

Expand All @@ -16,10 +16,7 @@ import (
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"log"
"math"
"math/big"
"os"
"strconv"

Expand Down Expand Up @@ -141,7 +138,10 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) {
}
var rowIterator rowXMLIterator
var token xml.Token
rows.rawCellValue, rows.sst = parseOptions(opts...).RawCellValue, rows.f.sharedStringsReader()
rows.rawCellValue = parseOptions(opts...).RawCellValue
if rows.sst, rowIterator.err = rows.f.sharedStringsReader(); rowIterator.err != nil {
return rowIterator.cells, rowIterator.err
}
for {
if rows.token != nil {
token = rows.token
Expand All @@ -162,21 +162,21 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) {
rows.seekRowOpts = extractRowOpts(xmlElement.Attr)
if rows.curRow > rows.seekRow {
rows.token = nil
return rowIterator.columns, rowIterator.err
return rowIterator.cells, rowIterator.err
}
}
if rows.rowXMLHandler(&rowIterator, &xmlElement, rows.rawCellValue); rowIterator.err != nil {
rows.token = nil
return rowIterator.columns, rowIterator.err
return rowIterator.cells, rowIterator.err
}
rows.token = nil
case xml.EndElement:
if xmlElement.Name.Local == "sheetData" {
return rowIterator.columns, rowIterator.err
return rowIterator.cells, rowIterator.err
}
}
}
return rowIterator.columns, rowIterator.err
return rowIterator.cells, rowIterator.err
}

// extractRowOpts extract row element attributes.
Expand All @@ -202,21 +202,21 @@ func appendSpace(l int, s []string) []string {
return s
}

// ErrSheetNotExist defines an error of sheet is not exist
// ErrSheetNotExist defines an error of sheet that does not exist
type ErrSheetNotExist struct {
SheetName string
}

func (err ErrSheetNotExist) Error() string {
return fmt.Sprintf("sheet %s is not exist", err.SheetName)
return fmt.Sprintf("sheet %s does not exist", err.SheetName)
}

// rowXMLIterator defined runtime use field for the worksheet row SAX parser.
type rowXMLIterator struct {
err error
inElement string
cellCol int
columns []string
err error
inElement string
cellCol, cellRow int
cells []string
}

// rowXMLHandler parse the row XML element of the worksheet.
Expand All @@ -230,15 +230,16 @@ func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.Sta
return
}
}
blank := rowIterator.cellCol - len(rowIterator.columns)
blank := rowIterator.cellCol - len(rowIterator.cells)
if val, _ := colCell.getValueFrom(rows.f, rows.sst, raw); val != "" || colCell.F != nil {
rowIterator.columns = append(appendSpace(blank, rowIterator.columns), val)
rowIterator.cells = append(appendSpace(blank, rowIterator.cells), val)
}
}
}

// Rows returns a rows iterator, used for streaming reading data for a
// worksheet with a large data. For example:
// worksheet with a large data. This function is concurrency safe. For
// example:
//
// rows, err := f.Rows("Sheet1")
// if err != nil {
Expand All @@ -259,6 +260,9 @@ func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.Sta
// fmt.Println(err)
// }
func (f *File) Rows(sheet string) (*Rows, error) {
if err := checkSheetName(sheet); err != nil {
return nil, err
}
name, ok := f.getSheetXMLPath(sheet)
if !ok {
return nil, ErrSheetNotExist{sheet}
Expand All @@ -267,7 +271,7 @@ func (f *File) Rows(sheet string) (*Rows, error) {
worksheet := ws.(*xlsxWorksheet)
worksheet.Lock()
defer worksheet.Unlock()
// flush data
// Flush data
output, _ := xml.Marshal(worksheet)
f.saveFileList(name, f.replaceNameSpaceBytes(name, output))
}
Expand Down Expand Up @@ -296,7 +300,7 @@ func (f *File) getFromStringItem(index int) string {
defer tempFile.Close()
}
f.sharedStringItem = [][]uint{}
f.sharedStringTemp, _ = ioutil.TempFile(os.TempDir(), "excelize-")
f.sharedStringTemp, _ = os.CreateTemp(os.TempDir(), "excelize-")
f.tempFiles.Store(defaultTempFileSST, f.sharedStringTemp.Name())
var (
inElement string
Expand Down Expand Up @@ -410,7 +414,7 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) {

// sharedStringsReader provides a function to get the pointer to the structure
// after deserialization of xl/sharedStrings.xml.
func (f *File) sharedStringsReader() *xlsxSST {
func (f *File) sharedStringsReader() (*xlsxSST, error) {
var err error
f.Lock()
defer f.Unlock()
Expand All @@ -420,7 +424,7 @@ func (f *File) sharedStringsReader() *xlsxSST {
ss := f.readXML(defaultXMLPathSharedStrings)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(ss))).
Decode(&sharedStrings); err != nil && err != io.EOF {
log.Printf("xml decode error: %s", err)
return f.SharedStrings, err
}
if sharedStrings.Count == 0 {
sharedStrings.Count = len(sharedStrings.SI)
Expand All @@ -434,81 +438,23 @@ func (f *File) sharedStringsReader() *xlsxSST {
f.sharedStringsMap[sharedStrings.SI[i].T.Val] = i
}
}
f.addContentTypePart(0, "sharedStrings")
rels := f.relsReader(relPath)
if err = f.addContentTypePart(0, "sharedStrings"); err != nil {
return f.SharedStrings, err
}
rels, err := f.relsReader(relPath)
if err != nil {
return f.SharedStrings, err
}
for _, rel := range rels.Relationships {
if rel.Target == "/xl/sharedStrings.xml" {
return f.SharedStrings
return f.SharedStrings, nil
}
}
// Update workbook.xml.rels
f.addRels(relPath, SourceRelationshipSharedStrings, "/xl/sharedStrings.xml", "")
}

return f.SharedStrings
}

// getValueFrom return a value from a column/row cell, this function is
// intended to be used with for range on rows an argument with the spreadsheet
// opened file.
func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) {
f.Lock()
defer f.Unlock()
switch c.T {
case "b":
if !raw {
if c.V == "1" {
return "TRUE", nil
}
if c.V == "0" {
return "FALSE", nil
}
}
return f.formattedValue(c.S, c.V, raw), nil
case "s":
if c.V != "" {
xlsxSI := 0
xlsxSI, _ = strconv.Atoi(c.V)
if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok {
return f.formattedValue(c.S, f.getFromStringItem(xlsxSI), raw), nil
}
if len(d.SI) > xlsxSI {
return f.formattedValue(c.S, d.SI[xlsxSI].String(), raw), nil
}
}
return f.formattedValue(c.S, c.V, raw), nil
case "str":
return f.formattedValue(c.S, c.V, raw), nil
case "inlineStr":
if c.IS != nil {
return f.formattedValue(c.S, c.IS.String(), raw), nil
}
return f.formattedValue(c.S, c.V, raw), nil
default:
if isNum, precision := isNumeric(c.V); isNum && !raw {
if precision == 0 {
c.V = roundPrecision(c.V, 15)
} else {
c.V = roundPrecision(c.V, -1)
}
}
return f.formattedValue(c.S, c.V, raw), nil
}
}

// roundPrecision provides a function to format floating-point number text
// with precision, if the given text couldn't be parsed to float, this will
// return the original string.
func roundPrecision(text string, prec int) string {
decimal := big.Float{}
if _, ok := decimal.SetString(text); ok {
flt, _ := decimal.Float64()
if prec == -1 {
return strconv.FormatFloat(flt, 'G', 15, 64)
}
return strconv.FormatFloat(flt, 'f', -1, 64)
}
return text
return f.SharedStrings, nil
}

// SetRowVisible provides a function to set visible of a single row by given
Expand Down Expand Up @@ -622,21 +568,27 @@ func (f *File) RemoveRow(sheet string, row int) error {
return f.adjustHelper(sheet, rows, row, -1)
}

// InsertRow provides a function to insert a new row after given Excel row
// number starting from 1. For example, create a new row before row 3 in
// Sheet1:
// InsertRows provides a function to insert new rows after the given Excel row
// number starting from 1 and number of rows. For example, create two rows
// before row 3 in Sheet1:
//
// err := f.InsertRow("Sheet1", 3)
// err := f.InsertRows("Sheet1", 3, 2)
//
// Use this method with caution, which will affect changes in references such
// as formulas, charts, and so on. If there is any referenced value of the
// worksheet, it will cause a file error when you open it. The excelize only
// partially updates these references currently.
func (f *File) InsertRow(sheet string, row int) error {
func (f *File) InsertRows(sheet string, row, n int) error {
if row < 1 {
return newInvalidRowNumberError(row)
}
return f.adjustHelper(sheet, rows, row, 1)
if row >= TotalRows || n >= TotalRows {
return ErrMaxRows
}
if n < 1 {
return ErrParameterInvalid
}
return f.adjustHelper(sheet, rows, row, n)
}

// DuplicateRow inserts a copy of specified row (by its Excel row number) below
Expand Down Expand Up @@ -725,7 +677,7 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in
row++
}
for _, rng := range ws.MergeCells.Cells {
coordinates, err := areaRefToCoordinates(rng.Ref)
coordinates, err := rangeRefToCoordinates(rng.Ref)
if err != nil {
return err
}
Expand All @@ -734,8 +686,8 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in
}
}
for i := 0; i < len(ws.MergeCells.Cells); i++ {
areaData := ws.MergeCells.Cells[i]
coordinates, _ := areaRefToCoordinates(areaData.Ref)
mergedCells := ws.MergeCells.Cells[i]
coordinates, _ := rangeRefToCoordinates(mergedCells.Ref)
x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3]
if y1 == y2 && y1 == row {
from, _ := CoordinatesToCellName(x1, row2)
Expand Down Expand Up @@ -770,7 +722,7 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in
// <c r="G15" s="1" />
// </row>
//
// Noteice: this method could be very slow for large spreadsheets (more than
// Notice: this method could be very slow for large spreadsheets (more than
// 3000 rows one sheet).
func checkRow(ws *xlsxWorksheet) error {
for rowIdx := range ws.SheetData.Row {
Expand Down Expand Up @@ -802,8 +754,8 @@ func checkRow(ws *xlsxWorksheet) error {
}

if colCount < lastCol {
oldList := rowData.C
newlist := make([]xlsxC, 0, lastCol)
sourceList := rowData.C
targetList := make([]xlsxC, 0, lastCol)

rowData.C = ws.SheetData.Row[rowIdx].C[:0]

Expand All @@ -812,13 +764,13 @@ func checkRow(ws *xlsxWorksheet) error {
if err != nil {
return err
}
newlist = append(newlist, xlsxC{R: cellName})
targetList = append(targetList, xlsxC{R: cellName})
}

rowData.C = newlist
rowData.C = targetList

for colIdx := range oldList {
colData := &oldList[colIdx]
for colIdx := range sourceList {
colData := &sourceList[colIdx]
colNum, _, err := CellNameToCoordinates(colData.R)
if err != nil {
return err
Expand All @@ -830,17 +782,24 @@ func checkRow(ws *xlsxWorksheet) error {
return nil
}

// hasAttr determine if row non-default attributes.
func (r *xlsxRow) hasAttr() bool {
return r.Spans != "" || r.S != 0 || r.CustomFormat || r.Ht != 0 ||
r.Hidden || r.CustomHeight || r.OutlineLevel != 0 || r.Collapsed ||
r.ThickTop || r.ThickBot || r.Ph
}

// SetRowStyle provides a function to set the style of rows by given worksheet
// name, row range, and style ID. Note that this will overwrite the existing
// styles for the rows, it won't append or merge style with existing styles.
//
// For example set style of row 1 on Sheet1:
//
// err = f.SetRowStyle("Sheet1", 1, 1, styleID)
// err := f.SetRowStyle("Sheet1", 1, 1, styleID)
//
// Set style of rows 1 to 10 on Sheet1:
//
// err = f.SetRowStyle("Sheet1", 1, 10, styleID)
// err := f.SetRowStyle("Sheet1", 1, 10, styleID)
func (f *File) SetRowStyle(sheet string, start, end, styleID int) error {
if end < start {
start, end = end, start
Expand All @@ -851,7 +810,13 @@ func (f *File) SetRowStyle(sheet string, start, end, styleID int) error {
if end > TotalRows {
return ErrMaxRows
}
if styleID < 0 {
s, err := f.stylesReader()
if err != nil {
return err
}
s.Lock()
defer s.Unlock()
if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID {
return newInvalidStyleID(styleID)
}
ws, err := f.workSheetReader(sheet)
Expand Down
235 changes: 176 additions & 59 deletions rows_test.go

Large diffs are not rendered by default.

181 changes: 86 additions & 95 deletions shape.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand All @@ -7,65 +7,73 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.15 or later.
// data. This library needs Go version 1.16 or later.

package excelize

import (
"encoding/json"
"strconv"
"strings"
)

// parseFormatShapeSet provides a function to parse the format settings of the
// parseShapeOptions provides a function to parse the format settings of the
// shape with default value.
func parseFormatShapeSet(formatSet string) (*formatShape, error) {
format := formatShape{
Width: 160,
Height: 160,
Format: formatPicture{
FPrintsWithSheet: true,
XScale: 1,
YScale: 1,
},
Line: formatLine{Width: 1},
func parseShapeOptions(opts *Shape) (*Shape, error) {
if opts == nil {
return nil, ErrParameterInvalid
}
if opts.Width == 0 {
opts.Width = defaultShapeSize
}
if opts.Height == 0 {
opts.Height = defaultShapeSize
}
if opts.Format.PrintObject == nil {
opts.Format.PrintObject = boolPtr(true)
}
if opts.Format.Locked == nil {
opts.Format.Locked = boolPtr(false)
}
err := json.Unmarshal([]byte(formatSet), &format)
return &format, err
if opts.Format.ScaleX == 0 {
opts.Format.ScaleX = defaultPictureScale
}
if opts.Format.ScaleY == 0 {
opts.Format.ScaleY = defaultPictureScale
}
if opts.Line.Width == nil {
opts.Line.Width = float64Ptr(defaultShapeLineWidth)
}
return opts, nil
}

// AddShape provides the method to add shape in a sheet by given worksheet
// index, shape format set (such as offset, scale, aspect ratio setting and
// print settings) and properties set. For example, add text box (rect shape)
// in Sheet1:
//
// err := f.AddShape("Sheet1", "G6", `{
// "type": "rect",
// "color":
// {
// "line": "#4286F4",
// "fill": "#8eb9ff"
// lineWidth := 1.2
// err := f.AddShape("Sheet1", "G6",
// &excelize.Shape{
// Type: "rect",
// Color: excelize.ShapeColor{Line: "#4286f4", Fill: "#8eb9ff"},
// Paragraph: []excelize.ShapeParagraph{
// {
// Text: "Rectangle Shape",
// Font: excelize.Font{
// Bold: true,
// Italic: true,
// Family: "Times New Roman",
// Size: 18,
// Color: "#777777",
// Underline: "sng",
// },
// },
// },
// Width: 180,
// Height: 40,
// Line: excelize.ShapeLine{Width: &lineWidth},
// },
// "paragraph": [
// {
// "text": "Rectangle Shape",
// "font":
// {
// "bold": true,
// "italic": true,
// "family": "Times New Roman",
// "size": 36,
// "color": "#777777",
// "underline": "sng"
// }
// }],
// "width": 180,
// "height": 90,
// "line":
// {
// "width": 1.2
// }
// }`)
// )
//
// The following shows the type of shape supported by excelize:
//
Expand Down Expand Up @@ -277,8 +285,8 @@ func parseFormatShapeSet(formatSet string) (*formatShape, error) {
// wavy
// wavyHeavy
// wavyDbl
func (f *File) AddShape(sheet, cell, format string) error {
formatSet, err := parseFormatShapeSet(format)
func (f *File) AddShape(sheet, cell string, opts *Shape) error {
options, err := parseShapeOptions(opts)
if err != nil {
return err
}
Expand All @@ -305,58 +313,38 @@ func (f *File) AddShape(sheet, cell, format string) error {
f.addSheetDrawing(sheet, rID)
f.addSheetNameSpace(sheet, SourceRelationship)
}
err = f.addDrawingShape(sheet, drawingXML, cell, formatSet)
if err != nil {
if err = f.addDrawingShape(sheet, drawingXML, cell, options); err != nil {
return err
}
f.addContentTypePart(drawingID, "drawings")
return err
return f.addContentTypePart(drawingID, "drawings")
}

// addDrawingShape provides a function to add preset geometry by given sheet,
// drawingXMLand format sets.
func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *formatShape) error {
func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) error {
fromCol, fromRow, err := CellNameToCoordinates(cell)
if err != nil {
return err
}
colIdx := fromCol - 1
rowIdx := fromRow - 1

textUnderlineType := map[string]bool{
"none": true,
"words": true,
"sng": true,
"dbl": true,
"heavy": true,
"dotted": true,
"dottedHeavy": true,
"dash": true,
"dashHeavy": true,
"dashLong": true,
"dashLongHeavy": true,
"dotDash": true,
"dotDashHeavy": true,
"dotDotDash": true,
"dotDotDashHeavy": true,
"wavy": true,
"wavyHeavy": true,
"wavyDbl": true,
}

width := int(float64(formatSet.Width) * formatSet.Format.XScale)
height := int(float64(formatSet.Height) * formatSet.Format.YScale)
width := int(float64(opts.Width) * opts.Format.ScaleX)
height := int(float64(opts.Height) * opts.Format.ScaleY)

colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, formatSet.Format.OffsetX, formatSet.Format.OffsetY,
colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, opts.Format.OffsetX, opts.Format.OffsetY,
width, height)
content, cNvPrID := f.drawingParser(drawingXML)
content, cNvPrID, err := f.drawingParser(drawingXML)
if err != nil {
return err
}
twoCellAnchor := xdrCellAnchor{}
twoCellAnchor.EditAs = formatSet.Format.Positioning
twoCellAnchor.EditAs = opts.Format.Positioning
from := xlsxFrom{}
from.Col = colStart
from.ColOff = formatSet.Format.OffsetX * EMU
from.ColOff = opts.Format.OffsetX * EMU
from.Row = rowStart
from.RowOff = formatSet.Format.OffsetY * EMU
from.RowOff = opts.Format.OffsetY * EMU
to := xlsxTo{}
to.Col = colEnd
to.ColOff = x2 * EMU
Expand All @@ -365,7 +353,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format
twoCellAnchor.From = &from
twoCellAnchor.To = &to
shape := xdrSp{
Macro: formatSet.Macro,
Macro: opts.Macro,
NvSpPr: &xdrNvSpPr{
CNvPr: &xlsxCNvPr{
ID: cNvPrID,
Expand All @@ -377,13 +365,13 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format
},
SpPr: &xlsxSpPr{
PrstGeom: xlsxPrstGeom{
Prst: formatSet.Type,
Prst: opts.Type,
},
},
Style: &xdrStyle{
LnRef: setShapeRef(formatSet.Color.Line, 2),
FillRef: setShapeRef(formatSet.Color.Fill, 1),
EffectRef: setShapeRef(formatSet.Color.Effect, 0),
LnRef: setShapeRef(opts.Color.Line, 2),
FillRef: setShapeRef(opts.Color.Fill, 1),
EffectRef: setShapeRef(opts.Color.Effect, 0),
FontRef: &aFontRef{
Idx: "minor",
SchemeClr: &attrValString{
Expand All @@ -401,31 +389,34 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format
},
},
}
if formatSet.Line.Width != 1 {
if *opts.Line.Width != 1 {
shape.SpPr.Ln = xlsxLineProperties{
W: f.ptToEMUs(formatSet.Line.Width),
W: f.ptToEMUs(*opts.Line.Width),
}
}
if len(formatSet.Paragraph) < 1 {
formatSet.Paragraph = []formatShapeParagraph{
defaultFont, err := f.GetDefaultFont()
if err != nil {
return err
}
if len(opts.Paragraph) < 1 {
opts.Paragraph = []ShapeParagraph{
{
Font: Font{
Bold: false,
Italic: false,
Underline: "none",
Family: f.GetDefaultFont(),
Family: defaultFont,
Size: 11,
Color: "#000000",
},
Text: " ",
},
}
}
for _, p := range formatSet.Paragraph {
u := p.Font.Underline
_, ok := textUnderlineType[u]
if !ok {
u = "none"
for _, p := range opts.Paragraph {
u := "none"
if idx := inStrSlice(supportedDrawingUnderlineTypes, p.Font.Underline, true); idx != -1 {
u = supportedDrawingUnderlineTypes[idx]
}
text := p.Text
if text == "" {
Expand All @@ -440,7 +431,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format
AltLang: "en-US",
U: u,
Sz: p.Font.Size * 100,
Latin: &aLatin{Typeface: p.Font.Family},
Latin: &xlsxCTTextFont{Typeface: p.Font.Family},
},
T: text,
},
Expand All @@ -460,8 +451,8 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format
}
twoCellAnchor.Sp = &shape
twoCellAnchor.ClientData = &xdrClientData{
FLocksWithSheet: formatSet.Format.FLocksWithSheet,
FPrintsWithSheet: formatSet.Format.FPrintsWithSheet,
FLocksWithSheet: *opts.Format.Locked,
FPrintsWithSheet: *opts.Format.PrintObject,
}
content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
f.Drawings.Store(drawingXML, content)
Expand Down
145 changes: 76 additions & 69 deletions shape_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,79 +12,86 @@ func TestAddShape(t *testing.T) {
if !assert.NoError(t, err) {
t.FailNow()
}

assert.NoError(t, f.AddShape("Sheet1", "A30", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`))
assert.NoError(t, f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`))
assert.NoError(t, f.AddShape("Sheet1", "C30", `{"type":"rect","paragraph":[]}`))
assert.EqualError(t, f.AddShape("Sheet3", "H1", `{
"type": "ellipseRibbon",
"color":
{
"line": "#4286f4",
"fill": "#8eb9ff"
shape := &Shape{
Type: "rect",
Paragraph: []ShapeParagraph{
{Text: "Rectangle", Font: Font{Color: "CD5C5C"}},
{Text: "Shape", Font: Font{Bold: true, Color: "2980B9"}},
},
"paragraph": [
{
"font":
{
"bold": true,
"italic": true,
"family": "Times New Roman",
"size": 36,
"color": "#777777",
"underline": "single"
}
}],
"height": 90
}`), "sheet Sheet3 is not exist")
assert.EqualError(t, f.AddShape("Sheet3", "H1", ""), "unexpected end of JSON input")
assert.EqualError(t, f.AddShape("Sheet1", "A", `{
"type": "rect",
"paragraph": [
{
"text": "Rectangle",
"font":
{
"color": "CD5C5C"
}
}
assert.NoError(t, f.AddShape("Sheet1", "A30", shape))
assert.NoError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []ShapeParagraph{{Text: "Rectangle"}, {}}}))
assert.NoError(t, f.AddShape("Sheet1", "C30", &Shape{Type: "rect"}))
assert.EqualError(t, f.AddShape("Sheet3", "H1",
&Shape{
Type: "ellipseRibbon",
Color: ShapeColor{Line: "#4286f4", Fill: "#8eb9ff"},
Paragraph: []ShapeParagraph{
{
Font: Font{
Bold: true,
Italic: true,
Family: "Times New Roman",
Size: 36,
Color: "#777777",
Underline: "single",
},
},
},
},
{
"text": "Shape",
"font":
{
"bold": true,
"color": "2980B9"
}
}]
}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
), "sheet Sheet3 does not exist")
assert.EqualError(t, f.AddShape("Sheet3", "H1", nil), ErrParameterInvalid.Error())
assert.EqualError(t, f.AddShape("Sheet1", "A", shape), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx")))

// Test add first shape for given sheet.
// Test add first shape for given sheet
f = NewFile()
assert.NoError(t, f.AddShape("Sheet1", "A1", `{
"type": "ellipseRibbon",
"color":
{
"line": "#4286f4",
"fill": "#8eb9ff"
},
"paragraph": [
{
"font":
{
"bold": true,
"italic": true,
"family": "Times New Roman",
"size": 36,
"color": "#777777",
"underline": "single"
}
}],
"height": 90,
"line":
{
"width": 1.2
}
}`))
lineWidth := 1.2
assert.NoError(t, f.AddShape("Sheet1", "A1",
&Shape{
Type: "ellipseRibbon",
Color: ShapeColor{Line: "#4286f4", Fill: "#8eb9ff"},
Paragraph: []ShapeParagraph{
{
Font: Font{
Bold: true,
Italic: true,
Family: "Times New Roman",
Size: 36,
Color: "#777777",
Underline: "single",
},
},
},
Height: 90,
Line: ShapeLine{Width: &lineWidth},
}))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx")))
// Test add shape with invalid sheet name
assert.EqualError(t, f.AddShape("Sheet:1", "A30", shape), ErrSheetNameInvalid.Error())
// Test add shape with unsupported charset style sheet
f.Styles = nil
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []ShapeParagraph{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8")
// Test add shape with unsupported charset content types
f = NewFile()
f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []ShapeParagraph{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8")
}

func TestAddDrawingShape(t *testing.T) {
f := NewFile()
path := "xl/drawings/drawing1.xml"
f.Pkg.Store(path, MacintoshCyrillicCharset)
assert.EqualError(t, f.addDrawingShape("sheet1", path, "A1",
&Shape{
Width: defaultShapeSize,
Height: defaultShapeSize,
Format: GraphicOptions{
PrintObject: boolPtr(true),
Locked: boolPtr(false),
},
},
), "XML syntax error on line 1: invalid UTF-8")
}
998 changes: 509 additions & 489 deletions sheet.go

Large diffs are not rendered by default.

492 changes: 301 additions & 191 deletions sheet_test.go

Large diffs are not rendered by default.

762 changes: 164 additions & 598 deletions sheetpr.go

Large diffs are not rendered by default.

559 changes: 80 additions & 479 deletions sheetpr_test.go

Large diffs are not rendered by default.

287 changes: 88 additions & 199 deletions sheetview.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2016 - 2022 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
Expand All @@ -7,156 +7,10 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.15 or later.
// data. This library needs Go version 1.16 or later.

package excelize

import "fmt"

// SheetViewOption is an option of a view of a worksheet. See
// SetSheetViewOptions().
type SheetViewOption interface {
setSheetViewOption(view *xlsxSheetView)
}

// SheetViewOptionPtr is a writable SheetViewOption. See
// GetSheetViewOptions().
type SheetViewOptionPtr interface {
SheetViewOption
getSheetViewOption(view *xlsxSheetView)
}

type (
// DefaultGridColor is a SheetViewOption. It specifies a flag indicating
// that the consuming application should use the default grid lines color
// (system dependent). Overrides any color specified in colorId.
DefaultGridColor bool
// ShowFormulas is a SheetViewOption. It specifies a flag indicating
// whether this sheet should display formulas.
ShowFormulas bool
// ShowGridLines is a SheetViewOption. It specifies a flag indicating
// whether this sheet should display gridlines.
ShowGridLines bool
// ShowRowColHeaders is a SheetViewOption. It specifies a flag indicating
// whether the sheet should display row and column headings.
ShowRowColHeaders bool
// ShowZeros is a SheetViewOption. It specifies a flag indicating whether
// to "show a zero in cells that have zero value". When using a formula to
// reference another cell which is empty, the referenced value becomes 0
// when the flag is true. (Default setting is true.)
ShowZeros bool
// RightToLeft is a SheetViewOption. It specifies a flag indicating whether
// the sheet is in 'right to left' display mode. When in this mode, Column
// A is on the far right, Column B ;is one column left of Column A, and so
// on. Also, information in cells is displayed in the Right to Left format.
RightToLeft bool
// ShowRuler is a SheetViewOption. It specifies a flag indicating this
// sheet should display ruler.
ShowRuler bool
// View is a SheetViewOption. It specifies a flag indicating how sheet is
// displayed, by default it uses empty string available options: normal,
// pageLayout, pageBreakPreview
View string
// TopLeftCell is a SheetViewOption. It specifies a location of the top
// left visible cell Location of the top left visible cell in the bottom
// right pane (when in Left-to-Right mode).
TopLeftCell string
// ZoomScale is a SheetViewOption. It specifies a window zoom magnification
// for current view representing percent values. This attribute is
// restricted to values ranging from 10 to 400. Horizontal & Vertical
// scale together.
ZoomScale float64
)

// Defaults for each option are described in XML schema for CT_SheetView

func (o DefaultGridColor) setSheetViewOption(view *xlsxSheetView) {
view.DefaultGridColor = boolPtr(bool(o))
}

func (o *DefaultGridColor) getSheetViewOption(view *xlsxSheetView) {
*o = DefaultGridColor(defaultTrue(view.DefaultGridColor)) // Excel default: true
}

func (o ShowFormulas) setSheetViewOption(view *xlsxSheetView) {
view.ShowFormulas = bool(o) // Excel default: false
}

func (o *ShowFormulas) getSheetViewOption(view *xlsxSheetView) {
*o = ShowFormulas(view.ShowFormulas) // Excel default: false
}

func (o ShowGridLines) setSheetViewOption(view *xlsxSheetView) {
view.ShowGridLines = boolPtr(bool(o))
}

func (o *ShowGridLines) getSheetViewOption(view *xlsxSheetView) {
*o = ShowGridLines(defaultTrue(view.ShowGridLines)) // Excel default: true
}

func (o ShowRowColHeaders) setSheetViewOption(view *xlsxSheetView) {
view.ShowRowColHeaders = boolPtr(bool(o))
}

func (o *ShowRowColHeaders) getSheetViewOption(view *xlsxSheetView) {
*o = ShowRowColHeaders(defaultTrue(view.ShowRowColHeaders)) // Excel default: true
}

func (o ShowZeros) setSheetViewOption(view *xlsxSheetView) {
view.ShowZeros = boolPtr(bool(o))
}

func (o *ShowZeros) getSheetViewOption(view *xlsxSheetView) {
*o = ShowZeros(defaultTrue(view.ShowZeros)) // Excel default: true
}

func (o RightToLeft) setSheetViewOption(view *xlsxSheetView) {
view.RightToLeft = bool(o) // Excel default: false
}

func (o *RightToLeft) getSheetViewOption(view *xlsxSheetView) {
*o = RightToLeft(view.RightToLeft)
}

func (o ShowRuler) setSheetViewOption(view *xlsxSheetView) {
view.ShowRuler = boolPtr(bool(o))
}

func (o *ShowRuler) getSheetViewOption(view *xlsxSheetView) {
*o = ShowRuler(defaultTrue(view.ShowRuler)) // Excel default: true
}

func (o View) setSheetViewOption(view *xlsxSheetView) {
view.View = string(o)
}

func (o *View) getSheetViewOption(view *xlsxSheetView) {
if view.View != "" {
*o = View(view.View)
return
}
*o = "normal"
}

func (o TopLeftCell) setSheetViewOption(view *xlsxSheetView) {
view.TopLeftCell = string(o)
}

func (o *TopLeftCell) getSheetViewOption(view *xlsxSheetView) {
*o = TopLeftCell(view.TopLeftCell)
}

func (o ZoomScale) setSheetViewOption(view *xlsxSheetView) {
// This attribute is restricted to values ranging from 10 to 400.
if float64(o) >= 10 && float64(o) <= 400 {
view.ZoomScale = float64(o)
}
}

func (o *ZoomScale) getSheetViewOption(view *xlsxSheetView) {
*o = ZoomScale(view.ZoomScale)
}

// getSheetView returns the SheetView object
func (f *File) getSheetView(sheet string, viewIndex int) (*xlsxSheetView, error) {
ws, err := f.workSheetReader(sheet)
Expand All @@ -170,75 +24,110 @@ func (f *File) getSheetView(sheet string, viewIndex int) (*xlsxSheetView, error)
}
if viewIndex < 0 {
if viewIndex < -len(ws.SheetViews.SheetView) {
return nil, fmt.Errorf("view index %d out of range", viewIndex)
return nil, newViewIdxError(viewIndex)
}
viewIndex = len(ws.SheetViews.SheetView) + viewIndex
} else if viewIndex >= len(ws.SheetViews.SheetView) {
return nil, fmt.Errorf("view index %d out of range", viewIndex)
return nil, newViewIdxError(viewIndex)
}

return &(ws.SheetViews.SheetView[viewIndex]), err
}

// SetSheetViewOptions sets sheet view options. The viewIndex may be negative
// and if so is counted backward (-1 is the last view).
//
// Available options:
//
// DefaultGridColor(bool)
// ShowFormulas(bool)
// ShowGridLines(bool)
// ShowRowColHeaders(bool)
// ShowZeros(bool)
// RightToLeft(bool)
// ShowRuler(bool)
// View(string)
// TopLeftCell(string)
// ZoomScale(float64)
//
// Example:
//
// err = f.SetSheetViewOptions("Sheet1", -1, ShowGridLines(false))
func (f *File) SetSheetViewOptions(sheet string, viewIndex int, opts ...SheetViewOption) error {
// setSheetView set sheet view by given options.
func (view *xlsxSheetView) setSheetView(opts *ViewOptions) {
if opts.DefaultGridColor != nil {
view.DefaultGridColor = opts.DefaultGridColor
}
if opts.RightToLeft != nil {
view.RightToLeft = *opts.RightToLeft
}
if opts.ShowFormulas != nil {
view.ShowFormulas = *opts.ShowFormulas
}
if opts.ShowGridLines != nil {
view.ShowGridLines = opts.ShowGridLines
}
if opts.ShowRowColHeaders != nil {
view.ShowRowColHeaders = opts.ShowRowColHeaders
}
if opts.ShowRuler != nil {
view.ShowRuler = opts.ShowRuler
}
if opts.ShowZeros != nil {
view.ShowZeros = opts.ShowZeros
}
if opts.TopLeftCell != nil {
view.TopLeftCell = *opts.TopLeftCell
}
if opts.View != nil {
if _, ok := map[string]interface{}{
"normal": nil,
"pageLayout": nil,
"pageBreakPreview": nil,
}[*opts.View]; ok {
view.View = *opts.View
}
}
if opts.ZoomScale != nil && *opts.ZoomScale >= 10 && *opts.ZoomScale <= 400 {
view.ZoomScale = *opts.ZoomScale
}
}

// SetSheetView sets sheet view options. The viewIndex may be negative and if
// so is counted backward (-1 is the last view).
func (f *File) SetSheetView(sheet string, viewIndex int, opts *ViewOptions) error {
view, err := f.getSheetView(sheet, viewIndex)
if err != nil {
return err
}

for _, opt := range opts {
opt.setSheetViewOption(view)
if opts == nil {
return err
}
view.setSheetView(opts)
return nil
}

// GetSheetViewOptions gets the value of sheet view options. The viewIndex may
// be negative and if so is counted backward (-1 is the last view).
//
// Available options:
//
// DefaultGridColor(bool)
// ShowFormulas(bool)
// ShowGridLines(bool)
// ShowRowColHeaders(bool)
// ShowZeros(bool)
// RightToLeft(bool)
// ShowRuler(bool)
// View(string)
// TopLeftCell(string)
// ZoomScale(float64)
//
// Example:
//
// var showGridLines excelize.ShowGridLines
// err = f.GetSheetViewOptions("Sheet1", -1, &showGridLines)
func (f *File) GetSheetViewOptions(sheet string, viewIndex int, opts ...SheetViewOptionPtr) error {
// GetSheetView gets the value of sheet view options. The viewIndex may be
// negative and if so is counted backward (-1 is the last view).
func (f *File) GetSheetView(sheet string, viewIndex int) (ViewOptions, error) {
opts := ViewOptions{
DefaultGridColor: boolPtr(true),
ShowFormulas: boolPtr(true),
ShowGridLines: boolPtr(true),
ShowRowColHeaders: boolPtr(true),
ShowRuler: boolPtr(true),
ShowZeros: boolPtr(true),
View: stringPtr("normal"),
ZoomScale: float64Ptr(100),
}
view, err := f.getSheetView(sheet, viewIndex)
if err != nil {
return err
return opts, err
}

for _, opt := range opts {
opt.getSheetViewOption(view)
if view.DefaultGridColor != nil {
opts.DefaultGridColor = view.DefaultGridColor
}
return nil
opts.RightToLeft = boolPtr(view.RightToLeft)
opts.ShowFormulas = boolPtr(view.ShowFormulas)
if view.ShowGridLines != nil {
opts.ShowGridLines = view.ShowGridLines
}
if view.ShowRowColHeaders != nil {
opts.ShowRowColHeaders = view.ShowRowColHeaders
}
if view.ShowRuler != nil {
opts.ShowRuler = view.ShowRuler
}
if view.ShowZeros != nil {
opts.ShowZeros = view.ShowZeros
}
opts.TopLeftCell = stringPtr(view.TopLeftCell)
if view.View != "" {
opts.View = stringPtr(view.View)
}
if view.ZoomScale >= 10 && view.ZoomScale <= 400 {
opts.ZoomScale = float64Ptr(view.ZoomScale)
}
return opts, err
}
242 changes: 37 additions & 205 deletions sheetview_test.go
Original file line number Diff line number Diff line change
@@ -1,218 +1,50 @@
package excelize

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

var _ = []SheetViewOption{
DefaultGridColor(true),
ShowFormulas(false),
ShowGridLines(true),
ShowRowColHeaders(true),
ShowZeros(true),
RightToLeft(false),
ShowRuler(false),
View("pageLayout"),
TopLeftCell("B2"),
ZoomScale(100),
// SheetViewOptionPtr are also SheetViewOption
new(DefaultGridColor),
new(ShowFormulas),
new(ShowGridLines),
new(ShowRowColHeaders),
new(ShowZeros),
new(RightToLeft),
new(ShowRuler),
new(View),
new(TopLeftCell),
new(ZoomScale),
}

var _ = []SheetViewOptionPtr{
(*DefaultGridColor)(nil),
(*ShowFormulas)(nil),
(*ShowGridLines)(nil),
(*ShowRowColHeaders)(nil),
(*ShowZeros)(nil),
(*RightToLeft)(nil),
(*ShowRuler)(nil),
(*View)(nil),
(*TopLeftCell)(nil),
(*ZoomScale)(nil),
}

func ExampleFile_SetSheetViewOptions() {
f := NewFile()
const sheet = "Sheet1"

if err := f.SetSheetViewOptions(sheet, 0,
DefaultGridColor(false),
ShowFormulas(true),
ShowGridLines(true),
ShowRowColHeaders(true),
RightToLeft(false),
ShowRuler(false),
View("pageLayout"),
TopLeftCell("C3"),
ZoomScale(80),
); err != nil {
fmt.Println(err)
}

var zoomScale ZoomScale
fmt.Println("Default:")
fmt.Println("- zoomScale: 80")

if err := f.SetSheetViewOptions(sheet, 0, ZoomScale(500)); err != nil {
fmt.Println(err)
}

if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil {
fmt.Println(err)
}

fmt.Println("Used out of range value:")
fmt.Println("- zoomScale:", zoomScale)

if err := f.SetSheetViewOptions(sheet, 0, ZoomScale(123)); err != nil {
fmt.Println(err)
}

if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil {
fmt.Println(err)
}

fmt.Println("Used correct value:")
fmt.Println("- zoomScale:", zoomScale)

// Output:
// Default:
// - zoomScale: 80
// Used out of range value:
// - zoomScale: 80
// Used correct value:
// - zoomScale: 123
}

func ExampleFile_GetSheetViewOptions() {
func TestSetView(t *testing.T) {
f := NewFile()
const sheet = "Sheet1"

var (
defaultGridColor DefaultGridColor
showFormulas ShowFormulas
showGridLines ShowGridLines
showRowColHeaders ShowRowColHeaders
showZeros ShowZeros
rightToLeft RightToLeft
showRuler ShowRuler
view View
topLeftCell TopLeftCell
zoomScale ZoomScale
)

if err := f.GetSheetViewOptions(sheet, 0,
&defaultGridColor,
&showFormulas,
&showGridLines,
&showRowColHeaders,
&showZeros,
&rightToLeft,
&showRuler,
&view,
&topLeftCell,
&zoomScale,
); err != nil {
fmt.Println(err)
}

fmt.Println("Default:")
fmt.Println("- defaultGridColor:", defaultGridColor)
fmt.Println("- showFormulas:", showFormulas)
fmt.Println("- showGridLines:", showGridLines)
fmt.Println("- showRowColHeaders:", showRowColHeaders)
fmt.Println("- showZeros:", showZeros)
fmt.Println("- rightToLeft:", rightToLeft)
fmt.Println("- showRuler:", showRuler)
fmt.Println("- view:", view)
fmt.Println("- topLeftCell:", `"`+topLeftCell+`"`)
fmt.Println("- zoomScale:", zoomScale)

if err := f.SetSheetViewOptions(sheet, 0, ShowGridLines(false)); err != nil {
fmt.Println(err)
}

if err := f.GetSheetViewOptions(sheet, 0, &showGridLines); err != nil {
fmt.Println(err)
}

if err := f.SetSheetViewOptions(sheet, 0, ShowZeros(false)); err != nil {
fmt.Println(err)
}

if err := f.GetSheetViewOptions(sheet, 0, &showZeros); err != nil {
fmt.Println(err)
}

if err := f.SetSheetViewOptions(sheet, 0, View("pageLayout")); err != nil {
fmt.Println(err)
}

if err := f.GetSheetViewOptions(sheet, 0, &view); err != nil {
fmt.Println(err)
}

if err := f.SetSheetViewOptions(sheet, 0, TopLeftCell("B2")); err != nil {
fmt.Println(err)
}

if err := f.GetSheetViewOptions(sheet, 0, &topLeftCell); err != nil {
fmt.Println(err)
}

fmt.Println("After change:")
fmt.Println("- showGridLines:", showGridLines)
fmt.Println("- showZeros:", showZeros)
fmt.Println("- view:", view)
fmt.Println("- topLeftCell:", topLeftCell)

// Output:
// Default:
// - defaultGridColor: true
// - showFormulas: false
// - showGridLines: true
// - showRowColHeaders: true
// - showZeros: true
// - rightToLeft: false
// - showRuler: true
// - view: normal
// - topLeftCell: ""
// - zoomScale: 0
// After change:
// - showGridLines: false
// - showZeros: false
// - view: pageLayout
// - topLeftCell: B2
}

func TestSheetViewOptionsErrors(t *testing.T) {
f := NewFile()
const sheet = "Sheet1"

assert.NoError(t, f.GetSheetViewOptions(sheet, 0))
assert.NoError(t, f.GetSheetViewOptions(sheet, -1))
assert.Error(t, f.GetSheetViewOptions(sheet, 1))
assert.Error(t, f.GetSheetViewOptions(sheet, -2))
assert.NoError(t, f.SetSheetViewOptions(sheet, 0))
assert.NoError(t, f.SetSheetViewOptions(sheet, -1))
assert.Error(t, f.SetSheetViewOptions(sheet, 1))
assert.Error(t, f.SetSheetViewOptions(sheet, -2))

assert.NoError(t, f.SetSheetView("Sheet1", -1, nil))
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).SheetViews = nil
assert.NoError(t, f.GetSheetViewOptions(sheet, 0))
expected := ViewOptions{
DefaultGridColor: boolPtr(false),
RightToLeft: boolPtr(false),
ShowFormulas: boolPtr(false),
ShowGridLines: boolPtr(false),
ShowRowColHeaders: boolPtr(false),
ShowRuler: boolPtr(false),
ShowZeros: boolPtr(false),
TopLeftCell: stringPtr("A1"),
View: stringPtr("normal"),
ZoomScale: float64Ptr(120),
}
assert.NoError(t, f.SetSheetView("Sheet1", 0, &expected))
opts, err := f.GetSheetView("Sheet1", 0)
assert.NoError(t, err)
assert.Equal(t, expected, opts)
// Test set sheet view options with invalid view index
assert.EqualError(t, f.SetSheetView("Sheet1", 1, nil), "view index 1 out of range")
assert.EqualError(t, f.SetSheetView("Sheet1", -2, nil), "view index -2 out of range")
// Test set sheet view options on not exists worksheet
assert.EqualError(t, f.SetSheetView("SheetN", 0, nil), "sheet SheetN does not exist")
}

func TestGetView(t *testing.T) {
f := NewFile()
_, err := f.getSheetView("SheetN", 0)
assert.EqualError(t, err, "sheet SheetN does not exist")
// Test get sheet view options with invalid view index
_, err = f.GetSheetView("Sheet1", 1)
assert.EqualError(t, err, "view index 1 out of range")
_, err = f.GetSheetView("Sheet1", -2)
assert.EqualError(t, err, "view index -2 out of range")
// Test get sheet view options on not exists worksheet
_, err = f.GetSheetView("SheetN", 0)
assert.EqualError(t, err, "sheet SheetN does not exist")
}
Loading