IsNumber

Share your advanced PureBasic knowledge/code with the community.
Trond
Always Here
Always Here
Posts: 7446
Joined: Mon Sep 22, 2003 6:45 pm
Location: Norway

Re: IsNumber

Post by Trond »

I tried to mimic PB and Delphi. I think it's cleaner design to trim the spaces before checking if a string is numeric.
Here's with negative numbers (and negative exponents):

Code: Select all

Structure SParseNumber
  Input.s
  Look.c
EndStructure

Procedure GetChar(*S.SParseNumber)
  *S\Look = Asc(*S\Input)
  *S\Input = Mid(*S\Input, 2)
EndProcedure

Procedure GetInteger(*S.SParseNumber)
  Protected Result = 0
  While *S\Look >= '0' And *S\Look <= '9'
    Result = 1
    GetChar(*S)
  Wend
  ProcedureReturn Result
EndProcedure

Procedure GetSign(*S.SParseNumber)
  If *S\Look = '-'
    GetChar(*S)
  EndIf
EndProcedure

Procedure GetFloat(*S.SParseNumber)
  ; Returns:
  ; 0 = syntax error
  ; 1 = valid integer
  ; 2 = valid floating point
  GetSign(*S)
  IsInt = GetInteger(*S)
  If IsInt And *S\Look = '.' ; decimal point
    GetChar(*S)
    IsInt = GetInteger(*S)
    If IsInt = 0
      ProcedureReturn 0 ; syntax error
    EndIf
    ProcedureReturn 2
  EndIf
  ProcedureReturn IsInt
EndProcedure

Procedure GetNumberType(S.s)
  ; PureBasic Forum: Trond
  ; Returns:
  ; 0 = syntax error
  ; 1 = valid integer
  ; 2 = valid floating point, but not with exponent
  ; 3 = valid number in exponent format  
  N.SParseNumber
  N\Input = S
  GetChar(@N)
  IsFloat = GetFloat(@N)
  If IsFloat And N\Look = 'e'
    GetChar(@N)
    GetSign(@N)
    IsInt = GetInteger(@N)
    If IsInt And N\Look = 0
      ProcedureReturn 3
    Else
      ProcedureReturn 0
    EndIf
  EndIf
  If N\Look = 0
    ProcedureReturn IsFloat
  Else
    ProcedureReturn 0
  EndIf
EndProcedure
User avatar
idle
Always Here
Always Here
Posts: 5839
Joined: Fri Sep 21, 2007 5:52 am
Location: New Zealand

Re: IsNumber

Post by idle »

skywalk wrote:Hi,
@Trond and Idle...
Not sure if your versions are complete?
They reject several examples of exponential numbers...

EDIT: By the way, Set your Debug window font to a fixed pitch font(courier new 8) for easy viewing.
I just found out this is possible! :)

Thanks,
Steve
yes it was probably because I had only allocated the memory for 16 bytes and using 18 bytes
User avatar
Demivec
Addict
Addict
Posts: 4260
Joined: Mon Jul 25, 2005 3:51 pm
Location: Utah, USA

Re: IsNumber

Post by Demivec »

Here's a rewrite of Xombie's version, same functionality and results but with some simplifications to make it easier to follow (for me mostly). I thought I would share it in case anyone else might find it useful.

Code: Select all

;Original version: Xombie (11/29/2006), rewrite by Demivec

