├── LICENSE
├── README.md
├── cell.go
├── cell_test.go
├── content_types.go
├── content_types_test.go
├── excl.go
├── row.go
├── row_test.go
├── shared_strings.go
├── shared_strings_test.go
├── sheet.go
├── sheet_test.go
├── styles.go
├── styles_test.go
├── tag.go
├── tag_test.go
├── temp
├── test.xlsx
└── test2.xlsx
├── theme.go
├── workbook.go
├── workbook_rel.go
├── workbook_rel_test.go
└── workbook_test.go
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 YuIwasaki
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | excl
2 | ====
3 |
4 | これはexcelコントロール用のライブラリ
5 |
6 | [](https://godoc.org/github.com/loadoff/excl)
7 | [](https://circleci.com/gh/loadoff/excl)
8 | [](https://goreportcard.com/report/github.com/loadoff/excl)
9 |
10 | ## Description
11 |
12 | 基本的にもとのexcelファイルを破壊せずにデータの入力を行うためのライブラリです。
13 | また大量のデータを扱う上でも優位になるように開発を行います。
14 |
15 | ## Usage
16 |
17 | 既存のExcelファイルを操作
18 | ```go
19 | // Excelファイルを読み込み
20 | w, _ := excl.Open("path/to/read.xlsx")
21 | // シートを開く
22 | s, _ := w.OpenSheet("Sheet1")
23 | // 一行目を取得
24 | r := s.GetRow(1)
25 | // 1列目のセルを取得
26 | c := r.GetCell(1)
27 | // セルに10を出力
28 | c.SetNumber("10")
29 | // セルに1を出力
30 | s.GetRow(2).GetCell(1).SetNumber(1)
31 | // セルに1.1を出力
32 | s.GetRow(3).GetCell(1).SetNumber(1.1)
33 | // 2列目のセルにABCDEという文字列を出力
34 | c = r.SetString("ABCDE", 2)
35 | // セルに日付を出力
36 | s.GetRow(4).GetCell(1).SetDate(time.Now())
37 | // セルに数式を出力
38 | s.GetRow(5).GetCell(1).SetFormula("SUM(A2:A3)")
39 | // シートを閉じる
40 | s.Close()
41 | // 保存
42 | w.Save("path/to/new.xlsx")
43 | ```
44 |
45 | 新規Excelファイルを作成
46 | ```go
47 | // 新規Excelファイルを作成
48 | w, _ := excl.Create()
49 | s, _ := w.OpenSheet("Sheet1")
50 | s.Close()
51 | w.Save("path/to/new.xlsx")
52 | ```
53 |
54 | セルの書式の設定方法
55 | ```go
56 | w, _ := excl.Open("path/to/read.xlsx")
57 | s, _ := w.OpenSheet("Sheet1")
58 | r := s.GetRow(1)
59 | c := r.GetCell(1)
60 | c.SetNumber("10000.00")
61 | // 数値のフォーマットを設定する
62 | c.SetNumFmt("#,##0.0")
63 | // フォントの設定
64 | c.SetFont(excl.Font{Size: 12, Color: "FF00FFFF", Bold: true, Italic: false,Underline: false})
65 | // 背景色の設定
66 | c.SetBackgroundColor("FFFF00FF")
67 | // 罫線の設定
68 | c.SetBorder(excl.Border{
69 | Left: &excl.BorderSetting{Style: "thin", Color: "FFFFFF00"},
70 | Right: &excl.BorderSetting{Style: "hair"},
71 | Top: &excl.BorderSetting{Style: "dashDotDot"},
72 | Bottom: nil,
73 | })
74 | s.Close()
75 | w.Save("path/to/new.xlsx")
76 | ```
77 |
78 | グリッド線の表示非表示
79 | ```go
80 | w, _ := excl.Open("path/to/read.xlsx")
81 | s, _ := w.OpenSheet("Sheet1")
82 | // シートのグリッド線を表示
83 | s.ShowGridlines(true)
84 | // シートのグリッド線を非表示
85 | s.ShowGridlines(false)
86 | s.Close()
87 | w.Save("path/to/new.xlsx")
88 | ```
89 |
90 | カラム幅の変更
91 | ```go
92 | w, _ := excl.Open("path/to/read.xlsx")
93 | s, _ := w.OpenSheet("Sheet1")
94 | // 5番目のカラム幅を1.1に変更
95 | s.SetColWidth(1.1, 5)
96 | s.Close()
97 | w.Save("path/to/new.xlsx")
98 | ```
99 |
100 | 計算式結果の更新が必要な場合はSetForceFormulaRecalculationを使用する
101 | この関数を利用することでExcelを開いた際に結果が自動的に更新される
102 | ```go
103 | w, _ := excl.Open("path/to/read.xlsx")
104 | // 何か処理...
105 | w.SetForceFormulaRecalculation(true)
106 | w.Save("path/to/new.xlsx")
107 | ```
108 |
109 | シート名変更
110 | ```go
111 | w, _ := excl.Open("path/to/read.xlsx")
112 | w.RenameSheet("oldname", "newname")
113 | w.Save("path/to/new.xlsx")
114 | ```
115 |
116 | シートの表示非表示切り替え
117 | ```go
118 | w, _ := excl.Open("path/to/read.xlsx")
119 | // シートを隠す
120 | w.HideSheet("Sheet1")
121 | // シートを表示する
122 | w.ShowSheet("Sheet1")
123 | w.Save("path/to/new.xlsx")
124 | ```
125 |
126 | ## Install
127 |
128 | ```bash
129 | $ go get github.com/loadoff/excl
130 | ```
131 |
132 | ## Licence
133 |
134 | [MIT](https://github.com/loadoff/excl/LICENCE)
135 |
136 | ## Author
137 |
138 | [YuIwasaki](https://github.com/loadoff)
139 |
--------------------------------------------------------------------------------
/cell.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "encoding/xml"
5 | "fmt"
6 | "regexp"
7 | "strconv"
8 | "time"
9 | )
10 |
11 | // Cell はセル一つ一つに対する構造体
12 | type Cell struct {
13 | cell *Tag
14 | colNo int
15 | R string
16 | sharedStrings *SharedStrings
17 | styleIndex int
18 | styles *Styles
19 | style *Style
20 | changed bool
21 | }
22 |
23 | // NewCell は新しくcellを作成する
24 | func NewCell(tag *Tag, sharedStrings *SharedStrings, styles *Styles) *Cell {
25 | cell := &Cell{cell: tag, sharedStrings: sharedStrings, colNo: -1, styles: styles}
26 | r := regexp.MustCompile("^([A-Z]+)[0-9]+$")
27 | for _, attr := range tag.Attr {
28 | if attr.Name.Local == "r" {
29 | strs := r.FindStringSubmatch(attr.Value)
30 | if len(strs) != 2 {
31 | return nil
32 | }
33 | cell.colNo = int(ColNumPosition(strs[1]))
34 | } else if attr.Name.Local == "s" {
35 | cell.styleIndex, _ = strconv.Atoi(attr.Value)
36 | }
37 | }
38 | if cell.colNo == -1 {
39 | return nil
40 | }
41 | return cell
42 | }
43 |
44 | // setValue セルに文字列を追加する
45 | func (cell *Cell) setValue(val string) *Cell {
46 | tag := &Tag{
47 | Name: xml.Name{Local: "v"},
48 | Children: []interface{}{
49 | xml.CharData(val),
50 | },
51 | }
52 | cell.cell.Children = []interface{}{tag}
53 | return cell
54 | }
55 |
56 | // SetString 文字列を追加する
57 | func (cell *Cell) SetString(val string) *Cell {
58 | v := cell.sharedStrings.AddString(val)
59 | cell.setValue(strconv.Itoa(v))
60 | cell.cell.setAttr("t", "s")
61 | return cell
62 | }
63 |
64 | // SetNumber set a number in a cell
65 | func (cell *Cell) SetNumber(val interface{}) *Cell {
66 | var str string
67 | switch t := val.(type) {
68 | case int:
69 | str = strconv.Itoa(t)
70 | case int16:
71 | str = strconv.FormatInt(int64(t), 10)
72 | case int32:
73 | str = strconv.FormatInt(int64(t), 10)
74 | case int64:
75 | str = strconv.FormatInt(t, 10)
76 | case float32:
77 | str = fmt.Sprint(t)
78 | case float64:
79 | str = fmt.Sprint(t)
80 | case string:
81 | str = t
82 | default:
83 | panic("")
84 | }
85 | cell.setValue(str)
86 | cell.cell.deleteAttr("t")
87 | return cell
88 | }
89 |
90 | // SetFormula set a formula in a cell
91 | func (cell *Cell) SetFormula(val string) *Cell {
92 | tag := &Tag{
93 | Name: xml.Name{Local: "f"},
94 | Children: []interface{}{
95 | xml.CharData(val),
96 | },
97 | }
98 | cell.cell.Children = []interface{}{tag}
99 | cell.cell.deleteAttr("t")
100 | return cell
101 | }
102 |
103 | // SetDate set a date in a cell
104 | func (cell *Cell) SetDate(val time.Time) *Cell {
105 | cell.cell.setAttr("t", "d")
106 | cell.setValue(val.Format("2006-01-02T15:04:05.999999999"))
107 | if cell.GetStyle().NumFmtID == 0 {
108 | cell.SetStyle(&Style{NumFmtID: 14})
109 | }
110 | return cell
111 | }
112 |
113 | // GetStyle Style構造体を取得する
114 | func (cell *Cell) GetStyle() *Style {
115 | if cell.style == nil {
116 | style := cell.styles.GetStyle(cell.styleIndex)
117 | if style == nil {
118 | style = &Style{}
119 | }
120 | cell.style = &Style{
121 | NumFmtID: style.NumFmtID,
122 | FontID: style.FontID,
123 | FillID: style.FillID,
124 | BorderID: style.BorderID,
125 | XfID: style.XfID,
126 | Horizontal: style.Horizontal,
127 | Vertical: style.Vertical,
128 | Wrap: style.Wrap,
129 | }
130 | }
131 | return cell.style
132 | }
133 |
134 | // SetNumFmt 数値フォーマット
135 | func (cell *Cell) SetNumFmt(fmt string) *Cell {
136 | if cell.style == nil {
137 | cell.GetStyle()
138 | }
139 | cell.style.NumFmtID = cell.styles.SetNumFmt(fmt)
140 | cell.changed = true
141 | return cell
142 | }
143 |
144 | // SetFont フォント情報をセットする
145 | func (cell *Cell) SetFont(font Font) *Cell {
146 | if cell.style == nil {
147 | cell.GetStyle()
148 | }
149 | cell.style.FontID = cell.styles.SetFont(font)
150 | cell.changed = true
151 | return cell
152 | }
153 |
154 | // SetBackgroundColor 背景色をセットする
155 | func (cell *Cell) SetBackgroundColor(color string) *Cell {
156 | if cell.style == nil {
157 | cell.GetStyle()
158 | }
159 | cell.style.FillID = cell.styles.SetBackgroundColor(color)
160 | cell.changed = true
161 | return cell
162 | }
163 |
164 | // SetBorder 罫線情報をセットする
165 | func (cell *Cell) SetBorder(border Border) *Cell {
166 | if cell.style == nil {
167 | cell.GetStyle()
168 | }
169 | cell.style.BorderID = cell.styles.SetBorder(border)
170 | cell.changed = true
171 | return cell
172 | }
173 |
174 | // SetStyle 数値フォーマットIDをセット
175 | func (cell *Cell) SetStyle(style *Style) *Cell {
176 | if style == nil {
177 | return cell
178 | }
179 | if cell.style == nil {
180 | cell.GetStyle()
181 | }
182 | if style.NumFmtID > 0 {
183 | cell.style.NumFmtID = style.NumFmtID
184 | }
185 | if style.FontID > 0 {
186 | cell.style.FontID = style.FontID
187 | }
188 | if style.FillID > 0 {
189 | cell.style.FillID = style.FillID
190 | }
191 | if style.BorderID > 0 {
192 | cell.style.BorderID = style.BorderID
193 | }
194 | if style.Horizontal != "" {
195 | cell.style.Horizontal = style.Horizontal
196 | }
197 | if style.Vertical != "" {
198 | cell.style.Vertical = style.Vertical
199 | }
200 | if style.Wrap != 0 {
201 | cell.style.Wrap = style.Wrap
202 | }
203 | cell.changed = true
204 | return cell
205 | }
206 |
207 | func (cell *Cell) resetStyleIndex() {
208 | if cell != nil && cell.changed {
209 | index := cell.styles.SetStyle(cell.style)
210 | cell.cell.setAttr("s", strconv.Itoa(index))
211 | }
212 | }
213 |
214 | // MarshalXML create xml for cell
215 | func (cell *Cell) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
216 | start.Name = cell.cell.Name
217 | start.Attr = cell.cell.Attr
218 | e.EncodeToken(start)
219 | e.Encode(cell.cell.Children)
220 | e.EncodeToken(start.End())
221 | return nil
222 | }
223 |
--------------------------------------------------------------------------------
/cell_test.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "bytes"
5 | "encoding/xml"
6 | "os"
7 | "testing"
8 | "time"
9 | )
10 |
11 | func TestNewCell(t *testing.T) {
12 | tag := &Tag{}
13 | cell := NewCell(tag, nil, nil)
14 | if cell != nil {
15 | t.Error("cell should be nil because colNo does not exist.")
16 | }
17 | attr := xml.Attr{
18 | Name: xml.Name{Local: "r"},
19 | Value: "",
20 | }
21 | tag.Attr = append(tag.Attr, attr)
22 | cell = NewCell(tag, nil, nil)
23 | if cell != nil {
24 | t.Error("cell should be nil because colNo is not correct.")
25 | }
26 | attr.Value = "A1"
27 | tag.Attr = []xml.Attr{attr}
28 | cell = NewCell(tag, nil, nil)
29 | if cell == nil {
30 | t.Error("cell should be created.")
31 | } else if cell.colNo != 1 {
32 | t.Error("colNo should be 1 but [", cell.colNo, "]")
33 | }
34 | tag.setAttr("s", "2")
35 | if cell = NewCell(tag, nil, nil); cell == nil {
36 | t.Error("cell should be created.")
37 | }
38 |
39 | }
40 |
41 | func TestSetNumber(t *testing.T) {
42 | tag := &Tag{}
43 | attr := xml.Attr{
44 | Name: xml.Name{Local: "r"},
45 | Value: "A1",
46 | }
47 | tag.Attr = []xml.Attr{attr}
48 | cell := &Cell{cell: tag, colNo: 1}
49 | cell.SetNumber("123")
50 | val := cell.cell.Children[0].(*Tag)
51 | if val.Name.Local != "v" {
52 | t.Error("tag should be v but [", val.Name.Local, "]")
53 | } else {
54 | data := val.Children[0].(xml.CharData)
55 | if string(data) != "123" {
56 | t.Error("value should be 123 but [", data, "]")
57 | }
58 | }
59 | typeAttr := xml.Attr{
60 | Name: xml.Name{Local: "t"},
61 | Value: "s",
62 | }
63 | tag.Attr = []xml.Attr{attr, typeAttr}
64 | cell = &Cell{cell: tag, colNo: 1}
65 | cell.SetNumber("456")
66 | if _, err := cell.cell.getAttr("t"); err == nil {
67 | t.Error("t attribute should be deleted.")
68 | }
69 |
70 | i := 123
71 | var i16 int16 = 234
72 | var i32 int32 = 345
73 | var i64 int64 = 456
74 | var f32 float32 = 56.78
75 | f64 := 67.89
76 |
77 | cell = &Cell{cell: tag, colNo: 1}
78 | cell.SetNumber(i)
79 | val = cell.cell.Children[0].(*Tag)
80 | data := val.Children[0].(xml.CharData)
81 | if string(data) != "123" {
82 | t.Error("value should be 123 but [", data, "]")
83 | }
84 | cell.SetNumber(i16)
85 | val = cell.cell.Children[0].(*Tag)
86 | data = val.Children[0].(xml.CharData)
87 | if string(data) != "234" {
88 | t.Error("value should be 234 but [", data, "]")
89 | }
90 | cell.SetNumber(i32)
91 | val = cell.cell.Children[0].(*Tag)
92 | data = val.Children[0].(xml.CharData)
93 | if string(data) != "345" {
94 | t.Error("value should be 345 but [", data, "]")
95 | }
96 | cell.SetNumber(i64)
97 | val = cell.cell.Children[0].(*Tag)
98 | data = val.Children[0].(xml.CharData)
99 | if string(data) != "456" {
100 | t.Error("value should be 456 but [", data, "]")
101 | }
102 | cell.SetNumber(f32)
103 | val = cell.cell.Children[0].(*Tag)
104 | data = val.Children[0].(xml.CharData)
105 | if string(data) != "56.78" {
106 | t.Error("value should be 56.78 but [", data, "]")
107 | }
108 | cell.SetNumber(f64)
109 | val = cell.cell.Children[0].(*Tag)
110 | data = val.Children[0].(xml.CharData)
111 | if string(data) != "67.89" {
112 | t.Error("value should be 67.89 but [", data, "]")
113 | }
114 |
115 | }
116 |
117 | func TestSetString(t *testing.T) {
118 | f, _ := os.Create("temp/sharedStrings.xml")
119 | sharedStrings := &SharedStrings{count: 0, tempFile: f, buffer: &bytes.Buffer{}}
120 | tag := &Tag{}
121 | tag.setAttr("r", "AB12")
122 | cell := &Cell{cell: tag, colNo: 1}
123 | cell.sharedStrings = sharedStrings
124 | cell.SetString("こんにちは")
125 | cTag := cell.cell.Children[0].(*Tag)
126 | if cTag.Name.Local != "v" {
127 | t.Error("tag name should be [v] but [", cTag.Name.Local, "]")
128 | } else if string(cTag.Children[0].(xml.CharData)) == "こんにちは" {
129 | t.Error("tag value should be こんにちは but [", cTag.Children[0].(xml.CharData), "]")
130 | } else if cell.cell.Attr[0].Value == "s" {
131 | t.Error("tag attribute value should be s but [", cTag.Attr[0].Value, "]")
132 | }
133 | f.Close()
134 | os.Remove("temp/sharedStrings.xml")
135 | }
136 |
137 | func TestSetDate(t *testing.T) {
138 | cell := &Cell{cell: &Tag{}, styles: &Styles{}}
139 | now := time.Now()
140 | cell.SetDate(now)
141 | if val, _ := cell.cell.getAttr("t"); val != "d" {
142 | t.Error("cell t attribute should be d but [", val, "]")
143 | }
144 | cTag := cell.cell.Children[0].(*Tag)
145 | if string(cTag.Children[0].(xml.CharData)) != now.Format("2006-01-02T15:04:05.999999999") {
146 | t.Error("cell value should be ", now.Format("2006-01-02T15:04:05.999999999"), " but ", string(cTag.Children[0].(xml.CharData)))
147 | }
148 | if cell.style.NumFmtID != 14 {
149 | t.Error("cell NumFmtID should be 14 but", cell.style.NumFmtID)
150 | }
151 | }
152 |
153 | func TestSetFormula(t *testing.T) {
154 | cell := &Cell{cell: &Tag{}, styles: &Styles{}}
155 | cell.SetFormula("SUM(A1:B1)")
156 | cTag := cell.cell.Children[0].(*Tag)
157 | if cTag.Name.Local != "f" {
158 | t.Error("tag name should be f but", cTag.Name.Local)
159 | }
160 | if string(cTag.Children[0].(xml.CharData)) != "SUM(A1:B1)" {
161 | t.Error("cell value should be SUM(A1:B1) but ", string(cTag.Children[0].(xml.CharData)))
162 | }
163 | }
164 |
165 | func TestSetCellNumFmt(t *testing.T) {
166 | cell := &Cell{}
167 | cell.styles = &Styles{}
168 | cell.styleIndex = 10
169 |
170 | if cell.SetNumFmt("format"); cell.style.NumFmtID != 0 {
171 | t.Error("numFmtId should be 0 but ", cell.style.NumFmtID)
172 | }
173 |
174 | if cell.SetNumFmt("format"); cell.style.NumFmtID != 1 {
175 | t.Error("numFmtId should be 1 but ", cell.style.NumFmtID)
176 | }
177 | }
178 |
179 | func TestCellSetFont(t *testing.T) {
180 | cell := &Cell{}
181 | cell.styles = &Styles{fonts: &Tag{}}
182 | cell.styleIndex = 10
183 |
184 | if cell.SetFont(Font{}); cell.style.FontID != 0 {
185 | t.Error("fontID should be 0 but ", cell.style.FontID)
186 | }
187 |
188 | if cell.SetFont(Font{}); cell.style.FontID != 1 {
189 | t.Error("fontID should be 1 but ", cell.style.FontID)
190 | }
191 | }
192 |
193 | func TestCellSetBackgroundColor(t *testing.T) {
194 | cell := &Cell{}
195 | cell.styles = &Styles{fills: &Tag{}}
196 | cell.styleIndex = 10
197 |
198 | if cell.SetBackgroundColor("FFFFFF"); cell.style.FillID != 0 {
199 | t.Error("fillID should be 0 but ", cell.style.FillID)
200 | }
201 |
202 | if cell.SetBackgroundColor("000000"); cell.style.FillID != 1 {
203 | t.Error("fillID should be 1 but ", cell.style.FillID)
204 | }
205 | }
206 |
207 | func TestCellSetBorder(t *testing.T) {
208 | cell := &Cell{}
209 | cell.styles = &Styles{borders: &Tag{}}
210 | cell.styleIndex = 10
211 |
212 | if cell.SetBorder(Border{}); cell.style.BorderID != 0 {
213 | t.Error("BorderID should be 0 but ", cell.style.BorderID)
214 | }
215 |
216 | if cell.SetBorder(Border{}); cell.style.BorderID != 1 {
217 | t.Error("BorderID should be 1 but ", cell.style.BorderID)
218 | }
219 | }
220 |
221 | func TestCellSetStyle(t *testing.T) {
222 | cell := &Cell{}
223 | cell.styles = &Styles{}
224 | cell.styleIndex = 10
225 | style := &Style{}
226 | cell.SetStyle(nil)
227 | if cell.style != nil {
228 | t.Error("style should be nil.")
229 | }
230 | cell.SetStyle(style)
231 | if cell.style.NumFmtID != 0 {
232 | t.Error("NumFmtID should be 0 but", cell.style.NumFmtID)
233 | }
234 | if cell.style.FontID != 0 {
235 | t.Error("FontID should be 0 but", cell.style.FontID)
236 | }
237 | if cell.style.FillID != 0 {
238 | t.Error("FillID should be 0 but", cell.style.FillID)
239 | }
240 | if cell.style.BorderID != 0 {
241 | t.Error("BorderID should be 0 but", cell.style.BorderID)
242 | }
243 | if cell.style.Horizontal != "" {
244 | t.Error("Horizontal should be empty but", cell.style.Horizontal)
245 | }
246 | if cell.style.Vertical != "" {
247 | t.Error("Vertical should be empty but", cell.style.Vertical)
248 | }
249 |
250 | style.NumFmtID = 1
251 | style.FontID = 2
252 | style.FillID = 3
253 | style.BorderID = 4
254 | style.Horizontal = "center"
255 | style.Vertical = "top"
256 | cell.SetStyle(style)
257 | if cell.style.NumFmtID != style.NumFmtID {
258 | t.Error("NumFmtID should be 1 but", cell.style.NumFmtID)
259 | }
260 |
261 | if cell.style.FontID != style.FontID {
262 | t.Error("FontID should be 2 but", cell.style.FontID)
263 | }
264 |
265 | if cell.style.FillID != style.FillID {
266 | t.Error("FillID should be 3 but", cell.style.FillID)
267 | }
268 |
269 | if cell.style.BorderID != style.BorderID {
270 | t.Error("BorderID should be 3 but", cell.style.BorderID)
271 | }
272 |
273 | if cell.style.Horizontal != style.Horizontal {
274 | t.Error("Horizontal should be center but", cell.style.Horizontal)
275 | }
276 |
277 | if cell.style.Vertical != style.Vertical {
278 | t.Error("Vertical should be top but", cell.style.Vertical)
279 | }
280 | }
281 |
282 | func TestResetStyleIndex(t *testing.T) {
283 | var cell *Cell
284 | if cell.resetStyleIndex(); cell != nil {
285 | t.Error("cell should be nil")
286 | }
287 | cell = &Cell{}
288 | cell.style = &Style{}
289 | cell.cell = &Tag{}
290 | cell.styles = &Styles{cellXfs: &Tag{}}
291 | cell.resetStyleIndex()
292 | if _, err := cell.cell.getAttr("s"); err == nil {
293 | t.Error("cell attribute should not be found.")
294 | }
295 |
296 | cell.changed = true
297 | cell.resetStyleIndex()
298 | if val, _ := cell.cell.getAttr("s"); val != "0" {
299 | t.Error("style index should be 0 but", val)
300 | }
301 | }
302 |
--------------------------------------------------------------------------------
/content_types.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "encoding/xml"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "path/filepath"
9 | )
10 |
11 | // ContentTypes ContentTypesの情報を保持
12 | type ContentTypes struct {
13 | path string
14 | types *ContentTypesXML
15 | }
16 |
17 | // ContentTypesXML [Content_Types].xmlファイルを読み込む
18 | type ContentTypesXML struct {
19 | XMLName xml.Name `xml:"Types"`
20 | Xmlns string `xml:"xmlns,attr"`
21 | Defaults []contentDefault `xml:"Default"`
22 | Overrides []contentOverride `xml:"Override"`
23 | }
24 |
25 | type contentOverride struct {
26 | XMLName xml.Name `xml:"Override"`
27 | PartName string `xml:"PartName,attr"`
28 | ContentType string `xml:"ContentType,attr"`
29 | }
30 |
31 | type contentDefault struct {
32 | XMLName xml.Name `xml:"Default"`
33 | Extension string `xml:"Extension,attr"`
34 | ContentType string `xml:"ContentType,attr"`
35 | }
36 |
37 | // createContentTypes [Content_Types].xmlファイルを作成する
38 | func createContentTypes(dir string) error {
39 | path := filepath.Join(dir, "[Content_Types].xml")
40 | f, err := os.Create(path)
41 | if err != nil {
42 | return err
43 | }
44 | defer f.Close()
45 | f.WriteString("\n")
46 | f.WriteString(``)
47 | f.WriteString(``)
48 | f.WriteString(``)
49 | f.WriteString(``)
50 | f.WriteString(``)
51 | f.WriteString(``)
52 | f.Close()
53 | return nil
54 | }
55 |
56 | // OpenContentTypes [Content_Types].xmlファイルを開き構造体に読み込む
57 | func OpenContentTypes(dir string) (*ContentTypes, error) {
58 | path := filepath.Join(dir, "[Content_Types].xml")
59 | f, err := os.Open(path)
60 | if err != nil {
61 | return nil, err
62 | }
63 | defer f.Close()
64 | data, err := ioutil.ReadAll(f)
65 | if err != nil {
66 | return nil, err
67 | }
68 | v := ContentTypesXML{}
69 | err = xml.Unmarshal(data, &v)
70 | if err != nil {
71 | return nil, err
72 | }
73 | f.Close()
74 | types := &ContentTypes{path, &v}
75 | return types, nil
76 | }
77 |
78 | // Close [Content_Types].xmlファイルを閉じる
79 | func (types *ContentTypes) Close() error {
80 | if types == nil {
81 | return nil
82 | }
83 | f, err := os.Create(types.path)
84 | if err != nil {
85 | return err
86 | }
87 | defer f.Close()
88 | d, err := xml.Marshal(types.types)
89 | if err != nil {
90 | return err
91 | }
92 | f.WriteString("\n")
93 | f.Write(d)
94 | return nil
95 | }
96 |
97 | // sheetCount シートの数を返す
98 | func (types *ContentTypes) sheetCount() int {
99 | var count int
100 | for _, override := range types.types.Overrides {
101 | if override.ContentType == "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" {
102 | count++
103 | }
104 | }
105 | return count
106 | }
107 |
108 | // addSheet シートを追加する
109 | func (types *ContentTypes) addSheet(count int) string {
110 | name := fmt.Sprintf("sheet%d.xml", count+1)
111 |
112 | override := contentOverride{
113 | XMLName: xml.Name{Space: "", Local: "Override"},
114 | PartName: "/xl/worksheets/" + name,
115 | ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"}
116 | types.types.Overrides = append(types.types.Overrides, override)
117 | return name
118 | }
119 |
120 | // hasSharedString sharedString.xmlファイルが存在するか確認する
121 | func (types *ContentTypes) hasSharedString() bool {
122 | for _, override := range types.types.Overrides {
123 | if override.PartName == "/xl/sharedStrings.xml" {
124 | return true
125 | }
126 | }
127 | return false
128 | }
129 |
130 | // addSharedString sharedString.xmlファイルを追加する
131 | func (types *ContentTypes) addSharedString() {
132 | if types.hasSharedString() {
133 | return
134 | }
135 | override := contentOverride{
136 | XMLName: xml.Name{Space: "", Local: "Override"},
137 | PartName: "/xl/sharedStrings.xml",
138 | ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"}
139 | types.types.Overrides = append(types.types.Overrides, override)
140 | }
141 |
--------------------------------------------------------------------------------
/content_types_test.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "os"
5 | "testing"
6 | )
7 |
8 | func TestOpenContentTypes(t *testing.T) {
9 | _, err := OpenContentTypes("./no/path/exist")
10 | if err == nil {
11 | t.Error(`xml file should not open.`)
12 | }
13 | f, _ := os.Create("./[Content_Types].xml")
14 | f.Close()
15 | defer os.Remove("./[Content_Types].xml")
16 | _, err = OpenContentTypes("./")
17 | if err == nil {
18 | t.Error("[Content_Types].xml is not xml file.")
19 | }
20 |
21 | f, _ = os.Create("./temp/[Content_Types].xml")
22 | f.WriteString("")
23 | f.Close()
24 | defer os.Remove("./temp/[Content_Types].xml")
25 | _, err = OpenContentTypes("./temp")
26 | if err != nil {
27 | t.Error("[Content_Types].xml should be opened. [", err.Error(), "]")
28 | }
29 | }
30 |
31 | func TestSheetCount(t *testing.T) {
32 | f, _ := os.Create("./temp/[Content_Types].xml")
33 | f.WriteString(``)
34 | f.Close()
35 | defer os.Remove("./temp/[Content_Types].xml")
36 | types, _ := OpenContentTypes("./temp")
37 | count := types.sheetCount()
38 | if count != 1 {
39 | t.Error("sheet count should be 1 not [", count, "]")
40 | }
41 | }
42 |
43 | func TestAddSheet(t *testing.T) {
44 | f, _ := os.Create("./temp/[Content_Types].xml")
45 | f.WriteString(``)
46 | f.Close()
47 | defer os.Remove("./temp/[Content_Types].xml")
48 | types, _ := OpenContentTypes("./temp")
49 | name := types.addSheet(1)
50 | if name != "sheet2.xml" {
51 | t.Error(`sheet name should be "sheet2.xml" but [`, name, "]")
52 | }
53 | count := types.sheetCount()
54 | if count != 2 {
55 | t.Error("sheet count should be 2 not [", count, "]")
56 | }
57 | if types.types.Overrides[len(types.types.Overrides)-1].PartName != "/xl/worksheets/sheet2.xml" {
58 | t.Error(`part name should be "/xl/worksheets/sheet2.xml" but [`, types.types.Overrides[count-1].PartName, "]")
59 | }
60 | }
61 |
62 | func TestHasSharedString(t *testing.T) {
63 | f, _ := os.Create("./temp/[Content_Types].xml")
64 | f.WriteString(``)
65 | f.Close()
66 | defer os.Remove("./temp/[Content_Types].xml")
67 |
68 | types, _ := OpenContentTypes("./temp")
69 | if !types.hasSharedString() {
70 | t.Error("sharedString.xml file should be exists.")
71 | }
72 |
73 | f, _ = os.Create("./temp/[Content_Types].xml")
74 | f.WriteString(``)
75 | f.Close()
76 |
77 | types, _ = OpenContentTypes("./temp")
78 | if types.hasSharedString() {
79 | t.Error("sharedString.xml file should not be exists.")
80 | }
81 | }
82 |
83 | func TestAddSharedString(t *testing.T) {
84 | f, _ := os.Create("./temp/[Content_Types].xml")
85 | f.WriteString(``)
86 | f.Close()
87 | defer os.Remove("./temp/[Content_Types].xml")
88 |
89 | types, _ := OpenContentTypes("./temp")
90 | if types.hasSharedString() {
91 | t.Error("sharedString.xml file should not be exists.")
92 | }
93 | types.addSharedString()
94 | if !types.hasSharedString() {
95 | t.Error("sharedString.xml file should be exists.")
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/excl.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | // NewExcl は
4 | func NewExcl() {
5 |
6 | }
7 |
--------------------------------------------------------------------------------
/row.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "encoding/xml"
5 | "fmt"
6 | "math"
7 | "sort"
8 | "strconv"
9 | "time"
10 | )
11 |
12 | // Row 行の構造体
13 | type Row struct {
14 | rowID int
15 | row *Tag
16 | cells []*Cell
17 | sharedStrings *SharedStrings
18 | colInfos []colInfo
19 | style string
20 | minColNo int
21 | maxColNo int
22 | styles *Styles
23 | }
24 |
25 | // NewRow は新しく行を追加する際に使用する
26 | func NewRow(tag *Tag, sharedStrings *SharedStrings, styles *Styles) *Row {
27 | row := &Row{row: tag, sharedStrings: sharedStrings, styles: styles}
28 | for _, attr := range tag.Attr {
29 | if attr.Name.Local == "r" {
30 | row.rowID, _ = strconv.Atoi(attr.Value)
31 | } else if attr.Name.Local == "s" {
32 | row.style = attr.Value
33 | }
34 | }
35 | for _, child := range tag.Children {
36 | switch col := child.(type) {
37 | case *Tag:
38 | if col.Name.Local == "c" {
39 | cell := NewCell(col, sharedStrings, styles)
40 | if cell == nil {
41 | return nil
42 | }
43 | cell.styles = row.styles
44 | row.cells = append(row.cells, cell)
45 | row.maxColNo = cell.colNo
46 | if row.minColNo == 0 {
47 | row.minColNo = cell.colNo
48 | }
49 | }
50 | }
51 | }
52 | return row
53 | }
54 |
55 | // CreateCells セル一覧を用意する
56 | func (row *Row) CreateCells(from int, to int) []*Cell {
57 | if row.maxColNo < to {
58 | row.maxColNo = to
59 | }
60 | cells := make([]*Cell, row.maxColNo)
61 | for _, cell := range row.cells {
62 | if cell == nil || cell.colNo == 0 {
63 | continue
64 | }
65 | cells[cell.colNo-1] = cell
66 | }
67 | for i := from; i <= to; i++ {
68 | if cells[i-1] != nil {
69 | continue
70 | }
71 | attr := []xml.Attr{
72 | xml.Attr{
73 | Name: xml.Name{Local: "r"},
74 | Value: fmt.Sprintf("%s%d", ColStringPosition(i), row.rowID),
75 | },
76 | }
77 | tag := &Tag{
78 | Name: xml.Name{Local: "c"},
79 | Attr: attr,
80 | }
81 | style := 0
82 | if row.style != "" {
83 | style, _ = strconv.Atoi(row.style)
84 | } else {
85 | for _, colInfo := range row.colInfos {
86 | if colInfo.style != "" && colInfo.min <= i && i <= colInfo.max {
87 | style, _ = strconv.Atoi(colInfo.style)
88 | break
89 | }
90 | }
91 | }
92 | cells[i-1] = &Cell{cell: tag, colNo: i, sharedStrings: row.sharedStrings, styleIndex: style, styles: row.styles}
93 | }
94 | row.cells = cells
95 | return row.cells
96 | }
97 |
98 | // GetCell セル番号のセルを取得する
99 | func (row *Row) GetCell(colNo int) *Cell {
100 |
101 | for i := len(row.cells) - 1; i >= 0; i-- {
102 | // for _, cell := range row.cells {
103 | cell := row.cells[i]
104 | if cell.colNo == colNo {
105 | return cell
106 | }
107 | if cell.colNo < colNo {
108 | break
109 | }
110 | }
111 |
112 | // 存在しない場合はセルを追加する
113 | tag := &Tag{Name: xml.Name{Local: "c"}}
114 | tag.setAttr("r", fmt.Sprintf("%s%d", ColStringPosition(int(colNo)), row.rowID))
115 | if row.style != "" {
116 | tag.setAttr("s", row.style)
117 | } else if style := getStyleNo(row.colInfos, int(colNo)); style != "" {
118 | tag.setAttr("s", style)
119 | }
120 |
121 | cell := NewCell(tag, row.sharedStrings, row.styles)
122 | row.cells = append(row.cells, cell)
123 | return cell
124 | }
125 |
126 | // SetString set string at a row
127 | func (row *Row) SetString(val string, colNo int) *Cell {
128 | cell := row.GetCell(colNo).SetString(val)
129 | return cell
130 | }
131 |
132 | // SetNumber set number at a row
133 | func (row *Row) SetNumber(val interface{}, colNo int) *Cell {
134 | cell := row.GetCell(colNo).SetNumber(val)
135 | return cell
136 | }
137 |
138 | // SetFormula set a formula at a row
139 | func (row *Row) SetFormula(val string, colNo int) *Cell {
140 | cell := row.GetCell(colNo).SetFormula(val)
141 | return cell
142 | }
143 |
144 | // SetDate set a date at a row
145 | func (row *Row) SetDate(val time.Time, colNo int) *Cell {
146 | cell := row.GetCell(colNo).SetDate(val)
147 | return cell
148 | }
149 |
150 | // SetHeight set row height
151 | func (row *Row) SetHeight(height float64) {
152 | row.row.setAttr("customHeight", "1")
153 | row.row.setAttr("ht", strconv.FormatFloat(height, 'f', 4, 64))
154 | }
155 |
156 | // ColStringPosition obtain AtoZ column string from column no
157 | func ColStringPosition(num int) string {
158 | atoz := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
159 | if num <= 26 {
160 | return atoz[num-1]
161 | }
162 | return ColStringPosition((num-1)/26) + atoz[(num-1)%26]
163 | }
164 |
165 | // ColNumPosition obtain column no from AtoZ column string
166 | func ColNumPosition(col string) int {
167 | var num int
168 | for i := len(col) - 1; i >= 0; i-- {
169 | p := math.Pow(26, float64(len(col)-i-1))
170 | num += int(p) * int(col[i]-0x40)
171 | }
172 | return num
173 | }
174 |
175 | // MarshalXML Create xml tags
176 | func (row *Row) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
177 | sort.Slice(row.cells, func(i, j int) bool {
178 | return row.cells[i].colNo < row.cells[j].colNo
179 | })
180 | start.Name = row.row.Name
181 | start.Attr = row.row.Attr
182 | e.EncodeToken(start)
183 | if err := e.Encode(row.cells); err != nil {
184 | return err
185 | }
186 | e.EncodeToken(start.End())
187 | return nil
188 | }
189 |
190 | func (row *Row) resetStyleIndex() {
191 | for _, cell := range row.cells {
192 | cell.resetStyleIndex()
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/row_test.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "bytes"
5 | "encoding/xml"
6 | "os"
7 | "testing"
8 | "time"
9 | )
10 |
11 | func TestNewRow(t *testing.T) {
12 | tag := &Tag{}
13 | tag.setAttr("r", "10")
14 | tag.setAttr("s", "5")
15 | row := NewRow(tag, nil, nil)
16 | if row.rowID != 10 {
17 | t.Error("row no should be 10 but", row.rowID)
18 | }
19 | if row.style != "5" {
20 | t.Error("row style index should be 5 but", row.style)
21 | }
22 |
23 | cellTag := &Tag{Name: xml.Name{Local: "c"}}
24 | cellTag.setAttr("r", "J10")
25 | tag.Children = append(tag.Children, cellTag)
26 | row = NewRow(tag, nil, nil)
27 | if row == nil {
28 | t.Error("row should not be nil.")
29 | }
30 | if row.maxColNo != 10 {
31 | t.Error("row maxColNo should be 10 but", row.maxColNo)
32 | }
33 | if row.minColNo != 10 {
34 | t.Error("row minColNo should be 10 but", row.minColNo)
35 | }
36 |
37 | tag.Children = append(tag.Children, &Tag{Name: xml.Name{Local: "c"}})
38 | row = NewRow(tag, nil, nil)
39 | if row != nil {
40 | t.Error("row should be nil.")
41 | }
42 | }
43 |
44 | func TestCreateCells(t *testing.T) {
45 | tag := &Tag{}
46 | tag.setAttr("r", "10")
47 | row := NewRow(tag, nil, nil)
48 | cells := row.CreateCells(2, 3)
49 | if len(cells) != 3 {
50 | t.Error("3 cells should be created")
51 | }
52 | colInfo := colInfo{min: 4, max: 5, style: "2"}
53 | row.colInfos = append(row.colInfos, colInfo)
54 | cells = row.CreateCells(3, 10)
55 | if len(cells) != 10 {
56 | t.Error("10 cells should be created")
57 | }
58 | if cells[3].styleIndex != 2 || cells[4].styleIndex != 2 || cells[5].styleIndex != 0 {
59 | t.Error("cell style index should be set.", cells[3].styleIndex, cells[4].styleIndex, cells[5].styleIndex)
60 | }
61 | row.style = "5"
62 | cells = row.CreateCells(12, 13)
63 | if cells[3].styleIndex != 2 || cells[4].styleIndex != 2 || cells[5].styleIndex != 0 || cells[11].styleIndex != 5 || cells[12].styleIndex != 5 {
64 | t.Error("row style should be set")
65 | }
66 | }
67 |
68 | func TestGetCell(t *testing.T) {
69 | row := &Row{row: &Tag{}}
70 | tag := &Tag{}
71 | row.row.Children = append(row.row.Children, tag)
72 | cell := &Cell{cell: tag, colNo: 3}
73 | row.cells = append(row.cells, cell)
74 | c := row.GetCell(3)
75 | if c != cell {
76 | t.Error("cell should be get.")
77 | }
78 | c = row.GetCell(4)
79 | if c.colNo != 4 {
80 | t.Error("colNo should be 4 but", c.colNo)
81 | }
82 | row.colInfos = append(row.colInfos, colInfo{min: 1, max: 1, style: "3"})
83 | c = row.GetCell(1)
84 | if c.colNo != 1 {
85 | t.Error("colNo should be 1 but", c.colNo)
86 | }
87 |
88 | if c.styleIndex != 3 {
89 | t.Error("cell style should be 3 but", c.style)
90 | }
91 |
92 | row.style = "4"
93 | c = row.GetCell(2)
94 | if c.colNo != 2 {
95 | t.Error("colNo should be 2 but", c.colNo)
96 | }
97 | if c.styleIndex != 4 {
98 | t.Error("cell style should be 4 but", c.style)
99 | }
100 | }
101 |
102 | func TestSetRowString(t *testing.T) {
103 | f, _ := os.Create("temp/test.xml")
104 | defer func() {
105 | f.Close()
106 | os.Remove("temp/test.xml")
107 | }()
108 | ss := &SharedStrings{tempFile: f, buffer: &bytes.Buffer{}}
109 | tag := &Tag{}
110 | tag.setAttr("r", "10")
111 | row := NewRow(tag, ss, nil)
112 | c := row.SetString("hello world", 10)
113 | if val, _ := c.cell.getAttr("t"); val != "s" {
114 | t.Error("cell attribute should be s but", val)
115 | }
116 |
117 | tag = c.cell.Children[0].(*Tag)
118 | if tag.Name.Local != "v" {
119 | t.Error("cell tag should be v but", c.cell.Children[0].(*Tag).Name.Local)
120 | }
121 | if string(tag.Children[0].(xml.CharData)) != "0" {
122 | t.Error("cell value should be 0 but", string(tag.Children[0].(xml.CharData)))
123 | }
124 | }
125 |
126 | func TestSetRowNumber(t *testing.T) {
127 | tag := &Tag{}
128 | tag.setAttr("r", "10")
129 | row := NewRow(tag, nil, nil)
130 | c := row.SetNumber("20", 10)
131 | if val, _ := c.cell.getAttr("t"); val == "s" {
132 | t.Error("cell attribute should not be s.")
133 | }
134 |
135 | tag = c.cell.Children[0].(*Tag)
136 | if tag.Name.Local != "v" {
137 | t.Error("cell tag should be v but", c.cell.Children[0].(*Tag).Name.Local)
138 | }
139 | if string(tag.Children[0].(xml.CharData)) != "20" {
140 | t.Error("cell value should be 20 but", string(tag.Children[0].(xml.CharData)))
141 | }
142 |
143 | c = row.SetNumber(20.1, 11)
144 | tag = c.cell.Children[0].(*Tag)
145 | if string(tag.Children[0].(xml.CharData)) != "20.1" {
146 | t.Error("cell value should be 20 but", string(tag.Children[0].(xml.CharData)))
147 | }
148 | }
149 |
150 | func TestSetRowDate(t *testing.T) {
151 | tag := &Tag{}
152 | tag.setAttr("r", "10")
153 | row := NewRow(tag, nil, &Styles{})
154 | now := time.Now()
155 | c := row.SetDate(now, 10)
156 | if val, _ := c.cell.getAttr("t"); val != "d" {
157 | t.Error("cell attribute should be d but", val)
158 | }
159 | tag = c.cell.Children[0].(*Tag)
160 | if string(tag.Children[0].(xml.CharData)) != now.Format("2006-01-02T15:04:05.999999999") {
161 | t.Error("cell value should be", now.Format("2006-01-02T15:04:05.999999999"), "but", string(tag.Children[0].(xml.CharData)))
162 | }
163 | }
164 |
165 | func TestSetRowFormula(t *testing.T) {
166 | tag := &Tag{}
167 | tag.setAttr("r", "10")
168 | row := NewRow(tag, nil, &Styles{})
169 | c := row.SetFormula("SUM(A1:A2)", 10)
170 | if val, _ := c.cell.getAttr("t"); val != "" {
171 | t.Error("cell attribute should be empty but", val)
172 | }
173 | tag = c.cell.Children[0].(*Tag)
174 | if tag.Name.Local != "f" {
175 | t.Error("tag name should be f but", tag.Name.Local)
176 | }
177 | if string(tag.Children[0].(xml.CharData)) != "SUM(A1:A2)" {
178 | t.Error("cell value should be SUM(A1:A2) but", string(tag.Children[0].(xml.CharData)))
179 | }
180 | }
181 | func TestColStringPosition(t *testing.T) {
182 | if ColStringPosition(26) != "Z" {
183 | t.Error("col id should be Z but", ColStringPosition(26))
184 | }
185 | if ColStringPosition(27) != "AA" {
186 | t.Error("col id should be AA but", ColStringPosition(27))
187 | }
188 | if ColStringPosition(52) != "AZ" {
189 | t.Error("col id should be AZ but", ColStringPosition(52))
190 | }
191 | }
192 |
193 | func TestColNumPosition(t *testing.T) {
194 | if ColNumPosition("Z") != 26 {
195 | t.Error("col no should be 26 but", ColNumPosition("Z"))
196 | }
197 | if ColNumPosition("AA") != 27 {
198 | t.Error("col no should be 27 but", ColNumPosition("AA"))
199 | }
200 | }
201 |
202 | func TestCreateRowXML(t *testing.T) {
203 | stdout := new(bytes.Buffer)
204 | tag := &Tag{Name: xml.Name{Local: "row"}}
205 | row := &Row{row: tag}
206 | xml.NewEncoder(stdout).Encode(row)
207 | if string(stdout.Bytes()) != "
" {
208 | t.Error("xml should be empty but", string(stdout.Bytes()))
209 | }
210 |
211 | cellTag := &Tag{Name: xml.Name{Local: "c"}}
212 | row.cells = append(row.cells, &Cell{cell: cellTag})
213 | xml.NewEncoder(stdout).Encode(row)
214 | if string(stdout.Bytes()) != "|
" {
215 | t.Error(`xml should be "|
" but`, string(stdout.Bytes()))
216 | }
217 | }
218 |
219 | func TestRowResetStyleIndex(t *testing.T) {
220 | tag := &Tag{Name: xml.Name{Local: "row"}}
221 | row := &Row{row: tag}
222 | cellTag := &Tag{Name: xml.Name{Local: "c"}}
223 | row.cells = append(row.cells, &Cell{cell: cellTag})
224 | row.resetStyleIndex()
225 | }
226 |
227 | func TestSetRowHeight(t *testing.T) {
228 | tag := &Tag{Name: xml.Name{Local: "row"}}
229 | row := &Row{row: tag}
230 | row.SetHeight(1.23)
231 | if val, err := row.row.getAttr("customHeight"); err != nil {
232 | t.Error(`tag's customHeight attribute should be set but`, err)
233 | } else if val != "1" {
234 | t.Error(`tag's customHeight attribute should be 1 but`, val)
235 | }
236 | if val, err := row.row.getAttr("ht"); err != nil {
237 | t.Error(`tag's ht attribute should be set but`, err)
238 | } else if val != "1.2300" {
239 | t.Error(`tag's ht attribute should be 1.2300 but`, val)
240 | }
241 | }
242 |
243 | func BenchmarkNewRow(b *testing.B) {
244 | row := &Row{rowID: 10}
245 | row.CreateCells(1, b.N)
246 | }
247 |
--------------------------------------------------------------------------------
/shared_strings.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "bytes"
5 | "encoding/xml"
6 | "errors"
7 | "io"
8 | "os"
9 | "path/filepath"
10 | "strings"
11 | )
12 |
13 | // SharedStrings 構造体
14 | type SharedStrings struct {
15 | file *os.File
16 | tempFile *os.File
17 | count int
18 | dir string
19 | afterString string
20 | buffer *bytes.Buffer
21 | }
22 |
23 | // OpenSharedStrings 新しいSharedString構造体を作成する
24 | func OpenSharedStrings(dir string) (*SharedStrings, error) {
25 | var f *os.File
26 | var err error
27 | path := filepath.Join(dir, "xl", "sharedStrings.xml")
28 | if !isFileExist(path) {
29 | f, err = os.Create(path)
30 | if err != nil {
31 | return nil, err
32 | }
33 | f.WriteString("\n")
34 | f.WriteString(``)
35 | f.Seek(0, os.SEEK_SET)
36 | } else {
37 | f, err = os.Open(path)
38 | if err != nil {
39 | return nil, err
40 | }
41 | }
42 | defer f.Close()
43 | tag := &Tag{}
44 | err = xml.NewDecoder(f).Decode(tag)
45 | if err != nil {
46 | return nil, err
47 | }
48 | f.Close()
49 | ss := &SharedStrings{dir: filepath.Join(dir, "xl"), buffer: &bytes.Buffer{}}
50 | ss.setStringCount(tag)
51 | if ss.count == -1 {
52 | return nil, errors.New("The sharedStrings.xml file is currupt.")
53 | }
54 | ss.setSeparatePoint(tag)
55 | var b bytes.Buffer
56 | xml.NewEncoder(&b).Encode(tag)
57 | strs := strings.Split(b.String(), "")
58 | if len(strs) != 2 {
59 | return nil, errors.New("The sharedStrings.xml file is currupt.")
60 | }
61 | ss.file, err = os.Create(path)
62 | if err != nil {
63 | return nil, err
64 | }
65 | ss.tempFile, err = os.Create(filepath.Join(dir, "xl", "__sharedStrings.xml"))
66 | if err != nil {
67 | ss.file.Close()
68 | return nil, err
69 | }
70 |
71 | ss.file.WriteString("\n")
72 | ss.file.WriteString(strs[0])
73 | ss.afterString = strs[1]
74 | return ss, nil
75 | }
76 |
77 | // Close sharedStrings情報をクローズする
78 | func (ss *SharedStrings) Close() error {
79 | if ss == nil {
80 | return nil
81 | }
82 | defer ss.tempFile.Close()
83 | defer ss.file.Close()
84 | var err error
85 | io.Copy(ss.tempFile, ss.buffer)
86 | ss.tempFile.Seek(0, os.SEEK_SET)
87 | if _, err = io.Copy(ss.file, ss.tempFile); err != nil {
88 | return err
89 | }
90 | if _, err = ss.file.WriteString(ss.afterString); err != nil {
91 | return err
92 | }
93 | if err = ss.tempFile.Close(); err != nil {
94 | return err
95 | }
96 | if err = ss.file.Close(); err != nil {
97 | return err
98 | }
99 | os.Remove(filepath.Join(ss.dir, "__sharedStrings.xml"))
100 | ss.tempFile = nil
101 | ss.file = nil
102 | return nil
103 | }
104 |
105 | var (
106 | escQuot = []byte(""") // shorter than """
107 | escApos = []byte("'") // shorter than "'"
108 | escAmp = []byte("&")
109 | escLt = []byte("<")
110 | escGt = []byte(">")
111 | escTab = []byte(" ")
112 | escNl = []byte("
")
113 | escCr = []byte("
")
114 | )
115 |
116 | func escapeText(w io.Writer, s []byte) error {
117 | var esc []byte
118 | output := make([]byte, len(s)*5)
119 | j := 0
120 | for i := 0; i < len(s); i++ {
121 | switch s[i] {
122 | case '"':
123 | esc = escQuot
124 | case '\'':
125 | esc = escApos
126 | case '&':
127 | esc = escAmp
128 | case '<':
129 | esc = escLt
130 | case '>':
131 | esc = escGt
132 | case '\t':
133 | esc = escTab
134 | case '\n':
135 | esc = escNl
136 | case '\r':
137 | esc = escCr
138 | default:
139 | output[j] = s[i]
140 | j++
141 | continue
142 | }
143 | for _, e := range esc {
144 | output[j] = e
145 | j++
146 | }
147 | }
148 | if _, err := w.Write(output[:j]); err != nil {
149 | return err
150 | }
151 | return nil
152 | }
153 |
154 | // AddString 文字列データを追加する
155 | // 戻り値はインデックス情報(0スタート)
156 | func (ss *SharedStrings) AddString(text string) int {
157 | if len(text) != 0 && (text[0] == ' ' || text[len(text)-1] == ' ') {
158 | ss.buffer.WriteString(``)
159 | } else {
160 | ss.buffer.WriteString("")
161 | }
162 | escapeText(ss.buffer, []byte(text))
163 | ss.buffer.WriteString("")
164 | if ss.buffer.Len() > 1024 {
165 | io.Copy(ss.tempFile, ss.buffer)
166 | ss.buffer = &bytes.Buffer{}
167 | }
168 | ss.count++
169 | return ss.count - 1
170 | }
171 |
172 | // setStringCount 文字列のカウントをセットする
173 | func (ss *SharedStrings) setStringCount(tag *Tag) {
174 | if tag.Name.Local != "sst" {
175 | ss.count = -1
176 | return
177 | }
178 | // countとuniqueCountを削除する
179 | var attrs []xml.Attr
180 | for _, attr := range tag.Attr {
181 | if attr.Name.Local == "count" || attr.Name.Local == "uniqueCount" {
182 | continue
183 | }
184 | attrs = append(attrs, attr)
185 | }
186 | tag.Attr = attrs
187 | ss.count = 0
188 | for _, t := range tag.Children {
189 | switch child := t.(type) {
190 | case *Tag:
191 | if child.Name.Local == "si" {
192 | ss.count++
193 | }
194 | }
195 | }
196 | }
197 |
198 | // setSeparatePoint sharedStrings.xmlのセパレートポイントをセットする
199 | func (ss *SharedStrings) setSeparatePoint(tag *Tag) {
200 | if ss.count == 0 {
201 | tag.Children = append(tag.Children, separateTag())
202 | return
203 | }
204 | count := 0
205 | for i := 0; i < len(tag.Children); i++ {
206 | switch child := tag.Children[i].(type) {
207 | case *Tag:
208 | if child.Name.Local == "si" {
209 | count++
210 | if count == ss.count {
211 | children := make([]interface{}, len(tag.Children)+1)
212 | copy(children, tag.Children[:i+1])
213 | children[i+1] = separateTag()
214 | copy(children[i+2:], tag.Children[i+1:])
215 | tag.Children = children
216 | return
217 | }
218 | }
219 | }
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/shared_strings_test.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "bytes"
5 | "encoding/xml"
6 | "io/ioutil"
7 | "os"
8 | "path/filepath"
9 | "testing"
10 | )
11 |
12 | func TestOpenSharedStrings(t *testing.T) {
13 | os.Mkdir("temp/xl", 0755)
14 | defer os.RemoveAll("temp/xl")
15 | _, err := OpenSharedStrings("nopath")
16 | if err == nil {
17 | t.Error("sharedStrings.xml file should not be opened.")
18 | }
19 | ss, err := OpenSharedStrings("temp")
20 | if ss == nil {
21 | t.Error("structure should be created but [", err.Error(), "]")
22 | } else {
23 | if !isFileExist(filepath.Join("temp", "xl", "sharedStrings.xml")) {
24 | t.Error("sharedStrings.xml file should be created.")
25 | }
26 | if ss.count != 0 {
27 | t.Error("count should be 0 but [", ss.count, "]")
28 | }
29 | if !isFileExist(filepath.Join("temp", "xl", "__sharedStrings.xml")) {
30 | t.Error("__sharedStrings.xml should be opened.")
31 | }
32 | ss.Close()
33 | if isFileExist(filepath.Join("temp", "xl", "__sharedStrings.xml")) {
34 | t.Error("__sharedStrings.xml should be removed.")
35 | }
36 | }
37 |
38 | f, _ := os.Create(filepath.Join("temp", "xl", "sharedStrings.xml"))
39 | f.Close()
40 | _, err = OpenSharedStrings("temp")
41 | if err == nil {
42 | t.Error("sharedStrings.xml should not be parsed.")
43 | }
44 |
45 | f, _ = os.Create(filepath.Join("temp", "xl", "sharedStrings.xml"))
46 | f.WriteString("")
47 | f.Close()
48 | _, err = OpenSharedStrings("temp")
49 | if err == nil {
50 | t.Error("sharedStrings.xml file should be currupt.")
51 | }
52 |
53 | f, _ = os.Create(filepath.Join("temp", "xl", "sharedStrings.xml"))
54 | f.WriteString("")
55 | f.Close()
56 | ss, err = OpenSharedStrings("temp")
57 | if err != nil {
58 | t.Error("sharedStrings.xml should be opend.[", err.Error(), "]")
59 | }
60 | if ss.count != 2 {
61 | t.Error("strings count should be 2 but [", ss.count, "]")
62 | }
63 | ss.Close()
64 | f, _ = os.Create(filepath.Join("temp", "xl", "sharedStrings.xml"))
65 | f.WriteString("")
66 | f.Close()
67 | ss, err = OpenSharedStrings("temp")
68 | if err == nil {
69 | t.Error("sharedString.xml should not be opened because the file is currupt.")
70 | }
71 | }
72 |
73 | func TestAddString(t *testing.T) {
74 | os.Mkdir("temp/xl", 0755)
75 | defer os.RemoveAll("temp/xl")
76 | f, _ := os.Create(filepath.Join("temp", "xl", "sharedStrings.xml"))
77 | f.WriteString("")
78 | f.Close()
79 | ss, _ := OpenSharedStrings("temp")
80 | if index := ss.AddString("hello world"); index != 1 {
81 | t.Error("index should be 1 but [", index, "]")
82 | }
83 | ss.Close()
84 | f, _ = os.Open(filepath.Join("temp", "xl", "sharedStrings.xml"))
85 | b, _ := ioutil.ReadAll(f)
86 | f.Close()
87 | if string(b) != "\nhello world" {
88 | t.Error(string(b))
89 | }
90 | }
91 |
92 | func TestEscapeText(t *testing.T) {
93 | buf := new(bytes.Buffer)
94 | escapeText(buf, []byte("\"'&<>\t\n\rあいう"))
95 | if string(buf.Bytes()) != ""'&<>
あいう" {
96 | t.Error("string should be "'&<>
あいう but ", string(buf.Bytes()))
97 | }
98 | }
99 |
100 | func BenchmarkAddString(b *testing.B) {
101 | s := "あいうえお"
102 | f, _ := os.Create("temp/sharedStrings.xml")
103 | //defer f.Close()
104 | //sharedStrings := &SharedStrings{tempFile: f}
105 | for i := 1; i < 100000; i++ {
106 | for j := 0; j < 20; j++ {
107 | //sharedStrings.AddString("あいうえお")
108 | //var b bytes.Buffer
109 | f.WriteString("")
110 | xml.EscapeText(f, []byte(s))
111 | f.WriteString("")
112 |
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/sheet.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "bytes"
5 | "encoding/xml"
6 | "errors"
7 | "fmt"
8 | "os"
9 | "path/filepath"
10 | "strconv"
11 | "strings"
12 | )
13 |
14 | // Sheet struct for control sheet data
15 | type Sheet struct {
16 | xml *SheetXML
17 | opened bool
18 | Rows []*Row
19 | Styles *Styles
20 | worksheet *Tag
21 | sheetView *Tag
22 | cols *Tag
23 | sheetData *Tag
24 | tempFile *os.File
25 | afterString string
26 | sharedStrings *SharedStrings
27 | sheetPath string
28 | tempSheetPath string
29 | colInfos colInfos
30 | maxRow int
31 | target string
32 | }
33 |
34 | // SheetXML sheet.xml information
35 | type SheetXML struct {
36 | XMLName xml.Name `xml:"sheet"`
37 | Name string `xml:"name,attr"`
38 | SheetID string `xml:"sheetId,attr"`
39 | RID string `xml:"id,attr"`
40 | }
41 |
42 | // colInfo Column style information
43 | type colInfo struct {
44 | min int
45 | max int
46 | style string
47 | width float64
48 | customWidth bool
49 | }
50 |
51 | type colInfos []colInfo
52 |
53 | // newSheet create new sheet information.
54 | func newSheet(name string, index int, rid string, target string) *Sheet {
55 | return &Sheet{
56 | xml: &SheetXML{
57 | XMLName: xml.Name{Space: "", Local: "sheet"},
58 | Name: name,
59 | SheetID: fmt.Sprintf("%d", index+1),
60 | RID: rid,
61 | },
62 | target: target,
63 | }
64 | }
65 |
66 | // Create create new sheet
67 | func (sheet *Sheet) Create(dir string) error {
68 | f, err := os.Create(filepath.Join(dir, "xl", sheet.target))
69 | if err != nil {
70 | return err
71 | }
72 | defer f.Close()
73 | f.WriteString(``)
74 | f.WriteString(``)
75 | f.WriteString("")
76 | f.WriteString("")
77 | f.Close()
78 | sheet.Open(dir)
79 | return nil
80 | }
81 |
82 | // Open open sheet.xml in directory
83 | func (sheet *Sheet) Open(dir string) error {
84 | var err error
85 | sheet.sheetPath = filepath.Join(dir, "xl", sheet.target)
86 | sheet.tempSheetPath = filepath.Join(dir, "xl", sheet.target+".tmp")
87 | f, err := os.Open(sheet.sheetPath)
88 | if err != nil {
89 | return err
90 | }
91 | defer f.Close()
92 | tag := &Tag{}
93 | if err = xml.NewDecoder(f).Decode(tag); err != nil {
94 | return err
95 | }
96 | if err = sheet.setData(tag); err != nil {
97 | return err
98 | }
99 | sheet.worksheet = tag
100 | sheet.setSeparatePoint()
101 | if sheet.tempFile, err = os.Create(sheet.tempSheetPath); err != nil {
102 | return err
103 | }
104 | sheet.opened = true
105 | return nil
106 | }
107 |
108 | // Close close sheet
109 | func (sheet *Sheet) Close() error {
110 | var err error
111 | if sheet == nil || sheet.opened == false {
112 | return nil
113 | }
114 | sheet.OutputAll()
115 | if _, err = sheet.tempFile.WriteString(sheet.afterString); err != nil {
116 | return err
117 | }
118 | sheet.tempFile.Close()
119 | os.Remove(sheet.sheetPath)
120 | if err := os.Rename(sheet.tempSheetPath, sheet.sheetPath); err != nil {
121 | return err
122 | }
123 | sheet.opened = false
124 | sheet.worksheet = nil
125 | sheet.sheetView = nil
126 | sheet.sheetData = nil
127 | sheet.tempFile = nil
128 | return nil
129 | }
130 |
131 | func (sheet *Sheet) setData(sheetTag *Tag) error {
132 | if sheetTag.Name.Local != "worksheet" {
133 | return errors.New("The file [" + sheet.sheetPath + "] is currupt.")
134 | }
135 | for _, child := range sheetTag.Children {
136 | switch tag := child.(type) {
137 | case *Tag:
138 | if tag.Name.Local == "sheetData" {
139 | for _, data := range tag.Children {
140 | switch row := data.(type) {
141 | case *Tag:
142 | if row.Name.Local == "row" {
143 | newRow := NewRow(row, sheet.sharedStrings, sheet.Styles)
144 | if newRow == nil {
145 | return errors.New("The file [" + sheet.sheetPath + "] is currupt.")
146 | }
147 | newRow.colInfos = sheet.colInfos
148 | sheet.Rows = append(sheet.Rows, newRow)
149 | sheet.maxRow = newRow.rowID
150 | }
151 | }
152 | }
153 | sheet.sheetData = tag
154 | break
155 | } else if tag.Name.Local == "cols" {
156 | sheet.cols = tag
157 | sheet.colInfos = getColInfos(tag)
158 | } else if tag.Name.Local == "sheetViews" {
159 | for _, view := range tag.Children {
160 | if v, ok := view.(*Tag); ok {
161 | if v.Name.Local == "sheetView" {
162 | sheet.sheetView = v
163 | }
164 | }
165 | }
166 | }
167 | }
168 | }
169 | if sheet.sheetData == nil {
170 | return errors.New("The file[sheet" + sheet.xml.SheetID + ".xml] is currupt. No sheetData tag found.")
171 | }
172 | return nil
173 | }
174 |
175 | func (sheet *Sheet) setSeparatePoint() {
176 | for i := 0; i < len(sheet.worksheet.Children); i++ {
177 | if sheet.cols != nil && sheet.worksheet.Children[i] == sheet.cols {
178 | sheet.worksheet.Children[i] = separateTag()
179 | break
180 | } else if sheet.worksheet.Children[i] == sheet.sheetData {
181 | sheet.cols = &Tag{Name: xml.Name{Local: "cols"}}
182 | sheet.worksheet.Children = append(sheet.worksheet.Children[:i], append([]interface{}{separateTag()},
183 | sheet.worksheet.Children[i:]...)...)
184 | break
185 | }
186 | }
187 | sheet.sheetData.Children = []interface{}{separateTag()}
188 | }
189 |
190 | // CreateRows create multiple rows
191 | func (sheet *Sheet) CreateRows(from int, to int) []*Row {
192 | if sheet.maxRow < to {
193 | sheet.maxRow = to
194 | }
195 | rows := make([]*Row, sheet.maxRow)
196 | for _, row := range sheet.Rows {
197 | if row == nil {
198 | continue
199 | }
200 | rows[row.rowID-1] = row
201 | }
202 | sheet.Rows = rows
203 | for i := from - 1; i < to; i++ {
204 | if rows[i] != nil {
205 | continue
206 | }
207 | attr := []xml.Attr{
208 | xml.Attr{
209 | Name: xml.Name{Local: "r"},
210 | Value: strconv.Itoa(from + i),
211 | },
212 | }
213 | tag := &Tag{
214 | Name: xml.Name{Local: "row"},
215 | Attr: attr,
216 | }
217 | rows[i] = &Row{rowID: i + 1, row: tag, sharedStrings: sheet.sharedStrings}
218 | }
219 | return sheet.Rows
220 | }
221 |
222 | // GetRow get row(from 1)
223 | func (sheet *Sheet) GetRow(rowNo int) *Row {
224 | for _, row := range sheet.Rows {
225 | if row.rowID == rowNo {
226 | return row
227 | }
228 | if row.rowID > rowNo {
229 | break
230 | }
231 | }
232 | // Cell is created if there is no cell NO.
233 | attr := []xml.Attr{
234 | xml.Attr{
235 | Name: xml.Name{Local: "r"},
236 | Value: strconv.Itoa(int(rowNo)),
237 | },
238 | }
239 | tag := &Tag{
240 | Name: xml.Name{Local: "row"},
241 | Attr: attr,
242 | }
243 | row := NewRow(tag, sheet.sharedStrings, sheet.Styles)
244 | row.colInfos = sheet.colInfos
245 | added := false
246 | rows := make([]*Row, len(sheet.Rows)+1)
247 | for i := 0; i < len(sheet.Rows); i++ {
248 | if sheet.Rows[i].rowID < rowNo {
249 | rows[i] = sheet.Rows[i]
250 | } else if sheet.Rows[i].rowID > rowNo {
251 | if !added {
252 | rows[i] = row
253 | added = true
254 | }
255 | rows[i+1] = sheet.Rows[i]
256 | }
257 | }
258 | if !added {
259 | rows[len(sheet.Rows)] = row
260 | }
261 | sheet.Rows = rows
262 | return row
263 | }
264 |
265 | // ShowGridlines switch show/hide grid lines
266 | func (sheet *Sheet) ShowGridlines(show bool) {
267 | if sheet.sheetView != nil {
268 | if show {
269 | sheet.sheetView.setAttr("showGridLines", "1")
270 | } else {
271 | sheet.sheetView.setAttr("showGridLines", "0")
272 | }
273 | }
274 | }
275 |
276 | func (sheet *Sheet) outputFirst() {
277 | var b bytes.Buffer
278 | xml.NewEncoder(&b).Encode(sheet.worksheet)
279 | strs := strings.Split(b.String(), "")
280 | sheet.tempFile.WriteString(strs[0])
281 | if len(sheet.colInfos) != 0 {
282 | xml.NewEncoder(sheet.tempFile).Encode(sheet.colInfos)
283 | }
284 | sheet.tempFile.WriteString(strs[1])
285 | sheet.afterString = strs[2]
286 | sheet.worksheet = nil
287 | }
288 |
289 | // OutputAll output all rows
290 | func (sheet *Sheet) OutputAll() {
291 | if sheet.worksheet != nil {
292 | sheet.outputFirst()
293 | }
294 | var buffer bytes.Buffer
295 | for i, row := range sheet.Rows {
296 | if row != nil {
297 | row.resetStyleIndex()
298 | xml.NewEncoder(&buffer).Encode(sheet.Rows[i])
299 | if i > 0 && i%100 == 0 {
300 | sheet.tempFile.Write(buffer.Bytes())
301 | buffer.Reset()
302 | }
303 | }
304 | }
305 | sheet.tempFile.Write(buffer.Bytes())
306 | sheet.Rows = nil
307 | }
308 |
309 | // OutputThroughRowNo output through to rowno
310 | func (sheet *Sheet) OutputThroughRowNo(rowNo int) {
311 | var i int
312 | if sheet.worksheet != nil {
313 | sheet.outputFirst()
314 | }
315 | var buffer bytes.Buffer
316 | for i = 0; i < len(sheet.Rows); i++ {
317 | if sheet.Rows[i] == nil {
318 | continue
319 | }
320 | if rowNo < sheet.Rows[i].rowID {
321 | break
322 | }
323 | sheet.Rows[i].resetStyleIndex()
324 | xml.NewEncoder(&buffer).Encode(sheet.Rows[i])
325 | if i > 0 && i%100 == 0 {
326 | sheet.tempFile.Write(buffer.Bytes())
327 | buffer.Reset()
328 | }
329 | }
330 | sheet.tempFile.Write(buffer.Bytes())
331 | sheet.tempFile.Sync()
332 |
333 | sheet.Rows = sheet.Rows[i:]
334 | }
335 |
336 | // getColsStyles obtain the style set in the column
337 | func getColInfos(tag *Tag) []colInfo {
338 | var infos []colInfo
339 | for _, child := range tag.Children {
340 | switch t := child.(type) {
341 | case *Tag:
342 | if t.Name.Local != "col" {
343 | continue
344 | }
345 | info := colInfo{width: -1}
346 | for _, attr := range t.Attr {
347 | if attr.Name.Local == "min" {
348 | info.min, _ = strconv.Atoi(attr.Value)
349 | } else if attr.Name.Local == "max" {
350 | info.max, _ = strconv.Atoi(attr.Value)
351 | } else if attr.Name.Local == "style" {
352 | info.style = attr.Value
353 | } else if attr.Name.Local == "width" {
354 | info.width, _ = strconv.ParseFloat(attr.Value, 64)
355 | } else if attr.Name.Local == "customWidth" {
356 | info.customWidth = false
357 | if attr.Value == "1" {
358 | info.customWidth = true
359 | }
360 | }
361 | }
362 | infos = append(infos, info)
363 | }
364 | }
365 | return infos
366 | }
367 |
368 | func getStyleNo(styles []colInfo, colNo int) string {
369 | for _, style := range styles {
370 | if style.min <= 0 || style.max <= 0 {
371 | continue
372 | }
373 | if style.min <= colNo && style.max >= colNo {
374 | return style.style
375 | }
376 | }
377 | return ""
378 | }
379 |
380 | // SetColWidth set column width
381 | func (sheet *Sheet) SetColWidth(width float64, colNo int) {
382 | for i, info := range sheet.colInfos {
383 | if info.min == colNo && colNo == info.max {
384 | // if min and max are same as colNo just replace width and customWidth value
385 | sheet.colInfos[i].width = width
386 | sheet.colInfos[i].customWidth = true
387 | return
388 | } else if info.min == colNo {
389 | // insert info before index
390 | info.width = width
391 | info.max = colNo
392 | info.customWidth = true
393 | sheet.colInfos[i].min++
394 | if i > 0 {
395 | sheet.colInfos = append(sheet.colInfos[:i-1], append([]colInfo{info}, sheet.colInfos[i-1:]...)...)
396 | } else {
397 | sheet.colInfos = append([]colInfo{info}, sheet.colInfos...)
398 | }
399 | return
400 | } else if info.max == colNo {
401 | // insert info after index
402 | sheet.colInfos[i].max--
403 | info.width = width
404 | info.min = colNo
405 | info.customWidth = true
406 | sheet.colInfos = append(sheet.colInfos[:i+1], append([]colInfo{info}, sheet.colInfos[i+1:]...)...)
407 | return
408 | } else if info.min < colNo && colNo < info.max {
409 | // devide three deferent informations
410 | beforeInfo := info.clone()
411 | afterInfo := info.clone()
412 | beforeInfo.max = colNo - 1
413 | afterInfo.min = colNo + 1
414 | info.width = width
415 | info.min = colNo
416 | info.max = colNo
417 | info.customWidth = true
418 | sheet.colInfos = append(sheet.colInfos[:i], append([]colInfo{beforeInfo, info, afterInfo}, sheet.colInfos[i+1:]...)...)
419 | return
420 | } else if info.min > colNo {
421 | // insert colInfo after index
422 | info.width = width
423 | info.min = colNo
424 | info.max = colNo
425 | info.customWidth = true
426 | sheet.colInfos = append(sheet.colInfos[:i], append([]colInfo{info}, sheet.colInfos[i:]...)...)
427 | return
428 | }
429 | }
430 | info := colInfo{min: colNo, max: colNo, width: width, customWidth: true}
431 | sheet.colInfos = append(sheet.colInfos, info)
432 | }
433 |
434 | func (info colInfo) clone() colInfo {
435 | return colInfo{min: info.min, max: info.max, style: info.style, width: info.width, customWidth: info.customWidth}
436 | }
437 |
438 | func (infos colInfos) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
439 | start.Name = xml.Name{Local: "cols"}
440 | e.EncodeToken(start)
441 | for _, info := range infos {
442 | if err := e.Encode(info); err != nil {
443 | return err
444 | }
445 | }
446 | e.EncodeToken(start.End())
447 | return nil
448 | }
449 |
450 | func (info colInfo) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
451 | start.Name = xml.Name{Local: "col"}
452 | start.Attr = []xml.Attr{
453 | xml.Attr{Name: xml.Name{Local: "min"}, Value: strconv.Itoa(info.min)},
454 | xml.Attr{Name: xml.Name{Local: "max"}, Value: strconv.Itoa(info.max)},
455 | }
456 | if info.style != "" {
457 | start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "style"}, Value: info.style})
458 | }
459 | if info.customWidth || info.width != -1 {
460 | if info.customWidth {
461 | start.Attr = append(start.Attr, []xml.Attr{
462 | xml.Attr{Name: xml.Name{Local: "width"}, Value: fmt.Sprint(info.width)},
463 | xml.Attr{Name: xml.Name{Local: "customWidth"}, Value: "1"},
464 | }...)
465 | } else {
466 | start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "width"}, Value: fmt.Sprint(info.width)})
467 | }
468 | }
469 | e.EncodeToken(start)
470 | e.EncodeToken(start.End())
471 | return nil
472 | }
473 |
--------------------------------------------------------------------------------
/sheet_test.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "bytes"
5 | "io/ioutil"
6 | "os"
7 | "testing"
8 | )
9 |
10 | func TestNewSheet(t *testing.T) {
11 | sheet := newSheet("hello", 0, "rId1", "worksheets/sheet1.xml")
12 | if sheet.xml.Name != "hello" {
13 | t.Error(`sheet name should be "hello".`)
14 | }
15 | if sheet.xml.SheetID != "1" {
16 | t.Error(`sheet id should be "1" but [`, sheet.xml.SheetID, "]")
17 | }
18 | }
19 |
20 | func TestOpen(t *testing.T) {
21 | os.MkdirAll("temp/xl/worksheets", 0755)
22 | defer os.RemoveAll("temp/xl")
23 | sheet := newSheet("hello", 0, "rId1", "worksheets/sheet1.xml")
24 | err := sheet.Open("")
25 | if err == nil {
26 | t.Error("sheet should not be opened. sheet does not exsist.")
27 | }
28 | f, _ := os.Create("temp/xl/worksheets/sheet1.xml")
29 | f.Close()
30 | if err = sheet.Open("temp1"); err == nil {
31 | t.Error("sheet should not be opened. file path does not exist.")
32 | }
33 | if err = sheet.Open("temp"); err == nil {
34 | t.Error("sheet should not be opened. xml file is currupt.")
35 | }
36 | f, _ = os.Create("temp/xl/worksheets/sheet1.xml")
37 | f.WriteString("")
38 | f.Close()
39 | if err = sheet.Open("temp"); err == nil {
40 | t.Error("sheet should not be opened. worksheet tag does not exist.")
41 | }
42 | f, _ = os.Create("temp/xl/worksheets/sheet1.xml")
43 | f.WriteString("")
44 | f.Close()
45 | if err = sheet.Open("temp"); err == nil {
46 | t.Error("sheet should not be opened. sheetData tag does not exist.")
47 | }
48 | f, _ = os.Create("temp/xl/worksheets/sheet1.xml")
49 | str := "
"
50 | f.WriteString(str)
51 | f.Close()
52 | if err = sheet.Open("temp"); err != nil {
53 | t.Error("sheet should be opened. [", err.Error(), "]")
54 | } else {
55 | sheet.Close()
56 | f, _ = os.Open("temp/xl/worksheets/sheet1.xml")
57 | b, _ := ioutil.ReadAll(f)
58 | f.Close()
59 | if string(b) != str {
60 | t.Error("new sheet file should be same as before string. [", string(b), "]")
61 | }
62 | }
63 | }
64 | func TestClose(t *testing.T) {
65 | var err error
66 | sheet := newSheet("sheet", 0, "rId1", "worksheets/sheet1.xml")
67 | if err = sheet.Close(); err != nil {
68 | t.Error("sheet should be closed because sheet is not opened.")
69 | }
70 | sheet.opened = true
71 | if err = sheet.Close(); err == nil {
72 | t.Error("sheet should not be closed because tempFile does not exist.")
73 | }
74 | }
75 |
76 | func TestGetRow(t *testing.T) {
77 | //var err error
78 | sheet := &Sheet{}
79 | row := &Row{rowID: 2}
80 | sheet.Rows = append(sheet.Rows, row)
81 | row2 := sheet.GetRow(2)
82 | if row != row2 {
83 | t.Error("row should be same.")
84 | }
85 | if row3 := sheet.GetRow(3); row3.rowID != 3 {
86 | t.Error("rowID should be 3 but [", row3.rowID, "]")
87 | }
88 | if row1 := sheet.GetRow(1); row1.rowID != 1 {
89 | t.Error("rowID should be 1 but [", row1.rowID, "]")
90 | }
91 | }
92 |
93 | func TestShowGridlines(t *testing.T) {
94 | os.MkdirAll("temp/xl/worksheets", 0755)
95 | defer os.RemoveAll("temp/xl")
96 | sheet := newSheet("hoge", 0, "rId1", "worksheets/sheet1.xml")
97 | sheet.ShowGridlines(true)
98 | if sheet.sheetView != nil {
99 | t.Error("sheetView should be nil.")
100 | }
101 | sheet.Create("temp")
102 | sheet.ShowGridlines(true)
103 |
104 | if v, err := sheet.sheetView.getAttr("showGridLines"); err != nil {
105 | t.Error("showGridLines should be exist.")
106 | } else if v != "1" {
107 | t.Error("value should be 1 but ", v)
108 | }
109 | sheet.Close()
110 |
111 | if b, err := ioutil.ReadFile("temp/xl/worksheets/sheet1.xml"); err != nil {
112 | t.Error("sheet1.xml should be readable.", err.Error())
113 | } else if string(b) != `` {
114 | t.Error("[" + string(b) + "]")
115 | }
116 | os.Remove("temp/xl/worksheets/sheet1.xml")
117 |
118 | sheet = newSheet("hoge", 1, "rId2", "worksheets/sheet2.xml")
119 | sheet.Create("temp")
120 | sheet.ShowGridlines(false)
121 | if v, err := sheet.sheetView.getAttr("showGridLines"); err != nil {
122 | t.Error("showGridLines should be exist.")
123 | } else if v != "0" {
124 | t.Error("value should be 0 but ", v)
125 | }
126 | sheet.Close()
127 | b, err := ioutil.ReadFile("temp/xl/worksheets/sheet2.xml")
128 | if err != nil {
129 | t.Error("sheet2.xml should be readable.", err.Error())
130 | } else if string(b) != `` {
131 | t.Error(string(b))
132 | }
133 | os.Remove("temp/xl/worksheets/sheet2.xml")
134 | }
135 |
136 | func TestColsWidth(t *testing.T) {
137 | os.MkdirAll("temp/xl/worksheets", 0755)
138 | defer os.RemoveAll("temp/xl")
139 | f, _ := os.Create("temp/xl/worksheets/sheet1.xml")
140 | f.WriteString(``)
141 | f.Close()
142 | sheet := newSheet("sheet1", 0, "rId1", "worksheets/sheet1.xml")
143 | sheet.Open("temp")
144 | defer sheet.Close()
145 | sheet.SetColWidth(1.2, 2)
146 | sheet.SetColWidth(1.1, 1)
147 | sheet.Close()
148 | b, _ := ioutil.ReadFile("temp/xl/worksheets/sheet1.xml")
149 | str := ``
150 | if string(b) != str {
151 | t.Error("file string should be [", str, "] but", string(b))
152 | }
153 |
154 | f, _ = os.Create("temp/xl/worksheets/sheet1.xml")
155 | f.WriteString(``)
156 | f.Close()
157 | sheet = newSheet("sheet1", 0, "rId1", "worksheets/sheet1.xml")
158 | sheet.Open("temp")
159 | defer sheet.Close()
160 |
161 | sheet.SetColWidth(2.2, 2)
162 | sheet.SetColWidth(6.6, 6)
163 | sheet.SetColWidth(4.4, 4)
164 | sheet.Close()
165 | b, _ = ioutil.ReadFile("temp/xl/worksheets/sheet1.xml")
166 | str = ``
167 | if string(b) != str {
168 | t.Error("file string should be [", str, "] but", string(b))
169 | }
170 | }
171 |
172 | func BenchmarkCreateRows(b *testing.B) {
173 | f, _ := os.Create("temp/__sharedStrings_utf8.xml")
174 | f2, _ := os.Create("temp/__sheet_utf8.xml")
175 | utf8 := "あいうえお"
176 | defer f.Close()
177 | defer f2.Close()
178 | sharedStrings := &SharedStrings{tempFile: f, buffer: &bytes.Buffer{}}
179 | sheet := &Sheet{sharedStrings: sharedStrings, tempFile: f2}
180 | for j := 0; j < 10; j++ {
181 | rows := sheet.CreateRows(10000*j+1, 10000*(j+1))
182 | for i := 10000 * j; i < 10000*(j+1); i++ {
183 | cells := rows[i].CreateCells(1, 20)
184 | for _, cell := range cells {
185 | cell.SetString(utf8)
186 | }
187 | }
188 | sheet.OutputThroughRowNo(10000 * (j + 1))
189 | }
190 | f2.Close()
191 | f.Close()
192 | }
193 |
194 | func BenchmarkCreateRowsNumber(b *testing.B) {
195 | f2, _ := os.Create("temp/__sheet_number.xml")
196 | defer f2.Close()
197 | sheet := &Sheet{tempFile: f2}
198 | for j := 0; j < 10; j++ {
199 | rows := sheet.CreateRows(10000*j+1, 10000*(j+1))
200 | for i := 10000 * j; i < 10000*(j+1); i++ {
201 | cells := rows[i].CreateCells(1, 20)
202 | for _, cell := range cells {
203 | cell.SetNumber("12345678901234567890")
204 | }
205 | }
206 | sheet.OutputThroughRowNo(10000 * (j + 1))
207 | }
208 | f2.Close()
209 | }
210 |
--------------------------------------------------------------------------------
/styles.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "encoding/xml"
5 | "errors"
6 | "os"
7 | "path/filepath"
8 | "strconv"
9 | )
10 |
11 | const defaultMaxNumfmt = 200
12 |
13 | // Styles スタイルの情報を持った構造体
14 | type Styles struct {
15 | path string
16 | styles *Tag
17 | numFmts *Tag
18 | fonts *Tag
19 | fills *Tag
20 | borders *Tag
21 | cellStyleXfs *Tag
22 | cellXfs *Tag
23 | styleList []*Style
24 | numFmtNumber int
25 | fontCount int
26 | fillCount int
27 | borderCount int
28 | backgroundColors map[string]int
29 | }
30 |
31 | // Style セルの書式情報
32 | type Style struct {
33 | xf *Tag
34 | NumFmtID int
35 | FontID int
36 | FillID int
37 | BorderID int
38 | XfID int
39 | applyNumberFormat int
40 | applyFont int
41 | applyFill int
42 | applyBorder int
43 | applyAlignment int
44 | applyProtection int
45 | Horizontal string
46 | Vertical string
47 | Wrap int
48 | }
49 |
50 | // Font フォントの設定
51 | type Font struct {
52 | Size int
53 | Color string
54 | Name string
55 | Bold bool
56 | Italic bool
57 | Underline bool
58 | }
59 |
60 | // BorderSetting 罫線の設定
61 | type BorderSetting struct {
62 | Style string
63 | Color string
64 | }
65 |
66 | // Border 罫線の設定
67 | type Border struct {
68 | Left *BorderSetting
69 | Right *BorderSetting
70 | Top *BorderSetting
71 | Bottom *BorderSetting
72 | }
73 |
74 | // createStyles styles.xmlを作成する
75 | func createStyles(dir string) error {
76 | os.Mkdir(filepath.Join(dir, "xl"), 0755)
77 | path := filepath.Join(dir, "xl", "styles.xml")
78 | f, err := os.Create(path)
79 | if err != nil {
80 | return err
81 | }
82 | defer f.Close()
83 | f.WriteString("\n")
84 | f.WriteString(``)
85 | f.WriteString(``)
86 | f.WriteString(``)
87 | f.WriteString(``)
88 | f.WriteString(``)
89 | f.WriteString(``)
90 | f.WriteString(``)
91 | f.Close()
92 | return nil
93 | }
94 |
95 | // OpenStyles styles.xmlファイルを開く
96 | func OpenStyles(dir string) (*Styles, error) {
97 | var f *os.File
98 | var err error
99 | path := filepath.Join(dir, "xl", "styles.xml")
100 | f, err = os.Open(path)
101 | if err != nil {
102 | return nil, err
103 | }
104 | defer f.Close()
105 | tag := &Tag{}
106 | err = xml.NewDecoder(f).Decode(tag)
107 | if err != nil {
108 | return nil, err
109 | }
110 | styles := &Styles{styles: tag, path: path}
111 | err = styles.setData()
112 | if err != nil {
113 | return nil, err
114 | }
115 | return styles, nil
116 | }
117 |
118 | // Close styles.xmlファイルを閉じる
119 | func (styles *Styles) Close() error {
120 | if styles == nil {
121 | return nil
122 | }
123 | f, err := os.Create(styles.path)
124 | if err != nil {
125 | return err
126 | }
127 | defer f.Close()
128 | f.WriteString("\n")
129 | err = xml.NewEncoder(f).Encode(styles)
130 | if err != nil {
131 | return err
132 | }
133 | return nil
134 | }
135 |
136 | func (styles *Styles) setData() error {
137 | tag := styles.styles
138 | if tag == nil {
139 | return errors.New("Tag (Styles.styles) is nil.")
140 | }
141 | if tag.Name.Local != "styleSheet" {
142 | return errors.New("The styles.xml file is currupt.")
143 | }
144 | styles.numFmtNumber = defaultMaxNumfmt
145 | for _, child := range tag.Children {
146 | switch tag := child.(type) {
147 | case *Tag:
148 | switch tag.Name.Local {
149 | case "numFmts":
150 | styles.numFmts = tag
151 | styles.setNumFmtNumber()
152 | case "fonts":
153 | styles.fonts = tag
154 | styles.setFontCount()
155 | case "fills":
156 | styles.fills = tag
157 | styles.setFillCount()
158 | case "borders":
159 | styles.borders = tag
160 | styles.setBorderCount()
161 | case "cellStyleXfs":
162 | styles.cellStyleXfs = tag
163 | case "cellXfs":
164 | styles.cellXfs = tag
165 | styles.setStyleList()
166 | }
167 | }
168 | }
169 | return nil
170 | }
171 |
172 | func (styles *Styles) setStyleList() {
173 | for _, child := range styles.cellXfs.Children {
174 | switch child.(type) {
175 | case *Tag:
176 | t := child.(*Tag)
177 | if t.Name.Local == "xf" {
178 | style := &Style{xf: t}
179 | for _, attr := range t.Attr {
180 | index, _ := strconv.Atoi(attr.Value)
181 | switch attr.Name.Local {
182 | case "numFmtId":
183 | style.NumFmtID = index
184 | case "fontId":
185 | style.FontID = index
186 | case "fillId":
187 | style.FillID = index
188 | case "borderId":
189 | style.BorderID = index
190 | case "applyNumberFormat":
191 | style.applyNumberFormat = index
192 | case "applyFont":
193 | style.applyFont = index
194 | case "applyFill":
195 | style.applyFill = index
196 | case "applyBorder":
197 | style.applyBorder = index
198 | case "applyAlignment":
199 | style.applyAlignment = index
200 | case "applyProtection":
201 | style.applyProtection = index
202 | }
203 | }
204 | // alignment
205 | if style.applyAlignment == 1 {
206 | for _, xfChild := range t.Children {
207 | switch xfChild.(type) {
208 | case *Tag:
209 | cTag := xfChild.(*Tag)
210 | if cTag.Name.Local == "alignment" {
211 | for _, attr := range cTag.Attr {
212 | if attr.Name.Local == "horizontal" {
213 | style.Horizontal = attr.Value
214 | } else if attr.Name.Local == "vertical" {
215 | style.Vertical = attr.Value
216 | } else if attr.Name.Local == "wrapText" {
217 | style.Wrap, _ = strconv.Atoi(attr.Value)
218 | }
219 | }
220 | }
221 | break
222 | }
223 | }
224 | }
225 | styles.styleList = append(styles.styleList, style)
226 | }
227 | }
228 | }
229 | }
230 |
231 | // setNumFmtNumber フォーマットID
232 | func (styles *Styles) setNumFmtNumber() {
233 | max := defaultMaxNumfmt
234 | for _, child := range styles.numFmts.Children {
235 | switch tag := child.(type) {
236 | case *Tag:
237 | for _, attr := range tag.Attr {
238 | if attr.Name.Local == "numFmtId" {
239 | i, _ := strconv.Atoi(attr.Value)
240 | if max <= i {
241 | max = i + 1
242 | }
243 | }
244 | }
245 | }
246 | }
247 | styles.numFmtNumber = max
248 | }
249 |
250 | func (styles *Styles) setFontCount() {
251 | for _, tag := range styles.fonts.Children {
252 | switch tag.(type) {
253 | case *Tag:
254 | if tag.(*Tag).Name.Local == "font" {
255 | styles.fontCount++
256 | }
257 | }
258 | }
259 | }
260 |
261 | func (styles *Styles) setFillCount() {
262 | for _, tag := range styles.fills.Children {
263 | switch tag.(type) {
264 | case *Tag:
265 | if tag.(*Tag).Name.Local == "fill" {
266 | styles.fillCount++
267 | }
268 | }
269 | }
270 | }
271 |
272 | func (styles *Styles) setBorderCount() {
273 | for _, tag := range styles.borders.Children {
274 | switch tag.(type) {
275 | case *Tag:
276 | if tag.(*Tag).Name.Local == "border" {
277 | styles.borderCount++
278 | }
279 | }
280 | }
281 | }
282 |
283 | // SetNumFmt 数値フォーマットをセットする
284 | func (styles *Styles) SetNumFmt(format string) int {
285 | if styles.numFmts == nil {
286 | styles.numFmts = &Tag{Name: xml.Name{Local: "numFmts"}}
287 | }
288 | tag := &Tag{Name: xml.Name{Local: "numFmt"}}
289 | tag.setAttr("numFmtId", strconv.Itoa(styles.numFmtNumber))
290 | tag.setAttr("formatCode", format)
291 | styles.numFmts.Children = append(styles.numFmts.Children, tag)
292 | styles.numFmtNumber++
293 | return styles.numFmtNumber - 1
294 | }
295 |
296 | // SetFont フォント情報を追加する
297 | func (styles *Styles) SetFont(font Font) int {
298 | tag := &Tag{Name: xml.Name{Local: "font"}}
299 | var t *Tag
300 | if font.Size > 0 {
301 | t = &Tag{Name: xml.Name{Local: "sz"}}
302 | t.setAttr("val", strconv.Itoa(font.Size))
303 | tag.Children = append(tag.Children, t)
304 | }
305 | if font.Name != "" {
306 | t = &Tag{Name: xml.Name{Local: "name"}}
307 | t.setAttr("val", font.Name)
308 | tag.Children = append(tag.Children, t)
309 | }
310 | if font.Color != "" {
311 | t = &Tag{Name: xml.Name{Local: "color"}}
312 | t.setAttr("rgb", font.Color)
313 | tag.Children = append(tag.Children, t)
314 | }
315 | if font.Bold {
316 | t = &Tag{Name: xml.Name{Local: "b"}}
317 | tag.Children = append(tag.Children, t)
318 | }
319 | if font.Italic {
320 | t = &Tag{Name: xml.Name{Local: "i"}}
321 | tag.Children = append(tag.Children, t)
322 | }
323 | if font.Underline {
324 | t = &Tag{Name: xml.Name{Local: "u"}}
325 | tag.Children = append(tag.Children, t)
326 | }
327 | styles.fonts.Children = append(styles.fonts.Children, tag)
328 | styles.fontCount++
329 | return styles.fontCount - 1
330 | }
331 |
332 | // SetBackgroundColor 背景色を追加する
333 | func (styles *Styles) SetBackgroundColor(color string) int {
334 | if styles.backgroundColors == nil {
335 | styles.backgroundColors = map[string]int{}
336 | } else if val, ok := styles.backgroundColors[color]; ok {
337 | return val
338 | }
339 | tag := &Tag{Name: xml.Name{Local: "fill"}}
340 | patternFill := &Tag{Name: xml.Name{Local: "patternFill"}}
341 | patternFill.setAttr("patternType", "solid")
342 | fgColor := &Tag{Name: xml.Name{Local: "fgColor"}}
343 | fgColor.setAttr("rgb", color)
344 | patternFill.Children = []interface{}{fgColor}
345 | tag.Children = []interface{}{patternFill}
346 | styles.fills.Children = append(styles.fills.Children, tag)
347 | styles.backgroundColors[color] = styles.fillCount
348 | styles.fillCount++
349 | return styles.fillCount - 1
350 | }
351 |
352 | // SetBorder 罫線を設定する
353 | func (styles *Styles) SetBorder(border Border) int {
354 | var color *Tag
355 | tag := &Tag{Name: xml.Name{Local: "border"}}
356 | left := &Tag{Name: xml.Name{Local: "left"}}
357 | right := &Tag{Name: xml.Name{Local: "right"}}
358 | top := &Tag{Name: xml.Name{Local: "top"}}
359 | bottom := &Tag{Name: xml.Name{Local: "bottom"}}
360 |
361 | if border.Left != nil {
362 | left.setAttr("style", border.Left.Style)
363 | if border.Left.Color != "" {
364 | color = &Tag{Name: xml.Name{Local: "color"}}
365 | color.setAttr("rgb", border.Left.Color)
366 | left.Children = []interface{}{color}
367 | }
368 | }
369 | if border.Right != nil {
370 | right.setAttr("style", border.Right.Style)
371 | if border.Right.Color != "" {
372 | color = &Tag{Name: xml.Name{Local: "color"}}
373 | color.setAttr("rgb", border.Right.Color)
374 | right.Children = []interface{}{color}
375 | }
376 | }
377 |
378 | if border.Top != nil {
379 | top.setAttr("style", border.Top.Style)
380 | if border.Top.Color != "" {
381 | color = &Tag{Name: xml.Name{Local: "color"}}
382 | color.setAttr("rgb", border.Top.Color)
383 | top.Children = []interface{}{color}
384 | }
385 | }
386 | if border.Bottom != nil {
387 | bottom.setAttr("style", border.Bottom.Style)
388 | if border.Bottom.Color != "" {
389 | color = &Tag{Name: xml.Name{Local: "color"}}
390 | color.setAttr("rgb", border.Bottom.Color)
391 | bottom.Children = []interface{}{color}
392 | }
393 | }
394 |
395 | tag.Children = append(tag.Children, left)
396 | tag.Children = append(tag.Children, right)
397 | tag.Children = append(tag.Children, top)
398 | tag.Children = append(tag.Children, bottom)
399 | styles.borders.Children = append(styles.borders.Children, tag)
400 | styles.borderCount++
401 | return styles.borderCount - 1
402 | }
403 |
404 | // SetStyle セルの書式を設定
405 | func (styles *Styles) SetStyle(style *Style) int {
406 | // すでに同じ書式が存在する場合はその書式を使用する
407 | for index, s := range styles.styleList {
408 | if s.NumFmtID == style.NumFmtID &&
409 | s.FontID == style.FontID &&
410 | s.FillID == style.FillID &&
411 | s.BorderID == style.BorderID &&
412 | s.XfID == style.XfID &&
413 | s.Horizontal == style.Horizontal &&
414 | s.Vertical == style.Vertical &&
415 | s.Wrap == style.Wrap {
416 | return index
417 | }
418 | }
419 | return styles.SetCellXfs(style)
420 | }
421 |
422 | // GetStyle Style構造体を取得する
423 | func (styles *Styles) GetStyle(index int) *Style {
424 | if len(styles.styleList) <= index {
425 | return nil
426 | }
427 | return styles.styleList[index]
428 | }
429 |
430 | // SetCellXfs cellXfsにタグを追加する
431 | func (styles *Styles) SetCellXfs(style *Style) int {
432 | s := &Style{
433 | NumFmtID: style.NumFmtID,
434 | FontID: style.FontID,
435 | FillID: style.FillID,
436 | BorderID: style.BorderID,
437 | XfID: style.XfID,
438 | Horizontal: style.Horizontal,
439 | Vertical: style.Vertical,
440 | Wrap: style.Wrap,
441 | }
442 | attr := []xml.Attr{
443 | xml.Attr{
444 | Name: xml.Name{Local: "numFmtId"},
445 | Value: strconv.Itoa(style.NumFmtID),
446 | },
447 | xml.Attr{
448 | Name: xml.Name{Local: "fontId"},
449 | Value: strconv.Itoa(style.FontID),
450 | },
451 | xml.Attr{
452 | Name: xml.Name{Local: "fillId"},
453 | Value: strconv.Itoa(style.FillID),
454 | },
455 | xml.Attr{
456 | Name: xml.Name{Local: "borderId"},
457 | Value: strconv.Itoa(style.BorderID),
458 | },
459 | xml.Attr{
460 | Name: xml.Name{Local: "xfId"},
461 | Value: strconv.Itoa(style.XfID),
462 | },
463 | }
464 | if style.NumFmtID != 0 {
465 | attr = append(attr, xml.Attr{
466 | Name: xml.Name{Local: "applyNumberFormat"},
467 | Value: "1",
468 | })
469 | s.applyNumberFormat = 1
470 | }
471 | if style.FontID != 0 {
472 | attr = append(attr, xml.Attr{
473 | Name: xml.Name{Local: "applyFont"},
474 | Value: "1",
475 | })
476 | s.applyFont = 1
477 | }
478 | if style.FillID != 0 {
479 | attr = append(attr, xml.Attr{
480 | Name: xml.Name{Local: "applyFill"},
481 | Value: "1",
482 | })
483 | s.applyFill = 1
484 | }
485 | if style.BorderID != 0 {
486 | attr = append(attr, xml.Attr{
487 | Name: xml.Name{Local: "applyBorder"},
488 | Value: "1",
489 | })
490 | s.applyBorder = 1
491 | }
492 | tag := &Tag{
493 | Name: xml.Name{Local: "xf"},
494 | Attr: attr,
495 | }
496 | if style.Horizontal != "" || style.Vertical != "" || style.Wrap != 0 {
497 | alignment := &Tag{Name: xml.Name{Local: "alignment"}}
498 | if style.Horizontal != "" {
499 | alignment.setAttr("horizontal", style.Horizontal)
500 | }
501 | if style.Vertical != "" {
502 | alignment.setAttr("vertical", style.Vertical)
503 | }
504 | if style.Wrap != 0 {
505 | alignment.setAttr("wrapText", strconv.Itoa(style.Wrap))
506 | }
507 | tag.Children = []interface{}{alignment}
508 | tag.Attr = append(tag.Attr, xml.Attr{
509 | Name: xml.Name{Local: "applyAlignment"},
510 | Value: "1",
511 | })
512 | s.applyAlignment = 1
513 | }
514 | s.xf = tag
515 | styles.cellXfs.Children = append(styles.cellXfs.Children, tag)
516 | styles.styleList = append(styles.styleList, s)
517 | return len(styles.styleList) - 1
518 | }
519 |
520 | // MarshalXML stylesからXMLを作り直す
521 | func (styles *Styles) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
522 | start.Name = styles.styles.Name
523 | start.Attr = styles.styles.Attr
524 | e.EncodeToken(start)
525 | if styles.numFmts != nil {
526 | e.Encode(styles.numFmts)
527 | }
528 | if styles.fonts != nil {
529 | e.Encode(styles.fonts)
530 | }
531 | if styles.fills != nil {
532 | e.Encode(styles.fills)
533 | }
534 | if styles.borders != nil {
535 | e.Encode(styles.borders)
536 | }
537 | if styles.cellStyleXfs != nil {
538 | e.Encode(styles.cellStyleXfs)
539 | }
540 | if styles.cellXfs != nil {
541 | e.Encode(styles.cellXfs)
542 | }
543 | outputsList := []string{"numFmts", "fonts", "fills", "borders", "cellStyleXfs", "cellXfs"}
544 | for _, v := range styles.styles.Children {
545 | switch v.(type) {
546 | case *Tag:
547 | child := v.(*Tag)
548 | if !IsExistString(outputsList, child.Name.Local) {
549 | if err := e.Encode(child); err != nil {
550 | return err
551 | }
552 | }
553 | }
554 | }
555 | e.EncodeToken(start.End())
556 | return nil
557 | }
558 |
559 | // IsExistString 配列内に文字列が存在するかを確認する
560 | func IsExistString(strs []string, str string) bool {
561 | for _, s := range strs {
562 | if s == str {
563 | return true
564 | }
565 | }
566 | return false
567 | }
568 |
--------------------------------------------------------------------------------
/styles_test.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "bytes"
5 | "encoding/xml"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 | "testing"
10 | )
11 |
12 | func TestOpenStypes(t *testing.T) {
13 | os.Mkdir(filepath.Join("temp", "xl"), 0755)
14 | defer os.RemoveAll(filepath.Join("temp", "xl"))
15 |
16 | _, err := OpenStyles("nopath")
17 | if err == nil {
18 | t.Error("styles.xml should not be opened.")
19 | }
20 | f, _ := os.Create(filepath.Join("temp", "xl", "styles.xml"))
21 | f.Close()
22 | _, err = OpenStyles("temp")
23 | if err == nil {
24 | t.Error("styles.xml should not be opened because syntax error.")
25 | }
26 | f, _ = os.Create(filepath.Join("temp", "xl", "styles.xml"))
27 | f.WriteString("")
28 | f.Close()
29 | _, err = OpenStyles("temp")
30 | if err == nil {
31 | t.Error("styles.xml should not be opened because worksheet tag does not exist.")
32 | }
33 |
34 | f, _ = os.Create(filepath.Join("temp", "xl", "styles.xml"))
35 | f.WriteString("")
36 | f.Close()
37 | if _, err := OpenStyles("temp"); err != nil {
38 | t.Error("styles.xml should be opened but", err.Error())
39 | }
40 | }
41 |
42 | func TestSetData(t *testing.T) {
43 | styles := &Styles{}
44 | err := styles.setData()
45 | if err == nil {
46 | t.Error("error should be occurred because worksheet tag is not exist.")
47 | }
48 | styles.styles = &Tag{}
49 | err = styles.setData()
50 | if err == nil {
51 | t.Error("error should be occurred because worksheet tag is not exist.")
52 | }
53 | styles.styles = &Tag{Name: xml.Name{Local: "styleSheet"}}
54 | err = styles.setData()
55 | if err != nil {
56 | t.Error("error should not be occurred.", err.Error())
57 | }
58 | if styles.numFmtNumber != defaultMaxNumfmt {
59 | t.Error("styles.numFmtNumber should be ", defaultMaxNumfmt, " but ", styles.numFmtNumber)
60 | }
61 | fonts := &Tag{Name: xml.Name{Local: "fonts"}}
62 | fills := &Tag{Name: xml.Name{Local: "fills"}}
63 | borders := &Tag{Name: xml.Name{Local: "borders"}}
64 | cellStyleXfs := &Tag{Name: xml.Name{Local: "cellStyleXfs"}}
65 | cellXfs := &Tag{Name: xml.Name{Local: "cellXfs"}}
66 | cellStyles := &Tag{Name: xml.Name{Local: "cellStyles"}}
67 | dxfs := &Tag{Name: xml.Name{Local: "dxfs"}}
68 | extLst := &Tag{Name: xml.Name{Local: "extLst"}}
69 | tag := styles.styles
70 | tag.Children = append(tag.Children, fonts)
71 | tag.Children = append(tag.Children, fills)
72 | tag.Children = append(tag.Children, borders)
73 | tag.Children = append(tag.Children, cellStyleXfs)
74 | tag.Children = append(tag.Children, cellXfs)
75 | tag.Children = append(tag.Children, cellStyles)
76 | tag.Children = append(tag.Children, dxfs)
77 | tag.Children = append(tag.Children, extLst)
78 | err = styles.setData()
79 | if err != nil {
80 | t.Error("error should not be occurred.", err.Error())
81 | }
82 | }
83 |
84 | func TestSetStyleList(t *testing.T) {
85 | styles := &Styles{}
86 | r := strings.NewReader(``)
87 | tag := &Tag{}
88 | xml.NewDecoder(r).Decode(tag)
89 | styles.cellXfs = tag
90 | styles.setStyleList()
91 | if len(styles.styleList) != 0 {
92 | t.Error("styleList should be 0 but ", styles.styleList)
93 | }
94 | r = strings.NewReader(``)
95 | tag = &Tag{}
96 | xml.NewDecoder(r).Decode(tag)
97 | styles.cellXfs = tag
98 | styles.setStyleList()
99 | if len(styles.styleList) != 1 {
100 | t.Error("styleList should be 1 but ", len(styles.styleList))
101 | } else if styles.styleList[0].NumFmtID != 1 {
102 | t.Error("numFmtID should be 1 but ", styles.styleList[0].NumFmtID)
103 | } else if styles.styleList[0].FontID != 2 {
104 | t.Error("fontID should be 2 but ", styles.styleList[0].FontID)
105 | } else if styles.styleList[0].FillID != 3 {
106 | t.Error("fillID should be 3 but ", styles.styleList[0].FillID)
107 | } else if styles.styleList[0].BorderID != 4 {
108 | t.Error("borderID should be 4 but ", styles.styleList[0].BorderID)
109 | } else if styles.styleList[0].applyNumberFormat != 1 {
110 | t.Error("applyNumberFormat should be 1 but ", styles.styleList[0].applyNumberFormat)
111 | } else if styles.styleList[0].applyFont != 1 {
112 | t.Error("applyFont should be 1 but ", styles.styleList[0].applyFont)
113 | } else if styles.styleList[0].applyFill != 1 {
114 | t.Error("applyFill should be 1 but ", styles.styleList[0].applyFill)
115 | } else if styles.styleList[0].applyBorder != 1 {
116 | t.Error("applyBorder should be 1 but ", styles.styleList[0].applyBorder)
117 | } else if styles.styleList[0].applyAlignment != 1 {
118 | t.Error("applyAlignment should be 1 but ", styles.styleList[0].applyAlignment)
119 | } else if styles.styleList[0].applyProtection != 1 {
120 | t.Error("applyProtection should be 1 but ", styles.styleList[0].applyProtection)
121 | } else if styles.styleList[0].Horizontal != "left" {
122 | t.Error("Horizontal should be left but ", styles.styleList[0].Horizontal)
123 | } else if styles.styleList[0].Vertical != "top" {
124 | t.Error("Vertical should be top but ", styles.styleList[0].Vertical)
125 | } else if styles.styleList[0].Wrap != 1 {
126 | t.Error("Wrap should be 1 but ", styles.styleList[0].Wrap)
127 | }
128 | }
129 |
130 | func TestSetNumFmt(t *testing.T) {
131 | r := strings.NewReader(``)
132 | tag := &Tag{}
133 | xml.NewDecoder(r).Decode(tag)
134 | styles := &Styles{numFmts: tag}
135 | styles.setNumFmtNumber()
136 | index := styles.SetNumFmt("#,##0.0")
137 | if index != 200 {
138 | t.Error("index should be 200 but ", index)
139 | }
140 | b := new(bytes.Buffer)
141 | xml.NewEncoder(b).Encode(styles.numFmts)
142 | if b.String() != `` {
143 | t.Error("xml is corrupt [", b.String(), "]")
144 | }
145 | index = styles.SetNumFmt(`"`)
146 | if index != 201 {
147 | t.Error("index should be 201 but ", index)
148 | }
149 | b = new(bytes.Buffer)
150 | xml.NewEncoder(b).Encode(styles.numFmts.Children[1])
151 | if b.String() != `` {
152 | t.Error("xml is corrupt [", b.String(), "]")
153 | }
154 | }
155 |
156 | func TestSetFont(t *testing.T) {
157 | r := strings.NewReader(``)
158 | tag := &Tag{}
159 | xml.NewDecoder(r).Decode(tag)
160 | styles := &Styles{fonts: tag}
161 | font := Font{Size: 12, Color: "FFFF00FF"}
162 | index := styles.SetFont(font)
163 | if index != 0 {
164 | t.Error("index should be 0 but ", index)
165 | }
166 | b := new(bytes.Buffer)
167 | xml.NewEncoder(b).Encode(styles.fonts)
168 | if b.String() != `` {
169 | t.Error("xml is corrupt [", b.String(), "]")
170 | }
171 | font = Font{Size: 13}
172 | index = styles.SetFont(font)
173 | if index != 1 {
174 | t.Error("index should be 1 but ", index)
175 | }
176 | b = new(bytes.Buffer)
177 | xml.NewEncoder(b).Encode(styles.fonts.Children[index].(*Tag))
178 | if b.String() != `` {
179 | t.Error("xml is currupt [", b.String(), "]")
180 | }
181 | font = Font{Color: "FF00FFFF"}
182 | index = styles.SetFont(font)
183 | if index != 2 {
184 | t.Error("index should be 2 but ", index)
185 | }
186 | b = new(bytes.Buffer)
187 | xml.NewEncoder(b).Encode(styles.fonts.Children[index].(*Tag))
188 | if b.String() != `` {
189 | t.Error("xml is currupt [", b.String(), "]")
190 | }
191 | }
192 |
193 | func TestSetBackgroundColor(t *testing.T) {
194 | r := strings.NewReader(``)
195 | tag := &Tag{}
196 | xml.NewDecoder(r).Decode(tag)
197 | styles := &Styles{fills: tag}
198 | index := styles.SetBackgroundColor("FF00FF00")
199 | if index != 0 {
200 | t.Error("index should be 0 but", index)
201 | }
202 | b := new(bytes.Buffer)
203 | xml.NewEncoder(b).Encode(styles.fills)
204 | if b.String() != `` {
205 | t.Error("xml is corrupt [", b.String(), "]")
206 | }
207 | }
208 |
209 | func TestSetBorder(t *testing.T) {
210 | r := strings.NewReader(``)
211 | tag := &Tag{}
212 | xml.NewDecoder(r).Decode(tag)
213 | styles := &Styles{borders: tag}
214 | border := Border{}
215 | index := styles.SetBorder(border)
216 | if index != 0 {
217 | t.Error("index should be 0 but", index)
218 | }
219 | b := new(bytes.Buffer)
220 | xml.NewEncoder(b).Encode(styles.borders)
221 | if b.String() != `` {
222 | t.Error("xml is corrupt [", b.String(), "]")
223 | }
224 | border.Left = &BorderSetting{Style: "thin"}
225 | index = styles.SetBorder(border)
226 | if index != 1 {
227 | t.Error("index should be 1 but", index)
228 | }
229 | b = new(bytes.Buffer)
230 | xml.NewEncoder(b).Encode(styles.borders.Children[index])
231 | if b.String() != `` {
232 | t.Error("xml is corrupt [", b.String(), "]")
233 | }
234 | border.Left.Color = "FFFFFFFF"
235 | b = new(bytes.Buffer)
236 | index = styles.SetBorder(border)
237 | if index != 2 {
238 | t.Error("index should be 2 but", index)
239 | }
240 | xml.NewEncoder(b).Encode(styles.borders.Children[index])
241 | if b.String() != `` {
242 | t.Error("xml is corrupt [", b.String(), "]")
243 | }
244 | border.Right = &BorderSetting{Style: "hair", Color: "FF000000"}
245 | border.Top = &BorderSetting{Style: "dashDotDot", Color: "FF111111"}
246 | border.Bottom = &BorderSetting{Style: "dotted", Color: "FF222222"}
247 | index = styles.SetBorder(border)
248 | if index != 3 {
249 | t.Error("index should be 3 but", index)
250 | }
251 | b = new(bytes.Buffer)
252 | xml.NewEncoder(b).Encode(styles.borders.Children[index])
253 | if b.String() != `` {
254 | t.Error("xml is corrupt [", b.String(), "]")
255 | }
256 | }
257 |
258 | func TestSetStyle(t *testing.T) {
259 | r := strings.NewReader(``)
260 | tag := &Tag{}
261 | xml.NewDecoder(r).Decode(tag)
262 | styles := &Styles{cellXfs: tag}
263 | style := &Style{}
264 | index := styles.SetStyle(style)
265 | if index != 0 {
266 | t.Error("index should be 0 but", index)
267 | }
268 | b := new(bytes.Buffer)
269 | xml.NewEncoder(b).Encode(styles.cellXfs)
270 | if b.String() != `` {
271 | t.Error("xml is corrupt [", b.String(), "]")
272 | }
273 | style.NumFmtID = 1
274 | style.FontID = 2
275 | style.FillID = 3
276 | style.BorderID = 4
277 | style.XfID = 5
278 | index = styles.SetStyle(style)
279 | if index != 1 {
280 | t.Error("index should be 1 but", index)
281 | }
282 | b = new(bytes.Buffer)
283 | xml.NewEncoder(b).Encode(styles.cellXfs.Children[index].(*Tag))
284 | if b.String() != `` {
285 | t.Error("xml is corrupt [", b.String(), "]")
286 | }
287 | style.Horizontal = "left"
288 | style.Vertical = "top"
289 | index = styles.SetStyle(style)
290 | if index != 2 {
291 | t.Error("index should be 2 but", index)
292 | }
293 | b = new(bytes.Buffer)
294 | xml.NewEncoder(b).Encode(styles.cellXfs.Children[index].(*Tag))
295 | if b.String() != `` {
296 | t.Error("xml is corrupt [", b.String(), "]")
297 | }
298 | }
299 |
300 | func TestGetStyle(t *testing.T) {
301 | styles := &Styles{}
302 | if styles.GetStyle(0) != nil {
303 | t.Error("return value should be nil.")
304 | }
305 | style := &Style{}
306 | styles.styleList = append(styles.styleList, style)
307 | if styles.GetStyle(0) != style {
308 | t.Error("return value should be same as style.")
309 | }
310 | }
311 |
--------------------------------------------------------------------------------
/tag.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "encoding/xml"
5 | "errors"
6 | "io"
7 | )
8 |
9 | // Tag タグの情報をすべて保管する
10 | type Tag struct {
11 | Name xml.Name
12 | Attr []xml.Attr
13 | Children []interface{}
14 | XmlnsList []xml.Attr
15 | }
16 |
17 | // MarshalXML タグからXMLを作成しなおす
18 | func (t *Tag) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
19 | start.Name = t.Name
20 | start.Attr = t.Attr
21 | e.EncodeToken(start)
22 | for _, v := range t.Children {
23 | switch v.(type) {
24 | case *Tag:
25 | if err := e.Encode(v); err != nil {
26 | return err
27 | }
28 | case xml.CharData:
29 | e.EncodeToken(v.(xml.CharData))
30 | }
31 | }
32 | e.EncodeToken(start.End())
33 | return nil
34 | }
35 |
36 | // UnmarshalXML タグにXMLを読み込む
37 | func (t *Tag) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
38 | t.Name = start.Name
39 | t.Attr = start.Attr
40 | for _, at := range t.XmlnsList {
41 | if t.Name.Space != at.Value {
42 | continue
43 | }
44 | t.Name.Space = ""
45 | t.Name.Local = at.Name.Local + ":" + t.Name.Local
46 | break
47 | }
48 | if t.Name.Space != "" {
49 | t.Name.Space = ""
50 | }
51 | for index, attr := range start.Attr {
52 | if attr.Name.Space == "xmlns" {
53 | t.XmlnsList = append(t.XmlnsList, attr)
54 | start.Attr[index].Name.Local = start.Attr[index].Name.Space + ":" + start.Attr[index].Name.Local
55 | start.Attr[index].Name.Space = ""
56 | continue
57 | }
58 | for _, at := range t.XmlnsList {
59 | if attr.Name.Space != at.Value {
60 | continue
61 | }
62 | start.Attr[index].Name.Space = ""
63 | start.Attr[index].Name.Local = at.Name.Local + ":" + start.Attr[index].Name.Local
64 | break
65 | }
66 | }
67 | for {
68 | token, err := d.Token()
69 | if err != nil {
70 | if err == io.EOF {
71 | return nil
72 | }
73 | return err
74 | }
75 | switch token.(type) {
76 | case xml.StartElement:
77 | tok := token.(xml.StartElement)
78 | data := &Tag{XmlnsList: t.XmlnsList}
79 | if err := d.DecodeElement(&data, &tok); err != nil {
80 | return err
81 | }
82 | t.Children = append(t.Children, data)
83 | case xml.CharData:
84 | t.Children = append(t.Children, token.(xml.CharData).Copy())
85 | case xml.Comment:
86 | t.Children = append(t.Children, token.(xml.Comment).Copy())
87 | }
88 | }
89 | }
90 |
91 | func separateTag() *Tag {
92 | return &Tag{Name: xml.Name{Local: "separate_tag"}}
93 | }
94 |
95 | // setAttr 要素をセットする。要素がある場合は上書きする
96 | // ない場合は追加する
97 | func (t *Tag) setAttr(name string, val string) xml.Attr {
98 | for index, attr := range t.Attr {
99 | if attr.Name.Local == name {
100 | t.Attr[index].Value = val
101 | return t.Attr[index]
102 | }
103 | }
104 | attr := xml.Attr{
105 | Name: xml.Name{Local: name},
106 | Value: val,
107 | }
108 | t.Attr = append(t.Attr, attr)
109 | return attr
110 | }
111 |
112 | // deleteAttr 要素を削除する
113 | func (t *Tag) deleteAttr(name string) {
114 | for i := 0; i < len(t.Attr); i++ {
115 | attr := t.Attr[i]
116 | if attr.Name.Local == name {
117 | t.Attr = append(t.Attr[:i], t.Attr[i+1:]...)
118 | break
119 | }
120 | }
121 | }
122 |
123 | func (t *Tag) getAttr(name string) (string, error) {
124 | for _, attr := range t.Attr {
125 | if attr.Name.Local == name {
126 | return attr.Value, nil
127 | }
128 | }
129 | return "", errors.New("No attr found.")
130 | }
131 |
--------------------------------------------------------------------------------
/tag_test.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import "testing"
4 |
5 | func TestSetAttr(t *testing.T) {
6 | tag := &Tag{}
7 | attr := tag.setAttr("s", "1")
8 | if attr.Name.Local != "s" {
9 | t.Error("attr name should be s but", attr.Name.Local)
10 | } else if attr.Value != "1" {
11 | t.Error("attr value should be 1 but", attr.Value)
12 | }
13 |
14 | tag.setAttr("s", "2")
15 | if tag.Attr[0].Value != "2" {
16 | t.Error("attr value should be 2 but", attr.Value)
17 | }
18 | }
19 |
20 | func TestDeleteAttr(t *testing.T) {
21 | tag := &Tag{}
22 | tag.setAttr("a", "1")
23 | tag.setAttr("b", "2")
24 | tag.setAttr("c", "3")
25 | tag.deleteAttr("b")
26 | if tag.Attr[0].Name.Local != "a" {
27 | t.Error("attr name should be a but", tag.Attr[0].Name.Local)
28 | } else if tag.Attr[0].Value != "1" {
29 | t.Error("attr value should be 1 but", tag.Attr[0].Value)
30 | } else if tag.Attr[1].Name.Local != "c" {
31 | t.Error("attr name should be c but", tag.Attr[1].Name.Local)
32 | } else if tag.Attr[1].Value != "3" {
33 | t.Error("attr value should be 3 but", tag.Attr[1].Value)
34 | } else if len(tag.Attr) != 2 {
35 | t.Error("attr count should be 2 but", len(tag.Attr))
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/temp/test.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/loadoff/excl/c6a9e4c4b4c4bd36713bf11a8cd52c7205142f7e/temp/test.xlsx
--------------------------------------------------------------------------------
/temp/test2.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/loadoff/excl/c6a9e4c4b4c4bd36713bf11a8cd52c7205142f7e/temp/test2.xlsx
--------------------------------------------------------------------------------
/theme.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | )
7 |
8 | func createTheme1(dir string) error {
9 | os.MkdirAll(filepath.Join(dir, "xl", "theme"), 0755)
10 | f, err := os.Create(filepath.Join(dir, "xl", "theme", "theme1.xml"))
11 | if err != nil {
12 | return err
13 | }
14 | defer f.Close()
15 | f.WriteString(`
16 | `)
17 | f.Close()
18 | return nil
19 | }
20 |
--------------------------------------------------------------------------------
/workbook.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "archive/zip"
5 | "crypto/rand"
6 | "encoding/binary"
7 | "encoding/xml"
8 | "errors"
9 | "fmt"
10 | "io"
11 | "io/ioutil"
12 | "log"
13 | "os"
14 | "path"
15 | "path/filepath"
16 | "strconv"
17 | "strings"
18 | "time"
19 |
20 | "golang.org/x/text/unicode/norm"
21 | )
22 |
23 | // Workbook はワークブック内の情報を格納する
24 | type Workbook struct {
25 | TempPath string
26 | types *ContentTypes
27 | opened bool
28 | maxSheetID int
29 | sheets []*Sheet
30 | SharedStrings *SharedStrings
31 | workbookRels *WorkbookRels
32 | Styles *Styles
33 | workbookTag *Tag
34 | sheetsTag *Tag
35 | calcPr *Tag
36 | }
37 |
38 | // WorkbookXML workbook.xmlに記載されているタグの中身
39 | type WorkbookXML struct {
40 | XMLName xml.Name `xml:"workbook"`
41 | Sheets sheetsXML `xml:"sheets"`
42 | }
43 |
44 | // sheetsXML workbook.xmlに記載されているタグの中身
45 | type sheetsXML struct {
46 | XMLName xml.Name `xml:"sheets"`
47 | Sheetlist []SheetXML `xml:"sheet"`
48 | }
49 |
50 | // Create 新しくワークブックを作成する
51 | func Create() (*Workbook, error) {
52 | dir, err := ioutil.TempDir("", "excl_"+strings.Replace(time.Now().Format("20060102030405.000"), ".", "", 1))
53 | if err != nil {
54 | return nil, err
55 | }
56 | workbook := &Workbook{TempPath: dir}
57 | defer func() {
58 | if !workbook.opened {
59 | workbook.Close()
60 | }
61 | }()
62 | if err := createContentTypes(dir); err != nil {
63 | return nil, err
64 | }
65 | if err := createRels(dir); err != nil {
66 | return nil, err
67 | }
68 | if err := createWorkbook(dir); err != nil {
69 | return nil, err
70 | }
71 | if err := createWorkbookRels(dir); err != nil {
72 | return nil, err
73 | }
74 | if err := createStyles(dir); err != nil {
75 | return nil, err
76 | }
77 | if err := createTheme1(dir); err != nil {
78 | return nil, err
79 | }
80 | if err := os.Mkdir(filepath.Join(dir, "xl", "worksheets"), 0755); err != nil {
81 | return nil, err
82 | }
83 | if err := workbook.setInfo(); err != nil {
84 | return nil, err
85 | }
86 | workbook.opened = true
87 | return workbook, nil
88 | }
89 |
90 | // Open Excelファイルを開く
91 | func Open(path string) (*Workbook, error) {
92 | if !isFileExist(path) {
93 | return nil, errors.New("Excel file does not exist.")
94 | }
95 | dir, err := ioutil.TempDir("", "excl"+strings.Replace(time.Now().Format("20060102030405"), ".", "", 1))
96 | if err != nil {
97 | return nil, err
98 | }
99 | workbook := &Workbook{TempPath: dir}
100 | if err := unzip(path, dir); err != nil {
101 | return nil, err
102 | }
103 |
104 | defer func() {
105 | if !workbook.opened {
106 | workbook.Close()
107 | }
108 | }()
109 |
110 | if !isFileExist(filepath.Join(dir, "[Content_Types].xml")) ||
111 | !isFileExist(filepath.Join(dir, "xl", "workbook.xml")) ||
112 | !isFileExist(filepath.Join(dir, "xl", "_rels", "workbook.xml.rels")) ||
113 | !isFileExist(filepath.Join(dir, "xl", "styles.xml")) {
114 | return nil, fmt.Errorf("this excel file is corrupt")
115 | }
116 | err = workbook.setInfo()
117 | if err != nil {
118 | return nil, err
119 | }
120 | workbook.opened = true
121 | return workbook, nil
122 | }
123 |
124 | // Save save and close a workbook
125 | func (workbook *Workbook) Save(path string) error {
126 | if workbook == nil || !workbook.opened {
127 | return nil
128 | }
129 | var err, sheetErr, ssErr, relsErr, stylesErr, typesErr error
130 | var f *os.File
131 | defer os.RemoveAll(workbook.TempPath)
132 | for _, sheet := range workbook.sheets {
133 | tempErr := sheet.Close()
134 | if sheetErr == nil && tempErr != nil {
135 | sheetErr = tempErr
136 | }
137 | }
138 | ssErr = workbook.SharedStrings.Close()
139 | relsErr = workbook.workbookRels.Close()
140 | stylesErr = workbook.Styles.Close()
141 | typesErr = workbook.types.Close()
142 | workbook.opened = false
143 | if sheetErr != nil {
144 | return sheetErr
145 | } else if ssErr != nil {
146 | return ssErr
147 | } else if relsErr != nil {
148 | return relsErr
149 | } else if stylesErr != nil {
150 | return stylesErr
151 | } else if typesErr != nil {
152 | return typesErr
153 | }
154 | f, err = os.Create(filepath.Join(workbook.TempPath, "xl", "workbook.xml"))
155 | if err != nil {
156 | return err
157 | }
158 | defer f.Close()
159 | f.WriteString("\n")
160 | if err = xml.NewEncoder(f).Encode(workbook.workbookTag); err != nil {
161 | return err
162 | }
163 | f.Close()
164 | if path != "" {
165 | createZip(path, getFiles(workbook.TempPath), workbook.TempPath)
166 | }
167 | return nil
168 | }
169 |
170 | // Close 操作中のブックを閉じる(保存はしない)
171 | func (workbook *Workbook) Close() error {
172 | return workbook.Save("")
173 | }
174 |
175 | // OpenSheet Open specified sheet
176 | // if there is no specified sheet then create new sheet
177 | func (workbook *Workbook) OpenSheet(name string) (*Sheet, error) {
178 | compName := strings.ToLower(string(norm.NFKC.Bytes([]byte(name))))
179 | for _, sheet := range workbook.sheets {
180 | sheetName := strings.ToLower(string(norm.NFKC.Bytes([]byte(sheet.xml.Name))))
181 | if sheetName != compName {
182 | continue
183 | }
184 | err := sheet.Open(workbook.TempPath)
185 | if err != nil {
186 | return nil, err
187 | }
188 | return sheet, nil
189 | }
190 | index := workbook.workbookRels.getSheetMaxIndex()
191 | sheetName := workbook.types.addSheet(index)
192 | rid := workbook.workbookRels.addSheet(sheetName)
193 | target := workbook.workbookRels.getTarget(rid)
194 |
195 | workbook.sheetsTag.Children = append(workbook.sheetsTag.Children, createSheetTag(name, rid, workbook.maxSheetID+1))
196 | sheet := newSheet(name, workbook.maxSheetID, rid, target)
197 | sheet.sharedStrings = workbook.SharedStrings
198 | sheet.Styles = workbook.Styles
199 | if err := sheet.Create(workbook.TempPath); err != nil {
200 | return nil, err
201 | }
202 | workbook.sheets = append(workbook.sheets, sheet)
203 | workbook.maxSheetID++
204 | return sheet, nil
205 | }
206 |
207 | // SetForceFormulaRecalculation set fullCalcOnLoad attribute to calcPr tag.
208 | // When this excel file is opened, all calculation fomula will be recalculated.
209 | func (workbook *Workbook) SetForceFormulaRecalculation(flg bool) {
210 | if workbook.calcPr != nil {
211 | if flg {
212 | workbook.calcPr.setAttr("fullCalcOnLoad", "1")
213 | } else {
214 | workbook.calcPr.deleteAttr("fullCalcOnLoad")
215 | }
216 | }
217 | }
218 |
219 | // RenameSheet rename sheet name from old name to new name.
220 | func (workbook *Workbook) RenameSheet(old string, new string) {
221 | for i, sheet := range workbook.sheets {
222 | if sheet.xml.Name != old {
223 | continue
224 | }
225 | sheet.xml.Name = new
226 | switch t := workbook.sheetsTag.Children[i].(type) {
227 | case *Tag:
228 | t.setAttr("name", new)
229 | }
230 | }
231 | }
232 |
233 | // HideSheet hide sheet
234 | func (workbook *Workbook) HideSheet(name string) {
235 | for i, sheet := range workbook.sheets {
236 | if sheet.xml.Name != name {
237 | continue
238 | }
239 | switch t := workbook.sheetsTag.Children[i].(type) {
240 | case *Tag:
241 | t.setAttr("state", "hidden")
242 | }
243 | break
244 | }
245 | }
246 |
247 | // ShowSheet show sheet
248 | func (workbook *Workbook) ShowSheet(name string) {
249 | for i, sheet := range workbook.sheets {
250 | if sheet.xml.Name != name {
251 | continue
252 | }
253 | switch t := workbook.sheetsTag.Children[i].(type) {
254 | case *Tag:
255 | t.deleteAttr("state")
256 | }
257 | break
258 | }
259 | }
260 |
261 | func createSheetTag(name string, rid string, sheetID int) *Tag {
262 | tag := &Tag{Name: xml.Name{Local: "sheet"}}
263 | tag.setAttr("name", name)
264 | tag.setAttr("sheetId", strconv.Itoa(sheetID))
265 | tag.setAttr("r:id", rid)
266 | return tag
267 | }
268 |
269 | // setInfo xlsx情報を読み込みセットする
270 | func (workbook *Workbook) setInfo() error {
271 | var err error
272 | workbook.types, err = OpenContentTypes(workbook.TempPath)
273 | if err != nil {
274 | return err
275 | }
276 | workbook.Styles, err = OpenStyles(workbook.TempPath)
277 | if err != nil {
278 | return err
279 | }
280 | workbook.workbookRels, err = OpenWorkbookRels(workbook.TempPath)
281 | if err != nil {
282 | return err
283 | }
284 | workbook.SharedStrings, err = OpenSharedStrings(workbook.TempPath)
285 | if err != nil {
286 | return err
287 | }
288 | workbook.workbookRels.addSharedStrings()
289 | workbook.types.addSharedString()
290 | err = workbook.openWorkbook()
291 | if err != nil {
292 | return err
293 | }
294 |
295 | return nil
296 | }
297 |
298 | // createWorkbook workbook.xmlファイルを作成する
299 | func createWorkbook(dir string) error {
300 | os.Mkdir(filepath.Join(dir, "xl"), 0755)
301 | path := filepath.Join(dir, "xl", "workbook.xml")
302 | f, err := os.Create(path)
303 | if err != nil {
304 | return err
305 | }
306 | defer f.Close()
307 | f.WriteString("\n")
308 | f.WriteString(``)
309 | f.WriteString(``)
310 | f.WriteString(``)
311 | f.Close()
312 |
313 | return nil
314 | }
315 |
316 | // createRels .relsファイルを作成する
317 | func createRels(dir string) error {
318 | os.Mkdir(filepath.Join(dir, "_rels"), 0755)
319 | path := filepath.Join(dir, "_rels", ".rels")
320 | f, err := os.Create(path)
321 | if err != nil {
322 | return err
323 | }
324 | defer f.Close()
325 | f.WriteString("\n")
326 | f.WriteString(``)
327 | f.WriteString(``)
328 | f.WriteString(``)
329 | f.Close()
330 | return nil
331 | }
332 |
333 | // openWorkbook open workbook.xml and set workbook information
334 | func (workbook *Workbook) openWorkbook() error {
335 | workbookPath := filepath.Join(workbook.TempPath, "xl", "workbook.xml")
336 | f, err := os.Open(workbookPath)
337 | if err != nil {
338 | return err
339 | }
340 | defer f.Close()
341 | data, _ := ioutil.ReadAll(f)
342 | f.Close()
343 | val := WorkbookXML{}
344 | err = xml.Unmarshal(data, &val)
345 | if err != nil {
346 | return err
347 | }
348 | for i := range val.Sheets.Sheetlist {
349 | sheet := &val.Sheets.Sheetlist[i]
350 | target := workbook.workbookRels.getTarget(sheet.RID)
351 | workbook.sheets = append(workbook.sheets,
352 | &Sheet{
353 | xml: sheet,
354 | Styles: workbook.Styles,
355 | sharedStrings: workbook.SharedStrings,
356 | target: target,
357 | })
358 | sheetID, _ := strconv.Atoi(sheet.SheetID)
359 | if workbook.maxSheetID < sheetID {
360 | workbook.maxSheetID = sheetID
361 | }
362 | }
363 | tag := &Tag{}
364 | f, _ = os.Open(workbookPath)
365 | defer f.Close()
366 | if err = xml.NewDecoder(f).Decode(tag); err != nil {
367 | return err
368 | }
369 | f.Close()
370 | workbook.workbookTag = tag
371 | workbook.setWorkbookInfo()
372 | return nil
373 | }
374 |
375 | func (workbook *Workbook) setWorkbookInfo() {
376 | for _, child := range workbook.workbookTag.Children {
377 | switch t := child.(type) {
378 | case *Tag:
379 | if t.Name.Local == "sheets" {
380 | workbook.sheetsTag = t
381 | } else if t.Name.Local == "calcPr" {
382 | workbook.calcPr = t
383 | }
384 | }
385 | }
386 | }
387 |
388 | // getFiles dir以下に存在するファイルの一覧を取得する
389 | func getFiles(dir string) []string {
390 | fileList := []string{}
391 | files, _ := ioutil.ReadDir(dir)
392 | for _, f := range files {
393 | if f.IsDir() == true {
394 | list := getFiles(path.Join(dir, f.Name()))
395 | fileList = append(fileList, list...)
396 | continue
397 | }
398 | fileList = append(fileList, path.Join(dir, f.Name()))
399 | }
400 | return fileList
401 | }
402 |
403 | // unzip unzip excel file
404 | func unzip(src, dest string) error {
405 | r, err := zip.OpenReader(src)
406 | if err != nil {
407 | return err
408 | }
409 | defer r.Close()
410 | os.MkdirAll(dest, 0755)
411 |
412 | for _, f := range r.File {
413 | rc, err := f.Open()
414 | if err != nil {
415 | return err
416 | }
417 | defer rc.Close()
418 |
419 | path := filepath.Join(dest, f.Name)
420 | if f.FileInfo().IsDir() {
421 | os.MkdirAll(path, 0755)
422 | } else {
423 | os.MkdirAll(filepath.Dir(path), 0755)
424 | to, err := os.OpenFile(
425 | path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
426 | if err != nil {
427 | return err
428 | }
429 | defer to.Close()
430 | _, err = io.Copy(to, rc)
431 | if err != nil {
432 | return err
433 | }
434 | to.Close()
435 | }
436 | }
437 | return nil
438 | }
439 |
440 | func createZip(zipPath string, fileList []string, replace string) {
441 | var zipfile *os.File
442 | var err error
443 | if zipfile, err = os.Create(zipPath); err != nil {
444 | log.Fatalln(err)
445 | }
446 | defer zipfile.Close()
447 | w := zip.NewWriter(zipfile)
448 | for _, file := range fileList {
449 | read, err := os.Open(file)
450 | defer read.Close()
451 | if err != nil {
452 | fmt.Println(err)
453 | continue
454 | }
455 | f, err := w.Create(strings.Replace(file, replace+"/", "", 1))
456 | if err != nil {
457 | fmt.Println(err)
458 | continue
459 | }
460 | if _, err = io.Copy(f, read); err != nil {
461 | fmt.Println(err)
462 | continue
463 | }
464 | }
465 | w.Close()
466 | }
467 |
468 | // isFileExist ファイルの存在確認
469 | func isFileExist(filename string) bool {
470 | stat, err := os.Stat(filename)
471 | if err != nil {
472 | return false
473 | }
474 | return !stat.IsDir()
475 | }
476 |
477 | // isDirExist ディレクトリの存在確認
478 | func isDirExist(dirname string) bool {
479 | stat, err := os.Stat(dirname)
480 | if err != nil {
481 | return false
482 | }
483 | return stat.IsDir()
484 | }
485 |
486 | func random() string {
487 | var n uint64
488 | binary.Read(rand.Reader, binary.LittleEndian, &n)
489 | return strconv.FormatUint(n, 36)
490 | }
491 |
--------------------------------------------------------------------------------
/workbook_rel.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "encoding/xml"
5 | "errors"
6 | "io/ioutil"
7 | "os"
8 | "path/filepath"
9 | "regexp"
10 | "strconv"
11 | "strings"
12 | "time"
13 | )
14 |
15 | // WorkbookRels workbook.xml.relの情報をもつ構造体
16 | type WorkbookRels struct {
17 | rels *Relationships
18 | path string
19 | }
20 |
21 | // Relationships Relationshipsタグの情報
22 | type Relationships struct {
23 | XMLName xml.Name `xml:"Relationships"`
24 | Xmlns string `xml:"xmlns,attr"`
25 | Rels []relationship `xml:"Relationship"`
26 | }
27 |
28 | type relationship struct {
29 | XMLName xml.Name `xml:"Relationship"`
30 | ID string `xml:"Id,attr"`
31 | Type string `xml:"Type,attr"`
32 | Target string `xml:"Target,attr"`
33 | }
34 |
35 | // createWorkbookRels workbook.xml.relsファイルを作成する
36 | func createWorkbookRels(dir string) error {
37 | os.Mkdir(filepath.Join(dir, "xl", "_rels"), 0755)
38 | path := filepath.Join(dir, "xl", "_rels", "workbook.xml.rels")
39 | f, err := os.Create(path)
40 | if err != nil {
41 | return err
42 | }
43 | defer f.Close()
44 | f.WriteString("\n")
45 | f.WriteString(``)
46 | f.WriteString(``)
47 | f.WriteString(``)
48 | f.WriteString(``)
49 | f.Close()
50 | return nil
51 | }
52 |
53 | // OpenWorkbookRels open workbook.xml.rels
54 | func OpenWorkbookRels(dir string) (*WorkbookRels, error) {
55 | path := filepath.Join(dir, "xl", "_rels", "workbook.xml.rels")
56 | if !isFileExist(path) {
57 | return nil, errors.New("The workbook.xml.rels is not exists.")
58 | }
59 | f, err := os.Open(path)
60 | if err != nil {
61 | return nil, err
62 | }
63 | defer f.Close()
64 | data, err := ioutil.ReadAll(f)
65 | if err != nil {
66 | return nil, err
67 | }
68 | rels := &Relationships{}
69 | err = xml.Unmarshal(data, rels)
70 | if err != nil {
71 | return nil, err
72 | }
73 | return &WorkbookRels{rels: rels, path: path}, nil
74 | }
75 |
76 | // Close close workbook.xml.rels
77 | func (wbr *WorkbookRels) Close() error {
78 | if wbr == nil {
79 | return nil
80 | }
81 | f, err := os.Create(wbr.path)
82 | if err != nil {
83 | return err
84 | }
85 | defer f.Close()
86 | data, err := xml.Marshal(wbr.rels)
87 | if err != nil {
88 | return err
89 | }
90 | f.WriteString("\n")
91 | f.Write(data)
92 | return nil
93 | }
94 |
95 | // addSharedStrings add sharedStrings.xml information
96 | func (wbr *WorkbookRels) addSharedStrings() string {
97 | for _, rel := range wbr.rels.Rels {
98 | if rel.Target == "sharedStrings.xml" {
99 | return rel.ID
100 | }
101 | }
102 | rel := relationship{
103 | XMLName: xml.Name{Local: "Relationship"},
104 | Target: "sharedStrings.xml",
105 | Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
106 | ID: strings.Replace(time.Now().Format("rId060102030405.000"), ".", "", 1) + random(),
107 | }
108 | wbr.rels.Rels = append(wbr.rels.Rels, rel)
109 | return rel.ID
110 | }
111 |
112 | // addSheet add sheet information to workbook.xml.rels
113 | func (wbr *WorkbookRels) addSheet(name string) string {
114 | rel := relationship{
115 | XMLName: xml.Name{Local: "Relationship"},
116 | Target: "worksheets/" + name,
117 | Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
118 | ID: strings.Replace(time.Now().Format("rId060102030405.000"), ".", "", 1) + random(),
119 | }
120 | wbr.rels.Rels = append(wbr.rels.Rels, rel)
121 | return rel.ID
122 | }
123 |
124 | func (wbr *WorkbookRels) getTarget(rid string) string {
125 | if wbr == nil {
126 | return ""
127 | }
128 | for _, rel := range wbr.rels.Rels {
129 | if rel.ID == rid {
130 | return rel.Target
131 | }
132 | }
133 | return ""
134 | }
135 |
136 | func (wbr *WorkbookRels) getSheetIndex(rid string) int {
137 | if wbr == nil {
138 | return -1
139 | }
140 | re := regexp.MustCompile(`\Aworksheets\/sheet([0-9]+)\.xml\z`)
141 | for _, rel := range wbr.rels.Rels {
142 | if rel.ID == rid {
143 | vals := re.FindStringSubmatch(rel.Target)
144 | if len(vals) != 2 {
145 | return -1
146 | }
147 | index, _ := strconv.Atoi(vals[1])
148 | return index
149 | }
150 | }
151 | return -1
152 | }
153 |
154 | func (wbr *WorkbookRels) getSheetMaxIndex() int {
155 | maxIndex := 0
156 | re := regexp.MustCompile(`\Aworksheets\/sheet([0-9]+)\.xml\z`)
157 | for _, rel := range wbr.rels.Rels {
158 | val := re.FindStringSubmatch(rel.Target)
159 | if len(val) == 2 {
160 | index, _ := strconv.Atoi(val[1])
161 | if index > maxIndex {
162 | maxIndex = index
163 | }
164 | }
165 | }
166 | return maxIndex
167 | }
168 |
--------------------------------------------------------------------------------
/workbook_rel_test.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "testing"
7 | )
8 |
9 | func TestOpenWorkbookRels(t *testing.T) {
10 | os.MkdirAll("./temp/xl/_rels", 0755)
11 | defer os.RemoveAll("./temp/xl")
12 | path := filepath.Join("temp", "xl", "_rels", "workbook.xml.rels")
13 | _, err := OpenWorkbookRels("nopath")
14 | if err == nil {
15 | t.Error("workbook.xml.rels should not be opened.")
16 | }
17 | f, _ := os.Create(path)
18 | f.Close()
19 | _, err = OpenWorkbookRels("temp")
20 | if err == nil {
21 | t.Error("workbook.xml.rels should not be opened because syntax error.")
22 | }
23 | f, _ = os.Create(path)
24 | f.WriteString("")
25 | f.Close()
26 | wbr, err := OpenWorkbookRels("temp")
27 | if err != nil {
28 | t.Error("workbook.xml.rels should be opened.")
29 | }
30 | if wbr == nil {
31 | t.Error("WorkbookRels should be created.")
32 | }
33 | }
34 |
35 | func TestAddSheetWorkbookRels(t *testing.T) {
36 | wbr := &WorkbookRels{rels: &Relationships{}}
37 | rid1 := wbr.addSheet("sheet1")
38 | if len(wbr.rels.Rels) != 1 {
39 | t.Error("Rels count should be 1 but [", len(wbr.rels.Rels), "]")
40 | }
41 | rid2 := wbr.addSharedStrings()
42 | wbr.addSheet("sheet2")
43 | wbr.addSharedStrings()
44 | if wbr.rels.Rels[0].ID != rid1 {
45 | t.Error("id should be [", rid1, "] but [", wbr.rels.Rels[0].ID, "]", wbr.rels.Rels[0].Target)
46 | }
47 | if wbr.rels.Rels[1].ID != rid2 {
48 | t.Error("id should be [", rid2, "] but [", wbr.rels.Rels[1].ID, "]")
49 | }
50 | wbr.addSheet("sheet3")
51 | err := wbr.Close()
52 | if err == nil {
53 | t.Error("close error should be happen.")
54 | }
55 | wbr.path = filepath.Join("temp", "workbook.xml.rels")
56 | defer os.Remove(filepath.Join("temp", "workbook.xml.rels"))
57 | err = wbr.Close()
58 | if err != nil {
59 | t.Error("workbook.xml.rels should be closed.", err.Error())
60 | }
61 | }
62 |
63 | func TestCloseWorkbookRels(t *testing.T) {
64 | var wbr *WorkbookRels
65 | var err error
66 | if err = wbr.Close(); err != nil {
67 | t.Error("error should not be happen.", err.Error())
68 | }
69 |
70 | wbr = &WorkbookRels{}
71 | wbr.rels = &Relationships{}
72 | if err = wbr.Close(); err == nil {
73 | t.Error("error should be happen.")
74 | }
75 |
76 | wbr.path = "temp/rels.xml"
77 | if err = wbr.Close(); err != nil {
78 | t.Error("error should not be happen.", err.Error())
79 | }
80 | os.Remove("temp/rels.xml")
81 | }
82 |
--------------------------------------------------------------------------------
/workbook_test.go:
--------------------------------------------------------------------------------
1 | package excl
2 |
3 | import (
4 | "archive/zip"
5 | "os"
6 | "path/filepath"
7 | "testing"
8 | )
9 |
10 | func createCurruputXLSX(from string, to string, delfile string) {
11 | os.Mkdir("temp/output", 0755)
12 | defer os.RemoveAll("temp/output")
13 | unzip(from, "temp/output")
14 | if delfile != "" {
15 | os.Remove(filepath.Join("temp/output", delfile))
16 | }
17 | createZip(to, getFiles("temp/output"), "temp/output")
18 | }
19 |
20 | func TestCreateWorkbook(t *testing.T) {
21 | wb, err := Create()
22 | if err != nil {
23 | t.Error("error should not be happen but ", err.Error())
24 | }
25 | wb.OpenSheet("hello")
26 | wb.Save("temp/new.xlsx")
27 | if !isFileExist("temp/new.xlsx") {
28 | t.Error("new.xlsx should be created.")
29 | }
30 | os.Remove("temp/new.xlsx")
31 | }
32 |
33 | func TestFileDirExist(t *testing.T) {
34 | if ok := isDirExist("no/exist/dir"); ok {
35 | t.Error("directory should not be exist.")
36 | }
37 | if ok := isDirExist("temp"); !ok {
38 | t.Error("directory should be exist.")
39 | }
40 | if ok := isDirExist("temp/test.xlsx"); ok {
41 | t.Error("temp/test.xlsx is not directory.")
42 | }
43 | if ok := isFileExist("no/file/exist"); ok {
44 | t.Error("file should not be exist.")
45 | }
46 | if ok := isFileExist("temp"); ok {
47 | t.Error("temp/out should be directory.")
48 | }
49 | if ok := isFileExist("temp/test.xlsx"); !ok {
50 | t.Error("temp/test.xlsx should be file.")
51 | }
52 | }
53 |
54 | func TestOpenWorkbook(t *testing.T) {
55 | var err error
56 | var workbook *Workbook
57 |
58 | os.MkdirAll("temp/out", 0755)
59 | defer os.RemoveAll("temp/out")
60 | if workbook, err = Open("temp/test.xlsx"); err != nil {
61 | t.Error(err.Error())
62 | t.Error("workbook should be opened.")
63 | }
64 | if ok := isFileExist(filepath.Join(workbook.TempPath, "[Content_Types].xml")); !ok {
65 | t.Error("[Content_Types].xml should be exists.")
66 | }
67 |
68 | if err = workbook.Save("temp/out/test.xlsx"); err != nil {
69 | t.Error("Close should be succeed.", err.Error())
70 | }
71 | if ok := isFileExist(filepath.Join(workbook.TempPath, "[Content_Types].xml")); ok {
72 | t.Error("[Content_Types].xml should be deleted.")
73 | }
74 | if ok := isFileExist("temp/out/test.xlsx"); !ok {
75 | t.Error("test.xml should be created.")
76 | }
77 | // error patern
78 |
79 | if workbook, err = Open("no/path/excel.xlsx"); err == nil {
80 | t.Error("workbook should not be opened.")
81 | workbook.Close()
82 | }
83 |
84 | f, _ := os.Create("temp/currupt.xlsx")
85 | z := zip.NewWriter(f)
86 | z.Close()
87 | f.Close()
88 | defer os.Remove("temp/currupt.xlsx")
89 | if workbook, err = Open("temp/currupt.xlsx"); err == nil {
90 | t.Error("workbook should not be opened because excel file must be currupt.")
91 | }
92 | workbook.Close()
93 |
94 | createZip("temp/empty.xlsx", nil, "")
95 | defer os.Remove("temp/empty.xlsx")
96 | if workbook, err = Open("temp/empty.xlsx"); err == nil {
97 | t.Error("workbook should not be opened beacause excel file is not zip file.")
98 | workbook.Close()
99 | }
100 |
101 | createCurruputXLSX("temp/test.xlsx", "temp/no_content_types.xlsx", "[Content_Types].xml")
102 | defer os.Remove("temp/no_content_types.xlsx")
103 | if workbook, err = Open("temp/no_content_types.xlsx"); err == nil {
104 | t.Error("workbook should not be opened beacause excel file does not include [Content_Types].xml.")
105 | workbook.Close()
106 | }
107 |
108 | createCurruputXLSX("temp/test.xlsx", "temp/no_workbook_xml.xlsx", "xl/workbook.xml")
109 | defer os.Remove("temp/no_workbook_xml.xlsx")
110 | if workbook, err = Open("temp/no_workbook_xml.xlsx"); err == nil {
111 | t.Error("workbook should not be opened beacause excel file does not include workbook.xml.")
112 | }
113 | workbook.Close()
114 |
115 | }
116 |
117 | func TestOpenWorkbookXML(t *testing.T) {
118 | var err error
119 | workbook := &Workbook{TempPath: ""}
120 | workbook.TempPath = ""
121 |
122 | if err = workbook.openWorkbook(); err == nil {
123 | t.Error("workbook.xml should not be opened because workbook.xml does not exist.")
124 | }
125 | os.MkdirAll("temp/workbook/xl", 0755)
126 | defer os.RemoveAll("temp/workbook")
127 | f1, _ := os.Create("temp/workbook/xl/workbook.xml")
128 | f1.Close()
129 | err = workbook.openWorkbook()
130 | if err == nil {
131 | t.Error("workbook.xml should not be parsed.")
132 | }
133 | f1, _ = os.Create("temp/workbook/xl/workbook.xml")
134 | f1.WriteString("")
135 | f1.Close()
136 |
137 | workbook.TempPath = "temp/workbook"
138 | err = workbook.openWorkbook()
139 | if err != nil {
140 | t.Error("workbook.xml should be opened. error[", err.Error(), "]")
141 | } else if len(workbook.sheets) != 2 {
142 | t.Error("sheet count should be 2 but [", len(workbook.sheets), "]")
143 | }
144 | }
145 |
146 | func TestOpenSheet(t *testing.T) {
147 | os.Mkdir("temp/out", 0755)
148 | defer os.RemoveAll("temp/out")
149 | workbook, _ := Open("temp/test.xlsx")
150 | sheet, _ := workbook.OpenSheet("Sheet1")
151 | if sheet == nil {
152 | t.Error("Sheet1 should be exist.")
153 | }
154 | if _, err := workbook.OpenSheet("Sheet2"); err != nil {
155 | t.Error("Sheet2 should be created. [", err.Error(), "]")
156 | }
157 |
158 | sheet, _ = workbook.OpenSheet("ペンギンペンギンAaAa00")
159 | sheet.Close()
160 | tempSheet, _ := workbook.OpenSheet("ペンギンペンギンaaaa00")
161 | if sheet != tempSheet {
162 | t.Error("ペンギンペンギンAaAa00 sheet should be same as ペンギンペンギンaaaa00 sheet.")
163 | }
164 | workbook.Close()
165 | }
166 |
167 | func TestSetInfo(t *testing.T) {
168 | os.Mkdir("temp/out", 0755)
169 | defer os.RemoveAll("temp/out")
170 | workbook := &Workbook{TempPath: "temp/out"}
171 | err := workbook.setInfo()
172 | if err == nil {
173 | t.Error("[Content_Types].xml should not be opened.")
174 | }
175 | createContentTypes("temp/out")
176 | err = workbook.setInfo()
177 | if err == nil {
178 | t.Error("workbook.xml should not be opened.")
179 | }
180 | createWorkbook("temp/out")
181 | f, _ := os.Create(filepath.Join("temp/out/xl/sharedStrings.xml"))
182 | f.Close()
183 | err = workbook.setInfo()
184 | if err == nil {
185 | t.Error("sharedStrings.xml should not be opened.")
186 | }
187 | os.Remove(filepath.Join("temp/out/xl/sharedStrings.xml"))
188 |
189 | }
190 |
191 | func TestCalcPr(t *testing.T) {
192 | workbook := &Workbook{calcPr: &Tag{}}
193 | workbook.SetForceFormulaRecalculation(true)
194 | if v, _ := workbook.calcPr.getAttr("fullCalcOnLoad"); v != "1" {
195 | t.Error("fullCalcOnLoad attribute should be 1 but", v)
196 | }
197 | workbook.SetForceFormulaRecalculation(false)
198 | if _, err := workbook.calcPr.getAttr("fullCalcOnLoad"); err == nil {
199 | t.Error("fullCalcOnLoad attribute should not be found.")
200 | }
201 | }
202 |
203 | func TestSheetIndex(t *testing.T) {
204 | os.Mkdir("temp/out", 0755)
205 | defer os.RemoveAll("temp/out")
206 | workbook, _ := Open("temp/test2.xlsx")
207 | sheet, _ := workbook.OpenSheet("new sheet")
208 | if sheet.xml.SheetID != "3" {
209 | t.Error("SheetID should be 3 but", sheet.xml.SheetID)
210 | }
211 | sheet.Close()
212 | workbook.Close()
213 | }
214 |
215 | func TestRenameSheet(t *testing.T) {
216 | os.Mkdir("temp/out", 0755)
217 | defer os.RemoveAll("temp/out")
218 | workbook, _ := Open("temp/test.xlsx")
219 | workbook.RenameSheet("Sheet1", "rename sheet")
220 | sheet, _ := workbook.OpenSheet("rename sheet")
221 | if sheet.xml.SheetID != "1" {
222 | t.Error("sheetID should be 1 but", sheet.xml.SheetID)
223 | }
224 | sheet.Close()
225 | workbook.Close()
226 | }
227 |
228 | func TestHideSheet(t *testing.T) {
229 | os.Mkdir("temp/out", 0755)
230 | defer os.RemoveAll("temp/out")
231 | workbook, _ := Open("temp/test.xlsx")
232 | workbook.HideSheet("Sheet1")
233 | for i, sheet := range workbook.sheets {
234 | if sheet.xml.Name != "Sheet1" {
235 | continue
236 | }
237 | switch tag := workbook.sheetsTag.Children[i].(type) {
238 | case *Tag:
239 | if state, _ := tag.getAttr("state"); state != "hidden" {
240 | t.Error("state should be hidden.")
241 | }
242 | }
243 | break
244 | }
245 | workbook.ShowSheet("Sheet1")
246 | for i, sheet := range workbook.sheets {
247 | if sheet.xml.Name != "Sheet1" {
248 | continue
249 | }
250 | switch tag := workbook.sheetsTag.Children[i].(type) {
251 | case *Tag:
252 | if _, err := tag.getAttr("state"); err == nil {
253 | t.Error("state should not be found.")
254 | }
255 | }
256 | break
257 | }
258 | workbook.Close()
259 | }
260 |
--------------------------------------------------------------------------------