アドベントカレンダー7日目です。
昨日は @kanekom の 【YouTube Data API】BGMを教えてくれる神コメントを探す - Qiita でした、面白かったですね!
さて、本カレンダーも1週間を生き残りました。そんなわけで小ネタ回です。
やったもの
vimでPDCAを回す。たまに終焉が訪れる。終焉より先には進めない。
— なかもと (@yashinawa_retai) December 6, 2020
単語のインクリメント(?)をやろうと思ったが既に先人がいたので敬意を払って改悪(分岐する時はランダムに進む)
先人: https://t.co/AHff5bNuPa pic.twitter.com/QZEL6Fa3YO
この記事では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>
で連番もできます。
vimのインクリメントとデクリメントと連番作成 pic.twitter.com/QNDTUiMogE
— なかもと (@yashinawa_retai) December 6, 2020
連番作成をしていて、スプレッドシートのように曜日とかもできればと思っていたところ、先人がいました。(のでPDCAを回すことにしました)
monday.vim : Ctrl-a、Ctrl-xで曜日、月をループ (+他の用途への応用) — 名無しのvim使い
そして拡張可能な状態にしてくださっているのでそれを利用することにしますが、せっかくなのでコードを読んで理解して、敬意を払うことを忘れずに本体も少し改変します。
(ちなみに、この記事では連番までは実装していません。)
コードを読む
PDCAを回すためには、こちら(再掲)のコードを読んで利用するのが一番早そうです。
monday.vim : Ctrl-a、Ctrl-xで曜日、月をループ (+他の用途への応用) — 名無しの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日本語ドキュメント
- prefixでスコープを宣言するので確認すると良いと思います
-
s:hoge
でhoge
はスクリプトローカル変数 -
<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,
と、出力されます。大まかには下記のような内容でした。
- シーケンシャルな値を登録するのではなく単語のペアを登録していく
- ペアの両方について、全てが小文字、先頭のみ大文字、全て大文字の3パターンを作成
3. 本来のmonday.vim
はコード内の日付を扱う目的なので - ペア内、ペア間でデリミタをそれぞれ
:
,,
として文字列s:words
で管理
ペアを辿る処理
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日本語ドキュメント、こちらで理解しましたnetrwを気持ちよく使いたい - Qiita
-
-
expand(<cword>)
で現在カーソルの下にある単語を取得 -
=~
は==
とは異なり右辺を検索パターンとしたパターンマッチ- 特に
=~#
では大文字小文字を区別 - 右辺の
'\<' . w . ':'
はw
がs:words
の任意の場所ではなく、単語の先頭で末尾にペア内のデリミタ:
がついているというパターン
- 特に
-
else
節では(おそらく)nummap
でマップを削除して通常のインクリメントをして再マップ
キーマップ
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、意図はこちら(再掲)で理解しましたnetrwを気持ちよく使いたい - Qiita
PDCAを追加する
どうやらPDCAサイクルを追加するだけなら下記のみで可能なようです。
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の追加
追加自体は上記と変わりありません。
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
はパターンマッチングの際にはじめに一致したペアを取得しています。
そのため、まず分岐数を取得し、その後ランダムに選定という方針で編集しました。
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点により、処理のわりにコードが少し長くなっている気がします。
- 文字列中からパターンマッチした個数を取得する関数がない
- 疑似乱数を生成するための関数がない
できあがりはページ上部に載せているのでこれで終わりになります。
終わり
書き上げてみると半分ぐらい人様のコードを読んでいるだけでした。
みんな、回せ〜〜〜〜〜〜〜!!!!