package com.llamacorp.equate; import android.content.Context; import android.content.res.Resources; import android.text.Spanned; import com.llamacorp.equate.unit.Unit; import com.llamacorp.equate.unit.UnitType; import com.llamacorp.equate.unit.UnitTypeList; import com.llamacorp.equate.view.ViewUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.ArrayList; import java.util.List; import java.util.Set; public class Calculator { private static final String FILENAME = "saved_data.json"; private static final String JSON_RESULT_LIST = "result_list"; private static final String JSON_UNIT_TYPE_LIST = "unit_type_array"; private static final String JSON_EXPRESSION = "expression"; private static final String JSON_HINTS = "hints"; private static final int RESULT_LIST_MAX_SIZE = 100; //TODO fix warning below by removing reference to mAppContext in calc class private static Calculator mCalculator; private Context mAppContext; //main expression private Expression mExpression; //object that handles all the math private Solver mSolver; //string of results; this will be directly manipulated by ResultListFragment private List<Result> mResultList; // stores the array of various types of units (length, area, volume, etc) // as well as current unit type position private UnitTypeList mUnitTypeList; public Preferences mPreferences; //precision for all calculations public static final int DISPLAY_PRECISION = 15; public static final int intCalcPrecision = DISPLAY_PRECISION + 2; private boolean mIsTestCalc = false; private Preview mPreview; //------THIS IS FOR TESTING ONLY----------------- private Calculator(Resources mockResources) { mResultList = new ArrayList<>(); mExpression = new Expression(DISPLAY_PRECISION); //mMcOperate = new MathContext(intCalcPrecision); mSolver = new Solver(intCalcPrecision); // try passing a dummy context, make sure we don't actually use Unit Type // in test mUnitTypeList = new UnitTypeList(mockResources); mIsTestCalc = true; mPreferences = new Preferences(); //load the calculating precision mSolver = new Solver(intCalcPrecision); mPreview = new Preview(mSolver); } //------THIS IS FOR TESTING ONLY----------------- static Calculator getTestCalculator(Resources mockResources) { mCalculator = new Calculator(mockResources); return mCalculator; } /** * Method turns calculator class into a singleton class * (one instance allowed) */ private Calculator(Context appContext) { //save our context mAppContext = appContext.getApplicationContext(); mResultList = new ArrayList<>(); mExpression = new Expression(DISPLAY_PRECISION); //set the unit type to length by default mPreferences = new Preferences(); //load the calculating precision mSolver = new Solver(intCalcPrecision); mPreview = new Preview(mSolver); mUnitTypeList = new UnitTypeList(appContext.getResources()); //over-right values above if this works try { loadState(); } //might be from a JSON object not existing (app update) catch (JSONException JE) { //delete the problem JSON file boolean del = mAppContext.deleteFile(FILENAME); String message = "Calculator reset due to JSONException. JSON file " + (del ? "successfully" : "NOT") + " deleted."; toast(message); resetCalc(); //reset the calc and we should be good } catch (Exception e) { toast("Exception in Calculator.loadState():" + e.toString()); } } /** * Method turns calculator class into a singleton class (one instance allowed) */ public static Calculator getCalculator(Context c) { if (mCalculator == null) mCalculator = new Calculator(c.getApplicationContext()); return mCalculator; } private void toast(String msg) { ViewUtils.toastLongCentered(msg, mAppContext); } private void loadState() throws IOException, JSONException { BufferedReader reader = null; try { // open and read the file into a StringBuilder InputStream in = mAppContext.openFileInput(FILENAME); reader = new BufferedReader(new InputStreamReader(in)); StringBuilder jsonString = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { // line breaks are omitted and irrelevant jsonString.append(line); } // parse the JSON using JSONTokener JSONObject jObjState = (JSONObject) new JSONTokener(jsonString.toString()).nextValue(); mExpression = new Expression(jObjState.getJSONObject(JSON_EXPRESSION), DISPLAY_PRECISION); mPreferences = new Preferences(jObjState.getJSONObject(JSON_HINTS)); JSONArray jResultArray = jObjState.getJSONArray(JSON_RESULT_LIST); // build the array of results from JSONObjects for (int i = 0; i < jResultArray.length(); i++) { mResultList.add(new Result(jResultArray.getJSONObject(i))); } mUnitTypeList = new UnitTypeList(mAppContext.getResources(), jObjState.getJSONObject(JSON_UNIT_TYPE_LIST)); } catch (FileNotFoundException e) { // we will ignore this one, since it happens when we start fresh } finally { if (reader != null) reader.close(); } } public void saveState() throws JSONException, IOException { JSONObject jObjState = new JSONObject(); jObjState.put(JSON_EXPRESSION, mExpression.toJSON()); jObjState.put(JSON_HINTS, mPreferences.toJSON()); JSONArray jResultArray = new JSONArray(); for (Result result : mResultList) jResultArray.put(result.toJSON()); jObjState.put(JSON_RESULT_LIST, jResultArray); jObjState.put(JSON_UNIT_TYPE_LIST, mUnitTypeList.toJSON()); // write the file to disk Writer writer = null; try { OutputStream out = mAppContext.openFileOutput(FILENAME, Context.MODE_PRIVATE); writer = new OutputStreamWriter(out); writer.write(jObjState.toString()); } finally { if (writer != null) writer.close(); } } /** * Clears the result list, expression, and unit selection. Remember that this * is only backend data changes, the screen will not have been updated to * reflect any changes. */ public void resetCalc() { mResultList.clear(); mExpression = new Expression(DISPLAY_PRECISION); mPreferences = new Preferences(); //load the calculating precision mSolver = new Solver(intCalcPrecision); mUnitTypeList.initialize(); } /** * Used to store some booleans used by CalculatorActivity after the * Calculator class handled the key-press */ public class CalculatorResultFlags { //has a solve been performed (used to determine if result list update is necessary) public boolean performedSolve = false; //has a unit been selected and then equals been pressed (give user feedback //that the user needs to select another unit public boolean createDiffUnitDialog = false; //used to determine if the instant result should be displayed public boolean displayInstantResult = false; } /** * Passed a key from calculator (num/op/back/clear/eq) and distributes it to its proper function * * @param sKey is either single character (but still a String) or a string from result list */ public CalculatorResultFlags parseKeyPressed(String sKey) { //create a return object CalculatorResultFlags resultFlags = new CalculatorResultFlags(); //first clear any highlighted chars (and the animation) clearHighlighted(); //if expression was displaying "Syntax Error" or similar (containing invalid chars) clear it if (isExpressionInvalid()) mExpression.clearExpression(); //if a convert was just done, main display will show "16 in", but no button will be colored //want this treated the same as 16 (as if no unit is actually selected) if (isSolved() && isUnitSelected()) clearSelectedUnit(); switch (sKey) { //check for equals case "=": // Help dialog: if "Convert 3 in to..." is showing, help user out if (isUnitSelected() & !mExpression.containsOps()){ //don't follow through with solve resultFlags.createDiffUnitDialog = true; return resultFlags; } // Display sci/engineering notation if expression is just a number //Result res = mSolver.tryToggleSciNote(mExpression, false); //solve expression, load into result list if answer not empty solveAndLoadIntoResultList(); resultFlags.performedSolve = isSolved(); return resultFlags; //check for long hold equals key case "g": //toggle between SI notation and not: //if (mPreview.isNumFormatEngineering()) // mPreview.set(new Expression(mExpression), Expression.NumFormat.NORMAL); //else mPreview.set(new Expression(mExpression), Expression.NumFormat.ENGINEERING); if (mExpression.isOnlyValidNumber()){ if (mExpression.isSciNotation()) //mPreview.set(mExpression, Expression.NumFormat.PLAIN); mExpression.roundAndCleanExpression(Expression.NumFormat.PLAIN); else //mPreview.set(mExpression, Expression.NumFormat.SCI_NOTE); mExpression.roundAndCleanExpression(Expression.NumFormat.SCI_NOTE); setSolved(false); } return resultFlags; //check for backspace key case "b": backspace(); break; //check for clear key case "c": clear(); break; //else try all other potential numbers and operators, as well as result list default: //if just hit equals, and we hit [.0-9(], then clear current unit type if (mExpression.isSolved() && sKey.matches("[.0-9(]")) clearSelectedUnit(); //if we hit an operator other than minus, load in the prev answer if (mExpression.isEmpty() && sKey.matches("[" + Expression.regexNonNegOperators + "]")) if (!mResultList.isEmpty()) sKey = mResultList.get(mResultList.size() - 1).getAnswerWithoutSep() + sKey; //deal with all other cases in expression boolean requestSolve = mExpression.keyPresses(sKey); //used when inverter key used after expression is solved if (requestSolve){ solveAndLoadIntoResultList(); resultFlags.performedSolve = isSolved(); return resultFlags; } break; } //want to make a copy of expression so original doesn't change mPreview.set(new Expression(mExpression), Expression.NumFormat.NORMAL); return resultFlags; } /** * Function used to convert from one unit to another * * @param fromUnit is unit being converted from * @param toUnit is unit being converted to */ public void convertFromTo(Unit fromUnit, Unit toUnit) { //if expression was displaying "Syntax Error" or similar (containing // invalid chars) clear it if (isExpressionInvalid()){ mExpression.clearExpression(); return; } //want to add a 1 if we just hit one unit and another if (isExpressionEmpty()) parseKeyPressed("1"); //first solve the function boolean solveSuccess = solveAndLoadIntoResultList(); //if there solve failed because there was nothing to solve, just leave // (this way result list isn't loaded) if (!solveSuccess) return; //next perform numerical unit conversion mSolver.convertFromTo(fromUnit, toUnit, mExpression); int fromUnitPos = getCurrUnitType().findUnitPosInUnitArray(fromUnit); int toUnitPos = getCurrUnitType().findUnitPosInUnitArray(toUnit); //load units into result list (this will also set contains unit flag) // (overrides that from solve) mResultList.get(mResultList.size() - 1).setResultUnit(fromUnit, fromUnitPos, toUnit, toUnitPos, mUnitTypeList.getCurrentKey()); //load the final value into the result list mResultList.get(mResultList.size() - 1).setAnswerWithSep(mExpression.toString()); } /** * Function that is called after user hits the "=" key * Called by calculator for solving current expression * * @return if solved expression */ private boolean solveAndLoadIntoResultList() { //the answer will be loaded into mExpression directly Result result = mSolver.solve(mExpression, Expression.NumFormat.NORMAL); return loadResultToArray(result); } /** * Add a result into the Result list array. Method checks * * @param result to add into the array */ private boolean loadResultToArray(Result result) { if (result == null) return false; //skip result list handling if no result was created mResultList.add(result); //if we hit size limit, remove oldest element if (mResultList.size() > RESULT_LIST_MAX_SIZE) mResultList.remove(0); //if result had an error, leave before setting units if (Expression.isInvalid(result.getAnswerWithoutSep())) return false; //also set result's unit if it's selected if (isUnitSelected()){ //load units into result list (this will also set contains unit flag Unit toUnit = getCurrUnitType().getCurrUnit(); int toUnitPos = getCurrUnitType().getCurrUnitButtonPos(); mResultList.get(mResultList.size() - 1).setResultUnit(toUnit, toUnitPos, toUnit, toUnitPos, mUnitTypeList.getCurrentKey()); } return true; } /** * Clear function for the calculator */ private void clear() { //clear the immediate expression mExpression.clearExpression(); //reset current unit clearSelectedUnit(); } /** * Backspace function for the calculator */ private void backspace() { //clear out unit selection and expression if we just solved or if expression empty if (mExpression.isSolved() || mExpression.isEmpty()){ clearSelectedUnit(); mExpression.clearExpression(); //we're done. don't want to execute code below return; } //since the expression isn't empty, delete last of calcExp list mExpression.backspaceAtSelection(); } private void clearSelectedUnit() { mUnitTypeList.getCurrent().clearUnitSelection(); } /** * Update values of units that are not static (currency) via * each unit's own HTTP/JSON API call. Note that this refresh * is asynchronous and will only happen sometime in the future * Internet connection permitting. * * @param forced should update be forced without waiting for time-out */ public void refreshAllDynamicUnits(boolean forced) { //JUnit tests can't find AsynTask class, so skip it for test calc if (!mIsTestCalc) mUnitTypeList.refreshDynamicUnits(mAppContext, forced); } /** * @return if there are characters marked for highlighting */ public boolean isHighlighted() { return mExpression.isHighlighted(); } public ArrayList<Integer> getHighlighted() { return mExpression.getHighlighted(); } /** * Clear highlighted character (those that are turned red, for example the * open bracket when close bracket is held down. */ public void clearHighlighted() { mExpression.clearHighlightedList(); } public List<Result> getResultList() { return mResultList; } public UnitTypeList getUnitTypeList() { return mUnitTypeList; } /** * Returns if a unit key in current UnitType is selected */ public boolean isUnitSelected() { return mUnitTypeList.getCurrent().isUnitSelected(); } public UnitType getUnitType(int pos) { return mUnitTypeList.get(pos); } public UnitType getCurrUnitType() { return mUnitTypeList.getCurrent(); } public String getUnitTypeName(int index) { return mUnitTypeList.get(index).getUnitTypeName(); } public int getUnitTypeSize() { return mUnitTypeList.numberVisible(); } public void setCurrentUnitTypePos(int index) { mUnitTypeList.setCurrent(index); } public int getUnitTypePos() { return mUnitTypeList.getCurrentIndex(); } /** * Gets the visible Unit Type index supplied key, if able. If the key does * not exist in the visible Unit Types, returns -1 */ public int getUnitTypeIndex(String key) { return mUnitTypeList.getIndex(key); } public boolean isExpressionEmpty() { return mExpression.isEmpty(); } public boolean isExpressionInvalid() { return mExpression.isInvalid(); } public void pasteIntoExpression(String str) { mExpression.pasteIntoExpression(str); } public void setSolved(boolean solved) { mExpression.setSolved(solved); } /** * Returns if current Expression is solved (equals/conversion was last operation) */ public boolean isSolved() { return mExpression.isSolved(); } public int getSelectionEnd() { return mExpression.getSelectionEnd(); } public int getSelectionStart() { return mExpression.getSelectionStart(); } public Expression.NumFormat getNumberFormat() { return mExpression.getNumFormat(); } public boolean isPreviewEmpty() { return mPreview.isEmpty(); } public Spanned getPreviewText(int suffixColor) { return mPreview.getText(suffixColor); } public void setSelectedUnitTypes(Set<String> set) { mUnitTypeList.setOrdered(set); } /** * Set the EditText selection for expression */ public void setSelection(int selStart, int selEnd) { mExpression.setSelection(selStart, selEnd); } @Override public String toString() { //needed for display updating return mExpression.toString(); } }