Procedure IsNumber(InString.s, DecimalCharacter.c = '.', ThousandsSeparator.c = ',')
  ;Returns 0 for non-numeric values, 1 for positive integers, -1 for negative integers,
  ;2 for positive floats and -2 for negative floats. 
  #NotNumber = 0
  #IsInteger = 1
  #IsFloat = 2
  
  InString = Trim(InString)
  Protected IsDecimal, CaughtThousand, CaughtDecimal, CaughtE
  Protected Sign = 1, IsSignPresent, IsSignAllowed = #True, CountNumeric
  Protected *CurrentChar.Character = @InString
  
  While *CurrentChar\c
    Select *CurrentChar\c
      Case '0' To '9'
        CountNumeric + 1
        If CaughtThousand And CountNumeric > 3: ProcedureReturn #NotNumber: EndIf 
        IsSignAllowed = #False
      Case ThousandsSeparator
        If CaughtDecimal Or CaughtE Or CountNumeric = 0 Or (CaughtThousand And CountNumeric <> 3)
          ProcedureReturn #NotNumber
        EndIf 
        
        CaughtThousand = #True
        CountNumeric = 0
      Case DecimalCharacter
        If CaughtDecimal Or CaughtE Or CountNumeric = 0 Or (CaughtThousand And CountNumeric <> 3)
          ProcedureReturn #NotNumber
        EndIf
        
        CountNumeric = 0
        CaughtDecimal = #True
        IsDecimal = #True
        CaughtThousand = #False
      Case  '-'
        If IsSignPresent Or Not IsSignAllowed: ProcedureReturn #NotNumber: EndIf 
        If Not CaughtE: Sign = -1: EndIf
        IsSignPresent = #True
      Case '+'
        If IsSignPresent Or Not IsSignAllowed: ProcedureReturn #NotNumber: EndIf 
        IsSignPresent = #True
      Case 'E', 'e'
        If CaughtE Or CountNumeric = 0 Or (CaughtThousand And CountNumeric <> 3)
          ProcedureReturn #NotNumber
        EndIf
        
        CaughtE = #True
        CountNumeric = 0
        CaughtDecimal = #False
        CaughtThousand = #False
        IsSignPresent = #False
        IsSignAllowed = #True
      Default
        ProcedureReturn #NotNumber
    EndSelect
    *CurrentChar + SizeOf(Character)
  Wend
  
  If CountNumeric = 0 Or (CaughtThousand And  CountNumeric <> 3): ProcedureReturn #NotNumber: EndIf
  If IsDecimal
    ProcedureReturn #IsFloat * Sign
  EndIf
  ProcedureReturn #IsInteger * Sign
EndProcedure
User avatar
skywalk
Addict
Addict
Posts: 4211
Joined: Wed Dec 23, 2009 10:14 pm
Location: Boston, MA

Re: IsNumber

Post by skywalk »

It didn't dawn on me that ValD("100eeee") = 100 instead of 0. until I tried to implement a masked edit string gadget.
ValD() actually truncates the text after it finds a numeric value?? Doh!

So, your code fixes my MaskedEdit gadget entry.

Thanks very much demivec and xombie!

Code: Select all

