Technical Article

SQLCalcEngine - evaluate numeric expression

,

SQLCalcEngine - evaluate numeric expression similar to the VB eval function.

The basic concept is to pass a string containing a numeric expression such as ((2+3)*(4/9)) and receive a numeric value back.

The algorithm is based loosely on material found at http://www.arstdesign.com/articles/expression_evaluation.html, as well as material found in the book "Writing Compilers and Interpreters" by Ronald Mak.

The next enhancement will be to implement two-character operators, as well as support the common SQL functions in the expressions. Look for updates to the script over the near future.

--
-- SQLCalcEngine
--
IF EXISTS(SELECT name FROM sysobjects
      WHERE name = 'FN_Compute' AND type = 'FN')
DROP FUNCTION FN_Compute
GO

CREATE FUNCTION FN_Compute (
   @LParm NUMERIC(16,6),
   @RParm NUMERIC(16,6),
   @Op    CHAR)
   RETURNS NUMERIC (16,6)
AS
BEGIN
RETURN (CASE @Op
        WHEN '+' THEN @LParm + @RParm
        WHEN '-' THEN @LParm - @RParm
        WHEN '*' THEN @LParm * @RParm
        WHEN '/' THEN (CASE WHEN @RParm = 0 THEN NULL ELSE @LParm / @RParm END)
        ELSE NULL
        END)
END
GO
-- test and debug       SELECT dbo.FN_Compute (3,5,'*')
-- test and debug       SELECT dbo.FN_Compute (3,0,'?')
-- test and debug       SELECT dbo.FN_Compute (3,0,'/')
-- test and debug       SELECT dbo.FN_Compute (3,2,'/')

IF EXISTS(SELECT name FROM sysobjects
      WHERE name = 'FN_Precedence' AND type = 'FN')
DROP FUNCTION FN_Precedence
GO

CREATE FUNCTION FN_Precedence (
   @Op     CHAR)
   RETURNS NUMERIC
AS
BEGIN
   RETURN (CASE @Op
           WHEN '*' THEN 2
           WHEN '/' THEN 2
           WHEN '+' THEN 1
           WHEN '-' THEN 1
           ELSE 0
           END)
END
GO

IF EXISTS(SELECT name FROM sysobjects
      WHERE name = 'FN_Evaluate' AND type = 'FN')
DROP FUNCTION FN_Evaluate
GO

CREATE FUNCTION FN_Evaluate (
   @InExpression VARCHAR(8000))
   RETURNS NUMERIC (16,6)
