1 /* 2 * Copyright (C) 2008 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.android.calculator2; 18 19 import com.android.calculator2.CalculatorDisplay.Scroll; 20 21 import android.text.TextUtils; 22 import android.view.KeyEvent; 23 import android.widget.EditText; 24 import android.content.Context; 25 26 import java.util.Locale; 27 28 import org.javia.arity.Symbols; 29 import org.javia.arity.SyntaxException; 30 import org.javia.arity.Util; 31 32 class Logic { 33 private CalculatorDisplay mDisplay; 34 private Symbols mSymbols = new Symbols(); 35 private History mHistory; 36 private String mResult = ""; 37 private boolean mIsError = false; 38 private int mLineLength = 0; 39 40 private static final String INFINITY_UNICODE = "\u221e"; 41 42 public static final String MARKER_EVALUATE_ON_RESUME = "?"; 43 44 // the two strings below are the result of Double.toString() for Infinity & NaN 45 // they are not output to the user and don't require internationalization 46 private static final String INFINITY = "Infinity"; 47 private static final String NAN = "NaN"; 48 49 static final char MINUS = '\u2212'; 50 51 private final String mErrorString; 52 53 public final static int DELETE_MODE_BACKSPACE = 0; 54 public final static int DELETE_MODE_CLEAR = 1; 55 56 private int mDeleteMode = DELETE_MODE_BACKSPACE; 57 58 public interface Listener { 59 void onDeleteModeChange(); 60 } 61 62 private Listener mListener; 63 64 Logic(Context context, History history, CalculatorDisplay display) { 65 mErrorString = context.getResources().getString(R.string.error); 66 mHistory = history; 67 mDisplay = display; 68 mDisplay.setLogic(this); 69 } 70 71 public void setListener(Listener listener) { 72 this.mListener = listener; 73 } 74 75 public void setDeleteMode(int mode) { 76 if (mDeleteMode != mode) { 77 mDeleteMode = mode; 78 mListener.onDeleteModeChange(); 79 } 80 } 81 82 public int getDeleteMode() { 83 return mDeleteMode; 84 } 85 86 void setLineLength(int nDigits) { 87 mLineLength = nDigits; 88 } 89 90 boolean eatHorizontalMove(boolean toLeft) { 91 EditText editText = mDisplay.getEditText(); 92 int cursorPos = editText.getSelectionStart(); 93 return toLeft ? cursorPos == 0 : cursorPos >= editText.length(); 94 } 95 96 private String getText() { 97 return mDisplay.getText().toString(); 98 } 99 100 void insert(String delta) { 101 mDisplay.insert(delta); 102 setDeleteMode(DELETE_MODE_BACKSPACE); 103 } 104 105 public void resumeWithHistory() { 106 clearWithHistory(false); 107 } 108 109 private void clearWithHistory(boolean scroll) { 110 String text = mHistory.getText(); 111 if (MARKER_EVALUATE_ON_RESUME.equals(text)) { 112 if (!mHistory.moveToPrevious()) { 113 text = ""; 114 } 115 text = mHistory.getText(); 116 evaluateAndShowResult(text, CalculatorDisplay.Scroll.NONE); 117 } else { 118 mResult = ""; 119 mDisplay.setText( 120 text, scroll ? CalculatorDisplay.Scroll.UP : CalculatorDisplay.Scroll.NONE); 121 mIsError = false; 122 } 123 } 124 125 private void clear(boolean scroll) { 126 mHistory.enter(""); 127 mDisplay.setText("", scroll ? CalculatorDisplay.Scroll.UP : CalculatorDisplay.Scroll.NONE); 128 cleared(); 129 } 130 131 void cleared() { 132 mResult = ""; 133 mIsError = false; 134 updateHistory(); 135 136 setDeleteMode(DELETE_MODE_BACKSPACE); 137 } 138 139 boolean acceptInsert(String delta) { 140 String text = getText(); 141 return !mIsError && 142 (!mResult.equals(text) || 143 isOperator(delta) || 144 mDisplay.getSelectionStart() != text.length()); 145 } 146 147 void onDelete() { 148 if (getText().equals(mResult) || mIsError) { 149 clear(false); 150 } else { 151 mDisplay.dispatchKeyEvent(new KeyEvent(0, KeyEvent.KEYCODE_DEL)); 152 mResult = ""; 153 } 154 } 155 156 void onClear() { 157 clear(mDeleteMode == DELETE_MODE_CLEAR); 158 } 159 160 void onEnter() { 161 if (mDeleteMode == DELETE_MODE_CLEAR) { 162 clearWithHistory(false); // clear after an Enter on result 163 } else { 164 evaluateAndShowResult(getText(), CalculatorDisplay.Scroll.UP); 165 } 166 } 167 168 public void evaluateAndShowResult(String text, Scroll scroll) { 169 try { 170 String result = evaluate(text); 171 if (!text.equals(result)) { 172 mHistory.enter(text); 173 mResult = result; 174 mDisplay.setText(mResult, scroll); 175 setDeleteMode(DELETE_MODE_CLEAR); 176 } 177 } catch (SyntaxException e) { 178 mIsError = true; 179 mResult = mErrorString; 180 mDisplay.setText(mResult, scroll); 181 setDeleteMode(DELETE_MODE_CLEAR); 182 } 183 } 184 185 void onUp() { 186 String text = getText(); 187 if (!text.equals(mResult)) { 188 mHistory.update(text); 189 } 190 if (mHistory.moveToPrevious()) { 191 mDisplay.setText(mHistory.getText(), CalculatorDisplay.Scroll.DOWN); 192 } 193 } 194 195 void onDown() { 196 String text = getText(); 197 if (!text.equals(mResult)) { 198 mHistory.update(text); 199 } 200 if (mHistory.moveToNext()) { 201 mDisplay.setText(mHistory.getText(), CalculatorDisplay.Scroll.UP); 202 } 203 } 204 205 void updateHistory() { 206 String text = getText(); 207 // Don't set the ? marker for empty text or the error string. 208 // There is no need to evaluate those later. 209 if (!TextUtils.isEmpty(text) && !TextUtils.equals(text, mErrorString) 210 && text.equals(mResult)) { 211 mHistory.update(MARKER_EVALUATE_ON_RESUME); 212 } else { 213 mHistory.update(getText()); 214 } 215 } 216 217 private static final int ROUND_DIGITS = 1; 218 String evaluate(String input) throws SyntaxException { 219 if (input.trim().equals("")) { 220 return ""; 221 } 222 223 // drop final infix operators (they can only result in error) 224 int size = input.length(); 225 while (size > 0 && isOperator(input.charAt(size - 1))) { 226 input = input.substring(0, size - 1); 227 --size; 228 } 229 230 double value = mSymbols.eval(input); 231 232 String result = ""; 233 for (int precision = mLineLength; precision > 6; precision--) { 234 result = tryFormattingWithPrecision(value, precision); 235 if (result.length() <= mLineLength) { 236 break; 237 } 238 } 239 return result.replace('-', MINUS).replace(INFINITY, INFINITY_UNICODE); 240 } 241 242 private String tryFormattingWithPrecision(double value, int precision) { 243 // The standard scientific formatter is basically what we need. We will 244 // start with what it produces and then massage it a bit. 245 String result = String.format(Locale.US, "%" + mLineLength + "." + precision + "g", value); 246 if (result.equals(NAN)) { // treat NaN as Error 247 mIsError = true; 248 return mErrorString; 249 } 250 String mantissa = result; 251 String exponent = null; 252 int e = result.indexOf('e'); 253 if (e != -1) { 254 mantissa = result.substring(0, e); 255 256 // Strip "+" and unnecessary 0's from the exponent 257 exponent = result.substring(e + 1); 258 if (exponent.startsWith("+")) { 259 exponent = exponent.substring(1); 260 } 261 exponent = String.valueOf(Integer.parseInt(exponent)); 262 } else { 263 mantissa = result; 264 } 265 266 int period = mantissa.indexOf('.'); 267 if (period == -1) { 268 period = mantissa.indexOf(','); 269 } 270 if (period != -1) { 271 // Strip trailing 0's 272 while (mantissa.length() > 0 && mantissa.endsWith("0")) { 273 mantissa = mantissa.substring(0, mantissa.length() - 1); 274 } 275 if (mantissa.length() == period + 1) { 276 mantissa = mantissa.substring(0, mantissa.length() - 1); 277 } 278 } 279 280 if (exponent != null) { 281 result = mantissa + 'e' + exponent; 282 } else { 283 result = mantissa; 284 } 285 return result; 286 } 287 288 static boolean isOperator(String text) { 289 return text.length() == 1 && isOperator(text.charAt(0)); 290 } 291 292 static boolean isOperator(char c) { 293 //plus minus times div 294 return "+\u2212\u00d7\u00f7/*".indexOf(c) != -1; 295 } 296 } 297