Doubling Down - Fixing Min/Max() for Doubles

Today I discovered that the built-in overload for Min() and Max() double in the Math unit do not always work as expected when you don’t care about a great deal of precision.  If you search on how to compare floating point types there are many StackOverflow answers, none of which have been adopted over the years by the built-in library.  I have come across this issue of the comparison of floating type values more than once, but keep forgetting not to use built-in functions.  I discovered that the code I was using to determine the range of values was failing on

MyX := Max(MinDouble,0);

MyX was not 0 as I expected because the value is used for a measure of distance for which I only cared about 3 decimal places, so I created a new Math unit with functions that return the expected result.  Hopefully this helps someone else or prevents me from falling into the same hole again.  Naturally these methods should also have overloads for the other floating point types.

unit unMath;

interface

uses
  Math,
  Types;

function Max(const Value1, Value2 :double) :double;
function Min(const Value1, Value2 :double) :double;

implementation

function Max(const Value1, Value2 :double) :double;
var
  relationship :TValueRelationShip;
begin
  relationship := CompareValue(Value1,Value2);
  if (relationship = GreaterThanValue) then
    Result := Value1
  else
    Result := Value2;
end;

function Min(const Value1, Value2 :double) :double;
var
  relationship :TValueRelationShip;
begin
  relationship := CompareValue(Value1,Value2);
  if (relationship = LessThanValue) then
    Result := Value1
  else
    Result := Value2;
end;

end.


5 Responses to “Doubling Down - Fixing Min/Max() for Doubles”

  1. Mike Says:

    Ähm… mindouble is indeed larger than zero so the expected value is actually not zero… am i missing something?

  2. Larry Hengen Says:

    @Mike,

    You are correct! What I failed to explain in the article is that I didn’t care for the degree of accuracy a double provided for the values as I was using them as a measure of distance. My versions of Min/Max use an epsilon that is automatically determined by the CompareValue method and thereby compensates for my use case.

    I’ve updated the article to be clearer (I hope).

  3. Arnaud Bouchez Says:

    Did you use the debugger to see what is the Min/Max Math.pas RTL culprit?
    if A > B then Result := A else Result := B;
    seems just fine to me.
    Perhaps using MinDouble constant doesn’t call the expected overloaded function.

    Your version may be correct, but will undoubtedly be much slower than the RTL’s.

  4. Mike Says:

    Hm.. I think that is a dangerous thing to do. The RTL function works as expected and I’m not sure if yours does.

    Actually you get different results in case:

    procedure TForm5.Button1Click(Sender: TObject);
    var m1, m2 : double;
    begin
    m1 := Max( 0, MinDouble );
    m2 := Max(MinDouble, 0);
    ShowMessage( Format( ‘%e, %e’, [ m1, m2 ]));
    end;

    which is not what you want I guess.. I would rather suggest to round the value to 3 decimals after all the max operations…

  5. Larry Hengen Says:

    @Mike,

    Thanks for the feedback. Yes there is an issue if the values are considered the same, the method returns Value1 which may or may not be the Max value depending on the order of the arguments.

    I have since implemented a new class to facilitate unit testing and created both class methods and instance methods that allow you to get the Min/Max values using either a class or a class instance. The class instance has the advantage in that you can calculate a given Epsilon once based on the number of decimal places you want, and it will use that Epsilon for all Min/Max comparisons.

    Unfortunately, I cannot attach the code to a comment.

Leave a Reply