/** pahooNormalizeText.cpp
 * テキスト正規化クラス：C++ソース
 *
 * @copyright	(c)studio pahoo
 * @author		パパぱふぅ
 * @動作環境	MinGW C++ + Boost C++ Libraries + MeCab
 * @参考URL		https://www.pahoo.org/e-soul/webtech/cpp01-18-01.shtm
 */
#include <iostream>
#include <fstream>
#include <stdio.h>
#include <stdlib.h>
#include <tchar.h>
#include <vector>
#include <string>
#include <locale>
#include <regex>
#include <sstream>
#include <cmath>
#include <winsock2.h>
#include <windows.h>
#include <commctrl.h>
#include <algorithm>
#include <locale>
#ifdef MECAB
	#include <mecab.h>
#endif
#include <boost/algorithm/string.hpp>
#include <boost/foreach.hpp>
#include "pahooNormalizeText.hpp"

using namespace std;

// 文字コード変換 =========================================================
/**
 * テキスト・コード変換：SJIS→wstring（Windows API使用）
 * @param  string src SJISテキスト
 * @return string 変換後テキスト
*/
std::wstring __sjis_wstring(std::string src) {
	auto const dest_size = ::MultiByteToWideChar(CP_ACP, 0U, src.data(), -1, nullptr, 0U);
	std::vector<wchar_t> dest(dest_size, L'\0');

	if (::MultiByteToWideChar(CP_ACP, 0U, src.data(), -1, dest.data(), dest.size()) == 0) {
		throw std::system_error{static_cast<int>(::GetLastError()), std::system_category()};
	}
	dest.resize(std::char_traits<wchar_t>::length(dest.data()));
	dest.shrink_to_fit();

	return std::wstring(dest.begin(), dest.end());
}

/**
 * テキスト・コード変換：wstring→SJIS（Windows API使用）
 * @param  wstring src テキスト
 * @return string 変換後テキスト（SJIS）
*/
std::string __wstring_sjis(std::wstring src) {
	auto const dest_size = ::WideCharToMultiByte(CP_ACP, 0U, src.data(), -1, nullptr, 0, nullptr, nullptr);
	std::vector<char> dest(dest_size, '\0');
	if (::WideCharToMultiByte(CP_ACP, 0U, src.data(), -1, dest.data(), dest.size(), nullptr, nullptr) == 0) {
		throw std::system_error{static_cast<int>(::GetLastError()), std::system_category()};
	}
	dest.resize(std::char_traits<char>::length(dest.data()));
	dest.shrink_to_fit();

	return std::string(dest.begin(), dest.end());
}

/**
 * テキスト・コード変換：UTF-8→wstring（Windows API使用）
 * @param  string src UTF-8テキスト
 * @return string 変換後テキスト
*/
std::wstring __utf8_wstring(std::string src) {
	auto const dest_size = ::MultiByteToWideChar(CP_UTF8, 0U, src.data(), -1, nullptr, 0U);
	std::vector<wchar_t> dest(dest_size, L'\0');
	if (::MultiByteToWideChar(CP_UTF8, 0U, src.data(), -1, dest.data(), dest.size()) == 0) {
		throw std::system_error{static_cast<int>(::GetLastError()), std::system_category()};
	}
	dest.resize(std::char_traits<wchar_t>::length(dest.data()));
	dest.shrink_to_fit();

	return std::wstring(dest.begin(), dest.end());
}

/**
 * テキスト・コード変換：wstring→UTF-8（Windows API使用）
 * @param  wstring src テキスト
 * @return string 変換後テキスト（UTF-8）
*/
std::string __wstring_utf8(std::wstring src) {
	auto const dest_size = ::WideCharToMultiByte(CP_UTF8, 0U, src.data(), -1, nullptr, 0, nullptr, nullptr);
	std::vector<char> dest(dest_size, '\0');

	if (::WideCharToMultiByte(CP_UTF8, 0U, src.data(), -1, dest.data(), dest.size(), nullptr, nullptr) == 0) {
		throw std::system_error{static_cast<int>(::GetLastError()), std::system_category()};
	}
	dest.resize(std::char_traits<char>::length(dest.data()));
	dest.shrink_to_fit();

	return std::string(dest.begin(), dest.end());
}

/**
 * テキスト・コード変換：SJIS→UTF-8（Windows API使用）
 * @param  wstring src SJISテキスト
 * @return string 変換後テキスト（UTF-8）
*/
std::string sjis_utf8(std::string src) {
	wstring const wide = __sjis_wstring(src);
	return __wstring_utf8(wide);
}

/**
 * テキスト・コード変換：UTF-8→SJIS（Windows API使用）
 * @param  wstring src UTF-8テキスト
 * @return string 変換後テキスト（UTF-8）
*/
std::string utf8_sjis(std::string src) {
	wstring const wide = __utf8_wstring(src);
	return __wstring_sjis(wide);
}

/**
 * 日本語テキスト変換（Win32 API利用）
 * @param	wstring sour   変換元テキスト
 * @param	DWORD   method 変換方式
 * @return	wstring 変換後テキスト
 */
wstring wconvString(wstring wsour, DWORD method) {
	// 濁点・半濁点は全角→半角で2文字に増えるので変換後領域は余裕を持たせる
	wchar_t wdest[wsour.length() * 2 + 1];
	for (size_t i = 0; i < wsour.length() * 2 + 1; i++) {
		wdest[i] = 0L;
	}
	// 変換実行
	LCMapStringW(LOCALE_SYSTEM_DEFAULT, method, (LPWSTR) wsour.c_str(),
			wsour.length(), (LPWSTR) wdest, wsour.length() * 2);

	return (wstring) wdest;
}

// テキスト正規化クラス ======================================================
/**
 * コンストラクタ
 */
pahooNormalizeText::pahooNormalizeText() {
#ifdef MECAB
	// MeCab動作チェック
	if (! this->tagger) {
		const char *msg = MeCab::getTaggerError();
		this->setError(_SW("MeCabをインストールしてください\n\n") + _SW((string)msg));
	}
	// 表記ゆれ統一辞書を空にする（初期化）
	VariableDict.clear();
#endif
}

// デストラクタ
pahooNormalizeText::~pahooNormalizeText() {
}

/**
 * エラー状況
 * @param	なし
 * @return	bool TRUE:異常／FALSE:正常
*/
bool pahooNormalizeText::isError(void) {
	return this->errmsg.length() > 0;
}

