株式会社エス・スリー・フォー

Windows-API による文字列比較オブジェクト

やっかいな文字列処理

コンピュータが扱う文字がASCIIだけであった旧き良き時代には、プログラマはとても幸せでした。ふたつの文字列の大小を比較するには標準関数 strcmp を呼び出すだけでよかったのですから。

ところがコンピュータが日本語と言うものを扱わねばならなくなってからというもの、strcmpだけでは済まなくなってしまったのです。

まず、ASCIIだけでなく、JIS,Shift-JIS,EUCなどの様々な文字セットに対して正しく処理しなければなりません。

幸いにもこれら文字セットについては内部的にはUnicode(wchar_t)に変換しておくことで文字セットによる扱いの違いを吸収することができます。

日本語処理の大きな問題のひとつは文字列の比較です。

すなわち、文字列の比較の条件として、

  • 全角/半角を区別する/しない
  • ひらがな/カタカナを区別する/しない
  • 大文字/小文字を区別する/しない

といったオプションが考えられ、単に strcmp のUnicode版wcscmp を使えばいいというわけにはいかないのです。

CompareString

Windowsは上述のような様々な比較オプションに対応した文字列比較関数 CompareString を提供しています。

 int CompareString(
        LCID    Locale,
        DWORD   dwCmpFlags,
        LPCTSTR lpString1,
        int     cchCount1.
        LPCTSTR lpString2,
        int     cchCount2
        );
  • Locale : ロケール
    意味
    LOCALE_SYSTEM_DEFAULT システムのデフォルトロケール
    LOCALE_USER_DEFAULT 現在のユーザーのデフォルトロケール
  • dwCmpFlags : 比較オプション(下記の値の論理和による組み合わせ)
    意味
    NORM_IGNORECASE 大文字/小文字を区別しない
    NORM_IGNOREKANATYPE ひらがな/カタカナを区別しない
    NORM_IGNORENONSPACE 場所をとらない文字をを区別しない
    NORM_IGNORESYMBOLS 記号を無視する
    NORM_IGNOREWIDTH 1バイト文字とそれと同じ2バイト文字を区別しない
    SORT_STRINGSORT 句読点を記号として扱う
  • dwCmpFlags : 比較オプション(下記の値の論理和による組み合わせ)
  • lpString1 : 第1文字列へのポインタ
  • cchString1 : 第1文字列の文字数 -1のときはナル文字まで。
  • lpString2 : 第2文字列へのポインタ
  • cchString2 : 第2文字列の文字数 -1のときはナル文字まで。

CompareStringは比較の結果、次の値を返します(比較失敗時は0)。

意味
CSTR_LESS_THAN 第1文字列 < 第2文字列
CSTR_EQUAL 第1文字列 = 第2文字列
CSTR_GREATER_THAN 第1文字列 > 第2文字列

ソースコード

CompareStringを使えばややこしい文字列の比較が簡単に実現できます。

そのまま呼び出しても構わないのですが、ここではstd::stringや標準C++ライブラリのアルゴリズムやコンテナに適応できるよう、比較ヘルパcompare_helperおよび関数オブジェクトを実装しました。

#include <windows.h>
#include <functional>
#include <stdexcept>

namespace stx {

  enum {
    ignore_case     = NORM_IGNORECASE,
    ignore_kanatype = NORM_IGNOREKANATYPE,
    ignore_nonspace = NORM_IGNORENONSPACE,
    ignore_symbols  = NORM_IGNORESYMBOLS,
    ignore_width    = NORM_IGNOREWIDTH,
    string_sort     = SORT_STRINGSORT,
    default_locale  = LOCALE_SYSTEM_DEFAULT
  };

  class compare_helper {
    DWORD flg_;
    LCID  loc_;

    static int get_result(int n) {
      switch ( n ) {
      case CSTR_LESS_THAN : return -1;
      case CSTR_EQUAL     : return  0;
      case CSTR_GREATER_THAN : return  1;
      }
      throw std::runtime_error("comparison failure");
    }

