/*
【Calc仕様】
１．四則演算子（+, -, *, /, %−opecode配列に収納）を用いた（整数の）算術計算式の値を取得する。
２．計算は左から右へ行う。
３．優先順位は単項演算子（+-）→()→*、/、%→+、-の順
４．値は10進数、16進数（Int32.ParseとSystem.Globalization.NumberStyles.HexNumberを使用）//https://learn.microsoft.com/ja-jp/dotnet/api/system.globalization.numberstyles?view=net-8.0
５．二項式は"operand0 四則演算子（+, -, *, /, %）operand1"とする。operand0と1は（空白文字を除く）文字列、その値はopevalu0と1整数とする。
６．最初に出てくること有るべき単項式（operand0が空文字""でopeval0が0）は単に"operand0 加減演算子（+, -）operand1"で処理し、*、/、%はエラーとする。
７．値はNumber（10進数）、HexNumber（16進数）対応とし、HexNumberには頭に"0x"か"0X"をつける必要がある。

【処理】
１．式文字列を左からチェックしてゆく。
２．式文字列は先ず加減演算関数（+, -）で処理し、求められたoperando0とoperand1の文字列を乗除演算関数（*, /, %）で処理する。これにより乗除が加減に優先する。
３．'('があった場合には、ポインターを先に進め、')'までの文字列を再帰処理する。')'がなければエラーとする。
*/
//ワイド文字を使用する
//#define		UNICODE	1	//Unicode使用の為
//#include	<wchar.h>	//Unicode使用の為

////////////////////////////////
// Calcクラス定義ファイル
//（整数算術式演算ライブラリー）
// Copyright (c) 2024 by Ysama
////////////////////////////////
class CALC
{
private:
	//メンバー変数
	wchar_t *m_formula = nullptr;			//演算式記録用
	//メンバー関数
	void Init(const wchar_t*);				//初期化関数（演算式の記録）
	bool TryMulDiv(int&, const wchar_t*);	//乗除演算、括弧演算を行う
	bool TryParse(int&, const wchar_t*);	//10、2または16進数文字列から整数値を返す
	wchar_t* SkipBraces(wchar_t*);			//括弧をスキップし、次のアドレスを返す

public:
	CALC();									//コンストラクター１
	CALC& operator = (wchar_t*);			//operator '='（wchar_t文字列の代入）
	CALC(wchar_t*);							//コンストラクター２
	~CALC();								//デストラクター
	bool TryCalc(int&, const wchar_t*);		//加減演算を行う主関数

	////////////////////////////////
	//クラス内クラス Part Of String
	//st - ed 間の文字列を保有し返す
	////////////////////////////////
	class CPOS
	{
	private:
		wstring m_partstr;
	public:
		CPOS() : m_partstr(L"") {}
		CPOS(const wchar_t* st, const wchar_t* ed) {
			Set(st, ed);
		}
		void Set(const wchar_t* st, const wchar_t* ed) {
			if(ed >= st)	//引数順序st (<=) ed
				m_partstr = wstring(st, ed + 1);
			else			//引数順序間違い対応
				m_partstr = wstring(ed, st + 1);
		}
		const wchar_t* Get() const {
			return m_partstr.c_str();
		}
	};
};

/////////////////////
//コンストラクター１
/////////////////////
CALC::CALC() {

	//ワイド文字使用のためロケールの初期化（日本語）
	setlocale(LC_CTYPE, "ja_JP");
	Init(L"");		//初期化関数
}

//////////////////////////////////////
//operator '='（wchar_t文字列の代入）
//////////////////////////////////////
CALC& CALC::operator = (wchar_t* str) {

	Init(str);		//初期化関数
	return *this;
}

/////////////////////
//コンストラクター２
/////////////////////
CALC::CALC(wchar_t* str) {

	CALC();			//引数無しコンストラクター
	Init(str);		//初期化関数
}

//////////////////
//デストラクター
//////////////////
CALC::~CALC() {

	delete [] m_formula;
}

