A C# Postcode Struct with Parser
As discussed recently, I’ve been trying to knock together a class struct to represent a UK postcode, provide a means of parsing a string as potentially being a valid postcode (or optionally just an outer code), and split that postcode into “outer” and “inner” code. The result of my efforts is shown below
I say potentially because, as you’ll know if you’ve looked into this topic at all, it’s impossible to validate whether a string truly does represent a postcode without querying the (copyrighted and expensive-to-license) Postcode Address File. As a result, my solution is not very stringent – it is possible to persuade it to successfully parse an invalid postcode, but hopefully there should be no occasions when a valid postcode is rejected.
The code validates strings by checking for compliance with the standards defined in BS7666, followed by checking for BFPO postcodes, and finally for a handful of notable exceptions (e.g. Girobank). If you know of any valid postcodes that are rejected by this routine, do let me know.
Oh, and incidentally, this was the first time I’ve used Visual Studio Orcas in anger to develop anything meatier than a Hello World. Although I wasn’t exactly pushing the technological envelope, I found the IDE to be pretty fast and stable, considering it’s a beta 1.
Source available on GitHub at https://github.com/ianfnelson/Postcode
using System;
using System.Text.RegularExpressions;
namespace IanFNelson.Utilities
{
/// <summary>
/// Represents a United Kingdom postcode.
/// </summary>
///
/// For more details, see http://ianfnelson.com/archives/2007/05/29/postcodestruct
///
[Serializable]
public struct Postcode
{
private static string regexBS7666Outer =
"(?[A-PR-UWYZ]" +
"([0-9]{1,2}|([A-HK-Y]
[0-9]|[A-HK-Y]
[0-9]([0-9]|[ABEHMNPRV-Y]))|[0-9]
[A-HJKS-UW]))";
private static string regexBS7666Inner = "(?[0-9]
[ABD-HJLNP-UW-Z]{2})";
private static readonly string regexBS7666Full = regexBS7666Outer + regexBS7666Inner;
private static readonly string regexBS7666OuterStandAlone = string.Concat(regexBS7666Outer + "\\s*$");
private static string regexBfpoOuter = "(?BFPO)";
private static string regexBfpoInner = "(?[0-9]{1,3})";
private static readonly string regexBfpoFull = regexBfpoOuter + regexBfpoInner;
private static readonly string regexBfpoOuterStandalone = string.Concat(regexBfpoOuter + "\\s*$");
private static readonly string[,] exceptionsToTheRule =
{
{"GIR", "0AA"}, // Girobank
{"SAN", "TA1"}, // Santa Claus
{"ASCN", "1ZZ"}, // Ascension Island
{"BIQQ", "1ZZ"}, // British Antarctic Territory
{"BBND", "1ZZ"}, // British Indian Ocean Territory
{"FIQQ", "1ZZ"}, // Falkland Islands
{"PCRN", "1ZZ"}, // Pitcairn Islands
{"STHL", "1ZZ"}, // Saint Helena
{"SIQQ", "1ZZ"}, // South Georgia and the Sandwich Islands
{"TDCU", "1ZZ"}, // Tristan da Cunha
{"TKCA", "1ZZ"} // Turks and Caicos Islands
};
private string _inCode;
private string _outCode;
/// <summary>
/// Outer portion of the Postcode
/// </summary>
public string OutCode
{
get { return _outCode; }
private set { _outCode = value; }
}
/// <summary>
/// Inner portion of the Postcode
/// </summary>
public string InCode
{
get { return _inCode; }
private set { _inCode = value; }
}
/// <summary>
/// Parses a string as a Postcode.
/// </summary>
///String to be parsed
/// Postcode object
///
/// If the passed string cannot be parsed as a UK postcode
///
/// Using this overload, the inner code is not mandatory.
public static Postcode Parse(string s)
{
return Parse(s, false);
}
/// <summary>
/// Parses a string as a Postcode.
/// </summary>
///String to be parsed
///
/// Indicates that the string passed must include a valid inner code.
///
/// Postcode object
///
/// If the passed string cannot be parsed as a UK postcode
///
public static Postcode Parse(string s, bool incodeMandatory)
{
var p = new Postcode();
if (TryParse(s, out p, incodeMandatory))
return p;
throw new FormatException();
}
/// <summary>
/// Attempts to parse a string as a Postcode.
/// </summary>
///String to be parsed
///Postcode object
///
/// Boolean indicating whether the string was successfully parsed as a UK Postcode
///
/// Using this overload, the inner code is not mandatory.
public static bool TryParse(string s, out Postcode result)
{
return TryParse(s, out result, false);
}
/// <summary>
/// Attempts to parse a string as a Postcode.
/// </summary>
///String to be parsed
///Postcode object
///
/// Indicates that the string passed must include a valid inner code.
///
///
/// Boolean indicating whether the string was successfully parsed as a UK Postcode
///
public static bool TryParse(string s, out Postcode result, bool incodeMandatory)
{
// Set output to new Postcode
result = new Postcode();
// Copy the input before messing with it
var input = s;
// Guard clause - check for null or empty string
if (string.IsNullOrEmpty(input)) return false;
// uppercase input and strip undesirable characters
input = Regex.Replace(input.ToUpperInvariant(), "[^A-Z0-9]", string.Empty);
// guard clause - input is more than seven characters
if (input.Length > 7) return false;
#region BS7666 Matching
// Try to match full standard postcode
var fullMatch = Regex.Match(input, regexBS7666Full);
if (fullMatch.Success)
{
result.OutCode = fullMatch.Groups["outCode"].Value;
result.InCode = fullMatch.Groups["inCode"].Value;
return true;
}
// Try to match outer standard postcode only
var outerMatch = Regex.Match(input, regexBS7666OuterStandAlone);
if (outerMatch.Success)
{
if (incodeMandatory) return false;
result.OutCode = outerMatch.Groups["outCode"].Value;
return true;
}
#endregion
#region BFPO Matching
// Try to match full BFPO postcode
var bfpoFullMatch = Regex.Match(input, regexBfpoFull);
if (bfpoFullMatch.Success)
{
result.OutCode = bfpoFullMatch.Groups["outCode"].Value;
result.InCode = bfpoFullMatch.Groups["inCode"].Value;
return true;
}
// Try to match outer BFPO postcode
var bfpoOuterMatch = Regex.Match(input, regexBfpoOuterStandalone);
if (bfpoOuterMatch.Success)
{
if (incodeMandatory) return false;
result.OutCode = bfpoOuterMatch.Groups["outCode"].Value;
return true;
}
#endregion
#region Exceptions to the rule matching
// Loop through exceptions to the rule
for (var i = 0; i < exceptionsToTheRule.GetLength(0); i++)
{
// Check for a full match
if (input == string.Concat(exceptionsToTheRule[i, 0], exceptionsToTheRule[i, 1]))
{
result.OutCode = exceptionsToTheRule[i, 0];
result.InCode = exceptionsToTheRule[i, 1];
return true;
}
// Check for partial match only
if (input == exceptionsToTheRule[i, 0])
{
if (incodeMandatory) return false;
result.OutCode = exceptionsToTheRule[i, 0];
return true;
}
}
#endregion
return false;
}
/// <summary>
/// Returns a string representation of this postcode
/// </summary>
///
public override string ToString()
{
if (string.IsNullOrEmpty(InCode))
{
return OutCode;
}
else
{
return string.Concat(OutCode, " ", InCode);
}
}
}
}









