!fnistype.bsi [106] - Functions for testing the type of string and char args
!--------------------------------------------------------------------
!Edit History
! [106] 06-Sep-23 / jdm / Add keywords (for funcidx)
! [105] 20-Jun-22 / jdm / Minor tinkering with fn'isnumfld
! [104] 10-Jun-22 / jdm / Add purenumeric arg to fn'isnumfld 
! [103] 02-Aug-17 / jdm / Add fn'isdate(s), fn'isnumfld(s), fn'istime(s)
! [102] 11-Sep-16 / jdm / Add fn'ishex(), fn'is'all'hex(), convert i2 to BOOLEAN,
!                           use .TRUE, .FALSE, use instr() for better performance; s0 for strings                        
! [101] 23-Oct-14 / jdm / Add ++ifdef, chg return type to i2
! [100] 23-Aug-14 / jdm / Created
!---------------------------------------------------------------------
!Keywords: types parsing validation
!
!Public Functions:
!   Function fn'isdigit(char as s1:inputonly) as BOOLEAN
!   Function fn'isalpha(char as s1:inputonly) as BOOLEAN
!   Function fn'isalnum(char as s1:inputonly) as BOOLEAN
!   Function fn'ispunc(char as s1:inputonly) as BOOLEAN
!   Function fn'chartype(char as s1:inputonly) as BOOLEAN
!   Function fn'ishex(char as s1:inputonly) as BOOLEAN
!   Function fn'all'hex(s) as BOOLEAN
!   Function fn'all'digits(s) as BOOLEAN
!   Function fn'isdate(s) as BOOLEAN         ! [103]
!   Function fn'istime(s) as BOOLEAN         ! [103]
!   Function fn'isnumfld(s,purenumeric) as BOOLEAN       ! [103][104] 
!----------------------------------------------------------------------
++ifnlbl fn'isdigit()
  ++include'once ashinc:types.def       ! [102] (for BOOLEAN)
  ++include'once ashinc:regex.def       ! [103] for instr regex used in fn'isdate
  ++include'once ashinc:gtlang.sdf      ! [103] 
! [103] flags affecting fn'istime() and fn'isdate()  
define ISDTF_IGN_LEADING  = &h0001      ! ignore leading junk (as long as separated by a space)
define ISDTF_IGN_TRAILING = &h0002      ! ignore trailing junk (as long as separated by a space)
define ISDTF_999999       = &h0004      ! allow ###### to be considered a date
!---------------------------------------------------------------------
!Function:
!   Check if character is a digit
!Params:
!   char  (s,1) [in] - character to test
!Returns:
!   .TRUE  (true) if so, 0 if not
!Globals:
!Notes:
!   if fn'isdigit(c$) then ...
!---------------------------------------------------------------------
Function fn'isdigit(char as s1:inputonly) as BOOLEAN
    if instr(1,"0123456789",char) then  ! [102] faster than >= 0 and <= 9
        .fn = .TRUE
    endif
End Function
!---------------------------------------------------------------------
!Function:
!   Check if character is a alpha
!Params:
!   char  (s,1) [in] - character to test
!Returns:
!   .TRUE  (true) if so, 0 if not
!Globals:
!Notes:
!   if fn'isalpha(c$) then ...
!
![102] switch to instr() test - faster than comparaing >= and <=
!---------------------------------------------------------------------
Function fn'isalpha(char as s1:inputonly) as BOOLEAN
    if instr(1,"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",char) then
        .fn = .TRUE
    endif
End Function
!---------------------------------------------------------------------
!Function:
!   Check if character is a alpha
!Params:
!   char  (s,1) [in] - character to test
!Returns:
!   .TRUE  (true) if so, 0 if not
!Globals:
!Notes:
!   if fn'isalpha(c$) then ...
!
![102] switch to instr() test - faster than comparaing >= and <=
!---------------------------------------------------------------------
Function fn'isalnum(char as s1:inputonly) as BOOLEAN
!>!    char = ucs(char)
!>!    if (char >= "0" and char <= "9") &
!>!    or (char >= "A" and char <= "Z") then
    if instr(1,"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",char) then ![102]
        .fn = .TRUE
    endif
