Recent

Author Topic: Single and Double, Conversion and Precision  (Read 1079 times)

kupferstecher

  • Hero Member
  • *****
  • Posts: 623
Single and Double, Conversion and Precision
« on: June 13, 2026, 12:12:11 am »
The binary floating point format has the limitation, that (most) decimal numbers cannot be represented correctly, e.g. the value 23.4. The FloatToStr function hides that by rounding to a precision that is smaller than the actual precision of the floating point number. At least for double that works well and the internal 2.3399999999999999E+001 is converted to the string '23.4'.

It becomes problematic when type Single variables are assigned to type Double variables. Now the FloatToStr fails with its rounding concept, as the original Single does not provide a number with a precision as high as required. I found in the source that FloatToStr internally rounds to precision 15, while type Single itself only has a precision of about 8.

As I cannot avoid the conversion I thought it should be possible to round the assigned double to a decimal precision that is possible with Single, e.g. precision 6 or 7, to allow FloatToStr(double) to work properly, again.

Below is the code and it worked for some test values. As can be seen in the comments, the type Single value of 2.34 becomes the ugly 2.33999991416931 after assigning to double and FloatToStr. And it stays the beautiful 2.34 after converting it to double with the function SingleToDouble and FloatToStr.

As short description of the algorithm, the code round(aValue*1000)/1000; e.g. will round to 3 decimals. As the aim is to round to a certain precision instead of decimals, the number of digits before the comma has to be calculated first, which can be done by a log10 calculation. The idea is that it should do a reasonable decimal rounding for any number in the float range.

So my questions:
- Is there already such kind of function/functionality? (This should somehow be a common problem, but I didn't find anything.)
- Does it make sense, or are there corner cases I didn't consider, that render the function problematic or useless?
- Could the implementation be improved? My focus is on speed and reliability.
- How can I make sure that there are not any (odd) input values that break the calculation?

Any comments welcome!

Usage:
Code: Pascal  [Select][+][-]
  1. var
  2.   single1: Single;
  3.   double1: Double;
  4. begin
  5.   single1:= 2.34;
  6.  
  7.   //direct assignment
  8.   double1:= single1;
  9.   writeln('double1: '+Floattostr(double1)+' ',double1); //Output: "double1: 2.33999991416931  2.3399999141693115E+000"
  10.   //with decimal precision rounding
  11.   double1:= SingleToDouble(single1);
  12.   writeln('double1: '+Floattostr(double1)+' ',double1); //Output: "double1: 2.34  2.3399999999999999E+000"
  13. end;

Function:
Code: Pascal  [Select][+][-]
  1. uses Math;
  2.  
  3. Function SingleToDouble(aValue: Single): Double;
  4. var
  5.   digits: integer;
  6.   factor: Double;
  7. begin
  8.   Result:= aValue;
  9.  
  10.   if Result = double(0.0) then EXIT;
  11.  
  12.   if Result > double(1.0) then begin
  13.     digits:= trunc(log10(abs(aValue)))+1;
  14.     if digits > 30 then digits:= 30; //Largest single ~3,4E38
  15.   end else begin
  16.     digits:= trunc(log10(abs(aValue)));
  17.     if digits < -30 then digits:= -30; //Smallest single ~1,2E−38
  18.   end;
  19.  
  20.   factor:= IntPower(10, 7-digits);
  21.   Result:= round(aValue*factor)/factor;
  22.  
  23. end;

ASerge

  • Hero Member
  • *****
  • Posts: 2510
Re: Single and Double, Conversion and Precision
« Reply #1 on: June 13, 2026, 05:51:17 am »
In the general case, it is not known whether this is a loss of precision or whether this was the original number.
If the author of the code knows, then you can use this knowledge.
In this case, if you need to round to two decimal places, you can use Math.SimpleRoundTo instead of SingleToDouble.

kupferstecher

  • Hero Member
  • *****
  • Posts: 623
Re: Single and Double, Conversion and Precision
« Reply #2 on: June 13, 2026, 11:08:22 am »
In the general case, it is not known whether this is a loss of precision or whether this was the original number.
Yes, the loss of precision is there, but only by one digit "in the end" of the number. The same does FloatToStr.

The SingleToDouble function also could be called RoundToDecimalWith7SignificantDigits.

Code: Pascal  [Select][+][-]
  1.   single1:= 1.0 / 3.0;
  2.   writeln(single1); //3.333333433E-01
  3.   double1:= SingleToDouble(single1);
  4.   writeln(Floattostr(double1)); //0,3333333
  5.   writeln(double1); //3.3333330000000000E-001

Perhaps it would work with rounding to 8 significant digits as well, I didn't test.

Quote
If the author of the code knows, then you can use this knowledge.
In this case, if you need to round to two decimal places, you can use Math.SimpleRoundTo instead of SingleToDouble.
The big difference is that with SingleToDouble the author doesn't need to know the number of decimals, it can be a very large number and it can be a very small number.

Code: Pascal  [Select][+][-]
  1.   single1:= 0.0000000234567890123456789012345678;
  2.   double1:= SingleToDouble(single1);
  3.   writeln(Floattostr(double1)); //2.345679E-8 <- rounded to 7 significant digits
  4.   writeln(double1); //2.3456789999999999E-008 <- Actual double number of the rounded value
  5.  
  6.   single1:= 23456789012345.6789012345678;
  7.   double1:= single1;
  8.   writeln(Floattostr(double1)); //23456789823488 <- digits after the first 8 are unrelated to the original number
  9.   double1:= SingleToDouble(single1);
  10.   writeln(Floattostr(double1)); //23456790000000 <- rounded to 7 significant digits
  11.   writeln(double1); //2.3456789999999984E+013 <- Actual double number of the rounded value
  12.  

Paolo

  • Hero Member
  • *****
  • Posts: 753
Re: Single and Double, Conversion and Precision
« Reply #3 on: June 13, 2026, 03:43:42 pm »
flaottostr has as parameter the number of decimal that you want retain.

the problem is that string representations of number are usually truncated and are not saying the whole truth...

see picture, if it helps.
« Last Edit: June 13, 2026, 06:19:33 pm by Paolo »

jamie

  • Hero Member
  • *****
  • Posts: 7852
Re: Single and Double, Conversion and Precision
« Reply #4 on: June 13, 2026, 09:44:48 pm »
It's interesting to note that the FloatTostr accepts Integers, why?

Also, I noticed if you pass a currency variable to it, it never displays the fractions?

Maybe the compiler version I am using has something to do with? 3.2.4RC ?

Maybe I had the number too large for it to display the fraction but really, accepting an Int64?

Jamie
« Last Edit: June 13, 2026, 09:59:02 pm by jamie »
The only true wisdom is knowing you know nothing

440bx

  • Hero Member
  • *****
  • Posts: 6559
Re: Single and Double, Conversion and Precision
« Reply #5 on: June 13, 2026, 11:07:10 pm »
It's interesting to note that the FloatTostr accepts Integers, why?
easy... because they are lighter than water therefore they float ;) 
FPC v3.2.2 and Lazarus v4.0rc3 on Windows 7 SP1 64bit.

