using System; using System.Collections.Generic; using System.Text; using HH.WMS.Utils.NPOI.Util; namespace HH.WMS.Utils.NPOI.SS.Util { public class NumberComparer { /** * This class attempts to reproduce Excel's behaviour for comparing numbers. Results are * mostly the same as those from {@link Double#compare(double, double)} but with some * rounding. For numbers that are very close, this code converts to a format having 15 * decimal digits of precision and a decimal exponent, before completing the comparison. *

* In Excel formula evaluation, expressions like "(0.06-0.01)=0.05" evaluate to "TRUE" even * though the equivalent java expression is false. In examples like this, * Excel achieves the effect by having additional logic for comparison operations. *

*

* Note - Excel also gives special treatment to expressions like "0.06-0.01-0.05" which * evaluates to "0" (in java, rounding anomalies give a result of 6.9E-18). The special * behaviour here is for different reasons to the example above: If the last operator in a * cell formula is '+' or '-' and the result is less than 250 times smaller than * first operand, the result is rounded to zero. * Needless to say, the two rules are not consistent and it is relatively easy to find * examples that satisfy
* "A=B" is "TRUE" but "A-B" is not "0"
* and
* "A=B" is "FALSE" but "A-B" is "0"
*
* This rule (for rounding the result of a final addition or subtraction), has not been * implemented in POI (as of Jul-2009). * * @return negative, 0, or positive according to the standard Excel comparison * of values a and b. */ public static int Compare(double a, double b) { long rawBitsA = BitConverter.DoubleToInt64Bits(a); long rawBitsB = BitConverter.DoubleToInt64Bits(b); int biasedExponentA = IEEEDouble.GetBiasedExponent(rawBitsA); int biasedExponentB = IEEEDouble.GetBiasedExponent(rawBitsB); if (biasedExponentA == IEEEDouble.BIASED_EXPONENT_SPECIAL_VALUE) { throw new ArgumentException("Special double values are not allowed: " + ToHex(a)); } if (biasedExponentB == IEEEDouble.BIASED_EXPONENT_SPECIAL_VALUE) { throw new ArgumentException("Special double values are not allowed: " + ToHex(a)); } int cmp; // sign bit is in the same place for long and double: bool aIsNegative = rawBitsA < 0; bool bIsNegative = rawBitsB < 0; // compare signs if (aIsNegative != bIsNegative) { // Excel seems to have 'normal' comparison behaviour around zero (no rounding) // even -0.0 < +0.0 (which is not quite the initial conclusion of bug 47198) return aIsNegative ? -1 : +1; } // then compare magnitudes (IEEE 754 has exponent bias specifically to allow this) cmp = biasedExponentA - biasedExponentB; int absExpDiff = Math.Abs(cmp); if (absExpDiff > 1) { return aIsNegative ? -cmp : cmp; } if (absExpDiff == 1) { // special case exponent differs by 1. There is still a chance that with rounding the two quantities could end up the same } else { // else - sign and exponents equal if (rawBitsA == rawBitsB) { // fully equal - exit here return 0; } } if (biasedExponentA == 0) { if (biasedExponentB == 0) { return CompareSubnormalNumbers(rawBitsA & IEEEDouble.FRAC_MASK, rawBitsB & IEEEDouble.FRAC_MASK, aIsNegative); } // else biasedExponentB is 1 return -CompareAcrossSubnormalThreshold(rawBitsB, rawBitsA, aIsNegative); } if (biasedExponentB == 0) { // else biasedExponentA is 1 return +CompareAcrossSubnormalThreshold(rawBitsA, rawBitsB, aIsNegative); } // sign and exponents same, but fractional bits are different ExpandedDouble edA = ExpandedDouble.FromRawBitsAndExponent(rawBitsA, biasedExponentA - IEEEDouble.EXPONENT_BIAS); ExpandedDouble edB = ExpandedDouble.FromRawBitsAndExponent(rawBitsB, biasedExponentB - IEEEDouble.EXPONENT_BIAS); NormalisedDecimal ndA = edA.NormaliseBaseTen().RoundUnits(); NormalisedDecimal ndB = edB.NormaliseBaseTen().RoundUnits(); cmp = ndA.CompareNormalised(ndB); if (aIsNegative) { return -cmp; } return cmp; } /** * If both numbers are subnormal, Excel seems to use standard comparison rules */ private static int CompareSubnormalNumbers(long fracA, long fracB, bool isNegative) { int cmp = fracA > fracB ? +1 : fracA < fracB ? -1 : 0; return isNegative ? -cmp : cmp; } /** * Usually any normal number is greater (in magnitude) than any subnormal number. * However there are some anomalous cases around the threshold where Excel produces screwy results * @param isNegative both values are either negative or positive. This parameter affects the sign of the comparison result * @return usually isNegative ? -1 : +1 */ private static int CompareAcrossSubnormalThreshold(long normalRawBitsA, long subnormalRawBitsB, bool isNegative) { long fracB = subnormalRawBitsB & IEEEDouble.FRAC_MASK; if (fracB == 0) { // B is zero, so A is definitely greater than B return isNegative ? -1 : +1; } long fracA = normalRawBitsA & IEEEDouble.FRAC_MASK; if (fracA <= 0x0000000000000007L && fracB >= 0x000FFFFFFFFFFFFAL) { // Both A and B close to threshold - weird results if (fracA == 0x0000000000000007L && fracB == 0x000FFFFFFFFFFFFAL) { // special case return 0; } // exactly the opposite return isNegative ? +1 : -1; } // else - typical case A and B is not close to threshold return isNegative ? -1 : +1; } /** * for formatting double values in error messages */ private static String ToHex(double a) { return "0x" + StringUtil.ToHexString(BitConverter.DoubleToInt64Bits(a)).ToUpper(); } } }