EndFunction
!---------------------------------------------------------------------
!Function:
!   Check if character is a punc char
!Params:
!   char  (s,1) [in] - character to test
!Returns:
!   .TRUE  (true) if so, 0 if not
!Globals:
!Notes:
!   if fn'isdigit(c$) then ...
!---------------------------------------------------------------------
Function fn'ispunc(char as s1:inputonly) as BOOLEAN
    if fn'isalpha(char) = 0 and fn'isdigit(char) = 0 and char # " " then 
        .fn = .TRUE
    endif
EndFunction
![102]
!---------------------------------------------------------------------
!Function:
!   Check if character is a hex digit
!Params:
!   char  (s,1) [in] - character to test
!Returns:
!   .TRUE  (true) if so, 0 if not
!Globals:
!Notes:
!   if fn'ishex(c$) then ...
!---------------------------------------------------------------------
Function fn'ishex(char as s1:inputonly) as BOOLEAN
    if instr(1,"0123456789ABCDEFabcdef",char) then
        .fn = .TRUE
    endif
EndFunction
!---------------------------------------------------------------------
!Function:
!   Check if entire string is made of only digits
!Params:
!   s  (s0) [in] - string to test
!Returns:
!   .TRUE  (true) if so, 0 if any non digits
!Globals:
!Notes:
!   Ignores trailing nulls
!   Empty field "" considered numeric
!   if fn'all'digits(s$) then ...
!   
!---------------------------------------------------------------------
Function fn'all'digits(s as s0:inputonly) as BOOLEAN
    map1 locals
        map2 i,f
        map2 n,f
    s = strip(s)
    n = len(s)
    .fn = .TRUE      ! assume true until proven false
    for i = 1 to n 
        if not fn'isdigit(s[i;1]) then
            .fn = .FALSE
            exit
        endif
    next i
EndFunction
!---------------------------------------------------------------------
!Function:
!   Check if entire string is made of only hex digits
!Params:
!   s  (s0) [in] - string to test
!Returns:
!   .TRUE  (true) if so, 0 if any non digits
!Globals:
!Notes:
!   Ignores trailing nulls
!   if fn'all'hex(s$) then ...
!   
!---------------------------------------------------------------------
Function fn'all'hex(s as s0:inputonly) as BOOLEAN
    map1 locals
        map2 i,f
        map2 n,f
    s = strip(s)
    n = len(s)
    .fn = .TRUE      ! assume true until proven false
    for i = 1 to n 
        if not fn'ishex(s[i;1]) then
            .fn = .FALSE
            exit
        endif
    next i
EndFunction
!---------------------------------------------------------------------
!Function:
!   Return a code indicating char type:
!Params:
!   char  (s,1) [in] - character to test
!Returns:
!   1=Alpha upper, 2=alpha lower, 3=digit, 4=punc, 5=space
!Globals:
!Notes:
!   on fn'chartype(c$) call alpha'upper,alpha'lower,digit,punc,space
!---------------------------------------------------------------------
Function fn'chartype(char as s1:inputonly) as BOOLEAN
    if fn'isalpha(char) then
        if char <= 'Z' then
            .fn = 1
        else
            .fn = 2
        endif
        Exitfunction
    endif
    if fn'isdigit(char) then
        .fn = 3
        Exitfunction
    endif
    if char = " " then
        .fn = 5
    else
        .fn = 4
    endif