jamie

  • Hero Member
  • *****
  • Posts: 7852
Re: Single and Double, Conversion and Precision
« Reply #6 on: June 14, 2026, 12:00:51 am »
Hmm
 Well, I guess also, you could say by removing the fraction you lose the center point of gravity, someone wasn't thinking! :o

jamie
The only true wisdom is knowing you know nothing

avk

  • Hero Member
  • *****
  • Posts: 833
Re: Single and Double, Conversion and Precision
« Reply #7 on: June 14, 2026, 07:39:32 pm »
@kupferstecher, perhaps what you need is known as the "shortest decimal representation of a floating-point number"?
Quote from: Google
...
The shortest decimal representation of a floating-point number is the shortest sequence
of decimal digits  that uniquely identifies the original binary floating-point value.
It guarantees a perfect "round-trip" - meaning converting the decimal back to its original
binary form results in the exact same floating-point number without precision loss.
...

kupferstecher

  • Hero Member
  • *****
  • Posts: 623
Re: Single and Double, Conversion and Precision
« Reply #8 on: June 15, 2026, 12:52:11 am »
see picture, if it helps.
Hi Paolo, I'm not so sure what you mean. Seems the Writeln-Funktion already cuts off plenty of digits. But the unprinted digits are alread outside of the precision of float and double, so I'd consider them as garbage. Its like knowing the first 1000 digits of pi, but that only calculating with type Single.

"shortest decimal representation of a floating-point number"?
Its a promising keyword, I didn't find too much, though. Most was about float to string conversion and that quite complex. My topic actually is highly related to the to-string conversion, as the single-to-double assignment has to consider, how the Double will be converted to string later

@Jamie, i don't really understand what you say.

--
As said, I think this should be a common topic with ready-made functions to do the conversion.
Is it not so clear what i try to achive, or is it not clear how to achive that?

The following code outputs "2.3", it is what one expects:
Code: Pascal  [Select][+][-]
  1.   var double1: Double;
  2.   double1:= 2.3;
  3.   writeln(FloatToStr(double1)); //-> 2.3
The following code outputs 2.299999952, is this an error in FloatToStr?
Code: Pascal  [Select][+][-]
  1.   var single1: Single;
  2.   single1:= 2.3;
  3.   writeln(Floattostr(single1));    //->2.299999952
And what I actually want is that the following also outputs 2.3:
Code: Pascal  [Select][+][-]
  1.   single1:= 2.3;
  2.   double1:= single1;
  3.   writeln(FloatToStr(double1)); //-> 2.29999995231628
  4.   double1:= SomeReasonableConversion(single1);
  5.   writeln(FloatToStr(double1)); //-> 2.3 <- what I need



