Implement percent and new inverse functions

Bug: 21493470

Add x^2 10^x and e^x functions, to make the recently added INV key
work as expected.

Implement % functionality.

10^x is essentially just macro expansions for now.

% and x^2 need trivial evaluator support to provide reasonable display
syntax.

We decided to add evaluator support for exp() as well.

Add corresponding exp() support to BoundedRational and its tests.

Tiny incidental changes for problems uncovered in the process:
Fix bug in tests/README.txt
Evaluate the constant e only once.
Add one more power test along with the exp() test.
Fix proguard.flags so BRTest runs again.

Change-Id: I26cfcaf6d99aeec11387297cc5586e2ddcab6add
diff --git a/proguard.flags b/proguard.flags
index 1185cca..0fa387a 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -54,3 +54,5 @@
 # Some small BoundedRational methods like equals() are not used by the
 # calculator, but crucial for testing.
 -keepclassmembers class com.android.calculator2.BoundedRational { *; }
+# Need CR comparison operators for testing.
+-keepclassmembers class com.hp.creals.CR { *; }
diff --git a/res/layout/pad_advanced.xml b/res/layout/pad_advanced.xml
index bd2f30a..0d6168a 100644
--- a/res/layout/pad_advanced.xml
+++ b/res/layout/pad_advanced.xml
@@ -107,6 +107,15 @@
         android:text="@string/fun_ln" />
 
     <Button
+        android:id="@+id/fun_exp"
+        style="@style/PadButtonStyle.Advanced"
+        android:layout_row="2"
+        android:layout_column="0"
+        android:contentDescription="@string/desc_fun_exp"
+        android:text="@string/fun_exp"
+        android:visibility="gone" />
+
+    <Button
         android:id="@+id/fun_log"
         style="@style/PadButtonStyle.Advanced"
         android:layout_row="2"
@@ -115,6 +124,15 @@
         android:text="@string/fun_log" />
 
     <Button
+        android:id="@+id/fun_10pow"
+        style="@style/PadButtonStyle.Advanced"
+        android:layout_row="2"
+        android:layout_column="1"
+        android:contentDescription="@string/desc_fun_10pow"
+        android:text="@string/fun_10pow"
+        android:visibility="gone" />
+
+    <Button
         android:id="@+id/op_fact"
         style="@style/PadButtonStyle.Advanced"
         android:layout_row="2"
@@ -170,4 +188,13 @@
         android:contentDescription="@string/desc_op_sqrt"
         android:text="@string/op_sqrt" />
 
+    <Button
+        android:id="@+id/op_sqr"
+        style="@style/PadButtonStyle.Advanced"
+        android:layout_row="4"
+        android:layout_column="2"
+        android:contentDescription="@string/desc_op_sqr"
+        android:text="@string/op_sqr"
+        android:visibility="gone" />
+
 </GridLayout>
diff --git a/res/layout/pad_advanced_tablet_port.xml b/res/layout/pad_advanced_tablet_port.xml
index 00b0a70..bccc44c 100644
--- a/res/layout/pad_advanced_tablet_port.xml
+++ b/res/layout/pad_advanced_tablet_port.xml
@@ -107,6 +107,15 @@
         android:text="@string/fun_ln" />
 
     <Button
+        android:id="@+id/fun_exp"
+        style="@style/PadButtonStyle.Advanced"
+        android:layout_row="1"
+        android:layout_column="1"
+        android:contentDescription="@string/desc_fun_exp"
+        android:text="@string/fun_exp"
+        android:visibility="gone" />
+
+    <Button
         android:id="@+id/fun_log"
         style="@style/PadButtonStyle.Advanced"
         android:layout_row="1"
@@ -115,6 +124,15 @@
         android:text="@string/fun_log" />
 
     <Button
+        android:id="@+id/fun_10pow"
+        style="@style/PadButtonStyle.Advanced"
+        android:layout_row="1"
+        android:layout_column="2"
+        android:contentDescription="@string/desc_fun_10pow"
+        android:text="@string/fun_10pow"
+        android:visibility="gone" />
+
+    <Button
         android:id="@+id/op_fact"
         style="@style/PadButtonStyle.Advanced"
         android:layout_row="1"
