////////////////////////////////////////////////
// Calc Class - Evaluates String Expression and
// retruns end-result (integer value).
////////////////////////////////////////////////
/*
【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"をつける必要がある。

【参考サイト】
String Class - https://learn.microsoft.com/ja-jp/dotnet/api/system.string?view=net-8.0
Chars[Int32] プロパティ
Length プロパティ
Substring(Int32 pos, Int32 len)

Char Class - https://learn.microsoft.com/ja-jp/dotnet/api/system.char?view=net-8.0
IsWhiteSpaceメソッド

Int32.TryParseメソッド
https://learn.microsoft.com/ja-jp/dotnet/api/system.int32.tryparse?view=net-9.0#system-int32-tryparse(system-string-system-globalization-numberstyles-system-iformatprovider-system-int32@)
*/
using System;
using System.Globalization;		//NumberStyle使用の為（オリジナルコードであれば不要）

namespace Calc_Int
{
	public class Calc
	{
		//クラス内定数
		const int ERR = -1;
		//メンバー変数
		public string Formula {get; set;}			//演算式記録用

		/////////////////////
		//コンストラクター
		/////////////////////
		public Calc(string str = "")
		{
			if(str != string.Empty)
				Formula = str;						//strをFormulaにコピー
		}

		///////////////////////////////////////////////
		//演算制御を行う主関数（+または-の処理を行う）
		//演算に成功するとtrue、失敗するとfalseを返す。
		//参照変数sol(ution)に解が入る。
		///////////////////////////////////////////////
		public bool TryCalc(ref int sol, string str = "")
		{
			//ローカル変数
			int ptr, start, end;					//順に演算式用、切り取り用ポインター
			int opeval = 0;							//式評価用オペランド値
			int opec = 0;							//オペコード（+は1、-は2）
			bool post_opec = false;					//「オペコード後」サイン
			string operand;							//文字列切り取り用
			//初期化
			sol = 0;								//解(solution)が入る参照変数を初期化
			if(str == string.Empty)					//strが既定値の空文であれば
				str = Formula;						//計算式（str）をFormulaとする
			else									//strに指定があれば
				Formula = str;						//strをFormulaにコピー
			ptr = 0;								//ポインターを演算式文字列の起点に初期化
			end = start = ERR;						//切り取りポインター初期化（値は0～str.Length - 1）
			//ループ処理
			for(ptr = 0; ptr < str.Length; ptr++)	//strの最後まで継続（C#はヌル終端でない為、文字列長で判断）
			{
				//空白処理
				if(Char.IsWhiteSpace(str, ptr))
				{
					if(start > ERR && end == ERR)	//startは設定され、endは設定されていない場合
					end = ptr - 1;					//endを空白文字直前に設定
				}
				//ポインターの指す文字により処理を振り分ける
				else
				{
					switch(str[ptr])
					{
					case '+':						//'+'の場合
					case '-':						//'-'の場合
						if(post_opec)					//オペレーター後にオペレーターが続けば
							return false;				//エラー
						if(start == ERR)				//単項演算（start == ERR）の場合
							start = ptr + 1;			//startを+-符号(Chars[0])の次に進める
						else							//二項演算で
						{
							if(end == ERR)				//オペランド未設定（end == ERR）ならば
								end = ptr - 1;			//'+'、'-'演算子の直前に設定
							//文字列切り取り－string.Substring(int startIndex, int length);
							operand = str.Substring(start, end - start + 1);
							if(!TryParse(ref opeval, operand))		//整数値でない場合
							{
								if(!TryMulDiv(ref opeval, operand))	//乗除演算が不可能
									return false;		//エラー
							}
						}
						if(opec == 0)					//初期値の場合（単項式の場合
							sol = opeval;				//第1オペランドであるsolに代入する
						else if(opec == 1)				//'+'の場合
							sol += opeval;				//（単項演算子を含む）加算または
						else if(opec == 2)				//'-'の場合
							sol -= opeval;				//（単項演算子を含む）減算を行う
						if(str[ptr] == '+')				//新しいオペコードをopecに設定
							opec = 1;					//'+'の設定
						else if(str[ptr] == '-')
							opec = 2;					//'-'の設定
						end = start = ERR;				//切り取りポインターの初期化
						post_opec = true;				//オペレーター後フラグ
						break;
					case '*':						//'*'の場合
					case '/':						//'/'の場合
					case '%':						//'%'の場合
						if(start == ERR)				//最初に'*'、'/'、'%'があれば
							return false;				//エラー
						if(post_opec)					//オペレーター後にオペレーターが続けば
							return false;				//エラー
						end = start;					//end == start（defaultでの処理の為）にして
						post_opec = true;				//オペレーター後フラグ
						break;
					default:						//'+'、'-'、'*'、'/'、'%'以外の非空白文字
						if(start == ERR)				//startが未設定であれば
							start = ptr;				//非空白文字の先頭に設定
						else if(start == end)			//'*'、'/'、'%'があった場合の次の非空白文字
							end = ERR;					//オペランド未設定（end == nullptr）に戻す
						else if(start > ERR && end > ERR)	//start、end共に設定済で演算子以外の場合
							return false;				//構文エラー
						if(str[ptr] == '(')				//括弧の場合
						{
							if(!SkipBraces(str, ref ptr))//括弧をスキップし、ポインターは')'を指す
								return false;			//括弧が閉じられていなければ構文エラー
							end = ptr;					//endも')'を指す
						}
						post_opec = false;				//オペランド後なので、オペレーター後フラグを戻す
						break;
					}
				}
			}
			//ポインターを進めて終端になった場合（+-のラップアップ処理）
			if(post_opec)						//オペレーター後にオペレーターが続けば
				return false;					//エラー
			if(end == ERR)						//endが未設定であれば
				end = ptr - 1;					//終端の直前に設定
			//文字列切り取り－string.Substring(int startIndex, int length);
			operand = str.Substring(start, end - start + 1);
			if(!TryParse(ref opeval, operand))		//整数値でない場合
			{
				if(!TryMulDiv(ref opeval, operand))	//乗除演算が不可能
					return false;				//エラー
			}
			if(opec == 0)						//初期値の場合
				sol = opeval;					//第1オペランドであるsolに代入する
			else if(opec == 1)					//'+'の場合
				sol += opeval;					//加算または
			else if(opec == 2)					//'-'の場合
				sol -= opeval;					//減算を行う
			return true;
		}