//////////////
//初期化関数
//////////////
void CALC::Init(const wchar_t* str) {

	if(m_formula)					//既存の文字列があれば破棄
		delete [] m_formula;
	int len = wcslen(str) + 1;		//引数文字列の長さ
	m_formula = new wchar_t[len];	//同じ長さのワイド文字配列を作成
	//数字、アルファベット、空白文字等以外を除外してm_formulaにコピーする
	int j = 0;
	for(int i = 0; i < len; i++) {
		if(	(str[i] >= u'\u0030' && str[i] <= u'\u0039') ||	//0-9の場合（半角）
			(str[i] >= u'\u0041' && str[i] <= u'\u0046') ||	//A-Fの場合（半角）
			str[i] == L'X'	||	//（半角X）
			(str[i] >= u'\u0061' && str[i] <= u'\u0066') ||	//a-fの場合（半角）
			str[i] == L'x'	||	//（半角x）
			(str[i] == L'('	|| str[i] == L')') ||	//()の場合（半角）
			iswspace(str[i])||	//（空白文字）
			str[i] == L'+'	||	//（半角+）
			str[i] == L'-'	||	//（半角-）
			str[i] == L'*'	||	//（半角*）
			str[i] == L'/'	||	//（半角/）
			str[i] == L'%') {	//（半角%）
			m_formula[j] = str[i];
			j++;
		}
	}
	m_formula[j] = L'\0';			//ヌル終端
/*	旧初期化関数（コントロールコードが入っていると混入し得る）
	if(m_formula)					//既存の文字列があれば破棄
		delete [] m_formula;
	int len = wcslen(str) + 1;		//引数文字列の長さ
	m_formula = new wchar_t[len];	//同じ長さのワイド文字配列を作成
	wcscpy(m_formula, str);			//m_formulaに算術式文字列をコピー
*/
}

///////////////////////////////////////////////
//演算制御を行う主関数（+または-の処理を行う）
//演算に成功するとtrue、失敗するとfalseを返す。
//参照変数sol(ution)に解が入る。
///////////////////////////////////////////////
bool CALC::TryCalc(int& sol, const wchar_t* str = L"") {

	//ローカル変数
	wchar_t *ptr, *start, *end;				//順に演算式用、切り取り用ポインター
	int opeval = 0;							//式評価用オペランド値
	int opec = 0;							//オペコード（+は1、-は2）
	bool post_opec = false;					//「オペコード後」サイン
	CPOS operand;							//文字列切り取り用CPOSインスタンス
	//初期化
	sol = 0;								//解(solution)が入る参照変数を初期化
	if(*str != L'\0')						//strが既定値の空文でなければ
		Init(str);							//計算式（str）でm_formulaを初期化
	ptr = m_formula;						//ポインターを演算式文字列の起点に初期化
	end = start = nullptr;					//切り取りポインターの初期化
	//ループ処理
	while(*ptr != L'\0') {					//ヌル終端に至るまで継続
		//空白処理
		if(iswspace(*ptr)) {
			if(start && !end)				//startは設定され、endは設定されていない場合
				end = ptr - 1;				//endを空白文字直前に設定
			ptr++;							//空白文字をスキップ
		}
		//ポインターの指す文字により処理を振り分ける
		else {
			switch(*ptr) {
			case L'+':						//'+'の場合
			case L'-':						//'-'の場合
				if(post_opec)					//オペレーター後にオペレーターが続けば
					return false;				//エラー
				if(!start)						//単項演算（start == nullptr）の場合−問題+-の後に+-が書かれていたら
					start++;					//startを+-符号の次に進める
				else {							//二項演算で
					if(!end)					//オペランド未設定（end == nullptr）ならば
						end = ptr - 1;			//+-演算子の直前に設定
					operand.Set(start, end);	//文字列切り取り
					if(!TryParse(opeval, operand.Get()))		//整数値でない場合
						if(!TryMulDiv(opeval, operand.Get()))	//乗除演算が不可能
							return false;						//エラー
				}
				if(opec == 0)					//初期値の場合（単項式の場合
					sol = opeval;				//第1オペランドであるsolに代入する
				else if(opec == 1)				//'+'の場合
					sol += opeval;				//（単項演算子を含む）加算または
				else if(opec == 2)				//'-'の場合
					sol -= opeval;				//（単項演算子を含む）減算を行う
				if(*ptr == L'+')				//新しいオペコードをopecに設定
					opec = 1;					//'+'の設定
				else if(*ptr == L'-')
					opec = 2;					//'-'の設定
				ptr++;							//次のオペランドへ進む
				end = start = nullptr;			//切り取りポインターの初期化
				post_opec = true;				//オペレーター後フラグ
				break;
			case L'*':						//'*'の場合
			case L'/':						//'/'の場合
			case L'%':						//'%'の場合
				if(!start)						//最初に'*'、'/'、'%'があれば
					return false;				//エラー
				if(post_opec)					//オペレーター後にオペレーターが続けば
					return false;				//エラー
				end = start;					//end == start（defaultでの処理の為）にして
				ptr++;							//ポインターを進める
				post_opec = true;				//オペレーター後フラグ
				break;
			default:						//'+'、'-'、'*'、'/'、'%'以外の非空白文字
				if(!start)						//startが未設定（nullptr）であれば
					start = ptr;				//非空白文字の先頭に設定
				else if(start == end)			//'*'、'/'、'%'があった場合の次の非空白文字
					end = nullptr;				//オペランド未設定（end == nullptr）に戻す
				else if(start && end)			//start、end共に設定済で演算子以外の場合
					return false;				//構文エラー
				if(*ptr == L'(') {				//括弧の場合
					ptr = SkipBraces(ptr);		//括弧をスキップし、ポインターは')'の次を指す
					if(!ptr)					//括弧が閉じられていなければ（ptr == nullptr）
						return false;			//構文エラー
				}
				else
					ptr++;						//ポインターを進める
				post_opec = false;				//オペランド後なので、オペレーター後フラグを戻す
				break;
			}
		}
		//ポインターを進めてヌル終端になった場合（+-のラップアップ処理）
		if(*ptr == L'\0') {
			if(post_opec)					//オペレーター後にオペレーターが続けば
				return false;				//エラー
			if(!end)						//endが未設定であれば
				end = ptr - 1;				//ヌル終端の直前に設定
			operand.Set(start, end);		//文字列切り取り
			if(!TryParse(opeval, operand.Get()))		//整数値でない場合
				if(!TryMulDiv(opeval, operand.Get()))	//乗除演算が不可能
					return false;						//エラー
			if(opec == 0)					//初期値の場合
				sol = opeval;				//第1オペランドであるsolに代入する
			else if(opec == 1)				//'+'の場合
				sol += opeval;				//加算または
			else if(opec == 2)				//'-'の場合
				sol -= opeval;				//減算を行う
		}
	}
	return true;
}