avk

  • Hero Member
  • *****
  • Posts: 833
Re: Single and Double, Conversion and Precision
« Reply #9 on: June 15, 2026, 10:01:23 am »
...
"shortest decimal representation of a floating-point number"?
Its a promising keyword, I didn't find too much, though. Most was about float to string conversion and that quite complex. My topic actually is highly related to the to-string conversion, as the single-to-double assignment has to consider, how the Double will be converted to string later
...

Several such algorithms have been published in recent years, but, yes, they are all quite complex.
By a funny coincidence, I literally just added to my library some implementation of the Schubfach algorithm (which seems simpler than the others).
Let's test it:
Code: Pascal  [Select][+][-]
  1. program test;
  2. {$mode objfpc}{$h+}
  3. uses
  4.   SysUtils, LgHelpers;
  5. var
  6.   single1: Single;
  7.   double1: Double;
  8. begin
  9.   single1 := 2.3;
  10.   double1 := Double.Parse(single1.ToDecStringDef);
  11.   WriteLn('single1 = ', single1.ToDecStringDef);
  12.   WriteLn('double1 = ', double1.ToString);
  13. end.
  14.  
>>>
Code: Text  [Select][+][-]
  1. single1 = 2,3
  2. double1 = 2,3
  3.  

MathMan

  • Hero Member
  • *****
  • Posts: 530
Re: Single and Double, Conversion and Precision
« Reply #10 on: June 15, 2026, 10:36:23 am »

<snip>

Several such algorithms have been published in recent years, but, yes, they are all quite complex.
By a funny coincidence, I literally just added to my library some implementation of the Schubfach algorithm (which seems simpler than the others).

<snip>


@avk: on an unrelated note (and sorry to chime in here) - when you implemented Schubfach, did you also look into Dragonbox? I know the original C++ implementation is a pain (at least to me) ...

kupferstecher

  • Hero Member
  • *****
  • Posts: 623
Re: Single and Double, Conversion and Precision
« Reply #11 on: June 15, 2026, 10:56:53 pm »
By a funny coincidence, I literally just added to my library some implementation of the Schubfach algorithm (which seems simpler than the others).
Thanks for the details! Now I also testet your code, some numbers have precision 7, others 8. Just as it is said about type single (e.g. on Wikipedia). I read that the algorithm provides a unique conversion result, i.e. that not two floats result in the same decimal representation, so it is reversible. My function from the first post does not provide that, as it always rounds to precision 7 although precision 8 would be needed sometimes (to be unique). But on the other side the algorithm is on my computer (and with the chosen numbers) about 7 times faster than via the string conversion with parsing. Parsing alone takes more than the half of the time. Perhaps string allocation also takes some significant time. The time values for 100000000 conversions was 2.6 seconds for the SingleToDouble() and about 19 seconds for the Double.Parse(single1.ToDecStringDef).


avk

  • Hero Member
  • *****
  • Posts: 833
Re: Single and Double, Conversion and Precision
« Reply #12 on: June 16, 2026, 06:55:03 am »

@avk: on an unrelated note (and sorry to chime in here) - when you implemented Schubfach, did you also look into Dragonbox?...

Yes.

...
But on the other side the algorithm is on my computer (and with thbers) about 7 times faster than via the string conversion with parsing. Parsing alone takes more than the half of the time. Perhaps string allocation also takes some significant time. The time values for 100000000 conversions was 2.6 seconds for the SingleToDouble() and about 19 seconds for the Double.Parse(single1.ToDecStringDef).

This version should be 3-4 times faster:
Code: Pascal  [Select][+][-]
  1. program test;
  2. {$mode objfpc}{$h+}
  3. uses
  4.   SysUtils, LgHelpers, LgJson;
  5. var
  6.   single1: Single;
  7.   double1: Double;
  8.   s: shortstring;
  9. begin
  10.   single1 := 2.3;
  11.   WriteLn('single1 = ', single1.ToDecStringDef);
  12.   s[Succ(Single.ToDecString(single1, s))] := #0;
  13.   if TryStr2Double(@s[1], double1) then
  14.     WriteLn('double1 = ', double1.ToString)
  15.   else
  16.     WriteLn('Oooops!');
  17. end.
  18.  
« Last Edit: June 16, 2026, 09:05:43 am by avk »

kupferstecher

  • Hero Member
  • *****
  • Posts: 623
Re: Single and Double, Conversion and Precision
« Reply #13 on: June 21, 2026, 11:04:06 pm »
@avk , sorry, I didn't see that you edited/added your reply. Now I testet your code and it really is much faster than before. Here the measured time for 100000000 conversions:
2.7 seconds for the SingleToDouble (not as accurate)
7.5 seconds for the enhanced single1.ToDecStringDef with ShortString
19 seconds for the original Double.Parse(single1.ToDecStringDef) with AnsiString

 

TinyPortal © 2005-2018