fozzybear
fozzybear

Reputation: 147

How to check for defined arbitrary numbers in bash, passed as value, string or reference?

I was looking for an extended checking method covering various valid numbers, passed as value, string or var reference to a method, but most solutions resort to basic cases like plain int or float numbers at most, or are dedicated to formatted date strings (which is not scope of my requirements).

So I'm posting a new question for a solution, which verifies, whether a passed argument is a common decimal/int, floating-point (delimited by either . or ,, hex (beginning with 0x, containing [0-9a-fA-F]), octal (beginning with 0), scientific notation/exponential (in the form 52e-14 or -2*10^8), or a custom base number (as <base>#n with base range from 2 to 64, allowing number digits [0-9a-fA-F]).

Upvotes: -1

Views: 70

Answers (1)

fozzybear
fozzybear

Reputation: 147

Here's my solution, including extensive testing cases, which verifies, whether a passed argument is a common decimal/int, floating-point (delimited by either . or ,, hex (beginning with 0x, containing [0-9a-fA-F]), octal (beginning with 0), scientific notation/exponential (in the form 52e-14 or -2*10^8), or a custom base number (as #n with base range from 2 to 64, allowing number digits [0-9a-fA-F]).

The elapsed time for 1000 iterations is about 6-7x slower than the fastest isint_Case() method from HERE, but covers and combines many more potentially encountered types of numbers or number strings, as described in GNU.org - Shell arithmetics.

The method numTest(), uses case + extended pattern matching (requiring shopt -s extglob) and returns 0 on success, and 1 on test failures. If required, the commented-out printf statements can pass the results to stdout, e. g. for successive result capturing into a var (which should work for just the values, due to the redirection of informative output to /dev/tty). Also added is a commented-out built-in RegEx-based solution (which is ~4s slower à 1000):

#! /bin/bash

shopt -s extglob

# Function accepts int (decimal), float, scientific (exponential), octal and hex numbers passed directly as values, expandable vars or var name references
numTest() {

    # Check for and link val to var name reference 
    if declare -p "$1" &>/dev/null; then 
        local -n input="$1" #&>/dev/null
    else
        local var="$1"
        local -n input=var
    fi
    
    ## CASE PATTERN CHECK SOLUTION (~18s for 1000)
    local isNum
    case ${input#[-+]} in
        # Octal, hex, decimal / scientific/exponential, as parsable by awk '{ print +$1, +$2 }' <<<'12345678e-6 -323*10^34'
        @(+([0-9])|0[xX]+([0-9aAbBcCdDeEfF])|+([0-9])?(@([eE]|'^'|'*10^')?([+-])+([0-9]))) ) 
            # Uncomment to print succeeded test values
            #printf '%s' "IS INT: " >/dev/tty
            #printf '%s\n' "$input"
            return 0 ;;
        # Explicit base 2 - 64
        +([0-9])\#+([0-9aAbBcCdDeEfF]) ) 
            local -i base=${input%%#*}
            if [[ $base -ge 2 && $base -le 64 ]]; then
                # Uncomment to print succeeded test values
                #printf '%s' "IS INT: " >/dev/tty
                #printf '%s\n' "$input"
                return 0
            fi ;;
        # Float / scientific/exponential, as parsable by awk '{ print +$1, +$2 }' <<<'0.0314159e2 -323*10^34'
        @(*([0-9])[.,]+([0-9])?(@([eE]|'^'|'*10^')?([+-])+([0-9])))|'NaN' ) 
            # Uncomment to print succeeded test values
            #printf '%s' "IS FLOAT: " >/dev/tty
            #printf '%s\n' "$input"
            return 0
            ;; 
    esac
    
    # ## REGEX CHECK SOLUTION (~22s, ~4s slower for 1000)
    # if [[ $input =~ ^[+-]?([0-9]+|0[xX][0-9aAbBcCdDeEfF]+|[0-9]+(([eE]|'^'|'*10^')[+-]?[0-9]+)?)$ ]]; then
        ## printf '%s' "IS INT: " >/dev/tty
        ## printf '%s\n' "$input"
        # return 0
    # elif [[ $input =~ ^[+-]?[0-9]*[.,][0-9]+(([eE]|'^'|'*10^')[+-]?[0-9]+)?$ || $input == 'NaN' ]]; then 
        ## printf '%s' "IS FLOAT: " >/dev/tty
        ## printf '%s\n' "$input"
        # return 0
    # fi
    
    # Uncomment to print failed test values
    #printf '%s' "TEST_FAIL: " >/dev/tty
    #printf '%s\n' "$input"
    return 1
}