////////////////
//乗除演算関数
////////////////
bool CALC::TryMulDiv(int& sol, const wchar_t* str) {

	//ローカル変数
	wchar_t *ptr, *start, *end;				//演算式ポインターと切り取り用ポインター
	int opeval = 0;							//式評価用オペランド値
	int opec = 0;							//オペコード（*は1、/は2、%は3：初期値は何もしない0）
	CPOS operand;							//文字列切り取り用CPOSインスタンス
	//初期化
	sol = 0;								//解(solution)が入る参照変数を初期化
	ptr = (wchar_t*)str;					//ポインターを引数文字列の起点に初期化
	end = start = nullptr;					//切り取りポインターの初期化
	//ループ処理
	while(*ptr != L'\0') {					//ヌル終端に至るまで
		//空白処理
		if(iswspace(*ptr)) {
			if(start && !end)				//startは設定され、endは設定されていない場合
				end = ptr - 1;				//endを空白文字直前に設定
			ptr++;							//空白文字をスキップ
		}
		//ポインターの指す文字により処理を振り分ける
		else {
			switch(*ptr) {
			case L'*':						//'*'の場合
			case L'/':						//'/'の場合
			case L'%':						//'%'の場合
				if(*start == L'('){				//startが'('であれば
					start++;					//startを'('の次に（endは')'の直前）進める
					operand.Set(start, end);	//オペランド文字列（含括弧）の切り取り
					if(!TryCalc(opeval, operand.Get()))	//再帰処理
						return false;					//処理不能であればエラー
				}
				else {
					if(!end)					//endが未設定であれば
						end = ptr - 1;			//'*'、'/'、'%'演算子の直前に設定
					operand.Set(start, end);	//オペランド文字列（含括弧）の切り取り
					if(!TryParse(opeval, operand.Get()))	//整数値でない場合
						return false;						//処理不能であればエラー
				}
				if(opec == 0)					//初期値の場合
					sol = opeval;				//第1オペランドであるsolに代入する
				else if(opec == 1)				//'*'の場合
					sol *= opeval;				//乗算、
				else if(opec == 2)				//'/'の場合
					sol /= opeval;				//除算または
				else if(opec == 3)				//'%'の場合
					sol %= opeval;				//剰余算を行う
				if(*ptr == L'*')				//新しいオペコードを設定
					opec = 1;					//'*'の設定
				else if(*ptr == L'/')
					opec = 2;					//'/'の設定
				else if(*ptr == L'%')
					opec = 3;					//'%'の設定
				ptr++;
				end = start = nullptr;			//切り取りポインターの初期化
				break;
			default:						//'*'、'/'、'%'、'('以外の非空白文字
				if(!start)						//startが未設定（nullptr）であれば
					start = ptr;				//非空白文字の先頭に設定
				else if(start && end)			//start、end共に設定済で演算子以外の場合
					return false;				//構文エラー
				if(*ptr == L'(') {				//括弧の場合
					ptr = SkipBraces(ptr);		//括弧をスキップし、ポインターは')'の次を指す
					if(!ptr)					//括弧が閉じられていなければ（ptr == nullptr）
						return false;			//構文エラー
					end = ptr - 2;				//endに')'の前のアドレスを記録
				}
				else
					ptr++;						//ポインターを進める
				break;
			}
		}
		//ポインターを進めてヌル終端になった場合（ラップアップ処理）
		if(*ptr == L'\0') {
			if(*start == L'('){				//startが'('であれば
				start++;					//startを'('の次に（endは')'の直前）進める
				operand.Set(start, end);	//オペランド文字列（含括弧）の切り取り
				if(!TryCalc(opeval, operand.Get()))	//再帰処理
					return false;					//処理不能であればエラー
			}
			else {
				if(!end)					//endが未設定であれば
					end = ptr - 1;			//'*'、'/'、'%'演算子の直前に設定
				operand.Set(start, end);	//オペランド文字列（含括弧）の切り取り
				if(!TryParse(opeval, operand.Get()))	//整数値でない場合
					return false;						//処理不能であればエラー
			}
			if(opec == 0)					//初期値の場合
				sol = opeval;				//第1オペランドであるsolに代入する
			else if(opec == 1)				//'*'の場合
				sol *= opeval;				//乗算、
			else if(opec == 2)				//'/'の場合
				sol /= opeval;				//除算または
			else if(opec == 3)				//'%'の場合
				sol %= opeval;				//剰余算を行う
		}
	}
	return true;
}

