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
17package com.android.calculator2;
18
19import android.content.res.Resources;
20import android.content.Context;
21import android.app.Activity;
22import android.util.Log;
23import android.view.View;
24import android.widget.Button;
25
26import java.text.DecimalFormatSymbols;
27import java.util.HashMap;
28import 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 */
37public class KeyMaps {
38    /**
39     * Map key id to corresponding (internationalized) display string.
40     * Pure function.
41     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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