1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.android.car.diagnostictools.utils;
18 
19 /**
20  * This class allows for custom formulas to translate input live/freeze frame data into the
21  * appropriate values. Ideally a `conversion` object should be used in the JSON but this allows for
22  * more flexibility if needed.
23  */
24 class MathEval {
25 
26     /**
27      * This is a confidence check for user generated strings to catch errors once instead of every
28      * time the data is translated
29      *
30      * @param str Translation string to test
31      * @return True if the string doesn't or won't fail when processing simple inputs
32      */
testTranslation(final String str)33     static boolean testTranslation(final String str) {
34         if (str == null || str.length() == 0) {
35             return true;
36         } else if (str.length() > 50) {
37             return false;
38         }
39 
40         try {
41             eval(str, (float) 100.0);
42             eval(str, (float) 10.0);
43             eval(str, (float) 1);
44             eval(str, (float) .1);
45             eval(str, (float) 0);
46             eval(str, (float) -100.0);
47             eval(str, (float) -10.0);
48             eval(str, (float) -1);
49             eval(str, (float) -.1);
50         } catch (Exception e) {
51             return false;
52         }
53         return true;
54     }
55 
56     /**
57      * Uses a translation string and applies the formula to a variable From
58      * https://stackoverflow.com/a/26227947 with modifications. String must only use +,-,*,/,^,(,)
59      * or "sqrt", "sin", "cos", "tan" (as a function ie sqrt(4)) and the variable x
60      *
61      * @param translationString Translation string which uses x as the variable
62      * @param variableIn Float that the translation is operating on
63      * @return New Float that has gone through operations defined in "translationString". If
64      *     translationString is non-operable then the variableIn will be returned
65      * @throws TranslationTooLongException Thrown if the translation string is longer than 50 chars
66      *     to prevent long execution times
67      */
eval(final String translationString, Float variableIn)68     static double eval(final String translationString, Float variableIn)
69             throws TranslationTooLongException {
70 
71         if (translationString == null || translationString.length() == 0) {
72             return variableIn;
73         } else if (translationString.length() > 50) {
74             throw new TranslationTooLongException(
75                     "Translation function " + translationString + " is too long");
76         }
77 
78         return new Object() {
79             int mPos = -1, mCh;
80 
81             void nextChar() {
82                 mCh = (++mPos < translationString.length()) ? translationString.charAt(mPos) : -1;
83             }
84 
85             boolean eat(int charToEat) {
86                 while (mCh == ' ') {
87                     nextChar();
88                 }
89                 if (mCh == charToEat) {
90                     nextChar();
91                     return true;
92                 }
93                 return false;
94             }
95 
96             double parse() {
97                 nextChar();
98                 double x = parseExpression();
99                 if (mPos < translationString.length()) {
100                     throw new RuntimeException("Unexpected: " + (char) mCh);
101                 }
102                 return x;
103             }
104 
105             // Grammar:
106             // expression = term | expression `+` term | expression `-` term
107             // term = factor | term `*` factor | term `/` factor
108             // factor = `+` factor | `-` factor | `(` expression `)`
109             //        | number | functionName factor | factor `^` factor
110 
111             double parseExpression() {
112                 double x = parseTerm();
113                 for (;; ) {
114                     if (eat('+')) {
115                         x += parseTerm(); // addition
116                     } else if (eat('-')) {
117                         x -= parseTerm(); // subtraction
118                     } else {
119                         return x;
120                     }
121                 }
122             }
123 
124             double parseTerm() {
125                 double x = parseFactor();
126                 for (;; ) {
127                     if (eat('*')) {
128                         x *= parseFactor(); // multiplication
129                     } else if (eat('/')) {
130                         x /= parseFactor(); // division
131                     } else {
132                         return x;
133                     }
134                 }
135             }
136 
137             double parseFactor() {
138                 if (eat('+')) {
139                     return parseFactor(); // unary plus
140                 }
141                 if (eat('-')) {
142                     return -parseFactor(); // unary minus
143                 }
144 
145                 double x;
146                 int startPos = this.mPos;
147                 if (eat('(')) { // parentheses
148                     x = parseExpression();
149                     eat(')');
150                 } else if ((mCh >= '0' && mCh <= '9') || mCh == '.') { // numbers
151                     while ((mCh >= '0' && mCh <= '9') || mCh == '.') {
152                         nextChar();
153                     }
154                     x = Double.parseDouble(translationString.substring(startPos, this.mPos));
155                 } else if (mCh == 'x') {
156                     x = variableIn;
157                     nextChar();
158                     // System.out.println(x);
159                 } else if (mCh >= 'a' && mCh <= 'z') { // functions
160                     while (mCh >= 'a' && mCh <= 'z') {
161                         nextChar();
162                     }
163                     String func = translationString.substring(startPos, this.mPos);
164                     x = parseFactor();
165                     switch (func) {
166                         case "sqrt":
167                             x = Math.sqrt(x);
168                             break;
169                         case "sin":
170                             x = Math.sin(Math.toRadians(x));
171                             break;
172                         case "cos":
173                             x = Math.cos(Math.toRadians(x));
174                             break;
175                         case "tan":
176                             x = Math.tan(Math.toRadians(x));
177                             break;
178                         default:
179                             throw new RuntimeException("Unknown function: " + func);
180                     }
181                 } else {
182                     throw new RuntimeException("Unexpected: " + (char) mCh);
183                 }
184 
185                 if (eat('^')) {
186                     x = Math.pow(x, parseFactor()); // exponentiation
187                 }
188 
189                 return x;
190             }
191         }.parse();
192     }
193 
194     /** Exception thrown if the translation string is too long */
195     static class TranslationTooLongException extends Exception {
196 
TranslationTooLongException(String errorMsg)197         TranslationTooLongException(String errorMsg) {
198             super(errorMsg);
199         }
200     }
201 }
202