Coverage Report - com.timjohnstondev.unitconverter.logic.FormulaParser
 
Classes in this File Line Coverage Branch Coverage Complexity
FormulaParser
95%
91/95
92%
39/42
2.438
 
 1  
 /**
 2  
  * Copyright 2009 Timothy Johnston Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
 3  
  * file except in compliance with the License. You may obtain a copy of the License at
 4  
  * 
 5  
  * http://www.apache.org/licenses/LICENSE-2.0
 6  
  * 
 7  
  * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 8  
  * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 9  
  * specific language governing permissions and limitations under the License.
 10  
  */
 11  
 package com.timjohnstondev.unitconverter.logic;
 12  
 
 13  
 import java.math.BigDecimal;
 14  
 import java.math.MathContext;
 15  
 import java.math.RoundingMode;
 16  
 
 17  
 /**
 18  
  * This parser can take a {@code String} as a formula that contains a variable (must be {@code X}) and the value for
 19  
  * that variable, do the calculations to return a {@code BigDecimal}.
 20  
  * <p>
 21  
  * There can be multiple occurrences of the variable, but only one variable. Symbols it can handle: {@literal (, ), ^,
 22  
  * *, /, +, -}
 23  
  */
 24  
 public final class FormulaParser
 25  
 {
 26  
   /**
 27  
    * The variable letter used in the formulas to be parsed.
 28  
    */
 29  
   static final String VARIABLE = "X";
 30  
   private static StringBuffer formula;
 31  
   private static int deleteCharsFromIndex;
 32  
   private static int deleteCharsToIndex;
 33  
   private static final String OPEN_PAREN = "(";
 34  
   private static final String CLOSE_PAREN = ")";
 35  
   private static final String POWER_SYMBOL = "^";
 36  1
   private static final MathContext MATH_CONTEXT = new MathContext(30, RoundingMode.HALF_UP);
 37  1
   private static final String[] SYMBOLS = {POWER_SYMBOL, "*", "/", "+", "-"};
 38  
 
 39  
   private FormulaParser()
 40  0
   {}
 41  
 
 42  
   /**
 43  
    * Returns the calculated value of the given formula with the variable value substituted in.
 44  
    * 
 45  
    * @param variableValue the value of the variable in the formula
 46  
    * @param newFormula the formula to be evaluated
 47  
    * @return the calculated value
 48  
    */
 49  
   public static BigDecimal parse(final String variableValue, final String newFormula)
 50  
   {
 51  1
     return parse(new BigDecimal(variableValue), newFormula);
 52  
   }
 53  
 
 54  
   /**
 55  
    * Returns the calculated value of the given formula with the variable value substituted in.
 56  
    * 
 57  
    * @param variableValue the value of the variable in the formula
 58  
    * @param newFormula the formula to be evaluated
 59  
    * @return the calculated value
 60  
    */
 61  
   static BigDecimal parse(final BigDecimal variableValue, final String newFormula)
 62  
   {
 63  31
     final String variable = variableValue.stripTrailingZeros().toPlainString();
 64  31
     String fullFormula = newFormula.toUpperCase().replaceAll(VARIABLE, variable);
 65  31
     fullFormula = fullFormula.replaceAll(" ", "");
 66  31
     formula = new StringBuffer(fullFormula);
 67  
 
 68  31
     findParens();
 69  31
     calulate();
 70  
 
 71  31
     return new BigDecimal(formula.toString());
 72  
   }
 73  
 
 74  
   private static void findParens()
 75  
   {
 76  74
     findParens(0);
 77  74
   }
 78  
 
 79  
   private static void findParens(final int fromIndex)
 80  
   {
 81  81
     final int openIndex = formula.indexOf(OPEN_PAREN, fromIndex);
 82  81
     if (openIndex >= fromIndex)
 83  
     {
 84  36
       final int anotherOpenIndex = formula.indexOf(OPEN_PAREN, openIndex + 1);
 85  36
       int closeIndex = formula.indexOf(CLOSE_PAREN, openIndex);
 86  36
       cleanupUnclosedParens(closeIndex, openIndex);
 87  
 
 88  36
       if (anotherOpenIndex > openIndex && anotherOpenIndex < closeIndex)
 89  
       {
 90  7
         goIntoAnotherLayerofParens(anotherOpenIndex);
 91  
       }
 92  36
       closeIndex = formula.indexOf(CLOSE_PAREN, openIndex);
 93  36
       if (closeIndex > openIndex)
 94  
       {
 95  29
         calulate(openIndex, closeIndex);
 96  29
         removeParens(openIndex);
 97  
       }
 98  36
       findParens();
 99  
     }
 100  81
   }
 101  
 
 102  
   private static void goIntoAnotherLayerofParens(final int index)
 103  
   {
 104  7
     findParens(index);
 105  7
     final int closeIndex = formula.indexOf(CLOSE_PAREN, index);
 106  
 
 107  7
     if (closeIndex > index)
 108  
     {
 109  0
       calulate(index, closeIndex);
 110  0
       removeParens(index);
 111  
     }
 112  7
     findParens();
 113  7
   }
 114  
 
 115  
   private static void cleanupUnclosedParens(final int closeIndex, final int openIndex)
 116  
 
 117  
   {
 118  36
     if (closeIndex == -1)
 119  
     {
 120  0
       formula.deleteCharAt(openIndex);
 121  
     }
 122  36
   }
 123  
 
 124  
   private static void removeParens(final int openIndex)
 125  
   {
 126  29
     final int closeIndex = formula.indexOf(CLOSE_PAREN, openIndex);
 127  29
     formula.deleteCharAt(closeIndex);
 128  29
     formula.deleteCharAt(openIndex);
 129  29
   }
 130  
 
 131  
   private static void calulate()
 132  
   {
 133  31
     calulate(0, formula.length() - 1);
 134  31
   }
 135  
 
 136  
   private static void calulate(final int fromIndex, final int toIndex)
 137  
   {
 138  60
     int newToIndex = toIndex;
 139  360
     for (String symbol : SYMBOLS)
 140  
     {
 141  300
       performMath(symbol, fromIndex, newToIndex);
 142  300
       if (formula.indexOf(CLOSE_PAREN, fromIndex) > 0)
 143  
       {
 144  145
         newToIndex = formula.indexOf(CLOSE_PAREN, fromIndex);
 145  
       }
 146  
       else
 147  
       {
 148  155
         newToIndex = formula.length() - 1;
 149  
       }
 150  
     }
 151  60
   }
 152  
 
 153  
   private static void performMath(final String symbol, final int fromIndex, final int toIndex)
 154  
   {
 155  300
     final int index = formula.indexOf(symbol, fromIndex);
 156  
 
 157  300
     if (index > fromIndex && index < toIndex)
 158  
     {
 159  46
       BigDecimal result = BigDecimal.ZERO;
 160  46
       final BigDecimal preNumber = getPreNumber(index, fromIndex);
 161  46
       final BigDecimal postNumber = getPostNumber(index, toIndex, symbol);
 162  
 
 163  46
       switch (symbol.charAt(0))
 164  
       {
 165  
         case '^':
 166  8
           result = preNumber.pow(postNumber.intValue(), MATH_CONTEXT);
 167  8
           break;
 168  
         case '*':
 169  10
           result = preNumber.multiply(postNumber, MATH_CONTEXT);
 170  10
           break;
 171  
         case '/':
 172  9
           result = preNumber.divide(postNumber, MATH_CONTEXT);
 173  9
           break;
 174  
         case '+':
 175  8
           result = preNumber.add(postNumber, MATH_CONTEXT);
 176  8
           break;
 177  
         case '-':
 178  11
           result = preNumber.subtract(postNumber, MATH_CONTEXT);
 179  11
           break;
 180  
         default:
 181  
           break;
 182  
       }
 183  
 
 184  46
       updateFormula(result);
 185  
     }
 186  300
   }
 187  
 
 188  
   private static BigDecimal getPreNumber(final int index, final int fromIndex)
 189  
   {
 190  46
     int preIndex = index - 1;
 191  46
     String preNumber = String.valueOf(formula.charAt(preIndex));
 192  46
     preIndex--;
 193  
 
 194  141
     while (preIndex >= fromIndex && isDigit(formula.charAt(preIndex)))
 195  
     {
 196  95
       preNumber = formula.charAt(preIndex) + preNumber;
 197  95
       preIndex--;
 198  
     }
 199  46
     deleteCharsFromIndex = ++preIndex;
 200  
 
 201  46
     return new BigDecimal(preNumber);
 202  
   }
 203  
 
 204  
   private static BigDecimal getPostNumber(final int index, final int toIndex, final String symbol)
 205  
   {
 206  46
     int postIndex = index + 1;
 207  46
     String postNumber = String.valueOf(formula.charAt(postIndex));
 208  46
     postIndex++;
 209  
 
 210  114
     while (postIndex <= toIndex && isDigit(formula.charAt(postIndex), symbol))
 211  
     {
 212  68
       postNumber += formula.charAt(postIndex);
 213  68
       postIndex++;
 214  
     }
 215  46
     deleteCharsToIndex = postIndex;
 216  
 
 217  46
     return new BigDecimal(postNumber);
 218  
   }
 219  
 
 220  
   private static void updateFormula(final BigDecimal result)
 221  
   {
 222  46
     formula.delete(deleteCharsFromIndex, deleteCharsToIndex);
 223  46
     formula.insert(deleteCharsFromIndex, result.stripTrailingZeros().toPlainString());
 224  46
     deleteCharsFromIndex = 0;
 225  46
     deleteCharsToIndex = 0;
 226  46
   }
 227  
 
 228  
   private static boolean isDigit(final char nextChar)
 229  
   {
 230  126
     return isDigit(nextChar, "");
 231  
   }
 232  
 
 233  
   private static boolean isDigit(final char nextChar, final String symbol)
 234  
   {
 235  226
     boolean isDigit = false;
 236  
 
 237  226
     if (Character.isDigit(nextChar))
 238  
     {
 239  154
       isDigit = true;
 240  
     }
 241  
 
 242  226
     if (!POWER_SYMBOL.equals(symbol) && ('.' == nextChar || ',' == nextChar))
 243  
     {
 244  9
       isDigit = true;
 245  
     }
 246  226
     return isDigit;
 247  
   }
 248  
 }