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