/**
 * エラーメッセージ取得
 * @param	なし
 * @return	wstring 現在発生しているエラーメッセージ
*/
wstring pahooNormalizeText::getError(void) {
	return this->errmsg;
}

/**
 * エラーをリセットする
 * @param	なし
 * @return	なし
*/
void pahooNormalizeText::resetError(void) {
	this->errmsg = L"";
}

/**
 * エラーをセットする
 * @param	wstring msg エラーメッセージ
 * @return	なし
*/
void pahooNormalizeText::setError(wstring msg) {
	this->errmsg = msg;
}

/**
 * フロントエンド置換：入力直後に文字列置換を行う
 * @param	string wsour 入力テキスト
 * @return	string 変換後文字列
*/
wstring pahooNormalizeText::frontend_replace(wstring wsour) {
	wstring tbl[2];

	// 文字列置換定義ファイルを読み込む
	ifstream ifs(FRONT_REPLACE_CSV);
	// 文字列置換定義ファイルが無い
	if (! ifs) {
		return wsour;
	}

	// 1行ずつ変換
	string ss;
	string delim = "\t";
	list<string> list_string;
	wstring wstr = L"";
	while (getline(ifs, ss)) {
		int n = 0;
		boost::split(list_string, ss, boost::is_any_of(delim));
		BOOST_FOREACH(string s, list_string) {
			tbl[n] = _SW(s);
			n++;
			if (n > 2)	break;
		}
		wregex re(tbl[0]);
		wsour = regex_replace(wsour, re, tbl[1]);
	}

	return wsour;
}

/**
 * 半角数字を漢数字に変換する
 * @param	wstring instr 半角数字
 *							小数、負数に対応；指数表記には未対応
 *							カンマは削除
 * @return	wstring 漢数字
*/
wstring pahooNormalizeText::num2kanji(wstring instr) {
	static wchar_t kantbl1[] =
		{ L'0', L'1', L'2', L'3', L'4', L'5', L'6', L'7',
			L'8', L'9', L'.', L'-' };
	static wchar_t kantbl2[] =
		{ 0x0000, 0x4E00, 0x4E8C, 0x4E09, 0x56DB, 0x4E94, 0x516D,	// 一～九
			0x4E03, 0x516B, 0x4E5D, 0xFF0E, 0xFF0D };				// ．－
	static wchar_t kantbl3[] = { 0x0000, 0x5341, 0x767E, 0x5343 };	// 十百千
	static wchar_t kantbl4[] = { 0x0000, 0x4E07, 0x5104, 0x5146, 0x4EAC };	// 万億兆京

	wstring outstr = L"";;
	wstring ws2;
	wchar_t wch1, wch2;
	int m = (int)instr.length() / 4;
	// 一、万、億、兆‥‥の繰り返し
	for (int i = 0; i <= m; i++) {
		ws2 = L"";
		// 一、十、百、千の繰り返し
		for (int j = 0; j < 4; j++) {
			int pos = instr.length() - i * 4 - j - 1;
			if (pos >= 0) {
				wchar_t* wch  = (wchar_t*)instr.substr(pos, 1).c_str();
				if (*wch == L',')	continue;		// カンマは無視
				for (int k = 0; k < (int)(sizeof(kantbl1) / sizeof(kantbl1[0])); k++) {
					// 漢数字 or 半角数字のまま
					wch1 = 0x0000;
					if (*wch == kantbl1[k]) {
						wch1 = kantbl2[k];
						break;
					}
				}
				wch2 = 0x0000;;
				if ((j >= 0) && (j <= 3)) {
					wch2 = kantbl3[j];
				}

				// 冒頭が「一」の場合の処理
				if (wch1 != 0x0000) {
					if ((wch1 == 0x4E00) && (wch2 != 0x0000)) {
						ws2 = (wstring){wch2} + ws2;
					} else if (wch2 != 0x0000) {
						ws2 = (wstring){wch1} + (wstring){wch2} + ws2;
					} else {
						ws2 = (wstring){wch1} + ws2;
					}
				}
			}
		}
		if (ws2 != L"") {
			if (kantbl4[i] == 0x0000) {
				outstr = ws2 + outstr;
			} else {
				outstr = ws2 + (wstring){kantbl4[i]} + outstr;
			}
		}
	}

	return outstr;
}

/**
 * 半角数字を位取り記法に変換する
 * @param	wstring instr 半角数字（小数，負数，指数表記は未対応）
 * @return	wstring 位取り記法
*/
wstring pahooNormalizeText::num2scale(wstring instr) {
	static wchar_t kantbl[] = { 0x0000, 0x4E07, 0x5104, 0x5146, 0x4EAC };	// 万億兆京
	// 余計な'0'を除く
	wregex re(_SW("0+([1-9]+)|０+([１-９]+)"));

	// 桁あふれ
	if (instr.length() > 20) {
		return instr;
	}

	// 右から1文字ずつ処理
	wstring outstr = L"";
	wstring ws = L"";
	int i = 0;
	bool flag = FALSE;
	for (int pos = instr.length() - 1; pos >= 0; pos--) {
		if (flag) {
			outstr = (wstring){kantbl[(int)(i / 4)]} + outstr;
			flag = FALSE;
		}
		wstring ss = instr.substr(pos, 1);
		ws = ss + ws;
		i++;
		if (i % 4 == 0) {
			if ((ws == L"0000") || (ws == _SW("００００"))) {
				outstr = (wstring){kantbl[(int)(i / 4)]};
			} else {
				outstr = regex_replace(ws, re, L"$1") + outstr;
				flag = TRUE;
			}
			ws = L"";
		}
	}
	outstr = ws + outstr;

	return outstr;
}

/**
 * 半角数字を漢数字に変換する
 * @param	wstring wsour 半角数字を含む文字列
 * @return	wstring 漢数字
*/
wstring pahooNormalizeText::num2kan(wstring wsour) {
	// 半角数字のパターン
	wregex re1(_SW("[0-9\\.]+"));
	wsmatch mt;

	wstring wdest = wsour;
	while (std::regex_search(wdest, mt, re1)) {
		wregex re2(mt[0].str());
		wstring kan = this->num2kanji(mt[0].str()) ;
		wdest = regex_replace(wdest, re2, kan);
	}

	return wdest;
}

