Formating floating point/double for display

BlitzMax Forums/BlitzMax Programming/Formating floating point/double for display

Grey Alien(Posted 2007) [#1]
Sorry "Formatting", anyway...

Hi, has anyone seen any decent floating point number formating functions kicking around in the code archives? I'd like to pass in a floating point number and say have it rounded (without bankers rounding) to one decimal place for example and return a printable string. As you know if you try to store something like 1.4 in float you get 1.39999 and I would like the display to say 1.4.

Last time I checked BMax and BPlus did Bankers Rounding http://en.wikipedia.org/wiki/Rounding which is why I (well actually Beaker did after I request it) wrote ccRound:

Function ccRound%(flot#)
	Return Floor(flot+0.5)
End Function


Thanks very much for any help.


tonyg(Posted 2007) [#2]
Number Format ?


Dreamora(Posted 2007) [#3]
Its actually quite simple to convert to fixed point math, round there and convert it back, assuming you use numbers in a reasonable size (ie number * 10^decimals < MaxFloat, if this does not hold, just make float to double and int to long)

function fRound:float(number:float,decimals:int)
 local t:int = 10^decimals
 local result:float = int(number * t + 0.5)
 return result / t
end function


There are most likely simpler and especially less breaking ways, but so far I didn't have reason to use them.


Grey Alien(Posted 2007) [#4]
Thanks Tony (keeper of the archives ;-), looks good but doesn't round. May still use it though, I'll ask Dmaz if I can put it in the framework.

Dreamora: Yeah I've already used that method before in my current game but I didn't stick it in a function. So I was looking for a combination function that can properly round and format floats. Mind if I put that function as is in my framework with a credit to you?

Anyway it doesn't solve the issue where I want the float to be outputted as the correct number and not a float approximation. For example:

test#= 1.4
Print test
Print fround(test,1)

Function fRound:Float(number:Float,decimals:Int)
 Local t:Int = 10^decimals
 Local result:Float = Int(number * t + 0.5)
 Return result / t
End Function


gives


1.39999998
1.39999998


because of course it just gets rounded back to a float. I want the user to see 1.4. I did it before with some messy code but wondered if anyone had made anything neater...

See my problem now?


Grey Alien(Posted 2007) [#5]
Here was the messy and non-flexible code I wrote:

	Method GetMultiplierStrings(M: Double,part1$ Var, part2$ Var)
		'Convert the multiplier number into a string.
		'First get the part before the decimal place.
		Local temp$ = String(M)		
		Part1$ = ccFirstStringToSubChop(temp,".")
		'Now make sure the number after the decimal is a single digit.
		'We need to do this because 1.4 in Double is stored as 1.399999 etc !		
		'We do this by multiplying by 10 and turning into an int.
		Local TempPart2 = Floor(0.05+(Float("0."+temp)*10)) 'round down! (add on a little bit of 0.05 to make sure that .999 numbers go over to the next whole number.)

		'Has it rounded up to the next number?
		If String(TempPart2) = "10" Then
			TempPart2 = 0
			Part1 = String(Part1.ToInt()+1)
		EndIf
		Part2 = String(TempPart2)
	End Method

Function ccFirstStringToSubChop$(s$ Var, sub$)
	'Pass in a String, this will only return the first part up to,
	'but not including, the substring (or end).
	'The source string will have the first part and the substring removed.	
	Local pos% = Instr(s$, sub$)
	'If pos = 0 then the end of the string was reached, so return the whole thing.
	If pos = 0 Then
		Local ret$ = s$
		'now clear s$
		s$ = ""
		Return ret$
	Else
		Local ret$ = Mid(s$, 1, pos-1)
		s$ = Mid$(s$, pos+1, Len(s$)) 'leave remainer in s$
		Return ret$		
	EndIf
End Function



Dreamora(Posted 2007) [#6]
There are 2 ways:

1. Use Double
2. Use a new mingw version (was mentioned to happen with the next version of BM anyway)

The default mingw sadly is that old that it has some problems with float (as you know from 2^intVariable which is caused by the outdated mingw as well)

A rounding function won't be able to fix a pure core limitation I fear.

And yes feel free to include it in your framework if you find use for it. Credit is ok :)

EDIT: your "messy" code at least has the pro right now that it is not cut from working due to float inaccuracies in the math module :)


Grey Alien(Posted 2007) [#7]
converting everything to double merely gives this:


1.3999999761581421
1.3999999999999999



What the new mingw fixes this? Hope it doesn't screw up my code that now expects this problem! :-O

Thanks re: including the function in my framework.

You mean my messy code is good because it fixes (for display) the float inaccuracies? Yeah that's why I made it :-)


Dreamora(Posted 2007) [#8]
The new MingW fixes several float inaccuracies and above that raises the math speed by 30-200% depending on the "core support" BRL is targetting (by raising the MinSpecs to P3 you get over 100% in different situations, right now it does not use any extended math commandset from CPU).
Haven't seen any "screw up" so far.

And yes, thats what I meant with my EDIT :)


Grey Alien(Posted 2007) [#9]
sounds cool. So I wonder what happens if someone runs it NOT on a P3?


tonyg(Posted 2007) [#10]
If you want the use to see '1.4' then can't you use the string without converting it back to a float.


Grey Alien(Posted 2007) [#11]
what string? I have a float (which I've calculated and could be 1.4 or whatever) and I need it to be converted to a nice rounded string (not just truncated). Am I mssing something there's no string in Dreamora's function? Perhaps I could intercept result before it's divided by t, convert to a string and work out where to truncate and place the decimal point.


Dreamora(Posted 2007) [#12]
Yes if you wanted to get a "nice String" you would use the result upfront the division, make it a string and split it at decimals from right by entering a . there.


Grey Alien(Posted 2007) [#13]
Anyone see anything wrong with this? It seems fine to me but I've not extensively tested it:

Strict

Local test#=1.356
Print test
Print ccRoundFloatToString(test,1)
Print ccRoundFloatToString(test,2)
Print ccRoundFloatToString(test,3)

Function ccRoundFloatToString$(number:Float,decimals:Int)
	'This gets rid of any floating point inaccuracies such as 1.4 stored in a float
	'displaying as 1.39999 etc.
	Local t:Int = 10^decimals
	Local result% = Int(number * t + 0.5)
	'Convert result into a string	
	Local display$ = String(result)
	'Now put in a decimal point at the correct place.
	display = Mid(display,1,Len(display)-decimals)+"."+Mid(display,Len(display)-decimals+1,decimals)
	Return display
End Function


Gives these results:


1.35599995
1.4
1.36
1.356




grable(Posted 2007) [#14]
Heres what i use to pretty print floats.
Theres probably a better sollution, but it works =)

Function PrettyFloat:String( value:Double)
Extern "C"
	Function modf:Double( value:Double, intpart:Double Var)
	Function snprintf_i:Int( buf:Byte Ptr, szbuf:Int, fmtstr$z, val:Int) = "snprintf"
	Function snprintf_f:Int( buf:Byte Ptr, szbuf:Int, fmtstr$z, val:Double) = "snprintf"	
EndExtern
	Local i:Double
	Local buf:Byte[32]
	Local l:Int
	' check for whole number
	If modf( value, i) = 0.0 Then 
		l = snprintf_i( buf, SizeOf(buf), "%d", i)
		Return String.FromBytes( buf, l)
	Else
		' eliminate ending zeros except the one after "."
		l = snprintf_f( buf, SizeOf(buf), "%f", value)
		While (l > 0) And (buf[l-1] = Asc("0")) And (buf[l-2] <> Asc("."))
			l :- 1
		Wend
		' terminate it, just in case
		buf[l+1] = 0
		Return String.FromBytes( buf, l)
	EndIf
EndFunction


EDIT: there was a typo, fixed now!


Grey Alien(Posted 2007) [#15]
Thx. I wondered when someone was going to resort to using printf from C.


Dreamora(Posted 2007) [#16]
GA: Looks correct to me, don't see any more or less obvious problem.


tonyg(Posted 2007) [#17]
0.05 gives odd results in your code GA.<edit> and 50.00
<edit> Knew it had been done before here <edit2> except that still struggles with 50.0.
Just for interest :
comment from Mark


Dreamora(Posted 2007) [#18]
Here is a corrected code. Note: replaced the int conversion with floor.

Strict

Local test#=50
Print test
Print ccRoundFloatToString(test,1)
Print ccRoundFloatToString(test,2)
Print ccRoundFloatToString(test,3)

Function ccRoundFloatToString$(number:Float,decimals:Int)
	'This gets rid of any floating point inaccuracies such as 1.4 stored in a float
	'displaying as 1.39999 etc.
	Local t:Int = 10^decimals
	Local result% = Floor(number * t + 0.5)
	'Convert result into a string	
	Local display$ = String(result)
	For Local i:Int = Len(display) To decimals
		display = "0" + display
	Next
	'Now put in a decimal point at the correct place.
	display = Mid(display,1,Len(display)-decimals)+"."+Mid(display,Len(display)-decimals+1,decimals)
	Return display
End Function



There are surely simpler solutions.


Grey Alien(Posted 2007) [#19]
TonyG, good research thanks! That other thread doesn't round though. [edit]Tony I'm confused, I plugged 0.5 and 50 into my latest code and it seemed fine. well 0.5 was .5 (missing leading 0) and 50 was fine. What exactly did you discover? thanks.

Dreamora: aha, I wondered about your use of Int as my ccRound uses Floor. What's the differece though between Int and Floor (apart from with negative numbers), they both round down right? Does it affect my routine for TonyG's test? Thanks.


Grey Alien(Posted 2007) [#20]
OK i've fixed it for when you specify 0 as the Decimals. But any number under 1 is messed up big time. Will fix.

Also noticed more weirdness, 1.05001 rounds up to 1.1 (if one decimal place) but 1.05 stays at 1.0 :-(


Grey Alien(Posted 2007) [#21]
wow, found out something weird about mid(), see the other thread. Anyway here's a fixed version for 0 (or negative) as the decimals param and also works for numbers < 1 now. Also that 1.05 problem is fixed.

Function ccRoundFloatToString$(number:Float,decimals:Int)
	'This gets rid of any floating point inaccuracies such as 1.4 stored in a float
	'displaying as 1.39999 etc.
	Local t:Int = 10^decimals
	Local result% = Floor(number * t + 0.5)
	'Convert result into a string	
	Local display$ = String(result)
	'Do we need to append leading zeros for numbers less than 1?
	While Len(display)-decimals <= 0
		display = "0"+display
	Wend
	'Now put in a decimal point at the correct place.
	If decimals > 0 Then display = Mid(display,1,Len(display)-decimals)+"."+Mid(display,Len(display)-decimals+1,decimals)
	Return display
End Function



tonyg(Posted 2007) [#22]
OK.
GA original code for 0.05 gave :
0.0500000007
.1
5.5
50.50
and for 50.00 or 50 gave :
50.0000000
50.0
50.00
49.950

Dreamora's corrected code for 0.05 gave :
0.0500000007
0.1
0.05
0.050
which seems ok but for 50.00 or 50 gave :
50.0000000
50.0
50.00
49.950

GA latest test for 0.05 gave :
0.0500000007
0.1
0.05
0.050
which, again, seems OK but 50.00 gave :
50.0000000
50.0
50.00
49.950

I could have it wrong but 49.950 seems incorrect.


Grey Alien(Posted 2007) [#23]
! your 50 result is WEIRD. This code on my PC:

Local test#=50
Print test
Print ccRoundFloatToString(test,1)
Print ccRoundFloatToString(test,2)
Print ccRoundFloatToString(test,3)

Function ccRoundFloatToString$(number:Float,decimals:Int)
	'This gets rid of any floating point inaccuracies such as 1.4 stored in a float
	'displaying as 1.39999 etc.
	Local t:Int = 10^decimals
	Local result% = Floor(number * t + 0.5)
	'Convert result into a string	
	Local display$ = String(result)
	'do we need to append a leading zero for numbers less than 1?
	'Now put in a decimal point at the correct place.
	While Len(display)-decimals <= 0
		display = "0"+display
	Wend
	If decimals > 0 Then display = Mid(display,1,Len(display)-decimals)+"."+Mid(display,Len(display)-decimals+1,decimals)
	Return display
End Function


gives:


50.0000000
50.0
50.00
50.000



Thanks for testing btw.

Dreamora: oops, just noticed you added in leading zeros which I did with a while loop. Your for loop should be faster as Len only needs to be evaluated once by the for loop. Changing mine now.


tonyg(Posted 2007) [#24]
I get 49.950 on my PC and laptop.


Grey Alien(Posted 2007) [#25]
Wonder if it's a difference in our Blitz or MinGW or CPUs, how odd. I'll try it on my Macbook Pro.


Grey Alien(Posted 2007) [#26]
MacBook pro gives 50 all the way. Are you pulling my leg?


tonyg(Posted 2007) [#27]
Nope. Using 1.24 fully synced plus MinGW 3.1.0


Grey Alien(Posted 2007) [#28]
I'm using 1.24 fully synced. Don't know what MinGW mine is but I haven't upgraded it. My CPU is an Intel btw...


MGE(Posted 2007) [#29]
I got this on my PC using GA's test:
50.0000000
50.0
50.00
50.000


tonyg(Posted 2007) [#30]
Here's the code with some extra print statements :

and just to confirm that this :
Local number:Float = 50.0
Local decimals:Int=3

Local t:Int = 10^decimals
Local result% = Floor(number * t + 0.5)
Print "t : " + t
Print "number : " + number
Print "result% : " + result

gives :
t : 999
number : 50.0000000
result% : 49950

<edit> Finally,
t1:Int = 10 ^ 3
Print t1
Local decimals:Int = 3
t2:Int = 10 ^ decimals
Print t2

produces
1000
999
I am sure I remember this being raised before.
...


Grey Alien(Posted 2007) [#31]
t : 1000
number : 50.0000000
result% : 50000

1000
1000


Pretty strange...


Dreamora(Posted 2007) [#32]
You have an AMD. Don't know for how far back this holds, but if your CPU has one, install the cool'n'quiet driver.
otherwise calculation and timing errors are assumed to be common.


Grey Alien(Posted 2007) [#33]
Amon installed those drivers and it got rid of some problems he was having, can't remember what though. To think that floating point maths would be affected just seems crazy! (if true)


tonyg(Posted 2007) [#34]
Hmmm. Don't think I can use Cool'n'Quiet with 2700+.


Dreamora(Posted 2007) [#35]
OK, CPU drivers in general.

Intel drivers come with Windows, AMDs don't, thats why Intel CPUs don't suffer this problems.

And its not crazy that this happens. Especially as long as programmers behave like morrons and use high resolution timer routines clearly marked by MS, Intel and AMD to be avoided for exactly this reason (high res are high broken with a dynamically stepped cpu)