UTF-8 全角カタカナを Shift_JIS 半角カタカナに変換した上で CSV を出力する

# お題

- アプリケーションは文字列を UTF-8 の全角カタカナなどで管理している
- CSV は Shift_JIS の半角カタカナなどで構成したい
  - 具体的には `ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 ｱｲｳｴｵｶｷｸｹｺｻｼｽｾｿﾀﾁﾂﾃﾄﾅﾆﾇﾈﾉﾊﾋﾌﾍﾎﾏﾐﾑﾒﾓ ﾔﾕﾖﾗﾘﾙﾚﾛﾜｦﾝ ｧｨｩｪｫｬｭｮｯ ﾞ（濁点） ﾟ（半濁点） \ ( ) -（ハイフン） ｰ（長音） / . , ･ _ ｢ ｣ 半角スペース` だけを使わないといけない
  - ref. https://www.tanshin.co.jp/business/netbk/pdf/zengin_moji.pdf

# やること

- [csv.NewWriter](https://golang.org/pkg/encoding/csv/#NewWriter) を使って UTF-8 文字列を Shift_JIS に変換する
  - ex. efbe80 を c0 にする
- ただこれだけだと、半角にならないので [width.Narrow](https://godoc.org/golang.org/x/text/width#example-Transformer--Narrow) transformer を使って全角を半角にする
  - ex. タ を ﾀ にする
- まだこれだけだと、濁点を含むカタカナを Shift_JIS に変換できないので [norm.NFD](https://golang.org/pkg/vendor/golang_org/x/text/unicode/norm/#Form) transformer を使って Unicode 正規化（分解）を適用する
  - ex. ダ を タ と 濁点にする
  - 補足: [Shift_JIS の 1 バイトコード内に ﾀﾞはない](http://charset.7jp.net/sjis.html)
- transformer は transform.Chain でまとめられるので順番に並べる

# コード

全銀仕様をベースにした振込申請を CSV で出力する。

```go
package main_test

import (
	"encoding/csv"
	"io"
	"testing"

	"bytes"
	"strconv"

	"log"

	"fmt"
	"io/ioutil"
	"strings"

	"golang.org/x/text/encoding/japanese"
	"golang.org/x/text/transform"
	"golang.org/x/text/unicode/norm"
	"golang.org/x/text/width"
)

// NewCSVWriter は w に CSV を書き出す Writer を生成する。
func NewCSVWriter(w io.Writer) *csv.Writer {
	return csv.NewWriter(transform.NewWriter(
		w,
		transform.Chain(
			// ex. ダをタと濁点に分解する
			norm.NFD,
			// ex. タをﾀに、濁点を ﾞ にそれぞれ変換する
			width.Narrow,
			// ex. efbe80 を c0 に efbe9e を de にそれぞれ変換する
			japanese.ShiftJIS.NewEncoder(),
		),
	))
}

// BankTransferRequest は振込申請を表す。
type BankTransferRequest struct {
	// BankCode は銀行コードを表す。
	BankCode string
	// BranchCode は支店コードを表す。
	BranchCode string
	// AccountTypeCode は口座種別コードを表す。
	AccountTypeCode string
	// AccountNumber は口座番号を表す。
	AccountNumber string
	// RecipientName は受取人名を表す。
	RecipientName string
	// TransferAmount は振込金額を表す。
	TransferAmount int64
}

// CSVHeaderKey はヘッダーの位置を表す。
type CSVHeaderKey int

const (
	// BankCode は銀行コードを表す。
	BankCode CSVHeaderKey = iota
	// BranchCode は支店コードを表す。
	BranchCode
	// AccountTypeCode は口座種別コードを表す。
	AccountTypeCode
	// AccountNumber は口座番号を表す。
	AccountNumber
	// RecipientName は受取人名を表す。
	RecipientName
	// TransferAmount は振込金額を表す。
	TransferAmount
	// EndHeaderKey はヘッダー定義の番兵を表す。それ以外の意味はない。
	EndHeaderKey
)

// CSVHeader は振込の申請に使う CSV のヘッダーレコードを返す。
func CSVHeader() []string {
	var header []string
	for i := BankCode; i < EndHeaderKey; i++ {
		header = append(header, i.String())
	}
	return header
}

func (headerKey CSVHeaderKey) String() string {
	switch headerKey {
	case BankCode:
		return "BankCode"
	case BranchCode:
		return "BranchCode"
	case AccountTypeCode:
		return "AccountTypeCode"
	case AccountNumber:
		return "AccountNumber"
	case RecipientName:
		return "RecipientName"
	case TransferAmount:
		return "TransferAmount"
	default:
		log.Printf("[BUG] not found headerKey %d", headerKey)
		return "Unknown"
	}
}
```

```go
// ConvertBankTransferRequestsToShiftJISCSV は UTF-8 文字列を含む振込申請のスライスを
// ShiftJIS 文字列を含む振込申請の CSV に変換して返す。
func ConvertBankTransferRequestsToShiftJISCSV(
	requests []*BankTransferRequest,
) (
	[]byte,
	error,
) {
	buf := &bytes.Buffer{}
	w := NewCSVWriter(buf)

	if err := w.Write(CSVHeader()); err != nil {
		return nil, err
	}

	for _, request := range requests {
		record := []string{
			request.BankCode,
			request.BranchCode,
			request.AccountTypeCode,
			request.AccountNumber,
			request.RecipientName,
			strconv.FormatInt(request.TransferAmount, 10),
		}
		if err := w.Write(record); err != nil {
			return nil, err
		}
	}
	w.Flush()

	return buf.Bytes(), nil
}
```

```go
func TestCreateShiftJISCSV(t *testing.T) {
	tests := []struct {
		name          string
		inputRequests []*BankTransferRequest
		wantCSV       func() ([]byte, error)
	}{
		{
			name: "濁点がないカタカナだけの振込申請を CSV に変換できる",
			inputRequests: []*BankTransferRequest{
				{
					BankCode:        "1",
					BranchCode:      "123",
					AccountTypeCode: "4",
					AccountNumber:   "1234567",
					RecipientName:   "ヤマナシ　タロウ",
					TransferAmount:  1000,
				},
			},
			wantCSV: func() ([]byte, error) {
				csv := fmt.Sprintf("%s,%s,%s,%s,%s,%d",
					"1",
					"123",
					"4",
					"1234567",
					"ﾔﾏﾅｼ ﾀﾛｳ",
					1000,
				)
				return utf8Tosjis(csv)
			},
		},
		{
			name: "濁点があるカタカナの振込申請を CSV に変換できる",
			inputRequests: []*BankTransferRequest{
				{
					BankCode:        "1",
					BranchCode:      "123",
					AccountTypeCode: "4",
					AccountNumber:   "1234567",
					RecipientName:   "ヤマナシ　タロウ",
					TransferAmount:  1000,
				},
				{
					BankCode:        "2",
					BranchCode:      "223",
					AccountTypeCode: "1",
					AccountNumber:   "2234567",
					RecipientName:   "ヤマダ　タロウ",
					TransferAmount:  100000,
				},
			},
			wantCSV: func() ([]byte, error) {
				csv := fmt.Sprintf("%s,%s,%s,%s,%s,%d",
					"1",
					"123",
					"4",
					"1234567",
					"ﾔﾏﾅｼ ﾀﾛｳ",
					1000,
				)
				csv += fmt.Sprintf("\n%s,%s,%s,%s,%s,%d",
					"2",
					"223",
					"1",
					"2234567",
					"ﾔﾏﾀﾞ ﾀﾛｳ",
					100000,
				)
				return utf8Tosjis(csv)
			},
		},
	}

	for _, test := range tests {
		got, err := ConvertBankTransferRequestsToShiftJISCSV(test.inputRequests)
		if err != nil {
			t.Errorf("[%s] got err %v, but want err nil", test.name, err)
		}

		wantCSV, err := test.wantCSV()
		if err != nil {
			t.Errorf("[%s] got err %v", err)
			continue
		}
		if string(got) != string(wantCSV) {
			t.Errorf(
				"[%s] got %s, but want %s (\n%x\n%x\n)",
				test.name,
				string(got),
				string(wantCSV),
				got,
				wantCSV,
			)
		}
	}
}

func utf8Tosjis(content string) ([]byte, error) {
	csv := "BankCode,BranchCode,AccountTypeCode,AccountNumber,RecipientName,TransferAmount\n"
	csv += content

	r := transform.NewReader(strings.NewReader(csv), japanese.ShiftJIS.NewEncoder())
	bs, err := ioutil.ReadAll(r)
	if err != nil {
		return nil, err
	}
	return bs, err
}
```