# Set to 1 to print test section captions
declare print=

## Declare test vars and array of test values

declare -i int_min
(( int_min=(1<<63) ))
declare -i int_max=$((int_min - 1))

## Success test var candidates

# Success test vars octal 

declare -i convertedOctal=073
declare -i convertedOctal2=-073
declare -i convertedOctal3=+073

# Success test vars hex

declare -ai successTestsHex=( x1450 0x9532 0xA0b32 0x0 -0x0 -0X0 +0x0 +0X0 +0x08842 +0X08842 -0x05431 -0X05431 )
declare -a successTestsHexStr=( 'x1450' '0x9532' '0xA0b32' '0x0' '-0x0' '-0X0' '+0x0' '+0X0' '+0x08842' '+0X08842' '-0x05431' '-0X05431' )

# "success" - parsed as null at var assignment -> 0
declare -ai successTestsPseudoHex=( -x0x0 -x0x0 +x0x0 )

# Auto-converted ints to hex
declare -ai successTestsConvertedOctal=( $convertedOctal $convertedOctal2 $convertedOctal3 )

# Success test vars int 

declare -ai successTestsInt=( 0 -0 +0 $int_max -$int_max +$int_max $int_min -$int_min +$int_min 1 -1 +1 9015 -9015 +9015 )

# Exponents (only possible as string)

declare -a successTestsIntStr=( '0' '-0' '+0' "$int_max" "-$int_max" "+$int_max" "$int_min" )
declare -a successTestsIntBaseStr=( '2#0' '2#1' '16#aF0' '16#AF0' '32#a' '32#A' '33#a' '33#A' '64#f' '64#F' '10#100' )
declare -a successTestsOctalStr=( '073' '-073' '+073' )
declare -a successTestsSciStr=( '0e0' '0E0' '087e-32' '087E-32' '-087e32' '-087E32' '-22048e-16' '-22048E-16' '+100e050' '+100E050' '+7e-25' '+7E-25' '+000e+00' '+000E+00' '-853e1' '-853E1' '-323^34' '-1^-3' '+166^8' '+89^-16' '-323*10^34' '-1*10^-3' '+166*10^8' '+89*10^-16' )

# Success test vars float 

declare -a successTestsFloatStr=( 'NaN' 0.0 +0.0 0,0 -0.0 -0,0 1.34 +1.34 1,34 -1.34 -1,34 .494 ,494 +,494 -.494 -,494 '571.0014' '571,0014' '-571.0014' '-571,0014' )
declare -a successTestsSciFloatStr=( '13.62e06' '13.62E06' '-13.62e06' '-13.62E06' '+13.62e06' '+13.62E06' '-3.23^34' '-0.1^-3' '+16.6^8' '+8.9^-16' '-3,23*10^34' '-0,1*10^-3' '+16,6*10^8' '+8,9*10^-16' )

## Fail tests 

# Fail test vars misc 

declare -a failTestsMiscStr=( '+' '-' ' ' '_' 'E' 'e' 'a' 'A' 'x' 'X' '.' '-.' ',' '-,' )

declare empty=''

# Fail test vars hex 

declare -a failTestsHexStr=( 'x0x0' '-x0x0' '+x0x0' '0x01x' '-0x01x' '+0x42x' '0x' 'x0' )
declare failHexStr="x0x0"
declare failHexStr1="-x0x0"
declare failHexStr2="+x0x0"

# Fail test vars int 