/**
 * 半角数字を漢数字に変換する（単純変換）
 * @param	wstring wsour 半角数字を含む文字列
 * @return	wstring 漢数字
*/
wstring pahooNormalizeText::num2kanSimple(wstring wsour) {
	static wchar_t kantbl1[] =
		{ L'0', L'1', L'2', L'3', L'4', L'5', L'6', L'7',
			L'8', L'9', L'.', L'-' };
	static wchar_t kantbl2[] =
		{ 0x3007, 0x4E00, 0x4E8C, 0x4E09, 0x56DB, 0x4E94, 0x516D,	// 一～九
			0x4E03, 0x516B, 0x4E5D, 0xFF0E, 0xFF0D };				// ．－

	// 左から1文字ずつ処理
	wstring wdest = L"";
	wstring wstr;
	wchar_t* wch;
	bool flag;
	for (int pos = 0; pos < (int)wsour.length(); pos++) {
		flag = FALSE;
		wstr = wsour.substr(pos, 1);
		wch  = (wchar_t*)wstr.c_str();
		for (int k = 0; k < (int)(sizeof(kantbl1) / sizeof(kantbl1[0])); k++) {
			if (*wch == kantbl1[k]) {
				wdest += (wstring){kantbl2[k]};
				flag = TRUE;
				break;
			}
		}
		if (! flag) {
			wdest += *wch;
		}
	}
	return wdest;
}

/**
 * 巨大な半角数字を位取り記法に変換する
 * @param	wstring wsour 半角数字を含む文字列
 * @return	wstring 漢数字
*/
wstring pahooNormalizeText::bignum2scale(wstring wsour) {
	// 半角数字のパターン
	wregex re1(_SW("[0-9]+0000"));
	wsmatch mt;

	wstring wdest = wsour;
	while (std::regex_search(wdest, mt, re1)) {
		wregex re2(mt[0].str());
		wstring kan = this->num2scale(mt[0].str()) ;
		wdest = regex_replace(wdest, re2, kan, std::regex_constants::format_first_only);	// wsourの中に400万と4億が混在する場合にも対応
	}

	return wdest;
}

/**
 * 漢数字を半角数字に変換する
 * @param	wstring kanji 漢数字
 * @param	int mode 出力書式／1=3桁カンマ区切り，2=漢字混じり, それ以外=ベタ打ち
 * @return	wstring 半角数字
*/
wstring pahooNormalizeText::kan2num(wstring kanji, int mode) {
	wstring dest = L"";
	wsmatch mt1;

	// 全角＝半角対応
	const wstring kan_num1 = _SW("０〇○１一壱２二弐３三参４四５五６六７七８八９九");
	const wstring kan_num2 = _SW("000111222333445566778899");

	// 位取り
	const wstring kan_deci_sub = _SW("十\百千");
	const wstring kan_deci = _SW("万億兆京");

	// 半角数字が混在していたら何もしない
	wregex re1(_SW("[0-9]+"));
	if (regex_search(kanji, mt1, re1)) {
		return kanji;
	}

	// 右側から解釈していく
	size_t ll = kanji.length();
	wstring a = L"";
	long long int deci = 1;
	long long int deci_sub = 1;
	long long int m = 0;
	long long int n = 0;

	for (int pos = ll - 1; pos >= 0; pos--) {
		wstring c = kanji.substr(pos, 1);
		size_t ps1 = kan_num1.find(c);
		size_t ps2 = kan_deci_sub.find(c);
		size_t ps3 = kan_deci.find(c);
		if (ps1 != wstring::npos) {
			a = kan_num2.substr(ps1, 1) + a;
		} else if (ps2 != wstring::npos) {
			if (a != L"") {
				m = m + stol(a) * deci_sub;
			} else if (deci_sub != 1) {
				m = m + deci_sub;
			}
			a = L"";
			deci_sub = pow(10, ps2 + 1);
		} else if (ps3 != wstring::npos) {
			if (a != L"") {
				m = m + stol(a) * deci_sub;
			} else if (deci_sub != 1) {
				m = m + deci_sub;
			}
			n = m * (long long int)deci + n;
			m = 0;
			a = L"";
			deci_sub = 1;
			deci = (long long int)pow(10000, ps3 + 1);
		}
	}

	wstring ss = L"";
	wregex re2(_SW("^(0+)"));
	if (regex_search(a, mt1, re2)) {
		ss = mt1[1].str();
	}
	if (a != L"") {
		m = m + stol(a) * deci_sub;
	} else if (deci_sub != 1) {
		m = m + deci_sub;
	}
	n = m * deci + n;

	return to_wstring(n);
}

/**
 * 漢数字混じりの単語を半角数字に変換する
 * @param	wstring kanji 漢数字混じりの単語
 * @return	wstring 半角数字
*/
wstring pahooNormalizeText::kanword2num(wstring kanji) {
	// 全角＝半角対応
	const wstring kan_num1 = _SW("０〇○１一壱２二弐３三参４四５五６六７七８八９九");
	const wstring kan_num2 = _SW("000111222333445566778899");

	// 変換結果
	wstring dest = L"";

	for (size_t pos = 0; pos < kanji.length(); pos++) {
		wstring c = kanji.substr(pos, 1);
		size_t ps = kan_num1.find(c);
		if (ps != wstring::npos) {
			dest += kan_num2.substr(ps, 1);
		} else {
			dest += c;
		}
	}

	return dest;
}

/**
 * 小数点の全角を半角に変換する
 * @param	string $str  テキスト
 * @return	string 変換後文字列
*/
wstring pahooNormalizeText::hanfloat(wstring wsour) {
	wregex re(_SW("([0-9]+)．([0-9]+)"));

	return regex_replace(wsour, re, L"$1.$2");
}

