Functional Weave
Code in Python

validation.uk-postcode@2.0.0

README.md

3,000 bytes · view raw

# validation.uk-postcode

Returns a result object rather than a boolean because the caller almost always
wants both answers at once: whether to accept the value, and what to store.
Storing what the user typed means "sw1a1aa" and "SW1A 1AA" are two different
rows for one address; storing `normalised` means they are one. Splitting outward
from inward is the difference between "can we deliver here" and "which delivery
office is this", and callers that only want the yes or no have isUkPostcode /
is_uk_postcode.

Malformed input returns valid=false with nulls rather than throwing, so
`result.normalised ?? typed` is always safe and a bad address never becomes an
exception in the middle of a form submission.

FORMAT: the outward code is one of six shapes - A9, A99, A9A, AA9, AA99, AA9A -
and the inward code is always 9AA. The letter restrictions are real rules, not
decoration, and they are what rejects plausible-looking strings: Q, V and X
never start an area; I, J and Z are never the second letter of a two-letter
area; the third position of the A9A form is restricted to ABCDEFGHJKPSTUW; the
fourth position of the AA9A form to ABEHMNPRVWXY; and the final two letters
exclude C, I, K, M, O and V, which are the ones most easily misread by a person
or a sorting machine.

GIR 0AA is accepted as a special case. It was issued to Girobank in 1968, fits
none of the six shapes, and is still valid on an address label; a validator that
rejects it is wrong in a way that is very hard for the affected user to argue
with.

NORMALISATION: every ASCII space is stripped and exactly one is reinserted
before the final three characters. People put the space in the wrong place far
more often than they leave it out, so its original position carries no
information worth preserving. Only the ASCII space is stripped - a hyphen or a
non-breaking space makes the value invalid rather than being cleaned up, because
those mean the value came from somewhere unexpected.

A WELL-FORMED POSTCODE IS NOT A REAL ADDRESS. This checks the shape, not
existence: "AA1 1AA" passes every rule above and has never been issued. Only the
Royal Mail Postcode Address File (PAF), or an address lookup service built on
it, can tell you a postcode exists and what is at it. Use this to catch typing
errors before the lookup, not instead of it.

OUT OF SCOPE: BFPO numbers for British Forces addresses, the overseas territory
codes (Gibraltar's GX11 1AA, the Falklands' FIQQ 1ZZ, Ascension's ASCN 1ZZ and
the rest) and Crown dependency codes are not special-cased; the ones that happen
to fit the six shapes pass, and the ones that do not are rejected. If you accept
forces or territory mail, test for those prefixes before calling this.

2.0.0 renames the function from `ukPostcode` to `validateUkPostcode` (`validate_uk_postcode` in Python and
Rust), so every validator that returns a result record is `validateX` and
every one that returns a bool is `isX`. Nothing else changed; 1.0.0 stays
published under the old name.