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); } } } }