Page 1 of 2

Printing floating point numbers with exponents.

Posted: Sun Jan 29, 2012 6:28 am
by Demivec
Code updated for 5.20+

Here are two functions for converting a floating point number into a string representation using exponent notation. The functions allow specifying the number of significant digits to show.

I looked around for quite a while to find functions like this for saving floating point numbers to text files without using a binary representation of them. There are a few in the forum but the ones I am posting here take a slightly different approach and seem to be smaller and more straight forward.

It would be great if something similar could be implemented natively in PureBasic. They could also be added with additional formatting options to a sPrintF() implementation (hint :wink: ).

Code: Select all

Procedure.s strFE(x.f, dec.b = 6)
  If x = 0: ProcedureReturn "0": EndIf
  Protected e.l = (((PeekL(@x) >> 23) & %11111111) - 127) * Log10(2)
  If dec < 0: dec = 0: EndIf 
  ProcedureReturn StrF(x / Pow(10, e), dec) + "e" +Str(e)
EndProcedure

Procedure.s strDE(x.d, dec.b = 6)
  If x = 0: ProcedureReturn "0": EndIf
  Protected e.q = (((PeekQ(@x) >> 52) & %11111111111) - 1023) * Log10(2)
  If dec < 0: dec = 0: EndIf 
  ProcedureReturn StrD(x / Pow(10, e), dec) + "e" +Str(e)
EndProcedure

Debug strDE(-3.5e20)
Debug strDE(-3.5e-20)
Debug strFE(3.5e20)
Debug strFE(3.5e-20)
@Edit: Made a correction to strDE() to use StrD() instead of StrF().

Re: Printing floating point numbers with exponents.

Posted: Sun Jan 29, 2012 7:47 am
by Little John
Very useful. Thank you, Demivec!
It would be great if something similar could be implemented natively in PureBasic. They could also be added with additional formatting options to a sPrintF() implementation (hint :wink: ).
I agree.

In Procedure strDE(), it reads "ProcedureReturn StrF(...) ...".
Is this intended? (just curious ;-) )

Best regards, Little John

Re: Printing floating point numbers with exponents.

Posted: Sun Jan 29, 2012 8:24 am
by Demivec
Little John wrote:In Procedure strDE(), it reads "ProcedureReturn StrF(...) ...".
Is this intended? (just curious ;-) )
It has been corrected now to use StrD(). Thanks for keeping me honest. :)

Re: Printing floating point numbers with exponents.

Posted: Sun Jan 29, 2012 6:30 pm
by skywalk
Demivec, that is one cool brain teaser of a solution. 8)
This is Trond's code with a small speed up change for negative exponents.
http://www.purebasic.fr/english/viewtop ... 84#p346484
Yours is ~9% faster for most sign cases, but I do prefer to maintain a fixed width output for all cases and I never print scientific notation.
Use Engineering Notation -> ±xx.yye±"multiple of 3" instead...
http://www.purebasic.fr/english/viewtop ... 74#p346474

Code: Select all

Procedure.s StrDsci(x.d, numsd.i=6) 
  ; Scientific notation, Trond
  ; skywalk, ExpSgn from .s to .i, faster for negative exponents.
  ; Supports ± exponent sign and fixed width for ± numbers.
  Protected.i Exp
  Protected.i Sgn = ' '
  Protected.i ExpSgn = '+'
  If x < 0
    sgn = '-'
    x * -1
  EndIf  
  If x <> 0
    While x >= 10
      x / 10
      Exp + 1
    Wend
    While x <= 1
      x * 10
      Exp + 1
      ExpSgn = '-'
    Wend
  EndIf
  ProcedureReturn Chr(sgn) + StrD(x, numsd-1) + "e" + Chr(ExpSgn) + Str(exp)
EndProcedure

Re: Printing floating point numbers with exponents.

Posted: Mon Jan 30, 2012 6:04 am
by wilbert
Both ways have issues.
Try
Debug StrDsci(Infinity())
or
Debug strDE(999)