@@ -170,4 +188,13 @@
         android:contentDescription="@string/desc_op_sqrt"
         android:text="@string/op_sqrt" />
 
+    <Button
+        android:id="@+id/op_sqr"
+        style="@style/PadButtonStyle.Advanced"
+        android:layout_row="2"
+        android:layout_column="4"
+        android:contentDescription="@string/desc_op_sqr"
+        android:text="@string/op_sqr"
+        android:visibility="gone" />
+
 </GridLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3dad1c0..f44ad2d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -78,23 +78,37 @@
     <!-- Subtraction operator (e.g. "1 - 2"). [CHAR_LIMIT=1] -->
     <string name="op_sub" translatable="false">−</string>
 
-    <!-- Abbrev. name of cosine function (e.g. "cos(π)". [CHAR_LIMIT=4] -->
+    <!-- Abbrev. name of cosine function (e.g. "cos(π)"). [CHAR_LIMIT=4] -->
     <string name="fun_cos" translatable="false">cos</string>
     <!-- Natural logarithm function (e.g. "ln(2)"). [CHAR_LIMIT=4] -->
     <string name="fun_ln" translatable="false">ln</string>
     <!-- Logarithm function (e.g. "log(10)"). [CHAR_LIMIT=4] -->
     <string name="fun_log" translatable="false">log</string>
-    <!-- Abbrev. name of sine function (e.g. "sin(π)". [CHAR_LIMIT=4] -->
+    <!-- Abbrev. name of sine function (e.g. "sin(π)"). [CHAR_LIMIT=4] -->
     <string name="fun_sin" translatable="false">sin</string>
-    <!-- Abbrev. name of tangent function (e.g. "tan(π)". [CHAR_LIMIT=4] -->
+    <!-- Abbrev. name of tangent function (e.g. "tan(π)"). [CHAR_LIMIT=4] -->
     <string name="fun_tan" translatable="false">tan</string>
-    <!-- Abbrev. name of cosine function (e.g. "arccos(π)". [CHAR_LIMIT=5] -->
+    <!-- Abbrev. name of cosine function (e.g. "arccos(π)"). [CHAR_LIMIT=5] -->
     <string name="fun_arccos" translatable="false">cos\u207B\u00B9</string>
-    <!-- Abbrev. name of sine function (e.g. "arcsin(π)". [CHAR_LIMIT=5] -->
+    <!-- Abbrev. name of sine function (e.g. "arcsin(π)"). [CHAR_LIMIT=5] -->
     <string name="fun_arcsin" translatable="false">sin\u207B\u00B9</string>
-    <!-- Abbrev. name of tangent function (e.g. "arctan(π)". [CHAR_LIMIT=5] -->
+    <!-- Abbrev. name of tangent function (e.g. "arctan(π)"). [CHAR_LIMIT=5] -->
     <string name="fun_arctan" translatable="false">tan\u207B\u00B9</string>
 
+    <!-- Abbrev. name of base 10 exponential function (e.g. "10^6"). [CHAR_LIMIT=5] -->
+    <string name="fun_10pow" translatable="false">10\u02E3</string>
+    <!-- Abbrev. name of exponential function (e.g. "e^6"). [CHAR_LIMIT=5] -->
+    <string name="fun_exp" translatable="false">e\u02E3</string>
+    <!-- Abbrev. name of suffix square function on key (e.g. "17^2"). [CHAR_LIMIT=5] -->
+    <string name="op_sqr" translatable="false">x\u00B2</string>
+    <!--
+      Abbrev. name of suffix square function in formula.
+      "^2" does not work, since it blends into a later constant.
+      -->
+    <string name="squared" translatable="false">²</string>
+    <!-- Abbrev. name of exponential function in formula.  -->
+    <string name="exponential" translatable = "false">exp</string>
+
     <!-- Abbrev. name of degree mode [CHAR_LIMIT=4] -->
     <string name="mode_deg">deg</string>
     <!-- Abbrev. name of radian mode. [CHAR_LIMIT=4] -->
@@ -139,6 +153,13 @@
     <!-- Content description for 'arctan' button. [CHAR_LIMIT=NONE] -->
     <string name="desc_fun_arctan">inverse tangent</string>
 
+    <!-- Content description for 10^ button. [CHAR_LIMIT=NONE] -->
+    <string name="desc_fun_10pow">ten to the power of</string>
+    <!-- Content description for e^ button. [CHAR_LIMIT=NONE] -->
+    <string name="desc_fun_exp">exponential function</string>
+    <!-- Content description for ^2 button. [CHAR_LIMIT=NONE] -->
+    <string name="desc_op_sqr">squared</string>
+
     <!-- Content description for '+' button. [CHAR_LIMIT=NONE] -->
     <string name="desc_op_add">plus</string>
     <!-- Content description for '÷' button. [CHAR_LIMIT=NONE] -->
diff --git a/src/com/android/calculator2/BoundedRational.java b/src/com/android/calculator2/BoundedRational.java
index ee2ee92..a6dd6d9 100644
--- a/src/com/android/calculator2/BoundedRational.java
+++ b/src/com/android/calculator2/BoundedRational.java
@@ -229,6 +229,14 @@
         return null;
     }
 
+    private static BoundedRational map0to1(BoundedRational r) {
+        if (r == null) return null;
+        if (r.mNum.equals(BigInteger.ZERO)) {
+            return ONE;
+        }
+        return null;
+    }
+
     private static BoundedRational map1to0(BoundedRational r) {
         if (r == null) return null;
         if (r.mNum.equals(r.mDen)) {
@@ -345,12 +353,7 @@
     }
 
     public static BoundedRational cos(BoundedRational r) {
-        // Maps 0 to 1, null otherwise
-        if (r == null) return null;
-        if (r.mNum.equals(BigInteger.ZERO)) {
-            return ONE;
-        }
-        return null;
+        return map0to1(r);
     }
 
     public static BoundedRational degreeCos(BoundedRational r) {
@@ -403,6 +406,10 @@
         return map1to0(r);
     }
 
+    public static BoundedRational exp(BoundedRational r) {
+        return map0to1(r);
+    }
+
     // Return the base 10 log of n, if n is a power of 10, -1 otherwise.
     // n must be positive.
     private static long b10Log(BigInteger n) {
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java
index 0970174..b2cf477 100644
--- a/src/com/android/calculator2/Calculator.java
+++ b/src/com/android/calculator2/Calculator.java
@@ -227,12 +227,18 @@
         mInvertibleButtons = new View[] {
                 findViewById(R.id.fun_sin),
                 findViewById(R.id.fun_cos),
-                findViewById(R.id.fun_tan)
+                findViewById(R.id.fun_tan),
+                findViewById(R.id.fun_ln),
+                findViewById(R.id.fun_log),
+                findViewById(R.id.op_sqrt)
         };
         mInverseButtons = new View[] {
                 findViewById(R.id.fun_arcsin),
                 findViewById(R.id.fun_arccos),
-                findViewById(R.id.fun_arctan)
+                findViewById(R.id.fun_arctan),
+                findViewById(R.id.fun_exp),
+                findViewById(R.id.fun_10pow),
+                findViewById(R.id.op_sqr)
         };
 
         mEvaluator = new Evaluator(this, mResultText);
diff --git a/src/com/android/calculator2/CalculatorExpr.java b/src/com/android/calculator2/CalculatorExpr.java
index e5be4b9..6771b52 100644
--- a/src/com/android/calculator2/CalculatorExpr.java
+++ b/src/com/android/calculator2/CalculatorExpr.java
@@ -582,7 +582,7 @@
         case R.id.const_pi:
             return new EvalRet(i+1, CR.PI, null);
         case R.id.const_e:
-            return new EvalRet(i+1, CR.valueOf(1).exp(), null);
+            return new EvalRet(i+1, REAL_E, null);
         case R.id.op_sqrt:
             // Seems to have highest precedence.
             // Does not add implicit paren.
@@ -635,6 +635,12 @@
             ratVal = BoundedRational.ln(argVal.mRatVal);
             if (ratVal != null) break;
             return new EvalRet(argVal.mPos, argVal.mVal.ln(), null);
+        case R.id.fun_exp:
+            argVal = evalExpr(i+1, ec);
+            if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
+            ratVal = BoundedRational.exp(argVal.mRatVal);
+            if (ratVal != null) break;
+            return new EvalRet(argVal.mPos, argVal.mVal.exp(), null);
         case R.id.fun_log:
             argVal = evalExpr(i+1, ec);
             if (isOperator(argVal.mPos, R.id.rparen, ec)) argVal.mPos++;
@@ -702,35 +708,59 @@
                      // Test for integer-ness to 100 bits past binary point.
     private static final BigInteger MASK =
             BigInteger.ONE.shiftLeft(-TEST_PREC).subtract(BigInteger.ONE);
+    private static final CR REAL_E = CR.valueOf(1).exp();
+    private static final CR REAL_ONE_HUNDREDTH = CR.valueOf(100).inverse();
+    private static final BoundedRational RATIONAL_ONE_HUNDREDTH =
+            new BoundedRational(1,100);
     private static boolean isApprInt(CR x) {
         BigInteger appr = x.get_appr(TEST_PREC);
         return appr.and(MASK).signum() == 0;
     }
 
-    private EvalRet evalFactorial(int i, EvalContext ec) throws SyntaxException {
+    private EvalRet evalSuffix(int i, EvalContext ec) throws SyntaxException {
         EvalRet tmp = evalUnary(i, ec);
         int cpos = tmp.mPos;
         CR cval = tmp.mVal;
         BoundedRational ratVal = tmp.mRatVal;
-        while (isOperator(cpos, R.id.op_fact, ec)) {
-            if (ratVal == null) {
-                // Assume it was an integer, but we
-                // didn't figure it out.
-                // KitKat may have used the Gamma function.
-                if (!isApprInt(cval)) {
-                    throw new ArithmeticException("factorial(non-integer)");
+        boolean isFact;
+        boolean isSquared = false;
+        while ((isFact = isOperator(cpos, R.id.op_fact, ec)) ||
+                (isSquared = isOperator(cpos, R.id.op_sqr, ec)) ||
+                isOperator(cpos, R.id.op_pct, ec)) {
+            if (isFact) {
+                if (ratVal == null) {
+                    // Assume it was an integer, but we
+                    // didn't figure it out.
+                    // KitKat may have used the Gamma function.
+                    if (!isApprInt(cval)) {
+                        throw new ArithmeticException("factorial(non-integer)");
+                    }
+                    ratVal = new BoundedRational(cval.BigIntegerValue());
                 }
-                ratVal = new BoundedRational(cval.BigIntegerValue());
+                ratVal = BoundedRational.fact(ratVal);
+                cval = ratVal.CRValue();
+            } else if (isSquared) {
+                ratVal = BoundedRational.multiply(ratVal, ratVal);
+                if (ratVal == null) {
+                    cval = cval.multiply(cval);
+                } else {
+                    cval = ratVal.CRValue();
+                }
+            } else /* percent */ {
+                ratVal = BoundedRational.multiply(ratVal, RATIONAL_ONE_HUNDREDTH);
+                if (ratVal == null) {
+                    cval = cval.multiply(REAL_ONE_HUNDREDTH);
+                } else {
+                    cval = ratVal.CRValue();
+                }
             }
-            ratVal = BoundedRational.fact(ratVal);
             ++cpos;
         }
-        if (ratVal != null) cval = ratVal.CRValue();
         return new EvalRet(cpos, cval, ratVal);
     }
 
     private EvalRet evalFactor(int i, EvalContext ec) throws SyntaxException {
-        final EvalRet result1 = evalFactorial(i, ec);
+        final EvalRet result1 = evalSuffix(i, ec);
         int cpos = result1.mPos;  // current position
         CR cval = result1.mVal;   // value so far
         BoundedRational ratVal = result1.mRatVal;  // int value so far
diff --git a/src/com/android/calculator2/Evaluator.java b/src/com/android/calculator2/Evaluator.java
index 4ddaf15..9b9e830 100644
--- a/src/com/android/calculator2/Evaluator.java
+++ b/src/com/android/calculator2/Evaluator.java
@@ -776,10 +776,15 @@
     // syntax issues, and the expression is unchanged.
     // Return true otherwise.
     boolean append(int id) {
-        mChangedValue = (KeyMaps.digVal(id) != KeyMaps.NOT_DIGIT
-                        || KeyMaps.isSuffix(id)
-                        || id == R.id.const_pi || id == R.id.const_e);
-        return mExpr.add(id);
+        if (id == R.id.fun_10pow) {
+            add10pow();  // Handled as macro expansion.
+            return true;
+        } else {
+            mChangedValue = (KeyMaps.digVal(id) != KeyMaps.NOT_DIGIT
+                    || KeyMaps.isSuffix(id)
+                    || id == R.id.const_pi || id == R.id.const_e);
+            return mExpr.add(id);
+        }
     }
 
     void delete() {
@@ -865,6 +870,17 @@
         mExpr.append(mSaved);
     }
 
+    // Add the power of 10 operator to the expression.  This is treated
+    // essentially as a macro expansion.
+    private void add10pow() {
+        CalculatorExpr ten = new CalculatorExpr();
+        ten.add(R.id.digit_1);
+        ten.add(R.id.digit_0);
+        mChangedValue = true;  // For consistency.  Reevaluation is probably not useful.
+        mExpr.append(ten);
+        mExpr.add(R.id.op_pow);
+    }
+
     // Retrieve the main expression being edited.
     // It is the callee's reponsibility to call cancelAll to cancel
     // ongoing concurrent computations before modifying the result.
diff --git a/src/com/android/calculator2/KeyMaps.java b/src/com/android/calculator2/KeyMaps.java
index e3b84e7..5385424 100644
--- a/src/com/android/calculator2/KeyMaps.java
+++ b/src/com/android/calculator2/KeyMaps.java
@@ -67,6 +67,9 @@
                 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:
@@ -79,6 +82,9 @@
                 return context.getString(R.string.op_div);
             case R.id.op_add:
                 return context.getString(R.string.op_add);
+            case R.id.op_sqr:
+                // Button label doesn't work.
+                return context.getString(R.string.squared);
             case R.id.op_sub:
                 return context.getString(R.string.op_sub);
             case R.id.dec_point:
@@ -132,6 +138,7 @@
         switch (id) {
             case R.id.op_fact:
             case R.id.op_pct:
+            case R.id.op_sqr:
                 return true;
             default:
                 return false;
diff --git a/tests/README.txt b/tests/README.txt
index 3069de8..b1cd968 100644
--- a/tests/README.txt
+++ b/tests/README.txt
@@ -2,7 +2,7 @@
 
 1) Build the tests.
 2) Install the calculator with
-adb install <tree root>/out/target/product/generic/data/app/ExactCalculatorTests/ExactCalculator.apk
+adb install <tree root>/out/target/product/generic/data/app/ExactCalculator/ExactCalculator.apk
 3) adb install <tree root>/out/target/product/generic/data/app/ExactCalculatorTests/ExactCalculatorTests.apk
 4) adb shell am instrument -w com.android.exactcalculator.tests/android.test.InstrumentationTestRunner
 
diff --git a/tests/src/com/android/calculator2/BRTest.java b/tests/src/com/android/calculator2/BRTest.java
index 1cd56a5..29a5ae9 100644
--- a/tests/src/com/android/calculator2/BRTest.java
+++ b/tests/src/com/android/calculator2/BRTest.java
@@ -92,6 +92,13 @@
         } catch (ArithmeticException ignored) {
             check((long_x - 90) % 180 == 0, "exception on defined tan: " + x);
         }
+        if (x.compareTo(BoundedRational.THIRTY) <= 0
+                && x.compareTo(BoundedRational.MINUS_THIRTY) >= 0) {
+            checkWeakEq(BoundedRational.exp(x), xAsCR.exp(), "exp:" + x);
+            checkWeakEq(BoundedRational.pow(BR_15, x),
+                    CR.valueOf(15).ln().multiply(xAsCR).exp(),
+                    "pow(15,x):" + x);
+        }
         if (x.compareTo(BoundedRational.ONE) <= 0
                 && x.compareTo(BoundedRational.MINUS_ONE) >= 0) {
             checkWeakEq(BoundedRational.asin(x), ASIN.execute(xAsCR),