#ifdef MECAB
/**
 * 日本語テキストを半角に統一
 * @param	wstring sour     変換元テキスト
 * @param	bool    trim     行頭・行末の空白を除くかどうか
 * @param	bool    variable 表記ゆれ統一するかどうか
 * @return	string  変換後テキスト
*/
wstring pahooNormalizeText::toHankaku(wstring wsour, bool trim, bool variable) {
	regex sep{"\\t|,"};
	wsmatch mt1, mt2, mt3;
	// 数字パターン
	wregex pat_kannum(_SW("^[^数]*[0123456789０１２３４５６７８９○〇一二三四五六七八九十\百千万億兆京]+$"));
	// MeCabの品詞パターン
	wregex re1(_SW("[数幾]"));
	// 「一九」対応
	wregex re19(_SW("一九"));
	// 月の漢数字
	wregex re2(_SW("([一二三四五六七八九十\]+)(月)"));
	// 名詞接続の場合はそのまま
	wregex re3(_SW("名詞接続"));
	// 行頭空白パターン
	wregex re4(_SW("^[ 　\\t\\n\\r]+"));
	// 行末空白パターン
	wregex re5(_SW("[ 　\\t\\n\\r]+$"));
	// 無変換パターン・その1
	wregex re71(_SW("副詞可.+"));
	wregex re72(_SW("接尾|格助詞"));
	// 無変換パターン・その2
	wregex re8(_SW("^[千万億兆]+$"));
	// 無変換パターン・その3
	wregex re9(_SW("一生"));

	// カンマ置換
	wregex re6(_SW(","));
	wsour = regex_replace(wsour, re6, _SW("，"));

	// 形態素に分解
	string input = _WS(wsour);
	const char *words = tagger->parse(input.c_str());

	// 変換処理
	bool flag = FALSE;
	bool adverb = FALSE;
	wstring dest = L"";
	wstring numstr = L"";
	wstring surface, pos;

	// 1語ずつ読み込む
	string ss0;
	stringstream ss;
	ss << words;
	while(ss && getline(ss, ss0)) {
		int cnt = 0;
		for (std::cregex_token_iterator end,
			ite{ss0.c_str(), ss0.c_str() + strlen(ss0.c_str()), sep, -1};
			ite != end; ++ite) {
			if (cnt == 0)		surface = _SW((*ite).str().c_str());
			else if (cnt == 2)	pos = _SW((*ite).str().c_str());
			cnt++;
		}
//		clog << _WS(surface) << " : " <<  _WS(pos) << endl;

		// 表記ゆれを統一する品詞パターン
		if (variable) {
			wregex reNoun(_SW("名詞"));
			if (regex_search(pos, mt1, reNoun)) {
				// 初回の辞書読み込み
				if (VariableDict.empty()) {
					VariableDict = this->loadVariableDictionary(VARIABILITY_REPLACE_CSV);
				}
				surface = this->variable2standard(surface);
			}
		}

		// 最後
		if (surface == _SW("EOS")) {
			break;
		// 月の処理
		} else if (regex_search(surface, mt1, re2)) {
			dest += this->kan2num(mt1[1].str(), 2) + mt1[2].str();
		// 無変換
		} else if (regex_search(pos, mt1, re71) || regex_search(surface, mt1, re9)) {
//			clog << _WS(surface) << endl;
			dest += surface;
		} else if (flag == FALSE) {
			// 副詞可能な数字
			if (regex_search(pos, mt2, re71)) {
				numstr = surface;
				flag = TRUE;
				adverb = TRUE;
			// 漢数字の1文字目
			 } else if (regex_search(surface, mt1, pat_kannum) && regex_search(pos, mt2, re1)) {
				numstr = surface;
				flag = TRUE;
				adverb = FALSE;
			// 「一九」対応
			 } else if (regex_search(surface, mt2, re19)) {
				numstr = surface;
				flag = TRUE;
				adverb = FALSE;
			// 数字ではない
			} else {
				dest += surface;
			}
		} else {
			// 無変換（副詞可能＋接尾）
			if (adverb && regex_search(pos, mt2, re72)) {
				dest += (numstr + surface);
				numstr = L"";
				flag = FALSE;
				adverb = FALSE;
			// 漢数字の2文字目以降
			} else if (regex_search(surface, mt1, pat_kannum)) {
				numstr += surface;
				flag = TRUE;
			// 数字以外
			} else {
				// 名詞接続の場合はそのまま
				if (regex_search(pos, mt2, re3)) {
					dest += (numstr + surface);
				// 無変換パターン
				} else if (regex_search(numstr, mt1, re8)) {
					dest += (numstr + surface);
				// 副詞可能の場合
				} else if (adverb) {
					dest += (this->kanword2num(numstr) + surface);
				// ここまでの漢数字を半角数字に
				} else {
					dest += (this->kan2num(numstr, 2) + surface);
				}
				numstr = L"";
				flag = FALSE;
				adverb = FALSE;
			}
		}
	}

	// 末尾処理
	if (flag == TRUE) {
		if (adverb == TRUE) {
			dest += numstr;
		} else {
			dest += this->kan2num(numstr, 2);
		}
	}

	// 行頭・行末空白処理
	if (trim) {
		wstring wss = regex_replace(dest, re4, L"");
		wss = regex_replace(wss, re5, L"");
		dest = wss + L"\\n";
	}

	// 「十八番」対応
	wregex re11(_SW("一八([^番]+)"));
	dest = regex_replace(dest, re11, L"18$1");
	// 「・」対応
	wregex re12(_SW("([0-9]+)・([0-9]+)"));
	dest = regex_replace(dest, re12, L"$1.$2");

	return wconvString(dest, LCMAP_HALFWIDTH);
}
#endif

// _han2zenのコールバック関数群
// 数字
bool _decimal(wchar_t wch) {
	return ((wch >= L'0') && (wch <= L'9'));
}
// アルファベット
bool _alphabet(wchar_t wch) {
	return (((wch >= L'A') && (wch <= L'Z'))
				|| ((wch >= L'a') && (wch <= L'z')));
}
// 半角カタカナ
bool _katakana(wchar_t wch) {
	return ((wch >= 0xFF66) && (wch <= 0xFF9F));		// ｦ～ﾟ
}
// 記号
bool _yakumono(wchar_t wch) {
	return (((wch >= L'!') && (wch <= L'/'))
				|| ((wch >= L':') && (wch <= L'@'))
				|| ((wch >= L'[') && (wch <= L'`'))
				|| ((wch >= L'{') && (wch <= L'~'))
				|| ((wch >= 0xFF3B) && (wch <= 0xFF40))
				|| ((wch >= 0xFF5B) && (wch <= 0xFF65)));
}

/**
 * 半角→全角変換に変換
 * @param	wstring sour 変換元テキスト
 * @param	bool (*func) 該当文字判定関数
 * @return	wstring  変換後テキスト
*/
wstring pahooNormalizeText::han2zen(wstring wsour, bool (*func)(wchar_t wch)) {
	wstring wss = L"";
	wstring wdest = L"";
	bool flag = FALSE;
//	clog << _WS(wsour) << endl;

	// 先頭から1文字ずつ
	for (size_t i = 0; i < wsour.length(); i++) {
		wchar_t* wch = (wchar_t*)wsour.substr(i, 1).c_str();
		// 該当文字ならwss0へ
		if (func(*wch)) {
			wss += (wstring){*wch};
			flag = TRUE;
		// 半角→全角変換
		} else if (flag) {
			wdest += wconvString(wss, LCMAP_FULLWIDTH);
			wdest += wsour.substr(i, 1);
			flag = FALSE;
			wss = L"";
		} else {
			wdest += wsour.substr(i, 1);
		}
	}
	// 最後の1文字が該当文字
	if (flag) {
		wdest += wconvString(wss, LCMAP_FULLWIDTH);
	}

	return wdest;
}

/**
 * 日本語テキストを正規化する
 * @param	wstring sour   漢数字混じりテキスト
 * @param	char*   option 変換オプション
 * @param	bool    trim   行頭・行末の空白を除くかどうか
 * @return	wstring 変換後テキスト
*/
wstring pahooNormalizeText::normalizeText(const wstring wsour, const char* option, bool trim) {
	wstring wdest = wsour;

	// フロントエンド置換
	wdest = this->frontend_replace(wdest);

	// 制御文字を削除する
	if (strchr(option, OPTION_CONTROL_DEL) != NULL) {
		wregex re2(_SW("([\\t\\r\\r]+)"));
		wdest = regex_replace(wdest, re2, L"");
	}
	// 表記ゆれを統一する
	bool variable = FALSE;
	if (strchr(option, OPTION_VARIABLE) != NULL) {
		variable = TRUE;
	}
	std::cout << variable << std::endl;

	// いったん半角に
#ifdef MECAB
	wdest = this->toHankaku(wdest, trim, variable);
#endif
//	clog << _WS(wdest) << endl;

	// 全角文字と隣り合う空白文字を除く
	wregex re11(_SW("[ 　\\t]+([^!-~].)"));
	wdest = regex_replace(wdest, re11, L"$1");
	wregex re12(_SW("([^!-~].)[ 　\\t]+"));
	wdest = regex_replace(wdest, re12, L"$1");

	// 英字：半角→全角
	if (strchr(option, OPTION_ALP_ZEN) != NULL) {
		wdest = this->han2zen(wdest, _alphabet);
	}
	// 数字：位取り記法
	if (strchr(option, OPTION_NUM_SCALE) != NULL) {
		wdest = this->bignum2scale(wdest);
	}
	// 数字：半角→全角
	if (strchr(option, OPTION_NUM_ZEN) != NULL) {
		wdest = this->han2zen(wdest, _decimal);
	// 数字：半角→漢数字
	} else if (strchr(option, OPTION_NUM_KAN) != NULL) {
		wdest = this->num2kan(wdest);
	// 数字：半角→漢数字(単純)
	} else if (strchr(option, OPTION_NUM_KAN2) != NULL) {
		wdest = this->num2kanSimple(wdest);
	}
	// 記号：半角→全角
	if (strchr(option, OPTION_YAK_ZEN) != NULL) {
		wdest = this->han2zen(wdest, _yakumono);
	}
	// カタカナ：半角→全角
	if (strchr(option, OPTION_KANA_ZEN) != NULL) {
		wdest = this->han2zen(wdest, _katakana);
	}
	// 小数点の全角を半角に変換
	wdest = this->hanfloat(wdest);

	// 半角文字と隣り合う全角空白は半角空白へ
	wregex re2(_SW("[　]+([!-~].)"));
	wdest = regex_replace(wdest, re2, L" $1");

	// 全角二重引用符の開閉
	if (strchr(option, OPTION_DBL_QUOTE) != NULL) {
		wregex re3(_SW("＂([^＂]*)＂"));
		wdest = regex_replace(wdest, re3, _SW("“") + L"$1" + _SW("”"));
	}

	return wdest;
}

// 表記ゆれを統一する ====================================================
/**
 * 表記ゆれ統一辞書ファイルを内部辞書に読み込む
 * 表記ゆれ統一辞書ファイルはUTF-8、内部辞書はswtring
 * @param	string fname 変換ファイル名
 * @return	std::vector<std::pair<std::wstring, std::wstring>> 変換辞書
*/
std::vector<std::pair<std::wstring, std::wstring>> pahooNormalizeText::loadVariableDictionary(const std::string& filename) {
	std::vector<std::pair<std::wstring, std::wstring>> dict;
	std::ifstream file(filename, std::ios::binary);
	if (!file) { 
		cout << "ファイル開けません: " << filename << endl;
		return dict;
	}

	std::string line;
	while (std::getline(file, line)) {
		// 末尾の \r または \n を削除
		while (!line.empty() && (line.back() == '\n' || line.back() == '\r')) {
			line.pop_back();
		}

		size_t commaPos = line.find(',');
		if (commaPos != std::string::npos) {
			std::wstring key = __utf8_wstring(line.substr(0, commaPos));
			std::wstring value = __utf8_wstring(line.substr(commaPos + 1));
			dict.emplace_back(key, value);
		}
	}

	// 1カラム目の長い順にソート
	std::sort(dict.begin(), dict.end(),
			[](const auto& a, const auto& b){ return a.first.size() > b.first.size(); });

    return dict;
}

/**
 * 単語の表記ゆれを統一する
 * @param	wstring 単語
 * @return	wstring 統一表記の単語
*/
wstring pahooNormalizeText::variable2standard(wstring word) {
	auto it = std::find_if(VariableDict.begin(), VariableDict.end(),
						[&word](const auto& p){ return p.first == word; });

	// 一致したら value に置換
	if (it != VariableDict.end()) {
		return it->second;
    }
	// 一致しなければそのまま
    return word;
}

// 西暦⇔元号変換 ==========================================================
/**
 * 西暦⇔元号変換ファイル読み込み
 * @param	copnst char* fname 読み込みファイル名
 * @return	bool TRUE/FALSE
*/
void pahooNormalizeText::readTableEra(const char* fname) {
	// 変換テーブルのパターン
	wregex re(_SW("[\'\"]([0-9 ]+)[\'\"]\\s*=>\\s*[\'\"](.*)[\'\"]"));
	wsmatch mt;

	// 変換テーブルを初期化
	TableEra.clear();
	TableEra.shrink_to_fit();

	// ファイル読み込み
	ifstream ifs(fname);
	if (! ifs) {
		this->setError(_SW((string)fname + " の読み込みに失敗しました"));
		return;
	}
	string ss;
	wstring wss;
	while (getline(ifs, ss)) {
		wss = _SW(ss);
		if (regex_search(wss, mt, re)) {
			_tableEra te;
			te.start = wcstol((const wchar_t*)mt[1].str().c_str(), NULL, 10);
			te.era = mt[2].str();
			TableEra.push_back(te);
		}
	}
}

