1
0
mirror of https://github.com/chylex/Query.git synced 2025-04-30 05:34:08 +02:00
Query/AppCalc/App.cs

236 lines
8.5 KiB
C#

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