Cheers Ian
Paul I’m trying to send a e-mail to the British Banking Association in London but as you’ve probably quested there requesting a post code. I’m trying to report a scam that was introduce to me via e-mail. Can you help me with a valid post code. Thanks for any assistance Sincerely Doug
,
Ian , here’s some unit tests for the postcode struct
using System;
using NUnit.Framework;
using IanFNelson.Utilities
// Unit test for postcode type based on BS7666 Adrress
// http://interim.cabinetoffice.gov.uk/govtalk/schemasstandards/e-gif/datastandards/address/postcode.aspx
namespace IanFNelson.Utilities.UnitTests
{
[TestFixture]
public class PostCodeTests
{
#region Test Invalid First letters Q,V,X
[Test]
[ExpectedException(typeof(FormatException))]
public void Parse_InvalidLetterQInFirstPosition_ThrowFormatException()
{
// Arrange
const string postcodeString = “QO7 5PQ”;
// Act
var actual = Postcode.Parse(postcodeString);
}
[Test]
[ExpectedException(typeof(FormatException))]
public void Parse_InvalidLetterVInFirstPosition_ThrowFormatException()
{
// Arrange
const string postcodeString = “VO7 5PQ”;
// Act
var actual = Postcode.Parse(postcodeString);
}
[Test]
[ExpectedException(typeof(FormatException))]
public void Parse_InvalidLetterXInFirstPosition_ThrowFormatException()
{
// Arrange
const string postcodeString = “XO7 5PQ”;
// Act
var actual = Postcode.Parse(postcodeString);
}
#endregion
#region Test Invalid Second letters I, J and Z
[Test]
[ExpectedException(typeof(FormatException))]
public void Parse_InvalidSecondLetterI_ThrowFormatException()
{
// Arrange
const string postcodeString = “PI7 5PQ”;
// Act
var actual = Postcode.Parse(postcodeString);
}
[Test]
[ExpectedException(typeof(FormatException))]
public void Parse_InvalidSecondLetterJ_ThrowFormatException()
{
// Arrange
const string postcodeString = “PJ7 5PQ”;
// Act
var actual = Postcode.Parse(postcodeString);
}
[Test]
[ExpectedException(typeof(FormatException))]
public void Parse_InvalidSecondLetterZ_ThrowFormatException()
{
// Arrange
const string postcodeString = “PZ7 5PQ”;
// Act
var actual = Postcode.Parse(postcodeString);
}
#endregion
#region OutCode tests
[Test]
public void Parse_ValidPartialPostCode_ReturnInputValueOutCode()
{
// Arrange
const string expected = “PO8″;
// Act
var actual = Postcode.Parse(expected);
// Assert
Assert.AreEqual(expected, actual.OutCode);
}
[Test]
public void Parse_InvalidPartialPostCode_OriginalValueNotReturned()
{
// Arrange
const string expected = “PZ7″;
// Act
var actual = Postcode.Parse(expected);
// Assert
Assert.AreNotEqual(expected, actual.OutCode);
}
#endregion
#region Valid data tests
[Test]
public void Parse_ValidPostCode_ReturnInputValue()
{
// Arrange
const string expected = “PO8 5PQ”;
// Act
var actual = Postcode.Parse(expected);
// Assert
Assert.AreEqual(expected, actual.ToString());
}
#endregion
}
}