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