Loading src/com/android/calculator2/Calculator.java +3 −2 Original line number Diff line number Diff line Loading @@ -547,11 +547,12 @@ public class Calculator extends Activity } // 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); } Loading src/com/android/calculator2/CalculatorResult.java +169 −48 Original line number Diff line number Diff line Loading @@ -65,10 +65,13 @@ public class CalculatorResult extends AlignedTextView { 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. Loading @@ -77,6 +80,15 @@ public class CalculatorResult extends AlignedTextView { // 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; Loading Loading @@ -164,47 +176,123 @@ public class CalculatorResult extends AlignedTextView { } } // 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; // 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); } // 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; /** * 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(); } // 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) { /** * 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) { mCurrentPos = (int) Math.ceil(initPrec * mCharWidth); } // Should logically be 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 transalate to a character position by dividing by mCharWidth. // we eventually translate 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 (minCharPos > -1 && minCharPos < MAX_LEADING_ZEROES + 2) { // Small number of leading zeroes, avoid scientific notation. minCharPos = -1; } if (leastDigPos < MAX_RIGHT_SCROLL) { mMaxPos = Math.min((int) Math.ceil(addExpSpace(leastDigPos) * mCharWidth), 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 { mMaxPos = MAX_RIGHT_SCROLL; 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; } 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. redisplay(); } void displayError(int resourceId) { Loading @@ -215,9 +303,27 @@ public class CalculatorResult extends AlignedTextView { 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 Loading @@ -227,35 +333,39 @@ public class CalculatorResult extends AlignedTextView { // 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; exp += resLen - msd - 1; Loading @@ -272,7 +382,7 @@ public class CalculatorResult extends AlignedTextView { // 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: Loading @@ -286,8 +396,18 @@ public class CalculatorResult extends AlignedTextView { // ++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; Loading @@ -302,7 +422,8 @@ public class CalculatorResult extends AlignedTextView { 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]); } Loading Loading @@ -356,7 +477,7 @@ public class CalculatorResult extends AlignedTextView { int getCurrentCharPos() { synchronized(mWidthLock) { return (int) Math.ceil(mCurrentPos / mCharWidth); return (int) Math.round(mCurrentPos / mCharWidth); } } Loading src/com/android/calculator2/Evaluator.java +57 −30 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -370,9 +370,8 @@ class Evaluator { 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; Loading Loading @@ -419,7 +418,7 @@ class Evaluator { // 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; Loading @@ -428,7 +427,7 @@ class Evaluator { // 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) { Loading Loading @@ -460,38 +459,66 @@ class Evaluator { 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; 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 Loading Loading @@ -540,7 +567,6 @@ class Evaluator { // Unknown, or could change on reevaluation return INVALID_MSD; } } // Return most significant digit position in the cache, if determined, Loading Loading @@ -611,8 +637,9 @@ class Evaluator { // 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; Loading Loading @@ -640,7 +667,7 @@ class Evaluator { // 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 Loading Loading @@ -712,8 +739,8 @@ class Evaluator { // 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); } } Loading Loading
src/com/android/calculator2/Calculator.java +3 −2 Original line number Diff line number Diff line Loading @@ -547,11 +547,12 @@ public class Calculator extends Activity } // 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); } Loading
src/com/android/calculator2/CalculatorResult.java +169 −48 Original line number Diff line number Diff line Loading @@ -65,10 +65,13 @@ public class CalculatorResult extends AlignedTextView { 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. Loading @@ -77,6 +80,15 @@ public class CalculatorResult extends AlignedTextView { // 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; Loading Loading @@ -164,47 +176,123 @@ public class CalculatorResult extends AlignedTextView { } } // 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; // 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); } // 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; /** * 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(); } // 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) { /** * 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) { mCurrentPos = (int) Math.ceil(initPrec * mCharWidth); } // Should logically be 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 transalate to a character position by dividing by mCharWidth. // we eventually translate 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 (minCharPos > -1 && minCharPos < MAX_LEADING_ZEROES + 2) { // Small number of leading zeroes, avoid scientific notation. minCharPos = -1; } if (leastDigPos < MAX_RIGHT_SCROLL) { mMaxPos = Math.min((int) Math.ceil(addExpSpace(leastDigPos) * mCharWidth), 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 { mMaxPos = MAX_RIGHT_SCROLL; 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; } 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. redisplay(); } void displayError(int resourceId) { Loading @@ -215,9 +303,27 @@ public class CalculatorResult extends AlignedTextView { 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 Loading @@ -227,35 +333,39 @@ public class CalculatorResult extends AlignedTextView { // 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; exp += resLen - msd - 1; Loading @@ -272,7 +382,7 @@ public class CalculatorResult extends AlignedTextView { // 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: Loading @@ -286,8 +396,18 @@ public class CalculatorResult extends AlignedTextView { // ++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; Loading @@ -302,7 +422,8 @@ public class CalculatorResult extends AlignedTextView { 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]); } Loading Loading @@ -356,7 +477,7 @@ public class CalculatorResult extends AlignedTextView { int getCurrentCharPos() { synchronized(mWidthLock) { return (int) Math.ceil(mCurrentPos / mCharWidth); return (int) Math.round(mCurrentPos / mCharWidth); } } Loading
src/com/android/calculator2/Evaluator.java +57 −30 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -370,9 +370,8 @@ class Evaluator { 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; Loading Loading @@ -419,7 +418,7 @@ class Evaluator { // 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; Loading @@ -428,7 +427,7 @@ class Evaluator { // 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) { Loading Loading @@ -460,38 +459,66 @@ class Evaluator { 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; 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 Loading Loading @@ -540,7 +567,6 @@ class Evaluator { // Unknown, or could change on reevaluation return INVALID_MSD; } } // Return most significant digit position in the cache, if determined, Loading Loading @@ -611,8 +637,9 @@ class Evaluator { // 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; Loading Loading @@ -640,7 +667,7 @@ class Evaluator { // 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 Loading Loading @@ -712,8 +739,8 @@ class Evaluator { // 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); } } Loading