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