Home | History | Annotate | Download | only in calculator2
      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      */
     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