/**
 * 変換範囲を設定する
 * @param	long start 開始年月日 YYYYMMDD（西暦）形式
 * @param	long end   終了年月日 YYYYMMDD（西暦）形式
 * @return	なし
*/
void pahooNormalizeText::setScope(long start, long end) {
	this->convStart =  start;
	this->convEnd   =  end;
}

/**
 * 年月日が変換範囲かどうか調べる
 * @param	long yyyymmdd 年月日
 * @return	bool TRUE/FALSE
*/
bool pahooNormalizeText::isConvEra(long yyyymmdd) {
	return ((yyyymmdd >= this->convStart) && (yyyymmdd <= this->convEnd));
}

/**
 * 文字列が元号かどうか判断する
 * @param	wstring str 文字列
 * @return	bool TRUE/FALSE
*/
bool pahooNormalizeText::isEra(wstring wstr) {
	wsmatch mt;

	if (wstr == L"")	return FALSE;

	for (auto& te : TableEra) {
		wregex re(te.era);
		if (regex_search(wstr, mt, re))		return TRUE;
	}
	return FALSE;
}

/**
 * 西暦を元号に変換する
 * @param	wstring prefix 年の前に付いている文字列
 * @param	int year  年
 * @param	int month 月（省略可能）
 * @param	int day   日（省略可能）
 * @param	char* option 変換オプション
 * @return	wstring 元号（＋月日）
*/
wstring pahooNormalizeText::ad2era(wstring prefix, int year, int month=0, int day=0, const char* option=NULL) {
	wstring dest = L"";
	wstring last = L"";
	wstring wstr = L"";
	wsmatch mt;
	wregex re(ESCYEAR);
	long yy = 0;
	long yyyymmdd = year * 10000 + month * 100 + day;

	// 和暦に変換するかどうか
	if (! regex_search(prefix, mt, re) && isConvEra(yyyymmdd)) {
		if (! this->isEra(prefix)) {
			for (auto& te : TableEra) {
				if (yyyymmdd >= te.start) {
					yy   = year - (te.start / 10000);
					last = te.era;
				} else if (last != L"") {
					if (yy == 0) {
						wstr = _SW("元");
					} else if (strchr(option, OPTION_ERA_ZEN) != NULL) {
						wstr = this->han2zen(to_wstring(yy + 1), _decimal);
					} else if (strchr(option, OPTION_ERA_KAN1) != NULL) {
						wstr = this->num2kanSimple(to_wstring(yy + 1));
					} else if (strchr(option, OPTION_ERA_KAN2) != NULL) {
						wstr = this->num2kan(to_wstring(yy + 1));
					} else {
						wstr = to_wstring(yy + 1);
					}
					dest = prefix + last + wstr + _SW("年");
					break;
				}
			}
		} else {
			if (year == 1) {
				wstr = _SW("元");
			} else if (strchr(option, OPTION_ERA_ZEN) != NULL) {
				wstr = this->han2zen(to_wstring(year), _decimal);
			} else if (strchr(option, OPTION_ERA_KAN1) != NULL) {
				wstr = this->num2kanSimple(to_wstring(year));
			} else if (strchr(option, OPTION_ERA_KAN2) != NULL) {
				wstr = this->num2kan(to_wstring(year));
			} else {
				wstr = to_wstring(year);
			}
			dest = wstr + _SW("年");
		}
	// 変換しない（西暦または元号のまま）
	} else {
		if (year == 1) {
			wstr = _SW("元");
		} else if (strchr(option, OPTION_ERA_ZEN) != NULL) {
			wstr = this->han2zen(to_wstring(year), _decimal);
		} else if (strchr(option, OPTION_ERA_KAN1) != NULL) {
			wstr = this->num2kanSimple(to_wstring(year));
		} else if (strchr(option, OPTION_ERA_KAN2) != NULL) {
			wstr = this->num2kan(to_wstring(year));
		} else {
			wstr = to_wstring(year);
		}
		dest = prefix + wstr + _SW("年");
	}

	if (month > 0) {
		dest += to_wstring(month) + _SW("月");
	}
	if (day > 0) {
		dest += to_wstring(day) + _SW("日");
	}

	return dest;
}

/**
 * 元号を西暦に変換する
 * @param	wstring prefix 年の前に付いている文字列（元号）
 * @param	int year  年
 * @param	int month 月（省略可能）
 * @param	int day   日（省略可能）
 * @param	char* option 変換オプション
 * @return	wstring 西暦（＋月日）
*/
wstring pahooNormalizeText::era2ad(wstring prefix, int year, int month=0, int day=0, const char* option=NULL) {
	wstring dest = L"";
	wstring last = L"";
	wstring wstr = L"0";
	wregex re(ESCYEAR);
	wsmatch mt;
	long yy = 0;

	// 西暦年へ変換
	for (auto& te : TableEra) {
		if ((te.era != L"") && (te.era == prefix)) {
			yy   = year + (int)(te.start / 10000) - 1;
			if (strchr(option, OPTION_AD_ZEN) != NULL) {
				wstr = this->han2zen(to_wstring(yy), _decimal);
			} else if (strchr(option, OPTION_AD_KAN1) != NULL) {
				wstr = this->num2kanSimple(to_wstring(yy));
			} else if (strchr(option, OPTION_AD_KAN2) != NULL) {
				wstr = this->num2kan(to_wstring(yy));
			} else {
				wstr = to_wstring(yy);
			}
			break;
		}
	}

	// 西暦に変換するかどうか
	long yyyymmdd = yy * 10000 + month * 100 + day;
	if (! regex_search(prefix, mt, re) && isConvEra(yyyymmdd)) {
		dest = wstr + _SW("年");
	// 変換しない（和暦のまま）
	} else {
		// 和暦の数字変換
		if (year == 1) {
			wstr = _SW("元");
		} else if (strchr(option, OPTION_AD_ZEN) != NULL) {
			wstr = this->han2zen(to_wstring(year), _decimal);
		} else if (strchr(option, OPTION_AD_KAN1) != NULL) {
			wstr = this->num2kanSimple(to_wstring(year));
		} else if (strchr(option, OPTION_AD_KAN2) != NULL) {
			wstr = this->num2kan(to_wstring(year));
		} else {
			wstr = to_wstring(year);
		}
		dest = prefix + wstr + _SW("年");
	}
	// 月日
	if (month > 0) {
		dest += to_wstring(month) + _SW("月");
	}
	if (day > 0) {
		dest += to_wstring(day) + _SW("日");
	}

	return dest;
}

/**
 * 西暦→和暦変換（漢数字対応）
 * @param	wsmatch mt 年月日パターンにマッチしたテキスト
 * @return	wstring 和暦
*/
wstring pahooNormalizeText::seireki2wareki(wsmatch mt, const char* option=NULL) {
	wstring year, month, day;

	if (mt[2].str() != L"") {
		if (mt[2].str() == _SW("元")) {
			year = L"1";
		} else {
			year = this->kan2num(mt[2].str(), 0);
		}
	} else {
		year = _SW("0");
	}
	if (mt[3].str() != L"") {
		month = this->kan2num(mt[3].str(), 0);
	} else {
		month = _SW("0");
	}
	if (mt[4].str() != L"") {
		day = this->kan2num(mt[4].str(), 0);
	} else {
		day = _SW("0");
	}

	return this->ad2era(mt[1].str(), stoi(year), stoi(month), stoi(day), option);
}

/**
 * 和暦に統一
 * @param	wstring sour オリジナル・テキスト
 * @param	char*   option 変換オプション
 * @param	bool    flag   TRUE=エスケープ文字を消去／FALSE=消去しない
 * @return	wstring 変換後テキスト
*/
wstring pahooNormalizeText::wareki(wstring wsour, const char* option=NULL, bool flag=TRUE) {
	wregex re(_SW("([^0-9０-９〇一二三四五六七八九十\百千万あ-ん、-〟！-￥]{0,4})\\s*([0-9０-９元〇一二三四五六七八九十\百千万]+)年\\s*([0-9０-９〇一二三四五六七八九十\]*)月?([0-9０-９〇一二三四五六七八九十\]*)日?"));
	wregex re2(ESCYEAR);
	wsmatch mt;

	wstring wdest = L"";
	wstring suffix = L"";
	for (bool ismatch = regex_search(wsour, mt, re); ismatch != FALSE;
			ismatch = regex_search(mt[0].second, mt.suffix().second, mt, re)) {
		if (flag) {
			wdest += (wstring)mt.prefix() + 
				regex_replace(this->seireki2wareki(mt, option), re2, L"");
		} else {
			wdest += (wstring)mt.prefix() + this->seireki2wareki(mt, option);
		}
		suffix = (wstring)mt.suffix();		// 未変換の部分文字列
	}
	wdest += suffix;

	return wdest;
}

/**
 * 和暦→西暦変換（漢数字対応）
 * @param	wsmatch mt 年月日パターンにマッチしたテキスト
 * @return	wstring 西暦
*/
wstring pahooNormalizeText::wareki2seireki(wsmatch mt, const char* option=NULL) {
	wstring year, month, day;

	if (mt[2].str() == _SW("元")) {
		year = L"1";
	} else if (mt[2].str() != L"") {
		year = this->kan2num(mt[2].str(), 0);
	}
	if (mt[3].str() != L"") {
		month = this->kan2num(mt[3].str(), 0);
	} else {
		month = _SW("0");
	}
	if (mt[4].str() != L"") {
		day = this->kan2num(mt[4].str(), 0);
	} else {
		day = _SW("0");
	}

	return this->era2ad(mt[1].str(), stoi(year), stoi(month), stoi(day), option);
}

/**
 * 西暦に統一
 * @param	wstring sour   オリジナル・テキスト
 * @param	char*   option 変換オプション
 * @param	bool    flag   TRUE=エスケープ文字を消去／FALSE=消去しない
 * @return	wstring 変換後テキスト
*/
wstring pahooNormalizeText::seireki(wstring wsour, const char* option=NULL, bool flag=TRUE) {
	wregex re(_SW("([^0-9０-９〇一二三四五六七八九十\百千万あ-ん、-〟！-￥]{0,4})\\s*([0-9０-９〇元一二三四五六七八九十\百千万]+)年\\s*([0-9０-９〇一二三四五六七八九十\]*)月?([0-9０-９〇一二三四五六七八九十\]*)日?"));
	wregex re2(ESCYEAR);
	wsmatch mt;

	wstring wdest = L"";
	wstring suffix = L"";
	for (bool ismatch = regex_search(wsour, mt, re); ismatch != FALSE;
			ismatch = regex_search(mt[0].second, mt.suffix().second, mt, re)) {
		if (flag) {
			wdest += (wstring)mt.prefix() + 
				regex_replace(this->wareki2seireki(mt, option), re2, L"");
		} else {
			wdest += (wstring)mt.prefix() + this->wareki2seireki(mt, option);
		}
		suffix = (wstring)mt.suffix();		// 未変換の部分文字列
	}
	wdest += suffix;

	return wdest;
}

/**
 * 和暦→西暦（和暦）変換
 * @param	wsmatch mt     年月日パターンにマッチしたテキスト
 * @param	char*   option 変換オプション
 * @return	wstring 西暦
*/
wstring pahooNormalizeText::seireki2mix(wsmatch mt, const char* option=NULL) {
	wstring wdest = L"";
	wstring wstr  = L"";
	wstring ad = this->wareki2seireki(mt, option);

	if (strchr(option, OPTION_ERA_ZEN) != NULL) {
		wstr = this->han2zen(mt[2].str(), _decimal);
	} else if (strchr(option, OPTION_ERA_KAN1) != NULL) {
		wstr = this->num2kanSimple(mt[2].str());
	} else if (strchr(option, OPTION_ERA_KAN2) != NULL) {
		wstr = this->num2kan(mt[2].str());
	} else {
		wstr = mt[2].str();
	}
	wstring wareki = mt[1].str() + wstr + _SW("年");

	if (ad != wareki) {
		wdest = ad + _SW("（") + wareki + _SW("）");
	} else {
		wdest = ad;
	}

	return wdest;
}

