I have a simple routine which calculates the aspect ratio from a floating point value. So for the value 1.77777779, the routine returns the string "16:9". I have tested this on my machine and it works fine.
The routine is given as :
public string AspectRatioAsString(float f)
{
bool carryon = true;
int index = 0;
double roundedUpValue = 0;
while (carryon)
{
index++;
float upper = index * f;
roundedUpValue = Math.Ceiling(upper);
if (roundedUpValue - upper <= (double)0.1 || index > 20)
{
carryon = false;
}
}
return roundedUpValue + ":" + index;
}
Now on another machine, I get completely different results. So on my machine, 1.77777779 gives "16:9" but on another machine I get "38:21".
Here's an interesting bit of the C# specifiction, from section 4.1.6:
Floating-point operations may be
performed with higher precision than
the result type of the operation. For
example, some hardware architectures
support an “extended” or “long double”
floating-point type with greater range
and precision than the double type,
and implicitly perform all
floating-point operations using this
higher precision type. Only at
excessive cost in performance can such
hardware architectures be made to
perform floating-point operations with
less precision, and rather than
require an implementation to forfeit
both performance and precision, C#
allows a higher precision type to be
used for all floating-point
operations. Other than delivering more
precise results, this rarely has any
measurable effects.
It is possible that this is one of the "measurable effects" thanks to that call to Ceiling. Taking the ceiling of a floating point number, as others have noted, magnifies a difference of 0.000000002 by nine orders of magnitude because it turns 15.99999999 into 16 and 16.00000001 into 17. Two numbers that differ slightly before the operation differ massively afterwards; the tiny difference might be accounted for by the fact that different machines can have more or less "extra precision" in their floating point operations.
Some related issues:
C# XNA Visual Studio: Difference between "release" and "debug" modes?
CLR JIT optimizations violates causality?
To address your specific problem of how to compute an aspect ratio from a float: I'd possibly solve this a completely different way. I'd make a table like this:
struct Ratio
{
public int X { get; private set; }
public int Y { get; private set; }
public Ratio (int x, int y) : this()
{
this.X = x;
this.Y = y;
}
public double AsDouble() { return (double)X / (double)Y; }
}
Ratio[] commonRatios = {
new Ratio(16, 9),
new Ratio(4, 3),
// ... and so on, maybe the few hundred most common ratios here.
// since you are pinning results to be less than 20, there cannot possibly
// be more than a few hundred.
};
and now your implementation is
public string AspectRatioAsString(double ratio)
{
var results = from commonRatio in commonRatios
select new {
Ratio = commonRatio,
Diff = Math.Abs(ratio - commonRatio.AsDouble())};
var smallestResult = results.Min(x=>x.Diff);
return String.Format("{0}:{1}", smallestResult.Ratio.X, smallestResult.Ratio.Y);
}
Notice how the code now reads very much like the operation you are trying to perform: from this list of common ratios, choose the one where the difference between the given ratio and the common ratio is minimized.
I wouldn't use floating point numbers unless I really had to. They're too prone to this sort of thing due to rounding errors.
Can you change the code to work in double precision? (decimal would be overkill). If you do this, does it give more consistent results?
As to why it's different on different machines, what are the differences between the two machines?
32 bit vs 64 bit?
Windows 7 vs Vista vs XP?
Intel vs AMD processor? (thanks Oded)
Something like this might be the cause.
Try Math.Round instead of Math.Ceiling. If you end up with 16.0000001 and round up you'll incorrectly discard that answer.
Miscellaneous other suggestions:
Doubles are better than floats.
(double) 0.1 cast is unnecessary.
Might want to throw an exception if you can't figure out what the aspect ratio is.
If you return immediately upon finding the answer you can ditch the carryon variable.
A perhaps more accurate check would be to calculate the aspect ratio for each guess and compare it to the input.
Revised (untested):
public string AspectRatioAsString(double ratio)
{
for (int height = 1; height <= 20; ++height)
{
int width = (int) Math.Round(height * ratio);
double guess = (double) width / height;
if (Math.Abs(guess - ratio) <= 0.01)
{
return width + ":" + height;
}
}
throw ArgumentException("Invalid aspect ratio", "ratio");
}
When index is 9, you would expect to get something like upper = 16.0000001 or upper = 15.9999999. Which one you get will depend on rounding error, which may differ on different machines. When it's 15.999999, roundedUpValue - upper <= 0.1 is true, and the loop ends. When it's 16.0000001, roundedUpValue - upper <= 0.1 is false and the loop keeps going until you get to index > 20.
Instead maybe you should try rounding upper to the nearest integer and checking if the absolute value of its difference from that integer is small. In otherwords, use something like if (Math.Abs(Math.Round(upper) - upper) <= (double)0.0001 || index > 20)
We had printf()-statements with floating point values that gave different roundings on computer 1 versus computer 2, even though both computers contained the same Visual Studio 2019 version and build.
The difference was found however in a slightly older Windows 10 SDK versus the newest version. How strange it may seem... After fixing that the differences were gone.
Related
In How does DoubleUtil.DoubleToInt(double val) work? we learn that the .NET Framework has a special way of rounding floating point values:
public static int DoubleToInt(double val)
{
return (0 < val) ? (int)(val + 0.5) : (int)(val - 0.5);
}
Why are they not just using (int)Math.Round(val)?
Or: Why is Math.Round not defined this way if this is superior? There must be some trade-off.
Math.Round would result in the creation of a double with the exact value needed, which would then need to be converted to an int. The code here avoids the creation of that double. It also allows for the elision of error handling, and the code related to other types of rounding modes or digits to round to.
They have different behaviour at value with a fractional part 1/2. According to Math.Round:
If the fractional component of a is halfway between two integers, one of which is even and the other odd, then the even number is returned.
So if val == 0.5, then Math.Round(val) == 0.0, whereas this DoubleToInt would give (int)(0.5+0.5) == 1. In other words, DoubleToInt round 1/2 away from zero (like the standard C round function).
There is also potential here for less desirable behaviour: if val is actually the double before 0.5 (i.e. 0.49999999999999994) then, depending on how C# handles intermediate precision, it may in fact give 1 (as val + 0.5 isn't representable by a double, and could be rounded to 1). This was in fact an infamous specification bug in Java 6 (and earlier).
I could see this being an optimization since to get the same behavior from Round you need to use the MidpointRounding.AwayFromZero option. From the reference source this is implemented via:
private static unsafe double InternalRound(double value, int digits, MidpointRounding mode) {
if (Abs(value) < doubleRoundLimit) {
Double power10 = roundPower10Double[digits];
value *= power10;
if (mode == MidpointRounding.AwayFromZero) {
double fraction = SplitFractionDouble(&value);
if (Abs(fraction) >= 0.5d) {
value += Sign(fraction);
}
}
else {
// On X86 this can be inlined to just a few instructions
value = Round(value);
}
value /= power10;
}
return value;
}
I can only guess that the author of the utility method did some performance comparison.
I have two values: X = -78.0921 and Y = -64.6294. Now, when I want to compute Math.Pow(X, Y) it returns NaN. What should I do? How can I solve this problem?
How should I calculate this power? Is there any other function that can calculate this?...or maybe it is not defined mathematically ?
You've tried to compute a number that is not real.
By not real I mean, if we tried every single number between the largest number and the smallest number you can think of, none of those numbers is the solution to -78.0921 to the power of -64.6294.
In fact, no real number is the solution to -1 to the power of 0.5, or the square root of -1, and in general for a^b if a is negative and b is non-integer, the result is not real.
The inability to express such a useful result in real numbers lead to the invention of complex numbers. We say sqrt(-1) = i, the imaginary unit, in the complex number system - all complex numbers have a real component and an imaginary component, expressed as a + b*i.
In general, no negative number to a fractional power produces a real result, as it will have some component of i in it - the closer to a .5 the power is, the more i, the closer to a .0, the more real, and the path follows a circle between real and imaginary, e.g.
-1^x = cos(pi*x)+i*sin(pi*x)
Read more about complex numbers: http://en.wikipedia.org/wiki/Complex_number
If you wish to work with complex numbers in C#, try http://msdn.microsoft.com/en-us/library/system.numerics.complex.aspx
However, unless complex numbers have some meaning in your problem domain (they are meaningful in many electrical engineering, physics and signal analysis problems, for example) it's possible that your data is wrong or your logic is wrong to be attempting to do such a thing in the first place.
The documentation states that the returned value for those inputs is NaN.
x < 0 but not NegativeInfinity; y is not an integer, NegativeInfinity, or PositiveInfinity: returns NaN
The reason that NaN is returned is that the function is not well-defined for your input values. The Wikipedia article on Exponentiation covers this topic.
I think you mean that it returns NaN because your input matches the following:
x < 0 but not NegativeInfinity; y is not an integer, NegativeInfinity, or PositiveInfinity
Which is correct, as per the documentation.
Here is what I used for (native C# library System.Numerics):
Complex.Pow(x, y).Real;
The result is the same as:
double checkSquareRoot(double x, double y)
{
var result = Math.Pow(x, y);
if (x > 0)
{
return result;
}
else
{
return -1 * Math.Pow(-x, y);
}
}
Hope it helps!
I had a similar issue and handled as shown below, you have to adjust the min and max values as needed, in my case they are 0 and 10.
double alpha = FastMath.pow(weight, parameters.getAlpha());
if(alpha == Double.NEGATIVE_INFINITY) {
alpha = 0d;
}
if(alpha == Double.POSITIVE_INFINITY) {
alpha = 10d;
}
double beta = FastMath.pow(1d / distanceMatrix[row][column],
parameters.getBeta());
if(beta == Double.NEGATIVE_INFINITY) {
beta = 0d;
}
if(beta == Double.POSITIVE_INFINITY) {
beta = 10d;
}
It is another weak point in C#. We know that cubic root of -125 is equal to -5, but the result of Console.Write(Math.Pow(-125,1.0/3)); is NaN.
Perhaps you should try this:
if (x>0) {
Console.Write(Math.Pow(x,y));
}
else if (x<0) {
double x = Abs(x);
double z = Math.Pow(x,y);
if (y%2==0)
Console.Write(z);
else
Console.Write(-z);
}
I am trying to calculate average for an array of floats. I need to use indices because this is inside a binary search so the top and bottom will move. (Big picture we are trying to optimize a half range estimation so we don't have to re-create the array each pass).
Anyway I wrote a custom average loop and I'm getting 2 places less accuracy than the c# Average() method
float test = input.Average();
int count = (top - bottom) + 1;//number of elements in this iteration
int pos = bottom;
float average = 0f;//working average
while (pos <= top)
{
average += input[pos];
pos++;
}
average = average / count;
example:
0.0371166766 - c#
0.03711666 - my loop
125090.148 - c#
125090.281 - my loop
http://pastebin.com/qRE3VrCt
I'm getting 2 places less accuracy than the c# Average()
No, you are only losing 1 significant digit. The float type can only store 7 significant digits, the rest are just random noise. Inevitably in a calculation like this, you can accumulate round-off error and thus lose precision. Getting the round-off errors to balance out requires luck.
The only way to avoid it is to use a floating point type that has more precision to accumulate the result. Not an issue, you have double available. Which is why the Linq Average method looks like this:
public static float Average(this IEnumerable<float> source) {
if (source == null) throw Error.ArgumentNull("source");
double sum = 0; // <=== NOTE: double
long count = 0;
checked {
foreach (float v in source) {
sum += v;
count++;
}
}
if (count > 0) return (float)(sum / count);
throw Error.NoElements();
}
Use double to reproduce the Linq result with a comparable number of significant digits in the result.
I'd rewrite this as:
int count = (top - bottom) + 1;//number of elements in this iteration
double sum = 0;
for(int i = bottom; i <= top; i++)
{
sum += input[i];
}
float average = (float)(sum/count);
That way you're using a high precision accumulator, which helps reduce rounding errors.
btw. if performance isn't that important, you can still use LINQ to calculate the average of an array slice:
input.Skip(bottom).Take(top - bottom + 1).Average()
I'm not entirely sure if that fits your problem, but if you need to calculate the average of many subarrays, it can be useful to create a persistent sum array, so calculating an average simply becomes two table lookups and a division.
Just to add to the conversation, be careful when using Floating point primitives.
What Every Computer Scientist Should Know About Floating-Point Arithmetic
Internally floating point numbers store additional least significant bits that are not reflected in the displayed value (aka: Guard Bits or Guard Digits). They are, however, utilized when performing mathematical operations and equality checks. One common result is that a variable containing 0f is not always zero. When accumulating floating point values this can also lead to precision errors.
Use Decimal for your accumulator:
Will not have rounding errors due to Guard Digits
Is a 128bit data type (less likely to exceed Max Value in your accumulator).
For more info:
What is the difference between Decimal, Float and Double in C#?
While testing the performance of floats in .NET, I stumbled unto a weird case: for certain values, multiplication seems way slower than normal. Here is the test case:
using System;
using System.Diagnostics;
namespace NumericPerfTestCSharp {
class Program {
static void Main() {
Benchmark(() => float32Multiply(0.1f), "\nfloat32Multiply(0.1f)");
Benchmark(() => float32Multiply(0.9f), "\nfloat32Multiply(0.9f)");
Benchmark(() => float32Multiply(0.99f), "\nfloat32Multiply(0.99f)");
Benchmark(() => float32Multiply(0.999f), "\nfloat32Multiply(0.999f)");
Benchmark(() => float32Multiply(1f), "\nfloat32Multiply(1f)");
}
static void float32Multiply(float param) {
float n = 1000f;
for (int i = 0; i < 1000000; ++i) {
n = n * param;
}
// Write result to prevent the compiler from optimizing the entire method away
Console.Write(n);
}
static void Benchmark(Action func, string message) {
// warm-up call
func();
var sw = Stopwatch.StartNew();
for (int i = 0; i < 5; ++i) {
func();
}
Console.WriteLine(message + " : {0} ms", sw.ElapsedMilliseconds);
}
}
}
Results:
float32Multiply(0.1f) : 7 ms
float32Multiply(0.9f) : 946 ms
float32Multiply(0.99f) : 8 ms
float32Multiply(0.999f) : 7 ms
float32Multiply(1f) : 7 ms
Why are the results so different for param = 0.9f?
Test parameters: .NET 4.5, Release build, code optimizations ON, x86, no debugger attached.
As others have mentioned, various processors do not support normal-speed calculations when subnormal floating-point values are involved. This is either a design defect (if the behavior impairs your application or is otherwise troublesome) or a feature (if you prefer the cheaper processor or alternative use of silicon that was enabled by not using gates for this work).
It is illuminating to understand why there is a transition at .5:
Suppose you are multiplying by p. Eventually, the value becomes so small that the result is some subnormal value (below 2-126 in 32-bit IEEE binary floating point). Then multiplication becomes slow. As you continue multiplying, the value continues decreasing, and it reaches 2-149, which is the smallest positive number that can be represented. Now, when you multiply by p, the exact result is of course 2-149p, which is between 0 and 2-149, which are the two nearest representable values. The machine must round the result and return one of these two values.
Which one? If p is less than ½, then 2-149p is closer to 0 than to 2-149, so the machine returns 0. Then you are not working with subnormal values anymore, and multiplication is fast again. If p is greater than ½, then 2-149p is closer to 2-149 than to 0, so the machine returns 2-149, and you continue working with subnormal values, and multiplication remains slow. If p is exactly ½, the rounding rules say to use the value that has zero in the low bit of its significand (fraction portion), which is zero (2-149 has 1 in its low bit).
You report that .99f appears fast. This should end with the slow behavior. Perhaps the code you posted is not exactly the code for which you measured fast performance with .99f? Perhaps the starting value or the number of iterations were changed?
There are ways to work around this problem. One is that the hardware has mode settings that specify to change any subnormal values used or obtained to zero, called “denormals as zero” or “flush to zero” modes. I do not use .NET and cannot advise you about how to set these modes in .NET.
Another approach is to add a tiny value each time, such as
n = (n+e) * param;
where e is at least 2-126/param. Note that 2-126/param should be calculated rounded upward, unless you can guarantee that n is large enough that (n+e) * param does not produce a subnormal value. This also presumes n is not negative. The effect of this is to make sure the calculated value is always large enough to be in the normal range, never subnormal.
Adding e in this way of course changes the results. However, if you are, for example, processing audio with some echo effect (or other filter), then the value of e is too small to cause any effects observable by humans listening to the audio. It is likely too small to cause any change in the hardware behavior when producing the audio.
I suspect this has something to do with denormal values (fp values smaller than ~ 1e-38) and the cost associated with processing them.
If you test for denormal values and remove them, sanity is restored.
static void float32Multiply(float param) {
float n = 1000f;
int zeroCount=0;
for (int i = 0; i < 1000000; ++i) {
n = n * param;
if(n<1e-38)n=0;
}
// Write result to prevent the compiler from optimizing the entire method away
Console.Write(n);
}
I've a double variable called x.
In the code, x gets assigned a value of 0.1 and I check it in an 'if' statement comparing x and 0.1
if (x==0.1)
{
----
}
Unfortunately it does not enter the if statement
Should I use Double or double?
What's the reason behind this? Can you suggest a solution for this?
It's a standard problem due to how the computer stores floating point values. Search here for "floating point problem" and you'll find tons of information.
In short – a float/double can't store 0.1 precisely. It will always be a little off.
You can try using the decimal type which stores numbers in decimal notation. Thus 0.1 will be representable precisely.
You wanted to know the reason:
Float/double are stored as binary fractions, not decimal fractions. To illustrate:
12.34 in decimal notation (what we use) means
1 * 101 + 2 * 100 + 3 * 10-1 + 4 * 10-2
The computer stores floating point numbers in the same way, except it uses base 2: 10.01 means
1 * 21 + 0 * 20 + 0 * 2-1 + 1 * 2-2
Now, you probably know that there are some numbers that cannot be represented fully with our decimal notation. For example, 1/3 in decimal notation is 0.3333333…. The same thing happens in binary notation, except that the numbers that cannot be represented precisely are different. Among them is the number 1/10. In binary notation that is 0.000110011001100….
Since the binary notation cannot store it precisely, it is stored in a rounded-off way. Hence your problem.
double and Double are the same (double is an alias for Double) and can be used interchangeably.
The problem with comparing a double with another value is that doubles are approximate values, not exact values. So when you set x to 0.1 it may in reality be stored as 0.100000001 or something like that.
Instead of checking for equality, you should check that the difference is less than a defined minimum difference (tolerance). Something like:
if (Math.Abs(x - 0.1) < 0.0000001)
{
...
}
You need a combination of Math.Abs on X-Y and a value to compare with.
You can use following Extension method approach
public static class DoubleExtensions
{
const double _3 = 0.001;
const double _4 = 0.0001;
const double _5 = 0.00001;
const double _6 = 0.000001;
const double _7 = 0.0000001;
public static bool Equals3DigitPrecision(this double left, double right)
{
return Math.Abs(left - right) < _3;
}
public static bool Equals4DigitPrecision(this double left, double right)
{
return Math.Abs(left - right) < _4;
}
...
Since you rarely call methods on double except ToString I believe its pretty safe extension.
Then you can compare x and y like
if(x.Equals4DigitPrecision(y))
Comparing floating point number can't always be done precisely because of rounding. To compare
(x == .1)
the computer really compares
(x - .1) vs 0
Result of sybtraction can not always be represeted precisely because of how floating point number are represented on the machine. Therefore you get some nonzero value and the condition evaluates to false.
To overcome this compare
Math.Abs(x- .1) vs some very small threshold ( like 1E-9)
From the documentation:
Precision in Comparisons
The Equals method should be used with caution, because two apparently equivalent values can be unequal due to the differing precision of the two values. The following example reports that the Double value .3333 and the Double returned by dividing 1 by 3 are unequal.
...
Rather than comparing for equality, one recommended technique involves defining an acceptable margin of difference between two values (such as .01% of one of the values). If the absolute value of the difference between the two values is less than or equal to that margin, the difference is likely to be due to differences in precision and, therefore, the values are likely to be equal. The following example uses this technique to compare .33333 and 1/3, the two Double values that the previous code example found to be unequal.
So if you really need a double, you should use the techique described on the documentation.
If you can, change it to a decimal. It' will be slower, but you won't have this type of problem.
Use decimal. It doesn't have this "problem".
Exact comparison of floating point values is know to not always work due to the rounding and internal representation issue.
Try imprecise comparison:
if (x >= 0.099 && x <= 0.101)
{
}
The other alternative is to use the decimal data type.
double (lowercase) is just an alias for System.Double, so they are identical.
For the reason, see Binary floating point and .NET.
In short: a double is not an exact type and a minute difference between "x" and "0.1" will throw it off.
Double (called float in some languages) is fraut with problems due to rounding issues, it's good only if you need approximate values.
The Decimal data type does what you want.
For reference decimal and Decimal are the same in .NET C#, as are the double and Double types, they both refer to the same type (decimal and double are very different though, as you've seen).
Beware that the Decimal data type has some costs associated with it, so use it with caution if you're looking at loops etc.
Official MS help, especially interested "Precision in Comparisons" part in context of the question.
https://learn.microsoft.com/en-us/dotnet/api/system.double.equals
// Initialize two doubles with apparently identical values
double double1 = .333333;
double double2 = (double) 1/3;
// Define the tolerance for variation in their values
double difference = Math.Abs(double1 * .00001);
// Compare the values
// The output to the console indicates that the two values are equal
if (Math.Abs(double1 - double2) <= difference)
Console.WriteLine("double1 and double2 are equal.");
else
Console.WriteLine("double1 and double2 are unequal.");
1) Should i use Double or double???
Double and double is the same thing. double is just a C# keyword working as alias for the class System.Double
The most common thing is to use the aliases! The same for string (System.String), int(System.Int32)
Also see Built-In Types Table (C# Reference)
Taking a tip from the Java code base, try using .CompareTo and test for the zero comparison. This assumes the .CompareTo function takes in to account floating point equality in an accurate manner. For instance,
System.Math.PI.CompareTo(System.Math.PI) == 0
This predicate should return true.
// number of digits to be compared
public int n = 12
// n+1 because b/a tends to 1 with n leading digits
public double MyEpsilon { get; } = Math.Pow(10, -(n+1));
public bool IsEqual(double a, double b)
{
// Avoiding division by zero
if (Math.Abs(a)<= double.Epsilon || Math.Abs(b) <= double.Epsilon)
return Math.Abs(a - b) <= double.Epsilon;
// Comparison
return Math.Abs(1.0 - a / b) <= MyEpsilon;
}
Explanation
The main comparison function done using division a/b which should go toward 1. But why division? it simply puts one number as reference defines the second one. For example
a = 0.00000012345
b = 0.00000012346
a/b = 0.999919002
b/a = 1.000081004
(a/b)-1 = 8.099789405475458e-5
1-(b/a) = 8.100445524503848e-5
or
a=12345*10^8
b=12346*10^8
a/b = 0.999919002
b/a = 1.000081004
(a/b)-1 = 8.099789405475458e-5
1-(b/a) = 8.100445524503848e-5
by division we get rid of trailing or leading zeros (or relatively small numbers) that pollute our judgement of number precision. In the example, the comparison is of order 10^-5, and we have 4 number accuracy, because of that in the beginning code I wrote comparison with 10^(n+1) where n is number accuracy.
Adding onto Valentin Kuzub's answer above:
we could use a single method that supports providing nth precision number:
public static bool EqualsNthDigitPrecision(this double value, double compareTo, int precisionPoint) =>
Math.Abs(value - compareTo) < Math.Pow(10, -Math.Abs(precisionPoint));
Note: This method is built for simplicity without added bulk and not with performance in mind.
As a general rule:
Double representation is good enough in most cases but can miserably fail in some situations. Use decimal values if you need complete precision (as in financial applications).
Most problems with doubles doesn't come from direct comparison, it use to be a result of the accumulation of several math operations which exponentially disturb the value due to rounding and fractional errors (especially with multiplications and divisions).
Check your logic, if the code is:
x = 0.1
if (x == 0.1)
it should not fail, it's to simple to fail, if X value is calculated by more complex means or operations it's quite possible the ToString method used by the debugger is using an smart rounding, maybe you can do the same (if that's too risky go back to using decimal):
if (x.ToString() == "0.1")
Floating point number representations are notoriously inaccurate because of the way floats are stored internally. E.g. x may actually be 0.0999999999 or 0.100000001 and your condition will fail. If you want to determine if floats are equal you need to specify whether they're equal to within a certain tolerance.
I.e.:
if(Math.Abs(x - 0.1) < tol) {
// Do something
}
My extensions method for double comparison:
public static bool IsEqual(this double value1, double value2, int precision = 2)
{
var dif = Math.Abs(Math.Round(value1, precision) - Math.Round(value2, precision));
while (precision > 0)
{
dif *= 10;
precision--;
}
return dif < 1;
}
To compare floating point, double or float types, use the specific method of CSharp:
if (double1.CompareTo(double2) > 0)
{
// double1 is greater than double2
}
if (double1.CompareTo(double2) < 0)
{
// double1 is less than double2
}
if (double1.CompareTo(double2) == 0)
{
// double1 equals double2
}
https://learn.microsoft.com/en-us/dotnet/api/system.double.compareto?view=netcore-3.1