Regular expressions are probably one of the most under-pursued yet powerful and useful Computer Science skills. As a programmer, in common day-to-day tasks like text–search, replace and input validation I find regular expression to be very efficient. Almost all modern languages and operating systems support text processing with regex and having them in my toolbox makes my workflow more productive.

Recently, for a side-project I wanted to validate an IP address(IPv4) input by the user. I decided to validate it with regular expressions and quickly came up with a basic pattern for the happy flow–

^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{0,3}$

As you can see this expression gives false positives for invalid IP addresses like 999.999.999.999. Looks like validating an IP address with a regular expression isn’t as easy as expected. Let’s break it down into smaller sub-problems.

To write the correct pattern we first need to list down all the valid cases for an IP address. All IPv4 addresses are divided into 4 groups separated with three dots and each of these groups follow the same rules. For an IP address to qualify as validated, each group should be validated by a pattern which considers the following cases:

With this picture it is clear that we need to put some kind of if–else logic to validate our input. In a programming language this is a trivial task, split the input by dots and validate each group with if–else condtions. But I decided to stick with the regular expression approach and continue writing the match pattern.

Pipe Operator

The pipe operator in a regular expression has the same behaviour as in programming languages, it does the logical OR of the operands and returns any of the matching strings. Have a look at the example below to understand it better.

let pattern = "(desk|lap)tops"

let regex = Regex(with: pattern)

let inputString = "desktops and laptops and not palmtops"

regex.findMatch(inputString)

// Returns: desktops, laptops 

For IP addresses, in a group, we have to either match one digit, two digits or three digits and additionally cover all the valid cases mentioned in the figure above. And then repeat it four times to validate the entire input. For one group the regex pattern looks like the one shown below:

Okay, if you are thinking this is long just remember that its just for one single group, copy-paste it three more times and check the length. In situations like these when a pattern is repeating we can tell regex to match exactly a particular number of occurences with the help of {}. Lets assume that we call the group pattern as shown above as “part”. Our final regex will look like this:

In code, by making the “part” as a variable, the pattern can look exactly like the one shown in figure, let’s see how it looks in Swift:

func isValidIPAddress(_ ip: String) -> Bool {
    let part = "[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]"

    let pattern = "^\(part)\\.{3}\(part)$"

    return ip.range(of: pattern, options: .regularExpression) != nil
}

If we had chosen to match each group with conditional statements we would’ve filled our code with if–else blocks and that would have increased the size of this function by about 4–5 times. With regular expressions our code looks short and clean. Though at first the long patterns make it a little difficult to understand but as you get to know more about regex these patterns start to make a lot of sense. This is one of the many great super powers of regex and like most of the skills in Computer Science, though they are not mandatory but using them will definitely give you a step-up.

Thanks for reading, please let me know if you have any feedback.