RegEx for matching UK Postcodes

前端 未结 30 2301
广开言路
广开言路 2020-11-22 01:38

I\'m after a regex that will validate a full complex UK postcode only within an input string. All of the uncommon postcode forms must be covered as well as the usual. For in

相关标签:
30条回答
  • 2020-11-22 01:59

    Have a look at the python code on this page:

    http://www.brunningonline.net/simon/blog/archives/001292.html

    I've got some postcode parsing to do. The requirement is pretty simple; I have to parse a postcode into an outcode and (optional) incode. The good new is that I don't have to perform any validation - I just have to chop up what I've been provided with in a vaguely intelligent manner. I can't assume much about my import in terms of formatting, i.e. case and embedded spaces. But this isn't the bad news; the bad news is that I have to do it all in RPG. :-(

    Nevertheless, I threw a little Python function together to clarify my thinking.

    I've used it to process postcodes for me.

    0 讨论(0)
  • 2020-11-22 02:00

    There is no such thing as a comprehensive UK postcode regular expression that is capable of validating a postcode. You can check that a postcode is in the correct format using a regular expression; not that it actually exists.

    Postcodes are arbitrarily complex and constantly changing. For instance, the outcode W1 does not, and may never, have every number between 1 and 99, for every postcode area.

    You can't expect what is there currently to be true forever. As an example, in 1990, the Post Office decided that Aberdeen was getting a bit crowded. They added a 0 to the end of AB1-5 making it AB10-50 and then created a number of postcodes in between these.

    Whenever a new street is build a new postcode is created. It's part of the process for obtaining permission to build; local authorities are obliged to keep this updated with the Post Office (not that they all do).

    Furthermore, as noted by a number of other users, there's the special postcodes such as Girobank, GIR 0AA, and the one for letters to Santa, SAN TA1 - you probably don't want to post anything there but it doesn't appear to be covered by any other answer.

    Then, there's the BFPO postcodes, which are now changing to a more standard format. Both formats are going to be valid. Lastly, there's the overseas territories source Wikipedia.

    +----------+----------------------------------------------+
    | Postcode |                   Location                   |
    +----------+----------------------------------------------+
    | AI-2640  | Anguilla                                     |
    | ASCN 1ZZ | Ascension Island                             |
    | STHL 1ZZ | Saint Helena                                 |
    | TDCU 1ZZ | Tristan da Cunha                             |
    | BBND 1ZZ | British Indian Ocean Territory               |
    | BIQQ 1ZZ | British Antarctic Territory                  |
    | FIQQ 1ZZ | Falkland Islands                             |
    | GX11 1AA | Gibraltar                                    |
    | PCRN 1ZZ | Pitcairn Islands                             |
    | SIQQ 1ZZ | South Georgia and the South Sandwich Islands |
    | TKCA 1ZZ | Turks and Caicos Islands                     |
    +----------+----------------------------------------------+

    Next, you have to take into account that the UK "exported" its postcode system to many places in the world. Anything that validates a "UK" postcode will also validate the postcodes of a number of other countries.

    If you want to validate a UK postcode the safest way to do it is to use a look-up of current postcodes. There are a number of options:

    • Ordnance Survey releases Code-Point Open under an open data licence. It'll be very slightly behind the times but it's free. This will (probably - I can't remember) not include Northern Irish data as the Ordnance Survey has no remit there. Mapping in Northern Ireland is conducted by the Ordnance Survey of Northern Ireland and they have their, separate, paid-for, Pointer product. You could use this and append the few that aren't covered fairly easily.

    • Royal Mail releases the Postcode Address File (PAF), this includes BFPO which I'm not sure Code-Point Open does. It's updated regularly but costs money (and they can be downright mean about it sometimes). PAF includes the full address rather than just postcodes and comes with its own Programmers Guide. The Open Data User Group (ODUG) is currently lobbying to have PAF released for free, here's a description of their position.

    • Lastly, there's AddressBase. This is a collaboration between Ordnance Survey, Local Authorities, Royal Mail and a matching company to create a definitive directory of all information about all UK addresses (they've been fairly successful as well). It's paid-for but if you're working with a Local Authority, government department, or government service it's free for them to use. There's a lot more information than just postcodes included.

    0 讨论(0)
  • 2020-11-22 02:01

    First half of postcode Valid formats

    • [A-Z][A-Z][0-9][A-Z]
    • [A-Z][A-Z][0-9][0-9]
    • [A-Z][0-9][0-9]
    • [A-Z][A-Z][0-9]
    • [A-Z][A-Z][A-Z]
    • [A-Z][0-9][A-Z]
    • [A-Z][0-9]

    Exceptions
    Position 1 - QVX not used
    Position 2 - IJZ not used except in GIR 0AA
    Position 3 - AEHMNPRTVXY only used
    Position 4 - ABEHMNPRVWXY

    Second half of postcode

    • [0-9][A-Z][A-Z]

    Exceptions
    Position 2+3 - CIKMOV not used

    Remember not all possible codes are used, so this list is a necessary but not sufficent condition for a valid code. It might be easier to just match against a list of all valid codes?

    0 讨论(0)
  • 2020-11-22 02:03

    I recently posted an answer to this question on UK postcodes for the R language. I discovered that the UK Government's regex pattern is incorrect and fails to properly validate some postcodes. Unfortunately, many of the answers here are based on this incorrect pattern.

    I'll outline some of these issues below and provide a revised regular expression that actually works.


    Note

    My answer (and regular expressions in general):

    • Only validates postcode formats.
    • Does not ensure that a postcode legitimately exists.
      • For this, use an appropriate API! See Ben's answer for more info.

    If you don't care about the bad regex and just want to skip to the answer, scroll down to the Answer section.

    The Bad Regex

    The regular expressions in this section should not be used.

    This is the failing regex that the UK government has provided developers (not sure how long this link will be up, but you can see it in their Bulk Data Transfer documentation):

    ^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z]))))[0-9][A-Za-z]{2})$
    

    Problems

    Problem 1 - Copy/Paste

    See regex in use here.

    As many developers likely do, they copy/paste code (especially regular expressions) and paste them expecting them to work. While this is great in theory, it fails in this particular case because copy/pasting from this document actually changes one of the characters (a space) into a newline character as shown below:

    ^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z]))))
    [0-9][A-Za-z]{2})$
    

    The first thing most developers will do is just erase the newline without thinking twice. Now the regex won't match postcodes with spaces in them (other than the GIR 0AA postcode).

    To fix this issue, the newline character should be replaced with the space character:

    ^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z])))) [0-9][A-Za-z]{2})$
                                                                                                                                                         ^
    

    Problem 2 - Boundaries

    See regex in use here.

    ^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z])))) [0-9][A-Za-z]{2})$
    ^^                     ^ ^                                                                                                                                            ^^
    

    The postcode regex improperly anchors the regex. Anyone using this regex to validate postcodes might be surprised if a value like fooA11 1AA gets through. That's because they've anchored the start of the first option and the end of the second option (independently of one another), as pointed out in the regex above.

    What this means is that ^ (asserts position at start of the line) only works on the first option ([Gg][Ii][Rr] 0[Aa]{2}), so the second option will validate any strings that end in a postcode (regardless of what comes before).

    Similarly, the first option isn't anchored to the end of the line $, so GIR 0AAfoo is also accepted.

    ^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z]))))[0-9][A-Za-z]{2})$
    

    To fix this issue, both options should be wrapped in another group (or non-capturing group) and the anchors placed around that:

    ^(([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z])))) [0-9][A-Za-z]{2}))$
    ^^                                                                                                                                                                      ^^
    

    Problem 3 - Improper Character Set

    See regex in use here.

    ^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z])))) [0-9][A-Za-z]{2})$
                                                                                           ^^
    

    The regex is missing a - here to indicate a range of characters. As it stands, if a postcode is in the format ANA NAA (where A represents a letter and N represents a number), and it begins with anything other than A or Z, it will fail.

    That means it will match A1A 1AA and Z1A 1AA, but not B1A 1AA.

    To fix this issue, the character - should be placed between the A and Z in the respective character set:

    ^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([A-Za-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z])))) [0-9][A-Za-z]{2})$
                                                                                            ^
    

    Problem 4 - Wrong Optional Character Set

    See regex in use here.

    ^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z])))) [0-9][A-Za-z]{2})$
                                                                                                                                            ^
    

    I swear they didn't even test this thing before publicizing it on the web. They made the wrong character set optional. They made [0-9] option in the fourth sub-option of option 2 (group 9). This allows the regex to match incorrectly formatted postcodes like AAA 1AA.

    To fix this issue, make the next character class optional instead (and subsequently make the set [0-9] match exactly once):

    ^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9][A-Za-z]?)))) [0-9][A-Za-z]{2})$
                                                                                                                                                    ^
    

    Problem 5 - Performance

    Performance on this regex is extremely poor. First off, they placed the least likely pattern option to match GIR 0AA at the beginning. How many users will likely have this postcode versus any other postcode; probably never? This means every time the regex is used, it must exhaust this option first before proceeding to the next option. To see how performance is impacted check the number of steps the original regex took (35) against the same regex after having flipped the options (22).

    The second issue with performance is due to the way the entire regex is structured. There's no point backtracking over each option if one fails. The way the current regex is structured can greatly be simplified. I provide a fix for this in the Answer section.

    Problem 6 - Spaces

    See regex in use here

    This may not be considered a problem, per se, but it does raise concern for most developers. The spaces in the regex are not optional, which means the users inputting their postcodes must place a space in the postcode. This is an easy fix by simply adding ? after the spaces to render them optional. See the Answer section for a fix.


    Answer

    1. Fixing the UK Government's Regex

    Fixing all the issues outlined in the Problems section and simplifying the pattern yields the following, shorter, more concise pattern. We can also remove most of the groups since we're validating the postcode as a whole (not individual parts):

    See regex in use here

    ^([A-Za-z][A-Ha-hJ-Yj-y]?[0-9][A-Za-z0-9]? ?[0-9][A-Za-z]{2}|[Gg][Ii][Rr] ?0[Aa]{2})$
    

    This can further be shortened by removing all of the ranges from one of the cases (upper or lower case) and using a case-insensitive flag. Note: Some languages don't have one, so use the longer one above. Each language implements the case-insensitivity flag differently.

    See regex in use here.

    ^([A-Z][A-HJ-Y]?[0-9][A-Z0-9]? ?[0-9][A-Z]{2}|GIR ?0A{2})$
    

    Shorter again replacing [0-9] with \d (if your regex engine supports it):

    See regex in use here.

    ^([A-Z][A-HJ-Y]?\d[A-Z\d]? ?\d[A-Z]{2}|GIR ?0A{2})$
    

    2. Simplified Patterns

    Without ensuring specific alphabetic characters, the following can be used (keep in mind the simplifications from 1. Fixing the UK Government's Regex have also been applied here):

    See regex in use here.

    ^([A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}|GIR ?0A{2})$
    

    And even further if you don't care about the special case GIR 0AA:

    ^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$
    

    3. Complicated Patterns

    I would not suggest over-verification of a postcode as new Areas, Districts and Sub-districts may appear at any point in time. What I will suggest potentially doing, is added support for edge-cases. Some special cases exist and are outlined in this Wikipedia article.

    Here are complex regexes that include the subsections of 3. (3.1, 3.2, 3.3).

    In relation to the patterns in 1. Fixing the UK Government's Regex:

    See regex in use here

    ^(([A-Z][A-HJ-Y]?\d[A-Z\d]?|ASCN|STHL|TDCU|BBND|[BFS]IQQ|PCRN|TKCA) ?\d[A-Z]{2}|BFPO ?\d{1,4}|(KY\d|MSR|VG|AI)[ -]?\d{4}|[A-Z]{2} ?\d{2}|GE ?CX|GIR ?0A{2}|SAN ?TA1)$
    

    And in relation to 2. Simplified Patterns:

    See regex in use here

    ^(([A-Z]{1,2}\d[A-Z\d]?|ASCN|STHL|TDCU|BBND|[BFS]IQQ|PCRN|TKCA) ?\d[A-Z]{2}|BFPO ?\d{1,4}|(KY\d|MSR|VG|AI)[ -]?\d{4}|[A-Z]{2} ?\d{2}|GE ?CX|GIR ?0A{2}|SAN ?TA1)$
    

    3.1 British Overseas Territories

    The Wikipedia article currently states (some formats slightly simplified):

    • AI-1111: Anguila
    • ASCN 1ZZ: Ascension Island
    • STHL 1ZZ: Saint Helena
    • TDCU 1ZZ: Tristan da Cunha
    • BBND 1ZZ: British Indian Ocean Territory
    • BIQQ 1ZZ: British Antarctic Territory
    • FIQQ 1ZZ: Falkland Islands
    • GX11 1ZZ: Gibraltar
    • PCRN 1ZZ: Pitcairn Islands
    • SIQQ 1ZZ: South Georgia and the South Sandwich Islands
    • TKCA 1ZZ: Turks and Caicos Islands
    • BFPO 11: Akrotiri and Dhekelia
    • ZZ 11 & GE CX: Bermuda (according to this document)
    • KY1-1111: Cayman Islands (according to this document)
    • VG1111: British Virgin Islands (according to this document)
    • MSR 1111: Montserrat (according to this document)

    An all-encompassing regex to match only the British Overseas Territories might look like this:

    See regex in use here.

    ^((ASCN|STHL|TDCU|BBND|[BFS]IQQ|GX\d{2}|PCRN|TKCA) ?\d[A-Z]{2}|(KY\d|MSR|VG|AI)[ -]?\d{4}|(BFPO|[A-Z]{2}) ?\d{2}|GE ?CX)$
    

    3.2 British Forces Post Office

    Although they've been recently changed it to better align with the British postcode system to BF# (where # represents a number), they're considered optional alternative postcodes. These postcodes follow(ed) the format of BFPO, followed by 1-4 digits:

    See regex in use here

    ^BFPO ?\d{1,4}$
    

    3.3 Santa?

    There's another special case with Santa (as mentioned in other answers): SAN TA1 is a valid postcode. A regex for this is very simply:

    ^SAN ?TA1$
    
    0 讨论(0)
  • 2020-11-22 02:04

    The accepted answer reflects the rules given by Royal Mail, although there is a typo in the regex. This typo seems to have been in there on the gov.uk site as well (as it is in the XML archive page).

    In the format A9A 9AA the rules allow a P character in the third position, whilst the regex disallows this. The correct regex would be:

    (GIR 0AA)|((([A-Z-[QVX]][0-9][0-9]?)|(([A-Z-[QVX]][A-Z-[IJZ]][0-9][0-9]?)|(([A-Z-[QVX]][0-9][A-HJKPSTUW])|([A-Z-[QVX]][A-Z-[IJZ]][0-9][ABEHMNPRVWXY])))) [0-9][A-Z-[CIKMOV]]{2}) 
    

    Shortening this results in the following regex (which uses Perl/Ruby syntax):

    (GIR 0AA)|([A-PR-UWYZ](([0-9]([0-9A-HJKPSTUW])?)|([A-HK-Y][0-9]([0-9ABEHMNPRVWXY])?))\s?[0-9][ABD-HJLNP-UW-Z]{2})
    

    It also includes an optional space between the first and second block.

    0 讨论(0)
  • 2020-11-22 02:04

    I needed a version that would work in SAS with the PRXMATCH and related functions, so I came up with this:

    ^[A-PR-UWYZ](([A-HK-Y]?\d\d?)|(\d[A-HJKPSTUW])|([A-HK-Y]\d[ABEHMNPRV-Y]))\s?\d[ABD-HJLNP-UW-Z]{2}$
    

    Test cases and notes:

    /* 
    Notes
    The letters QVX are not used in the 1st position.
    The letters IJZ are not used in the second position.
    The only letters to appear in the third position are ABCDEFGHJKPSTUW when the structure starts with A9A.
    The only letters to appear in the fourth position are ABEHMNPRVWXY when the structure starts with AA9A.
    The final two letters do not use the letters CIKMOV, so as not to resemble digits or each other when hand-written.
    */
    
    /*
        Bits and pieces
        1st position (any):         [A-PR-UWYZ]         
        2nd position (if letter):   [A-HK-Y]
        3rd position (A1A format):  [A-HJKPSTUW]
        4th position (AA1A format): [ABEHMNPRV-Y]
        Last 2 positions:           [ABD-HJLNP-UW-Z]    
    */
    
    
    data example;
    infile cards truncover;
    input valid 1. postcode &$10. Notes &$100.;
    flag = prxmatch('/^[A-PR-UWYZ](([A-HK-Y]?\d\d?)|(\d[A-HJKPSTUW])|([A-HK-Y]\d[ABEHMNPRV-Y]))\s?\d[ABD-HJLNP-UW-Z]{2}$/',strip(postcode));
    cards;
    1  EC1A 1BB  Special case 1
    1  W1A 0AX   Special case 2
    1  M1 1AE    Standard format
    1  B33 8TH   Standard format
    1  CR2 6XH   Standard format
    1  DN55 1PT  Standard format
    0  QN55 1PT  Bad letter in 1st position
    0  DI55 1PT  Bad letter in 2nd position
    0  W1Z 0AX   Bad letter in 3rd position
    0  EC1Z 1BB  Bad letter in 4th position
    0  DN55 1CT  Bad letter in 2nd group
    0  A11A 1AA  Invalid digits in 1st group
    0  AA11A 1AA  1st group too long
    0  AA11 1AAA  2nd group too long
    0  AA11 1AAA  2nd group too long
    0  AAA 1AA   No digit in 1st group
    0  AA 1AA    No digit in 1st group
    0  A 1AA     No digit in 1st group
    0  1A 1AA    Missing letter in 1st group
    0  1 1AA     Missing letter in 1st group
    0  11 1AA    Missing letter in 1st group
    0  AA1 1A    Missing letter in 2nd group
    0  AA1 1     Missing letter in 2nd group
    ;
    run;
    
    0 讨论(0)
提交回复
热议问题