using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Base; namespace AppCalc{ public sealed class App : IApp{ private static readonly Regex RegexValidCharacters = new Regex(@"^[\s\d\.\-+*/%^]+$", RegexOptions.Compiled); private static readonly Regex RegexTokenSeparator = new Regex(@"((?<!\d)-?(((\d+)?\.\d+(\.\.\.)?)|\d+))|[^\d\s]", RegexOptions.ExplicitCapture | RegexOptions.Compiled); private static readonly Regex RegexRecurringDecimal = new Regex(@"\b(?:(\d+?\.\d{0,}?)(\d+?)\2+|([\d+?\.]*))\b", RegexOptions.Compiled); private static readonly char[] SplitSpace = { ' ' }; public string[] RecognizedNames => new string[]{ "calc", "calculate", "calculator" }; public MatchConfidence GetConfidence(Command cmd){ return RegexValidCharacters.IsMatch(cmd.Text) ? MatchConfidence.Possible : MatchConfidence.None; } string IApp.ProcessCommand(Command cmd){ return ParseAndProcessExpression(cmd.Text); } private static string ParseAndProcessExpression(string text){ // text = RegexBalancedParentheses.Replace(text, match => " "+ParseAndProcessExpression(match.Groups[1].Value)+" "); // parens are handled as apps string expression = RegexTokenSeparator.Replace(text, match => " "+match.Value+" "); string[] tokens = expression.Split(SplitSpace, StringSplitOptions.RemoveEmptyEntries); decimal result = ProcessExpression(tokens); if (Math.Abs(result-decimal.Round(result)) < 0.0000000000000000000000000010M){ return decimal.Round(result).ToString(CultureInfo.InvariantCulture); } string res = result.ToString(CultureInfo.InvariantCulture); bool hasDecimalPoint = decimal.Round(result) != result; if (res.Length == 30 && hasDecimalPoint && res.IndexOf('.') < 15){ // Length 30 uses all available bytes res = res.Substring(0, res.Length-1); Match match = RegexRecurringDecimal.Match(res); if (match.Groups[2].Success){ string repeating = match.Groups[2].Value; StringBuilder build = new StringBuilder(34); build.Append(match.Groups[1].Value); do{ build.Append(repeating); }while(build.Length+repeating.Length <= res.Length); return build.Append(repeating.Substring(0, 1+build.Length-res.Length)).Append("...").ToString(); } } else if (hasDecimalPoint){ res = res.TrimEnd('0'); } return res; } private static decimal ProcessExpression(string[] tokens){ bool isPostfix; if (tokens.Length < 3){ isPostfix = false; } else{ try{ ParseNumberToken(tokens[0]); }catch(CommandException){ throw new CommandException("Prefix notation is not supported."); } try{ ParseNumberToken(tokens[1]); isPostfix = true; }catch(CommandException){ isPostfix = false; } } if (isPostfix){ return ProcessPostfixExpression(tokens); } else{ return ProcessPostfixExpression(ConvertInfixToPostfix(tokens)); } } private static IEnumerable<string> ConvertInfixToPostfix(IEnumerable<string> tokens){ Stack<string> operators = new Stack<string>(); foreach(string token in tokens){ if (Operators.With2Operands.Contains(token)){ int currentPrecedence = Operators.GetPrecedence(token); bool currentRightAssociative = Operators.IsRightAssociative(token); while(operators.Count > 0){ int topPrecedence = Operators.GetPrecedence(operators.Peek()); if ((currentRightAssociative && currentPrecedence < topPrecedence) || (!currentRightAssociative && currentPrecedence <= topPrecedence)){ yield return operators.Pop(); } else{ break; } } operators.Push(token); } else{ yield return ParseNumberToken(token).ToString(CultureInfo.InvariantCulture); } } while(operators.Count > 0){ yield return operators.Pop(); } } private static decimal ProcessPostfixExpression(IEnumerable<string> tokens){ Stack<decimal> stack = new Stack<decimal>(); foreach(string token in tokens){ decimal operand1, operand2; if (token == "-" && stack.Count == 1){ operand2 = stack.Pop(); operand1 = 0M; } else if (Operators.With2Operands.Contains(token)){ if (stack.Count < 2){ throw new CommandException("Incorrect syntax, not enough numbers in stack."); } operand2 = stack.Pop(); operand1 = stack.Pop(); } else{ operand1 = operand2 = 0M; } switch(token){ case "+": stack.Push(operand1+operand2); break; case "-": stack.Push(operand1-operand2); break; case "*": stack.Push(operand1*operand2); break; case "/": if (operand2 == 0M){ throw new CommandException("Cannot divide by zero."); } stack.Push(operand1/operand2); break; case "%": if (operand2 == 0M){ throw new CommandException("Cannot divide by zero."); } stack.Push(operand1%operand2); break; case "^": if (operand1 == 0M && operand2 == 0M){ throw new CommandException("Cannot evaluate 0 to the power of 0."); } else if (operand1 < 0M && Math.Abs(operand2) < 1M){ throw new CommandException("Cannot evaluate a root of a negative number."); } try{ stack.Push((decimal)Math.Pow((double)operand1, (double)operand2)); }catch(OverflowException ex){ throw new CommandException("Number overflow.", ex); } break; default: stack.Push(ParseNumberToken(token)); break; } } if (stack.Count != 1){ throw new CommandException("Incorrect syntax, too many numbers in stack."); } return stack.Pop(); } private static decimal ParseNumberToken(string token){ string str = token; if (str.StartsWith("-.")){ str = "-0"+str.Substring(1); } else if (str[0] == '.'){ str = "0"+str; } if (str.EndsWith("...")){ string truncated = str.Substring(0, str.Length-3); if (truncated.IndexOf('.') != -1){ str = truncated; } } decimal value; if (decimal.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out value)){ if (value.ToString(CultureInfo.InvariantCulture) != str){ throw new CommandException("Provided number is outside of decimal range: "+token); } return value; } else{ throw new CommandException("Invalid token, expected a number: "+token); } } } }