Procedure.i SF_IsNumeric(StrIn$, DecimalCharacter.c='.', ThousandsSeparator.c=',')
  ; REV:  100405, Skywalk
  ;       Modified PureBasic Forum: Xombie(061129) + Demivec(100323)
  ;       Removed 3 constants, add Lcase, add 'd' exponent support
  ; REV:  110311, Skywalk
  ;       Enabled leading '.' to define a numeric: -.1 = -0.1
  ; Return: 0 = non-numeric value = 0
  ;         1 = positive integer
  ;        -1 = negative integer
  ;         2 = positive float
  ;        -2 = negative float
  StrIn$ = LCase(Trim(StrIn$))  ; eliminate comparisons for E & D
  Protected.i IsDecimal, CaughtThousand, CaughtDecimal, CaughtE
  Protected.i Sgn = 1, IsSignPresent, IsSignAllowed = 1, CountNumeric
  Protected.i *CurrentChar.Character = @StrIn$
  While *CurrentChar\c
    Select *CurrentChar\c
    Case '0' To '9'
      CountNumeric + 1
      If CaughtThousand And CountNumeric > 3
        ProcedureReturn 0
      EndIf
      IsSignAllowed = 0
    Case ThousandsSeparator
      If CaughtDecimal Or CaughtE Or CountNumeric = 0 Or (CaughtThousand And CountNumeric <> 3)
        ProcedureReturn 0
      EndIf
      CaughtThousand = 1
      CountNumeric = 0
    Case DecimalCharacter
      ;If CaughtDecimal Or CaughtE Or CountNumeric = 0 Or (CaughtThousand And CountNumeric <> 3)
      If CaughtDecimal Or CaughtE Or (CaughtThousand And CountNumeric <> 3) ; Allow leading decimal to signify a numeric
        ProcedureReturn 0
      EndIf
      CountNumeric = 0
      CaughtDecimal = 1
      IsDecimal = 1
      CaughtThousand = 0
    Case '-'
      If IsSignPresent Or Not IsSignAllowed
        ProcedureReturn 0
      EndIf 
      If Not CaughtE: Sgn = -1: EndIf
      IsSignPresent = 1
    Case '+'
      If IsSignPresent Or Not IsSignAllowed
        ProcedureReturn 0
      EndIf 
      IsSignPresent = 1
    Case 'e', 'd' ; Lcase = Don't Care -> 'E', 'D'
      If CaughtE Or CountNumeric = 0 Or (CaughtThousand And CountNumeric <> 3)
        ProcedureReturn 0
      EndIf
      CaughtE = 1
      CountNumeric = 0
      CaughtDecimal = 0
      CaughtThousand = 0
      IsSignPresent = 0
      IsSignAllowed = 1
    Default
      ProcedureReturn 0
    EndSelect
    *CurrentChar + SizeOf(Character)
  Wend
  If CountNumeric = 0 Or (CaughtThousand And CountNumeric <> 3)
    ProcedureReturn 0
  EndIf
  If IsDecimal
    ProcedureReturn 2 * Sgn ; -> Float
  EndIf
  ProcedureReturn Sgn       ; -> Integer
EndProcedure

Enumeration 
  #Window_0 = 0
  #String_0 = 0 
  #Text_0 
EndEnumeration 

