Skip to content

Instantly share code, notes, and snippets.

@bboyle1234
Last active August 29, 2015 14:22
Show Gist options
  • Save bboyle1234/a4745f8bd3ad22d05421 to your computer and use it in GitHub Desktop.
Save bboyle1234/a4745f8bd3ad22d05421 to your computer and use it in GitHub Desktop.
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