テキストファイルをソートしたくなった時、どうすればよいのだろうか?
最近vimのC/C++用辞書ファイルを昇順にソートした。辞書ファイルにはC/C++のキーワードが450個ぐらい書いてある。キーワードは改行で区切られているので、1行に1単語だ。
最初に思いつくのはコマンドプロンプトのsortを使う方法だ。
sort c_cpp_keyword.dic > c_cpp_keyword.dic.new
しかしこの方法では英大文字と小文字が混ざってしまう。
ソートした結果が、ASCIIコード表にならって、
AB aa ac
↑のように並んで欲しかったのだが、
aa AB ac
となってしまう。
Unix由来のsortなら大文字と小文字を区別するので、期待したようにソートされる。単純に昇順ソートするだけなら、Windowsのsortと同じ書き方でOK。
sort c_cpp_keyword.dic > c_cpp_keyword.dic.new
大文字と小文字を区別せずにソートする場合はオプション-fをつければよい。
思わぬ落とし穴
ところで私はWindows上で作業しているので、Unix由来のsortと言っても、実際にはWindows上に移植したものを使うことになる。
手元にあるのがGnuWin32とUnxUtilsのバイナリなのだけど、どちらもオプション-fを設定しなくても英大文字と小文字が混ざってしまう。
このような場合、他のバイナリを探すか、自分でソート用の使い捨てアプリ/マクロを書くことになる。「面倒だけど手作業でソートする」という選択肢もデータ量次第でアリだけど*1、でもプログラマ的でないので嫌だ。
他のバイナリとしては、MSYSに含まれているものなら結果が思った通りになるようだ。もっともこのことは後日MSYSを入れてあるマシンで試してみて分かったことだ。
使い捨てアプリを書く場合はどうか? Rubyでソート用の使い捨てスクリプトをでっち上げてみたところ、こうなった。
# ruby sort.rb < input > output # ruby sort.rb input > output readlines.sort.each do |line| print line end
たった3行! 完全に「とりあえず動けばOK」レベルだけど、正直ここまで短くなるとは思わなかった。この程度ならワンライナーでも十分かもしれない。
ruby -e "readlines.sort.each {|ln| print ln }" c_cpp_keyword.dic > c_cpp_keyword.dic.new
他の言語ではどうだろうか? 物は試しとばかりに、このRubyスクリプトと似た動作のコンソールアプリをC++で書いてみた。さすがにC言語(標準ライブラリ限定)じゃ書く気になれない。
#include <fstream> #include <iostream> #include <string> #include <cassert> #include <cstdlib> #include <cstring> // 手抜きしてマルチセットで常にソートされた状態にしておく #include <set> typedef std::multiset<std::string> lines_t; // 入力ストリームから行指向でテキストを読み込む static void read_lines(lines_t& lines, std::istream *in) { assert(in != NULL); std::string line; while (!in->eof()) { std::getline(*in, line); if (in->eof() && line.empty()) { break; } lines.insert(lines.end(), line); line.clear(); } } // 出力ストリームに書き出す static void write_lines(lines_t& lines, std::ostream& out) { for (lines_t::const_iterator p = lines.begin(); p != lines.end(); ++p) { out << *p << std::endl; } } // メインルーチン int main(int argc, char **argv) { using namespace std; lines_t lines; try { if (argc <= 1) { read_lines(lines, &cin); write_lines(lines, cout); } else { for (int i = 1; i < argc; ++i) { ifstream in(argv[i]); if ((in.rdstate() & ifstream::failbit) != 0) { cerr << argv[i] << ": open failed" << endl; continue; } read_lines(lines, &in); in.close(); } write_lines(lines, cout); } } catch (...) { const char *pn = strrchr(argv[0], '\\'); pn = (pn != NULL) ? pn+1 : argv[0]; cerr << pn << ": error occured" << endl; return EXIT_FAILURE; } return EXIT_SUCCESS; }
60〜70行弱といったところか。正直C++はよく分からないので、不要なコードが含まれているかもしれない。
効率を考えたら、データを一旦ベクタかリストに読み込み、最後に1度だけソートする方がよいかもしれない。データ量が無茶苦茶大きい場合は外部記憶を使ってマージソートするとか、そういった工夫が必要になるかもしれない。
でもPC上で高々500件程度のテキストデータのソート、それも時々実行する処理なんて、今では効率も何も意味がない話だ。*2