blob: cdfe4e4f452c4dccda42841aa59c86a486fbbd41 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.calculator2;
import android.content.res.Resources;
import android.content.Context;
import android.app.Activity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import java.text.DecimalFormatSymbols;
import java.util.HashMap;
import java.util.Locale;
/**
* Collection of mapping functions between key ids, characters, internationalized
* and non-internationalized characters, etc.
* <p>
* KeyMap instances are not meaningful; everything here is static.
* All functions are either pure, or are assumed to be called only from a single UI thread.
*/
public class KeyMaps {
/**
* Map key id to corresponding (internationalized) display string.
* Pure function.
*/
public static String toString(Context context, int id) {
switch(id) {
case R.id.const_pi:
return context.getString(R.string.const_pi);
case R.id.const_e:
return context.getString(R.string.const_e);
case R.id.op_sqrt:
return context.getString(R.string.op_sqrt);
case R.id.op_fact:
return context.getString(R.string.op_fact);
case R.id.op_pct:
return context.getString(R.string.op_pct);
case R.id.fun_sin:
return context.getString(R.string.fun_sin) + context.getString(R.string.lparen);
case R.id.fun_cos:
return context.getString(R.string.fun_cos) + context.getString(R.string.lparen);
case R.id.fun_tan:
return context.getString(R.string.fun_tan) + context.getString(R.string.lparen);
case R.id.fun_arcsin:
return context.getString(R.string.fun_arcsin) + context.getString(R.string.lparen);
case R.id.fun_arccos:
return context.getString(R.string.fun_arccos) + context.getString(R.string.lparen);
case R.id.fun_arctan:
return context.getString(R.string.fun_arctan) + context.getString(R.string.lparen);
case R.id.fun_ln:
return context.getString(R.string.fun_ln) + context.getString(R.string.lparen);
case R.id.fun_log:
return context.getString(R.string.fun_log) + context.getString(R.string.lparen);
case R.id.fun_exp:
// Button label doesn't work.
return context.getString(R.string.exponential) + context.getString(R.string.lparen);
case R.id.lparen:
return context.getString(R.string.lparen);
case R.id.rparen:
return context.getString(R.string.rparen);
case R.id.op_pow:
return context.getString(R.string.op_pow);
case R.id.op_mul:
return context.getString(R.string.op_mul);
case R.id.op_div:
return context.getString(R.string.op_div);
case R.id.op_add:
return context.getString(R.string.op_add);
case R.id.op_sub:
return context.getString(R.string.op_sub);
case R.id.op_sqr:
// Button label doesn't work.
return context.getString(R.string.squared);
case R.id.dec_point:
return context.getString(R.string.dec_point);
case R.id.digit_0:
return context.getString(R.string.digit_0);
case R.id.digit_1:
return context.getString(R.string.digit_1);
case R.id.digit_2:
return context.getString(R.string.digit_2);
case R.id.digit_3:
return context.getString(R.string.digit_3);
case R.id.digit_4:
return context.getString(R.string.digit_4);
case R.id.digit_5:
return context.getString(R.string.digit_5);
case R.id.digit_6:
return context.getString(R.string.digit_6);
case R.id.digit_7:
return context.getString(R.string.digit_7);
case R.id.digit_8:
return context.getString(R.string.digit_8);
case R.id.digit_9:
return context.getString(R.string.digit_9);
default:
return "";
}
}
/**
* Map key id to a single byte, somewhat human readable, description.
* Used to serialize expressions in the database.
* The result is in the range 0x20-0x7f.
*/
public static byte toByte(int id) {
char result;
// We only use characters with single-byte UTF8 encodings in the range 0x20-0x7F.
switch(id) {
case R.id.const_pi:
result = 'p';
break;
case R.id.const_e:
result = 'e';
break;
case R.id.op_sqrt:
result = 'r';
break;
case R.id.op_fact:
result = '!';
break;
case R.id.op_pct:
result = '%';
break;
case R.id.fun_sin:
result = 's';
break;
case R.id.fun_cos:
result = 'c';
break;
case R.id.fun_tan:
result = 't';
break;
case R.id.fun_arcsin:
result = 'S';
break;
case R.id.fun_arccos:
result = 'C';
break;
case R.id.fun_arctan:
result = 'T';
break;
case R.id.fun_ln:
result = 'l';
break;
case R.id.fun_log:
result = 'L';
break;
case R.id.fun_exp:
result = 'E';
break;
case R.id.lparen:
result = '(';
break;
case R.id.rparen:
result = ')';
break;
case R.id.op_pow:
result = '^';
break;
case R.id.op_mul:
result = '*';
break;
case R.id.op_div:
result = '/';
break;
case R.id.op_add:
result = '+';
break;
case R.id.op_sub:
result = '-';
break;
case R.id.op_sqr:
result = '2';
break;
default:
throw new AssertionError("Unexpected key id");
}
return (byte)result;
}
/**
* Map single byte encoding generated by key id generated by toByte back to
* key id.
*/
public static int fromByte(byte b) {
switch((char)b) {
case 'p':
return R.id.const_pi;
case 'e':
return R.id.const_e;
case 'r':
return R.id.op_sqrt;
case '!':
return R.id.op_fact;
case '%':
return R.id.op_pct;
case 's':
return R.id.fun_sin;
case 'c':
return R.id.fun_cos;
case 't':
return R.id.fun_tan;
case 'S':
return R.id.fun_arcsin;
case 'C':
return R.id.fun_arccos;
case 'T':
return R.id.fun_arctan;
case 'l':
return R.id.fun_ln;
case 'L':
return R.id.fun_log;
case 'E':
return R.id.fun_exp;
case '(':
return R.id.lparen;
case ')':
return R.id.rparen;
case '^':
return R.id.op_pow;
case '*':
return R.id.op_mul;
case '/':
return R.id.op_div;
case '+':
return R.id.op_add;
case '-':
return R.id.op_sub;
case '2':
return R.id.op_sqr;
default:
throw new AssertionError("Unexpected single byte operator encoding");
}
}
/**
* Map key id to corresponding (internationalized) descriptive string that can be used
* to correctly read back a formula.
* Only used for operators and individual characters; not used inside constants.
* Returns null when we don't need a descriptive string.
* Pure function.
*/
public static String toDescriptiveString(Context context, int id) {
switch(id) {
case R.id.op_fact:
return context.getString(R.string.desc_op_fact);
case R.id.fun_sin:
return context.getString(R.string.desc_fun_sin)
+ " " + context.getString(R.string.desc_lparen);
case R.id.fun_cos:
return context.getString(R.string.desc_fun_cos)
+ " " + context.getString(R.string.desc_lparen);
case R.id.fun_tan:
return context.getString(R.string.desc_fun_tan)
+ " " + context.getString(R.string.desc_lparen);
case R.id.fun_arcsin:
return context.getString(R.string.desc_fun_arcsin)
+ " " + context.getString(R.string.desc_lparen);
case R.id.fun_arccos:
return context.getString(R.string.desc_fun_arccos)
+ " " + context.getString(R.string.desc_lparen);
case R.id.fun_arctan:
return context.getString(R.string.desc_fun_arctan)
+ " " + context.getString(R.string.desc_lparen);
case R.id.fun_ln:
return context.getString(R.string.desc_fun_ln)
+ " " + context.getString(R.string.desc_lparen);
case R.id.fun_log:
return context.getString(R.string.desc_fun_log)
+ " " + context.getString(R.string.desc_lparen);
case R.id.fun_exp:
return context.getString(R.string.desc_fun_exp)
+ " " + context.getString(R.string.desc_lparen);
case R.id.lparen:
return context.getString(R.string.desc_lparen);
case R.id.rparen:
return context.getString(R.string.desc_rparen);
case R.id.op_pow:
return context.getString(R.string.desc_op_pow);
case R.id.dec_point:
return context.getString(R.string.desc_dec_point);
default:
return null;
}
}
/**
* Does a button id correspond to a binary operator?
* Pure function.
*/
public static boolean isBinary(int id) {
switch(id) {
case R.id.op_pow:
case R.id.op_mul:
case R.id.op_div:
case R.id.op_add:
case R.id.op_sub:
return true;
default:
return false;
}
}
/**
* Does a button id correspond to a trig function?
* Pure function.
*/
public static boolean isTrigFunc(int id) {
switch(id) {
case R.id.fun_sin:
case R.id.fun_cos:
case R.id.fun_tan:
case R.id.fun_arcsin:
case R.id.fun_arccos:
case R.id.fun_arctan:
return true;
default:
return false;
}
}
/**
* Does a button id correspond to a function that introduces an implicit lparen?
* Pure function.
*/
public static boolean isFunc(int id) {
if (isTrigFunc(id)) {
return true;
}
switch(id) {
case R.id.fun_ln:
case R.id.fun_log:
case R.id.fun_exp:
return true;
default:
return false;
}
}
/**
* Does a button id correspond to a prefix operator?
* Pure function.
*/
public static boolean isPrefix(int id) {
switch(id) {
case R.id.op_sqrt:
case R.id.op_sub:
return true;
default:
return false;
}
}
/**
* Does a button id correspond to a suffix operator?
*/
public static boolean isSuffix(int id) {
switch (id) {
case R.id.op_fact:
case R.id.op_pct:
case R.id.op_sqr:
return true;
default:
return false;
}
}
public static final int NOT_DIGIT = 10;
public static final String ELLIPSIS = "\u2026";
public static final char MINUS_SIGN = '\u2212';
/**
* Map key id to digit or NOT_DIGIT
* Pure function.
*/
public static int digVal(int id) {
switch (id) {
case R.id.digit_0:
return 0;
case R.id.digit_1:
return 1;
case R.id.digit_2:
return 2;
case R.id.digit_3:
return 3;
case R.id.digit_4:
return 4;
case R.id.digit_5:
return 5;
case R.id.digit_6:
return 6;
case R.id.digit_7:
return 7;
case R.id.digit_8:
return 8;
case R.id.digit_9:
return 9;
default:
return NOT_DIGIT;
}
}
/**
* Map digit to corresponding key. Inverse of above.
* Pure function.
*/
public static int keyForDigVal(int v) {
switch(v) {
case 0:
return R.id.digit_0;
case 1:
return R.id.digit_1;
case 2:
return R.id.digit_2;
case 3:
return R.id.digit_3;
case 4:
return R.id.digit_4;
case 5:
return R.id.digit_5;
case 6:
return R.id.digit_6;
case 7:
return R.id.digit_7;
case 8:
return R.id.digit_8;
case 9:
return R.id.digit_9;
default:
return View.NO_ID;
}
}
// The following two are only used for recognizing additional
// input characters from a physical keyboard. They are not used
// for output internationalization.
private static char mDecimalPt;
private static char mPiChar;
/**
* Character used as a placeholder for digits that are currently unknown in a result that
* is being computed. We initially generate blanks, and then use this as a replacement
* during final translation.
* <p/>
* Note: the character must correspond closely to the width of a digit,
* otherwise the UI will visibly shift once the computation is finished.
*/
private static final char CHAR_DIGIT_UNKNOWN = '\u2007';
/**
* Map typed function name strings to corresponding button ids.
* We (now redundantly?) include both localized and English names.
*/
private static HashMap<String, Integer> sKeyValForFun;
/**
* Result string corresponding to a character in the calculator result.
* The string values in the map are expected to be one character long.
*/
private static HashMap<Character, String> sOutputForResultChar;
/**
* Locale corresponding to preceding map and character constants.
* We recompute the map if this is not the current locale.
*/
private static Locale sLocaleForMaps = null;
/**
* Activity to use for looking up buttons.
*/
private static Activity mActivity;
/**
* Set acttivity used for looking up button labels.
* Call only from UI thread.
*/
public static void setActivity(Activity a) {
mActivity = a;
}
/**
* Return the button id corresponding to the supplied character or return NO_ID.
* Called only by UI thread.
*/
public static int keyForChar(char c) {
validateMaps();
if (Character.isDigit(c)) {
int i = Character.digit(c, 10);
return KeyMaps.keyForDigVal(i);
}
switch (c) {
case '.':
case ',':
return R.id.dec_point;
case '-':
case MINUS_SIGN:
return R.id.op_sub;
case '+':
return R.id.op_add;
case '*':
case '\u00D7': // MULTIPLICATION SIGN
return R.id.op_mul;
case '/':
case '\u00F7': // DIVISION SIGN
return R.id.op_div;
// We no longer localize function names, so they can't start with an 'e' or 'p'.
case 'e':
case 'E':
return R.id.const_e;
case 'p':
case 'P':
return R.id.const_pi;
case '^':
return R.id.op_pow;
case '!':
return R.id.op_fact;
case '%':
return R.id.op_pct;
case '(':
return R.id.lparen;
case ')':
return R.id.rparen;
default:
if (c == mDecimalPt) return R.id.dec_point;
if (c == mPiChar) return R.id.const_pi;
// pi is not translated, but it might be typable on a Greek keyboard,
// or pasted in, so we check ...
return View.NO_ID;
}
}
/**
* Add information corresponding to the given button id to sKeyValForFun, to be used
* when mapping keyboard input to button ids.
*/
static void addButtonToFunMap(int button_id) {
Button button = (Button)mActivity.findViewById(button_id);
sKeyValForFun.put(button.getText().toString(), button_id);
}
/**
* Add information corresponding to the given button to sOutputForResultChar, to be used
* when translating numbers on output.
*/
static void addButtonToOutputMap(char c, int button_id) {
Button button = (Button)mActivity.findViewById(button_id);
sOutputForResultChar.put(c, button.getText().toString());
}
/**
* Ensure that the preceding map and character constants correspond to the current locale.
* Called only by UI thread.
*/
static void validateMaps() {
Locale locale = Locale.getDefault();
if (!locale.equals(sLocaleForMaps)) {
Log.v ("Calculator", "Setting locale to: " + locale.toLanguageTag());
sKeyValForFun = new HashMap<String, Integer>();
sKeyValForFun.put("sin", R.id.fun_sin);
sKeyValForFun.put("cos", R.id.fun_cos);
sKeyValForFun.put("tan", R.id.fun_tan);
sKeyValForFun.put("arcsin", R.id.fun_arcsin);
sKeyValForFun.put("arccos", R.id.fun_arccos);
sKeyValForFun.put("arctan", R.id.fun_arctan);
sKeyValForFun.put("asin", R.id.fun_arcsin);
sKeyValForFun.put("acos", R.id.fun_arccos);
sKeyValForFun.put("atan", R.id.fun_arctan);
sKeyValForFun.put("ln", R.id.fun_ln);
sKeyValForFun.put("log", R.id.fun_log);
sKeyValForFun.put("sqrt", R.id.op_sqrt); // special treatment
addButtonToFunMap(R.id.fun_sin);
addButtonToFunMap(R.id.fun_cos);
addButtonToFunMap(R.id.fun_tan);
addButtonToFunMap(R.id.fun_arcsin);
addButtonToFunMap(R.id.fun_arccos);
addButtonToFunMap(R.id.fun_arctan);
addButtonToFunMap(R.id.fun_ln);
addButtonToFunMap(R.id.fun_log);
// Set locale-dependent character "constants"
mDecimalPt =
DecimalFormatSymbols.getInstance().getDecimalSeparator();
// We recognize this in keyboard input, even if we use
// a different character.
Resources res = mActivity.getResources();
mPiChar = 0;
String piString = res.getString(R.string.const_pi);
if (piString.length() == 1) {
mPiChar = piString.charAt(0);
}
sOutputForResultChar = new HashMap<Character, String>();
sOutputForResultChar.put('e', "E");
sOutputForResultChar.put('E', "E");
sOutputForResultChar.put(' ', String.valueOf(CHAR_DIGIT_UNKNOWN));
sOutputForResultChar.put(ELLIPSIS.charAt(0), ELLIPSIS);
// Translate numbers for fraction display, but not the separating slash, which appears
// to be universal. We also do not translate the ln, sqrt, pi
sOutputForResultChar.put('/', "/");
sOutputForResultChar.put('(', "(");
sOutputForResultChar.put(')', ")");
sOutputForResultChar.put('l', "l");
sOutputForResultChar.put('n', "n");
sOutputForResultChar.put(',',
String.valueOf(DecimalFormatSymbols.getInstance().getGroupingSeparator()));
sOutputForResultChar.put('\u221A', "\u221A"); // SQUARE ROOT
sOutputForResultChar.put('\u03C0', "\u03C0"); // GREEK SMALL LETTER PI
addButtonToOutputMap('-', R.id.op_sub);
addButtonToOutputMap('.', R.id.dec_point);
for (int i = 0; i <= 9; ++i) {
addButtonToOutputMap((char)('0' + i), keyForDigVal(i));
}
sLocaleForMaps = locale;
}
}
/**
* Return function button id for the substring of s starting at pos and ending with
* the next "(". Return NO_ID if there is none.
* We currently check for both (possibly localized) button labels, and standard
* English names. (They should currently be the same, and hence this is currently redundant.)
* Callable only from UI thread.
*/
public static int funForString(String s, int pos) {
validateMaps();
int parenPos = s.indexOf('(', pos);
if (parenPos != -1) {
String funString = s.substring(pos, parenPos);
Integer keyValue = sKeyValForFun.get(funString);
if (keyValue == null) return View.NO_ID;
return keyValue;
}
return View.NO_ID;
}
/**
* Return the localization of the string s representing a numeric answer.
* Callable only from UI thread.
* A trailing e is treated as the mathematical constant, not an exponent.
*/
public static String translateResult(String s) {
StringBuilder result = new StringBuilder();
int len = s.length();
validateMaps();
for (int i = 0; i < len; ++i) {
char c = s.charAt(i);
if (i < len - 1 || c != 'e') {
String translation = sOutputForResultChar.get(c);
if (translation == null) {
// Should not get here. Report if we do.
Log.v("Calculator", "Bad character:" + c);
result.append(String.valueOf(c));
} else {
result.append(translation);
}
}
}
return result.toString();
}
}