Search code examples
phppostal-code

United Kingdom (GB) postal code validation without regex


I have tried several regexes and still some valid postal codes sometimes get rejected.

Searching the internet, Wikipedia and SO, I could only find regex validation solutions.

Is there a validation method which does not use regex? In any language, I guess it would be easy to port.

I supose the easiest would be to compare against a postal code database, yet that would need to be maintained and updated periodically from a reliable source.

Edit: To help future visitors and keep you from posting any more regexes, here's a regex which I have tested (as of 2013-04-24) to work for all postal codes in Code Point (see @Mikkel Løkke's answer):

//PHP PCRE (it was on Wikipedia, it isn't there anymore; I might have modified it, don't remember).
$strPostalCode=preg_replace("/[\s]/", "", $strPostalCode);
$bValid=preg_match("/^(GIR 0AA)|(((A[BL]|B[ABDHLNRSTX]?|C[ABFHMORTVW]|D[ADEGHLNTY]|E[HNX]?|F[KY]|G[LUY]?|H[ADGPRSUX]|I[GMPV]|JE|K[ATWY]|L[ADELNSU]?|M[EKL]?|N[EGNPRW]?|O[LX]|P[AEHLOR]|R[GHM]|S[AEGKLMNOPRSTY]?|T[ADFNQRSW]|UB|W[ADFNRSV]|YO|ZE)[1-9]?[0-9]|((E|N|NW|SE|SW|W)1|EC[1-4]|WC[12])[A-HJKMNPR-Y]|(SW|W)([2-9]|[1-9][0-9])|EC[1-9][0-9])[0-9][ABD-HJLNP-UW-Z]{2})$/i", $strPostalCode);

Solution

  • I'm writing this answer based on the wiki page.

    When checking on the validation part, it seems that there are 6 type of formats (A = letter and 9 = digit):

    AA9A 9AA                       AA9A9AA                   AA9A9AA
    A9A 9AA     Removing space     A9A9AA       order it     AA999AA
    A9 9AA    ------------------>  A99AA     ------------->  AA99AA
    A99 9AA                        A999AA                    A9A9AA
    AA9 9AA                        AA99AA                    A999AA
    AA99 9AA                       AA999AA                   A99AA
    

    As we can see, the length may vary from 5 to 7 and we have to take in account some special cases if we want to.

    So the function we are coding has to do the following:

    1. Remove spaces and convert to uppercase (or lower case).
    2. Check if the input is an exception, if it is it should return valid
    3. Check if the input's length is 4 < length < 8.
    4. Check if it's a valid postcode.

    The last part is tricky, but we will split it in 3 sections by length for some overview:

    1. Length = 7: AA9A9AA and AA999AA
    2. Length = 6: AA99AA, A9A9AA and A999AA
    3. Length = 5: A99AA

    For this we will be using a switch(). From now on it's just a matter of checking character by character if it's a letter or a number on the right place.

    So let's take a look at our PHP implementation:

    function check_uk_postcode($string){
        // Start config
        $valid_return_value = 'valid';
        $invalid_return_value = 'invalid';
        $exceptions = array('BS981TL', 'BX11LT', 'BX21LB', 'BX32BB', 'BX55AT', 'CF101BH', 'CF991NA', 'DE993GG', 'DH981BT', 'DH991NS', 'E161XL', 'E202AQ', 'E202BB', 'E202ST', 'E203BS', 'E203EL', 'E203ET', 'E203HB', 'E203HY', 'E981SN', 'E981ST', 'E981TT', 'EC2N2DB', 'EC4Y0HQ', 'EH991SP', 'G581SB', 'GIR0AA', 'IV212LR', 'L304GB', 'LS981FD', 'N19GU', 'N811ER', 'NG801EH', 'NG801LH', 'NG801RH', 'NG801TH', 'SE18UJ', 'SN381NW', 'SW1A0AA', 'SW1A0PW', 'SW1A1AA', 'SW1A2AA', 'SW1P3EU', 'SW1W0DT', 'TW89GS', 'W1A1AA', 'W1D4FA', 'W1N4DJ');
        // Add Overseas territories ?
        array_push($exceptions, 'AI-2640', 'ASCN1ZZ', 'STHL1ZZ', 'TDCU1ZZ', 'BBND1ZZ', 'BIQQ1ZZ', 'FIQQ1ZZ', 'GX111AA', 'PCRN1ZZ', 'SIQQ1ZZ', 'TKCA1ZZ');
        // End config
    
    
        $string = strtoupper(preg_replace('/\s/', '', $string)); // Remove the spaces and convert to uppercase.
        $exceptions = array_flip($exceptions);
        if(isset($exceptions[$string])){return $valid_return_value;} // Check for valid exception
        $length = strlen($string);
        if($length < 5 || $length > 7){return $invalid_return_value;} // Check for invalid length
        $letters = array_flip(range('A', 'Z')); // An array of letters as keys
        $numbers = array_flip(range(0, 9)); // An array of numbers as keys
    
        switch($length){
            case 7:
                if(!isset($letters[$string[0]], $letters[$string[1]], $numbers[$string[2]], $numbers[$string[4]], $letters[$string[5]], $letters[$string[6]])){break;}
                if(isset($letters[$string[3]]) || isset($numbers[$string[3]])){
                    return $valid_return_value;
                }
            break;
            case 6:
                if(!isset($letters[$string[0]], $numbers[$string[3]], $letters[$string[4]], $letters[$string[5]])){break;}
                if(isset($letters[$string[1]], $numbers[$string[2]]) || isset($numbers[$string[1]], $letters[$string[2]]) || isset($numbers[$string[1]], $numbers[$string[2]])){
                    return $valid_return_value;
                }
            break;
            case 5:
                if(isset($letters[$string[0]], $numbers[$string[1]], $numbers[$string[2]], $letters[$string[3]], $letters[$string[4]])){
                    return $valid_return_value;
                }
            break;
        }
    
        return $invalid_return_value;
    }
    

    Note that I've not added British Forces Post Office and non-geographic codes.

    Usage:

    echo check_uk_postcode('AE3A 6AR').'<br>'; // valid
    echo check_uk_postcode('Z9 9BA').'<br>'; // valid
    echo check_uk_postcode('AE3A6AR').'<br>'; // valid
    echo check_uk_postcode('EE34      6FR').'<br>'; // valid
    echo check_uk_postcode('A23A 7AR').'<br>'; // invalid
    echo check_uk_postcode('A23A   7AR').'<br>'; // invalid
    echo check_uk_postcode('WA3334E').'<br>'; // invalid
    echo check_uk_postcode('A2 AAR').'<br>'; // invalid