  public:
    explicit compare_helper(DWORD flags =0, LCID locale=default_locale)
      : flg_(flags), loc_(locale) {}

    int compare(const char* x, int lx, const char* y, int ly) const
      { return get_result(CompareStringA(loc_,flg_,x,lx,y,ly)); }

    int compare(const char* x, const char* y) const
      { return get_result(CompareStringA(loc_,flg_,x,-1,y,-1)); }

    int compare(const wchar_t* x, int lx, const wchar_t* y, int ly) const
      { return get_result(CompareStringW(loc_,flg_,x,lx,y,ly)); }

    int compare(const wchar_t* x, const wchar_t* y) const
      { return get_result(CompareStringW(loc_,flg_,x,-1,y,-1)); }
  };

  // ------ comparison objects -----

  template<class String>
  class string_equal_to : std::binary_function<String,String,bool> {
    DWORD flg_;
    LCID  loc_;
  public:
    explicit string_equal_to(DWORD flags =0, LCID locale =default_locale)
      : flg_(flags), loc_(locale) {}
    bool operator()(const String& x, const String& y) const {
      return compare_helper(flg_,loc_).compare(x.c_str(),x.size(),y.c_str(),y.size()) == 0;
    }
  };

  template<class String>
  class string_not_equal_to : std::binary_function<String,String,bool> {
    DWORD flg_;
    LCID  loc_;
  public:
    explicit string_not_equal_to(DWORD flags =0, LCID locale =default_locale)
      : flg_(flags), loc_(locale) {}
    bool operator()(const String& x, const String& y) const {
      return compare_helper(flg_,loc_).compare(x.c_str(),x.size(),y.c_str(),y.size()) != 0;
    }
  };

  template<class String>
  struct string_less : std::binary_function<String,String,bool> {
    DWORD flg_;
    LCID  loc_;
  public:
    explicit string_less(DWORD flags =0, LCID locale =default_locale)
      : flg_(flags), loc_(locale) {}
    bool operator()(const String& x, const String& y) const {
      return compare_helper(flg_,loc_).compare(x.c_str(),x.size(),y.c_str(),y.size()) < 0;
    }
  };

  template<class String>
  struct string_less_equal : std::binary_function<String,String,bool> {
    DWORD flg_;
    LCID  loc_;
  public:
    explicit string_less_equal(DWORD flags =0, LCID locale =default_locale)
      : flg_(flags), loc_(locale) {}
    bool operator()(const String& x, const String& y) const {
      return compare_helper(flg_,loc_).compare(x.c_str(),x.size(),y.c_str(),y.size()) <= 0;
    }
  };

  template<class String>
  struct string_greater : std::binary_function<String,String,bool> {
    DWORD flg_;
    LCID  loc_;
  public:
    explicit string_greater(DWORD flags =0, LCID locale =default_locale)
      : flg_(flags), loc_(locale) {}
    bool operator()(const String& x, const String& y) const {
      return compare_helper(flg_,loc_).compare(x.c_str(),x.size(),y.c_str(),y.size()) > 0;
    }
  };

  template<class String>
  struct string_greater_equal : std::binary_function<String,String,bool> {
    DWORD flg_;
    LCID  loc_;
  public:
    explicit string_greater_equal(DWORD flags =0, LCID locale =default_locale)
      : flg_(flags), loc_(locale) {}
    bool operator()(const String& x, const String& y) const {
      return compare_helper(flg_,loc_).compare(x.c_str(),x.size(),y.c_str(),y.size()) >= 0;
    }
  };

}

サンプル

では上記のオブジェクトを用いて、簡単な文字列の検索とソートを行なってみましょう。

7つの文字列:

  • "りんご" (ひらがな)
  • "apple" (ASCII 小文字)
  • "apple" (全角 小文字)
  • "リンゴ" (カタカナ)
  • "APPLE", (ASCII 大文字)
  • "APPLE" (全角 大文字)
  • "リンゴ" (半角 カナ)

