C++ でテキスト中の和暦・西暦年号を統一する

(1/1)
>C++でテキスト中の和暦・西暦年号を統一
日本語テキストに混在する和暦・西暦を統一し、「2020年(令和2年)11月14日」のように西暦・和暦混在のテキストに変換したり、西暦だけ、和暦だけに変換するアプリケーションである。年月日は半角・全角・漢数字から選ぶこともできる。変換したテキストは、クリップボードにコピーしたり、テキストファイルに保存することができる。
また、コマンドライン版アプリケーションを同梱しており、バッチや他のプログラムから流し込んだテキストを変換することができる。

PHPでテキスト中の和暦・西暦年号を統一する(その2)」で作ったPHPプログラムをC++に移植したものである。

(2024年4月20日)使用ライブラリ更新
(2023年12月17日)使用ライブラリ更新
(2023年7月23日)使用ライブラリ更新

目次

サンプル・プログラム

圧縮ファイルの内容
nengowin.msiインストーラ
bin/nengowin.exe実行プログラム本体(GUI版)
bin/nmtxt.exe実行プログラム本体(CUI版)
bin/option.txt変換オプションを保存するファイル
bin/etc/era.txt西暦⇔元号変換表
bin/etc/help.chmヘルプ・ファイル
sour/nengowin.cppソース・プログラム
sour/resource.hリソース・ヘッダ
sour/resource.rcリソース・ファイル(GUI版)
sour/resource2.rcリソース・ファイル(CUI版)
sour/application.icoアプリケーション・アイコン(GUI版)
sour/application2.icoアプリケーション・アイコン(CUI版)
sour/pahooNormalizeText.cppテキスト正規化クラス(ソース)
sour/pahooNormalizeText.hppテキスト正規化クラス(ヘッダ)
sour/makefileGUI版ビルド
sour/makefile_cmdCUI版ビルド
nengowin.cpp 更新履歴
バージョン 更新日 内容
1.2.4 2024/04/20 使用ライブラリ更新
1.2.3 2023/12/17 使用ライブラリ更新
1.2.2 2023/07/23 使用ライブラリ更新
1.2.1 2023/04/08 使用ライブラリ更新
1.2.0 2022/10/15 ウィンドウ位置を保存,設定クリア機能を追加
pahooNormalizeText.cpp 更新履歴
バージョン 更新日 内容
1.9.1 2023/12/17 pahooNormalizeText() - MECAB非使用時の対策
1.9.0 2023/10/25 normalizeText() - 二重引用符の開閉処理
1.8.0 2023/10/25 pahooNormalizeText() - MeCab動作チェック追加
1.7.2 2023/07/30 bignum2scale() - wsourの中に400万と4億が混在する場合にも対応
1.7.1 2023/04/22 toHankaku() - 「一九」対応

使用ライブラリ

CUI版でコマンダイン・オプションを操作する場合などに、オープンソースのライブラリ Boost C++ライブラリが必要になる。Boost C++ライブラリの導入方法等については、「C++ 開発環境の準備」をご覧いただきたい。

リソースの準備

Eclipse を起動し、新規プロジェクト nengowin を用意する。
ResEdit を起動し、resource.rc を用意する。

Eclipse に戻り、ソース・プログラム "nengowin.cpp" を追加する。
リンカー・フラグを -mwindows -static-libstdc++ -static-libgcc -lpthread -lwinpthread -static "(任意のパス)\libboost_program_options-mt.dll" -static "(任意のパス)\libmecab-2.dll" に設定する。
また、CUI版をビルドするために、構成 CMD を追加し、リンカー・フラグを -static-libstdc++ -static-libgcc -lpthread -static "(任意のパス)\libboost_program_options-mt.dll" -static "(任意のパス)\libmecab-2.dll"" に設定する。

MSYS2 コマンドラインからビルドするのであれば、"makefile" と "makefile_cmd" を利用してほしい。

プログラムの流れ

>C++でテキスト中の和暦・西暦年号を統一
プログラムの流れは上図の通りである。
変換ボタンがクリックされたら、後述する変換オプションの値に応じ、seirekiwarekimixture を呼び出す。
各々のメソッドは、内部で正規表現によるパターンマッチを行い、年号に合致する部分文字列を順次変換してゆく。

解説:テキスト正規化クラス

  55: //和暦・西暦年号変換オプション
  56: #define OPTION_MODE_AD      'A'     //西暦に統一
  57: #define OPTION_MODE_ERA     'W'     //和暦に統一
  58: #define OPTION_MODE_ADERA   'C'     //西暦(和暦)
  59: #define OPTION_SCOPE_1      '1'     //奈良時代以降
  60: #define OPTION_SCOPE_2      '2'     //明治以降
  61: #define OPTION_SCOPE_0      '0'     //変換しない
  62: #define OPTION_AD_HAN       'h'     //数字変換(西暦):半角数字
  63: #define OPTION_AD_ZEN       'z'     //  同上   :全角数字
  64: #define OPTION_AD_KAN1      'k'     //  同上   :漢数字(単純)
  65: #define OPTION_AD_KAN2      'l'     //  同上   :漢数字
  66: #define OPTION_ERA_HAN      'H'     //数字変換(和暦):半角数字
  67: #define OPTION_ERA_ZEN      'Z'     //  同上   :全角数字
  68: #define OPTION_ERA_KAN1     'K'     //  同上   :漢数字(単純)
  69: #define OPTION_ERA_KAN2     'L'     //  同上   :漢数字
  70: 
  71: //文字列置換定義ファイル
  72: #define FRONT_REPLACE_CSV   ".\\etc\\frontrep.csv"
  73: 
  74: //年号変換オプションの初期値
  75: #define CONVOPT_INIT        "C1hH"

