PageRenderTime 96ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

/Calculator.cs

https://bitbucket.org/rstarkov/tankiconmaker
C# | 269 lines | 256 code | 12 blank | 1 comment | 106 complexity | e3ad2b0d9284c20b9ae691cacfdf7d00 MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause, GPL-3.0, CC-BY-SA-3.0
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Linq;
  5. using RT.Util;
  6. namespace TankIconMaker
  7. {
  8. class Calculator
  9. {
  10. protected string Description;
  11. protected string Input;
  12. protected int Pos;
  13. protected char? Cur { get { return Pos >= Input.Length ? null : (char?) Input[Pos]; } }
  14. protected void ConsumeWhitespace()
  15. {
  16. while (Pos < Input.Length && (Input[Pos] == ' ' || Input[Pos] == '\t' || Input[Pos] == '\r' || Input[Pos] == '\n'))
  17. Pos++;
  18. }
  19. protected Exception NewParseException(string message)
  20. {
  21. return new StyleUserError(App.Translation.Calculator.CouldNotParseExpression + ".\n\n"
  22. + "*{0}:* {1}\n".Fmt(EggsML.Escape(App.Translation.Calculator.ErrLabel_Error), EggsML.Escape(message))
  23. + "*{0}:* {1}<Red>=\"<\"{2}\">\"={3}".Fmt(
  24. EggsML.Escape(App.Translation.Calculator.ErrLabel_Expression),
  25. EggsML.Escape(Input.Substring(0, Pos)),
  26. EggsML.Escape(App.Translation.Calculator.Err_LocationMarker),
  27. EggsML.Escape(Input.Substring(Pos)))
  28. + Description,
  29. formatted: true);
  30. }
  31. public double Parse(string expression, string descriptionEggsML = null)
  32. {
  33. Description = string.IsNullOrEmpty(descriptionEggsML) ? "" : ("\n\n" + descriptionEggsML);
  34. Input = expression;
  35. Pos = 0;
  36. ConsumeWhitespace();
  37. double result = ParseExpression();
  38. if (Cur != null)
  39. throw NewParseException(App.Translation.Calculator.Err_ExpectedEndOfExpression);
  40. if (double.IsInfinity(result))
  41. throw NewParseException(App.Translation.Calculator.Err_ResultInfinite);
  42. if (double.IsNaN(result))
  43. throw NewParseException(App.Translation.Calculator.Err_ResultNaN);
  44. return result;
  45. }
  46. private double ParseExpression()
  47. {
  48. double left = ParseExpressionMul(); // additive expressions are left-associative, so build result as we go. First one is mandatory.
  49. while (true)
  50. {
  51. var op = Cur;
  52. if (op != '+' && op != '-')
  53. return left;
  54. Pos++;
  55. ConsumeWhitespace();
  56. double right = ParseExpressionMul();
  57. if (op == '+')
  58. left = left + right;
  59. else if (op == '-')
  60. left = left - right;
  61. else
  62. throw new Exception();
  63. }
  64. }
  65. private double ParseExpressionMul()
  66. {
  67. double left = ParseExpressionPwr(); // multiplicative expressions are left-associative, so build result as we go. First one is mandatory.
  68. while (true)
  69. {
  70. var op = Cur;
  71. if (op != '*' && op != '/' && op != '%')
  72. return left;
  73. Pos++;
  74. ConsumeWhitespace();
  75. double right = ParseExpressionPwr();
  76. if (op == '*')
  77. left = left * right;
  78. else if (op == '/')
  79. left = left / right;
  80. else if (op == '%')
  81. left = Math.IEEERemainder(left, right);
  82. else
  83. throw new Exception();
  84. }
  85. }
  86. private double ParseExpressionPwr()
  87. {
  88. // Power expressions are right-associative, so must parse them all first
  89. var sequence = new List<double>();
  90. sequence.Add(ParseExpressionPrimary()); // the first primary is mandatory
  91. while (true)
  92. {
  93. if (Cur != '^')
  94. break;
  95. Pos++;
  96. ConsumeWhitespace();
  97. sequence.Add(ParseExpressionPrimary());
  98. }
  99. sequence.Reverse();
  100. double result = sequence[0];
  101. foreach (var next in sequence.Skip(1))
  102. result = Math.Pow(next, result);
  103. return result;
  104. }
  105. private double ParseExpressionPrimary()
  106. {
  107. if (Cur == null)
  108. throw NewParseException(App.Translation.Calculator.Err_UnexpectedEndOfExpression);
  109. else if (Cur == '(') // parentheses for precedence
  110. {
  111. Pos++;
  112. ConsumeWhitespace();
  113. var result = ParseExpression();
  114. if (Cur != ')')
  115. throw NewParseException(App.Translation.Calculator.Err_ExpectedParenthesisOrOperator);
  116. Pos++;
  117. ConsumeWhitespace();
  118. return result;
  119. }
  120. else if (Cur == '-') // unary minus
  121. {
  122. Pos++;
  123. ConsumeWhitespace();
  124. return -ParseExpressionPrimary();
  125. }
  126. else if (Cur == '+') // unary plus, a no-op
  127. {
  128. Pos++;
  129. ConsumeWhitespace();
  130. return ParseExpressionPrimary();
  131. }
  132. else if (Cur >= '0' && Cur <= '9') // a numeric literal
  133. {
  134. int fromPos = Pos;
  135. while (Cur >= '0' && Cur <= '9')
  136. Pos++;
  137. if (Cur == '.') // fractional part
  138. {
  139. Pos++;
  140. while (Cur >= '0' && Cur <= '9')
  141. Pos++;
  142. }
  143. string num = Input.Substring(fromPos, Pos - fromPos);
  144. double result;
  145. if (!double.TryParse(num, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out result))
  146. throw NewParseException(App.Translation.Calculator.Err_CannotParseNumber.Fmt(num));
  147. ConsumeWhitespace();
  148. return result;
  149. }
  150. else if (char.IsLetter(Cur.Value)) // a word or a sequence of words separated by dots
  151. {
  152. int fromPos = Pos;
  153. while (Cur == '.' || (Cur != null && char.IsLetter(Cur.Value)))
  154. Pos++;
  155. string word = Input.Substring(fromPos, Pos - fromPos);
  156. ConsumeWhitespace();
  157. if (Cur == '(')
  158. return EvalFunction(word);
  159. else
  160. return EvalVariable(word);
  161. }
  162. else
  163. throw NewParseException(App.Translation.Calculator.Err_UnexpectedCharacter.Fmt(Cur));
  164. }
  165. private List<double> ParseParameters()
  166. {
  167. if (Cur != '(')
  168. throw new Exception("24567837");
  169. Pos++;
  170. ConsumeWhitespace();
  171. var parameters = new List<double>();
  172. if (Cur == ')')
  173. {
  174. Pos++;
  175. ConsumeWhitespace();
  176. return parameters;
  177. }
  178. while (true)
  179. {
  180. parameters.Add(ParseExpression());
  181. if (Cur == ',')
  182. {
  183. Pos++;
  184. ConsumeWhitespace();
  185. }
  186. else if (Cur == ')')
  187. {
  188. Pos++;
  189. ConsumeWhitespace();
  190. return parameters;
  191. }
  192. else
  193. throw NewParseException(App.Translation.Calculator.Err_ExpectedCommaOrParenthesis);
  194. }
  195. }
  196. protected virtual double EvalVariable(string variable)
  197. {
  198. if (variable.EqualsNoCase("pi"))
  199. return Math.PI;
  200. else if (variable.EqualsNoCase("e"))
  201. return Math.E;
  202. else
  203. throw NewParseException(App.Translation.Calculator.Err_UnknownVariable.Fmt(variable));
  204. }
  205. protected virtual double EvalFunction(string function)
  206. {
  207. List<double> parameters = null;
  208. Action<int, int> parseParams = (int minRequired, int maxRequired) =>
  209. {
  210. parameters = ParseParameters();
  211. if (minRequired == maxRequired && minRequired != -1 && parameters.Count != minRequired)
  212. throw NewParseException(App.Translation.Calculator.Err_FunctionParamCountExact.Fmt(App.Translation, function, minRequired, parameters.Count));
  213. else if (minRequired != -1 && parameters.Count < minRequired)
  214. throw NewParseException(App.Translation.Calculator.Err_FunctionParamCountAtLeast.Fmt(App.Translation, function, minRequired, parameters.Count));
  215. else if (maxRequired != -1 && parameters.Count > maxRequired)
  216. throw NewParseException(App.Translation.Calculator.Err_FunctionParamCountAtMost.Fmt(App.Translation, function, maxRequired, parameters.Count));
  217. };
  218. switch (function.ToLower())
  219. {
  220. case "sqrt": parseParams(1, 1); return Math.Sqrt(parameters[0]);
  221. case "abs": parseParams(1, 1); return Math.Abs(parameters[0]);
  222. case "sign": parseParams(1, 1); return Math.Sign(parameters[0]);
  223. case "ceil": parseParams(1, 1); return Math.Ceiling(parameters[0]);
  224. case "floor": parseParams(1, 1); return Math.Floor(parameters[0]);
  225. case "round": parseParams(1, 1); return Math.Round(parameters[0]);
  226. case "trunc": parseParams(1, 1); return Math.Truncate(parameters[0]);
  227. case "sin": parseParams(1, 1); return Math.Sin(parameters[0]);
  228. case "cos": parseParams(1, 1); return Math.Cos(parameters[0]);
  229. case "tan": parseParams(1, 1); return Math.Tan(parameters[0]);
  230. case "sinh": parseParams(1, 1); return Math.Sinh(parameters[0]);
  231. case "cosh": parseParams(1, 1); return Math.Cosh(parameters[0]);
  232. case "tanh": parseParams(1, 1); return Math.Tanh(parameters[0]);
  233. case "acos": parseParams(1, 1); return Math.Acos(parameters[0]);
  234. case "asin": parseParams(1, 1); return Math.Asin(parameters[0]);
  235. case "atan": parseParams(1, 1); return Math.Atan(parameters[0]);
  236. case "atan2": parseParams(2, 2); return Math.Atan2(parameters[0], parameters[1]);
  237. case "deg": parseParams(1, 1); return Math.PI * parameters[0] / 180.0;
  238. case "exp": parseParams(1, 1); return Math.Exp(parameters[0]);
  239. case "log10": parseParams(1, 1); return Math.Log10(parameters[0]);
  240. case "log2": parseParams(1, 1); return Math.Log(parameters[0], 2);
  241. case "ln": parseParams(1, 1); return Math.Log(parameters[0]);
  242. case "log":
  243. parseParams(1, 2);
  244. if (parameters.Count == 1)
  245. return Math.Log(parameters[0]);
  246. else if (parameters.Count == 2)
  247. return Math.Log(parameters[1], parameters[0]); // base first, number second, like in the actual notation
  248. else
  249. throw new Exception("3135623"); // not reachable
  250. case "min": parseParams(1, -1); return parameters.Min();
  251. case "max": parseParams(1, -1); return parameters.Max();
  252. default:
  253. throw NewParseException(App.Translation.Calculator.Err_UnknownFunction.Fmt(function));
  254. }
  255. }
  256. }
  257. }