テキストファイルをソートする

テキストファイルをソートしたくなった時、どうすればよいのだろうか?

最近vimC/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上に移植したものを使うことになる。

手元にあるのがGnuWin32UnxUtilsのバイナリなのだけど、どちらもオプション-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

*1:私の場合、英単語レベルのデータなら5〜6件ぐらいまでは手作業でも我慢できる。でも例えば1データが20文字を超えるような場合は2〜3件でも手作業は嫌。

*2:1件のデータ量が無茶苦茶大きいとか、よほど旧式のPCを使っているのでもないならば……。