0. はじめに
下記の記事の執筆過程において,Windows 付属の SORT
コマンドに重複行を削除する /UNIQUE
オプションがいつの間にか追加されていることを知った。
今さら森博嗣「笑わない数学者」のビリヤードの問題を解く(13)~総括編
以下はその使用例である。AnotherSolver2
コマンドは自作の計算プログラムであり,問題の解の一つを与えると他の解を求めてくれるというものだ。ただし重複する解を多数含んでおり,これらの重複解を削除するために SORT
コマンドを使用した。
AnotherSolver2 /R 1 2 9 8 14 4 43 7 6 10 5 24 | sort /unique
1 14 10 20 7 6 3 2 17 4 8 41
1 14 3 2 4 7 21 8 25 10 12 26
1 15 5 3 25 2 7 4 6 12 14 39
1 2 12 31 25 4 9 10 7 11 16 5
1 2 14 12 32 19 6 5 4 18 13 7
1 2 14 4 37 7 8 27 5 6 13 9
1 2 9 8 14 4 43 7 6 10 5 24
1 22 14 20 5 13 8 3 4 2 10 31
1 3 12 34 21 2 8 9 5 6 7 25
1 3 23 24 6 22 10 11 18 2 5 8
1 3 8 9 5 19 23 16 13 2 28 6
1 4 16 3 15 10 12 14 17 33 2 6
1 4 19 20 27 3 6 25 7 8 2 11
1 4 20 3 40 10 9 2 15 16 6 7
1 4 7 3 16 2 6 17 20 9 13 35
1 5 12 21 29 11 3 16 4 22 2 7
1 7 13 12 3 11 5 18 4 2 48 9
1 8 10 5 7 21 4 2 11 3 26 35
だがしかし,数値を単なる文字列として比較してしまっており,ソート順序が不自然で美しくない。ということで,自然な順のソートコマンドをささっと作ることにした。
1. 仕様案(お品書き)
- 入力ファイル名
標準入力からリダイレクトされると標準入力から読み込む。入力ファイル名を指定すると指定した入力ファイルから読み込む。リダイレクトと入力ファイル名の指定が重なっている場合はリダイレクトを優先する。いずれも指定されていない場合はヘルプメッセージを表示する。 - 出力ファイル名
ソート結果を指定した出力ファイルに書き込む。出力ファイル名の指定は/O ファイル名
とする。オプション/O
とファイル名の間にはスペースを入れる。これはコマンドラインから入力する際にファイル名補完機能を使えるようにするため。出力ファイル名の指定を省略すると標準出力に出力する。 - 文字コードの指定
デフォルトではシステム規定値(日本語版 Windows だと Shift-JIS)に従うが,オプション/UTF8
の指定により UTF8 に変更できる。 - 安定ソート
自然順ソートもしくは大文字・小文字を区別しない場合,異なる文字列でも比較関数は一致とみなす場合がある。その場合,元の順序を保存するようにしたい。 - 逆順ソート
オプション/R
の指定により,逆順にソートできる。 - 重複行の削除
オプション/UNIQ
の指定により,重複行を削除できる。
2. ソート関数
手早く作りたいので Win32 API の StrCmpLogicalW
を用いる。
- 文字列内の数字はテキストではなく数値とみなす。
- 大文字と小文字は区別しない。
自然順ソートと呼んでいるが,Windows エクスプローラーのファイル名のソート順でもあるため,筆者はエクスプローラー互換ソートとも呼んでいる。
2string
3string
20string
st2ring
st3ring
st20ring
string2
string3
string20
マイクロソフトによれば今後仕様は変わる可能性があり,正規の並べ替え用途には使用すべきではないとのこと。
3. 実装方法
3.1 入力ファイルの読み込み
標準入力からのリダイレクトの場合もファイルからの読み込みの場合も同様に StreamReader
としてオープンし,一行ずつ読み込んでリスト list
に追加する。
var list = new List<string>();
while( !reader.EndOfStream ) {
string s = reader.ReadLine();
list.Add( s );
}
3.2 インデクス配列を作る
読み込んだリストを直接ソートするのではなく,行番号に相当するインデクス配列をソートすることにする。※安定ソートにするため
var index = new int[list.Count];
for( int i = 0; i < index.Length; i++ )
index[i] = i;
3.3 ソート本体
Win32 API の StrCmpLogicalW
のラッパクラスである。
class LogicalStringComparer : IComparer<string> {
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
extern static int StrCmpLogicalW( string s1, string s2 );
public int Compare( string s1, string s2 ) {
return StrCmpLogicalW( s1, s2 );
}
}
文字列が等しい(と判断される)ときはインデクスの値で比較を行うため,安定ソートになる。※インデクスの値が一致することはない。
var logical = new LogicalStringComparer();
Comparison<int> compare = delegate( int a, int b ) {
int c = logical.Compare( list[a], list[b] );
if( c != 0 ) return( c );
else return( a - b );
};
Array.Sort( index, compare );
逆順の場合はインデクス配列を入れ替える。
if( reverse_mode ) Array.Reverse( index );
3.4 ソート結果の出力
ソートした結果をそのまま出力する場合と重複行を削除する場合の2通りに対応する。
if( unique_mode ) {
string s0 = null;
for( int i = 0; i < index.Length; i++ ) {
string s1 = list[index[i]];
if( logical.Compare( s0, s1 ) != 0 ) writer.WriteLine( s1 );
s0 = s1;
}
} else {
for( int i = 0; i < index.Length; i++ ) {
string s1 = list[index[i]];
writer.WriteLine( s1 );
}
}
4. 実装コード
実装コードを以下に示す。エクスプローラー互換ソート(Explorer style SORT)なので ESORT
と名付けた。
ESORT.CS の実装コードはコチラ
//------------------------------------------------------------------------------
// テキストファイルを自然順に(エクスプローラ互換で)ソートします。
//------------------------------------------------------------------------------
using System;
using System.IO;
using System.Text;
using System.Reflection;
using System.Collections.Generic;
using System.Runtime.InteropServices;
//------------------------------------------------------------------------------
// 論理ソートクラス
//------------------------------------------------------------------------------
class LogicalStringComparer : IComparer<string> {
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
extern static int StrCmpLogicalW( string s1, string s2 );
public int Compare( string s1, string s2 ) {
return StrCmpLogicalW( s1, s2 );
}
}
//------------------------------------------------------------------------------
// 自然順ソートクラス
//------------------------------------------------------------------------------
class ESORT {
//--------------------------------------------------------------------------
// グローバル変数
//--------------------------------------------------------------------------
static string input_filename = ""; // 入力ファイル名
static string output_filename = ""; // 出力ファイル名
static Encoding encoding = Encoding.Default; // 文字コード
static bool reverse_mode = false; // 逆順に表示する
static bool unique_mode = false; // 重複行を削除する
static bool overwrite_mode = false; // 出力ファイルへの上書を許可
//--------------------------------------------------------------------------
// ヘルプメッセージの表示
//--------------------------------------------------------------------------
static int Usage() {
Console.Error.WriteLine( "テキストファイルを自然順に(エクスプローラ互換で)ソートします。" );
Console.Error.WriteLine( "" );
Console.Error.WriteLine( "ESORT(.EXE) (オプション) [入力ファイル名]" );
Console.Error.WriteLine( "" );
Console.Error.WriteLine( " [入力ファイル名] 入力ファイル名を指定します。" );
Console.Error.WriteLine( " 省略すると標準入力から入力します。" );
Console.Error.WriteLine( "" );
Console.Error.WriteLine( "/O [出力ファイル名] 出力ファイル名を指定します。" );
Console.Error.WriteLine( " 省略すると標準出力に出力します。" );
Console.Error.WriteLine( "" );
Console.Error.WriteLine( " /R 逆順で表示します。" );
Console.Error.WriteLine( " /Y 出力ファイルへの上書きを許可します。" );
Console.Error.WriteLine( "/UNIQ 重複する行を削除します。" );
Console.Error.WriteLine( "/UTF8 文字コードを UTF8 に変更します。" );
return( -1 );
}
//--------------------------------------------------------------------------
// オプション解析
//--------------------------------------------------------------------------
static int GetOption( string[] args ) {
for( int i = 0; i < args.Length; i++ ) {
string s = args[i];
if( s[0] == '/' || s[0] == '-' ) {
string t = s.Substring( 1 );
if( 0 == string.Compare( t, "O", true ) ) {
if( output_filename != "" ) {
Console.Error.WriteLine( "出力ファイル名は既に指定されています!!" );
return( -1 );
} else if( i >= args.Length ) {
Console.Error.WriteLine( "出力ファイル名の指定がありません!!" );
return( -1 );
} else {
output_filename = args[++i];
}
} else if( 0 == string.Compare( t, "R", true ) ) { reverse_mode = true;
} else if( 0 == string.Compare( t, "Y", true ) ) { overwrite_mode = true;
} else if( 0 == string.Compare( t, "UTF8", true ) ) { encoding = Encoding.UTF8;
} else if( 0 == string.Compare( t, "UNIQ", true ) ) { unique_mode = true;
} else {
Console.Error.WriteLine( "オプション {0} には対応していません!!", s );
return( -1 );
}
} else if( input_filename != "" ) {
Console.Error.WriteLine( "入力ファイル名は既に指定されています!!" );
return( -1 );
} else {
input_filename = s;
}
}
return( 0 );
}
//--------------------------------------------------------------------------
// メイン関数
//--------------------------------------------------------------------------
static int Main( string[] args ) {
//----------------------------------------------------------------------
// ヘルプメッセージ
//----------------------------------------------------------------------
bool redirect = Console.IsInputRedirected;
if( !redirect && args.Length == 0 ) return Usage();
//----------------------------------------------------------------------
// オプション解析
//----------------------------------------------------------------------
if( GetOption( args ) != 0 ) return( -1 );
//----------------------------------------------------------------------
// 入力ファイルのオープン
//----------------------------------------------------------------------
StreamReader reader;
if( redirect ) {
reader = new StreamReader( Console.OpenStandardInput(), encoding );
} else if( input_filename == "" ) {
Console.Error.WriteLine( "入力ファイルの指定がありません!!" );
return( -1 );
} else if( !File.Exists( input_filename ) ) {
Console.Error.WriteLine( "入力ファイル {0} は存在しません!!", input_filename );
return( -1 );
} else {
reader = new StreamReader( input_filename, encoding );
}
//----------------------------------------------------------------------
// 入力ファイルの読み込み
//----------------------------------------------------------------------
var list = new List<string>();
while( !reader.EndOfStream ) {
string s = reader.ReadLine();
list.Add( s );
}
//----------------------------------------------------------------------
// 入力ファイルのクローズ
//----------------------------------------------------------------------
reader.Close();
//----------------------------------------------------------------------
// テキストのソート
//----------------------------------------------------------------------
var logical = new LogicalStringComparer();
var index = new int[list.Count];
for( int i = 0; i < index.Length; i++ )
index[i] = i;
Comparison<int> compare = delegate( int a, int b ) {
int c = logical.Compare( list[a], list[b] );
if( c != 0 ) return( c );
else return( a - b );
};
Array.Sort( index, compare );
if( reverse_mode ) Array.Reverse( index );
//----------------------------------------------------------------------
// 出力ファイルのオープン
//----------------------------------------------------------------------
StreamWriter writer;
if( output_filename == "" ) {
writer = new StreamWriter( Console.OpenStandardOutput(), encoding );
} else if( File.Exists( output_filename ) && !overwrite_mode ) {
Console.Error.WriteLine( "出力ファイル {0} は存在します!!", output_filename );
return( -1 );
} else {
writer = new StreamWriter( output_filename, false, encoding );
}
//----------------------------------------------------------------------
// 出力ファイルへの書き込み
//----------------------------------------------------------------------
if( unique_mode ) {
string s0 = null;
for( int i = 0; i < index.Length; i++ ) {
string s1 = list[index[i]];
if( logical.Compare( s0, s1 ) != 0 ) writer.WriteLine( s1 );
s0 = s1;
}
} else {
for( int i = 0; i < index.Length; i++ ) {
string s1 = list[index[i]];
writer.WriteLine( s1 );
}
}
//----------------------------------------------------------------------
// 出力ファイルのクローズ
//----------------------------------------------------------------------
writer.Close();
return( 0 );
}
}
5. 実行結果
引数なしで実行するとヘルプメッセージを表示する。
esort
テキストファイルを自然順に(エクスプローラ互換で)ソートします。
ESORT(.EXE) (オプション) [入力ファイル名]
[入力ファイル名] 入力ファイル名を指定します。
省略すると標準入力から入力します。
/O [出力ファイル名] 出力ファイル名を指定します。
省略すると標準出力に出力します。
/R 逆順で表示します。
/Y 出力ファイルへの上書きを許可します。
/UNIQ 重複する行を削除します。
/UTF8 文字コードを UTF8 に変更します。
自然順(エクスプローラー互換)でソートする。
AnotherSolver2 /R 1 2 9 8 14 4 43 7 6 10 5 24 | esort /uniq
1 2 9 8 14 4 43 7 6 10 5 24
1 2 12 31 25 4 9 10 7 11 16 5
1 2 14 4 37 7 8 27 5 6 13 9
1 2 14 12 32 19 6 5 4 18 13 7
1 3 8 9 5 19 23 16 13 2 28 6
1 3 12 34 21 2 8 9 5 6 7 25
1 3 23 24 6 22 10 11 18 2 5 8
1 4 7 3 16 2 6 17 20 9 13 35
1 4 16 3 15 10 12 14 17 33 2 6
1 4 19 20 27 3 6 25 7 8 2 11
1 4 20 3 40 10 9 2 15 16 6 7
1 5 12 21 29 11 3 16 4 22 2 7
1 7 13 12 3 11 5 18 4 2 48 9
1 8 10 5 7 21 4 2 11 3 26 35
1 14 3 2 4 7 21 8 25 10 12 26
1 14 10 20 7 6 3 2 17 4 8 41
1 15 5 3 25 2 7 4 6 12 14 39
1 22 14 20 5 13 8 3 4 2 10 31