退職願AAを出力するコマンドを作った

# 退職の意思のご連絡

```bash
$ taishoku -y 2019 -m 3 -d 18 --your-name あなた野なま江
```

```



　　　代株　　　　二退こ　　　　　　
　　　表式　　　　〇職の　　　　　　
　　　取会　　　　一いた　　　　　　
　　　締社　　　　九たび　　退　　　
　　　役ジ　　　　年し一　　　　　　
　　　　ョ　　　　三た身　　職　　　
　　　ジー　　　　月く上　　　　　　
　　　ョク　　　　一この　　願　　　
　　　ーコ　　　　七こ都　　　　　　
　　　クマ　　　　日に合　　　　　　
　　　山ン　　　　　おに　　　　　　
　　　悪ド　　　　　願よ　　　　　　
　　　ふ　　　　　　いり　　　　　　
　　　ざ　　　　　　申、　　　　　　
　　　け　　あジ　　し二　　　　　　
　　　太　　なョ　　上〇　　　　　　
　　　郎　　たー　　げ一　　　　　　
　　　殿　　野ク　　ま九　　　　　　
　　　　　　なコ　　す年　　　　　　
　　　　　　まマ　　。三　　　　　　
　　　　　　江ン　　　月　　　　　　
　　　　　　　ド　　　一　　　　　　
　　　　　　印開　　　八　　　　　　
　　　　　　　発　　　日　　　　　　
　　　　　　　部　　　を　　　　　　
　　　　　　　　　　　も　私　　　　
　　　　　　　第　　　っ　儀　　　　
　　　　　　　一　　　て　　　　　　
　　　　　　　課　　　　　　　　　　




```

# はじめに

退職願・退職届のアスキーアートを出力するコマンドを作成しました。
開発言語はGoです。

https://github.com/jiro4989/taishoku

# 作った経緯

退職の意思を伝える方法は、何も手書きの紙だけである必要はない、そう思いませんか？
コマンドラインからリモートサーバ先にアスキーアートで伝えても良い
HTMLファイルでもよい、いろんなプローチがあっても良い...そう思ったからではないです。

アスキーアートで表現できれば、例えば以下のようにリモートサーバで共同で作業をしている方に
退職の意思を伝えるテロが可能です。

```bash
$ taishoku | wall
```

CLIから扱えれば、`mail`コマンドや`at`コマンドで時間差で退職の意思を伝えることが可能で、
明日からしばらく職場に復帰できない、でも退職の意思は伝えておきたいといった状況に対応できるかと思います。別にそういった用途を想定していたわけではありませんが。

*本当に作った経緯は、単純にジョーク系のコマンドを何か作ってみたかったからというだけです。*

# インストール

以下のコマンド、あるいは前述のリンク先のGitHubReleaseから。

```bash
go get -u github.com/jiro4989/taishoku
```

# 使い方

ヘルプにも書いていますが、動的に変化しうる箇所についてはすべてコマンドラインオプションから指定できるようになっています。

```
Flags:
      --todoke                  taishoku todoke
  -o, --offset int              offset (default 3)
      --format string           format [html]
  -y, --year int                year (default 2999)
  -m, --month int               month (default 12)
  -d, --day int                 day (default 31)
  -D, --department string       your department (default "ジョークコマンド開発部")
  -T, --team string             your team (default "第一課")
  -n, --your-name string        your name (default "真面目田マジメ")
  -C, --company string          company name (default "株式会社ジョークコマンド")
  -P, --president string        president (default "代表取締役")
  -N, --president-name string   president name (default "ジョーク山悪ふざけ太郎")
  -X, --debug                   debug logging flag.
  -h, --help                    help for taishoku
```

HTML出力できるようにもなっています。退職届もだせます。
以下のように。

```bash
$ taishoku --format html > a.html
```