Procedure.i ForceNumeric(gID.i)
  ; Verify numeric entries...
  If SF_IsNumeric(GetGadgetText(gID))     
    SetGadgetColor(gID, #PB_Gadget_BackColor, #Green)  
    ProcedureReturn #True
  Else
    SetGadgetColor(gID, #PB_Gadget_BackColor, #Red)
    ProcedureReturn #False
  EndIf
EndProcedure

If OpenWindow(#Window_0, 434, 225, 193, 174, "Verify Numeric Entry",  #PB_Window_SystemMenu | #PB_Window_SizeGadget | #PB_Window_TitleBar ) 
  Attributes.i = #ES_CENTER
  TextGadget(#Text_0, 10, 10, 150, 20, "Enter Numeric Text Below", #PB_Text_Center) 
  hStringGadget = StringGadget(#String_0, 10, 45, 150, 20, "-1.234e-12", Attributes) 
  SendMessage_(hStringGadget, #EM_SETLIMITTEXT, 16, 0) ; Force character limit = 16
EndIf 

quit = #False 
Repeat 
  event = WaitWindowEvent() 
  Select event 
  Case #PB_Event_Gadget 
    Select EventGadget() 
    Case #String_0      
      If EventType() = #PB_EventType_Change            
        ForceNumeric(#String_0)         
      EndIf
    EndSelect 
  Case #PB_Event_CloseWindow 
    quit = #True 
  EndSelect 
Until quit
Last edited by skywalk on Sat Apr 01, 2017 5:39 pm, edited 2 times in total.
User avatar
Frarth
Enthusiast
Enthusiast
Posts: 241
Joined: Tue Jul 21, 2009 11:11 am
Location: On the planet
Contact:

Re: IsNumber

Post by Frarth »

I always prefer to keep things simple. If a user enters '23rubbish' the Val function would return 23, so it is numeric. Each language has its specific VAL functions and to me, it would be logical to write an IsNumeric function which honours the VAL function's behaviour.

Having said this, I believe this would be a good start (with support for Hex numbers):

Code: Select all

; IsNumeric by Frarth (P) April 2010

Procedure.b IsNumeric(Text.s)
  ; something to test?
  If Text = ""
    ProcedureReturn #False
  EndIf
  ; non zero integer?
  If Val(Text) <> 0
    ProcedureReturn #True
  EndIf
  ; non zero float?
  If ValD(Text) <> 0
    ProcedureReturn #True
  EndIf
  ; starts with zero?
  If Left(Text, 1) = "0"
    ProcedureReturn #True
  EndIf
  ; starts with signed or float zero or hex zero?
  If Len(Text) > 1
    Select Left(Text, 2)
    Case "-0", "+0", ".0", "$0"
      ProcedureReturn #True
    EndSelect
    ; starts with signed float or hex?
    If Len(Text) > 2
      Select Left(Text, 3)
      Case "-.0", "+.0", "-$0", "+$0"
        ProcedureReturn #True
      EndSelect
    EndIf
  EndIf
  ProcedureReturn #False
EndProcedure

; Examples

Text.s = "1"
Debug IsNumeric(Text)

Text.s = "1rubbish"
Debug IsNumeric(Text)

Text.s = ".0"
Debug IsNumeric(Text)

Text.s = "-.0"
Debug IsNumeric(Text)

Text.s = "-1.5E-7"
Debug IsNumeric(Text)

Text.s = "$beef"
Debug IsNumeric(Text)

Text.s = "-$beef"
Debug IsNumeric(Text)
PureBasic 5.41 LTS | Xubuntu 16.04 (x32) | Windows 7 (x64)
User avatar
skywalk
Addict
Addict
Posts: 4211
Joined: Wed Dec 23, 2009 10:14 pm
Location: Boston, MA

Re: IsNumber

Post by skywalk »

Frarth wrote:I always prefer to keep things simple. If a user enters '23rubbish' the Val function would return 23, so it is numeric. Each language has its specific VAL functions and to me, it would be logical to write an IsNumeric function which honours the VAL function's behaviour.

Having said this, I believe this would be a good start (with support for Hex numbers)
Hi Frarth,
What a crazy wacky world where a user is entering numbers in Hex? :shock:
Do you have Vulcans for customers? :)

Seriously though, I have had applications where users required a bit stream(variable lengths) to be defined for a serial interface to control custom integrated circuits.
Entering in HEX would complicate the subtle state changes.
ie: 10101010 = AA, The '1' state locations are way easier to follow in Binary than the HEX 'A' defines.

And restricting user inputs on Binary StringGadgets is limited to '1's and '0's and any don't care letter you choose, like 'X'.

As for my current application, if a user enters a '23rubbish', I want to notify them via a Red background, and not accept an erroneous entry.
Like you, I thought following the Val("stringnumeric") logic would have met my needs, but it fails in this case.
Last edited by skywalk on Sun Nov 25, 2012 9:01 pm, edited 1 time in total.
User avatar
Frarth
Enthusiast
Enthusiast
Posts: 241
Joined: Tue Jul 21, 2009 11:11 am
Location: On the planet
Contact:

Re: IsNumber

Post by Frarth »

Hi Steve,
Nope, I do not have Vulcans on board here; instead there are Arcturians :lol: . Just thought it to be nice to support hex numbers.

Allowing customers to only follow very strict rules often requires a lot more coding, as we have seen in this topic. IMO there is more flexibility in allowing rubbish (erronous entries or formatted code) behind a number. If the outcome is 23, which it should be, who cares about any dismissed part? But that's just MHO.

Handling numeric data can be complicated anyway. For instance, handling geographic coordinates, dates or times. Most applications do not allow other separators than the standard (I hate those irritating error messages). But if the three parts can be retrieved because the separators are any other than numeric, the outcome will be correct. That's why I seldomly use a numeric filter with a string gadget.

Frank
PureBasic 5.41 LTS | Xubuntu 16.04 (x32) | Windows 7 (x64)
Post Reply