VimでPDCAを回す

アドベントカレンダー7日目です。
昨日は [@kanekom](https://qiita.com/kanekom) の [【YouTube Data API】BGMを教えてくれる神コメントを探す - Qiita](https://qiita.com/kanekom/items/fe142047efe9a60aa188) でした、面白かったですね！
さて、本カレンダーも1週間を生き残りました。そんなわけで小ネタ回です。

## やったもの

<blockquote class="twitter-tweet" data-theme="dark"><p lang="ja" dir="ltr">vimでPDCAを回す。たまに終焉が訪れる。終焉より先には進めない。<br><br>単語のインクリメント(?)をやろうと思ったが既に先人がいたので敬意を払って改悪（分岐する時はランダムに進む） <br><br>先人: <a href="https://t.co/AHff5bNuPa">https://t.co/AHff5bNuPa</a> <a href="https://t.co/QZEL6Fa3YO">pic.twitter.com/QZEL6Fa3YO</a></p>&mdash; なかもと (@yashinawa_retai) <a href="https://twitter.com/yashinawa_retai/status/1335602324610269185?ref_src=twsrc%5Etfw">December 6, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

この記事ではvimscriptのコードを読んで、少し改変します。

## 題材

### PDCA
PDCAとはなんだったかというと、業務改善のプロセスで **P**lan, **D**o, **C**heck, **A**ct の頭文字を取っています。

とても分かりやすい内容の上、とにかく回してさえいれば安心するものだそうで、4,5年前はPDCAとハンドスピナーのどちらかを回していれば大丈夫みたいな感じだった気がします。

最近はもう浸透しすぎて特段話題になることはありませんが、4月から社会人の自分はとても不安でPDCAを是非回したくなりました。そこで普段から滞在時間の長いvimでいつでも回せるようにします。

インスパイア元はこちらです。
[一生回してろ](https://vaaaaanquish.jp/pdca/)

### vimのインクリメント/デクリメント

vimには加算減算の機能があります。
>加算と減算
CTRL-A  カーソルの下または後の数字またはアルファベットに [count] を加える。
CTRL-X  カーソルの下または後の数字またはアルファベットから [count] を減じる。
[change - Vim日本語ドキュメント](https://vim-jp.org/vimdoc-ja/change.html#CTRL-A)

大まかに説明すると、数字の上にカーソルがあるとき、`<c-a>`とするとその数字が1増えます。`5<c-a>`ならば5増えます。`<c-a>`を`<c-x>`に変えれば減ります。

また、`g<c-a>`で連番もできます。
<blockquote class="twitter-tweet" data-theme="dark"><p lang="ja" dir="ltr">vimのインクリメントとデクリメントと連番作成 <a href="https://t.co/QNDTUiMogE">pic.twitter.com/QNDTUiMogE</a></p>&mdash; なかもと (@yashinawa_retai) <a href="https://twitter.com/yashinawa_retai/status/1335598030850990081?ref_src=twsrc%5Etfw">December 6, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

連番作成をしていて、スプレッドシートのように曜日とかもできればと思っていたところ、先人がいました。(のでPDCAを回すことにしました)
[monday.vim : Ctrl-a、Ctrl-xで曜日、月をループ （＋他の用途への応用） — 名無しのvim使い](https://nanasi.jp/articles/vim/monday_vim.html)


そして拡張可能な状態にしてくださっているのでそれを利用することにしますが、せっかくなのでコードを読んで理解して、敬意を払うことを忘れずに本体も少し改変します。
(ちなみに、この記事では連番までは実装していません。)

## コードを読む
PDCAを回すためには、こちら(再掲)のコードを読んで利用するのが一番早そうです。
[monday.vim : Ctrl-a、Ctrl-xで曜日、月をループ （＋他の用途への応用） — 名無しのvim使い](https://nanasi.jp/articles/vim/monday_vim.html)

大体の内容は
1. 対応する単語のペアを登録
2. ペアを辿る処理
3. キーマップ登録
となっているようです。

### ペアの集合を作成

```vimscript:monday.vimより
function s:AddPair(word1, word2)
    let w10 = tolower(a:word1)
    let w11 = toupper(matchstr(w10, '.')) . matchstr(w10, '.*', 1) 
    let w12 = toupper(w10)

    let w20 = tolower(a:word2)
    let w21 = toupper(matchstr(w20, '.')) . matchstr(w20, '.*', 1) 
    let w22 = toupper(w20)

    let s:words = s:words . w10 . ':' . w20 . ','
    let s:words = s:words . w11 . ':' . w21 . ','
    let s:words = s:words . w12 . ':' . w22 . ','
endfunction

let s:words = ''

" default AddPair pattern
call <SID>AddPair('monday', 'tuesday')
call <SID>AddPair('tuesday', 'wednesday')
call <SID>AddPair('wednesday', 'thursday')
call <SID>AddPair('thursday', 'friday')
call <SID>AddPair('friday', 'saturday')
call <SID>AddPair('saturday', 'sunday')
call <SID>AddPair('sunday', 'monday')
" ペア登録は抜粋
```

vimscriptに関する説明としては

- `a:fuga`で関数の引数`fuga`を参照
    - [usr\_41 - Vim日本語ドキュメント](https://vim-jp.org/vimdoc-ja/usr_41.html#41.7)
    - prefixでスコープを宣言するので確認すると良いと思います
- `s:hoge`で`hoge`はスクリプトローカル変数
- `<SID>`も`s:`と同じくスクリプトローカルを宣言するprefix
    - 違いはマップするときに用いるという点で、外部から呼び出したときにどのスクリプトから呼び出されたかわかる
        - [map - Vim日本語ドキュメント](https://vim-jp.org/vimdoc-ja/map.html#%3CSID%3E)

くらいでしょうか


`s:words`にペアを登録しているようなので確認します。
このコードのみをvimで編集しているときに、末尾に

```vimscript:上記に追記
echo s:words
```
と追記して、

```:コマンドモード
:w | so %
```
というコマンドを実行すると

```:出力例
monday:tuesday,Monday:Tuesday,MONDAY:TUESDAY,tuesday:wednesday,Tuesday:Wednesday,TUESDAY:WEDNESDAY,wednesday:thursday,Wednesday:Thursday,
WEDNESDAY:THURSDAY,thursday:friday,Thursday:Friday,THURSDAY:FRIDAY,friday:saturday,Friday:Saturday,FRIDAY:SATURDAY,saturday:sunday,Saturd
ay:Sunday,SATURDAY:SUNDAY,sunday:monday,Sunday:Monday,SUNDAY:MONDAY,
```

と、出力されます。大まかには下記のような内容でした。

1. シーケンシャルな値を登録するのではなく単語のペアを登録していく
2. ペアの両方について、全てが小文字、先頭のみ大文字、全て大文字の3パターンを作成
    3. 本来の`monday.vim`はコード内の日付を扱う目的なので
3. ペア内、ペア間でデリミタをそれぞれ`:`, `,`として文字列`s:words`で管理



### ペアを辿る処理

```vimscript:monday.vimより
function s:IncDec(inc_or_dec)
    let N = (v:count < 1) ? 1 : v:count
    let i = 0
    if a:inc_or_dec == 'inc'
        while i < N
            let w = expand('<cword>')
            if s:words =~# '\<' . w . ':'
                let n = match(s:words, w . ':\i\+\C')
                let n = match(s:words, ':', n)
                let a = matchstr(s:words, '\i\+', n)
                execute "normal ciw" . a
            else
                nunmap <c-a>
                execute "normal \<c-a>"
                call <SID>MakeMapping('inc')
            endif
            let i = i + 1
        endwhile
" 残りはデクリメントとendif, endwhile, endfunctionなので割愛
```
ここが主な処理だと思われます。エディタ内で動く言語として、テキスト処理が得意なvimscriptならではの表記が多いです。
数回の`match`, `matchstr`で`s:words`内を辿っています。

- `v:hoge`でvim内で定義されているグローバル変数`hoge`を参照
    - `v:count`には呼び出した時のカウント、`3wd`とかしたときの3が格納されます
        - こちらで理解できなくて[eval - Vim日本語ドキュメント](https://vim-jp.org/vimdoc-ja/eval.html#v:count)、こちらで理解しました[netrwを気持ちよく使いたい - Qiita](https://qiita.com/astrorobot110/items/50eb2b8666bdf706789a#vcount%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9)
- `expand(<cword>)`で現在カーソルの下にある単語を取得
    - [eval - Vim日本語ドキュメント](https://vim-jp.org/vimdoc-ja/eval.html#functions)
- 　`=~`は`==`とは異なり右辺を検索パターンとしたパターンマッチ
    - 特に`=~#`では大文字小文字を区別
    - 右辺の`'\<' . w . ':'`は`w`が`s:words`の任意の場所ではなく、単語の先頭で末尾にペア内のデリミタ`:`がついているというパターン
- `else`節では（おそらく）`nummap`でマップを削除して通常のインクリメントをして再マップ
    - [map - Vim日本語ドキュメント](https://vim-jp.org/vimdoc-ja/map.html#:nunmap)

### キーマップ

```vimscript:monday.vimより
function s:MakeMapping(inc_or_dec)
    if a:inc_or_dec == 'inc' || a:inc_or_dec == 'both'
        nmap <silent> <c-a> :<c-u>call <SID>IncDec('inc')<cr>
    endif
    if a:inc_or_dec == 'dec' || a:inc_or_dec == 'both'
        nmap <silent> <c-x> :<c-u>call <SID>IncDec('dec')<cr>
    endif
endfunction
call <SID>MakeMapping('both')
```
こちらはキーマップをしているだけでしたがいくつかわからない点があったのでそこだけまとめます。

- `<c-u>call`の`<c-u>`はコマンドモードでカーソル位置から行頭までを削除
    - 挙動はこれで理解して[key bindings - Understanding CTRL-U combination - Vi and Vim Stack Exchange](https://vi.stackexchange.com/questions/9751/understanding-ctrl-u-combination)、意図はこちら(再掲)で理解しました[netrwを気持ちよく使いたい - Qiita](https://qiita.com/astrorobot110/items/50eb2b8666bdf706789a#vcount%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9)

## PDCAを追加する

どうやらPDCAサイクルを追加するだけなら下記のみで可能なようです。

```vimscript:pdca.vimより
call <SID>AddPair('P', 'D')
call <SID>AddPair('D', 'C')
call <SID>AddPair('C', 'A')
call <SID>AddPair('A', 'P')

call <SID>AddPair('plan', 'do')
call <SID>AddPair('do', 'check')
call <SID>AddPair('check', 'act')
call <SID>AddPair('act', 'plan')
```

## PDCAの諸説に対応するために本体を改変する
しかし我々はPDCAには諸説あることを知っています。

- plan(計画), delay(遅延), cancel(中止), apologize(謝罪)
- plan(計画), doomsday(最後の日), catastrophe(破滅), apocalypse(終焉)

他にもpanic(狼狽), chaos(混沌)など多くが存在し、現場では何が起きるかわからないためこのプラグインでも対応したいものです。`monday.vim`を概観してきたのである程度のカスタマイズであれば可能ですね。

### 派生PDCAの追加
追加自体は上記と変わりありません。

```vimscript:pdca.vimより
call <SID>AddPair('plan', 'delay')
call <SID>AddPair('delay', 'cancel')
call <SID>AddPair('cancel', 'apologize')
call <SID>AddPair('apologize', 'plan')

call <SID>AddPair('plan', 'doomsday')
call <SID>AddPair('doomsday', 'catastrophe')
call <SID>AddPair('catastrophe', 'apocalypse')
```

`apocalypse`は終焉なのでの次のサイクルはありません。

### 運命のPlan

現在は3パターン登録してあります。全てplanから始まるので、運命を決める計画立案ということになります。`†計画の質†`で分岐させたいところですが今回はランダムに、1/3の確率で終焉を迎えることとします。

当然ながら本来はインクリメントで分岐したくないので、これまで読んできた参考元の`monday.vim`はパターンマッチングの際にはじめに一致したペアを取得しています。

そのため、まず分岐数を取得し、その後ランダムに選定という方針で編集しました。

```vimscript:pdca.vimより

function s:IncDec(inc_or_dec)
    let N = (v:count < 1) ? 1 : v:count
    let i = 0
    if a:inc_or_dec == 'inc'
        while i < N
            let w = expand('<cword>')
            " 対象の単語はペアとしていくつ登録されているか
            let hits = len(split(s:words, '\<' . w . ':', 1)) - 1
            " 登録されていなければ通常のインクリメント
            if !hits
                nunmap <c-a>
                execute "normal \<c-a>"
                call <SID>MakeMapping('inc')
            else
                " 1ペアであればmonday.vim
                if hits == 1
                    let n = match(s:words, w . ':\i\+\C')
                " 複数ペアであればランダムに選択
                else
                    let j = 0
                    let offsets = []
                    let offset = 0
                    while j < hits " s:wordsに登場する位置を保存していく
                        let n = match(s:words, w . ':\i\+\C', offset + 1)
                        let offsets = add(offsets, n)
                        let offset = offsets[-1]
                        let j = j + 1
                    endwhile
                    " 擬似乱数でリストから無作為に選択
                    let match_end = matchend(reltimestr(reltime()), '\d\+\.') + 1
                    let rand = reltimestr(reltime())[match_end : ] % hits
                    let n = offsets[rand]
                endif
                let n = match(s:words, ':', n)
                let a = matchstr(s:words, '\i\+', n)
                execute "normal ciw" . a
            endif
            let i = i + 1
        endwhile
" 残りはデクリメントとendif, endwhile, endfunctionなので割愛
```

下記の2点により、処理のわりにコードが少し長くなっている気がします。

- 文字列中からパターンマッチした個数を取得する関数がない
    - [Vim script で文字列内に一致する単語をカウント - C++でゲームプログラミング](https://osyo-manga.hatenadiary.org/entry/20130416/1366119991)
- 疑似乱数を生成するための関数がない
    - [vim-jp » Hack \#98: VimScriptで疑似乱数を生成する](https://vim-jp.org/vim-users-jp/2009/11/05/Hack-98.html)

できあがりはページ上部に載せているのでこれで終わりになります。

## 終わり

書き上げてみると半分ぐらい人様のコードを読んでいるだけでした。

みんな、回せ〜〜〜〜〜〜〜！！！！