提交 5c87effc 编写于 作者: C Cameron Howey 提交者: xurime

Stream to Excel table (#530)

* Support all datatypes for StreamWriter

* Support setting styles with StreamWriter

**NOTE:** This is a breaking change. Values are now explicitly
passed as a []interface{} for simplicity. We also let styles to be
set at the same time.

* Create function to write stream into a table

* Write rows directly to buffer

Avoiding the xml.Encoder makes the streamer faster and use less
memory.

Using the included benchmark, the results went from:

> BenchmarkStreamWriter-4   514  2576155 ns/op  454918 B/op  6592 allocs/op

down to:

> BenchmarkStreamWriter-4  1614   777480 ns/op  147608 B/op  5570 allocs/op

* Use AddTable instead of SetTable

This requires reading the cells after they have been written,
which requires additional structure for the temp file.

As a bonus, we now efficiently allocate only one buffer when
reading the file back into memory, using the same approach
as ioutil.ReadFile.

* Use an exported Cell type to handle inline styles for StreamWriter
上级 8b960ee1
......@@ -196,10 +196,14 @@ func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, nu
// areaRefToCoordinates provides a function to convert area reference to a
// pair of coordinates.
func (f *File) areaRefToCoordinates(ref string) ([]int, error) {
coordinates := make([]int, 4)
rng := strings.Split(ref, ":")
firstCell := rng[0]
lastCell := rng[1]
return areaRangeToCoordinates(rng[0], rng[1])
}
// areaRangeToCoordinates provides a function to convert cell range to a
// pair of coordinates.
func areaRangeToCoordinates(firstCell, lastCell string) ([]int, error) {
coordinates := make([]int, 4)
var err error
coordinates[0], coordinates[1], err = CellNameToCoordinates(firstCell)
if err != nil {
......@@ -209,6 +213,19 @@ func (f *File) areaRefToCoordinates(ref string) ([]int, error) {
return coordinates, err
}
func sortCoordinates(coordinates []int) error {
if len(coordinates) != 4 {
return errors.New("coordinates length must be 4")
}
if coordinates[2] < coordinates[0] {
coordinates[2], coordinates[0] = coordinates[0], coordinates[2]
}
if coordinates[3] < coordinates[1] {
coordinates[3], coordinates[1] = coordinates[1], coordinates[3]
}
return nil
}
// coordinatesToAreaRef provides a function to convert a pair of coordinates
// to area reference.
func (f *File) coordinatesToAreaRef(coordinates []int) (string, error) {
......
......@@ -83,7 +83,8 @@ func (f *File) SetCellValue(sheet, axis string, value interface{}) error {
case []byte:
err = f.SetCellStr(sheet, axis, string(v))
case time.Duration:
err = f.SetCellDefault(sheet, axis, strconv.FormatFloat(v.Seconds()/86400.0, 'f', -1, 32))
_, d := setCellDuration(v)
err = f.SetCellDefault(sheet, axis, d)
if err != nil {
return err
}
......@@ -131,28 +132,50 @@ func (f *File) setCellIntFunc(sheet, axis string, value interface{}) error {
// setCellTimeFunc provides a method to process time type of value for
// SetCellValue.
func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error {
excelTime, err := timeToExcelTime(value)
xlsx, err := f.workSheetReader(sheet)
if err != nil {
return err
}
if excelTime > 0 {
err = f.SetCellDefault(sheet, axis, strconv.FormatFloat(excelTime, 'f', -1, 64))
if err != nil {
return err
}
cellData, col, _, err := f.prepareCell(xlsx, sheet, axis)
if err != nil {
return err
}
cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
var isNum bool
cellData.T, cellData.V, isNum, err = setCellTime(value)
if err != nil {
return err
}
if isNum {
err = f.setDefaultTimeStyle(sheet, axis, 22)
if err != nil {
return err
}
} else {
err = f.SetCellStr(sheet, axis, value.Format(time.RFC3339Nano))
if err != nil {
return err
}
}
return err
}
func setCellTime(value time.Time) (t string, b string, isNum bool, err error) {
var excelTime float64
excelTime, err = timeToExcelTime(value)
if err != nil {
return
}
isNum = excelTime > 0
if isNum {
t, b = setCellDefault(strconv.FormatFloat(excelTime, 'f', -1, 64))
} else {
t, b = setCellDefault(value.Format(time.RFC3339Nano))
}
return
}
func setCellDuration(value time.Duration) (t string, v string) {
v = strconv.FormatFloat(value.Seconds()/86400.0, 'f', -1, 32)
return
}
// SetCellInt provides a function to set int type value of a cell by given
// worksheet name, cell coordinates and cell value.
func (f *File) SetCellInt(sheet, axis string, value int) error {
......@@ -165,11 +188,15 @@ func (f *File) SetCellInt(sheet, axis string, value int) error {
return err
}
cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
cellData.T = ""
cellData.V = strconv.Itoa(value)
cellData.T, cellData.V = setCellInt(value)
return err
}
func setCellInt(value int) (t string, v string) {
v = strconv.Itoa(value)
return
}
// SetCellBool provides a function to set bool type value of a cell by given
// worksheet name, cell name and cell value.
func (f *File) SetCellBool(sheet, axis string, value bool) error {
......@@ -182,13 +209,18 @@ func (f *File) SetCellBool(sheet, axis string, value bool) error {
return err
}
cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
cellData.T = "b"
cellData.T, cellData.V = setCellBool(value)
return err
}
func setCellBool(value bool) (t string, v string) {
t = "b"
if value {
cellData.V = "1"
v = "1"
} else {
cellData.V = "0"
v = "0"
}
return err
return
}
// SetCellFloat sets a floating point value into a cell. The prec parameter
......@@ -210,11 +242,15 @@ func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int
return err
}
cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
cellData.T = ""
cellData.V = strconv.FormatFloat(value, 'f', prec, bitSize)
cellData.T, cellData.V = setCellFloat(value, prec, bitSize)
return err
}
func setCellFloat(value float64, prec, bitSize int) (t string, v string) {
v = strconv.FormatFloat(value, 'f', prec, bitSize)
return
}
// SetCellStr provides a function to set string type value of a cell. Total
// number of characters that a cell can contain 32767 characters.
func (f *File) SetCellStr(sheet, axis, value string) error {
......@@ -226,21 +262,25 @@ func (f *File) SetCellStr(sheet, axis, value string) error {
if err != nil {
return err
}
cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
cellData.T, cellData.V, cellData.XMLSpace = setCellStr(value)
return err
}
func setCellStr(value string) (t string, v string, ns xml.Attr) {
if len(value) > 32767 {
value = value[0:32767]
}
// Leading and ending space(s) character detection.
if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) {
cellData.XMLSpace = xml.Attr{
ns = xml.Attr{
Name: xml.Name{Space: NameSpaceXML, Local: "space"},
Value: "preserve",
}
}
cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
cellData.T = "str"
cellData.V = value
return err
t = "str"
v = value
return
}
// SetCellDefault provides a function to set string type value of a cell as
......@@ -255,11 +295,15 @@ func (f *File) SetCellDefault(sheet, axis, value string) error {
return err
}
cellData.S = f.prepareCellStyle(xlsx, col, cellData.S)
cellData.T = ""
cellData.V = value
cellData.T, cellData.V = setCellDefault(value)
return err
}
func setCellDefault(value string) (t string, v string) {
v = value
return
}
// GetCellFormula provides a function to get formula from cell by given
// worksheet name and axis in XLSX file.
func (f *File) GetCellFormula(sheet, axis string) (string, error) {
......
......@@ -12,20 +12,23 @@ package excelize
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"reflect"
"strconv"
"strings"
"time"
)
// StreamWriter defined the type of stream writer.
type StreamWriter struct {
tmpFile *os.File
File *File
Sheet string
SheetID int
SheetData bytes.Buffer
File *File
Sheet string
SheetID int
rawData bufferedWriter
tableParts string
}
// NewStreamWriter return stream writer struct by given worksheet name for
......@@ -46,7 +49,7 @@ type StreamWriter struct {
// row[colID] = rand.Intn(640000)
// }
// cell, _ := excelize.CoordinatesToCellName(1, rowID)
// if err := streamWriter.SetRow(cell, &row); err != nil {
// if err := streamWriter.SetRow(cell, row, nil); err != nil {
// panic(err)
// }
// }
......@@ -62,157 +65,433 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) {
if sheetID == 0 {
return nil, fmt.Errorf("sheet %s is not exist", sheet)
}
rsw := &StreamWriter{
sw := &StreamWriter{
File: f,
Sheet: sheet,
SheetID: sheetID,
}
rsw.SheetData.WriteString("<sheetData>")
return rsw, nil
ws, err := f.workSheetReader(sheet)
if err != nil {
return nil, err
}
sw.rawData.WriteString(XMLHeader + `<worksheet` + templateNamespaceIDMap)
bulkAppendOtherFields(&sw.rawData, ws, "XMLName", "SheetData", "TableParts")
sw.rawData.WriteString(`<sheetData>`)
return sw, nil
}
// SetRow writes an array to streaming row by given worksheet name, starting
// coordinate and a pointer to array type 'slice'. Note that, cell settings
// with styles are not supported currently and after set rows, you must call the
// 'Flush' method to end the streaming writing process. The following
// shows the supported data types:
// AddTable creates an Excel table for the StreamWriter using the given
// coordinate area and format set. For example, create a table of A1:D5:
//
// int
// string
// err := sw.AddTable("A1", "D5", ``)
//
func (sw *StreamWriter) SetRow(axis string, slice interface{}) error {
col, row, err := CellNameToCoordinates(axis)
// Create a table of F2:H6 with format set:
//
// err := sw.AddTable("F2", "H6", `{"table_name":"table","table_style":"TableStyleMedium2","show_first_column":true,"show_last_column":true,"show_row_stripes":false,"show_column_stripes":true}`)
//
// Note that the table must be at least two lines including the header. The
// header cells must contain strings and must be unique.
//
// Currently only one table is allowed for a StreamWriter. AddTable must be
// called after the rows are written but before Flush.
//
// See File.AddTable for details on the table format.
func (sw *StreamWriter) AddTable(hcell, vcell, format string) error {
formatSet, err := parseFormatTableSet(format)
if err != nil {
return err
}
// Make sure 'slice' is a Ptr to Slice
v := reflect.ValueOf(slice)
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Slice {
return errors.New("pointer to slice expected")
coordinates, err := areaRangeToCoordinates(hcell, vcell)
if err != nil {
return err
}
v = v.Elem()
sw.SheetData.WriteString(fmt.Sprintf(`<row r="%d">`, row))
for i := 0; i < v.Len(); i++ {
axis, err := CoordinatesToCellName(col+i, row)
sortCoordinates(coordinates)
// Correct the minimum number of rows, the table at least two lines.
if coordinates[1] == coordinates[3] {
coordinates[3]++
}
// Correct table reference coordinate area, such correct C1:B3 to B1:C3.
ref, err := sw.File.coordinatesToAreaRef(coordinates)
if err != nil {
return err
}
// create table columns using the first row
tableHeaders, err := sw.getRowValues(coordinates[1], coordinates[0], coordinates[2])
if err != nil {
return err
}
tableColumn := make([]*xlsxTableColumn, len(tableHeaders))
for i, name := range tableHeaders {
tableColumn[i] = &xlsxTableColumn{
ID: i + 1,
Name: name,
}
}
tableID := sw.File.countTables() + 1
name := formatSet.TableName
if name == "" {
name = "Table" + strconv.Itoa(tableID)
}
table := xlsxTable{
XMLNS: NameSpaceSpreadSheet,
ID: tableID,
Name: name,
DisplayName: name,
Ref: ref,
AutoFilter: &xlsxAutoFilter{
Ref: ref,
},
TableColumns: &xlsxTableColumns{
Count: len(tableColumn),
TableColumn: tableColumn,
},
TableStyleInfo: &xlsxTableStyleInfo{
Name: formatSet.TableStyle,
ShowFirstColumn: formatSet.ShowFirstColumn,
ShowLastColumn: formatSet.ShowLastColumn,
ShowRowStripes: formatSet.ShowRowStripes,
ShowColumnStripes: formatSet.ShowColumnStripes,
},
}
sheetRelationshipsTableXML := "../tables/table" + strconv.Itoa(tableID) + ".xml"
tableXML := strings.Replace(sheetRelationshipsTableXML, "..", "xl", -1)
// Add first table for given sheet.
sheetPath, _ := sw.File.sheetMap[trimSheetName(sw.Sheet)]
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels"
rID := sw.File.addRels(sheetRels, SourceRelationshipTable, sheetRelationshipsTableXML, "")
sw.tableParts = fmt.Sprintf(`<tableParts count="1"><tablePart r:id="rId%d"></tablePart></tableParts>`, rID)
sw.File.addContentTypePart(tableID, "table")
b, _ := xml.Marshal(table)
sw.File.saveFileList(tableXML, b)
return nil
}
// Extract values from a row in the StreamWriter.
func (sw *StreamWriter) getRowValues(hrow, hcol, vcol int) (res []string, err error) {
res = make([]string, vcol-hcol+1)
r, err := sw.rawData.Reader()
if err != nil {
return nil, err
}
dec := xml.NewDecoder(r)
for {
token, err := dec.Token()
if err == io.EOF {
return res, nil
}
if err != nil {
return err
return nil, err
}
switch val := v.Index(i).Interface().(type) {
case int:
sw.SheetData.WriteString(fmt.Sprintf(`<c r="%s"><v>%d</v></c>`, axis, val))
case string:
sw.SheetData.WriteString(sw.setCellStr(axis, val))
default:
sw.SheetData.WriteString(sw.setCellStr(axis, fmt.Sprint(val)))
startElement, ok := getRowElement(token, hrow)
if !ok {
continue
}
}
sw.SheetData.WriteString(`</row>`)
// Try to use local storage
chunk := 1 << 24
if sw.SheetData.Len() >= chunk {
if sw.tmpFile == nil {
err := sw.createTmp()
// decode cells
var row xlsxRow
if err := dec.DecodeElement(&row, &startElement); err != nil {
return nil, err
}
for _, c := range row.C {
col, _, err := CellNameToCoordinates(c.R)
if err != nil {
// can not use local storage
return nil
return nil, err
}
if col < hcol || col > vcol {
continue
}
res[col-hcol] = c.V
}
// use local storage
_, err := sw.tmpFile.Write(sw.SheetData.Bytes())
if err != nil {
return nil
return res, nil
}
}
// Check if the token is an XLSX row with the matching row number.
func getRowElement(token xml.Token, hrow int) (startElement xml.StartElement, ok bool) {
startElement, ok = token.(xml.StartElement)
if !ok {
return
}
ok = startElement.Name.Local == "row"
if !ok {
return
}
ok = false
for _, attr := range startElement.Attr {
if attr.Name.Local != "r" {
continue
}
row, _ := strconv.Atoi(attr.Value)
if row == hrow {
ok = true
return
}
sw.SheetData.Reset()
}
return err
return
}
// Flush ending the streaming writing process.
func (sw *StreamWriter) Flush() error {
sw.SheetData.WriteString(`</sheetData>`)
// Cell can be used directly in StreamWriter.SetRow to specify a style and
// a value.
type Cell struct {
StyleID int
Value interface{}
}
ws, err := sw.File.workSheetReader(sw.Sheet)
// SetRow writes an array to stream rows by giving a worksheet name, starting
// coordinate and a pointer to an array of values. Note that you must call the
// 'Flush' method to end the streaming writing process.
//
// As a special case, if Cell is used as a value, then the Cell.StyleID will be
// applied to that cell.
func (sw *StreamWriter) SetRow(axis string, values []interface{}) error {
col, row, err := CellNameToCoordinates(axis)
if err != nil {
return err
}
sheetXML := fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID)
delete(sw.File.Sheet, sheetXML)
delete(sw.File.checked, sheetXML)
var sheetDataByte []byte
if sw.tmpFile != nil {
// close the local storage file
if err = sw.tmpFile.Close(); err != nil {
return err
}
file, err := os.Open(sw.tmpFile.Name())
fmt.Fprintf(&sw.rawData, `<row r="%d">`, row)
for i, val := range values {
axis, err := CoordinatesToCellName(col+i, row)
if err != nil {
return err
}
sheetDataByte, err = ioutil.ReadAll(file)
if err != nil {
return err
c := xlsxC{R: axis}
if v, ok := val.(Cell); ok {
c.S = v.StyleID
val = v.Value
} else if v, ok := val.(*Cell); ok && v != nil {
c.S = v.StyleID
val = v.Value
}
if err := file.Close(); err != nil {
return err
switch val := val.(type) {
case int:
c.T, c.V = setCellInt(val)
case int8:
c.T, c.V = setCellInt(int(val))
case int16:
c.T, c.V = setCellInt(int(val))
case int32:
c.T, c.V = setCellInt(int(val))
case int64:
c.T, c.V = setCellInt(int(val))
case uint:
c.T, c.V = setCellInt(int(val))
case uint8:
c.T, c.V = setCellInt(int(val))
case uint16:
c.T, c.V = setCellInt(int(val))
case uint32:
c.T, c.V = setCellInt(int(val))
case uint64:
c.T, c.V = setCellInt(int(val))
case float32:
c.T, c.V = setCellFloat(float64(val), -1, 32)
case float64:
c.T, c.V = setCellFloat(val, -1, 64)
case string:
c.T, c.V, c.XMLSpace = setCellStr(val)
case []byte:
c.T, c.V, c.XMLSpace = setCellStr(string(val))
case time.Duration:
c.T, c.V = setCellDuration(val)
case time.Time:
c.T, c.V, _, err = setCellTime(val)
case bool:
c.T, c.V = setCellBool(val)
case nil:
c.T, c.V, c.XMLSpace = setCellStr("")
default:
c.T, c.V, c.XMLSpace = setCellStr(fmt.Sprint(val))
}
err = os.Remove(sw.tmpFile.Name())
if err != nil {
return err
}
writeCell(&sw.rawData, c)
}
sw.rawData.WriteString(`</row>`)
return sw.rawData.Sync()
}
sheetDataByte = append(sheetDataByte, sw.SheetData.Bytes()...)
replaceMap := map[string][]byte{
"XMLName": {},
"SheetData": sheetDataByte,
func writeCell(buf *bufferedWriter, c xlsxC) {
buf.WriteString(`<c`)
if c.XMLSpace.Value != "" {
fmt.Fprintf(buf, ` xml:%s="%s"`, c.XMLSpace.Name.Local, c.XMLSpace.Value)
}
sw.SheetData.Reset()
sw.File.XLSX[fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID)] =
StreamMarshalSheet(ws, replaceMap)
return err
fmt.Fprintf(buf, ` r="%s"`, c.R)
if c.S != 0 {
fmt.Fprintf(buf, ` s="%d"`, c.S)
}
if c.T != "" {
fmt.Fprintf(buf, ` t="%s"`, c.T)
}
buf.WriteString(`>`)
if c.V != "" {
buf.WriteString(`<v>`)
xml.EscapeText(buf, []byte(c.V))
buf.WriteString(`</v>`)
}
buf.WriteString(`</c>`)
}
// createTmp creates a temporary file in the operating system default
// temporary directory.
func (sw *StreamWriter) createTmp() (err error) {
sw.tmpFile, err = ioutil.TempFile(os.TempDir(), "excelize-")
return err
// Flush ending the streaming writing process.
func (sw *StreamWriter) Flush() error {
sw.rawData.WriteString(`</sheetData>`)
sw.rawData.WriteString(sw.tableParts)
sw.rawData.WriteString(`</worksheet>`)
if err := sw.rawData.Flush(); err != nil {
return err
}
sheetXML := fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID)
delete(sw.File.Sheet, sheetXML)
delete(sw.File.checked, sheetXML)
defer sw.rawData.Close()
b, err := sw.rawData.Bytes()
if err != nil {
return err
}
sw.File.XLSX[sheetXML] = b
return nil
}
// StreamMarshalSheet provides method to serialization worksheets by field as
// streaming.
func StreamMarshalSheet(ws *xlsxWorksheet, replaceMap map[string][]byte) []byte {
// bulkAppendOtherFields bulk-appends fields in a worksheet, skipping the
// specified field names.
func bulkAppendOtherFields(w io.Writer, ws *xlsxWorksheet, skip ...string) {
skipMap := make(map[string]struct{})
for _, name := range skip {
skipMap[name] = struct{}{}
}
s := reflect.ValueOf(ws).Elem()
typeOfT := s.Type()
var marshalResult []byte
marshalResult = append(marshalResult, []byte(XMLHeader+`<worksheet`+templateNamespaceIDMap)...)
enc := xml.NewEncoder(w)
for i := 0; i < s.NumField(); i++ {
content, ok := replaceMap[typeOfT.Field(i).Name]
if ok {
marshalResult = append(marshalResult, content...)
f := s.Field(i)
if _, ok := skipMap[typeOfT.Field(i).Name]; ok {
continue
}
out, _ := xml.Marshal(s.Field(i).Interface())
marshalResult = append(marshalResult, out...)
enc.Encode(f.Interface())
}
marshalResult = append(marshalResult, []byte(`</worksheet>`)...)
return marshalResult
}
// setCellStr provides a function to set string type value of a cell as
// streaming. Total number of characters that a cell can contain 32767
// characters.
func (sw *StreamWriter) setCellStr(axis, value string) string {
if len(value) > 32767 {
value = value[0:32767]
// bufferedWriter uses a temp file to store an extended buffer. Writes are
// always made to an in-memory buffer, which will always succeed. The buffer
// is written to the temp file with Sync, which may return an error. Therefore,
// Sync should be periodically called and the error checked.
type bufferedWriter struct {
tmp *os.File
buf bytes.Buffer
}
// Write to the in-memory buffer. The err is always nil.
func (bw *bufferedWriter) Write(p []byte) (n int, err error) {
return bw.buf.Write(p)
}
// WriteString wites to the in-memory buffer. The err is always nil.
func (bw *bufferedWriter) WriteString(p string) (n int, err error) {
return bw.buf.WriteString(p)
}
// Reader provides read-access to the underlying buffer/file.
func (bw *bufferedWriter) Reader() (io.Reader, error) {
if bw.tmp == nil {
return bytes.NewReader(bw.buf.Bytes()), nil
}
if err := bw.Flush(); err != nil {
return nil, err
}
fi, err := bw.tmp.Stat()
if err != nil {
return nil, err
}
// Leading and ending space(s) character detection.
if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) {
return fmt.Sprintf(`<c xml:space="preserve" r="%s" t="str"><v>%s</v></c>`, axis, value)
// os.File.ReadAt does not affect the cursor position and is safe to use here
return io.NewSectionReader(bw.tmp, 0, fi.Size()), nil
}
// Bytes returns the entire content of the bufferedWriter. If a temp file is
// used, Bytes will efficiently allocate a buffer to prevent re-allocations.
func (bw *bufferedWriter) Bytes() ([]byte, error) {
if bw.tmp == nil {
return bw.buf.Bytes(), nil
}
if err := bw.Flush(); err != nil {
return nil, err
}
var buf bytes.Buffer
if fi, err := bw.tmp.Stat(); err == nil {
if size := fi.Size() + bytes.MinRead; size > bytes.MinRead {
if int64(int(size)) == size {
buf.Grow(int(size))
} else {
return nil, bytes.ErrTooLarge
}
}
}
if _, err := bw.tmp.Seek(0, 0); err != nil {
return nil, err
}
_, err := buf.ReadFrom(bw.tmp)
return buf.Bytes(), err
}
// Sync will write the in-memory buffer to a temp file, if the in-memory buffer
// has grown large enough. Any error will be returned.
func (bw *bufferedWriter) Sync() (err error) {
// Try to use local storage
const chunk = 1 << 24
if bw.buf.Len() < chunk {
return nil
}
if bw.tmp == nil {
bw.tmp, err = ioutil.TempFile(os.TempDir(), "excelize-")
if err != nil {
// can not use local storage
return nil
}
}
return bw.Flush()
}
// Flush the entire in-memory buffer to the temp file, if a temp file is being
// used.
func (bw *bufferedWriter) Flush() error {
if bw.tmp == nil {
return nil
}
_, err := bw.buf.WriteTo(bw.tmp)
if err != nil {
return err
}
bw.buf.Reset()
return nil
}
// Close the underlying temp file and reset the in-memory buffer.
func (bw *bufferedWriter) Close() error {
bw.buf.Reset()
if bw.tmp == nil {
return nil
}
return fmt.Sprintf(`<c r="%s" t="str"><v>%s</v></c>`, axis, value)
defer os.Remove(bw.tmp.Name())
return bw.tmp.Close()
}
package excelize
import (
"encoding/xml"
"fmt"
"math/rand"
"path/filepath"
"strings"
......@@ -9,6 +11,25 @@ import (
"github.com/stretchr/testify/assert"
)
func BenchmarkStreamWriter(b *testing.B) {
file := NewFile()
row := make([]interface{}, 10)
for colID := 0; colID < 10; colID++ {
row[colID] = colID
}
for n := 0; n < b.N; n++ {
streamWriter, _ := file.NewStreamWriter("Sheet1")
for rowID := 10; rowID <= 110; rowID++ {
cell, _ := CoordinatesToCellName(1, rowID)
streamWriter.SetRow(cell, row)
}
}
b.ReportAllocs()
}
func TestStreamWriter(t *testing.T) {
file := NewFile()
streamWriter, err := file.NewStreamWriter("Sheet1")
......@@ -17,16 +38,16 @@ func TestStreamWriter(t *testing.T) {
// Test max characters in a cell.
row := make([]interface{}, 1)
row[0] = strings.Repeat("c", 32769)
assert.NoError(t, streamWriter.SetRow("A1", &row))
assert.NoError(t, streamWriter.SetRow("A1", row))
// Test leading and ending space(s) character characters in a cell.
row = make([]interface{}, 1)
row[0] = " characters"
assert.NoError(t, streamWriter.SetRow("A2", &row))
assert.NoError(t, streamWriter.SetRow("A2", row))
row = make([]interface{}, 1)
row[0] = []byte("Word")
assert.NoError(t, streamWriter.SetRow("A3", &row))
assert.NoError(t, streamWriter.SetRow("A3", row))
for rowID := 10; rowID <= 51200; rowID++ {
row := make([]interface{}, 50)
......@@ -34,26 +55,13 @@ func TestStreamWriter(t *testing.T) {
row[colID] = rand.Intn(640000)
}
cell, _ := CoordinatesToCellName(1, rowID)
assert.NoError(t, streamWriter.SetRow(cell, &row))
assert.NoError(t, streamWriter.SetRow(cell, row))
}
assert.NoError(t, streamWriter.Flush())
// Save xlsx file by the given path.
assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx")))
// Test error exceptions
_, err = file.NewStreamWriter("SheetN")
assert.EqualError(t, err, "sheet SheetN is not exist")
}
func TestFlush(t *testing.T) {
// Test error exceptions
file := NewFile()
streamWriter, err := file.NewStreamWriter("Sheet1")
assert.NoError(t, err)
streamWriter.Sheet = "SheetN"
assert.EqualError(t, streamWriter.Flush(), "sheet SheetN is not exist")
// Test close temporary file error
file = NewFile()
streamWriter, err = file.NewStreamWriter("Sheet1")
......@@ -64,17 +72,49 @@ func TestFlush(t *testing.T) {
row[colID] = rand.Intn(640000)
}
cell, _ := CoordinatesToCellName(1, rowID)
assert.NoError(t, streamWriter.SetRow(cell, &row))
assert.NoError(t, streamWriter.SetRow(cell, row))
}
assert.NoError(t, streamWriter.tmpFile.Close())
assert.NoError(t, streamWriter.rawData.Close())
assert.Error(t, streamWriter.Flush())
}
func TestStreamTable(t *testing.T) {
file := NewFile()
streamWriter, err := file.NewStreamWriter("Sheet1")
assert.NoError(t, err)
// Write some rows. We want enough rows to force a temp file (>16MB).
assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"}))
row := []interface{}{1, 2, 3}
for r := 2; r < 10000; r++ {
assert.NoError(t, streamWriter.SetRow(fmt.Sprintf("A%d", r), row))
}
// Write a table.
assert.NoError(t, streamWriter.AddTable("A1", "C2", ``))
assert.NoError(t, streamWriter.Flush())
// Verify the table has names.
var table xlsxTable
assert.NoError(t, xml.Unmarshal(file.XLSX["xl/tables/table1.xml"], &table))
assert.Equal(t, "A", table.TableColumns.TableColumn[0].Name)
assert.Equal(t, "B", table.TableColumns.TableColumn[1].Name)
assert.Equal(t, "C", table.TableColumns.TableColumn[2].Name)
}
func TestNewStreamWriter(t *testing.T) {
// Test error exceptions
file := NewFile()
_, err := file.NewStreamWriter("Sheet1")
assert.NoError(t, err)
_, err = file.NewStreamWriter("SheetN")
assert.EqualError(t, err, "sheet SheetN is not exist")
}
func TestSetRow(t *testing.T) {
// Test error exceptions
file := NewFile()
streamWriter, err := file.NewStreamWriter("Sheet1")
assert.NoError(t, err)
assert.EqualError(t, streamWriter.SetRow("A", &[]interface{}{}), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
assert.EqualError(t, streamWriter.SetRow("A1", []interface{}{}), `pointer to slice expected`)
assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册