![html.png](https://qiita-image-store.s3.amazonaws.com/0/69665/da2967e4-3fca-df02-3632-91139aadf797.png)

フロントエンドから今まで縁遠かったのでCSS縦書きを使うのはこれが初めてでした。

HTML出力はできますけれど、別に印刷用に幅を整えていたりはしないので
多分印刷して提出しようとしてもいい感じにはならないと思います。
印刷して確認してすらいないです。

動作確認はChromeとFirefoxだけ。
Firefoxはブラウザ間差異で左端によってしまってましたが、スルーしました。
Chromeでは中央に表示されます。

# 実装

AA出力のロジックは下記の通り。

1. AAに関してはテンプレートになるテキストに動的に変化する値をReplaceで埋め込む
2. 全角空白でパディング
3. 横書きを縦書きに変換
4. 出力

HTMLに関しては、AAと違い空白パディングや縦書き変換の必要はなくて
縦書きにする処理をすべてCSSに一任しています。
よって、前述の箇条書きの１と４だけです。
やってることは`template.Execute`だけですし、特に書くことがないので割愛。

## テンプレートへのReplace埋め込み

何ら特殊なことをしていない、普通の置換です。雑です。

```go
const taishokuNegai = `
　　　退　職　願
　　　　　　　　　　　　　　　　　　　　　　　　　私儀

このたび一身上の都合により、{taishokuDate}をもって
退職いたしたくここにお願い申し上げます。
{today}

　　　　　　　　　　　　　　{department}　{team}
　　　　　　　　　　　　　　{yourName}　印

{company}
{president}　{presidentName}殿
`

// makeTaishokuText は退職テキストを生成する。
func makeTaishokuText(templateText, taishokuDate, today, department, team, yourName, company, president, presidentName string) string {
	text := templateText
	convs := map[string]string{
		"{taishokuDate}":  taishokuDate,
		"{today}":         today,
		"{department}":    department,
		"{team}":          team,
		"{yourName}":      yourName,
		"{company}":       company,
		"{president}":     president,
		"{presidentName}": presidentName,
	}
	for k, v := range convs {
		text = strings.Replace(text, k, v, 1)
	}
	return text[1 : len(text)-1]
}
```

## 全角空白でのパディング

空白はすべて全角スペースで埋めています。
単純にテキストとして出力したときの見栄え的に、半角スペースで埋めると
プロポーショナルフォントのときに見た目が崩れるためです。
そのため、半角英数字は全角英数字に内部的に変換するようにしています。

```go
// padSpace は文字列のスライスのうち、一番長い文字列にあわせてテキストの後ろに全角空白を埋める。
// 返却する文字列は新しい配列なので、引数に渡した配列に破壊的変更は与えない。
// また、空白はマルチバイト全角空白である。
// ここでの空白埋めは見た目上のテキストの長さを指す。
func padSpace(s []string) []string {
	text := make([]string, len(s))
	copy(text, s)

	var maxLength int
	// 一番長い文字列の長さを取得
	for _, t := range text {
		l := utf8.RuneCountInString(t)
		if maxLength < l {
			maxLength = l
		}
	}

	// 一番長い文字列の長さに合わせて空白を追加
	for i := 0; i < len(text); i++ {
		diff := maxLength - utf8.RuneCountInString(text[i])
		if 0 < diff {
			pad := strings.Repeat("　", diff)
			text[i] += pad
		}
	}

	return text
}
```

## 横書きを縦書きに変換

最初にReplaceで値を埋め込んだテキストは横書きなので、
このまま出力しても退職願っぽくないので、縦書きに変換します。

[マルチバイト文字列に単純に配列のインデックスとしてアクセスすると化ける](https://play.golang.org/p/_cWSt5lfw4e)ので
一度rune配列に変換してアクセスしています。

文字列の長さも`len`関数で単純に数えるとバイトサイズになってしまうので
マルチバイト文字列の文字の数、として数えるためにencoding/utf8パッケージの `RuneCountInString(string)`を使用しています。

Goはマルチバイト文字が楽に扱えるのでテキスト処理がやりやすいですね。

```go
// reverse は配列の並びを反転させる。
// 文字順で逆順ソートとかしたりはしない。
func reverse(s []string) []string {
	s2 := make([]string, len(s))
	for i, j := len(s)-1, 0; 0 <= i; i, j = i-1, j+1 {
		s2[j] = s[i]
	}
	return s2
}

// toVertical は横書き文字列配列を縦書き配列に変換する。
// 前提として、引数の配列はrune文字列としてすべて長さが同じでなければならない。
func toVertical(s []string) (ret []string) {
	if len(s) < 1 {
		return []string{}
	}

	l := utf8.RuneCountInString(s[0])
	for i := 0; i < l; i++ {
		var line []string
		for _, row := range s {
			for colIdx, c := range []rune(row) {
				if colIdx == i {
					line = append(line, string(c))
				}
			}
		}
		line = reverse(line)
		s := strings.Join(line, "")
		ret = append(ret, s)
	}
	return
}
```

# 余談

余談ですが、今月（2019年3月）末で現職を退職します（マジ）。
別にこのコマンドは使ってないですし、普通に手書きで作成して提出しました。
お世話になった会社ですので流石に。

この記事を見た方も、いないとは思いますが
くれぐれも冗談以外でこんなコマンド使わないように。

## 追記 (2020/03/03)

退職後、数ヶ月遊んでから2019年7月末に転職しました。
現在は新しい職場で元気にやっています。