am e4a959ec: Merge "Consistently avoid displaying trailing zeroes" into mnc-dev
* commit 'e4a959ec862ff83a1ceb5904225fbe2d4248a9b8':
Consistently avoid displaying trailing zeroes
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java
index 414ec0c..11fd165 100644
--- a/src/com/android/calculator2/Calculator.java
+++ b/src/com/android/calculator2/Calculator.java
@@ -547,11 +547,12 @@
}
// Initial evaluation completed successfully. Initiate display.
- public void onEvaluate(int initDisplayPrec, int leastDigPos, String truncatedWholeNumber) {
+ public void onEvaluate(int initDisplayPrec, int msd, int leastDigPos,
+ String truncatedWholeNumber) {
// Invalidate any options that may depend on the current result.
invalidateOptionsMenu();
- mResultText.displayResult(initDisplayPrec, leastDigPos, truncatedWholeNumber);
+ mResultText.displayResult(initDisplayPrec, msd, leastDigPos, truncatedWholeNumber);
if (mCurrentState != CalculatorState.INPUT) { // in EVALUATE or INIT state
onResult(mCurrentState != CalculatorState.INIT);
}
diff --git a/src/com/android/calculator2/CalculatorResult.java b/src/com/android/calculator2/CalculatorResult.java
index 5b4fb86..7752525 100644
--- a/src/com/android/calculator2/CalculatorResult.java
+++ b/src/com/android/calculator2/CalculatorResult.java
@@ -65,10 +65,13 @@
private int mMinPos; // Minimum position before all digits disappear off the right. Pixels.
private int mMaxPos; // Maximum position before we start displaying the infinite
// sequence of trailing zeroes on the right. Pixels.
+ private int mMaxCharPos; // The same, but in characters.
+ private int mLsd; // Position of least-significant digit in result
+ // (1 = tenths, -1 = tens), or Integer.MAX_VALUE.
private final Object mWidthLock = new Object();
// Protects the next two fields.
private int mWidthConstraint = -1;
- // Our total width in pixels.
+ // Our total width in pixels minus space for ellipsis.
private float mCharWidth = 1;
// Maximum character width. For now we pretend that all characters
// have this width.
@@ -77,6 +80,15 @@
// is not noticeable.
private static final int MAX_WIDTH = 100;
// Maximum number of digits displayed
+ private static final int MAX_LEADING_ZEROES = 6;
+ // Maximum number of leading zeroes after decimal point before we
+ // switch to scientific notation with negative exponent.
+ private static final int MAX_TRAILING_ZEROES = 6;
+ // Maximum number of trailing zeroes before the decimal point before
+ // we switch to scientific notation with positive exponent.
+ private static final int SCI_NOTATION_EXTRA = 1;
+ // Extra digits for standard scientific notation. In this case we
+ // have a deecimal point and no ellipsis.
private ActionMode mActionMode;
private final ForegroundColorSpan mExponentColorSpan;
@@ -164,49 +176,125 @@
}
}
- // Given that the last non-zero digit is at pos, compute the precision we have to ask
- // ask for to actually get the digit at pos displayed. This is not an identity
- // function, since we may need to drop digits to the right to make room for the exponent.
- private int addExpSpace(int lastDigit) {
- if (lastDigit < getMaxChars() - 1) {
- // The decimal point will be in view when displaying the rightmost digit.
- // no exponent needed.
- // TODO: This will change if we stop scrolling to the left of the decimal
- // point, which might be desirable in the traditional scientific notation case.
- return lastDigit;
- }
- // When the last digit is displayed, the exponent will look like "e-<lastDigit>".
- // The length of that string is the extra precision we need.
- return lastDigit + (int)Math.ceil(Math.log10((double)lastDigit)) + 2;
+ // Return the length of the exponent representation for the given exponent, in
+ // characters.
+ private final int expLen(int exp) {
+ if (exp == 0) return 0;
+ return (int)Math.ceil(Math.log10(Math.abs((double)exp))) + (exp >= 0 ? 1 : 2);
}
- // Display a new result, given initial displayed precision, position of the rightmost
- // nonzero digit (or Integer.MAX_VALUE if non-terminating), and the string representing
- // the whole part of the number to be displayed.
- // We pass the string, instead of just the length, so we have one less place to fix in case
- // we ever decide to fully handle a variable width font.
- void displayResult(int initPrec, int leastDigPos, String truncatedWholePart) {
- mLastPos = INVALID;
- synchronized(mWidthLock) {
- mCurrentPos = (int) Math.ceil(initPrec * mCharWidth);
- }
- // Should logically be
- // mMinPos = - (int) Math.ceil(getPaint().measureText(truncatedWholePart)), but
- // we eventually transalate to a character position by dividing by mCharWidth.
- // To avoid rounding issues, we use the analogous computation here.
- mMinPos = - (int) Math.ceil(truncatedWholePart.length() * mCharWidth);
- if (leastDigPos < MAX_RIGHT_SCROLL) {
- mMaxPos = Math.min((int) Math.ceil(addExpSpace(leastDigPos) * mCharWidth),
- MAX_RIGHT_SCROLL);
- } else {
- mMaxPos = MAX_RIGHT_SCROLL;
- }
- mScrollable = (leastDigPos != (initPrec == -1 ? 0 : initPrec));
- // We assume that initPrec allows most significant digit to be displayed.
- // If there is nothing to the right of initPrec, there is no point in scrolling.
+ /**
+ * Initiate display of a new result.
+ * The parameters specify various properties of the result.
+ * @param initPrec Initial display precision computed by evaluator. (1 = tenths digit)
+ * @param msd Position of most significant digit. Offset from left of string.
+ Evaluator.INVALID_MSD if unknown.
+ * @param leastDigPos Position of least significant digit (1 = tenths digit)
+ * or Integer.MAX_VALUE.
+ * @param truncatedWholePart Result up to but not including decimal point.
+ Currently we only use the length.
+ */
+ void displayResult(int initPrec, int msd, int leastDigPos, String truncatedWholePart) {
+ initPositions(initPrec, msd, leastDigPos, truncatedWholePart);
redisplay();
}
+ /**
+ * Set up scroll bounds and determine whether the result is scrollable, based on the
+ * supplied information about the result.
+ * This is unfortunately complicated because we need to predict whether trailing digits
+ * will eventually be replaced by an exponent.
+ * Just appending the exponent during formatting would be simpler, but would produce
+ * jumpier results during transitions.
+ */
+ private void initPositions(int initPrec, int msd, int leastDigPos, String truncatedWholePart) {
+ float charWidth;
+ int maxChars = getMaxChars();
+ mLastPos = INVALID;
+ mLsd = leastDigPos;
+ synchronized(mWidthLock) {
+ charWidth = mCharWidth;
+ }
+ mCurrentPos = mMinPos = (int) Math.round(initPrec * charWidth);
+ // Prevent scrolling past initial position, which is calculated to show leading digits.
+ if (msd == Evaluator.INVALID_MSD) {
+ // Possible zero value
+ if (leastDigPos == Integer.MIN_VALUE) {
+ // Definite zero value.
+ mMaxPos = mMinPos;
+ mMaxCharPos = (int) Math.round(mMaxPos/charWidth);
+ mScrollable = false;
+ } else {
+ // May be very small nonzero value. Allow user to find out.
+ mMaxPos = mMaxCharPos = MAX_RIGHT_SCROLL;
+ mScrollable = true;
+ }
+ return;
+ }
+ int wholeLen = truncatedWholePart.length();
+ int negative = truncatedWholePart.charAt(0) == '-' ? 1 : 0;
+ boolean adjustedForExp = false; // Adjusted for normal exponent.
+ if (msd > wholeLen && msd <= wholeLen + 3) {
+ // Avoid tiny negative exponent; pretend msd is just to the right of decimal point.
+ msd = wholeLen - 1;
+ }
+ int minCharPos = msd - negative - wholeLen;
+ // Position of leftmost significant digit relative to dec. point.
+ // Usually negative.
+ mMaxCharPos = MAX_RIGHT_SCROLL; // How far does it make sense to scroll right?
+ // If msd is left of decimal point should logically be
+ // mMinPos = - (int) Math.ceil(getPaint().measureText(truncatedWholePart)), but
+ // we eventually translate to a character position by dividing by mCharWidth.
+ // To avoid rounding issues, we use the analogous computation here.
+ if (minCharPos > -1 && minCharPos < MAX_LEADING_ZEROES + 2) {
+ // Small number of leading zeroes, avoid scientific notation.
+ minCharPos = -1;
+ }
+ if (leastDigPos < MAX_RIGHT_SCROLL) {
+ mMaxCharPos = leastDigPos;
+ if (mMaxCharPos < -1 && mMaxCharPos > -(MAX_TRAILING_ZEROES + 2)) {
+ mMaxCharPos = -1;
+ }
+ // leastDigPos is positive or negative, never 0.
+ if (mMaxCharPos < -1) {
+ // Number entirely to left of decimal point.
+ // We'll need a positive exponent or displayed zeros to display entire number.
+ mMaxCharPos = Math.min(-1, mMaxCharPos + expLen(-minCharPos - 1));
+ if (mMaxCharPos >= -1) {
+ // Unlikely; huge exponent.
+ mMaxCharPos = -1;
+ } else {
+ adjustedForExp = true;
+ }
+ } else if (minCharPos > -1 || mMaxCharPos >= maxChars) {
+ // Number either entirely to the right of decimal point, or decimal point not
+ // visible when scrolled to the right.
+ // We will need an exponent when looking at the rightmost digit.
+ // Allow additional scrolling to make room.
+ mMaxCharPos += expLen(-(minCharPos + 1));
+ adjustedForExp = true;
+ // Assumed an exponent for standard scientific notation for now.
+ // Adjusted below if necessary.
+ }
+ mScrollable = (mMaxCharPos - minCharPos + negative >= maxChars);
+ if (mScrollable) {
+ if (adjustedForExp) {
+ // We may need a slightly larger negative exponent while scrolling.
+ mMaxCharPos += expLen(-leastDigPos) - expLen(-(minCharPos + 1));
+ }
+ }
+ mMaxPos = Math.min((int) Math.round(mMaxCharPos * charWidth), MAX_RIGHT_SCROLL);
+ if (!mScrollable) {
+ // Position the number consistently with our assumptions to make sure it
+ // actually fits.
+ mCurrentPos = mMaxPos;
+ }
+ } else {
+ mMaxPos = mMaxCharPos = MAX_RIGHT_SCROLL;
+ mScrollable = true;
+ }
+ }
+
void displayError(int resourceId) {
mValid = true;
mScrollable = false;
@@ -215,9 +303,27 @@
private final int MAX_COPY_SIZE = 1000000;
+ /*
+ * Return the most significant digit position in the given string or Evaluator.INVALID_MSD.
+ * Unlike Evaluator.getMsdPos, we treat a final 1 as significant.
+ */
+ public static int getNaiveMsdPos(String s) {
+ int len = s.length();
+ int nonzeroPos = -1;
+ for (int i = 0; i < len; ++i) {
+ char c = s.charAt(i);
+ if (c != '-' && c != '.' && c != '0') {
+ return i;
+ }
+ }
+ return Evaluator.INVALID_MSD;
+ }
+
// Format a result returned by Evaluator.getString() into a single line containing ellipses
- // (if appropriate) and an exponent (if appropriate). digs is the value that was passed to
+ // (if appropriate) and an exponent (if appropriate). prec is the value that was passed to
// getString and thus identifies the significance of the rightmost digit.
+ // A value of 1 means the rightmost digits corresponds to tenths.
+ // maxDigs is the maximum number of characters in the result.
// We add two distinct kinds of exponents:
// 1) If the final result contains the leading digit we use standard scientific notation.
// 2) If not, we add an exponent corresponding to an interpretation of the final result as
@@ -227,37 +333,41 @@
// 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.
- public String formatResult(String res, int digs,
+ public String formatResult(String res, int prec,
int maxDigs, boolean truncated,
boolean negative) {
+ int msd; // Position of most significant digit in res or indication its outside res.
+ int minusSpace = negative ? 1 : 0;
if (truncated) {
res = KeyMaps.ELLIPSIS + res.substring(1, res.length());
+ msd = -1;
+ } else {
+ msd = getNaiveMsdPos(res); // INVALID_MSD is OK and is treated as large.
}
int decIndex = res.indexOf('.');
int resLen = res.length();
- if (decIndex == -1 && digs != -1) {
- // No decimal point displayed, and it's not just to the right of the last digit.
+ if ((decIndex == -1 || msd != Evaluator.INVALID_MSD
+ && msd - decIndex > MAX_LEADING_ZEROES + 1) && prec != -1) {
+ // No decimal point displayed, and it's not just to the right of the last digit,
+ // or we should suppress leading zeroes.
// Add an exponent to let the user track which digits are currently displayed.
// This is a bit tricky, since the number of displayed digits affects the displayed
// exponent, which can affect the room we have for mantissa digits. We occasionally
// display one digit too few. This is sometimes unavoidable, but we could
// avoid it in more cases.
- int exp = digs > 0 ? -digs : -digs - 1;
+ int exp = prec > 0 ? -prec : -prec - 1;
// Can be used as TYPE (2) EXPONENT. -1 accounts for decimal point.
- int msd; // Position of most significant digit in res or indication its outside res.
boolean hasPoint = false;
- if (truncated) {
- msd = -1;
- } else {
- msd = Evaluator.getMsdPos(res); // INVALID_MSD is OK
- }
- if (msd < maxDigs - 1 && msd >= 0) {
+ if (msd < maxDigs - 1 && msd >= 0 &&
+ resLen - msd + 1 /* dec. pt. */ + minusSpace <= maxDigs + SCI_NOTATION_EXTRA) {
// TYPE (1) EXPONENT computation and transformation:
// Leading digit is in display window. Use standard calculator scientific notation
// with one digit to the left of the decimal point. Insert decimal point and
// delete leading zeroes.
+ // We try to keep leading digits roughly in position, and never
+ // lengthen the result by more than SCI_NOT_EXTRA.
String fraction = res.substring(msd + 1, resLen);
- res = (negative ? "-" : "") + res.substring(msd, msd+1) + "." + fraction;
+ res = (negative ? "-" : "") + res.substring(msd, msd + 1) + "." + fraction;
exp += resLen - msd - 1;
// Original exp was correct for decimal point at right of fraction.
// Adjust by length of fraction.
@@ -272,7 +382,7 @@
// Drop digits even if there is room. Otherwise the scrolling gets jumpy.
if (dropDigits >= resLen - 1) {
dropDigits = Math.max(resLen - 2, 0);
- // Jumpy is better than no mantissa.
+ // Jumpy is better than no mantissa. Probably impossible anyway.
}
if (!hasPoint) {
// Special handling for TYPE(2) EXPONENT:
@@ -286,8 +396,18 @@
// ++expDigits; (dead code)
++dropDigits;
++exp;
+ expAsString = Integer.toString(exp);
// This cannot increase the length a second time.
}
+ if (prec - dropDigits > mLsd) {
+ // This can happen if e.g. result = 10^40 + 10^10
+ // It turns out we would otherwise display ...10e9 because
+ // it takes the same amount of space as ...1e10 but shows one more digit.
+ // But we don't want to display a trailing zero, even if it's free.
+ ++dropDigits;
+ ++exp;
+ expAsString = Integer.toString(exp);
+ }
}
res = res.substring(0, resLen - dropDigits);
res = res + "e" + expAsString;
@@ -302,7 +422,8 @@
final boolean truncated[] = new boolean[1];
final boolean negative[] = new boolean[1];
final int requested_prec[] = {pos};
- final String raw_res = mEvaluator.getString(requested_prec, maxSize, truncated, negative);
+ final String raw_res = mEvaluator.getString(requested_prec, mMaxCharPos,
+ maxSize, truncated, negative);
return formatResult(raw_res, requested_prec[0], maxSize, truncated[0], negative[0]);
}
@@ -356,7 +477,7 @@
int getCurrentCharPos() {
synchronized(mWidthLock) {
- return (int) Math.ceil(mCurrentPos / mCharWidth);
+ return (int) Math.round(mCurrentPos / mCharWidth);
}
}
diff --git a/src/com/android/calculator2/Evaluator.java b/src/com/android/calculator2/Evaluator.java
index 51ba7b9..b127f05 100644
--- a/src/com/android/calculator2/Evaluator.java
+++ b/src/com/android/calculator2/Evaluator.java
@@ -55,10 +55,10 @@
// When we are in danger of not having digits to display in response
// to further scrolling, we initiate a background computation to higher
// precision. If we actually do fall behind, we display placeholder
-// characters, e.g. '?', and schedule a display update when the computation
+// characters, e.g. blanks, and schedule a display update when the computation
// completes.
// The code is designed to ensure that the error in the displayed
-// result (excluding any '?' characters) is always strictly less than 1 in
+// result (excluding any placeholder characters) is always strictly less than 1 in
// the last displayed digit. Typically we actually display a prefix
// of a result that has this property and additionally is computed to
// a significantly higher precision. Thus we almost always round correctly
@@ -370,9 +370,8 @@
initCache = res.mVal.toString(prec);
msd = getMsdPos(initCache);
}
- int initDisplayPrec =
- getPreferredPrec(initCache, msd,
- BoundedRational.digitsRequired(res.mRatVal));
+ int lsd = getLsd(res.mRatVal, initCache, initCache.indexOf('.'));
+ int initDisplayPrec = getPreferredPrec(initCache, msd, lsd);
int newPrec = initDisplayPrec + EXTRA_DIGITS;
if (newPrec > prec) {
prec = newPrec;
@@ -419,7 +418,7 @@
// checking for change.
int init_prec = result.mInitDisplayPrec;
int msd = getMsdPos(mCache);
- int leastDigPos = BoundedRational.digitsRequired(mRatVal);
+ int leastDigPos = getLsd(mRatVal, mCache, dotPos);
int new_init_prec = getPreferredPrec(mCache, msd, leastDigPos);
if (new_init_prec < init_prec) {
init_prec = new_init_prec;
@@ -428,7 +427,7 @@
// happen if they're not. e.g. because
// CalculatorResult.MAX_WIDTH was too small.
}
- mCalculator.onEvaluate(init_prec, leastDigPos, truncatedWholePart);
+ mCalculator.onEvaluate(init_prec, msd, leastDigPos, truncatedWholePart);
}
@Override
protected void onCancelled(InitialResult result) {
@@ -460,38 +459,66 @@
mCurrentReevaluator.execute(mCacheDigsReq);
}
- // Retrieve the preferred precision for the currently
- // displayed result, given the number of characters we
- // have room for and the current string approximation for
- // the result.
- // lastDigit is the position of the last digit on the right
- // if there is such a thing, or Integer.MAX_VALUE.
- // May be called in non-UI thread.
+ /**
+ * Return the rightmost nonzero digit position, if any.
+ * @param ratVal Rational value of result or null.
+ * @param cache Current cached decimal string representation of result.
+ * @param decPos Index of decimal point in cache.
+ * @result Position of rightmost nonzero digit relative to decimal point.
+ * Integer.MIN_VALUE if ratVal is zero. Integer.MAX_VALUE if there is no lsd,
+ * or we cannot determine it.
+ */
+ int getLsd(BoundedRational ratVal, String cache, int decPos) {
+ if (ratVal != null && ratVal.signum() == 0) return Integer.MIN_VALUE;
+ int result = BoundedRational.digitsRequired(ratVal);
+ if (result == 0) {
+ int i;
+ for (i = -1; decPos + i > 0 && cache.charAt(decPos + i) == '0'; --i) { }
+ result = i;
+ }
+ return result;
+ }
+
+ /**
+ * Retrieve the preferred precision for the currently displayed result.
+ * May be called from non-UI thread.
+ * @param cache Current approximation as string.
+ * @param msd Position of most significant digit in result. Index in cache.
+ * Can be INVALID_MSD if we haven't found it yet.
+ * @param lastDigit Position of least significant digit (1 = tenths digit)
+ * or Integer.MAX_VALUE.
+ */
int getPreferredPrec(String cache, int msd, int lastDigit) {
int lineLength = mResult.getMaxChars();
int wholeSize = cache.indexOf('.');
+ int negative = cache.charAt(0) == '-' ? 1 : 0;
// Don't display decimal point if result is an integer.
if (lastDigit == 0) lastDigit = -1;
- if (lastDigit != Integer.MAX_VALUE
- && ((wholeSize <= lineLength && lastDigit == 0)
- || wholeSize + lastDigit + 1 /* d.p. */ <= lineLength)) {
- // Prefer to display as integer, without decimal point
- if (lastDigit == 0) return -1;
- return lastDigit;
+ if (lastDigit != Integer.MAX_VALUE) {
+ if (wholeSize <= lineLength && lastDigit <= 0) {
+ // Exact integer. Prefer to display as integer, without decimal point.
+ return -1;
+ }
+ if (lastDigit >= 0 && wholeSize + lastDigit + 1 /* dec.pt. */ <= lineLength) {
+ // Display full exact number wo scientific notation.
+ return lastDigit;
+ }
}
if (msd > wholeSize && msd <= wholeSize + 4) {
- // Display number without scientific notation.
- // Treat leading zero as msd.
+ // Display number without scientific notation. Treat leading zero as msd.
msd = wholeSize - 1;
}
if (msd > wholeSize + MAX_MSD_PREC) {
- // Display a probably but uncertain 0 as "0.000000000",
+ // Display a probable but uncertain 0 as "0.000000000",
// without exponent. That's a judgment call, but less likely
// to confuse naive users. A more informative and confusing
// option would be to use a large negative exponent.
return lineLength - 2;
}
- return msd - wholeSize + lineLength - 2;
+ // Return position corresponding to having msd at left, effectively
+ // presuming scientific notation that preserves the left part of the
+ // result.
+ return msd - wholeSize + lineLength - negative - 1;
}
// Get a short representation of the value represented by
@@ -540,7 +567,6 @@
// Unknown, or could change on reevaluation
return INVALID_MSD;
}
-
}
// Return most significant digit position in the cache, if determined,
@@ -611,8 +637,9 @@
// getRational() can be used to determine whether the result
// is exact, or whether we dropped trailing digits.
// If the requested prec[0] value is out of range, we update
- // it in place and use the updated value.
- public String getString(int[] prec, int maxDigs,
+ // it in place and use the updated value. But we do not make it
+ // greater than maxPrec.
+ public String getString(int[] prec, int maxPrec, int maxDigs,
boolean[] truncated, boolean[] negative) {
int digs = prec[0];
mLastDigs = digs;
@@ -640,7 +667,7 @@
// includes 1 for dec. pt
if (myNegative) --integralDigits;
int minDigs = Math.min(-integralDigits + MIN_DIGS, -1);
- digs = Math.max(digs, minDigs);
+ digs = Math.min(Math.max(digs, minDigs), maxPrec);
prec[0] = digs;
int offset = mCacheDigs - digs; // trailing digits to drop
int deficit = 0; // The number of digits we're short
@@ -712,8 +739,8 @@
// Notify immediately, reusing existing result.
int dotPos = mCache.indexOf('.');
String truncatedWholePart = mCache.substring(0, dotPos);
- int leastDigPos = BoundedRational.digitsRequired(mRatVal);
- mCalculator.onEvaluate(mLastDigs, leastDigPos, truncatedWholePart);
+ int leastDigPos = getLsd(mRatVal, mCache, dotPos);
+ mCalculator.onEvaluate(mLastDigs, getMsd(), leastDigPos, truncatedWholePart);
}
}