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_sub:
86                 return context.getString(R.string.op_sub);
87             case R.id.op_sqr:
88                 // Button label doesn't work.
89                 return context.getString(R.string.squared);
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 a single byte, somewhat human readable, description.
119      * Used to serialize expressions in the database.
120      * The result is in the range 0x20-0x7f.
121      */
toByte(int id)122     public static byte toByte(int id) {
123         char result;
124         // We only use characters with single-byte UTF8 encodings in the range 0x20-0x7F.
125         switch(id) {
126             case R.id.const_pi:
127                 result = 'p';
128                 break;
129             case R.id.const_e:
130                 result = 'e';
131                 break;
132             case R.id.op_sqrt:
133                 result = 'r';
134                 break;
135             case R.id.op_fact:
136                 result = '!';
137                 break;
138             case R.id.op_pct:
139                 result = '%';
140                 break;
141             case R.id.fun_sin:
142                 result = 's';
143                 break;
144             case R.id.fun_cos:
145                 result = 'c';
146                 break;
147             case R.id.fun_tan:
148                 result = 't';
149                 break;
150             case R.id.fun_arcsin:
151                 result = 'S';
152                 break;
153             case R.id.fun_arccos:
154                 result = 'C';
155                 break;
156             case R.id.fun_arctan:
157                 result = 'T';
158                 break;
159             case R.id.fun_ln:
160                 result = 'l';
161                 break;
162             case R.id.fun_log:
163                 result = 'L';
164                 break;
165             case R.id.fun_exp:
166                 result = 'E';
167                 break;
168             case R.id.lparen:
169                 result = '(';
170                 break;
171             case R.id.rparen:
172                 result = ')';
173                 break;
174             case R.id.op_pow:
175                 result = '^';
176                 break;
177             case R.id.op_mul:
178                 result = '*';
179                 break;
180             case R.id.op_div:
181                 result = '/';
182                 break;
183             case R.id.op_add:
184                 result = '+';
185                 break;
186             case R.id.op_sub:
187                 result = '-';
188                 break;
189             case R.id.op_sqr:
190                 result = '2';
191                 break;
192             default:
193                 throw new AssertionError("Unexpected key id");
194         }
195         return (byte)result;
196     }
197 
198     /**
199      * Map single byte encoding generated by key id generated by toByte back to
200      * key id.
201      */
fromByte(byte b)202     public static int fromByte(byte b) {
203         switch((char)b) {
204             case 'p':
205                 return R.id.const_pi;
206             case 'e':
207                 return R.id.const_e;
208             case 'r':
209                 return R.id.op_sqrt;
210             case '!':
211                 return R.id.op_fact;
212             case '%':
213                 return R.id.op_pct;
214             case 's':
215                 return R.id.fun_sin;
216             case 'c':
217                 return R.id.fun_cos;
218             case 't':
219                 return R.id.fun_tan;
220             case 'S':
221                 return R.id.fun_arcsin;
222             case 'C':
223                 return R.id.fun_arccos;
224             case 'T':
225                 return R.id.fun_arctan;
226             case 'l':
227                 return R.id.fun_ln;
228             case 'L':
229                 return R.id.fun_log;
230             case 'E':
231                 return R.id.fun_exp;
232             case '(':
233                 return R.id.lparen;
234             case ')':
235                 return R.id.rparen;
236             case '^':
237                 return R.id.op_pow;
238             case '*':
239                 return R.id.op_mul;
240             case '/':
241                 return R.id.op_div;
242             case '+':
243                 return R.id.op_add;
244             case '-':
245                 return R.id.op_sub;
246             case '2':
247                 return R.id.op_sqr;
248             default:
249                 throw new AssertionError("Unexpected single byte operator encoding");
250         }
251     }
252 
253     /**
254      * Map key id to corresponding (internationalized) descriptive string that can be used
255      * to correctly read back a formula.
256      * Only used for operators and individual characters; not used inside constants.
257      * Returns null when we don't need a descriptive string.
258      * Pure function.
259      */
toDescriptiveString(Context context, int id)260     public static String toDescriptiveString(Context context, int id) {
261         switch(id) {
262             case R.id.op_fact:
263                 return context.getString(R.string.desc_op_fact);
264             case R.id.fun_sin:
265                 return context.getString(R.string.desc_fun_sin)
266                         + " " + context.getString(R.string.desc_lparen);
267             case R.id.fun_cos:
268                 return context.getString(R.string.desc_fun_cos)
269                         + " " + context.getString(R.string.desc_lparen);
270             case R.id.fun_tan:
271                 return context.getString(R.string.desc_fun_tan)
272                         + " " + context.getString(R.string.desc_lparen);
273             case R.id.fun_arcsin:
274                 return context.getString(R.string.desc_fun_arcsin)
275                         + " " + context.getString(R.string.desc_lparen);
276             case R.id.fun_arccos:
277                 return context.getString(R.string.desc_fun_arccos)
278                         + " " + context.getString(R.string.desc_lparen);
279             case R.id.fun_arctan:
280                 return context.getString(R.string.desc_fun_arctan)
281                         + " " + context.getString(R.string.desc_lparen);
282             case R.id.fun_ln:
283                 return context.getString(R.string.desc_fun_ln)
284                         + " " + context.getString(R.string.desc_lparen);
285             case R.id.fun_log:
286                 return context.getString(R.string.desc_fun_log)
287                         + " " + context.getString(R.string.desc_lparen);
288             case R.id.fun_exp:
289                 return context.getString(R.string.desc_fun_exp)
290                         + " " + context.getString(R.string.desc_lparen);
291             case R.id.lparen:
292                 return context.getString(R.string.desc_lparen);
293             case R.id.rparen:
294                 return context.getString(R.string.desc_rparen);
295             case R.id.op_pow:
296                 return context.getString(R.string.desc_op_pow);
297             case R.id.dec_point:
298                 return context.getString(R.string.desc_dec_point);
299             default:
300                 return null;
301         }
302     }
303 
304     /**
305      * Does a button id correspond to a binary operator?
306      * Pure function.
307      */
isBinary(int id)308     public static boolean isBinary(int id) {
309         switch(id) {
310             case R.id.op_pow:
311             case R.id.op_mul:
312             case R.id.op_div:
313             case R.id.op_add:
314             case R.id.op_sub:
315                 return true;
316             default:
317                 return false;
318         }
319     }
320 
321     /**
322      * Does a button id correspond to a trig function?
323      * Pure function.
324      */
isTrigFunc(int id)325     public static boolean isTrigFunc(int id) {
326         switch(id) {
327             case R.id.fun_sin:
328             case R.id.fun_cos:
329             case R.id.fun_tan:
330             case R.id.fun_arcsin:
331             case R.id.fun_arccos:
332             case R.id.fun_arctan:
333                 return true;
334             default:
335                 return false;
336         }
337     }
338 
339     /**
340      * Does a button id correspond to a function that introduces an implicit lparen?
341      * Pure function.
342      */
isFunc(int id)343     public static boolean isFunc(int id) {
344         if (isTrigFunc(id)) {
345             return true;
346         }
347         switch(id) {
348             case R.id.fun_ln:
349             case R.id.fun_log:
350             case R.id.fun_exp:
351                 return true;
352             default:
353                 return false;
354         }
355     }
356 
357     /**
358      * Does a button id correspond to a prefix operator?
359      * Pure function.
360      */
isPrefix(int id)361     public static boolean isPrefix(int id) {
362         switch(id) {
363             case R.id.op_sqrt:
364             case R.id.op_sub:
365                 return true;
366             default:
367                 return false;
368         }
369     }
370 
371     /**
372      * Does a button id correspond to a suffix operator?
373      */
isSuffix(int id)374     public static boolean isSuffix(int id) {
375         switch (id) {
376             case R.id.op_fact:
377             case R.id.op_pct:
378             case R.id.op_sqr:
379                 return true;
380             default:
381                 return false;
382         }
383     }
384 
385     public static final int NOT_DIGIT = 10;
386 
387     public static final String ELLIPSIS = "\u2026";
388 
389     public static final char MINUS_SIGN = '\u2212';
390 
391     /**
392      * Map key id to digit or NOT_DIGIT
393      * Pure function.
394      */
digVal(int id)395     public static int digVal(int id) {
396         switch (id) {
397         case R.id.digit_0:
398             return 0;
399         case R.id.digit_1:
400             return 1;
401         case R.id.digit_2:
402             return 2;
403         case R.id.digit_3:
404             return 3;
405         case R.id.digit_4:
406             return 4;
407         case R.id.digit_5:
408             return 5;
409         case R.id.digit_6:
410             return 6;
411         case R.id.digit_7:
412             return 7;
413         case R.id.digit_8:
414             return 8;
415         case R.id.digit_9:
416             return 9;
417         default:
418             return NOT_DIGIT;
419         }
420     }
421 
422     /**
423      * Map digit to corresponding key.  Inverse of above.
424      * Pure function.
425      */
keyForDigVal(int v)426     public static int keyForDigVal(int v) {
427         switch(v) {
428         case 0:
429             return R.id.digit_0;
430         case 1:
431             return R.id.digit_1;
432         case 2:
433             return R.id.digit_2;
434         case 3:
435             return R.id.digit_3;
436         case 4:
437             return R.id.digit_4;
438         case 5:
439             return R.id.digit_5;
440         case 6:
441             return R.id.digit_6;
442         case 7:
443             return R.id.digit_7;
444         case 8:
445             return R.id.digit_8;
446         case 9:
447             return R.id.digit_9;
448         default:
449             return View.NO_ID;
450         }
451     }
452 
453     // The following two are only used for recognizing additional
454     // input characters from a physical keyboard.  They are not used
455     // for output internationalization.
456     private static char mDecimalPt;
457 
458     private static char mPiChar;
459 
460     /**
461      * Character used as a placeholder for digits that are currently unknown in a result that
462      * is being computed.  We initially generate blanks, and then use this as a replacement
463      * during final translation.
464      * <p/>
465      * Note: the character must correspond closely to the width of a digit,
466      * otherwise the UI will visibly shift once the computation is finished.
467      */
468     private static final char CHAR_DIGIT_UNKNOWN = '\u2007';
469 
470     /**
471      * Map typed function name strings to corresponding button ids.
472      * We (now redundantly?) include both localized and English names.
473      */
474     private static HashMap<String, Integer> sKeyValForFun;
475 
476     /**
477      * Result string corresponding to a character in the calculator result.
478      * The string values in the map are expected to be one character long.
479      */
480     private static HashMap<Character, String> sOutputForResultChar;
481 
482     /**
483      * Locale corresponding to preceding map and character constants.
484      * We recompute the map if this is not the current locale.
485      */
486     private static Locale sLocaleForMaps = null;
487 
488     /**
489      * Activity to use for looking up buttons.
490      */
491     private static Activity mActivity;
492 
493     /**
494      * Set acttivity used for looking up button labels.
495      * Call only from UI thread.
496      */
setActivity(Activity a)497     public static void setActivity(Activity a) {
498         mActivity = a;
499     }
500 
501     /**
502      * Return the button id corresponding to the supplied character or return NO_ID.
503      * Called only by UI thread.
504      */
keyForChar(char c)505     public static int keyForChar(char c) {
506         validateMaps();
507         if (Character.isDigit(c)) {
508             int i = Character.digit(c, 10);
509             return KeyMaps.keyForDigVal(i);
510         }
511         switch (c) {
512             case '.':
513             case ',':
514                 return R.id.dec_point;
515             case '-':
516             case MINUS_SIGN:
517                 return R.id.op_sub;
518             case '+':
519                 return R.id.op_add;
520             case '*':
521             case '\u00D7': // MULTIPLICATION SIGN
522                 return R.id.op_mul;
523             case '/':
524             case '\u00F7': // DIVISION SIGN
525                 return R.id.op_div;
526             // We no longer localize function names, so they can't start with an 'e' or 'p'.
527             case 'e':
528             case 'E':
529                 return R.id.const_e;
530             case 'p':
531             case 'P':
532                 return R.id.const_pi;
533             case '^':
534                 return R.id.op_pow;
535             case '!':
536                 return R.id.op_fact;
537             case '%':
538                 return R.id.op_pct;
539             case '(':
540                 return R.id.lparen;
541             case ')':
542                 return R.id.rparen;
543             default:
544                 if (c == mDecimalPt) return R.id.dec_point;
545                 if (c == mPiChar) return R.id.const_pi;
546                     // pi is not translated, but it might be typable on a Greek keyboard,
547                     // or pasted in, so we check ...
548                 return View.NO_ID;
549         }
550     }
551 
552     /**
553      * Add information corresponding to the given button id to sKeyValForFun, to be used
554      * when mapping keyboard input to button ids.
555      */
addButtonToFunMap(int button_id)556     static void addButtonToFunMap(int button_id) {
557         Button button = (Button)mActivity.findViewById(button_id);
558         sKeyValForFun.put(button.getText().toString(), button_id);
559     }
560 
561     /**
562      * Add information corresponding to the given button to sOutputForResultChar, to be used
563      * when translating numbers on output.
564      */
addButtonToOutputMap(char c, int button_id)565     static void addButtonToOutputMap(char c, int button_id) {
566         Button button = (Button)mActivity.findViewById(button_id);
567         sOutputForResultChar.put(c, button.getText().toString());
568     }
569 
570     /**
571      * Ensure that the preceding map and character constants correspond to the current locale.
572      * Called only by UI thread.
573      */
validateMaps()574     static void validateMaps() {
575         Locale locale = Locale.getDefault();
576         if (!locale.equals(sLocaleForMaps)) {
577             Log.v ("Calculator", "Setting locale to: " + locale.toLanguageTag());
578             sKeyValForFun = new HashMap<String, Integer>();
579             sKeyValForFun.put("sin", R.id.fun_sin);
580             sKeyValForFun.put("cos", R.id.fun_cos);
581             sKeyValForFun.put("tan", R.id.fun_tan);
582             sKeyValForFun.put("arcsin", R.id.fun_arcsin);
583             sKeyValForFun.put("arccos", R.id.fun_arccos);
584             sKeyValForFun.put("arctan", R.id.fun_arctan);
585             sKeyValForFun.put("asin", R.id.fun_arcsin);
586             sKeyValForFun.put("acos", R.id.fun_arccos);
587             sKeyValForFun.put("atan", R.id.fun_arctan);
588             sKeyValForFun.put("ln", R.id.fun_ln);
589             sKeyValForFun.put("log", R.id.fun_log);
590             sKeyValForFun.put("sqrt", R.id.op_sqrt); // special treatment
591             addButtonToFunMap(R.id.fun_sin);
592             addButtonToFunMap(R.id.fun_cos);
593             addButtonToFunMap(R.id.fun_tan);
594             addButtonToFunMap(R.id.fun_arcsin);
595             addButtonToFunMap(R.id.fun_arccos);
596             addButtonToFunMap(R.id.fun_arctan);
597             addButtonToFunMap(R.id.fun_ln);
598             addButtonToFunMap(R.id.fun_log);
599 
600             // Set locale-dependent character "constants"
601             mDecimalPt =
602                 DecimalFormatSymbols.getInstance().getDecimalSeparator();
603                 // We recognize this in keyboard input, even if we use
604                 // a different character.
605             Resources res = mActivity.getResources();
606             mPiChar = 0;
607             String piString = res.getString(R.string.const_pi);
608             if (piString.length() == 1) {
609                 mPiChar = piString.charAt(0);
610             }
611 
612             sOutputForResultChar = new HashMap<Character, String>();
613             sOutputForResultChar.put('e', "E");
614             sOutputForResultChar.put('E', "E");
615             sOutputForResultChar.put(' ', String.valueOf(CHAR_DIGIT_UNKNOWN));
616             sOutputForResultChar.put(ELLIPSIS.charAt(0), ELLIPSIS);
617             // Translate numbers for fraction display, but not the separating slash, which appears
618             // to be universal.  We also do not translate the ln, sqrt, pi
619             sOutputForResultChar.put('/', "/");
620             sOutputForResultChar.put('(', "(");
621             sOutputForResultChar.put(')', ")");
622             sOutputForResultChar.put('l', "l");
623             sOutputForResultChar.put('n', "n");
624             sOutputForResultChar.put(',',
625                     String.valueOf(DecimalFormatSymbols.getInstance().getGroupingSeparator()));
626             sOutputForResultChar.put('\u221A', "\u221A"); // SQUARE ROOT
627             sOutputForResultChar.put('\u03C0', "\u03C0"); // GREEK SMALL LETTER PI
628             addButtonToOutputMap('-', R.id.op_sub);
629             addButtonToOutputMap('.', R.id.dec_point);
630             for (int i = 0; i <= 9; ++i) {
631                 addButtonToOutputMap((char)('0' + i), keyForDigVal(i));
632             }
633 
634             sLocaleForMaps = locale;
635 
636         }
637     }
638 
639     /**
640      * Return function button id for the substring of s starting at pos and ending with
641      * the next "(".  Return NO_ID if there is none.
642      * We currently check for both (possibly localized) button labels, and standard
643      * English names.  (They should currently be the same, and hence this is currently redundant.)
644      * Callable only from UI thread.
645      */
funForString(String s, int pos)646     public static int funForString(String s, int pos) {
647         validateMaps();
648         int parenPos = s.indexOf('(', pos);
649         if (parenPos != -1) {
650             String funString = s.substring(pos, parenPos);
651             Integer keyValue = sKeyValForFun.get(funString);
652             if (keyValue == null) return View.NO_ID;
653             return keyValue;
654         }
655         return View.NO_ID;
656     }
657 
658     /**
659      * Return the localization of the string s representing a numeric answer.
660      * Callable only from UI thread.
661      * A trailing e is treated as the mathematical constant, not an exponent.
662      */
translateResult(String s)663     public static String translateResult(String s) {
664         StringBuilder result = new StringBuilder();
665         int len = s.length();
666         validateMaps();
667         for (int i = 0; i < len; ++i) {
668             char c = s.charAt(i);
669             if (i < len - 1 || c != 'e') {
670                 String translation = sOutputForResultChar.get(c);
671                 if (translation == null) {
672                     // Should not get here.  Report if we do.
673                     Log.v("Calculator", "Bad character:" + c);
674                     result.append(String.valueOf(c));
675                 } else {
676                     result.append(translation);
677                 }
678             }
679         }
680         return result.toString();
681     }
682 
683 }
684