1 /*
2  * Copyright (C) 2015 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 android.content.res.Resources;
20 import android.content.Context;
21 import android.app.Activity;
22 import android.util.Log;
23 import android.view.View;
24 import android.widget.Button;
25 
26 import java.text.DecimalFormatSymbols;
27 import java.util.HashMap;
28 import java.util.Locale;
29 
30 /**
31  * Collection of mapping functions between key ids, characters, internationalized
32  * and non-internationalized characters, etc.
33  * <p>
34  * KeyMap instances are not meaningful; everything here is static.
35  * All functions are either pure, or are assumed to be called only from a single UI thread.
36  */
37 public class KeyMaps {
38     /**
39      * Map key id to corresponding (internationalized) display string.
40      * Pure function.
41      */
toString(Context context, int id)42     public static String toString(Context context, int id) {
43         switch(id) {
44             case R.id.const_pi:
45                 return context.getString(R.string.const_pi);
46             case R.id.const_e:
47                 return context.getString(R.string.const_e);
48             case R.id.op_sqrt:
49                 return context.getString(R.string.op_sqrt);
50             case R.id.op_fact:
51                 return context.getString(R.string.op_fact);
52             case R.id.op_pct:
53                 return context.getString(R.string.op_pct);
54             case R.id.fun_sin:
55                 return context.getString(R.string.fun_sin) + context.getString(R.string.lparen);
56             case R.id.fun_cos:
57                 return context.getString(R.string.fun_cos) + context.getString(R.string.lparen);
58             case R.id.fun_tan:
59                 return context.getString(R.string.fun_tan) + context.getString(R.string.lparen);
60             case R.id.fun_arcsin:
61                 return context.getString(R.string.fun_arcsin) + context.getString(R.string.lparen);
62             case R.id.fun_arccos:
63                 return context.getString(R.string.fun_arccos) + context.getString(R.string.lparen);
64             case R.id.fun_arctan:
65                 return context.getString(R.string.fun_arctan) + context.getString(R.string.lparen);
66             case R.id.fun_ln:
67                 return context.getString(R.string.fun_ln) + context.getString(R.string.lparen);
68             case R.id.fun_log:
69                 return context.getString(R.string.fun_log) + context.getString(R.string.lparen);
70             case R.id.fun_exp:
71                 // Button label doesn't work.
72                 return context.getString(R.string.exponential) + context.getString(R.string.lparen);
73             case R.id.lparen:
74                 return context.getString(R.string.lparen);
75             case R.id.rparen:
76                 return context.getString(R.string.rparen);
77             case R.id.op_pow:
78                 return context.getString(R.string.op_pow);
79             case R.id.op_mul:
80                 return context.getString(R.string.op_mul);
81             case R.id.op_div:
82                 return context.getString(R.string.op_div);
83             case R.id.op_add:
84                 return context.getString(R.string.op_add);
85             case R.id.op_sqr:
86                 // Button label doesn't work.
87                 return context.getString(R.string.squared);
88             case R.id.op_sub:
89                 return context.getString(R.string.op_sub);
90             case R.id.dec_point:
91                 return context.getString(R.string.dec_point);
92             case R.id.digit_0:
93                 return context.getString(R.string.digit_0);
94             case R.id.digit_1:
95                 return context.getString(R.string.digit_1);
96             case R.id.digit_2:
97                 return context.getString(R.string.digit_2);
98             case R.id.digit_3:
99                 return context.getString(R.string.digit_3);
100             case R.id.digit_4:
101                 return context.getString(R.string.digit_4);
102             case R.id.digit_5:
103                 return context.getString(R.string.digit_5);
104             case R.id.digit_6:
105                 return context.getString(R.string.digit_6);
106             case R.id.digit_7:
107                 return context.getString(R.string.digit_7);
108             case R.id.digit_8:
109                 return context.getString(R.string.digit_8);
110             case R.id.digit_9:
111                 return context.getString(R.string.digit_9);
112             default:
113                 return "";
114         }
115     }
116 
117     /**
118      * Map key id to corresponding (internationalized) descriptive string that can be used
119      * to correctly read back a formula.
120      * Only used for operators and individual characters; not used inside constants.
121      * Returns null when we don't need a descriptive string.
122      * Pure function.
123      */
toDescriptiveString(Context context, int id)124     public static String toDescriptiveString(Context context, int id) {
125         switch(id) {
126             case R.id.op_fact:
127                 return context.getString(R.string.desc_op_fact);
128             case R.id.fun_sin:
129                 return context.getString(R.string.desc_fun_sin)
130                         + " " + context.getString(R.string.desc_lparen);
131             case R.id.fun_cos:
132                 return context.getString(R.string.desc_fun_cos)
133                         + " " + context.getString(R.string.desc_lparen);
134             case R.id.fun_tan:
135                 return context.getString(R.string.desc_fun_tan)
136                         + " " + context.getString(R.string.desc_lparen);
137             case R.id.fun_arcsin:
138                 return context.getString(R.string.desc_fun_arcsin)
139                         + " " + context.getString(R.string.desc_lparen);
140             case R.id.fun_arccos:
141                 return context.getString(R.string.desc_fun_arccos)
142                         + " " + context.getString(R.string.desc_lparen);
143             case R.id.fun_arctan:
144                 return context.getString(R.string.desc_fun_arctan)
145                         + " " + context.getString(R.string.desc_lparen);
146             case R.id.fun_ln:
147                 return context.getString(R.string.desc_fun_ln)
148                         + " " + context.getString(R.string.desc_lparen);
149             case R.id.fun_log:
150                 return context.getString(R.string.desc_fun_log)
151                         + " " + context.getString(R.string.desc_lparen);
152             case R.id.fun_exp:
153                 return context.getString(R.string.desc_fun_exp)
154                         + " " + context.getString(R.string.desc_lparen);
155             case R.id.lparen:
156                 return context.getString(R.string.desc_lparen);
157             case R.id.rparen:
158                 return context.getString(R.string.desc_rparen);
159             case R.id.op_pow:
160                 return context.getString(R.string.desc_op_pow);
161             case R.id.dec_point:
162                 return context.getString(R.string.desc_dec_point);
163             default:
164                 return null;
165         }
166     }
167 
168     /**
169      * Does a button id correspond to a binary operator?
170      * Pure function.
171      */
isBinary(int id)172     public static boolean isBinary(int id) {
173         switch(id) {
174             case R.id.op_pow:
175             case R.id.op_mul:
176             case R.id.op_div:
177             case R.id.op_add:
178             case R.id.op_sub:
179                 return true;
180             default:
181                 return false;
182         }
183     }
184 
185     /**
186      * Does a button id correspond to a function that introduces an implicit lparen?
187      * Pure function.
188      */
isFunc(int id)189     public static boolean isFunc(int id) {
190         switch(id) {
191             case R.id.fun_sin:
192             case R.id.fun_cos:
193             case R.id.fun_tan:
194             case R.id.fun_arcsin:
195             case R.id.fun_arccos:
196             case R.id.fun_arctan:
197             case R.id.fun_ln:
198             case R.id.fun_log:
199             case R.id.fun_exp:
200                 return true;
201             default:
202                 return false;
203         }
204     }
205 
206     /**
207      * Does a button id correspond to a prefix operator?
208      * Pure function.
209      */
isPrefix(int id)210     public static boolean isPrefix(int id) {
211         switch(id) {
212             case R.id.op_sqrt:
213             case R.id.op_sub:
214                 return true;
215             default:
216                 return false;
217         }
218     }
219 
220     /**
221      * Does a button id correspond to a suffix operator?
222      */
isSuffix(int id)223     public static boolean isSuffix(int id) {
224         switch (id) {
225             case R.id.op_fact:
226             case R.id.op_pct:
227             case R.id.op_sqr:
228                 return true;
229             default:
230                 return false;
231         }
232     }
233 
234     public static final int NOT_DIGIT = 10;
235 
236     public static final String ELLIPSIS = "\u2026";
237 
238     public static final char MINUS_SIGN = '\u2212';
239 
240     /**
241      * Map key id to digit or NOT_DIGIT
242      * Pure function.
243      */
digVal(int id)244     public static int digVal(int id) {
245         switch (id) {
246         case R.id.digit_0:
247             return 0;
248         case R.id.digit_1:
249             return 1;
250         case R.id.digit_2:
251             return 2;
252         case R.id.digit_3:
253             return 3;
254         case R.id.digit_4:
255             return 4;
256         case R.id.digit_5:
257             return 5;
258         case R.id.digit_6:
259             return 6;
260         case R.id.digit_7:
261             return 7;
262         case R.id.digit_8:
263             return 8;
264         case R.id.digit_9:
265             return 9;
266         default:
267             return NOT_DIGIT;
268         }
269     }
270 
271     /**
272      * Map digit to corresponding key.  Inverse of above.
273      * Pure function.
274      */
keyForDigVal(int v)275     public static int keyForDigVal(int v) {
276         switch(v) {
277         case 0:
278             return R.id.digit_0;
279         case 1:
280             return R.id.digit_1;
281         case 2:
282             return R.id.digit_2;
283         case 3:
284             return R.id.digit_3;
285         case 4:
286             return R.id.digit_4;
287         case 5:
288             return R.id.digit_5;
289         case 6:
290             return R.id.digit_6;
291         case 7:
292             return R.id.digit_7;
293         case 8:
294             return R.id.digit_8;
295         case 9:
296             return R.id.digit_9;
297         default:
298             return View.NO_ID;
299         }
300     }
301 
302     // The following two are only used for recognizing additional
303     // input characters from a physical keyboard.  They are not used
304     // for output internationalization.
305     private static char mDecimalPt;
306 
307     private static char mPiChar;
308 
309     /**
310      * Character used as a placeholder for digits that are currently unknown in a result that
311      * is being computed.  We initially generate blanks, and then use this as a replacement
312      * during final translation.
313      * <p/>
314      * Note: the character must correspond closely to the width of a digit,
315      * otherwise the UI will visibly shift once the computation is finished.
316      */
317     private static final char CHAR_DIGIT_UNKNOWN = '\u2007';
318 
319     /**
320      * Map typed function name strings to corresponding button ids.
321      * We (now redundantly?) include both localized and English names.
322      */
323     private static HashMap<String, Integer> sKeyValForFun;
324 
325     /**
326      * Result string corresponding to a character in the calculator result.
327      * The string values in the map are expected to be one character long.
328      */
329     private static HashMap<Character, String> sOutputForResultChar;
330 
331     /**
332      * Locale string corresponding to preceding map and character constants.
333      * We recompute the map if this is not the current locale.
334      */
335     private static String sLocaleForMaps = "none";
336 
337     /**
338      * Activity to use for looking up buttons.
339      */
340     private static Activity mActivity;
341 
342     /**
343      * Set acttivity used for looking up button labels.
344      * Call only from UI thread.
345      */
setActivity(Activity a)346     public static void setActivity(Activity a) {
347         mActivity = a;
348     }
349 
350     /**
351      * Return the button id corresponding to the supplied character or return NO_ID.
352      * Called only by UI thread.
353      */
keyForChar(char c)354     public static int keyForChar(char c) {
355         validateMaps();
356         if (Character.isDigit(c)) {
357             int i = Character.digit(c, 10);
358             return KeyMaps.keyForDigVal(i);
359         }
360         switch (c) {
361             case '.':
362             case ',':
363                 return R.id.dec_point;
364             case '-':
365             case MINUS_SIGN:
366                 return R.id.op_sub;
367             case '+':
368                 return R.id.op_add;
369             case '*':
370             case '\u00D7': // MULTIPLICATION SIGN
371                 return R.id.op_mul;
372             case '/':
373             case '\u00F7': // DIVISION SIGN
374                 return R.id.op_div;
375             // We no longer localize function names, so they can't start with an 'e' or 'p'.
376             case 'e':
377             case 'E':
378                 return R.id.const_e;
379             case 'p':
380             case 'P':
381                 return R.id.const_pi;
382             case '^':
383                 return R.id.op_pow;
384             case '!':
385                 return R.id.op_fact;
386             case '%':
387                 return R.id.op_pct;
388             case '(':
389                 return R.id.lparen;
390             case ')':
391                 return R.id.rparen;
392             default:
393                 if (c == mDecimalPt) return R.id.dec_point;
394                 if (c == mPiChar) return R.id.const_pi;
395                     // pi is not translated, but it might be typable on a Greek keyboard,
396                     // or pasted in, so we check ...
397                 return View.NO_ID;
398         }
399     }
400 
401     /**
402      * Add information corresponding to the given button id to sKeyValForFun, to be used
403      * when mapping keyboard input to button ids.
404      */
addButtonToFunMap(int button_id)405     static void addButtonToFunMap(int button_id) {
406         Button button = (Button)mActivity.findViewById(button_id);
407         sKeyValForFun.put(button.getText().toString(), button_id);
408     }
409 
410     /**
411      * Add information corresponding to the given button to sOutputForResultChar, to be used
412      * when translating numbers on output.
413      */
addButtonToOutputMap(char c, int button_id)414     static void addButtonToOutputMap(char c, int button_id) {
415         Button button = (Button)mActivity.findViewById(button_id);
416         sOutputForResultChar.put(c, button.getText().toString());
417     }
418 
419     // Ensure that the preceding map and character constants are
420     // initialized and correspond to the current locale.
421     // Called only by a single thread, namely the UI thread.
validateMaps()422     static void validateMaps() {
423         Locale locale = Locale.getDefault();
424         String lname = locale.toString();
425         if (lname != sLocaleForMaps) {
426             Log.v ("Calculator", "Setting local to: " + lname);
427             sKeyValForFun = new HashMap<String, Integer>();
428             sKeyValForFun.put("sin", R.id.fun_sin);
429             sKeyValForFun.put("cos", R.id.fun_cos);
430             sKeyValForFun.put("tan", R.id.fun_tan);
431             sKeyValForFun.put("arcsin", R.id.fun_arcsin);
432             sKeyValForFun.put("arccos", R.id.fun_arccos);
433             sKeyValForFun.put("arctan", R.id.fun_arctan);
434             sKeyValForFun.put("asin", R.id.fun_arcsin);
435             sKeyValForFun.put("acos", R.id.fun_arccos);
436             sKeyValForFun.put("atan", R.id.fun_arctan);
437             sKeyValForFun.put("ln", R.id.fun_ln);
438             sKeyValForFun.put("log", R.id.fun_log);
439             sKeyValForFun.put("sqrt", R.id.op_sqrt); // special treatment
440             addButtonToFunMap(R.id.fun_sin);
441             addButtonToFunMap(R.id.fun_cos);
442             addButtonToFunMap(R.id.fun_tan);
443             addButtonToFunMap(R.id.fun_arcsin);
444             addButtonToFunMap(R.id.fun_arccos);
445             addButtonToFunMap(R.id.fun_arctan);
446             addButtonToFunMap(R.id.fun_ln);
447             addButtonToFunMap(R.id.fun_log);
448 
449             // Set locale-dependent character "constants"
450             mDecimalPt =
451                 DecimalFormatSymbols.getInstance().getDecimalSeparator();
452                 // We recognize this in keyboard input, even if we use
453                 // a different character.
454             Resources res = mActivity.getResources();
455             mPiChar = 0;
456             String piString = res.getString(R.string.const_pi);
457             if (piString.length() == 1) {
458                 mPiChar = piString.charAt(0);
459             }
460 
461             sOutputForResultChar = new HashMap<Character, String>();
462             sOutputForResultChar.put('e', "E");
463             sOutputForResultChar.put('E', "E");
464             sOutputForResultChar.put(' ', String.valueOf(CHAR_DIGIT_UNKNOWN));
465             sOutputForResultChar.put(ELLIPSIS.charAt(0), ELLIPSIS);
466             sOutputForResultChar.put('/', "/");
467                         // Translate numbers for fraction display, but not
468                         // the separating slash, which appears to be
469                         // universal.
470             addButtonToOutputMap('-', R.id.op_sub);
471             addButtonToOutputMap('.', R.id.dec_point);
472             for (int i = 0; i <= 9; ++i) {
473                 addButtonToOutputMap((char)('0' + i), keyForDigVal(i));
474             }
475 
476             sLocaleForMaps = lname;
477 
478         }
479     }
480 
481     /**
482      * Return function button id for the substring of s starting at pos and ending with
483      * the next "(".  Return NO_ID if there is none.
484      * We currently check for both (possibly localized) button labels, and standard
485      * English names.  (They should currently be the same, and hence this is currently redundant.)
486      * Callable only from UI thread.
487      */
funForString(String s, int pos)488     public static int funForString(String s, int pos) {
489         validateMaps();
490         int parenPos = s.indexOf('(', pos);
491         if (parenPos != -1) {
492             String funString = s.substring(pos, parenPos);
493             Integer keyValue = sKeyValForFun.get(funString);
494             if (keyValue == null) return View.NO_ID;
495             return keyValue;
496         }
497         return View.NO_ID;
498     }
499 
500     /**
501      * Return the localization of the string s representing a numeric answer.
502      * Callable only from UI thread.
503      */
translateResult(String s)504     public static String translateResult(String s) {
505         StringBuilder result = new StringBuilder();
506         int len = s.length();
507         validateMaps();
508         for (int i = 0; i < len; ++i) {
509             char c = s.charAt(i);
510             String translation = sOutputForResultChar.get(c);
511             if (translation == null) {
512                 // Should not get here.  Report if we do.
513                 Log.v("Calculator", "Bad character:" + c);
514                 result.append(String.valueOf(c));
515             } else {
516                 result.append(translation);
517             }
518         }
519         return result.toString();
520     }
521 
522 }
523