Re: Printing floating point numbers with exponents.

Posted: Mon Jan 30, 2012 7:06 am
by skywalk
wilbert wrote:Both ways have issues.
Try
Debug StrDsci(Infinity())
No issues here?
My computer says it will be done when I finish drinking Lake Superior. :P

Re: Printing floating point numbers with exponents.

Posted: Mon Jan 30, 2012 7:19 am
by skywalk
wilbert wrote:Both ways have issues.
Try
Debug strDE(999)
The 999 results are valid numbers in scientific notation.
StrDe(999,4) = 0.9990e3
StrDsci(999,4) = 9.990e+2
And from my older post, StrDe(999,4) = 999.0e+0 :wink:

Re: Printing floating point numbers with exponents.

Posted: Mon Jan 30, 2012 8:57 am
by wilbert
Here's my attempt at getting consistent results

Code: Select all

Procedure.s SciStrD(x.d, dec.a = 5, engineering = #False)
  
  Protected.s{1} sgn, esgn
  Protected.l e
  
  ; check for Infinity and NaN
  !mov edx, 0x7ff00000
  !mov eax, [p.v_x + 4]
  !and edx, eax
  !xor edx, 0x7ff00000 
  !jnz SciStrD_cont1
  ProcedureReturn StrD(x)
  
  ; extract sign
  !SciStrD_cont1:
  !btr eax, 31
  !mov [p.v_x + 4], eax
  !mov al, 32; ' '
  !mov cl, 45; '-'
  !cmovc eax, ecx
  !mov [p.v_sgn], al
  !xor edx, 0x7ff00000 
  !jz SciStrD_cont2
  
  ; extract exponent and exponent sign
  If engineering
    e = Int(Round(Log10(x) / 3.0, #PB_Round_Down)) * 3
    x / Pow(10.0, e)
    If Val(StrD(x, dec)) >= 1000.0
      x / 1000.0 : e + 3
    EndIf
  Else
    e = Int(Round(Log10(x), #PB_Round_Down))
    x / Pow(10.0, e)
    If Val(StrD(x, dec)) >= 10.0
      x / 10.0 : e + 1
    EndIf
  EndIf
  !SciStrD_cont2:
  !mov edx, [p.v_e]
  !mov eax, edx
  !neg eax
  !cmovs eax, edx
  !mov [p.v_e], eax
  !mov al, 43; '+'
  !mov cl, 45; '-'
  !cmovg eax, ecx
  !mov [p.v_esgn], al
  
  ; return result
  ProcedureReturn sgn + StrD(x, dec) + "e" + esgn + Str(e)
  
EndProcedure
Results

Code: Select all

MinusZero.d
PokeQ(@MinusZero, 1 << 63)

Debug StrDE(Infinity())     ; +Infinitye308   ***
Debug StrDE(MinusZero)      ; 0
Debug StrDE(NaN())          ; 0               ***
Debug StrDE(0.3)            ; 3.000000e-1
Debug StrDE(0.1)            ; 10.000000e-2
Debug StrDE(300)            ; 3.000000e2
Debug StrDE(100)            ; 10.000000e1

Debug StrDsci(Infinity())   ; hangs           ***
Debug StrDsci(MinusZero)    ;  0.00000e0
Debug StrDsci(NaN())        ; -NaNe+0         ***
Debug StrDsci(0.3)          ;  3.00000e-1
Debug StrDsci(0.1)          ;  10.00000e-2
Debug StrDsci(300)          ;  3.00000e+2
Debug StrDsci(100)          ;  10.00000e-3    ***

Debug SciStrD(Infinity())   ; +Infinity
Debug SciStrD(MinusZero)    ; -0.00000e+0
Debug SciStrD(NaN())        ; NaN
Debug SciStrD(0.3)          ;  3.00000e-1
Debug SciStrD(0.1)          ;  1.00000e-1
Debug SciStrD(300)          ;  3.00000e+2
Debug SciStrD(100)          ;  1.00000e+2
Engineering notation results

Code: Select all

Debug SciStrD(0.23, 5, #True)   ;  230.00000e-3
Debug SciStrD(2.30, 5, #True)   ;  2.30000e+0
Debug SciStrD(23.0, 5, #True)   ;  23.00000e+0
Debug SciStrD(2300, 5, #True)   ;  2.30000e+3

Re: Printing floating point numbers with exponents.

Posted: Mon Jan 30, 2012 10:41 am
by Demivec
wilbert wrote:Debug strDE(999)
@wilbert: Nice catch. It is nice to know that the result isn't wrong, just inconsistent. I was still exploring any possible improvements that could be made, and hadn't addressed the Infinity or NaN results yet. I'm not sure if I consider MinusZero worth the bother though.

I like your version of SciStrD(). It's classy. :)

Re: Printing floating point numbers with exponents.

Posted: Mon Jan 30, 2012 10:52 am
by wilbert
@Demivec, yes, the results were only inconsistent. It has to do with the way you are getting the exponent.
If you would use something like
Protected e.d = Round(Log10(Abs(x)), #PB_Round_Down)
the inconsistencies should be gone.
Checking for NaN or Infinity could be done like this

Code: Select all

Procedure.s strDE(x.d, dec.a = 6)
  If x = x * 2 : ProcedureReturn StrD(x, 0) : EndIf
  Protected e = Int(Round(Log10(Abs(x)), #PB_Round_Down)) : x / Pow(10, e)
  If Val(StrD(x, dec)) >= 10.0 : x / 10.0 : e + 1 : EndIf
  ProcedureReturn StrD(x, dec) + Left("e+", e >> 9 + 2) + Str(e)
EndProcedure

Debug StrDE(Infinity())     ; +Infinity
Debug StrDE(MinusZero)      ; 0
Debug StrDE(NaN())          ; NaN
Debug StrDE(0.3)            ; 3.000000e-1
Debug StrDE(0.1)            ; 1.000000e-1
Debug StrDE(300)            ; 3.000000e+2
Debug StrDE(100)            ; 1.000000e+2

Re: Printing floating point numbers with exponents.

Posted: Mon Jan 30, 2012 5:31 pm
by akj
@wilbert:

There are still minor inconsistencies in your solution. Try for example:

Code: Select all

Debug StrDE(999, 0)
Debug StrDE(999, 1)
Debug StrDE(999, 2)
Debug StrDE(999, 3)
Also, I think it would be better to use dec.a rather than dec.b for the number of decimal places as it's value must be positive.

Re: Printing floating point numbers with exponents.

Posted: Mon Jan 30, 2012 6:37 pm
by wilbert
@akj, I tried to built in a correction.
Is it working now or still inconsistencies ?
If you have a better solution, feel free to share.

Re: Printing floating point numbers with exponents.

Posted: Mon Jan 30, 2012 7:26 pm
by skywalk
Hi wilbert,
It's rare I beat your asm code(~5-10% faster), but I am not checking for Infinity() or deviating from Engineering Notation.
If the number exceeds a Double, I get a 'NaN' result anyway.
Also, I need the string result to maintain fixed width, given a number of significant digits, not based on number of decimal places. SciStrD() gives variable widths depending on Engineering notation.

Code: Select all

Debug SciStrD(3.5e20,6,1)   ; [ 350.000000e+18]
Debug SciStrD(-3.5e-20,6,1) ; [-35.000000e-21]
Debug StrDe(3.5e20,6)       ; [ 350.000e+18]
Debug StrDe(-3.5e-20,6)     ; [-35.0000e-21]

Code: Select all

Procedure.s StrDe(x.d, numsd.i=6)
  ; skywalk
  ;Debug StrDE(1.23456e7, 4)   ; " 12.35e+6" <-- fixed widths
  ;Debug StrDE(-1.23456e7, 4)  ; "-12.35e+6" <-- and Exponent in multiples of 3
  ;Debug StrDE(0, 4)           ; "        0" <-- 0 is simplified, not 0.000e+0  
  ; GIVEN: my$ = StrDE(x, numsd)
  ; IN:
  ;   x = real number to be formatted.
  ;   numsd = integer specifying total number of significant digits.
  ;   "aa.bbbe+3" -> 2 + 3 = 5
  ; RETURN:
  ;   string of x with numsd significant digits using engineering notation.
  ; NOTE:
  ;   Does not check for x = Infinity(), but does print NaN instead.
  ;   Double -> ±2.2250738585072013e-308 to ±1.7976931348623157e+308
  Protected.i Exp, Sgn
  If x > 0
    Sgn = #SPC   ; or ASC(" ") or ' '
  ElseIf x < 0
    Sgn = #DASH ; '-'
    x * -1
  Else          ; Should fill " 000.000e+0", +1 for 'sign', +1 for '.', +3 for 'e+0'
    ProcedureReturn RSet("0", numsd + 5)
  EndIf
  exp = Round(Log10(X),#PB_Round_Down)
  If exp > 0
    exp / 3 * 3
  Else
    exp = (-exp + 3) / 3 * (-3)
  EndIf
  x = x * Pow(10,-exp)
  If x >= 1000
    x / 1000
    exp + 3
  EndIf
  If x >= 100
    numsd - 3
  ElseIf x >= 10
    numsd - 2
  Else
    numsd - 1
  EndIf
  If numsd < 0
    numsd = 0
  EndIf
  If exp < 0
    ProcedureReturn Chr(sgn) + StrD(x, numsd) + "e" + Str(exp)
  Else
    ProcedureReturn Chr(sgn) + StrD(x, numsd) + "e+" + Str(exp)
  EndIf
EndProcedure

Re: Printing floating point numbers with exponents.

Posted: Tue Jan 31, 2012 1:02 pm
by wilbert
@skywalk, your routine isn't always fixed width.
The width is different for
Debug StrDE(999999, 2)
Debug StrDE(1000000, 2)

skywalk wrote:It's rare I beat your asm code(~5-10% faster)
I didn't optimize for speed.
This probably will be faster

Code: Select all

Procedure.s SciStrD(x.d, dec = 5, engineering = #False)
  
  Protected.s{1} sgn, esgn
  Protected.l e
  Static Dim pow10table.d(618)
  Static setTable = #True
  
  ; ** pow10 table **
  If setTable
    For e = -309 To 309
      pow10table(e + 309) = Pow(10, e)
    Next
    setTable = #False
  EndIf
  
  ; ** check for Infinity and NaN **
  !mov edx, 0x7ff00000
  !mov eax, dword [p.v_x + 4]
  !and edx, eax
  !xor edx, 0x7ff00000 
  !jnz SciStrD_cont1
  ProcedureReturn StrD(x)
  
  ; ** extract sign **
  !SciStrD_cont1:
  !btr eax, 31
  !mov dword [p.v_x + 4], eax
  !mov al, 32; ' '
  !mov cl, 45; '-'
  !cmovc eax, ecx
  !mov byte [p.v_sgn], al
  !xor edx, 0x7ff00000 
  !jz SciStrD_cont4
  
  ; ** extract exponent and exponent sign **
  !rol edx, 12
  !sub edx, 1022
  !cvtsi2ss xmm0, edx
  
  ; set things based on engineering flag
  !test byte [p.v_engineering], -1
  !jz SciStrD_noEng
  !mulss xmm0, [SciStrD_lg2div3]
  !cvtss2si edx, xmm0
  !lea edx, [edx + edx * 2]
  !mov eax, 1000
  !mov ecx, 3
  !jmp SciStrD_Eng  
  !SciStrD_noEng:
  !mulss xmm0, [SciStrD_lg2]
  !cvtss2si edx, xmm0
  !mov eax, 10
  !mov ecx, 1
  !SciStrD_Eng:
  !cvtsi2sd xmm3, eax
  
  ; x / pow(10,e)
  !movd xmm0, edx
  !add edx, 309
  CompilerIf #PB_Compiler_Processor = #PB_Processor_x86
    !mov eax, dword [s_SciStrD.a_pow10table]
    !movsd xmm1, [eax + edx * 8]
    !mov edx, dword [p.v_dec]
    !movsd xmm2, [eax + (edx + 309) * 8]
  CompilerElse
    !mov rax, qword [s_SciStrD.a_pow10table]
    !movsd xmm1, [rax + rdx * 8]
    !mov rdx, qword [p.v_dec]
    !movsd xmm2, [rax + (rdx + 309) * 8]
  CompilerEndIf
  !movd edx, xmm0; edx = e
  !movsd xmm0, [p.v_x]; xmm0 = x
  !divsd xmm0, xmm1
  !comisd xmm0, [SciStrD_1]
  !jnc SciStrD_cont2
  !mulsd xmm0, xmm3
  !sub edx, ecx
  
  ; correct for rounding issues
  !SciStrD_cont2:  
  !movsd xmm1, xmm0
  !mulsd xmm1, xmm2
  !addsd xmm1, [SciStrD_1div2]
  !divsd xmm1, xmm2
  !comisd xmm1, xmm3
  !jc SciStrD_cont3
  !divsd xmm0, xmm3
  !add edx, ecx
  
  ; extract exponent sign
  !SciStrD_cont3:  
  !movsd [p.v_x], xmm0
  !SciStrD_cont4:
  !mov eax, edx
  !neg eax
  !cmovs eax, edx
  !mov dword [p.v_e], eax
  !mov al, 43; '+'
  !mov cl, 45; '-'
  !cmovg eax, ecx
  !mov byte [p.v_esgn], al
  
  ; ** return result **
  ProcedureReturn sgn + StrD(x, dec) + "e" + esgn + Str(e)
  
  !SciStrD_1div2     : dd 0, 0x3fe00000
  !SciStrD_1         : dd 0, 0x3ff00000
  
  !SciStrD_lg2       : dd 0x3e9a209b
  !SciStrD_lg2div3   : dd 0x3dcd80ce
  
EndProcedure
and hopefully still works :shock:

Re: Printing floating point numbers with exponents.

Posted: Wed Feb 01, 2012 8:44 am
by skywalk
Hi wilbert,
I couldn't get your last asm version SciStrD() to run - Operand size error?
Good catch on the StrDsci(1000,2) :shock:
I really only use the engineering format of StrDe(x.d, 6), as any fewer significant digits are not acceptable to retain accuracy in the numbers I store. So, StrDe(x.d, 2) is not intended to retain a fixed width or be used.
I have no need for straight scientific notation, but this is my best guess at a fix for StrDsci().

Code: Select all

Procedure.s StrDsciT(x.d, ndec.i=6)
  ; Scientific notation, Trond
  ; skywalk, changed ExpSgn.s to ExpSgn.i. Faster for negative exponents.
  ;          fixed redundant exp summing for cases like x = 10,100,etc.
  Protected.i Exp
  Protected.i Sgn = ' '
  Protected.i ExpSgn = '+'
  ;If ndec < 3: ndec = 3: EndIf
  If x < 0
    sgn = '-'
    x * -1
  EndIf
  If x <> 0
    While x >= 10  ; 'While x >= 10' caused error when x = 10,100,1000,etc.
      x / 10
      Exp + 1
    Wend
    If exp = 0     ; Don't add to Exp if already set.
      While x <= 1
        x * 10
        Exp + 1
        ExpSgn = '-'
      Wend
    EndIf
  EndIf
  ProcedureReturn Chr(sgn) + StrD(x, ndec) + "e" + Chr(ExpSgn) + Str(exp)
EndProcedure