EndFunction
!---------------------------------------------------------------------
!Function:
!   Check if string is a date in one of these formats:
!       #{#}/#{#}{/##{##}}   separator can be - or /
!       #{#}-aaa{-##{##}}       "       "
!Params:
!   s  (str) [in] - string to test
!   flags (num) [in] - optional ISDTF_ flags to adjust matching rules
!                      if 0, entire field must match; else flags can
!                      relax rule to ignore leading or trailing junk
!Returns:
!   .TRUE (-1) if so
!Globals:
!Notes:
!   Ignores leading/trailing spaces; uses REGEX
!   if fn'isdate(s) then ...
!---------------------------------------------------------------------
Function fn'isdate(s as s100:inputonly, flags as T_BITFLAGS16:inputonly) as BOOLEAN
    map1 locals
        map2 i,f
        map2 n,f
        map2 pattern$,s,60,"\d{1,2}[-/](\d{1,2}|[A-Z]{3})([-/]\d{2,4})??"
    xcall TRIM, s
    if flags and ISDTF_IGN_LEADING then       ! if ignoring leading junk
        pattern$ = "(^|\s)" + pattern$        ! pattern must start with start of field or blank
    else
        pattern$ = "^" + pattern$             ! else pattern must match start of field
    endif
    if flags and ISDTF_IGN_TRAILING then      ! if ignoring trailing junk
        pattern$ += "($|\s)"                  ! pattern must end with blank or end of field
    else 
        pattern$ += "$"                       ! else it must match end of field
    endif
    ! since it's a bit difficult to combine the two date formats (assuming that in the
    ! dd-mon-yr version the -yr is mandatory and slashes are not an option
    if instr(1,s,pattern$,PCRE_CASELESS) then  
        debug.print "fn'isdate("+s+") .true " + pattern$
        .fn = .TRUE
    elseif flags and ISDTF_999999 then          ! allow simple string of 6 digits
        if flags and ISDTF_IGN_LEADING then
            s = s[-7,-1]
        elseif flags and ISDTF_IGN_TRAILING then
            s = s[1,7]
        endif
        xcall TRIM,s
        if len(s)=6 and fn'all'digits(s) then
            debug.print "fn'isdate("+s+") .true (######)"
            .fn = .TRUE
        endif
    endif
    if not .fn then
        debug.print "fn'isdate("+s+") .false"
    endif    
EndFunction
!---------------------------------------------------------------------
!Function:
!   Check if string is a time field in format:
!       #{#}:##{:##}{ AM/PM}
!Params:
!   s  (str) [in] - string to test
!   flags (num) [in] - optional ISDTF_ flags to adjust matching rules
!                      if 0, entire field must match; else flags can
!                      relax rule to ignore leading or trailing junk
!Returns:
!   .TRUE (-1) if so
!Globals:
!Notes:
!   Ignores leading/trailing spaces; uses REGEX
!   Decision based on pattern only - doesn't consider whether the
!      values themselves are valid (i.e. allows 00-xyz-0001, 88/77/5555)
!   
!   if fn'isdate(s) then ...
!---------------------------------------------------------------------
Function fn'istime(s as s100:inputonly, flags as T_BITFLAGS16:inputonly) as BOOLEAN
    map1 locals
        map2 i,f
        map2 n,f
        map2 pattern$,s,50,"\d{1,2}:\d\d(:\d\d)?? ??([AP]M)??"
    xcall TRIM, s
    if flags and ISDTF_IGN_LEADING then       ! if ignoring leading junk
        pattern$ = "(^|\s)" + pattern$        ! pattern must start with start of field or blank
    else
        pattern$ = "^" + pattern$             ! else pattern must match start of field
    endif
    if flags and ISDTF_IGN_TRAILING then      ! if ignoring trailing junk
        pattern$ += "($|\s)"                  ! pattern must end with blank or end of field
    else 
        pattern$ += "$"                       ! else it must match end of field
    endif
    if instr(1,s,pattern$,PCRE_CASELESS) then  ! hh:mm{:ss}{ am/pm} variations
        debug.print "fn'istime("+s+") .true"
        .fn = .TRUE
    else
        debug.print "fn'istime("+s+") .false"
    endif    
