mirror of
https://github.com/chylex/Query.git
synced 2025-04-30 05:34:08 +02:00
236 lines
8.5 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|