summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorHans Boehm <hboehm@google.com>2015-06-29 16:07:15 -0700
committerHans Boehm <hboehm@google.com>2015-07-09 11:05:38 -0700
commit0b9806f624f25e7e0302da4cf55eda21f8c28163 (patch)
tree80918120420737b42c03c14c64c31e5214baf70f /src
parenta8f47d36d5bee2825a5088be754a2fc87b2b6fde (diff)
downloadandroid_packages_apps_ExactCalculator-0b9806f624f25e7e0302da4cf55eda21f8c28163.tar.gz
android_packages_apps_ExactCalculator-0b9806f624f25e7e0302da4cf55eda21f8c28163.tar.bz2
android_packages_apps_ExactCalculator-0b9806f624f25e7e0302da4cf55eda21f8c28163.zip
Support pasting of scientific notation numbers
Bug: 21470972 Support pasting of numbers using scientific notation with 'E'. This is intentionally very restricted to dodge ambiguities with the constant e. We only accept a scientific notation constant if it is 1) Contained within a single pasted text element. 2) Uses capital 'E' to introduce the exponent. 3) Does not contain an explicit '+' in the exponent. We do currently use the same notion of 'digit' as elsewhere, i.e. Character.isDigit(), which might be too general. For consistency, and to make sure that we can recognize machine generated output, this also adds a few more aliases for text input of arithmetic operators. For consistency, always use 'E' internally for scientific notation as well. We ensure that a pasted numeric string is not concatenated with a pre-existing constant. This is a judgment call, but it means that pasting a previous calculator result gets similar treatment whether or not we are still running the same calculator instance. We support limited editing on exponents. Once an exponent is deleted, the only way to restore it is via pasting. The 10^x button produces similar results, though with different operator precedence behavior. Change-Id: I2d0f3dceb641cdad327fd3c3540b5eea38030146
Diffstat (limited to 'src')
-rw-r--r--src/com/android/calculator2/Calculator.java26
-rw-r--r--src/com/android/calculator2/CalculatorExpr.java85
-rw-r--r--src/com/android/calculator2/CalculatorResult.java6
-rw-r--r--src/com/android/calculator2/Evaluator.java55
-rw-r--r--src/com/android/calculator2/KeyMaps.java7
5 files changed, 150 insertions, 29 deletions
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java
index 5cb6685..f1c9835 100644
--- a/src/com/android/calculator2/Calculator.java
+++ b/src/com/android/calculator2/Calculator.java
@@ -903,7 +903,7 @@ public class Calculator extends Activity
* are added to mUnprocessedChars, which is presumed to immediately precede the newly
* added characters.
* @param moreChars Characters to be added.
- * @param explicit These characters were explicitly typed by the user.
+ * @param explicit These characters were explicitly typed by the user, not pasted.
*/
private void addChars(String moreChars, boolean explicit) {
if (mUnprocessedChars != null) {
@@ -911,9 +911,33 @@ public class Calculator extends Activity
}
int current = 0;
int len = moreChars.length();
+ boolean lastWasDigit = false;
while (current < len) {
char c = moreChars.charAt(current);
int k = KeyMaps.keyForChar(c);
+ if (!explicit) {
+ int expEnd;
+ if (lastWasDigit && current !=
+ (expEnd = Evaluator.exponentEnd(moreChars, current))) {
+ // Process scientific notation with 'E' when pasting, in spite of ambiguity
+ // with base of natural log.
+ // Otherwise the 10^x key is the user's friend.
+ mEvaluator.addExponent(moreChars, current, expEnd);
+ current = expEnd;
+ lastWasDigit = false;
+ continue;
+ } else {
+ boolean isDigit = KeyMaps.digVal(k) != KeyMaps.NOT_DIGIT;
+ if (current == 0 && (isDigit || k == R.id.dec_point)
+ && mEvaluator.getExpr().hasTrailingConstant()) {
+ // Refuse to concatenate pasted content to trailing constant.
+ // This makes pasting of calculator results more consistent, whether or
+ // not the old calculator instance is still around.
+ addKeyToExpr(R.id.op_mul);
+ }
+ lastWasDigit = (isDigit || lastWasDigit && k == R.id.dec_point);
+ }
+ }
if (k != View.NO_ID) {
mCurrentButton = findViewById(k);
if (explicit) {
diff --git a/src/com/android/calculator2/CalculatorExpr.java b/src/com/android/calculator2/CalculatorExpr.java
index 2162bb2..3023b5c 100644
--- a/src/com/android/calculator2/CalculatorExpr.java
+++ b/src/com/android/calculator2/CalculatorExpr.java
@@ -79,19 +79,22 @@ class CalculatorExpr {
// Supports addition and removal of trailing characters; hence mutable.
private static class Constant extends Token implements Cloneable {
private boolean mSawDecimal;
- String mWhole; // part before decimal point
- private String mFraction; // part after decimal point
+ String mWhole; // String preceding decimal point.
+ private String mFraction; // String after decimal point.
+ private int mExponent; // Explicit exponent, only generated through addExponent.
Constant() {
mWhole = "";
mFraction = "";
- mSawDecimal = false;
+ mSawDecimal = false;
+ mExponent = 0;
};
Constant(DataInput in) throws IOException {
mWhole = in.readUTF();
mSawDecimal = in.readBoolean();
mFraction = in.readUTF();
+ mExponent = in.readInt();
}
@Override
@@ -100,19 +103,33 @@ class CalculatorExpr {
out.writeUTF(mWhole);
out.writeBoolean(mSawDecimal);
out.writeUTF(mFraction);
+ out.writeInt(mExponent);
}
// Given a button press, append corresponding digit.
// We assume id is a digit or decimal point.
// Just return false if this was the second (or later) decimal point
// in this constant.
+ // Assumes that this constant does not have an exponent.
boolean add(int id) {
if (id == R.id.dec_point) {
- if (mSawDecimal) return false;
+ if (mSawDecimal || mExponent != 0) return false;
mSawDecimal = true;
return true;
}
int val = KeyMaps.digVal(id);
+ if (mExponent != 0) {
+ if (Math.abs(mExponent) <= 10000) {
+ if (mExponent > 0) {
+ mExponent = 10 * mExponent + val;
+ } else {
+ mExponent = 10 * mExponent - val;
+ }
+ return true;
+ } else { // Too large; refuse
+ return false;
+ }
+ }
if (mSawDecimal) {
mFraction += val;
} else {
@@ -121,10 +138,18 @@ class CalculatorExpr {
return true;
}
+ void addExponent(int exp) {
+ // Note that adding a 0 exponent is a no-op. That's OK.
+ mExponent = exp;
+ }
+
// Undo the last add.
// Assumes the constant is nonempty.
void delete() {
- if (!mFraction.isEmpty()) {
+ if (mExponent != 0) {
+ mExponent /= 10;
+ // Once zero, it can only be added back with addExponent.
+ } else if (!mFraction.isEmpty()) {
mFraction = mFraction.substring(0, mFraction.length() - 1);
} else if (mSawDecimal) {
mSawDecimal = false;
@@ -146,28 +171,24 @@ class CalculatorExpr {
result += '.';
result += mFraction;
}
- return KeyMaps.translateResult(result);
- }
-
- // Eliminates leading decimal, which some of our
- // other packages don't like.
- // Meant for machine consumption:
- // Doesn't internationalize decimal point or digits.
- public String toEasyString() {
- String result = mWhole;
- if (result.isEmpty()) result = "0";
- if (mSawDecimal) {
- result += '.';
- result += mFraction;
+ if (mExponent != 0) {
+ result += "E" + mExponent;
}
- return result;
+ return KeyMaps.translateResult(result);
}
+ // Return non-null BoundedRational representation.
public BoundedRational toRational() {
String whole = mWhole;
if (whole.isEmpty()) whole = "0";
BigInteger num = new BigInteger(whole + mFraction);
BigInteger den = BigInteger.TEN.pow(mFraction.length());
+ if (mExponent > 0) {
+ num = num.multiply(BigInteger.TEN.pow(mExponent));
+ }
+ if (mExponent < 0) {
+ den = den.multiply(BigInteger.TEN.pow(-mExponent));
+ }
return new BoundedRational(num, den);
}
@@ -186,6 +207,7 @@ class CalculatorExpr {
res.mWhole = mWhole;
res.mFraction = mFraction;
res.mSawDecimal = mSawDecimal;
+ res.mExponent = mExponent;
return res;
}
}
@@ -350,6 +372,15 @@ class CalculatorExpr {
}
}
+ boolean hasTrailingConstant() {
+ int s = mExpr.size();
+ if (s == 0) {
+ return false;
+ }
+ Token t = mExpr.get(s-1);
+ return t instanceof Constant;
+ }
+
private boolean hasTrailingBinary() {
int s = mExpr.size();
if (s == 0) return false;
@@ -410,6 +441,15 @@ class CalculatorExpr {
}
/**
+ * Add exponent to the constant at the end of the expression.
+ * Assumes there is a constant at the end of the expression.
+ */
+ void addExponent(int exp) {
+ Token lastTok = mExpr.get(mExpr.size() - 1);
+ ((Constant) lastTok).addExponent(exp);
+ }
+
+ /**
* Remove trailing op_add and op_sub operators.
*/
void removeTrailingAdditiveOperators() {
@@ -595,18 +635,19 @@ class CalculatorExpr {
// that was not used as part of the evaluation.
private EvalRet evalUnary(int i, EvalContext ec) throws SyntaxException {
Token t = mExpr.get(i);
+ BoundedRational ratVal;
CR value;
if (t instanceof Constant) {
Constant c = (Constant)t;
- value = CR.valueOf(c.toEasyString(),10);
- return new EvalRet(i+1, value, c.toRational());
+ ratVal = c.toRational();
+ value = ratVal.CRValue();
+ return new EvalRet(i+1, value, ratVal);
}
if (t instanceof PreEval) {
PreEval p = (PreEval)t;
return new EvalRet(i+1, p.mValue, p.mRatValue);
}
EvalRet argVal;
- BoundedRational ratVal;
switch(((Operator)(t)).mId) {
case R.id.const_pi:
return new EvalRet(i+1, CR.PI, null);
diff --git a/src/com/android/calculator2/CalculatorResult.java b/src/com/android/calculator2/CalculatorResult.java
index 27f5d09..8d3dfee 100644
--- a/src/com/android/calculator2/CalculatorResult.java
+++ b/src/com/android/calculator2/CalculatorResult.java
@@ -349,7 +349,7 @@ public class CalculatorResult extends AlignedTextView {
// We add ellipses and exponents in a way that leaves most digits in the position they
// would have been in had we not done so.
// This minimizes jumps as a result of scrolling. Result is NOT internationalized,
- // uses "e" for exponent.
+ // uses "E" for exponent.
public String formatResult(String in, int precOffset, int maxDigs, boolean truncated,
boolean negative, int lastDisplayedOffset[], boolean forcePrecision) {
final int minusSpace = negative ? 1 : 0;
@@ -415,7 +415,7 @@ public class CalculatorResult extends AlignedTextView {
result = result.substring(0, result.length() - dropDigits);
lastDisplayedOffset[0] -= dropDigits;
}
- result = result + "e" + Integer.toString(exponent);
+ result = result + "E" + Integer.toString(exponent);
} // else don't add zero exponent
}
if (truncated || negative && result.charAt(0) != '-') {
@@ -504,7 +504,7 @@ public class CalculatorResult extends AlignedTextView {
int maxChars = getMaxChars();
int lastDisplayedOffset[] = new int[1];
String result = getFormattedResult(currentCharOffset, maxChars, lastDisplayedOffset, false);
- int expIndex = result.indexOf('e');
+ int expIndex = result.indexOf('E');
result = KeyMaps.translateResult(result);
if (expIndex > 0 && result.indexOf('.') == -1) {
// Gray out exponent if used as position indicator
diff --git a/src/com/android/calculator2/Evaluator.java b/src/com/android/calculator2/Evaluator.java
index 9c4346e..4bdc56f 100644
--- a/src/com/android/calculator2/Evaluator.java
+++ b/src/com/android/calculator2/Evaluator.java
@@ -613,7 +613,7 @@ class Evaluator {
}
if (totalDigits <= SHORT_TARGET_LENGTH - 3) {
return negativeSign + cache.charAt(msdIndex) + "."
- + cache.substring(msdIndex + 1, lsdIndex + 1) + "e" + exponent;
+ + cache.substring(msdIndex + 1, lsdIndex + 1) + "E" + exponent;
}
}
// We need to abbreviate.
@@ -624,7 +624,7 @@ class Evaluator {
// Need abbreviation + exponent
return negativeSign + cache.charAt(msdIndex) + "."
+ cache.substring(msdIndex + 1, msdIndex + SHORT_TARGET_LENGTH - negative - 4)
- + KeyMaps.ELLIPSIS + "e" + exponent;
+ + KeyMaps.ELLIPSIS + "E" + exponent;
}
// Return the most significant digit position in the given string
@@ -1008,4 +1008,55 @@ class Evaluator {
return mExpr;
}
+ private static final int MAX_EXP_CHARS = 8;
+
+ /**
+ * Return the index of the character after the exponent starting at s[offset].
+ * Return offset if there is no exponent at that position.
+ * Exponents have syntax E[-]digit* .
+ * "E2" and "E-2" are valid. "E+2" and "e2" are not.
+ * We allow any Unicode digits, and either of the commonly used minus characters.
+ */
+ static int exponentEnd(String s, int offset) {
+ int i = offset;
+ int len = s.length();
+ if (i >= len - 1 || s.charAt(i) != 'E') {
+ return offset;
+ }
+ ++i;
+ if (KeyMaps.keyForChar(s.charAt(i)) == R.id.op_sub) {
+ ++i;
+ }
+ if (i == len || i > offset + MAX_EXP_CHARS || !Character.isDigit(s.charAt(i))) {
+ return offset;
+ }
+ ++i;
+ while (i < len && Character.isDigit(s.charAt(i))) {
+ ++i;
+ }
+ return i;
+ }
+
+ /**
+ * Add the exponent represented by s[begin..end) to the constant at the end of current
+ * expression.
+ * The end of the current expression must be a constant.
+ * Exponents have the same syntax as for exponentEnd().
+ */
+ void addExponent(String s, int begin, int end) {
+ int sign = 1;
+ int exp = 0;
+ int i = begin + 1;
+ // We do the decimal conversion ourselves to exactly match exponentEnd() conventions
+ // and handle various kinds of digits on input. Also avoids allocation.
+ if (KeyMaps.keyForChar(s.charAt(i)) == R.id.op_sub) {
+ sign = -1;
+ ++i;
+ }
+ for (; i < end; ++i) {
+ exp = 10 * exp + Character.digit(s.charAt(i), 10);
+ }
+ mExpr.addExponent(sign * exp);
+ mChangedValue = true;
+ }
}
diff --git a/src/com/android/calculator2/KeyMaps.java b/src/com/android/calculator2/KeyMaps.java
index c28e80d..b4bfdf9 100644
--- a/src/com/android/calculator2/KeyMaps.java
+++ b/src/com/android/calculator2/KeyMaps.java
@@ -184,6 +184,8 @@ public class KeyMaps {
public static final String ELLIPSIS = "\u2026";
+ public static final char MINUS_SIGN = '\u2212';
+
/**
* Map key id to digit or NOT_DIGIT
* Pure function.
@@ -309,12 +311,15 @@ public class KeyMaps {
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':
@@ -337,7 +342,7 @@ public class KeyMaps {
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,
- // so we check ...
+ // or pasted in, so we check ...
return View.NO_ID;
}
}