├── 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 | [![godoc](https://godoc.org/github.com/loadoff/excl?status.svg)](https://godoc.org/github.com/loadoff/excl) 7 | [![CircleCI](https://circleci.com/gh/loadoff/excl.svg?style=svg)](https://circleci.com/gh/loadoff/excl) 8 | [![go report](https://goreportcard.com/badge/github.com/loadoff/excl)](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 | --------------------------------------------------------------------------------