View Javadoc

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    private static final MathContext MATH_CONTEXT = new MathContext(30, RoundingMode.HALF_UP);
37    private static final String[] SYMBOLS = {POWER_SYMBOL, "*", "/", "+", "-"};
38  
39    private FormulaParser()
40    {}
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      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      final String variable = variableValue.stripTrailingZeros().toPlainString();
64      String fullFormula = newFormula.toUpperCase().replaceAll(VARIABLE, variable);
65      fullFormula = fullFormula.replaceAll(" ", "");
66      formula = new StringBuffer(fullFormula);
67  
68      findParens();
69      calulate();
70  
71      return new BigDecimal(formula.toString());
72    }
73  
74    private static void findParens()
75    {
76      findParens(0);
77    }
78  
79    private static void findParens(final int fromIndex)
80    {
81      final int openIndex = formula.indexOf(OPEN_PAREN, fromIndex);
82      if (openIndex >= fromIndex)
83      {
84        final int anotherOpenIndex = formula.indexOf(OPEN_PAREN, openIndex + 1);
85        int closeIndex = formula.indexOf(CLOSE_PAREN, openIndex);
86        cleanupUnclosedParens(closeIndex, openIndex);
87  
88        if (anotherOpenIndex > openIndex && anotherOpenIndex < closeIndex)
89        {
90          goIntoAnotherLayerofParens(anotherOpenIndex);
91        }
92        closeIndex = formula.indexOf(CLOSE_PAREN, openIndex);
93        if (closeIndex > openIndex)
94        {
95          calulate(openIndex, closeIndex);
96          removeParens(openIndex);
97        }
98        findParens();
99      }
100   }
101 
102   private static void goIntoAnotherLayerofParens(final int index)
103   {
104     findParens(index);
105     final int closeIndex = formula.indexOf(CLOSE_PAREN, index);
106 
107     if (closeIndex > index)
108     {
109       calulate(index, closeIndex);
110       removeParens(index);
111     }
112     findParens();
113   }
114 
115   private static void cleanupUnclosedParens(final int closeIndex, final int openIndex)
116 
117   {
118     if (closeIndex == -1)
119     {
120       formula.deleteCharAt(openIndex);
121     }
122   }
123 
124   private static void removeParens(final int openIndex)
125   {
126     final int closeIndex = formula.indexOf(CLOSE_PAREN, openIndex);
127     formula.deleteCharAt(closeIndex);
128     formula.deleteCharAt(openIndex);
129   }
130 
131   private static void calulate()
132   {
133     calulate(0, formula.length() - 1);
134   }
135 
136   private static void calulate(final int fromIndex, final int toIndex)
137   {
138     int newToIndex = toIndex;
139     for (String symbol : SYMBOLS)
140     {
141       performMath(symbol, fromIndex, newToIndex);
142       if (formula.indexOf(CLOSE_PAREN, fromIndex) > 0)
143       {
144         newToIndex = formula.indexOf(CLOSE_PAREN, fromIndex);
145       }
146       else
147       {
148         newToIndex = formula.length() - 1;
149       }
150     }
151   }
152 
153   private static void performMath(final String symbol, final int fromIndex, final int toIndex)
154   {
155     final int index = formula.indexOf(symbol, fromIndex);
156 
157     if (index > fromIndex && index < toIndex)
158     {
159       BigDecimal result = BigDecimal.ZERO;
160       final BigDecimal preNumber = getPreNumber(index, fromIndex);
161       final BigDecimal postNumber = getPostNumber(index, toIndex, symbol);
162 
163       switch (symbol.charAt(0))
164       {
165         case '^':
166           result = preNumber.pow(postNumber.intValue(), MATH_CONTEXT);
167           break;
168         case '*':
169           result = preNumber.multiply(postNumber, MATH_CONTEXT);
170           break;
171         case '/':
172           result = preNumber.divide(postNumber, MATH_CONTEXT);
173           break;
174         case '+':
175           result = preNumber.add(postNumber, MATH_CONTEXT);
176           break;
177         case '-':
178           result = preNumber.subtract(postNumber, MATH_CONTEXT);
179           break;
180         default:
181           break;
182       }
183 
184       updateFormula(result);
185     }
186   }
187 
188   private static BigDecimal getPreNumber(final int index, final int fromIndex)
189   {
190     int preIndex = index - 1;
191     String preNumber = String.valueOf(formula.charAt(preIndex));
192     preIndex--;
193 
194     while (preIndex >= fromIndex && isDigit(formula.charAt(preIndex)))
195     {
196       preNumber = formula.charAt(preIndex) + preNumber;
197       preIndex--;
198     }
199     deleteCharsFromIndex = ++preIndex;
200 
201     return new BigDecimal(preNumber);
202   }
203 
204   private static BigDecimal getPostNumber(final int index, final int toIndex, final String symbol)
205   {
206     int postIndex = index + 1;
207     String postNumber = String.valueOf(formula.charAt(postIndex));
208     postIndex++;
209 
210     while (postIndex <= toIndex && isDigit(formula.charAt(postIndex), symbol))
211     {
212       postNumber += formula.charAt(postIndex);
213       postIndex++;
214     }
215     deleteCharsToIndex = postIndex;
216 
217     return new BigDecimal(postNumber);
218   }
219 
220   private static void updateFormula(final BigDecimal result)
221   {
222     formula.delete(deleteCharsFromIndex, deleteCharsToIndex);
223     formula.insert(deleteCharsFromIndex, result.stripTrailingZeros().toPlainString());
224     deleteCharsFromIndex = 0;
225     deleteCharsToIndex = 0;
226   }
227 
228   private static boolean isDigit(final char nextChar)
229   {
230     return isDigit(nextChar, "");
231   }
232 
233   private static boolean isDigit(final char nextChar, final String symbol)
234   {
235     boolean isDigit = false;
236 
237     if (Character.isDigit(nextChar))
238     {
239       isDigit = true;
240     }
241 
242     if (!POWER_SYMBOL.equals(symbol) && ('.' == nextChar || ',' == nextChar))
243     {
244       isDigit = true;
245     }
246     return isDigit;
247   }
248 }