[vim] csvで指定された文字数から、別カラムの文字列長を変更する

2020/12/29に公開

急に対応が必要になったのでメモ。
以下csvの第2カラムの文字列を第1カラムで指定された文字数に到達するような文字列に修正します。

スタート

文字数,文字列
10,hogehogeho
10,over_over_over_over
30,aabbcc
40,男女男男

ゴール

文字数,文字列
10,hogehogeho
10,Invalid
30,aabbcccccccccccccccccccccccccc
40,男女男男男男男男男男男男男男男男男男男男男男男男男男男男男男男男男男男男男男男男

前提:

  • 第1カラムには必ず1以上の数値が入る
  • 第2カラムには改行コード、カンマは含まれない
  • 第1カラム、第2カラムともにダブルクォートでの囲み無し
  • 第2カラムにはマルチバイト文字が入ることあり。

対応内容:

  1. 第2カラムの文字列が第1カラムの文字数に到達していない場合、第2カラムの末尾文字を付け足して文字数に到達する文字列に修正する(文字数5, 文字列"abc" -> 修正後文字列"abccc")
  2. 第2カラムの文字数が第1カラムの文字数を超える場合は"Invalid"に修正する(文字数5, 文字列"abcdef" -> 修正後文字列"Invalid")
  3. 第2カラムの文字数が第1カラムの文字数と一致する場合、修正せずそのまま。

実装

まず以下関数を作成します。

" 文字数(第1カラム)と文字列(第2カラム)を受け取り、修正後の文字列を返却する関数
function! MyCsvFormat(num, str) abort
  let l:chars = split(a:str, '\ze')
  let l:length = len(chars)
  if a:num < length
    return "Invalid"
  else
    return a:str . repeat(l:chars[-1], a:num - l:length)
  endif
endfunction

そして、csvファイルを開き、以下コマンドを実行します。

:%s/^\(\d\+\),\zs\(.\+\)$/\=MyCsvFormat(submatch(1), submatch(2))/g

以上で期待通り修正できました。:substitute実行時に何を行っているか少し説明すると、
\(...\) でキャプチャ、
\zs でマッチの開始地点(今回のケースでいうと置換の開始地点)を指定、
\= で式(今回のケースでいうとMyCsvFormat関数)を使用して置換、
submatch() でキャプチャした第1カラムと第2カラムを関数の引数に指定しています。

\= 使うと関数呼び出せるのでもう何でもあり。
めちゃくちゃ強力だと思うので、是非使ってみてください。

あと、MyCsvFormatの中で文字列の文字数を取得する際にlen()を使ったのですが、この関数だと文字数ではなく、バイト数を返すようでうまくできませんでした。仕方なくsplit()で配列にしてから要素数で文字数を求めていますが、なんかびみょい。

strchars() という文字数を返すドンピシャな関数を見つけたのですが、ヘルプに記載されている合成文字というのが何かよく分からず未使用。。ただ、おそらくマルチバイト文字含む文字列の末尾の文字を取得するのもいったん配列にするしかなさそう ("漢字漢"[-1:]もマルチバイトに対応していないみたいだった)なので、まーこのままで良しとします!

:h :substitute
:h len()
:h split()
:h strchars()
:h submatch()
:h s/\=
:h /\(
:h /\zs

Discussion