declare -a failTestsIntStr=( "-$int_min" "+$int_min" '#1' '2_' 'i3x' ' 1 ' '22 ' ' 333' ' -1 ' '-02 ' ' -033 ' '- 0444' '+0-535' '+-5350' '-+5350' '--5350' '++5350' '0-' '0+' '0ate234' '1993-' '78844355+' )
declare -a failTestsIntBaseStr=( '0#0' '1#1' '0#1' '2#-1' '65#0' '65#a' '2#g' '2#G' )

# Fail test vars float 

declare -a failTestsFloatStr=( 'gdXm0.18#' 'gdXm0,18#' '.0-' '.0+' '7.01-' '238.446+' '0.' '0,' '.0.' ',0,' 'df-93.87#' 'df-93,87#' ' -2.057,' ' 09.002 ' '-09.002 ' '+09,002 ' '1.01 ' '-1,01 ' ' +2.02' ' .2,02' ' -33.66 ' '-33,66 ' ' -5 .4 ' '-5 ,4' ' -0. 06' '-0, 06 ' 'e13.62e06' '13E.06' '-13.62Ee06' '-13.62eE06' '+13.62ee06' '+13.62EE06' )

## Run tests and measure elapsed time

declare start=$EPOCHREALTIME

[[ $print ]] && echo -e "\nBEGIN SUCCESS TESTS DIRECT NUMBER INPUT" 
for (( i=0; i<100; i++)); do

# Auto-converted octal -> decimal
numTest 0160
numTest -0160
numTest +0160
# Auto-converted (0x) hex -> decimal
numTest 0x3932
numTest 0xaF3e2
numTest -0x3932
numTest +0x3932

numTest 0
numTest -0
numTest +0
numTest $int_max 
numTest -$int_max 
numTest +$int_max 
numTest $int_min 
numTest 94316667
numTest -94316667
numTest +94316667

numTest 0e0
numTest 0E0
numTest 087e-32
numTest 087E-32
numTest -087e32
numTest -087E32
numTest -22048e-16
numTest -22048E-16
numTest +100e050
numTest +100E050
numTest +7e-25
numTest +7E-25
numTest +000e+00
numTest +000E+00
numTest -853e1
numTest -853E1

numTest '0'
numTest '-0'
numTest '+0'
numTest "$int_max"
numTest "-$int_max"
numTest "+$int_max" 
numTest "$int_min"
numTest '99'
numTest '-2377'
numTest '+356'

numTest NaN
numTest 0.0
numTest -0.0
numTest +0.0
numTest 0,0
numTest -0,0
numTest +0,0
numTest .3930
numTest -.3930
numTest +.3930
numTest ,3930
numTest -,3930
numTest +,3930

numTest 261087.0532
numTest -261087.0532
numTest +261087.0532
numTest 261087,0532
numTest -261087,0532
numTest +261087,0532

# Auto-converted hex 0x

[[ $print ]] && echo -e "\nBEGIN SUCCESS TESTS HEX"

_IFS="$IFS"; IFS=$'\n'
for param in "${successTestsHex[@]}"; do
    numTest param
done
for param in "${successTestsPseudoHex[@]}"; do
    numTest param
done
[[ $print ]] && echo -e "\nBEGIN SUCCESS TESTS CONVERTED INT HEX"
for param in "${successTestsConvertedOctal[@]}"; do
    numTest param
done
[[ $print ]] && echo -e "\nBEGIN SUCCESS TESTS INT"
for param in "${successTestsInt[@]}"; do
    numTest param
done
[[ $print ]] && echo -e "\nBEGIN SUCCESS TESTS INT STR"
for param in "${successTestsIntStr[@]}"; do
    numTest param
done
[[ $print ]] && echo -e "\nBEGIN SUCCESS TESTS INT BASE STR"
for param in "${successTestsIntBaseStr[@]}"; do
    numTest param
done
[[ $print ]] && echo -e "\nBEGIN SUCCESS TESTS OCTAL STR"
for param in "${successTestsOctalStr[@]}"; do
    numTest param