変換オプションは、英文字1文字で識別するようにしている。これを同時に複数指定することができる。ただし、aとAのように排他的であるオプションは、同時に指定しても意味をなさない。

解説:西暦⇔元号変換

1257: /**
1258:  * 西暦⇔元号変換
1259:  * @param   wstring sour   オリジナル・テキスト
1260:  * @param   char*   option 変換オプション
1261:  * @return  wstring 変換後テキスト
1262: */
1263: wstring pahooNormalizeText::convNengo(wstring wsour, const char* option) {
1264:     wstring wdest = L"";
1265: 
1266:     //変換範囲
1267:     if (strchr(option, OPTION_SCOPE_1!NULL) {
1268:         setScope(START_SCOPE1, END_SCOPE);
1269:     } else if (strchr(option, OPTION_SCOPE_2!NULL) {
1270:         setScope(START_SCOPE2, END_SCOPE);
1271:     } else if (strchr(option, OPTION_SCOPE_0!NULL) {
1272:         setScope(END_SCOPE, END_SCOPE);
1273:     }
1274: 
1275:     //西暦に統一
1276:     if (strchr(option, OPTION_MODE_AD!NULL) {
1277:         wdest = this->seireki(wsour, option, TRUE);
1278:     //和暦に統一
1279:     } else if (strchr(option, OPTION_MODE_ERA!NULL) {
1280:         wdest = this->wareki(wsour, option, TRUE);
1281:     //西暦(和暦)
1282:     } else if (strchr(option, OPTION_MODE_ADERA!NULL) {
1283:         wdest = this->mixture(wsour, option, TRUE);
1284:     }
1285:     //月日
1286:     wdest = this->monthday(wdest, option);
1287: 
1288:     return wdest;
1289: }

変換の入口は convNengo メソッドである。

解説:西暦に統一

1119: /**
1120:  * 西暦に統一
1121:  * @param   wstring sour   オリジナル・テキスト
1122:  * @param   char*   option 変換オプション
1123:  * @param   bool    flag   TRUE=エスケープ文字を消去/FALSE=消去しない
1124:  * @return  wstring 変換後テキスト
1125: */
1126: wstring pahooNormalizeText::seireki(wstring wsour, const char* option=NULL, bool flag=TRUE) {
1127:     wregex re(_SW("([^0-90-9〇一二三四五六七八九十\百千万あ-ん、-?!-¥]{0,4})\\s*([0-90-9〇元一二三四五六七八九十\百千万]+)年\\s*([0-90-9〇一二三四五六七八九十\]*)月?([0-90-9〇一二三四五六七八九十\]*)日?"));
1128:     wregex re2(ESCYEAR);
1129:     wsmatch mt;
1130: 
1131:     wstring wdest = L"";
1132:     wstring suffix = L"";
1133:     for (bool ismatch = regex_search(wsour, mt, re); ismatch !FALSE;
1134:             ismatch = regex_search(mt[0].second, mt.suffix().second, mt, re)) {
1135:         if (flag) {
1136:             wdest += (wstring)mt.prefix() + 
1137:                 regex_replace(this->wareki2seireki(mt, option), re2, L"");
1138:         } else {
1139:             wdest += (wstring)mt.prefix() + this->wareki2seireki(mt, option);
1140:         }
1141:         suffix = (wstring)mt.suffix();      //未変換の部分文字列
1142:     }
1143:     wdest +suffix;
1144: 
1145:     return wdest;
1146: }

1092: /**
1093:  * 和暦→西暦変換(漢数字対応)
1094:  * @param   wsmatch mt 年月日パターンにマッチしたテキスト
1095:  * @return  wstring 西暦
1096: */
1097: wstring pahooNormalizeText::wareki2seireki(wsmatch mt, const char* option=NULL) {
1098:     wstring year, month, day;
1099: 
1100:     if (mt[2].str() == _SW("元")) {
1101:         year = L"1";
1102:     } else if (mt[2].str() !L"") {
1103:         year = this->kan2num(mt[2].str(), 0);
1104:     }
1105:     if (mt[3].str() !L"") {
1106:         month = this->kan2num(mt[3].str(), 0);
1107:     } else {
1108:         month = _SW("0");
1109:     }
1110:     if (mt[4].str() !L"") {
1111:         day = this->kan2num(mt[4].str(), 0);
1112:     } else {
1113:         day = _SW("0");
1114:     }
1115: 
1116:     return this->era2ad(mt[1].str(), stoi(year), stoi(month), stoi(day), option);
1117: }

西暦への統一は、seirekiwareki2seireki の2つのメソッドで処理する。

解説:和暦に統一

1063: /**
1064:  * 和暦に統一
1065:  * @param   wstring sour オリジナル・テキスト
1066:  * @param   char*   option 変換オプション
1067:  * @param   bool    flag   TRUE=エスケープ文字を消去/FALSE=消去しない
1068:  * @return  wstring 変換後テキスト
1069: */
1070: wstring pahooNormalizeText::wareki(wstring wsour, const char* option=NULL, bool flag=TRUE) {
1071:     wregex re(_SW("([^0-90-9〇一二三四五六七八九十\百千万あ-ん、-?!-¥]{0,4})\\s*([0-90-9元〇一二三四五六七八九十\百千万]+)年\\s*([0-90-9〇一二三四五六七八九十\]*)月?([0-90-9〇一二三四五六七八九十\]*)日?"));
1072:     wregex re2(ESCYEAR);
1073:     wsmatch mt;
1074: 
1075:     wstring wdest = L"";
1076:     wstring suffix = L"";
1077:     for (bool ismatch = regex_search(wsour, mt, re); ismatch !FALSE;
1078:             ismatch = regex_search(mt[0].second, mt.suffix().second, mt, re)) {
1079:         if (flag) {
1080:             wdest += (wstring)mt.prefix() + 
1081:                 regex_replace(this->seireki2wareki(mt, option), re2, L"");
1082:         } else {
1083:             wdest += (wstring)mt.prefix() + this->seireki2wareki(mt, option);
1084:         }
1085:         suffix = (wstring)mt.suffix();      //未変換の部分文字列
1086:     }
1087:     wdest +suffix;
1088: 
1089:     return wdest;
1090: }

1032: /**
1033:  * 西暦→和暦変換(漢数字対応)
1034:  * @param   wsmatch mt 年月日パターンにマッチしたテキスト
1035:  * @return  wstring 和暦
1036: */
1037: wstring pahooNormalizeText::seireki2wareki(wsmatch mt, const char* option=NULL) {
1038:     wstring year, month, day;
1039: 
1040:     if (mt[2].str() !L"") {
1041:         if (mt[2].str() == _SW("元")) {
1042:             year = L"1";
1043:         } else {
1044:             year = this->kan2num(mt[2].str(), 0);
1045:         }
1046:     } else {
1047:         year = _SW("0");
1048:     }
1049:     if (mt[3].str() !L"") {
1050:         month = this->kan2num(mt[3].str(), 0);
1051:     } else {
1052:         month = _SW("0");
1053:     }
1054:     if (mt[4].str() !L"") {
1055:         day = this->kan2num(mt[4].str(), 0);
1056:     } else {
1057:         day = _SW("0");
1058:     }
1059: 
1060:     return this->ad2era(mt[1].str(), stoi(year), stoi(month), stoi(day), option);
1061: }

西暦への統一は、warekiseireki2wareki の2つのメソッドで処理する。

解説:西暦(和暦)混合変換

1179: /**
1180:  * 西暦(和暦)混合変換
1181:  * @param   wstring sour   オリジナル・テキスト
1182:  * @param   char*   option 変換オプション
1183:  * @param   bool    flag   TRUE=エスケープ文字を消去/FALSE=消去しない
1184:  * @return  wstring 変換後テキスト
1185: */
1186: wstring pahooNormalizeText::mixture(wstring wsour, const char* option=NULL, bool flag=TRUE) {
1187:     wregex re(_SW("([^0-90-9〇一二三四五六七八九十\百千万あ-ん、-?!-¥]{0,4})([0-9元]+)年"));
1188:     wregex re2(ESCYEAR);
1189:     wsmatch mt;
1190:     static const char opt[] = { OPTION_ERA_HAN, 0x00 };
1191: 
1192:     wstring wstr = this->wareki(wsour, opt, FALSE);     //和暦(半角数字)に統一
1193: 
1194:     wstring wdest  = L"";
1195:     wstring suffix = L"";
1196:     for (bool ismatch = regex_search(wstr, mt, re); ismatch !FALSE;
1197:             ismatch = regex_search(mt[0].second, mt.suffix().second, mt, re)) {
1198:         if (flag) {
1199:             wdest += (wstring)mt.prefix() +
1200:                 regex_replace(this->seireki2mix(mt, option), re2, L"");
1201:         } else {
1202:             wdest += (wstring)mt.prefix() + this->seireki2mix(mt, option);
1203:         }
1204:         suffix = (wstring)mt.suffix();      //未変換の部分文字列
1205:     }
1206:     wdest +suffix;
1207: 
1208:     return wdest;
1209: }

1148: /**
1149:  * 和暦→西暦(和暦)変換
1150:  * @param   wsmatch mt     年月日パターンにマッチしたテキスト
1151:  * @param   char*   option 変換オプション
1152:  * @return  wstring 西暦
1153: */
1154: wstring pahooNormalizeText::seireki2mix(wsmatch mt, const char* option=NULL) {
1155:     wstring wdest = L"";
1156:     wstring wstr  = L"";
1157:     wstring ad = this->wareki2seireki(mt, option);
1158: 
1159:     if (strchr(option, OPTION_ERA_ZEN!NULL) {
1160:         wstr = this->han2zen(mt[2].str(), _decimal);
1161:     } else if (strchr(option, OPTION_ERA_KAN1!NULL) {
1162:         wstr = this->num2kanSimple(mt[2].str());
1163:     } else if (strchr(option, OPTION_ERA_KAN2!NULL) {
1164:         wstr = this->num2kan(mt[2].str());
1165:     } else {
1166:         wstr = mt[2].str();
1167:     }
1168:     wstring wareki = mt[1].str() + wstr + _SW("年");
1169: 
1170:     if (ad !wareki) {
1171:         wdest = ad + _SW("("+ wareki + _SW(")");
1172:     } else {
1173:         wdest = ad;
1174:     }
1175: 
1176:     return wdest;
1177: }

西暦(和暦)混合変換は、mixture に入り、まず、wareki メソッドを使って和暦に統一する。続いて、seireki2mix を使って西暦(和暦)に変換する。

解説:西暦⇔元号変換表

   1: //西暦⇔元号変換表
   2: //
   3: //書式 'yyyymmdd' => '元号'  yyyymmddは当該元号の開始日(西暦年月日)
   4: 
   5: //飛鳥時代
   6: '06450717' => '大化',
   7: '06500322' => '白雉',
   8: '06541124' => '',           //空白期間
   9: '06860814' => '朱鳥',
  10: '06861001' => '',           //空白期間

定数 TABLEERA_FNAME で示されるファイルが西暦⇔元号変換表である。テキスト・ファイル(シフトJIS)で、元号と、当該元号の開始日(西暦年月日)を列挙している。
変換表はアプリケーション起動時に readTableEra メソッドを使って読み込む。ファイルの内容を書き換えれば、再コンパイルすることなく変換ルールを変更できる。

同梱する変換表では、南北朝時代は南朝の元号を採用している。参考として、北朝の元号をコメントアウトしてあり、南朝と入れ換えることができる。

本アプリケーションでは旧暦/新暦の変換計算を行っていない。このため、月日は西暦という前提である。「明治5年12月2日」は旧暦なので、「1872年12月31日」と変換すべきだが、「1872年12月2日」に変換する。
そこで、変換条件として「新暦以降」(1873年1月1日以降)を指定できるようにしている。。「新暦以降」を指定して西暦変換すると、明治5年12月2日は変換しないが、明治6年1月1日は1873年1月1日に変換する。
新暦から旧暦への変換については、「旧暦の計算 - PHPで3ヶ月カレンダーを作る」を参考にしていただきたい。

解説:定数など

  37: // 定数など ==================================================================
  38: #define MAKER       "pahoo.org"             //作成者
  39: #define APPNAME     "nengowin"              //アプリケーション名
  40: #define APPNAMEJP   "和暦・西暦年号変換"    //アプリケーション名(日本語)
  41: #define APPVERSION  "1.2.4"                 //バージョン
  42: #define APPYEAR     "2020-2024"             //作成年
  43: #define REFERENCE   "https://www.pahoo.org/e-soul/webtech/cpp01/cpp01-19-01.shtm"   //参考サイト
  44: 
  45: //ヘルプ・ファイル
  46: #define HELPFILE    ".\\etc\\help.chm"
  47: 
  48: //デフォルト保存ファイル名
  49: #define SAVEFILE    "nengowin.txt"
  50: 
  51: //西暦⇔元号変換ファイル名
  52: #define TABLEERA_FNAME  "etc/era.txt"
  53: 
  54: //オプション保存ファイル名:変更不可
  55: #define OPTIONFILE  "option.txt"
  56: 
  57: //現在のインターフェイス
  58: HINSTANCE hInst;
  59: 
  60: //アプリケーション・ウィンドウ
  61: HWND hParent;
  62: 
  63: //変換プション
  64: string Option;
  65: 
  66: //変換オプションの初期値
  67: #define NOPTION_INIT    "C1hH"
  68: 
  69: //アプリケーション・ウィンドウ位置
  70: unsigned hParent_X, hParent_Y;
  71: 
  72: //エラー・メッセージ格納用【変更不可】
  73: string ErrorMessage;
  74: 
  75: //pahooNormalizeTextオブジェクト
  76: pahooNormalizeText* pNT;
  77: 
  78: //char*バッファサイズ
  79: #define SIZE_BUFF   512
  80: 
  81: //変換元テキスト(初期値)
  82: #define DEF_SOUR    "慶応四年一月三日、鳥羽・伏見の戦いが起きる。10月23日、明治天皇が即位。改元は1868年1月25日に遡って適用された。明治2年5月17日、五稜郭が開放され、戊辰戦争は終わる。"

GUIおよびCUI版のプログラムは、"nengowin.cpp" に記述しており、マクロ定数 CMDAPP が定義されている場合にはCUI版が、そうでない場合にはGUI版がビルドできるようにしてある。
とくに注意記載が無い限り、定数は自由に変更できる。

解説:変換オプション取得・設定

 467: /**
 468:  * 変換オプションを設定する
 469:  * @param   HWND hDlg   ウィンドウ・ハンドラ
 470:  * @param   string opt  変換オプション
 471:  * @return  なし
 472:  */
 473: void setOption(HWND hDlg, string opt) {
 474:     for (int i = 0i < (int)opt.length(); i++) {
 475:         char* ch = (char*)opt.substr(i, 1).c_str();
 476:         switch (*ch) {
 477:         //変換方式
 478:         case OPTION_MODE_AD:
 479:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_MODE_AD),
 480:                             BM_SETCHECK, BST_CHECKED,   0);
 481:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_MODE_ERA),
 482:                             BM_SETCHECK, BST_UNCHECKED, 0);
 483:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_MODE_ADERA),
 484:                             BM_SETCHECK, BST_UNCHECKED, 0);
 485:             break;
 486:         case OPTION_MODE_ERA:
 487:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_MODE_AD),
 488:                             BM_SETCHECK, BST_UNCHECKED, 0);
 489:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_MODE_ERA),
 490:                             BM_SETCHECK, BST_CHECKED,   0);
 491:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_MODE_ADERA),
 492:                             BM_SETCHECK, BST_UNCHECKED, 0);
 493:             break;
 494:         case OPTION_MODE_ADERA:
 495:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_MODE_AD),
 496:                             BM_SETCHECK, BST_UNCHECKED, 0);
 497:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_MODE_ERA),
 498:                             BM_SETCHECK, BST_UNCHECKED, 0);
 499:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_MODE_ADERA),
 500:                             BM_SETCHECK, BST_CHECKED,   0);
 501:             break;
 502:         //変換範囲
 503:         case OPTION_SCOPE_1:
 504:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_SCOPE_1),
 505:                             BM_SETCHECK, BST_CHECKED,   0);
 506:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_SCOPE_2),
 507:                             BM_SETCHECK, BST_UNCHECKED, 0);
 508:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_SCOPE_0),
 509:                             BM_SETCHECK, BST_UNCHECKED, 0);
 510:             break;
 511:         case OPTION_SCOPE_2:
 512:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_SCOPE_1),
 513:                             BM_SETCHECK, BST_UNCHECKED, 0);
 514:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_SCOPE_2),
 515:                             BM_SETCHECK, BST_CHECKED,   0);
 516:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_SCOPE_0),
 517:                             BM_SETCHECK, BST_UNCHECKED, 0);
 518:             break;
 519:         case OPTION_SCOPE_0:
 520:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_SCOPE_1),
 521:                             BM_SETCHECK, BST_UNCHECKED, 0);
 522:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_SCOPE_2),
 523:                             BM_SETCHECK, BST_UNCHECKED, 0);
 524:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_SCOPE_0),
 525:                             BM_SETCHECK, BST_CHECKED,   0);
 526:             break;
 527:         //数字変換(西暦)
 528:         case OPTION_AD_HAN:
 529:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_HAN),
 530:                             BM_SETCHECK, BST_CHECKED,   0);
 531:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_ZEN),
 532:                             BM_SETCHECK, BST_UNCHECKED, 0);
 533:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_KAN1),
 534:                             BM_SETCHECK, BST_UNCHECKED, 0);
 535:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_KAN2),
 536:                             BM_SETCHECK, BST_UNCHECKED, 0);
 537:             break;
 538:         case OPTION_AD_ZEN:
 539:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_HAN),
 540:                             BM_SETCHECK, BST_UNCHECKED, 0);
 541:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_ZEN),
 542:                             BM_SETCHECK, BST_CHECKED,   0);
 543:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_KAN1),
 544:                             BM_SETCHECK, BST_UNCHECKED, 0);
 545:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_KAN2),
 546:                             BM_SETCHECK, BST_UNCHECKED, 0);
 547:             break;
 548:         case OPTION_AD_KAN1:
 549:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_HAN),
 550:                             BM_SETCHECK, BST_UNCHECKED, 0);
 551:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_ZEN),
 552:                             BM_SETCHECK, BST_UNCHECKED, 0);
 553:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_KAN1),
 554:                             BM_SETCHECK, BST_CHECKED,   0);
 555:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_KAN2),
 556:                             BM_SETCHECK, BST_UNCHECKED, 0);
 557:             break;
 558:         case OPTION_AD_KAN2:
 559:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_HAN),
 560:                             BM_SETCHECK, BST_UNCHECKED, 0);
 561:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_ZEN),
 562:                             BM_SETCHECK, BST_UNCHECKED, 0);
 563:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_KAN1),
 564:                             BM_SETCHECK, BST_UNCHECKED, 0);
 565:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_KAN2),
 566:                             BM_SETCHECK, BST_CHECKED,   0);
 567:             break;
 568:         //数字変換(和暦)
 569:         case OPTION_ERA_HAN:
 570:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_HAN),
 571:                             BM_SETCHECK, BST_CHECKED,   0);
 572:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_ZEN),
 573:                             BM_SETCHECK, BST_UNCHECKED, 0);
 574:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_KAN1),
 575:                             BM_SETCHECK, BST_UNCHECKED, 0);
 576:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_KAN2),
 577:                             BM_SETCHECK, BST_UNCHECKED, 0);
 578:             break;
 579:         case OPTION_ERA_ZEN:
 580:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_HAN),
 581:                             BM_SETCHECK, BST_UNCHECKED, 0);
 582:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_ZEN),
 583:                             BM_SETCHECK, BST_CHECKED,   0);
 584:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_KAN1),
 585:                             BM_SETCHECK, BST_UNCHECKED, 0);
 586:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_KAN2),
 587:                             BM_SETCHECK, BST_UNCHECKED, 0);
 588:             break;
 589:         case OPTION_ERA_KAN1:
 590:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_HAN),
 591:                             BM_SETCHECK, BST_UNCHECKED, 0);
 592:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_ZEN),
 593:                             BM_SETCHECK, BST_UNCHECKED, 0);
 594:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_KAN1),
 595:                             BM_SETCHECK, BST_CHECKED,   0);
 596:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_KAN2),
 597:                             BM_SETCHECK, BST_UNCHECKED, 0);
 598:             break;
 599:         case OPTION_ERA_KAN2:
 600:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_HAN),
 601:                             BM_SETCHECK, BST_UNCHECKED, 0);
 602:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_ZEN),
 603:                             BM_SETCHECK, BST_UNCHECKED, 0);
 604:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_KAN1),
 605:                             BM_SETCHECK, BST_UNCHECKED, 0);
 606:             SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_KAN2),
 607:                             BM_SETCHECK, BST_CHECKED,   0);
 608:             break;
 609:         default:
 610:             break;
 611:         }
 612:     }
 613: }

 615: /**
 616:  * 変換オプションを取得する
 617:  * @param   HWND hDlg   ウィンドウ・ハンドラ
 618:  * @return  string 変換オプション
 619:  */
 620: string getOption(HWND hDlg) {
 621:     string opt = "";
 622:     //変換方式
 623:     if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_MODE_AD), BM_GETCHECK, 0, 0)) {
 624:         opt +OPTION_MODE_AD;
 625:     } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_MODE_ERA), BM_GETCHECK, 0, 0)) {
 626:         opt +OPTION_MODE_ERA;
 627:     } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_MODE_ADERA), BM_GETCHECK, 0, 0)) {
 628:         opt +OPTION_MODE_ADERA;
 629:     }
 630:     //変換範囲
 631:     if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_SCOPE_1), BM_GETCHECK, 0, 0)) {
 632:         opt +OPTION_SCOPE_1;
 633:     } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_SCOPE_2), BM_GETCHECK, 0, 0)) {
 634:         opt +OPTION_SCOPE_2;
 635:     } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_SCOPE_0), BM_GETCHECK, 0, 0)) {
 636:         opt +OPTION_SCOPE_0;
 637:     }
 638:     //数字変換(西暦)
 639:     if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_HAN), BM_GETCHECK, 0, 0)) {
 640:         opt +OPTION_AD_HAN;
 641:     } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_ZEN), BM_GETCHECK, 0, 0)) {
 642:         opt +OPTION_AD_ZEN;
 643:     } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_KAN1), BM_GETCHECK, 0, 0)) {
 644:         opt +OPTION_AD_KAN1;
 645:     } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_AD_KAN2), BM_GETCHECK, 0, 0)) {
 646:         opt +OPTION_AD_KAN2;
 647:     }
 648:     //数字変換(和暦)
 649:     if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_HAN), BM_GETCHECK, 0, 0)) {
 650:         opt +OPTION_ERA_HAN;
 651:     } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_ZEN), BM_GETCHECK, 0, 0)) {
 652:         opt +OPTION_ERA_ZEN;
 653:     } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_KAN1), BM_GETCHECK, 0, 0)) {
 654:         opt +OPTION_ERA_KAN1;
 655:     } else if (SendMessage(GetDlgItem(hDlg, IDC_RADIO_ERA_KAN2), BM_GETCHECK, 0, 0)) {
 656:         opt +OPTION_ERA_KAN2;
 657:     }
 658: 
 659:     return opt;
 660: }

