Go言語でJavaScriptを使ってオリジナルなコマンドシェルを作る（EXCELの読み込みオブジェクトの追加)

# 概要

前回投稿した「[Go言語でJavaScriptを使ってオリジナルなコマンドシェルを作る](https://qiita.com/h6591/items/6dec513868360becd02e)」にEXCELを読み込む関数を追加する。これを実装することでGo側でJavaScriptのJSON, Arrayの作り方、そしてJavaScript側のCallback関数の呼び出しを実装する

# 環境

- 前回投稿のソースに追加するだけなので、全ソースコードは掲載しません
- EXCELのパッケージ
    - github.com/tealeg/xlsx
- テストEXCELデータ
    - ブック名 Book1.xlsx   
    - シート名 Sheet1
    
| A  |  B |  C  |
|----|-----|----|
|1| 111| aaa|
|2| 112| bbb|
|3| 113| ccc|
|4 | 114| ddd|

# 実装

`excel(ブック名, シート名)`の関数を定義する

```go:initialSetting()
	rt.Set("excel", js.excel)
```


```go

import "github.com/tealeg/xlsx"

type excelSheet struct {
	js    *jsRuntime
	sheet *xlsx.Sheet
}

func (js *jsRuntime) excel(file, sheet string) *excelSheet {
	rt := js.runtime
	xfile, err := xlsx.OpenFile(file)
	if err != nil {
		rt.Interrupt(rt.NewGoError(err))
		return nil
	}
	xsheet := xfile.Sheet[sheet]
	if xsheet == nil {
		rt.Interrupt("Error: not found excel sheet: " + sheet)
		return nil
	}
	return &excelSheet{js: js, sheet: xsheet}
}
```

オブジェクトは出来ましたがメソッドがないので、これから次の3メソッドを作っていきます。すべてJavaScriptのCallBack関数を呼び出す仕様です。

- ForEachSlice
    - Go言語の`Slice`を返すがJavaScriptの`Array`ではないので`Array`のメソッドで使えないものがあります。
- ForEachArray
    - JavaScriptの`Array`オブジェクトを返す
- ForEachJSON
    - JavaScriptの`JSON`オブジェクトを返す

## ForEachSliceの実装

`convData`関数はEXCELのデータタイプを判断してタイプに合った型に変換します。ただし、今回は`xlsx.CellTypeNumeric`とそれ以外だけにしました。

```go
func (s *excelSheet) convData(cell *xlsx.Cell) goja.Value {
	rt := s.js.runtime
	typ := cell.Type()
	if typ == xlsx.CellTypeNumeric {
		val, err := cell.Int()
		if err == nil {
			return rt.ToValue(val)
		}
		return rt.ToValue(cell.String())
	}
	return rt.ToValue(cell.String())
}
```

`ForEachSlice`のソース。引数はJavaScriptのCallBack関数です。CallBack関数は`function(行番号,行のSlice)`です。行番号は1から始めます。


```go
func (s *excelSheet) ForEachSlice(callBack goja.Callable) {
	rt := s.js.runtime
	if s.sheet == nil {
		rt.Interrupt("sheet is closed")
		return
	}
	for rix, row := range s.sheet.Rows {
		cells := make([]interface{}, len(row.Cells))
		for cix, cell := range row.Cells {
			cells[cix] = s.convData(cell)
		}
		_, err := callBack(goja.Undefined(), rt.ToValue(rix+1), rt.ToValue(cells))
		if err != nil {
			rt.Interrupt(err)
			return
		}
	}
	s.sheet = nil
}
```

`callBack`の第一引数を`undefined`としていますが、第一引数はJavaScript側で`this`として使われますので、JavaScript側で`this`が必要なら`ForEachSlice`の引数に`this`が受け取れるように追加してください。当然呼び出し側も`this`にしたいオブジェクトをセットします。

```go
func (s *excelSheet) ForEachSlice(this goja.Value, callBack goja.Callable) {
       callBack(this, rt.ToValue(rix+1), rt.ToValue(cells))
}
```


```javascript
go run main.go
> excel("Book1.xlsx", "Sheet1").ForEachSlice(function(ix, row) {
...>    print(ix, row)
...> })
1 [1,111,"aaa"]
2 [2,112,"bbb"]
3 [3,113,"ccc"]
4 [4,114,"ddd"]
undefined
>
```

JavaScriptのArrayではないので`push`などは使えません。`forEach`は使えます。

```javascript
go run main.go
> excel("Book1.xlsx", "Sheet1").ForEachSlice(function(ix, row) {
...>   row.push(ix)
...>   print(ix,row)
...> })
TypeError: Cannot extend Go slice at push (native) at console:1:43(9)
>
```
## ForEachArrayの実装

`newArray`関数は`Array`オブジェクトの生成と要素を追加する`push`メソッドを取得します

```go
func (s *excelSheet) newArray() (goja.Value, goja.Callable) {
	rt := s.js.runtime
	arr, err := rt.RunString("new Array()")
	if err != nil {
		rt.Interrupt(err)
		return nil, nil
	}
	arrObj, ok := arr.(*goja.Object)
	if !ok {
		rt.Interrupt("Array not defined")
		return nil, nil
	}

	push := arrObj.Get("push")
	pushFunc, ok := goja.AssertFunction(push)
	if !ok {
		rt.Interrupt("Array.push not defined")
		return nil, nil
	}
	return arr, pushFunc
}
```

`ForEachArray`のソース。引数はJavaScriptのCallBack関数です。CallBack関数は`function(行番号,行のArray)`です。行番号は1から始めます。

```go
func (s *excelSheet) ForEachArray(callBack goja.Callable) {
	rt := s.js.runtime
	if s.sheet == nil {
		rt.Interrupt("sheet is closed")
		return
	}
	for rix, row := range s.sheet.Rows {
		arr, pushFunc := s.newArray()
		if arr == nil {
			return
		}
		for _, cell := range row.Cells {
			pushFunc(arr, s.convData(cell))
		}
		_, err := callBack(goja.Undefined(), rt.ToValue(rix+1), arr)
		if err != nil {
			rt.Interrupt(err)
			return
		}
	}
	s.sheet = nil
}
```

```javascript
go run main.go
> excel("Book1.xlsx", "Sheet1").ForEachArray(function(ix, row) {
...>   print(ix,row)
...> })
1 [1,111,"aaa"]
2 [2,112,"bbb"]
3 [3,113,"ccc"]
4 [4,114,"ddd"]
undefined
>
```

上記の結果は`ForEachSlice`と変わらなく見えます。今度は`push`を使ってみます。

```javascript
go run main.go
> excel("Book1.xlsx", "Sheet1").ForEachArray(function(ix, row) {
...>   row.push(ix)
...>   print(ix,row)
...> })
1 [1,111,"aaa",1]
2 [2,112,"bbb",2]
3 [3,113,"ccc",3]
4 [4,114,"ddd",4]
undefined
>
```

ちゃんと`push`できています。

## ForEachJSONの実装

`ForEachJSON`のソース。引数はJavaScriptのCallBack関数です。CallBack関数は`function(行番号,行のJSON)`です。行番号は1から始めます。下記のJSONのフィールド名は`A1,B1,C1`のようにEXCELのセルの形式にしました。ただし`A~Z`までです。

```go
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

func (s *excelSheet) ForEachJSON(callBack goja.Callable) {
	rt := s.js.runtime
	if s.sheet == nil {
		rt.Interrupt("sheet is closed")
		return
	}
	for rix, row := range s.sheet.Rows {
		json := rt.NewObject()
		for cix, cell := range row.Cells {
			name := fmt.Sprintf("%s%d", alphabet[cix:cix+1], rix+1)
			json.Set(name, s.convData(cell))
		}
		_, err := callBack(goja.Undefined(), rt.ToValue(rix+1), json)
		if err != nil {
			rt.Interrupt(err)
			return
		}
	}
	s.sheet = nil
}
```

```javascript
go run main.go
> excel("Book1.xlsx", "Sheet1").ForEachJSON(function(ix, row) {
...>   print(ix,JSON.stringify(row))
...> })
1 {"A1":1,"B1":111,"C1":"aaa"}
2 {"A2":2,"B2":112,"C2":"bbb"}
3 {"A3":3,"B3":113,"C3":"ccc"}
4 {"A4":4,"B4":114,"C4":"ddd"}
undefined
>
```

サブドキュメントを作るには

```go
   json := rt.NewObject()
   json.Set("a", rt.ToValue(1))
   subdoc := rt.NewObject()
   subdoc.Set("field1", rt.ToValue("abcde"))
   json.Set("subdoc", subdoc)
```

とすると下記のようなJSONができます。

```json
{"a":1,"subdoc":{"field1":"abcde"}}
```


# 参照
- EXCELパッケージ　https://github.com/tealeg/xlsx

# まとめ

３つのメソッドを用意しましたが、一つで良ければ`excel(ブック名,シート名,callback関数)`だけで良いかもしれません。

これでJSONとcallbackができたのでほとんどの事が出来ます。
データ量が少なければ２次元配列またはJSONの配列で全データを返すもありだと思います。この時はcallback関数は不要で`excel()`の戻り値として処理します。

いままでエラーは`Interrupt`を使っていますが、JavaScript実行中に呼び出されたGo言語で`panic`を使っても異常終了せずに、JavaScriptで`throw`と同様のふるまいをします。