が格納された vector<string> に対し、様々なオプションで "apple" と "りんご"を検索し、そして昇順にソートします。

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

using namespace std;

const int N = 7;

const char* table[N] = {
  "りんご", 
  "apple", 
  "apple", 
  "リンゴ", 
  "APPLE", 
  "APPLE",
  "リンゴ",
};

void find_and_sort(const string& str, int n) {
  vector<string> container(N);
  copy(table, table+N, container.begin());
  cout << "find '" << str << "' (";
  // オプションフラグを作る
  DWORD f = 0;
  if ( n & 0x01 ) { f |= stx::ignore_case;     cout << "ignore_case ";     }
  if ( n & 0x02 ) { f |= stx::ignore_kanatype; cout << "ignore_kanatype "; }
  if ( n & 0x04 ) { f |= stx::ignore_width;    cout << "ignore_width ";    }
  if ( n & 0x08 ) { f |= stx::ignore_nonspace; cout << "ignore_nonspace "; }
  if ( n & 0x10 ) { f |= stx::ignore_symbols;  cout << "ignore_symbols ";  }
  if ( n & 0x20 ) { f |= stx::string_sort;     cout << "string_sort ";     }    
  cout << ')' << endl << "    /";
  // 一致(x == y)を調べるオブジェクト str_eq でvector内を検索する
  stx::string_equal_to<string> str_eq(f);
  for ( vector<string>::iterator it = container.begin();
        it != container.end(); ++it )
    if ( str_eq(str,*it) ) cout << *it << '/';
  cout << endl << "sort: /";
  // 小さいか一致(x < y)を調べるオブジェクト str_lt でvector内をソートする
  stx::string_less<string> str_lt(f);
  sort(container.begin(), container.end(), str_lt);
  copy(container.begin(), container.end(), ostream_iterator<string>(cout,"/"));
  cout << endl << endl;
}

int main() {
  for ( int i = 0; i < 16; ++i ) {
    find_and_sort("apple",i);
    find_and_sort("りんご",i);
  }
  return 0;
}

実行結果は次のようになりました。

find 'apple' ()
    /apple/
sort: /apple/apple/APPLE/APPLE/リンゴ/リンゴ/りんご/

find 'りんご' ()
    /りんご/
sort: /apple/apple/APPLE/APPLE/リンゴ/リンゴ/りんご/

find 'apple' (ignore_case )
    /apple/APPLE/
sort: /apple/APPLE/apple/APPLE/リンゴ/リンゴ/りんご/

find 'りんご' (ignore_case )
    /りんご/
sort: /apple/APPLE/apple/APPLE/リンゴ/リンゴ/りんご/

find 'apple' (ignore_kanatype )
    /apple/
sort: /apple/apple/APPLE/APPLE/リンゴ/りんご/リンゴ/

find 'りんご' (ignore_kanatype )
    /りんご/リンゴ/
sort: /apple/apple/APPLE/APPLE/リンゴ/りんご/リンゴ/

find 'apple' (ignore_case ignore_kanatype )
    /apple/APPLE/
sort: /apple/APPLE/apple/APPLE/リンゴ/りんご/リンゴ/

find 'りんご' (ignore_case ignore_kanatype )
    /りんご/リンゴ/
sort: /apple/APPLE/apple/APPLE/リンゴ/りんご/リンゴ/

find 'apple' (ignore_width )
    /apple/apple/
sort: /apple/apple/APPLE/APPLE/リンゴ/リンゴ/りんご/

find 'りんご' (ignore_width )
    /りんご/
sort: /apple/apple/APPLE/APPLE/リンゴ/リンゴ/りんご/

find 'apple' (ignore_case ignore_width )
    /apple/apple/APPLE/APPLE/
sort: /apple/apple/APPLE/APPLE/リンゴ/リンゴ/りんご/