GUI版は、変換オプションをラジオボタンに設定したり取り出すために、関数 setOptiongetOption の2種類の関数を用意した。

解説:変換オプション読込・保存・削除

 136: /**
 137:  * AppDataのパスを取得
 138:  * @param   char* appname アプリケーション名
 139:  * @return  string パス
 140:  */
 141: string getMyPath(const char* appname) {
 142:     static TCHAR myPath[MAX_PATH] = "";
 143: 
 144:     if (strlen(myPath) == 0) {
 145:         if (SHGetSpecialFolderPath(NULL, myPath, CSIDL_APPDATA, 0)) {
 146:             TCHAR *ptmp = _tcsrchr(myPath, _T('\\'));
 147:             if (ptmp !NULL) {
 148:                 ptmp = _tcsinc(ptmp);
 149:                 *ptmp = _T('\0');
 150:             }
 151:             strcat(myPath, _T("Roaming"));
 152:             CreateDirectory((LPCTSTR)myPath, NULL);
 153:             strcat(myPath, _T("\\pahoo.org"));
 154:             CreateDirectory((LPCTSTR)myPath, NULL);
 155:             strcat(myPath, _T("\\"));
 156:             strcat(myPath, _T(appname));
 157:             CreateDirectory((LPCTSTR)myPath, NULL);
 158:             strcat(myPath, _T("\\"));
 159:         } else {
 160:         }
 161:     }
 162:     return (string)myPath;
 163: }

 176: /**
 177:  * パラメータの読み込み
 178:  * @param   なし
 179:  * @return  なし
 180:  */
 181: void loadParameter(void) {
 182:     ptree pt;
 183: 
 184:     //初期値設定
 185:     initParameter();
 186: 
 187:     //XMLファイル読み込み
 188:     try {
 189:         xml_parser::read_xml(getMyPath(APPNAME+ APPNAME + ".xml", pt);
 190: 
 191:         //XML解釈
 192:         try {
 193:             //形式チェック
 194:             if (optional<string>str = pt.get_optional<string>("parameter")) {
 195:             } else {
 196:                 return;
 197:             }
 198:             //パラメータ読み込み
 199:             for (auto it : pt.get_child("parameter")) {
 200:                 string typeit.second.get_optional<string>("<xmlattr>.type").value();
 201:                 if (type == "option") {
 202:                     Option = (string)it.second.data();
 203:                 } else if (type == "wx") {
 204:                     hParent_X = (unsigned)stoi(it.second.data());
 205:                 } else if (type == "wy") {
 206:                     hParent_Y = (unsigned)stoi(it.second.data());
 207:                 }
 208:             }
 209:         //解釈失敗したら初期値設定
 210:         } catch (xml_parser_error& e) {
 211:             initParameter();
 212:             return;
 213:         }
 214:     //読み込み失敗したら初期値設定
 215:     } catch (xml_parser_error& e) {
 216:         initParameter();
 217:         return;
 218:     }
 219: 
 220:     //アプリケーション・ウィンドウの位置(デスクトップ範囲外なら原点移動)
 221:     HWND hDesktop = GetDesktopWindow();
 222:     WINDOWINFO windowInfo;
 223:     windowInfo.cbSize = sizeof(WINDOWINFO);
 224:     GetWindowInfo(hDesktop, &windowInfo);
 225:     if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
 226:         hParent_X = 0;
 227:     }
 228:     if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
 229:         hParent_Y = 0;
 230:     }
 231: }

 233: /**
 234:  * パラメータの保存
 235:  * @param   なし
 236:  * @return  なし
 237:  */
 238: void saveParameter(void) {
 239: #ifndef CMDAPP
 240:     //アプリケーション・ウィンドウの位置取得
 241:     WINDOWINFO windowInfo;
 242:     windowInfo.cbSize = sizeof(WINDOWINFO);
 243:     GetWindowInfo(hParent, &windowInfo);
 244:     hParent_X = (unsigned)windowInfo.rcWindow.left;
 245:     hParent_Y = (unsigned)windowInfo.rcWindow.top;
 246:     if (hParent_X >= (unsigned)windowInfo.rcWindow.right) {
 247:         hParent_X = 0;
 248:     }
 249:     if (hParent_Y >= (unsigned)windowInfo.rcWindow.bottom) {
 250:         hParent_Y = 0;
 251:     }
 252: #endif
 253: 
 254:     //XMLファイルへ書き込む
 255:     ptree pt;
 256:     ptree& child1 = pt.add("parameter.param", Option);
 257:     child1.add("<xmlattr>.type", "option");
 258:     ptree& child2 = pt.add("parameter.param", hParent_X);
 259:     child2.add("<xmlattr>.type", "wx");
 260:     ptree& child3 = pt.add("parameter.param", hParent_Y);
 261:     child3.add("<xmlattr>.type", "wy");
 262: 
 263:     const int indent = 4;
 264:     write_xml(getMyPath(APPNAME+ APPNAME + ".xml", pt, std::locale(),
 265:         xml_writer_make_settings<std::string>(' ', indent));
 266: }

 268: /**
 269:  * パラメータの削除
 270:  * @param   なし
 271:  * @return  なし
 272:  */
 273: void delParameter(void) {
 274:     remove((getMyPath(APPNAME+ APPNAME + ".xml").c_str());
 275: 
 276:     //初期値設定
 277:     initParameter();
 278: }

プログラム起動時に loadParameter 関数によって、変換オプションを読み込む。また、終了時に saveParameter 関数によって、変換オプションを保存する。変数 option の値をテキスト・ファイルとして読み書きしている。
保存場所は、getMyPath 関数によって、ユーザーのAppDataの下にアプリケーションフォルダを作って保存する。
この処理は、GUI/CUI共通だ。

CUI用メインプログラム

 847: /**
 848:  * CUI用メインプログラム
 849:  * @param   int argc
 850:  * @paramm  char* argv[]
 851:  * @return  int リターンコード
 852:  */
 853: int main(int argc, char* argv[]) {
 854:     //パラメータ読み込み
 855:     loadParameter();
 856:     string sour = "";
 857: 
 858:     //pahooNormalizeTextオブジェクト
 859:     pNT = new pahooNormalizeText();
 860:     //西暦⇔元号変換表読み込み
 861:     pNT->readTableEra(TABLEERA_FNAME);
 862: 
 863:     //コマンドライン・オプションの定義
 864:     options_description options("コマンドライン・オプション");
 865:     options.add_options()
 866:         ("option,o", value<std::string>()->default_value(Option), "変換オプション")
 867:         ("sour,s", value<std::string>(), "入力テキスト")
 868:         ("clip,c", "クリップボードから入力")
 869:         ("dest,d", value<std::string>(), "出力テキスト")
 870:         ("paste,p", "クリップボードへ出力")
 871:         ("help,h", "ヘルプ")
 872:         ("version,v", "バージョン情報")
 873:         ;
 874:     //コマンドライン・オプションの取得
 875:     variables_map vm;
 876:     try {
 877:         store(parse_command_line(argc, argv, options), vm);
 878:     } catch(const boost::program_options::error_with_option_name& e) {
 879:         ErrorMessage =  e.what();
 880:         cerr << ErrorMessage << endl;
 881:         return 1;
 882:     }
 883:     notify(vm);
 884: 
 885:     //変換オプション
 886:     auto opt = vm["option"].as<string>();
 887: 
 888:     wstring wdest = L"";
 889:     //ヘルプ情報
 890:     if (vm.count("help")) {
 891:         wdest = _SW(Help);
 892:     //バージョン情報
 893:     } else if (vm.count("version")) {
 894:         wdest = _SW(Version);
 895: 
 896:     //変換実行
 897:     } else {
 898:         //入力ファイル
 899:         wstring wsour = _SW(DEF_SOUR);
 900:         if (vm.count("sour")) {
 901:             auto infile = vm["sour"].as<string>();
 902:             ifstream ifs(infile.c_str());
 903:             if (ifs.fail()) {
 904:                 ErrorMessage = infile + " が見つかりません";
 905:                 cerr << ErrorMessage << endl;
 906:                 return 1;
 907:             }
 908:             string sour = "";
 909:             string ss;
 910:             while (getline(ifs, ss)) {
 911:                 sour +ss + "\r";
 912:             }
 913:             wsour = _SW(sour);
 914:         //クリップボードから
 915:         } else if (vm.count("clip")) {
 916:             char *ss = getClipboardData();
 917:             sour = ss;
 918:             wsour = _SW(sour);
 919:         //標準入力から
 920:         } else {
 921:             string sour;
 922:             cin >> sour;
 923:             if (sour.length() > 0) {
 924:                 wsour = _SW(sour);
 925:             }
 926:         }
 927:         wdest = pNT->convNengo(wsour, opt.c_str());
 928:     }
 929: 
 930:     //出力ファイル
 931:     if (vm.count("dest")) {
 932:         //改行コード置換
 933:         wregex re(_SW("\r"));
 934:         wdest = regex_replace(wdest, re, _SW("\n"));
 935:         auto outfile = vm["dest"].as<string>();
 936:         ofstream ofs(outfile.c_str());
 937:         ofs << _WS(wdest);
 938:         if (ofs.bad()) {
 939:             ErrorMessage = outfile + " への書き込みに失敗しました";
 940:             cerr << ErrorMessage << endl;
 941:             return 1;
 942:         }
 943:         ofs.close();
 944:     //クリップボードへ
 945:     } else if (vm.count("paste")) {
 946:         //改行コード置換
 947:         wregex re(_SW("\r"));
 948:         wdest = regex_replace(wdest, re, _SW("\n"));
 949:         setClipboardData(_WS(wdest));
 950:     //標準出力へ
 951:     } else {
 952:         //改行コード置換
 953:         wregex re(_SW("\r"));
 954:         wdest = regex_replace(wdest, re, _SW("\n"));
 955:         cout << _WS(wdest);
 956:     }
 957: 
 958:     //オブジェクト解放
 959:     delete pNT;
 960: 
 961:     //パラメータ保存
 962:     Option = opt;
 963:     saveParameter();
 964: 
 965:     return 0;
 966: }

CUI版は、昔ながらの main 関数ではじまる。
GUI版の WinMain 関数はマルチスレッド対応で、すべてのスレッドが終わる前にコマンドラインに戻ってきてしまう。また、標準入出力のパイプ処理への対応も面倒であるため、CUI版は実行ファイルを分けることにした。

コマンドラインオプションの解釈は、Boost C++ライブラリoptions を利用した。
その他の関数、ヘルプファイルやインストーラー作成方法については、これまでの連載で説明してきたとおりである。

参考サイト

(この項おわり)
header