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_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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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.
    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      */
    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      */
    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