Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Mailing lists / fpc-pascal / 2022-06

Functions for dealing with floating-point precision

2 messages · 2 people · –

Reply by email Subscribe to this list Your subscription

Read-only archive. Use j and k to move between messages.

Thomas Kurz

Hello,

in my program I have need for checking floating-point precision. I'm internally using floating-points for calculations, but in the end I have to use integer numbers. I cannot use Round() because I have to check for thresholds. I.e. that I wish to accept a value of 1.9999.... as being ">=2". As you can see, I cannot use Trunc() either, so I decided to check for the difference in ULPs (unit in the last place).

I couldn't find the corresponding functions in the RTL, but I think they should be included. I wrote the functions listed below. I would appreciate if the maintainers of FPC decide to add them to the Math unit. I skipped the functions for the Extended type, but if it's being desired, I can add hand them in later.

Kind regards,
Thomas

*** code:

const MAX_ULP_SINGLE = 2.028240960E+31;
const MAX_ULP_DOUBLE = 1.9958403095347198E+292;
function SingleToLongBits (Argument: Single): UInt32;
  begin
    Move (Argument, Result, SizeOf(UInt32));
  end;
{$ifdef FPC_HAS_TYPE_DOUBLE}
function DoubleToLongBits (Argument: Double): UInt64;
  begin
    Move (Argument, Result, SizeOf(UInt64));
  end;
{$endif FPC_HAS_TYPE_DOUBLE}
function LongBitsToSingle (Argument: UInt32): Single;
  begin
    Move (Argument, Result, SizeOf(Single));
  end;
{$ifdef FPC_HAS_TYPE_DOUBLE}
function LongBitsToDouble (Argument: UInt64): Double;
  begin
    Move (Argument, Result, SizeOf(Double));
  end;
{$endif FPC_HAS_TYPE_DOUBLE}
// returns the first floating-point argument with the sign of the second floating-point argument
function CopySign (x: Single; y: Single): Single;
  begin
    Exit (Abs(x) * Sign(y));
  end;
{$ifdef FPC_HAS_TYPE_DOUBLE}
// returns the first floating-point argument with the sign of the second floating-point argument
function CopySign (x: Double; y: Double): Double;
  begin
    Exit (Abs(x) * Sign(y));
  end;
{$endif FPC_HAS_TYPE_DOUBLE}
function IsSameSign (x: Single; y: Single): Boolean;
  begin
    Exit (CopySign(x, y) = x);
  end;
{$ifdef FPC_HAS_TYPE_DOUBLE}
function IsSameSign (x: Double; y: Double): Boolean;
  begin
    Exit (CopySign(x, y) = x);
  end;
{$endif FPC_HAS_TYPE_DOUBLE}
function NextAfter (Start: Single; Direction: Single): Single;
  var
    absStart: Single;
    absDir: Single;
    toZero: Boolean;
  begin
    // If either argument is a NaN, then NaN is returned.
    if IsNan(Start) OR IsNan(Direction) then Exit (NaN);
    // If both arguments compare as equal the second argument is returned.
    if (Start = Direction) THEN Exit (Direction);
    absStart := Abs(Start);
    absDir := Abs(Direction);
    toZero := NOT IsSameSign (Start, Direction) OR (AbsDir < absStart);
    if (toZero) then begin
      // we are reducing the magnitude, going toward zero.
      if (absStart = MinSingle) then Exit (CopySign (0.0, Start));
      if IsInfinite(absStart) THEN Exit (CopySign (MaxSingle, Start));
      Exit (CopySign (LongBitsToSingle (Pred(SingleToLongBits(absStart))), Start));
    end else begin
      // we are increasing the magnitude, toward +-Infinity
      if (Start = 0.0) then Exit (CopySign (MinSingle, Direction));
      if (absStart = MaxSingle) then Exit (CopySign (Infinity, Start));
      Exit (CopySign (LongBitsToSingle(Succ(SingleToLongBits(absStart))), Start));
    end;
  end;
{$ifdef FPC_HAS_TYPE_DOUBLE}
function NextAfter (Start: Double; Direction: Double): Double;
  var
    absStart: Double;
    absDir: Double;
    toZero: Boolean;
  begin
    // If either argument is a NaN, then NaN is returned.
    if IsNan(Start) OR IsNan(Direction) then Exit (NaN);
    // If both arguments compare as equal the second argument is returned.
    if (Start = Direction) THEN Exit (Direction);
    absStart := Abs(Start);
    absDir := Abs(Direction);
    toZero := NOT IsSameSign (Start, Direction) OR (AbsDir < AbsStart);
    if (toZero) then begin
      // we are reducing the magnitude, going toward zero.
      if (absStart = MinDouble) then Exit (CopySign (0.0, Start));
      if IsInfinite(absStart) THEN Exit (CopySign (MaxDouble, Start));
      Exit (CopySign (LongBitsToDouble (Pred(DoubleToLongBits(absStart))), Start));
    end else begin
      // we are increasing the magnitude, toward +-Infinity
      if (Start = 0.0) then Exit (CopySign (MinDouble, Direction));
      if (AbsStart = MaxDouble) then Exit (CopySign (Infinity, Start));
      Exit (CopySign (LongBitsToDouble(Succ(DoubleToLongBits(absStart))), start));
    end;
  end;
{$endif FPC_HAS_TYPE_DOUBLE}
function FloatPrior (Argument: Single): Single;
  begin
    Exit (NextAfter (Argument, NegInfinity));
  end;
