This is the ninth part of the SQLxD series. For your convenience you can find other parts in the table of contents in Part 1 – XML Transformation

As the last predicate we implement LIKE operator. It is not that simple so the implementation is pretty long. By the way, did you know that there is LIKE operator available in C#? You just need to use some internal VB implementation.

However, SQLxD implementation looks as follows:

using System;
using System.Text;
using System.Text.RegularExpressions;
using Model;
using QueryLogic.Expressions.RowExpressions;

namespace QueryLogic.Predicates.Simple
{
    public class LikePredicate : IPredicate
    {
        private string _csharpRegex;

        public LikePredicate(IRowExpression left, IRowExpression right, char escapeCharacter)
        {
            Left = left;
            Right = right;
            EscapeCharacter = escapeCharacter;
        }

        public char EscapeCharacter { get; private set; }
        public IRowExpression Left { get; private set; }
        public IRowExpression Right { get; private set; }

        public bool KeepRow(Row row)
        {
            Cell leftCell = Left.Calculate(row);
            Cell rightCell = Right.Calculate(row);

            if (leftCell == null || rightCell == null)
            {
                return false;
            }

            string leftValue = leftCell.Value;
            string sqlRegex = rightCell.Value;

            if (_csharpRegex == null)
            {
                _csharpRegex = TranslateSqlRegexToCSharp(sqlRegex);
            }

            try
            {
                bool result = Regex.IsMatch(leftValue, _csharpRegex);
                return result;
            }
            catch (ArgumentException)
            {
                return false;
            }
        }

        protected bool Equals(LikePredicate other)
        {
            return EscapeCharacter == other.EscapeCharacter && Equals(Left, other.Left) &&
                   Equals(Right, other.Right);
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != GetType()) return false;
            return Equals((LikePredicate)obj);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                int hashCode = EscapeCharacter.GetHashCode();
                hashCode = (hashCode * 397) ^ (Left != null ? Left.GetHashCode() : 0);
                hashCode = (hashCode * 397) ^ (Right != null ? Right.GetHashCode() : 0);
                return hashCode;
            }
        }

        private string TranslateSqlRegexToCSharp(string sqlRegex)
        {
            var result = new StringBuilder("^");
            var state = new AlgorithmState();

            foreach (char character in sqlRegex)
            {
                result.Append(HandleCharacter(character, state));
            }

            return result + "$";
        }

        private string HandleCharacter(char character, AlgorithmState state)
        {
            return state.IsInGroup()
                ? HandleCharacterInGroup(character, state)
                : HandleCharacterOutOfGroup(character, state);
        }

        private string HandleCharacterInGroup(char character, AlgorithmState state)
        {
            if (state.IsBeforeFirstCharacterInGroup())
            {
                return HandleFirstCharacterInGroup(character, state);
            }

            return state.IsAfterEscapeCharacter()
                ? HandleEscapedCharacterInGroup(character, state)
                : HandleUnescapedCharacterInGroup(character, state);
        }

        private string HandleUnescapedCharacterInGroup(char character, AlgorithmState state)
        {
            state.NormalCharacter();
            if (character == EscapeCharacter)
            {
                state.EscapeCharacter();
                return "";
            }
            if (character == '\\')
            {
                return @"\\";
            }
            if (character == ']')
            {
                state.ExitGroup();
            }

            return string.Format("{0}", character);
        }

        private string HandleEscapedCharacterInGroup(char character, AlgorithmState state)
        {
            state.NormalCharacter();

            if (character == '^')
            {
                return @"\^";
            }
            if (character == '\\')
            {
                return @"\\";
            }
            if (character == ']')
            {
                return @"\]";
            }

            return string.Format("{0}", character);
        }

        private string HandleFirstCharacterInGroup(char character, AlgorithmState state)
        {
            if (character == EscapeCharacter)
            {
                state.EscapeCharacter();
                return "";
            }
            if (character == '\\')
            {
                state.NormalCharacter();
                return @"\\";
            }

            state.NormalCharacter();
            return string.Format("{0}", character);
        }

        private string HandleCharacterOutOfGroup(char character, AlgorithmState state)
        {
            return state.IsAfterEscapeCharacter()
                ? HandleEscapedCharacterOutOfGroup(character, state)
                : HandleUnescapedCharacterOutOfGroup(character, state);
        }

        private string HandleEscapedCharacterOutOfGroup(char character, AlgorithmState state)
        {
            state.NormalCharacter();

            if (character == '\\')
            {
                return @"[\\]";
            }
            return string.Format("[{0}]", character);
        }

        private string HandleUnescapedCharacterOutOfGroup(char character, AlgorithmState state)
        {
            if (character == EscapeCharacter)
            {
                state.EscapeCharacter();
                return "";
            }
            if (character == '_')
            {
                state.NormalCharacter();
                return ".";
            }
            if (character == '%')
            {
                state.NormalCharacter();
                return ".*";
            }
            if (character == '[')
            {
                state.NormalCharacter();
                state.EnterGroup();
                return "[";
            }
            if (character == '\\')
            {
                return @"[\\]";
            }

            state.NormalCharacter();
            return string.Format("[{0}]", character);
        }
    }

    internal class AlgorithmState
    {
        private States _state;

        public bool IsInGroup()
        {
            return _state == States.InGroupAfterFirstCharacter ||
                   _state == States.InGroupAfterFirstCharacterAfterEscapeCharacter ||
                   _state == States.InGroupBeforeFirstCharacter;
        }

        public void EscapeCharacter()
        {
            _state = IsInGroup()
                ? States.InGroupAfterFirstCharacterAfterEscapeCharacter
                : States.OutOfGroupAfterEscapeCharacter;
        }

        public bool IsAfterEscapeCharacter()
        {
            return _state == States.InGroupAfterFirstCharacterAfterEscapeCharacter ||
                   _state == States.OutOfGroupAfterEscapeCharacter;
        }

        public void EnterGroup()
        {
            _state = States.InGroupBeforeFirstCharacter;
        }

        public bool IsBeforeFirstCharacterInGroup()
        {
            return _state == States.InGroupBeforeFirstCharacter;
        }

        public void NormalCharacter()
        {
            if (_state == States.OutOfGroup || _state == States.OutOfGroupAfterEscapeCharacter)
            {
                _state = States.OutOfGroup;
                return;
            }

            _state = States.InGroupAfterFirstCharacter;
        }

        public void ExitGroup()
        {
            _state = States.OutOfGroup;
        }

        private enum States
        {
            OutOfGroup,
            OutOfGroupAfterEscapeCharacter,
            InGroupBeforeFirstCharacter,
            InGroupAfterFirstCharacter,
            InGroupAfterFirstCharacterAfterEscapeCharacter
        }
    }
}

Since there are multiple edge cases with escaping characters, it is important to test the code. In the next part we will unit test all operators.