/**
 * 西暦（和暦）混合変換
 * @param	wstring sour   オリジナル・テキスト
 * @param	char*   option 変換オプション
 * @param	bool    flag   TRUE=エスケープ文字を消去／FALSE=消去しない
 * @return	wstring 変換後テキスト
*/
wstring pahooNormalizeText::mixture(wstring wsour, const char* option=NULL, bool flag=TRUE) {
	wregex re(_SW("([^0-9０-９〇一二三四五六七八九十\百千万あ-ん、-〟！-￥]{0,4})([0-9元]+)年"));
	wregex re2(ESCYEAR);
	wsmatch mt;
	static const char opt[] = { OPTION_ERA_HAN, 0x00 };

	wstring wstr = this->wareki(wsour, opt, FALSE);		// 和暦（半角数字）に統一

	wstring wdest  = L"";
	wstring suffix = L"";
	for (bool ismatch = regex_search(wstr, mt, re); ismatch != FALSE;
			ismatch = regex_search(mt[0].second, mt.suffix().second, mt, re)) {
		if (flag) {
			wdest += (wstring)mt.prefix() +
				regex_replace(this->seireki2mix(mt, option), re2, L"");
		} else {
			wdest += (wstring)mt.prefix() + this->seireki2mix(mt, option);
		}
		suffix = (wstring)mt.suffix();		// 未変換の部分文字列
	}
	wdest += suffix;

	return wdest;
}

/**
 * 月日の変換
 * @param	wstring sour   オリジナル・テキスト
 * @param	char*   option 変換オプション
 * @return	wstring 変換後テキスト
*/
wstring pahooNormalizeText::monthday(wstring wsour, const char* option=NULL) {
	wregex re(_SW("([0-9０-９〇一二三四五六七八九十\]{1,2})(月|日)"));
	wsmatch mt;

	wstring suffix = L"";
	wstring wdest  = L"";
	wstring wstr   = L"";

	for (bool ismatch = regex_search(wsour, mt, re); ismatch != FALSE;
			ismatch = regex_search(mt[0].second, mt.suffix().second, mt, re)) {
		if ((strchr(option, OPTION_MODE_AD) != NULL) || 
				(strchr(option, OPTION_MODE_ADERA) != NULL)) {
			if (strchr(option, OPTION_AD_ZEN) != NULL) {
				wstr = this->han2zen(mt[1].str(), _decimal);
			} else if (strchr(option, OPTION_AD_KAN1) != NULL) {
				wstr = this->num2kanSimple(mt[1].str());
			} else if (strchr(option, OPTION_AD_KAN2) != NULL) {
				wstr = this->num2kan(mt[1].str());
			} else {
				wstr = wconvString(mt[1].str(), LCMAP_HALFWIDTH);
			}
		} else if (strchr(option, OPTION_MODE_ERA) != NULL) { 
			if (strchr(option, OPTION_ERA_ZEN) != NULL) {
				wstr = this->han2zen(mt[1].str(), _decimal);
			} else if (strchr(option, OPTION_ERA_KAN1) != NULL) {
				wstr = this->num2kanSimple(mt[1].str());
			} else if (strchr(option, OPTION_ERA_KAN2) != NULL) {
				wstr = this->num2kan(mt[1].str());
			} else {
				wstr = wconvString(mt[1].str(), LCMAP_HALFWIDTH);
			}
		}
		wdest += (wstring)mt.prefix() + wstr + mt[2].str();
		suffix = (wstring)mt.suffix();		// 未変換の部分文字列
	}
	wdest += suffix;

	return wdest;
}

/**
 * 西暦⇔元号変換
 * @param	wstring sour   オリジナル・テキスト
 * @param	char*   option 変換オプション
 * @return	wstring 変換後テキスト
*/
wstring pahooNormalizeText::convNengo(wstring wsour, const char* option) {
	wstring wdest = L"";

	// 変換範囲
	if (strchr(option, OPTION_SCOPE_1) != NULL) {
		setScope(START_SCOPE1, END_SCOPE);
	} else if (strchr(option, OPTION_SCOPE_2) != NULL) {
		setScope(START_SCOPE2, END_SCOPE);
	} else if (strchr(option, OPTION_SCOPE_0) != NULL) {
		setScope(END_SCOPE, END_SCOPE);
	}

	// 西暦に統一
	if (strchr(option, OPTION_MODE_AD) != NULL) {
		wdest = this->seireki(wsour, option, TRUE);
	// 和暦に統一
	} else if (strchr(option, OPTION_MODE_ERA) != NULL) {
		wdest = this->wareki(wsour, option, TRUE);
	// 西暦(和暦)
	} else if (strchr(option, OPTION_MODE_ADERA) != NULL) {
		wdest = this->mixture(wsour, option, TRUE);
	}
	// 月日
	wdest = this->monthday(wdest, option);

	return wdest;
}

/*
 ** バージョンアップ履歴 =====================================================
 *
 * @version 2.0.1  2025/11/23 bug-fix: 変数variable未使用ワーニング対策
 * @version 2.0.0  2025/08/30 表記ゆれ統一機能を追加
 * @version 1.9.1  2023/12/17 pahooNormalizeText() - MECAB非使用時の対策
 * @version 1.9.0  2023/10/25 normalizeText() - 二重引用符の開閉処理
 * @version 1.8.0  2023/10/25 pahooNormalizeText() - MeCab動作チェック追加
 * @version 1.7.2  2023/07/30 bignum2scale() - wsourの中に400万と4億が混在する場合にも対応
 * @version 1.7.1  2023/04/22 toHankaku() - 「一九」対応
 * @version 1.7.0  2023/04/08 toHankaku() - 「十八番」「・」対応
 * @version 1.6    2022/07/03 フロントエンド置換 frontend_replace() を追加
 * @version 1.53   2022/05/06 toHankaku():月の処理を無変換より優先に
 * @version 1.52   2022/04/29 制御文字削除機能追加
 * @version 1.51   2022/02/19 bug-fix
 * @version 1.5    2022/01/03 1.3～1.4の対策などを品詞対策に改良
 * @version 1.4    2021/12/02 「一挙」「一番」など文字化け対策
 * @version 1.3    2021/11/06 「一律」「何千万」など文字化け対策
 * @version 1.11   2021/02/01 _yakumono() に不足していた記号を追加
 * @version 1.1    2020/11/14 西暦⇔元号変換を追加
 * @version 1.01   2020/11/10 toHankaku(), _yakumono() bug-fix
 * @version 1.0    2020/10/20 初版
 */
