VimでPDCAを回す


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

やったもの

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

題材

PDCA

PDCAとはなんだったかというと、業務改善のプロセスで Plan, Do, Check, Act の頭文字を取っています。

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

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

インスパイア元はこちらです。
一生回してろ

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

vimには加算減算の機能があります。

加算と減算
CTRL-A カーソルの下または後の数字またはアルファベットに [count] を加える。
CTRL-X カーソルの下または後の数字またはアルファベットから [count] を減じる。
change - Vim日本語ドキュメント

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

また、g<c-a>で連番もできます。

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

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

コードを読む

PDCAを回すためには、こちら(再掲)のコードを読んで利用するのが一番早そうです。
monday.vim : Ctrl-a、Ctrl-xで曜日、月をループ (+他の用途への応用) — 名無しのvim使い

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

ペアの集合を作成

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を参照
  • s:hogehogeはスクリプトローカル変数
  • <SID>s:と同じくスクリプトローカルを宣言するprefix
    • 違いはマップするときに用いるという点で、外部から呼び出したときにどのスクリプトから呼び出されたかわかる

くらいでしょうか

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

上記に追記
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パターンを作成
    1. 本来のmonday.vimはコード内の日付を扱う目的なので
  3. ペア内、ペア間でデリミタをそれぞれ:, ,として文字列s:wordsで管理

ペアを辿る処理

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, matchstrs:words内を辿っています。

  • v:hogeでvim内で定義されているグローバル変数hogeを参照
  • expand(<cword>)で現在カーソルの下にある単語を取得
  •  =~==とは異なり右辺を検索パターンとしたパターンマッチ
    • 特に=~#では大文字小文字を区別
    • 右辺の'\<' . w . ':'ws:wordsの任意の場所ではなく、単語の先頭で末尾にペア内のデリミタ:がついているというパターン
  • else節では(おそらく)nummapでマップを削除して通常のインクリメントをして再マップ

キーマップ

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')

こちらはキーマップをしているだけでしたがいくつかわからない点があったのでそこだけまとめます。

PDCAを追加する

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

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の追加

追加自体は上記と変わりありません。

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はパターンマッチングの際にはじめに一致したペアを取得しています。

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

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点により、処理のわりにコードが少し長くなっている気がします。

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

終わり

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

みんな、回せ〜〜〜〜〜〜〜!!!!