空白区切りの文字列をそれぞれのワードで括る簡単なやり方
vim-jp の slack で以下のような質問をした
すると、すぐに数人の方からレスポンスをいただいた。
置換を使う方法や
ciw と ドットオペレーションと組み合わせてやる方法
マクロを使うことを提案していただいたりもした。
置換を使う方法で、 &
が 使われていて、私はこの記号を使ったことがなかったので勉強になった。おそらく、 検索文字全体を表す記号
なんだとおもう。
また、ciw と ドットオペレーションを組わせてやる方法では、 <C-R><C-O>
でレジスタの中身を挿入することでドットオペレーションで繰り返しても問題なく動作するようにするテクニックが使われており、色々と勉強になった。
ciwとドットオペレーションの繰り返しでは、<C-R>
でやる方法ではうまくいかない。
理由は以下の通り、<C-R>
ではドットレジスタに挿入後の値が入ってしまうため、繰り返しても期待した動作にならない。 <C-R><C-O>
は ドットレジスタに <C-R><C-O
がそのまま積まれるので、繰り返しても都度レジスタの内容が展開され、問題なく動作するということのようだ。
結論
Vim は奥が深い!!
vaffle.vim から netrw にお試しで移行してみた
今まで、vimのファイラとして vaffle.vim を使ってきた。正確な使用期間は思い出せないが私がvimを使い始めてから結構な期間使っていたと思う。
ただ、vaffle.vim は buftype を nofile に設定している関係上、 grep をしたときに閉じずに残ってしまうという現象が発生する。これは、dirvish でも同じ現象になるので、vaffle.vim だけの問題ではなさそうだった。自前でファイラを作ってしまおうか?とも思ったが、調べていく途中で、netrwでは発生しない問題だということに気いたので、netrwをカスタマイズして使えるようにすれば解決するのではないか?という思いから、vaffle.vim をやめてnetrw に移行することにした。
最近、Vim界隈で有名なゴリラさんもnetrwをお勧めされているようで、以下に記事がある。それを参考にしつつ私のvimrcもいじってみた。
以下が私のnetrwの設定である。
" バナーを非表示 let g:netrw_banner=0 " ファイル/ディレクトリ名のみ表示(日付等は表示しない) let g:netrw_liststyle=0 " プレビューは縦分割する(使うことはあまりなさそう?) let g:netrw_preview=1 " デフォルト表示をドットファイル非表示に設定 let g:netrw_hide=1 " g:netrw_hide で非表示にするファイルの設定. " これがないとちゃんと非表示にならない let g:netrw_list_hide='\(^\|\s\s\)\zs\.\S\+' augroup NetrwKeyMap au! " 親ディレクトリに移動する (h) au FileType netrw nmap <buffer> h - " ファイルを開く、ディレクトリ移動 (l) au FileType netrw nmap <buffer> l <CR> " ウィンドウ移動 (s) // デフォルト動作はソートの種類変更 " (あまりつかわないかなぁという理由でつぶす) au FileType netrw nmap <buffer> s <C-W> " ドットファイルの表示/非表示切り替え " vaffle.vim に挙動を合わせる au FileType netrw nmap <buffer> . gh augroup END
とりあえずはこの設定でやってみようと思う。懸念点としては、今まで i
でファイルを作成していたがそれができなくなったことくらいかなぁと思っている。netrwのいい点は、デフォルトで x
で関連付けられたプログラムでファイルを表示することができることや、 c
で表示中のディレクトリをカレントディレクトリにすることができること。vaffle.vim ではシンプルさを追求しているため、このあたりの動作は実装されていなかったので自前で拡張する必要があったが、netrwはその必要がないので、vimrcがすっきりしそうだなぁという感覚がある。
vim の :rubyfile は rb_load を使うが ruby の load は rb_f_load を使う
事の発端
if_ruby を使って何かしたいなぁということで :rubyfile test.rb
を実行したが、LoadError: cannot load such file -- test.rb
となって実行できなかった。ただ、:ruby load "test.rb"
は実行できるので、何が違うのか?という疑問が事の発端
vim-jp の slack で質問
slack で質問してみると :rubyfile
は rubyの公開APIの rb_load()
を呼んでいるだけとのこと。また、ruby load
は Kernel.#load‘ を使用しているが、なぜ
:rubyfile` のほうが動かないのかはわからないとのことで原因を追究してみたくなった。
Kernel.#load は rb_load
を実行しているのか?
2.6のブランチの load.c
を見ると、 load
は rb_f_load()
をコールしているようです。
https://github.com/ruby/ruby/blob/ruby_2_6/load.c#L1233
rb_load
も load.c
に実装があり、単に rb_load_internal()
をコールしているのみとなっているようです。
https://github.com/ruby/ruby/blob/ruby_2_6/load.c#L659
rb_f_load()
の中でも rb_load_internal()
をコールしていますが、前処理がいくつか入っているため、その部分に今回の原因がありそう?
rb_f_load の前処理を追う
追うといっても前処理で生成されている文字列を一個一個printfで出力してみるというだけのことをしました。
static VALUE rb_f_load(int argc, VALUE *argv) { printf("rb_f_load start\n"); // 追加 VALUE fname, wrap, path, orig_fname; rb_scan_args(argc, argv, "11", &fname, &wrap); printf("fname(1): %s\n", StringValueCStr(fname)); // 追加 file_to_load(fname); orig_fname = rb_get_path_check_to_string(fname, rb_safe_level()); printf("orig_fname:%s\n", StringValueCStr(orig_fname)); // 追加 fname = rb_str_encode_ospath(orig_fname); printf("fname(2): %s\n", StringValueCStr(fname)); // 追加 RUBY_DTRACE_HOOK(LOAD_ENTRY, RSTRING_PTR(orig_fname)); path = rb_find_file(fname); if (!path) { printf("path is null?? \n"); // 追加 if (!rb_file_load_ok(RSTRING_PTR(fname))) load_failed(orig_fname); path = fname; } printf("path: %s\n", StringValueCStr(path)); // 追加 printf("wrap: %d\n", RTEST(wrap)); // 追加 rb_load_internal(path, RTEST(wrap)); RUBY_DTRACE_HOOK(LOAD_RETURN, RSTRING_PTR(orig_fname)); printf("rb_f_load start\n"); // 追加 return Qtrue; }
結果的には、load "foo.rb"
のようなスクリプトで文字列が変化することはなく。rb_load_internal()
には foo.rb
という文字列がそのまま渡されているようでした。wrap
の値が怪しいのかな?とも思って見てみましたが、それも if_ruby の値と同じでした。
rb_load の実装を見てみる
rb_load()
はrb_load_internal()
をコールしているだけといいましたが、実際には引数で渡ってきたパスを一旦file_to_load()
関数に渡していました。
void rb_load(VALUE fname, int wrap) { rb_load_internal(file_to_load(fname), wrap); }
file_to_load()
の実装は以下の通りです。FilePathValue()
ではエラーになってなかったっぽいので、rb_find_file()
でエラーになって返り値が0になるため、load_failed()
が呼ばれてしまうようです。
static VALUE file_to_load(VALUE fname) { VALUE tmp = rb_find_file(FilePathValue(fname)); if (!tmp) load_failed(fname); return tmp; }
rb_find_file
は rb_f_load()
の中でも使われており、返り値が0の場合の処理が rb_load()
とは異なっているようです。
path = rb_find_file(fname); if (!path) { if (!rb_file_load_ok(RSTRING_PTR(fname))) load_failed(orig_fname); path = fname; }
rb_file_load_ok()
という関数がキモな気がしてきました。
rb_file_load_ok()
は file.c
内に定義されています。ただ、内容を見た感じ、fdを取得して内部でロード可能かを判断しているだけのようなので、特別何があるというわけでもなさそう。カレントディレクトリにロード対象のファイルがあれば、それをちゃんとロード可能だと判断してくれそうな気がします。というかしてくれます。
結論
Kernel.#load
は rb_f_load()
を呼んでいて、vimの:rubyfile
はrb_load()
を呼んでいる。rb_load()
の中では file_to_load()
が使われていて、この関数の中のrb_find_file()
が返り値0を返すためロードエラーとなっている。rb_f_load()
でも同関数が使われている。ただ、一度ロードには失敗するが、そのあとの処理でrb_file_load_ok()
でロード可能と判断されるため正常にロードされる。
なぜ、このような違いがあるのかまでは今回は追えなかったが、一応 :rubyfile
が ruby load
と異なる動作をする理由がわかったので今回は終了とする。
if_ruby 付き vim を使って ruby を実行するには PATHに ruby_builtin_dlls も足す必要がある
Could not load library msvcrt-ruby240.dll · Issue #2660 · vim/vim · GitHub
毎日ビルドしている Vim に if_ruby を足したのだが、 :ruby print "Hello"
をしても dllが見つからない旨のエラーが発生してしまってなんでだ?と思っていたら、上のサイトで示されている通り、ruby_builtin_dlls
を 環境変数の PATHに設定する必要があるとのこと。昔はそれでもよかったっぽいけど、ruby 2.4 以降からは必要らしい。日本語の説明記事がなかったので、メモとして書いた。
WEB上で打刻できるタイムカードサービスを作った
タイトルで落ちてるんですが、WEB上で打刻できるタイムカードサービスを作りました。
アクセスすると、ログインダイアログが表示されます。
Sign-Up
ボタンを押すと、ユーザー登録画面になります。
ユーザー登録してもらうと、仮登録状態になります。
登録したメール宛にメールが送信されるのでメール上のリンクを 押してもらって、本登録を完了してください。
リンクをクリックすると、以下のページが表示されます。 このページはこれ以上なにも起こらないので閉じてください。
上記のページが表示されると、ユーザー検証中のページが以下のページに遷移します。
名前を入れてもらうと、タイムカードページになります。
オレンジのボタンを押してもらうと、出社状態になります。
退社ボタンで退社します。
はい、それだけです。
時間が自動で計算されたりもしないし、管理者モードみたいなものもないので使い勝手は微妙ですが、もしよかったら使ってみてください。
ソースは こちら にありますので、PRもお待ちしております。
自然順ソート(マルチバイト対応改良版 & Windows エクスプローラー準拠)
何度も同じようなブログを連投してしまいますが、先のブログには大きな数字を使うとちゃんと動かないという欠点がありました。今回はそれを改良したバージョンになります。
先のバージョンでは、strconv.Atoi を使用している関係で どうしても数値が int の範囲を超えることができませんでした。
今回はstrconv.Atoi を使用せずに、文字列比較のみで数字を順番に並べます。
Windows エクスプローラーを色々触って確かめていると、どうもゼロサプレスした値を右寄せ比較しているだけなのではないか?ということに気付きました。
なので、数字比較の処理の中で頭の 0
は読み飛ばした後、右寄せ比較を行うようにしました。
そうすると、WIndows エクスプローラーのソートに近づけることができました。
package main import ( "fmt" "sort" "unicode" ) func getRune(ru []rune, i int) (rune, int) { for x := i; x < len(ru); x++ { if unicode.IsSpace(ru[x]) { continue } return ru[x], x } return rune(0), len(ru) } func compareNumber(a, b []rune, ai, bi int) int { aj := ai for len(a) > aj { if a[aj] != '0' { break } aj++ } bj := bi for len(b) > bj { if b[bj] != '0' { break } bj++ } bias := 0 for { if (len(a) <= aj || !unicode.IsNumber(a[aj])) && (len(b) <= bj || !unicode.IsNumber(b[bj])) { return bias } if len(a) <= aj || !unicode.IsNumber(a[aj]) { return -1 } if len(b) <= bj || !unicode.IsNumber(b[bj]) { return 1 } ca := a[aj] cb := b[bj] if ca < cb { if bias == 0 { bias = -1 } } if ca > cb { if bias == 0 { bias = 1 } } aj++ bj++ } } func natualSort0(a, b []rune) int { var ca, cb rune var ai, bi int for { if len(a) <= ai && len(b) <= bi { return 0 } if len(a) <= ai { return -1 } if len(b) <= bi { return 1 } ca, ai = getRune(a, ai) cb, bi = getRune(b, bi) if unicode.IsNumber(ca) && unicode.IsNumber(cb) { if ret := compareNumber(a, b, ai, bi); ret != 0 { return ret } } if ca < cb { return -1 } if ca > cb { return 1 } ai++ bi++ } } func naturalSort(strings []string) func(i, j int) bool { return func(i, j int) bool { return natualSort0([]rune(strings[i]), []rune(strings[j])) <= 0 } } func dumpByNaturalSort(strings []string) { sort.Slice(strings, naturalSort(strings)) for i, s := range strings { if i != 0 { fmt.Print(" < ") } fmt.Printf("%+v", s) } fmt.Println() } func main() { dumpByNaturalSort([]string{"a20", "a10", "a2", "a1b", "a1a", "a1", "a0", "a"}) // a < a0 < a1 < a1a < a1b < a2 < a10 < a20 dumpByNaturalSort([]string{"x8-y8", "x2-y08", "x2-y7", "x2-g8", "x01-y09"}) // x01-y09 < x2-g8 < x2-y7 < x2-y08 < x8-y8 dumpByNaturalSort([]string{"1.3", "1.1", "1.02", "1.010", "1.002", "1.001"}) // 1.001 < 1.1 < 1.002 < 1.02 < 1.3 < 1.010 dumpByNaturalSort([]string{"001_a", "001", "01", "1", "10", "010", "2", "002"}) // 001 < 001_a < 01 < 1 < 002 < 2 < 010 < 10 dumpByNaturalSort([]string{"a001", "a01", "a1", "a10", "a010", "a2", "a002"}) // a001 < a01 < a1 < a002 < a2 < a010 < a10 dumpByNaturalSort([]string{"3", "1", "10", "2", "20"}) // 1 < 2 < 3 < 10 < 20 dumpByNaturalSort([]string{"4294967294", "1", "4294967296", "4294967299", "11111111111111111222222222222222"}) // 1 < 4294967294 < 4294967296 < 4294967299 < 11111111111111111222222222222222 }
自然順ソート(マルチバイト対応)
2019/4/17 追記
改良版書きました!!
前回、前々回 の続きですが、マルチバイトの数字には対応できていなかったので対応させました。こちら を参考にしつつ、Windows エクスプローラー準拠の動作を実現させました。
前回のコードのままだと strconv.Atoi()
でマルチバイト数字はパースエラーとなってしまうので、その前段に golang.org/x/text/width
パッケージを挟むことで、全角数字を半角数字にしてから数字に変換しています。golang にはこういうパッケージが標準/準標準でたくさんあるのでありがたいですね。
package main import ( "fmt" "sort" "strconv" "unicode" "golang.org/x/text/width" ) func getRune(ru []rune, i int) (rune, int) { for x := i; x < len(ru); x++ { if unicode.IsSpace(ru[x]) { continue } return ru[x], x } return rune(0), len(ru) } func natualSort0(a, b []rune) int { var ca, cb rune var ai, bi int for { if len(a) <= ai && len(b) <= bi { return 0 } if len(a) <= ai { return -1 } if len(b) <= bi { return 1 } ca, ai = getRune(a, ai) cb, bi = getRune(b, bi) if unicode.IsNumber(ca) && unicode.IsNumber(cb) { aj := ai + 1 for len(a) > aj { if !unicode.IsNumber(rune(a[aj])) { aj-- break } aj++ } bj := bi + 1 for len(b) > bj { if !unicode.IsNumber(rune(b[bj])) { bj-- break } bj++ } sa := a[ai:aj] if len(sa) == 0 { sa = []rune{a[ai]} } ia, err := strconv.Atoi(width.Narrow.String(string(sa))) if err != nil { panic(err) } sb := b[bi:bj] if len(sb) == 0 { sb = []rune{b[bi]} } ib, err := strconv.Atoi(width.Narrow.String(string(sb))) if err != nil { panic(err) } if ia < ib { return -1 } if ia > ib { return 1 } // Windows エクスプローラーの仕様と合わせるために // 今回は削除 // if len(a[ai:aj]) < len(b[bi:bj]) { // return -1 // } // if len(a[ai:aj]) > len(b[bi:bj]) { // return -1 // } // ai = aj // bi = bj } if ca < cb { return -1 } if ca > cb { return 1 } ai++ bi++ } } func naturalSort(strings []string) func(i, j int) bool { return func(i, j int) bool { return natualSort0([]rune(strings[i]), []rune(strings[j])) <= 0 } } func dumpByNaturalSort(strings []string) { sort.Slice(strings, naturalSort(strings)) for i, s := range strings { if i != 0 { fmt.Print(" < ") } fmt.Printf("%+v", s) } fmt.Println() } func main() { dumpByNaturalSort([]string{"a20", "a10", "a2", "a1b", "a1a", "a1", "a0", "a"}) // a < a0 < a1 < a1a < a1b < a2 < a10 < a20 dumpByNaturalSort([]string{"x8-y8", "x2-y08", "x2-y7", "x2-g8"}) // x2-g8 < x2-y7 < x2-y08 < x8-y8 dumpByNaturalSort([]string{"1.3", "1.1", "1.02", "1.010", "1.002", "1.001"}) // 1.001 < 1.1 < 1.002 < 1.02 < 1.3 < 1.010 dumpByNaturalSort([]string{"001", "01", "1", "10", "010", "2", "002"}) // 001 < 01 < 1 < 002 < 2 < 010 < 10 dumpByNaturalSort([]string{"a001", "a01", "a1", "a10", "a010", "a2", "a002"}) // a001 < a01 < a1 < a002 < a2 < a010 < a10 dumpByNaturalSort([]string{"3", "1", "10", "2", "20"}) // 1 < 2 < 3 < 10 < 20 }