//////////////////////////////////////////
//文字列の整数化関数
//2、16または10進数文字列から整数値を返す
//先頭詞：2進数は0B||0b、16進数は0X||0x
//////////////////////////////////////////
bool CALC::TryParse(int& sol, const wchar_t* str) {

	sol = 0;									//solを初期化する
	int len = wcslen(str);						//strの長さ-1（NULL文字非算入）
	if(!len)									//strが空文字列なら（""）
		return false;							//エラーとする
	////////////////////////////////////////////////////////////////////////////////
	//int head = 0;								//整数先頭アドレス（既定値は10進数）
	//int base = 10;							//基数（底）（既定値は10進数）
	//wchar_t max = L'9';						//エラー用最大値（既定値は10進数）
	//2進数はhead = 2, base = 2, max = L'1'とすればループ処理を共通化できる
	////////////////////////////////////////////////////////////////////////////////
	//2進数（0Bまたは0b）文字列から整数値を返す
	if(str[0] == L'0' && (str[1] == L'B' || str[1] == L'b')) {
		for(int i = 2; i < len; i++) {
			if(str[i] < L'0' || str[i] > L'1')	//2進数以外の文字があればエラー
				return false;
			sol = sol * 2 + (str[i] - L'0');
		}
	}
	//16進数（0Xまたは0x）文字列から整数値を返す
	else if(str[0] == L'0' && (str[1] == L'X' || str[1] == L'x')) {
		for(int i = 2; i < len; i++) {
			if(str[i] < L'0' || (str[i] > L'9' && str[i] < L'A') || (str[i] > L'F' && str[i] <L'a') || str[i] > L'f')
				return false;					//16進数以外の文字があればエラー
			if(str[i] >= L'0' && str[i] <= L'9')
				sol = sol * 16 + (str[i] - L'0');
			else if(str[i] >= L'A' && str[i] <= L'F')
				sol = sol * 16 + (str[i] - L'A' + 10);
			else
				sol = sol * 16 + (str[i] - L'a' + 10);
		}
	}
	//10進数文字列から整数値を返す
	else {
		for(int i = 0; i < len; i++) {
			if(str[i] < L'0' || str[i] > L'9')	//10進数以外の文字があればエラー
				return false;
			sol = sol * 10 + (str[i] - L'0');
		}
	}
	return true;
}

////////////////////////////////////////////
//括弧をスキップし、次のアドレスを返す
////////////////////////////////////////////
wchar_t* CALC::SkipBraces(wchar_t* str) {

	wchar_t* ptr = str;			//str文字列のpointer
	if(*ptr != L'(')			//文字列引数がL')'で始まらない場合
		return 0;				//エラー
	else {
		++ptr;					//ポインターを一つ進める
		while(*ptr != L'\0' &&	//文字列列が終了せず
				*ptr != L')') {			//L')'でもなければ
			if(*ptr == L'(')			//多重括弧は再帰的に処理し
				ptr = SkipBraces(ptr);	//括弧の次から進める
			else
				ptr++;					//ptrを一つ進める
		}
		if(*ptr == L'\0')		//括弧が閉じられずに文字列が終了
			return nullptr;		//エラー
		return ++ptr;			//括弧の次のアドレスを返す（含む終端文字）
	}
}
