using System; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using System.Threading; namespace RobvanderWoude { class ClCalc { static readonly string progver = "2.01"; static string expression; // calculated end result of input expression static string expressionstring; // corrected uncalculated input expression static SortedList functions; // list of available Math functions static bool debugmode = false; // in debug mode, intermediate values for expression and expressionstring are shown static bool error = false; static int Main( string[] args ) { #if DEBUG debugmode = true; #endif #region Initialize // force decimal dots only, instead of a mixture of dots and commas Thread.CurrentThread.CurrentCulture = new CultureInfo( "en-US" ); CreateFunctionsList( ); #endregion Initialize #region Parse Command Line if ( args.Contains( "/?" ) ) { return ShowHelp( ); } // join all arguments into a single string expression expression = string.Join( " ", args ).Trim( ); if ( Regex.IsMatch( expression, "/DEBUG", RegexOptions.IgnoreCase ) ) { debugmode = true; expression = Regex.Replace( expression, @"\s?/DEBUG\s?", " ", RegexOptions.IgnoreCase ).Trim( ); } if ( string.IsNullOrWhiteSpace( expression ) ) { return InteractiveConsole( ); } expressionstring = expression; DebugOutput( "Initial input" ); #endregion Parse Command Line EvaluateExpression( ); // Display result Console.Error.Write( "{0} = ", expressionstring ); Console.WriteLine( "{0}", expression ); // Exit program int rc = 0; int.TryParse( Calculate( string.Format( "Math.Round( {0} )", expression ) ), out rc ); return rc; } public static string Calculate( string exp ) { // Based on code by Reed Copsey, Jr // https://social.msdn.microsoft.com/Forums/vstudio/en-US/2f737e51-bc99-4dc1-9a01-f0a4f7e8aa64/how-to-get-a-list-of-math-methods?forum=csharpgeneral#2af1c643-689a-4469-ad3c-20a3c8b6a24e-isAnswer Regex regex; double result; #region RegEx Patterns // Math function with single numeric parameter (double) string pattern1 = @"Math\.(\w+)\(\s*([-\d][\d\.]*)\s*\)"; // Math function with two numeric parameters (both double) string pattern2 = @"Math\.(\w+)\(\s*([-\d][\d\.]*)\s*,\s*([-\d][\d\.]*)\s*\)"; // Random function without parameters string patternrandom0 = @"(?:Math\.)?Random\(\s*\)"; // Random function with 1 parameter string patternrandom1 = @"(?:Math\.)?Random\(\s*([-\d][\d\.]*)\s*\)"; // Random function with 2 parameters string patternrandom2 = @"(?:Math\.)?Random\(\s*([-\d][\d\.]*)\s*,\s*([-\d][\d\.]*)\s*\)"; // Math.Round with 2 numeric parameters (double, int) string patternround2 = @"Math\.Round\(\s*([-\d][\d\.]*)\s*,\s*(\d+)\s*\)"; // Math.BigMul with 2 int parameters string patternbigmul = @"Math\.BigMul\(\s*(\d+)\s*,\s*(\d+)\s*\)"; #endregion RegEx Patterns if ( Regex.IsMatch( exp, @"Random\(", RegexOptions.IgnoreCase ) ) { #region Random int seed = DateTime.Now.Millisecond; Random random = new Random( seed ); if ( Regex.IsMatch( exp, patternrandom0, RegexOptions.IgnoreCase ) ) { // Random function without parameters regex = new Regex( patternrandom0, RegexOptions.IgnoreCase ); MatchCollection matches = regex.Matches( exp ); foreach ( Match match in matches ) { return random.NextDouble( ).ToString( new CultureInfo( "en-US" ) ); } } else if ( Regex.IsMatch( exp, patternrandom1, RegexOptions.IgnoreCase ) ) { // Random function with 1 parameter regex = new Regex( patternrandom1, RegexOptions.IgnoreCase ); MatchCollection matches = regex.Matches( exp ); foreach ( Match match in matches ) { if ( int.TryParse( match.Groups[1].Value, out int upperlimit ) ) { return random.Next( upperlimit ).ToString( ); } } } else if ( Regex.IsMatch( exp, patternrandom2, RegexOptions.IgnoreCase ) ) { // Random function with 2 parameters regex = new Regex( patternrandom2, RegexOptions.IgnoreCase ); MatchCollection matches = regex.Matches( exp ); foreach ( Match match in matches ) { if ( int.TryParse( match.Groups[1].Value, out int lowerlimit ) && int.TryParse( match.Groups[2].Value, out int upperlimit ) && upperlimit > lowerlimit ) { return random.Next( lowerlimit, upperlimit ).ToString( ); } } } } else if ( Regex.IsMatch( exp, patternround2, RegexOptions.IgnoreCase ) ) { // Math.Round with 2 numeric parameters (double, int) regex = new Regex( patternround2, RegexOptions.IgnoreCase ); MatchCollection matches = regex.Matches( exp ); foreach ( Match match in matches ) { if ( double.TryParse( match.Groups[1].Value, out double val ) && int.TryParse( match.Groups[2].Value, out int decimals ) ) { Type[] types = { typeof( double ), typeof( int ) }; var methodInfo = typeof( Math ).GetMethod( "Round", types ); object[] valobj = { val, decimals }; result = (double)methodInfo.Invoke( null, valobj ); return result.ToString( new CultureInfo( "en-US" ) ); } } #endregion Random } else if ( Regex.IsMatch( exp, patternbigmul, RegexOptions.IgnoreCase ) ) { #region BigMul // Math.BigMul with 2 int parameters regex = new Regex( patternbigmul, RegexOptions.IgnoreCase ); MatchCollection matches = regex.Matches( exp ); foreach ( Match match in matches ) { if ( int.TryParse( match.Groups[1].Value, out int val1 ) && int.TryParse( match.Groups[2].Value, out int val2 ) ) { Type[] types = { typeof( int ), typeof( int ) }; var methodInfo = typeof( Math ).GetMethod( "BigMul", types ); object[] valobj = { val1, val2 }; result = (Int64)methodInfo.Invoke( null, valobj ); return result.ToString( new CultureInfo( "en-US" ) ); } } #endregion BigMul } else if ( Regex.IsMatch( exp, pattern1, RegexOptions.IgnoreCase ) ) { #region Math Single Parameter // Math function with single numeric parameter (double) regex = new Regex( pattern1, RegexOptions.IgnoreCase ); MatchCollection matches = regex.Matches( exp ); foreach ( Match match in matches ) { string func = match.Groups[1].Value; if ( double.TryParse( match.Groups[2].Value, out double val ) ) { Type[] types = { typeof( double ) }; var methodInfo = typeof( Math ).GetMethod( func, types ); if ( methodInfo != null ) { object[] valobj = { val }; result = (double)methodInfo.Invoke( null, valobj ); return result.ToString( new CultureInfo( "en-US" ) ); } error = true; return "Unknown Function Math." + func; } } #endregion Math Single Parameter } else if ( Regex.IsMatch( exp, pattern2, RegexOptions.IgnoreCase ) ) { #region Math Two Parameters // Math function with two numeric parameters (both double) regex = new Regex( pattern2, RegexOptions.IgnoreCase ); MatchCollection matches = regex.Matches( exp ); foreach ( Match match in matches ) { string func = match.Groups[1].Value; if ( double.TryParse( match.Groups[2].Value, out double val1 ) && double.TryParse( match.Groups[3].Value, out double val2 ) ) { Type[] types = { typeof( double ), typeof( double ) }; var methodInfo = typeof( Math ).GetMethod( func, types ); if ( methodInfo != null ) { object[] valobj = { val1, val2 }; result = (double)methodInfo.Invoke( null, valobj ); return result.ToString( new CultureInfo( "en-US" ) ); } error = true; return "Unknown Function Math." + func; } } #endregion Math Two Parameters } else { #region Numerical Expressions // use JScript to evaluate numerical expressions with standard operators try { result = JScriptEvaluator.EvalToDouble( exp ); return result.ToString( new CultureInfo( "en-US" ) ); } catch ( Exception ) { // no action } #endregion Numerical Expressions } return string.Empty; } public static void CreateFunctionsList( ) { var methods = typeof( Math ).GetMethods( BindingFlags.Public | BindingFlags.Static ).Select( m => m.Name ).ToList( ); methods = methods.Distinct( ).ToList( ); methods.Remove( "DivRem" ); // this function cannot be implemented easily on the command line functions = new SortedList( ); foreach ( string method in methods ) { functions.Add( method, method ); } // for JScript Math compatibility functions.Add( "Ceil", "Ceiling" ); functions.Add( "Int", "Truncate" ); } public static void DebugOutput( string description ) { if ( debugmode ) { Console.Error.Write( "{0}:\t{1} = ", description, expressionstring ); Console.WriteLine( "{0}", expression ); } } public static void EnvVarSubst( ) { string pattern = @"(?|&/%+]*)\)"; regex = new Regex( pattern, RegexOptions.IgnoreCase ); if ( regex.IsMatch( expression ) ) { matches = regex.Matches( expression ); completed = Regex.IsMatch( matches[0].Groups[1].Value, @"^\s*[-\d][\d\.]*\s*$" ); matches = regex.Matches( expression ); foreach ( Match match in matches ) { expression = expression.Replace( match.Value, string.Format( "( {0} )", Calculate( match.Groups[1].Value ) ) ); DebugOutput( "Numeric only" ); } } #endregion Numeric Only #region Random Calls // Evaluate all Random calls with numeric parameters only string patternrandom0 = @"Random\((\s*)\)"; regex = new Regex( patternrandom0, RegexOptions.IgnoreCase ); int seed = DateTime.Now.Millisecond; Random random = new Random( seed ); if ( regex.IsMatch( expression ) ) { completed = false; matches = regex.Matches( expression ); foreach ( Match match in matches ) { expression = expression.Replace( match.Value, random.NextDouble( ).ToString( new CultureInfo( "en-US" ) ) ); DebugOutput( "Random 0 params" ); } } string patternrandom1 = @"Random\(\s*(\d+)\s*\)"; regex = new Regex( patternrandom1, RegexOptions.IgnoreCase ); if ( regex.IsMatch( expression ) ) { completed = false; matches = regex.Matches( expression ); foreach ( Match match in matches ) { if ( int.TryParse( match.Groups[1].Value, out int singleparam ) ) { expression = expression.Replace( match.Value, Calculate( string.Format( "Random( {0} )", singleparam ) ) ); } DebugOutput( "Random 1 param" ); } } string patternrandom2 = @"Random\(\s*(\d+)\s*,\s*(\d+)\s*\)"; regex = new Regex( patternrandom2, RegexOptions.IgnoreCase ); if ( regex.IsMatch( expression ) ) { completed = false; matches = regex.Matches( expression ); foreach ( Match match in matches ) { if ( int.TryParse( match.Groups[1].Value, out int lowerlimit ) && int.TryParse( match.Groups[2].Value, out int upperlimit ) ) { expression = expression.Replace( match.Value, Calculate( string.Format( "Random( {0}, {1} )", lowerlimit, upperlimit ) ) ); } DebugOutput( "Random 2 params" ); } } #endregion Random Calls #region Math Calls With Numeric Parameters // Evaluate all Math functions with numeric parameters only pattern = @"Math\.(\w+)\(([^\(\)]+)\)"; regex = new Regex( pattern, RegexOptions.IgnoreCase ); if ( regex.IsMatch( expression ) ) { completed = false; matches = regex.Matches( expression ); foreach ( Match match in matches ) { expression = expression.Replace( match.Value, Calculate( match.Value ) ); DebugOutput( "Math numeric" ); } } #endregion Math Calls With Numeric Parameters } while ( !completed && !error ); // loop must be executed at least once without ANY match for both RegExs #endregion Evaluate Expressions Loop #region Final Evaluation if ( !error ) { expression = Calculate( expression ); DebugOutput( "Final run" ); } #endregion Final Evaluation } } public static int InteractiveConsole( ) { expression = string.Empty; bool debugprompt = true; bool quit = false; while ( !quit ) { #region Prompt if ( debugprompt ) { Console.ForegroundColor = ConsoleColor.DarkYellow; Console.WriteLine( "Debug mode is {0}", debugmode ? "ON" : "OFF" ); Console.WriteLine( "Type DEBUG and press Enter to toggle debug mode." ); debugprompt = false; } Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine( "\nType an expression to calculate, or ? for help, or Q to quit, and press Enter:\n" ); Console.Write( "CLCALC:> " ); Console.ResetColor( ); #endregion Prompt // Read keyboard input expression = Console.ReadLine( ); // "Q" for Quit if ( expression.ToUpper( ).Trim( ) == "Q" ) { quit = true; } else { Console.WriteLine( ); // "?" or "HELP" for help on screen if ( expression.ToUpper( ).Trim( ) == "HELP" || expression.Trim( ) == "?" ) { ShowHelpInteractive( ); } else if ( !error ) { #region Toggle DEBUG Mode // "DEBUG" to toggle debug mode if ( expression.ToUpper( ).Contains( "DEBUG" ) ) { debugmode = !debugmode; // toggle debug mode expression = string.Empty; expressionstring = string.Empty; Console.ForegroundColor = ConsoleColor.DarkYellow; Console.WriteLine( "Debug mode is {0}", debugmode ? "ON" : "OFF" ); Console.WriteLine( "Type DEBUG and press Enter to toggle debug mode." ); Console.ResetColor( ); } #endregion Toggle DEBUG Mode if ( !string.IsNullOrWhiteSpace( expression ) ) { expressionstring = expression; // in interactive mode, we don't have the "automatic" evaluation of environment // variables like we have on the command line, so we need to handle that ourselves EnvVarSubst( ); EvaluateExpression( ); } } error = false; if ( !string.IsNullOrWhiteSpace( expressionstring ) && !string.IsNullOrWhiteSpace( expression ) ) { Console.WriteLine( "\n{0} = {1}\n", expressionstring, expression ); } } } return 0; } public static void ListFunctions( ) { List funcs = new List( ); foreach ( string function in functions.Keys ) { if ( function == functions[function] ) { funcs.Add( string.Format( "Math.{0}", function ) ); } else { funcs.Add( string.Format( "Math.{0} *", function ) ); } } funcs.Add( "Random *" ); funcs.Sort( ); for ( int i = 0; i < funcs.Count; i++ ) { if ( i % 4 == 3 ) { Console.Error.WriteLine( funcs[i] ); } else { Console.Error.Write( "{0,-20}", funcs[i] ); } } } public static string RequiredDotNETVersion( ) { // Get the required .NET Framework version // By Fernando Gonzalez Sanchez on StackOverflow.com // https://stackoverflow.com/a/18623516 object[] list = Assembly.GetExecutingAssembly( ).GetCustomAttributes( true ); var attribute = list.OfType( ).First( ); // requires Linq return attribute.FrameworkDisplayName; } public static int ShowHelp( params string[] errorMessage ) { #region Error Message if ( errorMessage.Length > 0 ) { List errargs = new List( errorMessage ); errargs.RemoveAt( 0 ); Console.Error.WriteLine( ); Console.ForegroundColor = ConsoleColor.Red; Console.Error.Write( "ERROR:\t" ); Console.ForegroundColor = ConsoleColor.White; Console.Error.WriteLine( errorMessage[0], errargs.ToArray( ) ); Console.ResetColor( ); } #endregion Error Message #region Help Text Console.Error.Write( string.Format( @" ClCalc.exe, Version {0} Command Line Calculator Usage: CLCALC [ expression ] [ /DEBUG ] Run the program without expression to open an interactive console. Available Operators: ==================== + - * / % << >> & | Available Constants: ==================== Math.PI (pi) and Math.E (e) Available Functions: ==================== ", progver ) ); ListFunctions( ); Console.Error.Write( string.Format( @" * Math.Ceil is equivalent of Math.Ceiling, Math.Int of Math.Truncate; these equivalents were created for backwards compatibility with ClCalc 1.* Random is an added function, not part of the .NET System.Math class but of the .NET System.Random class. Function names are not case sensitive, but constants e and pi are. The Math. prefix is not mandatory in the input, except for upper case Math.E and Math.PI. For more details on functions and parameters, navigate to: https://learn.microsoft.com/en-us/dotnet/api/system.math?view=netframework-4.5 EXAMPLES ======== Expression: Evaluates To: Remark: =========== ============= ======= {0}%var1% & %var2%{0} 0 or 1 AND operator {0}%var1% | %var2%{0} 0 or 1 OR operator {0}16 >> 1{0} 8 shift right {0}16 << 2{0} 64 shift left {0}16 % 3{0} 1 use whitespace! Math.Abs( -12 ) 12 Math.Acos( 0.707106781186548 ) 0.785398163397448 Math.Asin( 0.707106781186548 ) 0.785398163397448 Math.Atan( 1 ) 0.785398163397448 Math.Atan2( 1, 1 ) 0.785398163397448 Math.BigMul( 2147483647, 2147483647 ) 4.61168601413242E+18 Int64 result Math.Ceiling( Math.PI ) 4 round up to int Math.Cos( pi / 4 ) 0.707106781186548 Math.Cosh( 1 ) 1.54308063481524 Math.Exp( -1 ) * Math.E 1 Math.Floor( Math.E ) 2 round down to int Math.IEEERemainder( 4, 3 ) 1 like {0}4 % 3{0} Math.IEEERemainder( 5, 3 ) -1 unlike {0}5 % 3{0} Math.Log( Math.E ) 1 natural logarithm Math.Log10( 1000 ) 3 base 10 logarithm Math.Max( -10, -12 ) -10 Math.Min( -10, -12 ) -12 Math.Pow( 2, 8 ) 256 Math.Round( 12.5 ) 13 Math.Sign( -3 ) -1 Math.Sin( pi / 4 ) 0.707106781186548 Math.Sinh( 1.5 ) 2.12927945509482 Math.Sqrt( 9 ) 3 Math.Tan( pi / 4 ) 1 Math.Tanh( 2 ) 0.964027580075817 Math.Truncate( -12.6 ) -12 Random( ) 0 <= result < 1 floating point Random( 100 ) 0 <= result < 100 integer Random( 1, 101 ) 1 <= result < 101 integer pi / Math.Acos( Math.Pow(0.5, 0.5) ) 4 nested Math calls Notes: ====== In non-interactive mode, the result is shown on screen and the exit code ({0}errorlevel{0}) equals the rounded value of result, or 0 in case of error or overflow. In non-interactive mode, the result is shown in Standard Output stream, and the initial expression in Standard Error stream, to allow capturing or redirection of the result only. In interactive console mode type DEBUG to toggle debug mode. If expression contains special characters like < or > or | or & it is highly recommended to enclose the entire expression in doublequotes and use whitespace around these operators, e.g. {0}16 << 1{0}. Available constants are case sensitive, function names are not. {0}Math.PI{0} may be abbreviated to {0}pi{0} (lower case), {0}Math.E{0} to {0}e{0}. In ClCalc all Math functions are limited to 2 parameters at most, usually of types Double or Int32. This program requires {1}. Culture is set to en-US, so use and expect decimal dots, not commas. See this web page for methods available in .NET's Math class: https://learn.microsoft.com/en-us/dotnet/api/system.math?view=netframework-4.5 Credits: ======== Originally based on Eval function (using JScript) by {0}markman{0}: https://www.codeproject.com/Articles/11939/Evaluate-C-Code-Eval-Function Written by Rob van der Woude https://www.robvanderwoude.com ", (char)34, RequiredDotNETVersion( ) ) ); #endregion Help Text return 0; } public static void ShowHelpInteractive( ) { Console.WriteLine( "\nAvailable Operators:" ); Console.WriteLine( "====================" ); Console.WriteLine( "+ - * / % << >> & |\n" ); Console.WriteLine( "Available Constants:" ); Console.WriteLine( "====================" ); Console.WriteLine( "Math.PI (pi) and Math.E (e)\n" ); Console.WriteLine( "Available Functions:" ); Console.WriteLine( "====================" ); ListFunctions( ); Console.WriteLine( "\nFunction names are not case sensitive, but constants e and pi are." ); Console.WriteLine( "The Math. prefix is not mandatory in the input, except for upper case" ); Console.WriteLine( "Math.E an Math.PI." ); Console.WriteLine( "\nFor more details, run CLCALC with the /? command line switch." ); } } public class JScriptEvaluator { // Eval function using JScript, by "markman" // https://www.codeproject.com/Articles/11939/Evaluate-C-Code-Eval-Function public static int EvalToInteger( string statement ) { string s = EvalToString( statement ); return int.Parse( s.ToString( ) ); } public static double EvalToDouble( string statement ) { string s = EvalToString( statement ); return double.Parse( s ); } public static string EvalToString( string statement ) { object o = EvalToObject( statement ); return o.ToString( ); } public static object EvalToObject( string statement ) { return _evaluatorType.InvokeMember( "Eval", BindingFlags.InvokeMethod, null, _evaluator, new object[] { statement } ); } static JScriptEvaluator( ) { CodeDomProvider provider = new Microsoft.JScript.JScriptCodeProvider( ); CompilerParameters parameters; parameters = new CompilerParameters { GenerateInMemory = true }; CompilerResults results; results = provider.CompileAssemblyFromSource( parameters, _jscriptSource ); Assembly assembly = results.CompiledAssembly; _evaluatorType = assembly.GetType( "Evaluator.Evaluator" ); _evaluator = Activator.CreateInstance( _evaluatorType ); } private static object _evaluator = null; private static Type _evaluatorType = null; private static readonly string _jscriptSource = @"package Evaluator { class Evaluator { public function Eval(expr : String) : String { return eval(expr); } } }"; } }