function FloatNext (Argument: Single): Single;
  begin
    Exit (NextAfter (Argument, Infinity));
  end;
{$ifdef FPC_HAS_TYPE_DOUBLE}
function FloatPrior (Argument: Double): Double;
  begin
    Exit (NextAfter (Argument, NegInfinity));
  end;
{$endif FPC_HAS_TYPE_DOUBLE}
{$ifdef FPC_HAS_TYPE_DOUBLE}
function FloatNext (Argument: Double): Double;
  begin
    Exit (NextAfter (Argument, Infinity));
  end;
{$endif FPC_HAS_TYPE_DOUBLE}
function ULP (Argument: Single): Single;
  begin
    // If the argument is NaN, then the result is NaN.
    if IsNaN (Argument) then Exit (NaN);
    // If the argument is positive or negative infinity, then the result is positive infinity.
    if IsInfinite (Argument) then Exit (Infinity);
    // If the argument is positive or negative zero, then the result is Single.MIN_VALUE.
    if (Argument = 0.0) then Exit (MinSingle);
    Argument := Abs(Argument);
    // If the argument is ±Single.MAX_VALUE, then the result is equal to 2^104.
    if (Argument = MaxSingle) then Exit (MAX_ULP_SINGLE);
    Exit (FloatNext(Argument) - Argument);
  end;
{$ifdef FPC_HAS_TYPE_DOUBLE}
function ULP (Argument: Double): Double;
  begin
    // If the argument is NaN, then the result is NaN.
    if IsNaN (Argument) then Exit (NaN);
    // If the argument is positive or negative infinity, then the result is positive infinity.
    if IsInfinite (Argument) then Exit (Infinity);
    // If the argument is positive or negative zero, then the result is Double.MIN_VALUE.
    if (Argument = 0.0) then Exit (MinDouble);
    Argument := Abs(Argument);
    // If the argument is ±Double.MAX_VALUE, then the result is equal to 2^971.
    if (Argument = MaxDouble) then Exit (MAX_ULP_DOUBLE);
    Exit (FloatNext(Argument) - Argument);
  end;
{$endif FPC_HAS_TYPE_DOUBLE}

Reply by email

Ekkehard Domning
to Thomas Kurz

Dear Thomas,
thank your for the excellent approach for solving the problem, finding the closest smaller or greater floating point value to a given one. We had an descent discussion about the same topic on "LazarusForum.de"

https://www.lazarusforum.de/viewtopic.php?f=10&t=16019&hilit=kleinste&start=15

where your solution comes up.
I packed your code into a working unit, so that it may be easier to include into other projects.
While performing an intense testing, I found two minor bugs.

The first one is, that the const MinDouble from the unit math.pas is not the smallest Double which could be generated. Thus the code will bypass all values between MinDouble (2.2250738585072014e-308) and 4.9406564584124654E-324

The second one is that the usage of the const MaxSingle, will not work as expected. The compiler will obviously convert MaxSingle into a Double value and subsequently calling the related Double functions, not as expected the Single function.
Example:
CopySign (MaxSingle, Start)

will call

   function CopySign (x: Double; y: Double): Double;

and not

   function CopySign (x: Single; y: Single): Single;

This will luckily not lead into errors.

In the function NextAfter

  if (toZero) then begin
  (...)
  end else begin
    // we are increasing the magnitude, toward +-Infinity
    if (Start = 0.0) then Exit (CopySign (MinSingle, Direction));
    if (absStart = MaxSingle) then Exit (CopySign (Infinity, Start));
    Exit (CopySign (LongBitsToSingle(Succ(SingleToLongBits(absStart))),
Start));
   end;

this conversion leads to an unwanted behaviour in the line:

    if (absStart = MaxSingle) then Exit (CopySign (Infinity, Start));

since the term (absStart = MaxSingle) will never become true, even if NextAfter is called with a value containing MaxSingle. The reason is that
Single(MaxSingle) MaxSingle
Surprisingly the function is working and delivers the correct result, since the following line

CopySign (LongBitsToSingle(Succ(SingleToLongBits(absStart))),Start));

executed absStart containing MaxSingle, will deliver Infinity as expected. So I decided to remove this line.

The unit contains some new defined consts and uses them to bypass the described problems

   MinMinDouble = 4.9406564584124654E-324;
   SingleMaxSingle : Single = Single(MaxSingle);
   NegSingleMaxSingle : Single = Single(-MaxSingle);

I attached the unit, which is also slightly modified due to my different pascal formatting style.

Thank you very much for sharing.

Best regards
Ekkehard Domning

Reply by email