Go言語でExcelファイルを作るメモ

## はじめに

みんなが大好きなExcel、きっとGo言語でも生成したいという要望があるかと思う。
探してみると[xlsx](https://github.com/tealeg/xlsx)というライブラリがあったのでそれを使ってExcelファイル出力を試してみる。

なお、[goexcel](https://github.com/mikeshimura/goexcel)という上記ライブラリをラップして使いやすくしたライブラリもあるようだが、excelライブラリの変更に追随出来ていないようで、この記事執筆時点はうまく動かない。

## Excelファイルの新規作成

[xlsxライブラリのREADME](https://github.com/tealeg/xlsx#writing-xlsx-files)にサンプルコードがあるので引用する。

```go
package main

import (
        "fmt"
        "github.com/tealeg/xlsx"
)

func main() {
        var file *xlsx.File
        var sheet *xlsx.Sheet
        var row *xlsx.Row
        var cell *xlsx.Cell
        var err error

        file = xlsx.NewFile()
        sheet, err = file.AddSheet("Sheet1")
        if err != nil {
                fmt.Printf(err.Error())
        }
        row = sheet.AddRow()
        cell = row.AddCell()
        cell.Value = "I am a cell!"
        err = file.Save("MyXLSXFile.xlsx")
        if err != nil {
                fmt.Printf(err.Error())
        }
}
```

なお、下記のように`*xlsx.Sheet.AddRow()`や`*xlsx.Row.AddCell()`を使わなくても内部でやってくれるので問題ない。

```go
package main

import (
        "fmt"
        "github.com/tealeg/xlsx"
)

func main() {
        var file *xlsx.File
        var sheet *xlsx.Sheet
        var err error

        file = xlsx.NewFile()
        sheet, err = file.AddSheet("Sheet1")
        if err != nil {
                fmt.Printf(err.Error())
        }
        sheet.Cell(0, 0).Value = "I am a A1 cell!"
        sheet.Cell(0, 1).Value = "I am a A2 cell!"
        sheet.Cell(1, 1).Value = "I am a B2 cell!"
        err = file.Save("MyXLSXFile.xlsx")
        if err != nil {
                fmt.Printf(err.Error())
        }
}
```

## 既存Excelファイルの読み込み

こちらも[xlsxライブラリのREADME](https://github.com/tealeg/xlsx#reading-xlsx-files)にサンプルコードがあるので引用する。

```go
package main

import (
        "fmt"
        "github.com/tealeg/xlsx"
)

func main() {
        excelFileName := "/home/tealeg/foo.xlsx"
        xlFile, err := xlsx.OpenFile(excelFileName)
        if err != nil {
                ...
        }
        for _, sheet := range xlFile.Sheets {
                for _, row := range sheet.Rows {
                        for _, cell := range row.Cells {
                                text, _ := cell.String()
                                fmt.Printf("%s\n", text)
                        }
                }
        }
}
```

`xlsx.OpenFile()`で返ってくるのは`xlsx.NewFile()`と同じく`*xlsx.File`なので、あとは良しなにやることができる。
また、`[]byte`型として読み込み済みのファイルを読み込む`xlsx.OpenBinary()`、`io.ReaderAt`型を使って読み込む`xlsx.OpenReaderAt()`なども存在する。

### Excelファイルの内容をスライスとして読み込む

`xlsx.OpenFile()`等は`*xlsx.File`型を返すが、ファイルの中身を読み込んで`[][][]string`型のスライスとして返してくれる`xlsx.FileToSlice()`というメソッドがある。

```go
package main

import (
        "fmt"
        "github.com/tealeg/xlsx"
)

func main() {
        slice, err := xlsx.FileToSlice("test.xlsx")
        if err != nil {
                fmt.Printf(err.Error())
        }

        fmt.Println(slice[0][1][2]) // 1シート目のC2の内容を出力
}
```

単純に読み込むだけであればスライスとして読み込んだほうが後々楽なケースも多いだろう。
また、`*xlsx.File`型で読み込んだとしても`*xlsx.File.ToSlice()`メソッドを使えば同様に`[][][]string`型のスライスを返してくれる。

## フォーマット指定

ソースを読んだ限りでは後からフォーマット指定をすることが出来ず、値をセットするときにフォーマットも指定する方法しかないようだ。

| メソッド                                                         | 分類                        |
|------------------------------------------------------------------|-----------------------------|
| `func (c *Cell) SetBool(b bool)`                                 | 文字列(真 = "1" / 偽 = "0") |
| `func (c *Cell) SetDate(t time.Time)`                            | 日付("mm-dd-yy")            |
| `func (c *Cell) SetDateTime(t time.Time)`                        | 日付 + 時間("m/d/yy h:mm")  |
| `func (c *Cell) SetDateTimeWithFormat(n float64, format string)` | 日付のフォーマット指定      |
| `func (c *Cell) SetFloat(n float64)`                             | 数値                        |
| `func (c *Cell) SetFloatWithFormat(n float64, format string)`    | 数値のフォーマット指定      |
| `func (c *Cell) SetFormula(formula string)`                      | 数式                        |
| `func (c *Cell) SetInt(n int)`                                   | 数値                        |
| `func (c *Cell) SetInt64(n int64)`                               | 数値                        |
| `func (c *Cell) SetString(s string)`                             | 文字列                      |
| `func (c *Cell) SetValue(n interface{})`                         | 標準                        |

`*xlsx.Cell.SetDateTimeWithFormat()`は`time.Time`型を渡すことができず、自前でExcelの日付シリアル値を求めて`float64`型で渡す必要がある。
`*xlsx.Cell.SetDate()`及び`*xlsx.Cell.SetDateTime()`についてはどのタイムゾーンを設定していたとしても内部で良しなにやって出力してくれるようだ。
(内部処理については[この辺り](https://github.com/tealeg/xlsx/blob/master/cell.go#L121)を参照)
`*xlsx.Cell.SetFormula()`はExcel VBAの`Formula`プロパティ相当(A1形式で指定するもの)で、R1C1形式を指定しても実行時にはエラーとならないが、出力されたExcelファイルを開くと「ファイルが壊れている」と表示される。

## セル値の読み込み

| メソッド                                                         | 備考                               |
|------------------------------------------------------------------|------------------------------------|
| `func (c *Cell) Bool() bool`                                     | 下記「Boolの判定」参照             |
| `func (c *Cell) Float() (float64, error)`                        |                                    |
| `func (c *Cell) FormattedValue() (string, error)`                | 期待通りにならないケースあり       |
| `func (c *Cell) Formula() string`                                | 設定された式を返す                 |
| `func (c *Cell) GetNumberFormat() string`                        |                                    |
| `func (c *Cell) GetTime(date1904 bool) (t time.Time, err error)` | 下記「日付シリアル値の基準日」参照 |
| `func (c *Cell) Int() (int, error)`                              |                                    |
| `func (c *Cell) Int64() (int64, error)`                          |                                    |
| `func (c *Cell) String() (string, error)`                        |                                    |

### Boolの判定

Office Open XMLの仕様を軽く流し読みしても真偽値については出てこず、Excelの書式設定にも真偽値はないため、恐らくこのライブラリ固有のものだと思われる。
判定は以下の流れで行われる。

1. セルの値が`*xlsx.Cell.SetBool()`でセットされたものの場合、"1"であれば`true`、それ以外は`false`
2. セルの表示形式が「数値」か「標準」の場合、"0"であれば`false`、それ以外は`true`
3. 上記以外の場合、空欄であれば`false`、それ以外は`true`

### 日付シリアル値の基準日

日付シリアル値とは、基準日からの経過日数のことである。
[UNIX時間](https://ja.wikipedia.org/wiki/UNIX%E6%99%82%E9%96%93)がUTC 1970年1月1日 0時0分0秒を基準とした経過秒数だが、それを経過日数で表し、コンマ以下を時刻に割り当てたものと理解すれば分かりやすいのではないだろうか。
この基準日だが、Excelでは2種類の基準日が存在する。

- 1900年1月1日 0時0分0秒
- 1904年1月1日 0時0分0秒

実を言うとこのエントリを書くまで基準日に1904年のものがあるということを知らなかったのだが、調べてみると随分とややこしいことになっているようだ。
標準では1900年のほうが基準日となっているのだが、こちらを基準とした場合、本来存在しない「1900年2月29日」なる日付が出てしまう（シリアル値=60）。
これはどうやらLotus 1-2-3との互換性を維持するためのもののようで、Lotus 1-2-3が実際にあったかどうかはさておき、4年に1度うるう年が来るという大雑把な設計をしていたためにこういう状況になっているようだ。
なお、このようなことが起きるのは「1900年2月29日」のみであり、以降はちゃんとうるう年を判定してくれる。
このように本来存在しない日付がある場合、シリアル値を計算してセルに値を入れ込む場合に誤差が出てしまう。
そういったことに対応するためにもうひとつの基準日である「1904年」というものが存在するようだ。
`*xlsx.Cell.GetTime()`の引数`date1904`は`false`を渡すと「1900年」を基準日とし、`true`を渡すと「1904年」を基準日として`time.Time`型に直してくれる。

また、上記の基準日をシリアル値に直すと「1」になることも注意が必要である。
`*xlsx.Cell.SetDateTimeWithFormat()`は`time.Time`型を受け付けず`float64`で渡す必要があるため自分で計算する必要があるが、私のようにまぬけにも基準日を「0」と勘違いして計算するとずれが出てしまうことになる。

## スタイル設定

`*xlsx.Cell.GetStyle()`で既存スタイル定義を取得するか、`xlsx.NewStyle()`で新規スタイル定義を作成する。

```go
// スタイルの適用方法
style := xlsx.NewStyle()
sheet.Cell(0, 0).SetStyle(style)
```

```go
// xlsx.Style型の定義
type Style struct {
        Border          Border
        Fill            Fill
        Font            Font
        ApplyBorder     bool
        ApplyFill       bool
        ApplyFont       bool
        ApplyAlignment  bool
        Alignment       Alignment
        NamedStyleIndex *int
}
```

### 罫線

```go
type Border struct {
        Left        string
        LeftColor   string
        Right       string
        RightColor  string
        Top         string
        TopColor    string
        Bottom      string
        BottomColor string
}
```

単純に細い黒線を引きたいだけであれば`Left`、`Right`、`Top`、`Bottom`に`"thin"`、`*Color`に`"FF000000"`を入れて`*xlsx.Style.ApplyBorder`を`true`にすれば適用される。

```go
style := xlsx.NewStyle()
borderStyle = "thin"
borderColor = "FF000000"
style.ApplyBorder = true
style.Border.Left = borderStyle
style.Border.LeftColor = borderColor
style.Border.Right = borderStyle
style.Border.RightColor = borderColor
style.Border.Top = borderStyle
style.Border.TopColor = borderColor
style.Border.Bottom = borderStyle
style.Border.BottomColor = borderColor
```

また`xlsx.NewBorder()`で一気に指定することもできるが、こちらでやると色指定ができない。

```go
style := xlsx.NewStyle()
style.ApplyBorder = true
style.Border = xlsx.newBorder("thin", "thin", "thin", "thin")
```

### 背景色

```go
type Fill struct {
        PatternType string
        BgColor     string
        FgColor     string
}
```

恐らくRGB指定しか対応していないと思われる。
`PatternType`は以下のものが設定できる。
**2017/01/24 綴りが間違っていたり一部大文字に直さなければ動かない部分があったため修正**

- darkDown
- darkGray
- darkGrid
- darkHorizontal
- darkTrellis
- darkUp
- darkVertical
- lightDown
- lightGray
- lightGrid
- lightHorizontal
- lightTrellis
- lightUp
- lightVertical
- gray0625
- gray125
- mediumGray
- solid
- none

デフォルトでは`"none"`が設定されているため、単純に塗りつぶしたければ`"solid"`を指定すること。
`*xlsx.Style.ApplyFill`はデフォルトでは`false`だが、そのままでも適用されるようだ。

### フォント指定

```go
type Font struct {
        Size      int
        Name      string
        Family    int
        Charset   int
        Color     string
        Bold      bool
        Italic    bool
        Underline bool
}
```

デフォルトでは12ptのVerdanaが設定されている。
フォント名は`"ＭＳ Ｐゴシック"`のように日本語指定でも通るようだ(もちろん`"MS PGothic"`でも通る)。
`Charset`は恐らく[この辺り](https://msdn.microsoft.com/ja-jp/library/documentformat.openxml.drawing.symbolfont%28v=office.14%29.aspx)の16進数を10進数に直してセットすればよいのだろうが、私がすぐ確認できるフォントでは特にセットしなくても問題なかった。

また、デフォルトのフォントを指定する場合は`xlsx.SetDefaultFont()`を利用する。

```go
xlsx.SetDefaultFont(11, "ＭＳ Ｐゴシック") // 11ptのＭＳ Ｐゴシックをデフォルトフォントとして指定
```

`xlsx.SetDefaultFont()`は`xlsx.NewStyle()`や`*xlsx.Cell.GetStyle()`を呼ぶ前に設定しなければならないので注意。

### セル内の文字位置関係

```go
type Alignment struct {
        Horizontal   string
        Indent       int
        ShrinkToFit  bool
        TextRotation int
        Vertical     string
        WrapText     bool
}
```

それぞれ型に合わせて値を入れていけば設定される。
こちらで試した限り、`*xlsx.Style.ApplyAlignment`はデフォルトで`false`となっているが、`false`のままでも適用されるようだ。

`Vertical`は`"top"`、`"center"`、`"bottom"`を、`Horizontal`は`"left"`、`"center"`、`"right"`、または`"general"`を設定する。
デフォルトでは`Vertical`が`"bottom"`、`Horizontal`が`"general"`となる。
`ShrinkToFit`と`WrapText`は順序がどうであれ`WrapText`のほうが優先されるようだ（これはExcelの仕様？）。
`TextRotation`はExcel上の表現は-90～90となっているが、golangから設定する場合マイナス指定は無視されるため、91～180を指定する必要がある。
なお180を超えた場合、181～254はExcelの書式設定を開こうとすると「-90 から 90 の範囲でなければなりません」と怒られてしまう。
255は書式設定上の数値は0となるが、表示上は中央寄せして縦書きにしたような状態となる。
256からは再び0を指定した状態に戻るようだ。

### セルのスタイル

`*xlsx.Style.NamedStyleIndex`に`int`型のポインタを入れることで利用できるように見せかけている。
Office Open XMLでいうcellStyle要素のxfId属性を指すものなのだが、現状使ってはいけない項目のようだ。
新規作成時はセルのスタイルを作ることが出来ないため特に気にしなくてもよいが、セルのスタイルが定義されている既存ファイルを読み込む際にExcelが設定したものを壊してしまい、出力したファイルをExcelで開く際に「ファイルが壊れている」と怒られる羽目になってしまった。
既存ファイルを読み込む際にはセルのスタイルを定義しないよう気をつける必要がある。

### 範囲指定してスタイル適用

そんなものはない。
背景色や罫線を範囲指定で一気に適用したいケースがあるが、私は愚直にforループで適用させることにした。

```go
// A1 ～ D5までに同一スタイルを適用させる場合
startRow := 0
startCol := 0
endRow := 3
endCol := 4
for r := startRow; r <= endRow; r++ {
  for c := startCol; c <= endCol; c++ {
    sheet.Cell(r, c).SetStyle(style)
  }
}
```

Excel VBAでいう`Range`オブジェクトのようなものが欲しいと思うが、この辺りは要提案だろうか。

## セルのマージ

`*xlsx.Cell.Merge()`で行う。

```go
sheet.Cell(0, 0).Merge(1, 1) // A1～B2をマージ
sheet.Cell(3, 4).Merge(1, 1) // D5～E6をマージ
```

上記のように指定セルから見て右下セルの相対位置を指定してマージする。

## 列幅の指定

`*xlsx.Sheet.SetColWidth()`で行う。

```go
sheet.SetColWidth(0, 0, 10.0) // A列
sheet.SetColWidth(1, 3, 15.0) // B～D列
```

上記のように指定する範囲の列幅を指定できる。
なお、上記のように`10.0`を指定してExcelを生成してみたが、開いてみると`9.38pt(80px)`で表示されている。
`9.0`で`8.38(72px)`、`11.0`で`10.38(88px)`、`12.0`で`11.38(96px)`となるので`1.0` = `8px`として数値を決めると良さそうだ。

## ファイルの保存

`*xlsx.File.Save()`または`*xlsx.File.Write()`行う。

```go
var err error
var writer io.Writer

// パスを指定して保存
err = file.Save('/path/to/file.xlsx')

// io.Writer型に保存
writer = &bytes.Buffer{}
err = file.Write(writer)
```

`*xlsx.File`は`xlsx.OpenFile()`で開いた場合であってもファイルパスを保持しないため、上書き保存する場合でもファイルパスを指定する必要がある。
`*xlsx.File.Write()`については`io.Writer`を実装したものであれば何でも渡せるので、恐らくこちらを使ったほうが柔軟に利用できるだろう。

## おわりに

`xlsx`ライブラリの機能を一通り書き出してみたが、セルのスタイルを破壊してしまうのには注意が必要だが、新規作成からテンプレートを用いた出力まで一通り対応できるようだ。
途中にも書いたが範囲指定が出来るものが欲しいと思うが、今のところ独自に型を定義してそれっぽく動くようにして凌いでいる。