EndFunction
!------------------------------------------------------------------
! Return .TRUE if the string contains only numeric chars and formatting
!   Check if string is a numeric field consisting only of digits, 
!   and numeric formatting chars.
!Params:
!   s  (str) [in] - string to test
!   purenumeric (BOOLEAN) [in] - (default .FALSE) If .TRUE, accept
!                            only digits, LDF decimal pt, minus sign, spaces [104]
!Returns:
!   .TRUE (-1) if so
!Globals:
!Notes:
!   Ignores leading/trailing spaces
!   if fn'isnumfld(s) then ...
!------------------------------------------------------------------
Function fn'isnumfld(s as s100:inputonly, &
                    purenumeric=.FALSE as BOOLEAN:inputonly) as BOOLEAN
    map1 locals
        map2 i,BOOLEAN
        map2 c,s,1
        map2 x,f
        map2 y,f
        map2 lenstring,f
        map2 deci,i,1
        map2 tsp,i,1
        map2 digcnt,b,1
        map2 dolflag,BOOLEAN            ! we've seen a dollar
        map2 dashflag,BOOLEAN           ! we've seen a dash
        map2 pctflag,BOOLEAN            ! we've seen a %
        map2 status,f
    map1 ldf,ST_GTLANG   
    static map1 s'lang'decimal,s,1
    static map1 s'lang'thousands,s,1
    if s'lang'decimal = "" then
        xcall GTLANG, status, ldf
        s'lang'decimal = ldf.DECIMAL
        s'lang'thousands = ldf.THOUSANDS
    endif
    if (s="") Exitfunction          ! empty string is not numeric!
    lenstring = len(s)
    xcall TRIM, s                   ! [105]
    for i = 1 to lenstring
        c = s[i;1]
        if c # " " then
            if fn'isdigit(c) = 0 then
                switch c        ! [132]- apply rules to the non-digits
                case "$"
                    if purenumeric exitfunction   ! [104]
                    if i=2 and dashflag then      ! [162] handle -$ at start
                        dolflag = .TRUE
                        exit
                    endif
                    if (i#1 and i#lenstring) or dolflag Exitfunction  ![162]
                    dolflag = .TRUE
                    exit
                case "-"
                    if i=2 and dolflag then      ! [162] handle $- at start
                        dashflag = .TRUE
                        exit
                    endif
                    if (i#1 and i#lenstring) or dashflag Exitfunction
                    dashflag = .TRUE
                    exit
                case "."                        ! we're assuming these are the only two
                case ","                        ! possibilities for lang'decimal & lang'thousands
                    if c = s'lang'decimal then
                        if deci > 0 Exitfunction        ! only one decimal pt allowed
                        deci = i
                    elseif purenumeric then             ! [104]
                        exitfunction                    ! [104]
                    else                                ! thousands separator
                        if tsp > 0 then
                            if (i-tsp) # 4 Exitfunction
                        else
                            if digcnt > 3 Exitfunction
                        endif
                        tsp = i
                    endif
                    exit
                case "("
                    if purenumeric or (i # 1) Exitfunction  ! [104]
                    exit
                case ")"
                    if purenumeric or (i # lenstring) Exitfunction  ! [104]
                    exit
                case "%"                            ! must be first or last
                    if (i#1 and i#lenstring) or pctflag or purenumeric then ! [104]
                        Exitfunction
                    else
                        pctflag = .TRUE
                    endif
                    exit
                default
                    Exitfunction        ! non-digit, non formatting char
                endswitch       ! 
            else
                digcnt = digcnt + 1
            endif
        endif
    next i
    .fn = .TRUE
$EXIT:                      ! [105]
    debug.print "fn'is'num: field=["+s+"] ...",ifelse$(.fn,".true",".false")
EndFunction
++endif