		////////////////
		//乗除演算関数
		////////////////
		private bool TryMulDiv(ref int sol, string str)
		{
			//ローカル変数
			int ptr, start, end;					//順に演算式用、切り取り用ポインター
			int opeval = 0;							//式評価用オペランド値
			int opec = 0;							//オペコード（*は1、/は2、%は3）
			string operand;							//文字列切り取り用
			//初期化
			sol = 0;								//解(solution)が入る参照変数を初期化
			ptr = 0;								//ポインターを演算式文字列の起点に初期化
			end = start = ERR;						//切り取りポインター初期化（値は0～str.Length - 1）
			//ループ処理
			for(ptr = 0; ptr < str.Length; ptr++)	//strの最後まで継続（C#はヌル終端でない為、文字列長で判断）
			{
				//空白処理
				if(Char.IsWhiteSpace(str, ptr))
				{
					if(start >= 0 && end < 0)		//startは設定され、endは設定されていない場合
					end = ptr - 1;					//endを空白文字直前に設定
				}
				//ポインターの指す文字により処理を振り分ける
				else
				{
					switch(str[ptr])
					{
					case '*':						//'*'の場合
					case '/':						//'/'の場合
					case '%':						//'%'の場合
						if(str[start] == '(')			//startが'('であれば
						{
							start++;					//startを'('の次に（endは')'の直前）進める
							//文字列切り取り－string.Substring(int startIndex, int length);
							operand = str.Substring(start, end - start + 1);
							if(!TryCalc(ref opeval, operand))	//再帰処理
								return false;			//処理不能であればエラー
						}
						else
						{
							if(end < 0)					//オペランド未設定（end == ERR）ならば
								end = ptr - 1;			//'*'、'/'、'%'演算子の直前に設定
							//文字列切り取り－string.Substring(int startIndex, int length);
 							operand = str.Substring(start, end - start + 1);
							if(!TryParse(ref opeval, operand))	//整数値でない場合
								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(str[ptr] == '*')				//新しいオペコードを設定
							opec = 1;					//'*'の設定
						else if(str[ptr] == '/')
							opec = 2;					//'/'の設定
						else if(str[ptr] == '%')
							opec = 3;					//'%'の設定
						end = start = ERR;				//切り取りポインターの初期化
						break;
					default:						//'*'、'/'、'%'以外の非空白文字
						if(start < 0)					//startが未設定であれば
							start = ptr;				//非空白文字の先頭に設定
						else if(start >= 0 && end >= 0)	//start、end共に設定済で演算子以外の場合
							return false;				//構文エラー
						if(str[ptr] == '(')				//括弧の場合
						{
							if(!SkipBraces(str, ref ptr))	//括弧をスキップし、ポインターは')'を指す
								return false;			//括弧が閉じられていなければ構文エラー
							end = ptr - 1;				//endに')'の前のアドレスを記録
						}
						break;
					}
				}
			}
			//ポインターを進めて終端になった場合（ラップアップ処理）
			if(str[start] == '(')					//startが'('であれば
			{
				start++;							//startを'('の次に（endは')'の直前）進める
				//文字列切り取り－string.Substring(int startIndex, int length);
				operand = str.Substring(start, end - start + 1);
				if(!TryCalc(ref opeval, operand))	//再帰処理
					return false;					//処理不能であればエラー
			}
			else
			{
				if(end < 0)							//オペランド未設定（end == ERR）ならば
					end = ptr - 1;					//'*'、'/'、'%'演算子の直前に設定
				//文字列切り取り－string.Substring(int startIndex, int length);
				operand = str.Substring(start, end - start + 1);
				if(!TryParse(ref opeval, operand))	//整数値でない場合
					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
		//////////////////////////////////////////
		private bool TryParse(ref int sol, string str)
		{
			sol = 0;							//solを初期化する
			int len = str.Length;				//strの長さ
			if(len == 0)						//strが空文字列なら（""）
				return false;					//エラーとする
			//2進数（0Bまたは0b）文字列から整数値を返す
			if(str[0] == '0' && (str[1] == 'B' || str[1] == 'b'))
			{
				for(int i = 2; i < len; i++)
				{
					if(str[i] < '0' || str[i] > '1')	//2進数以外の文字があればエラー
						return false;
					sol = sol * 2 + (str[i] - '0');
				}
			/*	if(!int.TryParse(str, NumberStyles.BinaryNumber, new CultureInfo("ja-JP"), out sol))
					return false;
			>>> ChatGPTで確認 <<<
			「NumberStyles.BinaryNumberは、.NET 7以降で新たに導入された機能です。
			そのため、C# 5.0（.NET Framework 4.5に対応）では使用できず、
			コンパイル時に「定義がありません」というエラーが発生します。」
			*/
			}
			//16進数（0Xまたは0x）文字列から整数値を返す
			else if(str[0] == '0' && (str[1] == 'X' || str[1] == 'x'))
			{
				if(!int.TryParse(str.Substring(2), NumberStyles.HexNumber, new CultureInfo("ja-JP"), out sol))
					return false;
			/*	//オリジナルの文字列16進数変換コード
				for(int i = 2; i < len; i++)
				{
					if(str[i] < '0' || (str[i] > '9' &&
						str[i] < 'A') || (str[i] > 'F' &&
						str[i] < 'a') || str[i] > 'f')
						return false;					//16進数以外の文字があればエラー
					if(str[i] >= '0' && str[i] <= '9')
						sol = sol * 16 + (str[i] - '0');
					else if(str[i] >= 'A' && str[i] <= 'F')
						sol = sol * 16 + (str[i] - 'A' + 10);
					else
						sol = sol * 16 + (str[i] - 'a' + 10);
				}
			*/
			}
			//10進数文字列から整数値を返す
			else
			{
				if(!int.TryParse(str, NumberStyles.Number, new CultureInfo("ja-JP"), out sol))
					return false;
			/*	//オリジナルの文字列10進数変換コード
				for(int i = 0; i < len; i++) {
					if(str[i] < '0' || str[i] > '9')	//10進数以外の文字があればエラー
						return false;
					sol = sol * 10 + (str[i] - '0');
				}
			*/
			}
			return true;
		}

		/////////////////////////////////////////////////////
		//（多重）括弧をスキップし、閉じ括弧のアドレスを返す
		/////////////////////////////////////////////////////
		private bool SkipBraces(string str, ref int ptr)
		{
			int len = str.Length;			//引数文字列の長さ
			if(str[ptr] != '(' ||			//文字列引数がL'('で始まらない場合又は
				ptr >= len)					//ptrがstrの長さより大きい場合
				return false;				//エラー
			else
			{
				ptr++;						//'('の次に進める
				while(ptr < len &&			//文字列が終了せず
					str[ptr] != ')')		//')'でもなければ
				{
					if(str[ptr] == '(')		//多重括弧は
					{
						if(!SkipBraces(str, ref ptr))	//再帰的に処理する
							return false;
					}
					ptr++;				//ptrを一つ進める
				}
				if(str[ptr] != ')')			//括弧が閉じられずに文字列が終了
					return false;			//エラー
			}
			return true;					//ptrは')'を指す
		}
	}
}
