Last active
August 29, 2015 14:22
-
-
Save bboyle1234/a4745f8bd3ad22d05421 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Text; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using System.Windows.Forms; | |
namespace ConsoleApplication1 { | |
// this code gives and tests an algorithm for comparing the values of a 'double' variables | |
// using a given number of significant digits. | |
// The use case of this algorithm is because small errors are introduced to doubles during | |
// mathematical operations, they will not exactly equate when they should, so comparisons | |
// using native .net methods will yield incorrect results. | |
// eg, when comparing 0.1111111112 and 0.1111111111 we, really want the result to be "equal" not "unequal" | |
class Program { | |
[STAThread] | |
static void Main(string[] args) { | |
var v1 = 1.0 / 3.0; // 0.333333333333333333333333333333.... | |
var v2 = 0.333333; // six significant figures | |
// note this line of code will use the native double comparison method | |
var sign1 = v1.CompareTo(v2); // result is 1 because v1 is larger than v2 | |
// note the following two lines of code will use our custom double comparison override method | |
// because of the extra parameter indicating that rounding to significant figures is required | |
var sign2 = v1.CompareTo(v2, 8); // result is 1 because 0.33333333 is larger than 0.333333 | |
var sign3 = v1.CompareTo(v2, 6); // result is 0 because 0.333333 equals 0.333333 | |
// to make your code more readable, name the numSignificantFigures parameter, like this: | |
var sign4 = v1.CompareTo(v2, numSignificantFigures: 6); | |
Console.ReadKey(); | |
} | |
} | |
/// <summary> | |
/// Made with help from http://stackoverflow.com/questions/374316/round-a-double-to-x-significant-figures | |
/// </summary> | |
public static class DoubleExtensions { | |
/// <summary> | |
/// A cache for all the powers of ten as decimals for the entire range of decimals. | |
/// </summary> | |
static readonly decimal[] _decimalPowersOfTen; | |
/// <summary> | |
/// Initializes the DoubleExtensions class | |
/// </summary> | |
static DoubleExtensions() { | |
// setup the decimal powers of ten array | |
_decimalPowersOfTen = GetDecimalPowersOfTen(); | |
} | |
/// <summary> | |
/// Creates an array containing all the powers of ten as decimals for the entire range of decimals. | |
/// </summary> | |
static decimal[] GetDecimalPowersOfTen() { | |
var result = new decimal[28 + 1 + 28]; | |
result[28] = 1; | |
decimal powerUp = 1, powerDown = 1; | |
for (var i = 1; i < 29; i++) { | |
powerUp *= 10; | |
powerDown /= 10; | |
result[28 + i] = powerUp; | |
result[28 - i] = powerDown; | |
} | |
return result; | |
} | |
/// <summary> | |
/// Gets a value indicating whether 'value' is equal to, greater than, or less than 'other'. | |
/// The optional numSignificantFigures parameter allows you to specify that the inputs must | |
/// be rounded to a specific number of significant places first, to remove small inaccuracies | |
/// from the way mathematical operations can introduce errors to doubles | |
/// </summary> | |
public static int CompareTo(this double value, double other, int numSignificantFigures) { | |
if (numSignificantFigures <= 0) | |
throw new ArgumentException("numSignificantFigures must be greater than 0", "numSignificantFigures"); | |
value = value.RoundToSignificantFigures(numSignificantFigures); | |
other = other.RoundToSignificantFigures(numSignificantFigures); | |
var difference = value - other; | |
return difference.Sign(); | |
} | |
/// <summary> | |
/// Gets a value indicating whether the given input is equal to, greater than, or less than zero. | |
/// </summary> | |
public static int Sign(this double value) { | |
if (value == 0.0) | |
return 0; | |
return value > 0 ? 1 : -1; | |
} | |
/// <summary> | |
/// Rounds a double value to the given number of significant figures. | |
/// Use this method to get rid of the small errors that are introduced to doubles by mathematical operations. | |
/// </summary> | |
public static double RoundToSignificantFigures(this double value, int digits) { | |
if (value == 0.0 || double.IsNaN(value) || double.IsInfinity(value)) | |
return value; | |
// c#'s only native rounding utility is to round to a given number of decimal places. | |
// therefore we will have to adjust 'value' by a certain scale (a multiple of 10) | |
// so that we can use the decimal places rounding utility. | |
// lets find out where the value's decimal point is in relation to its first digit. | |
// a value of 1000 will give a result of 4 | |
// a value of 0.0001 will give a result of -4 | |
var decimalExponent = (int)Math.Floor(Math.Log10(Math.Abs(value))) + 1; | |
// lets check if we are outside the range of decimal | |
// If so, we'll use a slightly flawed method using doubles. | |
if (decimalExponent < -28 + digits || decimalExponent > 28 - digits) { | |
// from decimalExponent, we can figure out the scale we need to use to shift value by | |
// a magnitude (power of 10) so that all the desired significant figures are to the right of the decimal point. | |
// a value of 1000 will yield a scale of 10,000 so it can be divided out to 0.1000 | |
// a value of 0.0001 will yield a scale of 0.001 so it can be divided out to 0.1000 | |
var scale = Math.Pow(10, decimalExponent); | |
// now scale the value, round it, and then undo the scaling | |
var result = scale * Math.Round(value / scale, digits, MidpointRounding.AwayFromZero); | |
// finally, since the previous operation would have introduced more errors (thanks c#!) | |
// we need to perform one more rounding operation | |
// here's the flaw ... this one more rounding operation isn't effective when the number being rounded | |
// is very large, because the introduced errors won't get zapped away | |
// Get the actual number of decimal places that there would be in the final result | |
var numDecimalPlaces = decimalExponent >= digits | |
? 0 | |
: digits - decimalExponent; | |
// And perform that final rounding. Now we're all good to go. | |
return Math.Round(result, numDecimalPlaces, MidpointRounding.AwayFromZero); | |
} | |
// we are within the range of decimals, so we'll continue with the more accurate decimal arithmetic | |
// logic is the same as that used above, so we won't repeat the commenting. | |
// It's just more accurate because we're using decimals instead of doubles. | |
// We use cached decimal powers of ten because .net doesn't give native Math.Pow function for decimals. | |
// Notice there's no need for the final rounding step either. | |
var scaleAsDecimal = _decimalPowersOfTen[decimalExponent + 28]; | |
return (double)(scaleAsDecimal * Math.Round((decimal)value / scaleAsDecimal, digits, MidpointRounding.AwayFromZero)); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment