mirror of
https://github.com/chylex/Query.git
synced 2025-05-20 19:34:06 +02:00
242 lines
6.5 KiB
C#
242 lines
6.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);
|
|
}
|
|
}
|
|
}
|
|
}
|