Search code examples
pythonregexpython-3.x2d-games

Validating user input with regex


I have very limited experience with regular expressions so I'm hoping someone can help me out.

I'm making a Python 3 game that has a rectangular grid as the board. I'm trying to make a way for users to enter several board coordinates at once in the following form into a string called coords:

(x1, y1), (x2, y2), (x3, y3), ... (xn, yn)

I want the output to be a list of tuples called cells in a similar form:

[(x1, y1), (x2, y2), (x3, y3), ... (xn, yn)]

So essentially I want to mimic how tuples can be written in Python code.

Right now I am using:

cells = [tuple(coords[i.find('(') + 1: i.rfind(')')].split(',')) for i in coords.split()]

which produces the desired result for inputs in the form (1,2) (3,4) (5,6), with no spaces inside the inputted tuples and spaces between the tuples. However, this breaks for inputs not following exactly that form, and it does nothing to check for valid inputs. For every x- and y-value in the tuples in cells, I need to validate that:

  1. type(y-value) == type(x-value) == int
  2. 0 <= y-value < game_board.height
  3. 0 <= x-value < game_board.width

Ideally, if the users enters multiple valid coordinates and some invalid ones, the valid tuples would be added to cells and the user would be given a message like "The following coordinates were invalid: (x1, y1), ...".

I know I could do this all with a mess of loops and flow control, but is there a more Pythonic way to accomplish this with regex?

Edit: spelling


Solution

  • Regex is used to test the overall structure - the rest is done w/o regex:

    inp = "(3, 4), (a, 7), (-3, 3), (3, 3)"
    
    def MaybeInt(n):
        """Returns an int if possible, else the original value."""
        try:
            return int(n)
        except:
            return n
    
    def inX(n):
        """True if inside X of board."""
        return 0<=n<5
    
    def inY(n):
        """True if inside Y of board."""
        return 0<=n<5
    
    def structOk(t):
        import re
        return re.match(r'^\s*([^,]+,[^,]+\)\s*(?:,\s*\([^,]+,[^,]+\))*)\s*$',t)
    
    def validate(t):
        t = t.replace(" ","")
        if not structOk(t):
            raise ValueError("Structually unsound - try again: "+t)
    
        k = [ x.lstrip("(") for x in t.replace(" ","").rstrip(")").split("),")]
        tups = [ tuple(map(MaybeInt,tu.split(","))) for tu in k]
    
        # our "decider" for whats valid/invalid which is used to divide the tuples
        # into our valid/invalid return list's
        test = lambda q: all(isinstance(num,int) for num in q) and inX(q[0]) and inY(q[1])
    
    
        rv = [[],[]]  # prepare and append by our decider test
        for k in tups:
            # True == 1, False == 0
            rv[1-test(k)].append(k)
    
        return rv
    
    valid, invalid = validate(inp)
    
    print(valid)
    print(invalid)
    

    Output:

    [(3, 4), (3, 3)]    # valid
    [('a', 7), (-3, 3)] # invalid
    

    See https://regex101.com/r/OrHGaR/1 for the regex and detailed explanation.

    Short description: Its looking for ( ... , ...) with ... not being , -you can refine it by using f.e. [1234567890a-zA-Z] instead of [^,] but then its going to ValueError sooner then later.