find 'りんご' (ignore_case ignore_width )
    /りんご/
sort: /apple/apple/APPLE/APPLE/リンゴ/リンゴ/りんご/

find 'apple' (ignore_kanatype ignore_width )
    /apple/apple/
sort: /apple/apple/APPLE/APPLE/りんご/リンゴ/リンゴ/

find 'りんご' (ignore_kanatype ignore_width )
    /りんご/リンゴ/リンゴ/
sort: /apple/apple/APPLE/APPLE/りんご/リンゴ/リンゴ/

find 'apple' (ignore_case ignore_kanatype ignore_width )
    /apple/apple/APPLE/APPLE/
sort: /apple/apple/APPLE/APPLE/りんご/リンゴ/リンゴ/

find 'りんご' (ignore_case ignore_kanatype ignore_width )
    /りんご/リンゴ/リンゴ/
sort: /apple/apple/APPLE/APPLE/りんご/リンゴ/リンゴ/

find 'apple' (ignore_nonspace )
    /apple/
sort: /apple/apple/APPLE/APPLE/リンゴ/リンゴ/りんご/

find 'りんご' (ignore_nonspace )
    /りんご/
sort: /apple/apple/APPLE/APPLE/リンゴ/リンゴ/りんご/

find 'apple' (ignore_case ignore_nonspace )
    /apple/APPLE/
sort: /apple/APPLE/apple/APPLE/リンゴ/リンゴ/りんご/

find 'りんご' (ignore_case ignore_nonspace )
    /りんご/
sort: /apple/APPLE/apple/APPLE/リンゴ/リンゴ/りんご/

find 'apple' (ignore_kanatype ignore_nonspace )
    /apple/
sort: /apple/apple/APPLE/APPLE/リンゴ/りんご/リンゴ/

find 'りんご' (ignore_kanatype ignore_nonspace )
    /りんご/リンゴ/
sort: /apple/apple/APPLE/APPLE/リンゴ/りんご/リンゴ/

find 'apple' (ignore_case ignore_kanatype ignore_nonspace )
    /apple/APPLE/
sort: /apple/APPLE/apple/APPLE/リンゴ/りんご/リンゴ/

find 'りんご' (ignore_case ignore_kanatype ignore_nonspace )
    /りんご/リンゴ/
sort: /apple/APPLE/apple/APPLE/リンゴ/りんご/リンゴ/

find 'apple' (ignore_width ignore_nonspace )
    /apple/apple/
sort: /apple/apple/APPLE/APPLE/リンゴ/リンゴ/りんご/

find 'りんご' (ignore_width ignore_nonspace )
    /りんご/
sort: /apple/apple/APPLE/APPLE/リンゴ/リンゴ/りんご/

find 'apple' (ignore_case ignore_width ignore_nonspace )
    /apple/apple/APPLE/APPLE/
sort: /apple/apple/APPLE/APPLE/リンゴ/リンゴ/りんご/

find 'りんご' (ignore_case ignore_width ignore_nonspace )
    /りんご/
sort: /apple/apple/APPLE/APPLE/リンゴ/リンゴ/りんご/

find 'apple' (ignore_kanatype ignore_width ignore_nonspace )
    /apple/apple/
sort: /apple/apple/APPLE/APPLE/りんご/リンゴ/リンゴ/

find 'りんご' (ignore_kanatype ignore_width ignore_nonspace )
    /りんご/リンゴ/リンゴ/
sort: /apple/apple/APPLE/APPLE/りんご/リンゴ/リンゴ/

find 'apple' (ignore_case ignore_kanatype ignore_width ignore_nonspace )
    /apple/apple/APPLE/APPLE/
sort: /apple/apple/APPLE/APPLE/りんご/リンゴ/リンゴ/

find 'りんご' (ignore_case ignore_kanatype ignore_width ignore_nonspace )
    /りんご/リンゴ/リンゴ/
sort: /apple/apple/APPLE/APPLE/りんご/リンゴ/リンゴ/