done
[[ $print ]] && echo -e "\nBEGIN SUCCESS TESTS SCI INT STR"
for param in "${successTestsSciStr[@]}"; do
    numTest param
done
[[ $print ]] && echo -e "\nBEGIN SUCCESS TESTS FLOAT"
for param in "${successTestsFloatStr[@]}"; do
    numTest param
done
[[ $print ]] && echo -e "\nBEGIN SUCCESS TESTS SCI FLOAT STR"
for param in "${successTestsSciFloatStr[@]}"; do
    numTest param
done
[[ $print ]] && echo -e "\nBEGIN FAIL TESTS MISC"
numTest empty
for param in "${failTestsMiscStr[@]}"; do
    numTest param
done
[[ $print ]] && echo -e "\nBEGIN FAIL TESTS HEX"
numTest $failHexStr
numTest failHexStr
numTest $failHexStr1
numTest failHexStr1
numTest $failHexStr2
numTest failHexStr2
for param in "${failTestsHexStr[@]}"; do
    numTest param
done
[[ $print ]] && echo -e "\nBEGIN FAIL TESTS INT"
for param in "${failTestsIntStr[@]}"; do
    numTest param
done
[[ $print ]] && echo -e "\nBEGIN FAIL TESTS INT BASE STR"
for param in "${failTestsIntBaseStr[@]}"; do
    numTest param
done
[[ $print ]] && echo -e "\nBEGIN FAIL TESTS FLOAT"
for param in "${failTestsFloatStr[@]}"; do
    numTest param
done
IFS="$_IFS"

[[ $print ]] && echo -e "\nBEGIN FAIL TESTS DIRECT NUMBER INPUT"

numTest
numTest ''
numTest '+'
numTest '+'
numTest '-'
numTest ' '
numTest '_'
numTest 'E'
numTest 'e'
numTest 'a'
numTest 'A'
numTest 'x'
numTest 'X'

numTest '0x'
numTest 'x0'
numTest 0x
numTest x0

numTest 0-
numTest 0+
numTest '0-'
numTest '0+'
numTest -$int_min 
numTest +$int_min
numTest "-$int_min"
numTest "+$int_min"
numTest 0ate234
numTest '1993-'
numTest 1993-
numTest '78844355+'
numTest 78844355+

numTest .0-
numTest .0+
numTest 7.01-
numTest 238.446+

done 

declare stop=$EPOCHREALTIME
declare startMsStr="${start##*.}"; "${startMsStr%%[!0]*}" 2>/dev/null 
startMsStr="${startMsStr#*$_}"
declare stopMsStr="${stop##*.}"; "${stopMsStr%%[!0]*}" 2>/dev/null 
stopMsStr="${stopMsStr#*$_}"
declare -i elapsedSeconds=$(( (${stop//.} - ${start//.}) / 1000000 ))
#declare -i elapsedMs=$( [[ $stopMsStr -gt $startMsStr ]] && echo $(( ( stopMsStr - startMsStr ) / 1000 )) || echo $(( ( startMsStr - stopMsStr ) / 1000 )) )
declare -i elapsedMs
if [[ $stopMsStr -gt $startMsStr ]]; then
    elapsedMs=$(( ( stopMsStr - startMsStr ) / 1000 )) 
else
    elapsedMs=$(( ( startMsStr - stopMsStr ) / 1000 ))
fi

echo -e "ELAPSED TIME: $elapsedSeconds.${elapsedMs}s"

shopt -u extglob

The provided tests should cover most checked success and fail cases, applying both value-passing and reference-passing.

There's also fail cases to failTestsHexStr() for wrong hex STR values, for which at INT var assignment time pseudo-'success' occurs and successTestsPseudoHex(), demonstrating pseudo-success result at var assignment time, due to preliminary fail-to-null -> 0 evaluation.

A small glitch occurs for input of _, when printing the results, where not the value, but the ref name is printed instead, but fail reporting is Ok.

Upvotes: 0

Related Questions