AS
BEGIN
   DECLARE
      @OperandStack TABLE (GID     NUMERIC IDENTITY (1,1)  NOT NULL,
                           Operand VARCHAR(30))
   DECLARE
      @OperatorStack    VARCHAR(500),
      @Result           NUMERIC (16,6),

      -- for processing all expressions
      @ExprLen          smallint,
      @StrPtr           smallint,
      @testChar         varchar(1),
      -- for working with OperandStack
      @MaxOperandGID    NUMERIC,
      -- For working with operators and operands
      @thisOperator     CHAR,
      @thisOperand      VARCHAR(30),
      @LeftOperand      VARCHAR(30),
      @RightOperand     VARCHAR(30),
      -- For evaluating parenthetic subexpressions
      @ParenDepth       NUMERIC,
      @SubExpression    VARCHAR(8000)

   SET @ExprLen = LEN (@InExpression)
   SET @StrPtr = 1
   WHILE @StrPtr <= @ExprLen
   BEGIN
      SET @testChar = SUBSTRING(@InExpression, @StrPtr, 1)
      IF @testChar IN ('1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '.')
      BEGIN -- handle a numeric value
         SET @thisOperand = ''
         WHILE @TestChar IN ('1', '2', '3', '4', '5', '6', '7', '8', '9', '0','.')
         BEGIN
            SET @thisOperand = @thisOperand + @TestChar
            SET @StrPtr = @StrPtr + 1
            SET @TestChar = SUBSTRING(@InExpression, @StrPtr, 1)
         END
         INSERT INTO @OperandStack (Operand) VALUES (@thisOperand)
      END
      ELSE IF @testChar IN ('*', '/', '-', '+')
      BEGIN -- handle an operator
         WHILE (dbo.FN_StringStackIsEmpty (@OperatorStack) = 'F' AND
                dbo.FN_Precedence (dbo.FN_StringStackTop(@OperatorStack)) >
                dbo.FN_Precedence (@TestChar)
                )
         BEGIN
            SET @thisOperator = dbo.FN_StringStackTop(@OperatorStack)
            SET @OperatorStack = dbo.FN_StringStackPop(@OperatorStack)
            SELECT @MaxOperandGID = MAX(GID) FROM @OperandStack
            SELECT @RightOperand = Operand FROM @OperandStack WHERE GID = @MaxOperandGID
            DELETE FROM @OperandStack WHERE GID = @MaxOperandGID
            SELECT @MaxOperandGID = MAX(GID) FROM @OperandStack
            SELECT @LeftOperand = Operand FROM @OperandStack WHERE GID = @MaxOperandGID
            DELETE FROM @OperandStack WHERE GID = @MaxOperandGID
            INSERT INTO @OperandStack (Operand) VALUES (dbo.FN_Compute (@LeftOperand, @RightOperand, @thisOperator))
         END
         SET @OperatorStack = dbo.FN_StringStackPush(@OperatorStack, @TestChar)
         SET @StrPtr = @StrPtr + 1
      END
      ELSE IF @testChar = '('
      BEGIN -- Recursive call to this routine
         SET @ParenDepth = 1
         SET @SubExpression = ''
         -- Eat the LParen
         SET @StrPtr = @StrPtr + 1
         WHILE (@StrPtr <= @ExprLen AND
                @ParenDepth > 0)
         BEGIN
            SET @TestChar = SUBSTRING(@InExpression, @StrPtr, 1)
            IF @TestChar = '(' SET @ParenDepth = @ParenDepth + 1
            IF @TestChar = ')' SET @ParenDepth = @ParenDepth - 1
            IF @ParenDepth > 0 SET @SubExpression = @SubExpression + @TestChar
            SET @StrPtr = @StrPtr + 1
         END
         INSERT INTO @OperandStack (Operand) VALUES (dbo.FN_Evaluate (@SubExpression))
      END
   END
   WHILE (dbo.FN_StringStackIsEmpty (@OperatorStack) = 'F')
   BEGIN
      SET @thisOperator = dbo.FN_StringStackTop(@OperatorStack)
-- test and debug      SET @Result = '<' + @OperatorStack + '>'
-- test and debug      SET @Result = @thisOperator
      SET @OperatorStack = dbo.FN_StringStackPop(@OperatorStack)
      SELECT @MaxOperandGID = MAX(GID) FROM @OperandStack
      SELECT @RightOperand = Operand FROM @OperandStack WHERE GID = @MaxOperandGID
      DELETE FROM @OperandStack WHERE GID = @MaxOperandGID
      SELECT @MaxOperandGID = MAX(GID) FROM @OperandStack
      SELECT @LeftOperand = Operand FROM @OperandStack WHERE GID = @MaxOperandGID
      DELETE FROM @OperandStack WHERE GID = @MaxOperandGID
      INSERT INTO @OperandStack (Operand) VALUES (dbo.FN_Compute (@LeftOperand, @RightOperand, @thisOperator))
   END
   SELECT @Result = Operand FROM @OperandStack
-- test & debug   SELECT @Result = 'OperatorStack = ' + RTRIM(@OperatorStack)
-- test & debug   SELECT @Result = @Result + '    OperandStack = '
-- test & debug   SELECT @Result = COALESCE(@Result + ',', '') + RTRIM(Operand) FROM @OperandStack ORDER BY GID
   RETURN (@Result)
END
GO
-- test and debug       DECLARE @MyExpression VARCHAR(500)
-- test and debug       SET @MyExpression = '1+2'
-- test and debug       SELECT @MyExpression,dbo.FN_Evaluate (@MyExpression), 'Should equal 3'
-- test and debug       SET @MyExpression = '1+2+3'
-- test and debug       SELECT @MyExpression,dbo.FN_Evaluate (@MyExpression), 'Should equal 6'
-- test and debug       SET @MyExpression = '1+2+3*4.5'
-- test and debug       SELECT @MyExpression,dbo.FN_Evaluate (@MyExpression), 'Should equal 16.5'
-- test and debug       SET @MyExpression = '1+2+(3*4)'
-- test and debug       SELECT @MyExpression,dbo.FN_Evaluate (@MyExpression), 'Should equal 15'
-- test and debug       SET @MyExpression = '(1+2+(3*4))'
-- test and debug       SELECT @MyExpression,dbo.FN_Evaluate (@MyExpression), 'Should equal 15'
-- test and debug       SET @MyExpression = '(1+2)+((3*4)*2)'
-- test and debug       SELECT @MyExpression,dbo.FN_Evaluate (@MyExpression), 'Should equal 27'

IF EXISTS(SELECT name FROM sysobjects
      WHERE name = 'FN_ExpressionIsValid' AND type = 'FN')
DROP FUNCTION FN_ExpressionIsValid
GO

CREATE FUNCTION FN_ExpressionIsValid (
   @InExpression VARCHAR(8000))
   RETURNS VARCHAR(1)
AS
BEGIN
   DECLARE
      @OperandStack TABLE (GID     NUMERIC IDENTITY (1,1)  NOT NULL,
                           Operand VARCHAR(30))
   DECLARE
      @OperatorStack    VARCHAR(500),
      @IsValidInd       VARCHAR(1),
      @thisOperator     VARCHAR(1),

      -- for processing all expressions
      @ExprLen          SMALLINT,
      @StrPtr           SMALLINT,
      @testChar         VARCHAR(1)

   SET @IsValidInd = 'T'
   SET @ExprLen = LEN (@InExpression)
   SET @StrPtr = 1
   WHILE @StrPtr <= @ExprLen
     AND @IsValidInd = 'T'
   BEGIN
      SET @testChar = SUBSTRING(@InExpression, @StrPtr, 1)
      IF @testChar NOT IN ('1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '.',
                           '*', '/', '-', '+',
                           '(', ')') SET @IsValidInd = 'F'
      IF @IsValidInd = 'T'
      BEGIN
         IF @TestChar = '('
         BEGIN
            SET @OperatorStack = dbo.FN_StringStackPush(@OperatorStack, @TestChar)
         END
         ELSE IF @TestChar = ')'
         BEGIN
            IF dbo.FN_StringStackIsEmpty (@OperatorSTack) = 'T' SET @IsValidInd = 'F'
            SET @thisOperator  = dbo.FN_StringStackTop(@OperatorStack)
            SET @OperatorStack = dbo.FN_StringStackPop(@OperatorStack)
            IF @thisOperator != '(' SET @IsValidInd = 'F'
         END
      END
      SET @StrPtr = @StrPtr + 1
   END
   IF @IsValidInd = 'T'
   BEGIN
      IF dbo.FN_StringStackIsEmpty (@OperatorSTack) = 'F' SET @IsValidInd = 'F'
   END
   RETURN (@IsValidInd)
END
GO
-- test and debug       DECLARE @MyExpression VARCHAR(500)
-- test and debug       SET @MyExpression = '1+2'
-- test and debug       SELECT @MyExpression,dbo.FN_ExpressionIsValid (@MyExpression), 'Should equal T'
-- test and debug       SET @MyExpression = '1+2+3'
-- test and debug       SELECT @MyExpression,dbo.FN_ExpressionIsValid (@MyExpression), 'Should equal T'
-- test and debug       SET @MyExpression = '1+2+3*4.5'
-- test and debug       SELECT @MyExpression,dbo.FN_ExpressionIsValid (@MyExpression), 'Should equal T'
-- test and debug       SET @MyExpression = '1+2+(3*4)'
-- test and debug       SELECT @MyExpression,dbo.FN_ExpressionIsValid (@MyExpression), 'Should equal T'
-- test and debug       SET @MyExpression = '(1+2+(3*4))'
-- test and debug       SELECT @MyExpression,dbo.FN_ExpressionIsValid (@MyExpression), 'Should equal T'
-- test and debug       SET @MyExpression = '(1+2)+((3*4)*2)'
-- test and debug       SELECT @MyExpression,dbo.FN_ExpressionIsValid (@MyExpression), 'Should equal T'
-- test and debug       SET @MyExpression = '((1+2)+((3*4)*2)'
-- test and debug       SELECT @MyExpression,dbo.FN_ExpressionIsValid (@MyExpression), 'Should equal F'
-- test and debug       SET @MyExpression = '(1+2)+((3*4)*2))'
-- test and debug       SELECT @MyExpression,dbo.FN_ExpressionIsValid (@MyExpression), 'Should equal F'
-- test and debug       SET @MyExpression = '(1+2)+((3*4))*2)'
-- test and debug       SELECT @MyExpression,dbo.FN_ExpressionIsValid (@MyExpression), 'Should equal F'
-- test and debug       SET @MyExpression = '(1+2)+((3m4)*2))'
-- test and debug       SELECT @MyExpression,dbo.FN_ExpressionIsValid (@MyExpression), 'Should equal F'

Rate

You rated this post out of 5. Change rating

Share

Share

Rate

You rated this post out of 5. Change rating