mirror of
https://github.com/pbnjay/grate.git
synced 2024-12-13 13:58:27 +02:00
why not? initial implementation of xlsx
This commit is contained in:
parent
ee3b4224e0
commit
8812c44704
82
xlsx/formats.go
Normal file
82
xlsx/formats.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package xlsx
|
||||||
|
|
||||||
|
var builtInFormats = map[uint16]string{
|
||||||
|
0: `General`,
|
||||||
|
1: `0`,
|
||||||
|
2: `0.00`,
|
||||||
|
3: `#,##0`,
|
||||||
|
4: `#,##0.00`,
|
||||||
|
9: `0%`,
|
||||||
|
10: `0.00%`,
|
||||||
|
|
||||||
|
11: `0.00E+00`,
|
||||||
|
12: `# ?/?`,
|
||||||
|
13: `# ??/??`,
|
||||||
|
14: `mm-dd-yy`,
|
||||||
|
15: `d-mmm-yy`,
|
||||||
|
16: `d-mmm`,
|
||||||
|
17: `mmm-yy`,
|
||||||
|
18: `h:mm AM/PM`,
|
||||||
|
19: `h:mm:ss AM/PM`,
|
||||||
|
20: `h:mm`,
|
||||||
|
21: `h:mm:ss`,
|
||||||
|
22: `m/d/yy h:mm`,
|
||||||
|
37: `#,##0 ;(#,##0)`,
|
||||||
|
38: `#,##0 ;[Red](#,##0)`,
|
||||||
|
39: `#,##0.00;(#,##0.00)`,
|
||||||
|
40: `#,##0.00;[Red](#,##0.00)`,
|
||||||
|
|
||||||
|
41: `_(* #,##0_);_(* \(#,##0\);_(* "-"_);_(@_)`,
|
||||||
|
42: `_("$"* #,##0_);_("$"* \(#,##0\);_("$"* "-"_);_(@_)`,
|
||||||
|
43: `_(* #,##0.00_);_(* \(#,##0.00\);_(* "-"??_);_(@_)`,
|
||||||
|
44: `_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)`,
|
||||||
|
|
||||||
|
45: `mm:ss`,
|
||||||
|
46: `[h]:mm:ss`,
|
||||||
|
47: `mmss.0`,
|
||||||
|
48: `##0.0E+0`,
|
||||||
|
49: `@`,
|
||||||
|
|
||||||
|
// zh-cn format codes
|
||||||
|
27: `yyyy"年"m"月"`,
|
||||||
|
28: `m"月"d"日"`,
|
||||||
|
29: `m"月"d"日"`,
|
||||||
|
30: `m-d-yy`,
|
||||||
|
31: `yyyy"年"m"月"d"日"`,
|
||||||
|
32: `h"时"mm"分"`,
|
||||||
|
33: `h"时"mm"分"ss"秒"`,
|
||||||
|
34: `上午/下午 h"时"mm"分"`,
|
||||||
|
35: `上午/下午 h"时"mm"分"ss"秒"`,
|
||||||
|
36: `yyyy"年"m"月"`,
|
||||||
|
50: `yyyy"年"m"月"`,
|
||||||
|
51: `m"月"d"日"`,
|
||||||
|
52: `yyyy"年"m"月"`,
|
||||||
|
53: `m"月"d"日"`,
|
||||||
|
54: `m"月"d"日"`,
|
||||||
|
55: `上午/下午 h"时"mm"分"`,
|
||||||
|
56: `上午/下午 h"时"mm"分"ss"秒`,
|
||||||
|
57: `yyyy"年"m"月"`,
|
||||||
|
58: `m"月"d"日"`,
|
||||||
|
|
||||||
|
// th-th format codes
|
||||||
|
59: `t0`,
|
||||||
|
60: `t0.00`,
|
||||||
|
61: `t#,##0`,
|
||||||
|
62: `t#,##0.00`,
|
||||||
|
67: `t0%`,
|
||||||
|
68: `t0.00%`,
|
||||||
|
69: `t# ?/?`,
|
||||||
|
70: `t# ??/??`,
|
||||||
|
// th format code, but translated to aid the parser
|
||||||
|
71: `d/m/yyyy`, // `ว/ด/ปปปป`,
|
||||||
|
72: `d-mmm-yy`, // `ว-ดดด-ปป`,
|
||||||
|
73: `d-mmm`, // `ว-ดดด`,
|
||||||
|
74: `mmm-yy`, // `ดดด-ปป`,
|
||||||
|
75: `h:mm`, // `ช:นน`,
|
||||||
|
76: `h:mm:ss`, // `ช:นน:ทท`,
|
||||||
|
77: `d/m/yyyy h:mm`, // `ว/ด/ปปปป ช:นน`,
|
||||||
|
78: `mm:ss`, // `นน:ทท`,
|
||||||
|
79: `[h]:mm:ss`, // `[ช]:นน:ทท`,
|
||||||
|
80: `mm:ss.0`, // `นน:ทท.0`,
|
||||||
|
81: `d/m/bb`, // `d/m/bb`,
|
||||||
|
}
|
269
xlsx/sheets.go
Normal file
269
xlsx/sheets.go
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
package xlsx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Sheet struct {
|
||||||
|
d *Document
|
||||||
|
relID string
|
||||||
|
name string
|
||||||
|
docname string
|
||||||
|
|
||||||
|
err error
|
||||||
|
|
||||||
|
minRow int
|
||||||
|
maxRow int
|
||||||
|
minCol int
|
||||||
|
maxCol int
|
||||||
|
rows []*row
|
||||||
|
empty bool
|
||||||
|
|
||||||
|
iterRow int
|
||||||
|
}
|
||||||
|
|
||||||
|
type row struct {
|
||||||
|
// each value must be one of: int, float64, string, or time.Time
|
||||||
|
cols []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sheet) parseSheet() error {
|
||||||
|
linkmap := make(map[string]string)
|
||||||
|
base := filepath.Base(s.docname)
|
||||||
|
sub := strings.TrimSuffix(s.docname, base)
|
||||||
|
relsname := filepath.Join(sub, "_rels", base+".rels")
|
||||||
|
dec, clo, err := s.d.openXML(relsname)
|
||||||
|
if err == nil {
|
||||||
|
// rels might not exist for every sheet
|
||||||
|
tok, err := dec.Token()
|
||||||
|
for ; err == nil; tok, err = dec.Token() {
|
||||||
|
if v, ok := tok.(xml.StartElement); ok && v.Name.Local == "Relationship" {
|
||||||
|
ax := attrMap(v.Attr)
|
||||||
|
if ax["TargetMode"] == "External" && ax["Type"] == "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" {
|
||||||
|
linkmap[ax["Id"]] = ax["Target"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clo.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
dec, clo, err = s.d.openXML(s.docname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer clo.Close()
|
||||||
|
|
||||||
|
currentCellType := BlankCellType
|
||||||
|
currentCell := ""
|
||||||
|
numFormat := ""
|
||||||
|
tok, err := dec.Token()
|
||||||
|
for ; err == nil; tok, err = dec.Token() {
|
||||||
|
switch v := tok.(type) {
|
||||||
|
case xml.CharData:
|
||||||
|
if currentCell == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c, r := refToIndexes(currentCell)
|
||||||
|
if c >= 0 && r >= 0 {
|
||||||
|
var val interface{} = string(v)
|
||||||
|
switch currentCellType {
|
||||||
|
case BooleanCellType:
|
||||||
|
if v[0] == '1' {
|
||||||
|
val = true
|
||||||
|
} else {
|
||||||
|
val = false
|
||||||
|
}
|
||||||
|
case DateCellType:
|
||||||
|
log.Println("CELL DATE", val, numFormat)
|
||||||
|
case NumberCellType:
|
||||||
|
fval, err := strconv.ParseFloat(string(v), 64)
|
||||||
|
if err == nil {
|
||||||
|
val = fval
|
||||||
|
}
|
||||||
|
//log.Println("CELL NUMBER", val, numFormat)
|
||||||
|
case SharedStringCellType:
|
||||||
|
//log.Println("CELL SHSTR", val, currentCellType, numFormat)
|
||||||
|
si, _ := strconv.ParseInt(string(v), 10, 64)
|
||||||
|
val = s.d.strings[si]
|
||||||
|
case BlankCellType:
|
||||||
|
//log.Println("CELL BLANK")
|
||||||
|
// don't place any values
|
||||||
|
continue
|
||||||
|
case ErrorCellType, FormulaStringCellType, InlineStringCellType:
|
||||||
|
//log.Println("CELL ERR/FORM/INLINE", val, currentCellType)
|
||||||
|
default:
|
||||||
|
log.Println("CELL UNKNOWN", val, currentCellType, numFormat)
|
||||||
|
}
|
||||||
|
s.placeValue(r, c, val)
|
||||||
|
} else {
|
||||||
|
//log.Println("FAIL row/col: ", currentCell)
|
||||||
|
}
|
||||||
|
case xml.StartElement:
|
||||||
|
ax := attrMap(v.Attr)
|
||||||
|
switch v.Name.Local {
|
||||||
|
case "dimension":
|
||||||
|
if ax["ref"] == "A1" {
|
||||||
|
// short-circuit empty sheet
|
||||||
|
s.minCol, s.minRow = 0, 0
|
||||||
|
s.maxCol, s.maxRow = 1, 1
|
||||||
|
s.empty = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dims := strings.Split(ax["ref"], ":")
|
||||||
|
s.minCol, s.minRow = refToIndexes(dims[0])
|
||||||
|
s.maxCol, s.maxRow = refToIndexes(dims[1])
|
||||||
|
//log.Println("DIMENSION:", s.minRow, s.minCol, ">", s.maxRow, s.maxCol)
|
||||||
|
case "row":
|
||||||
|
//currentRow = ax["r"] // unsigned int row index
|
||||||
|
//log.Println("ROW", currentRow)
|
||||||
|
case "c":
|
||||||
|
currentCellType = CellType(ax["t"])
|
||||||
|
if currentCellType == BlankCellType {
|
||||||
|
currentCellType = NumberCellType
|
||||||
|
}
|
||||||
|
currentCell = ax["r"] // always an A1 style reference
|
||||||
|
style := ax["s"]
|
||||||
|
sid, _ := strconv.ParseInt(style, 10, 64)
|
||||||
|
numFormat = s.d.xfs[sid] // unsigned integer lookup
|
||||||
|
//log.Println("CELL", currentCell, sid, numFormat, currentCellType)
|
||||||
|
case "v":
|
||||||
|
//log.Println("CELL VALUE", ax)
|
||||||
|
|
||||||
|
case "mergeCell":
|
||||||
|
dims := strings.Split(ax["ref"], ":")
|
||||||
|
startCol, startRow := refToIndexes(dims[0])
|
||||||
|
endCol, endRow := refToIndexes(dims[1])
|
||||||
|
for r := startRow; r <= endRow; r++ {
|
||||||
|
for c := startCol; c <= endCol; c++ {
|
||||||
|
if r == startRow && c == startCol {
|
||||||
|
// has data already!
|
||||||
|
} else if c == startCol {
|
||||||
|
// first and last column MAY be the same
|
||||||
|
if r == endRow {
|
||||||
|
s.placeValue(r, c, endRowMerged)
|
||||||
|
} else {
|
||||||
|
s.placeValue(r, c, continueRowMerged)
|
||||||
|
}
|
||||||
|
} else if c == endCol {
|
||||||
|
// first and last column are NOT the same
|
||||||
|
s.placeValue(r, c, endColumnMerged)
|
||||||
|
} else {
|
||||||
|
s.placeValue(r, c, continueColumnMerged)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "hyperlink":
|
||||||
|
col, row := refToIndexes(ax["ref"])
|
||||||
|
link := linkmap[ax["id"]]
|
||||||
|
if len(s.rows) > row && len(s.rows[row].cols) > col {
|
||||||
|
if sstr, ok := s.rows[row].cols[col].(string); ok {
|
||||||
|
link = sstr + " <" + link + ">"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.placeValue(row, col, link)
|
||||||
|
|
||||||
|
case "mergeCells", "hyperlinks":
|
||||||
|
// NB don't need these outer containers
|
||||||
|
case "f":
|
||||||
|
//log.Println("start: ", v.Name.Local, v.Attr)
|
||||||
|
default:
|
||||||
|
//log.Println("start: ", v.Name.Local, v.Attr)
|
||||||
|
}
|
||||||
|
case xml.EndElement:
|
||||||
|
|
||||||
|
switch v.Name.Local {
|
||||||
|
case "c":
|
||||||
|
currentCell = ""
|
||||||
|
case "row":
|
||||||
|
//currentRow = ""
|
||||||
|
}
|
||||||
|
//log.Println(" end: ", v.Name.Local)
|
||||||
|
default:
|
||||||
|
//log.Printf("%T %+v", tok, tok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sheet) placeValue(rowIndex, colIndex int, val interface{}) {
|
||||||
|
if colIndex > s.maxCol || rowIndex > s.maxRow {
|
||||||
|
// invalid
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure we always have a complete matrix
|
||||||
|
for len(s.rows) <= rowIndex {
|
||||||
|
emptyRow := make([]interface{}, s.maxCol+1)
|
||||||
|
for i := 0; i <= s.maxCol; i++ {
|
||||||
|
emptyRow[i] = staticBlank
|
||||||
|
}
|
||||||
|
s.rows = append(s.rows, &row{emptyRow})
|
||||||
|
}
|
||||||
|
s.empty = false
|
||||||
|
s.rows[rowIndex].cols[colIndex] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next advances to the next row of content.
|
||||||
|
// It MUST be called prior to any Scan().
|
||||||
|
func (s *Sheet) Next() bool {
|
||||||
|
s.iterRow++
|
||||||
|
return s.iterRow < len(s.rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sheet) Strings() []string {
|
||||||
|
currow := s.rows[s.iterRow]
|
||||||
|
res := make([]string, len(currow.cols))
|
||||||
|
for i, col := range currow.cols {
|
||||||
|
res[i] = fmt.Sprint(col)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan extracts values from the row into the provided arguments
|
||||||
|
// Arguments must be pointers to one of 5 supported types:
|
||||||
|
// bool, int, float64, string, or time.Time
|
||||||
|
func (s *Sheet) Scan(args ...interface{}) error {
|
||||||
|
currow := s.rows[s.iterRow]
|
||||||
|
|
||||||
|
for i, a := range args {
|
||||||
|
switch v := a.(type) {
|
||||||
|
case *bool:
|
||||||
|
*v = currow.cols[i].(bool)
|
||||||
|
case *int:
|
||||||
|
*v = currow.cols[i].(int)
|
||||||
|
case *float64:
|
||||||
|
*v = currow.cols[i].(float64)
|
||||||
|
case *string:
|
||||||
|
*v = currow.cols[i].(string)
|
||||||
|
case *time.Time:
|
||||||
|
*v = currow.cols[i].(time.Time)
|
||||||
|
default:
|
||||||
|
return ErrInvalidType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sheet) IsEmpty() bool {
|
||||||
|
return s.empty
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err returns the last error that occured.
|
||||||
|
func (s *Sheet) Err() error {
|
||||||
|
return s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrInvalidType is returned by Scan for invalid arguments.
|
||||||
|
var ErrInvalidType = errors.New("xlsx: Scan only supports *bool, *int, *float64, *string, *time.Time arguments")
|
32
xlsx/simple_test.go
Normal file
32
xlsx/simple_test.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package xlsx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func noTestOpen(t *testing.T) {
|
||||||
|
_, err := Open("test.xlsx")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpen2(t *testing.T) {
|
||||||
|
wb, err := Open("test2.xlsx")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range wb.Sheets() {
|
||||||
|
//log.Println(s)
|
||||||
|
sheet, err := wb.Get(s)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for sheet.Next() {
|
||||||
|
sheet.Strings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
88
xlsx/types.go
Normal file
88
xlsx/types.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package xlsx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CellType string
|
||||||
|
|
||||||
|
// CellTypes define data type in section 18.18.11
|
||||||
|
const (
|
||||||
|
BlankCellType CellType = ""
|
||||||
|
BooleanCellType CellType = "b"
|
||||||
|
DateCellType CellType = "d"
|
||||||
|
ErrorCellType CellType = "e"
|
||||||
|
NumberCellType CellType = "n"
|
||||||
|
SharedStringCellType CellType = "s"
|
||||||
|
FormulaStringCellType CellType = "str"
|
||||||
|
InlineStringCellType CellType = "inlineStr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type staticCellType rune
|
||||||
|
|
||||||
|
const (
|
||||||
|
staticBlank staticCellType = 0
|
||||||
|
|
||||||
|
// marks a continuation column within a merged cell.
|
||||||
|
continueColumnMerged staticCellType = '→'
|
||||||
|
// marks the last column of a merged cell.
|
||||||
|
endColumnMerged staticCellType = '⇥'
|
||||||
|
|
||||||
|
// marks a continuation row within a merged cell.
|
||||||
|
continueRowMerged staticCellType = '↓'
|
||||||
|
// marks the last row of a merged cell.
|
||||||
|
endRowMerged staticCellType = '⤓'
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s staticCellType) String() string {
|
||||||
|
if s == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string([]rune{rune(s)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the 0-based index of the column string:
|
||||||
|
// "A"=0, "B"=1, "AA"=26, "BB"=53
|
||||||
|
func col2int(col string) int {
|
||||||
|
idx := 0
|
||||||
|
for _, c := range col {
|
||||||
|
idx *= 26
|
||||||
|
idx += int(c - '@')
|
||||||
|
}
|
||||||
|
return idx - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func refToIndexes(r string) (column, row int) {
|
||||||
|
if len(r) < 2 {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
i1 := strings.IndexAny(r, "0123456789")
|
||||||
|
if i1 <= 0 {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// A1 Reference mode
|
||||||
|
col1 := r[:i1]
|
||||||
|
i2 := strings.IndexByte(r[i1:], 'C')
|
||||||
|
if i2 == -1 {
|
||||||
|
rn, _ := strconv.ParseInt(r[i1:], 10, 64)
|
||||||
|
return col2int(col1), int(rn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// R1C1 Reference Mode
|
||||||
|
col1 = r[i1:i2]
|
||||||
|
row1 := r[i2+1:]
|
||||||
|
cn, _ := strconv.ParseInt(col1, 10, 64)
|
||||||
|
rn, _ := strconv.ParseInt(row1, 10, 64)
|
||||||
|
return int(cn), int(rn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func attrMap(attrs []xml.Attr) map[string]string {
|
||||||
|
m := make(map[string]string, len(attrs))
|
||||||
|
for _, a := range attrs {
|
||||||
|
m[a.Name.Local] = a.Value
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
167
xlsx/workbook.go
Normal file
167
xlsx/workbook.go
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
package xlsx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Document) parseRels(dec *xml.Decoder, basedir string) error {
|
||||||
|
tok, err := dec.Token()
|
||||||
|
for ; err == nil; tok, err = dec.Token() {
|
||||||
|
switch v := tok.(type) {
|
||||||
|
// the tags we're interested in are all self-closing
|
||||||
|
case xml.StartElement:
|
||||||
|
switch v.Name.Local {
|
||||||
|
case "Relationship":
|
||||||
|
vals := make(map[string]string, 5)
|
||||||
|
for _, a := range v.Attr {
|
||||||
|
vals[a.Name.Local] = a.Value
|
||||||
|
}
|
||||||
|
if _, ok := d.rels[vals["Type"]]; !ok {
|
||||||
|
d.rels[vals["Type"]] = make(map[string]string)
|
||||||
|
}
|
||||||
|
d.rels[vals["Type"]][vals["Id"]] = filepath.Join(basedir, vals["Target"])
|
||||||
|
if vals["Type"] == "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" {
|
||||||
|
d.primaryDoc = vals["Target"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) parseWorkbook(dec *xml.Decoder) error {
|
||||||
|
tok, err := dec.Token()
|
||||||
|
for ; err == nil; tok, err = dec.Token() {
|
||||||
|
switch v := tok.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
//log.Println("start: ", v.Name.Local)
|
||||||
|
|
||||||
|
switch v.Name.Local {
|
||||||
|
case "sheet":
|
||||||
|
vals := make(map[string]string, 5)
|
||||||
|
for _, a := range v.Attr {
|
||||||
|
vals[a.Name.Local] = a.Value
|
||||||
|
}
|
||||||
|
sheetID, ok1 := vals["id"]
|
||||||
|
sheetName, ok2 := vals["name"]
|
||||||
|
if !ok1 || !ok2 {
|
||||||
|
return errors.New("xlsx: invalid sheet definition")
|
||||||
|
}
|
||||||
|
s := &Sheet{
|
||||||
|
d: d,
|
||||||
|
relID: sheetID,
|
||||||
|
name: sheetName,
|
||||||
|
docname: d.rels["http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"][sheetID],
|
||||||
|
}
|
||||||
|
d.sheets = append(d.sheets, s)
|
||||||
|
}
|
||||||
|
case xml.EndElement:
|
||||||
|
//log.Println(" end: ", v.Name.Local)
|
||||||
|
default:
|
||||||
|
//log.Printf("%T %+v", tok, tok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) parseStyles(dec *xml.Decoder) error {
|
||||||
|
csxfNumFormat := []string{}
|
||||||
|
d.xfs = d.xfs[:0]
|
||||||
|
|
||||||
|
section := 0
|
||||||
|
tok, err := dec.Token()
|
||||||
|
for ; err == nil; tok, err = dec.Token() {
|
||||||
|
switch v := tok.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
attrs := attrMap(v.Attr)
|
||||||
|
|
||||||
|
switch v.Name.Local {
|
||||||
|
case "cellStyleXfs":
|
||||||
|
section = 1
|
||||||
|
case "cellXfs":
|
||||||
|
section = 2
|
||||||
|
n, _ := strconv.ParseInt(attrs["count"], 10, 64)
|
||||||
|
d.xfs = make([]string, 0, n)
|
||||||
|
|
||||||
|
case "xf":
|
||||||
|
if section == 1 {
|
||||||
|
if _, ok := attrs["applyNumberFormat"]; ok {
|
||||||
|
csxfNumFormat = append(csxfNumFormat, attrs["numFmtId"])
|
||||||
|
} else {
|
||||||
|
csxfNumFormat = append(csxfNumFormat, "-")
|
||||||
|
}
|
||||||
|
} else if section == 2 {
|
||||||
|
baseID, _ := strconv.ParseInt(attrs["xfId"], 10, 64)
|
||||||
|
thisXF := csxfNumFormat[baseID]
|
||||||
|
if _, ok := attrs["applyNumberFormat"]; ok {
|
||||||
|
thisXF = attrs["numFmtId"]
|
||||||
|
} else {
|
||||||
|
thisXF = "="
|
||||||
|
}
|
||||||
|
|
||||||
|
nfid, _ := strconv.ParseInt(thisXF, 10, 16)
|
||||||
|
thisXF = builtInFormats[uint16(nfid)]
|
||||||
|
d.xfs = append(d.xfs, thisXF)
|
||||||
|
} else {
|
||||||
|
panic("wheres is this xf??")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
//log.Println("start: ", v.Name.Local, v.Attr)
|
||||||
|
}
|
||||||
|
case xml.EndElement:
|
||||||
|
switch v.Name.Local {
|
||||||
|
case "cellStyleXfs":
|
||||||
|
section = 0
|
||||||
|
case "cellXfs":
|
||||||
|
section = 0
|
||||||
|
}
|
||||||
|
//log.Println(" end: ", v.Name.Local)
|
||||||
|
default:
|
||||||
|
//log.Printf("%T %+v", tok, tok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) parseSharedStrings(dec *xml.Decoder) error {
|
||||||
|
val := ""
|
||||||
|
tok, err := dec.Token()
|
||||||
|
for ; err == nil; tok, err = dec.Token() {
|
||||||
|
switch v := tok.(type) {
|
||||||
|
case xml.CharData:
|
||||||
|
val += string(v)
|
||||||
|
case xml.StartElement:
|
||||||
|
switch v.Name.Local {
|
||||||
|
case "si":
|
||||||
|
val = ""
|
||||||
|
default:
|
||||||
|
//log.Println("start: ", v.Name.Local)
|
||||||
|
}
|
||||||
|
case xml.EndElement:
|
||||||
|
if v.Name.Local == "si" {
|
||||||
|
d.strings = append(d.strings, val)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
//log.Println(" end: ", v.Name.Local)
|
||||||
|
default:
|
||||||
|
//log.Printf("%T %+v", tok, tok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
157
xlsx/xlsx.go
Normal file
157
xlsx/xlsx.go
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
package xlsx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Document contains an Office Open XML document.
|
||||||
|
type Document struct {
|
||||||
|
filename string
|
||||||
|
r *zip.Reader
|
||||||
|
primaryDoc string
|
||||||
|
|
||||||
|
// type => id => filename
|
||||||
|
rels map[string]map[string]string
|
||||||
|
sheets []*Sheet
|
||||||
|
strings []string
|
||||||
|
xfs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Open(filename string) (*Document, error) {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
info, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
z, err := zip.NewReader(f, info.Size())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
d := &Document{
|
||||||
|
filename: filename,
|
||||||
|
r: z,
|
||||||
|
}
|
||||||
|
|
||||||
|
d.rels = make(map[string]map[string]string, 4)
|
||||||
|
|
||||||
|
// parse the primary relationships
|
||||||
|
dec, c, err := d.openXML("_rels/.rels")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = d.parseRels(dec, "")
|
||||||
|
c.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if d.primaryDoc == "" {
|
||||||
|
return nil, errors.New("xlsx: invalid document")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the secondary relationships to primary doc
|
||||||
|
base := filepath.Base(d.primaryDoc)
|
||||||
|
sub := strings.TrimSuffix(d.primaryDoc, base)
|
||||||
|
relfn := filepath.Join(sub, "_rels", base+".rels")
|
||||||
|
dec, c, err = d.openXML(relfn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = d.parseRels(dec, sub)
|
||||||
|
c.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the workbook structure
|
||||||
|
dec, c, err = d.openXML(d.primaryDoc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = d.parseWorkbook(dec)
|
||||||
|
c.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
styn := d.rels["http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"]
|
||||||
|
for _, sst := range styn {
|
||||||
|
//log.Println(styn)
|
||||||
|
// parse the shared string table
|
||||||
|
dec, c, err = d.openXML(sst)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = d.parseStyles(dec)
|
||||||
|
c.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ssn := d.rels["http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"]
|
||||||
|
for _, sst := range ssn {
|
||||||
|
//log.Println(ssn)
|
||||||
|
// parse the shared string table
|
||||||
|
dec, c, err = d.openXML(sst)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = d.parseSharedStrings(dec)
|
||||||
|
c.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range d.sheets {
|
||||||
|
err = s.parseSheet()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) openXML(name string) (*xml.Decoder, io.Closer, error) {
|
||||||
|
//log.Println(name)
|
||||||
|
for _, zf := range d.r.File {
|
||||||
|
if zf.Name == name {
|
||||||
|
zfr, err := zf.Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
dec := xml.NewDecoder(zfr)
|
||||||
|
return dec, zfr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) Sheets() []string {
|
||||||
|
res := make([]string, 0, len(d.sheets))
|
||||||
|
for _, s := range d.sheets {
|
||||||
|
//if (s.HiddenState & 0x03) == 0 {
|
||||||
|
res = append(res, s.name)
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) Get(sheetName string) (*Sheet, error) {
|
||||||
|
for _, s := range d.sheets {
|
||||||
|
if s.name == sheetName {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("xlsx: sheet not found")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user