On a form that I was working on recently there is functionality to populate address details given a United Kingdom postcode. The postcode data is retrieved from AFD Postcode Anywhere, but unfortunately the addresses do not come back in a “human friendly” order. (It may be that the API can be configured to do this, but the documentation is not forthcoming in this regard. So, what we get for Postcode N8 7PT for example, is:
1 Campsbourne Road, LONDON
11 Campsbourne Road, LONDON
11a Campsbourne Road, LONDON
13 Campsbourne Road, LONDON
1a Campsbourne Road, LONDON
1b Campsbourne Road, LONDON
1c Campsbourne Road, LONDON
1d Campsbourne Road, LONDON
1e Campsbourne Road, LONDON
2 Campsbourne Road, LONDON
2b Campsbourne Road, LONDON
3 Campsbourne Road, LONDON
4 Campsbourne Road, LONDON
5 Campsbourne Road, LONDON
7 Campsbourne Road, LONDON
7b Campsbourne Road, LONDON
9 Campsbourne Road, LONDON
Cavendish Cars, 2a Campsbourne Road, LONDON
which is OK, but would be better as:
1 Campsbourne Road, LONDON
1a Campsbourne Road, LONDON
1b Campsbourne Road, LONDON
1c Campsbourne Road, LONDON
1d Campsbourne Road, LONDON
1e Campsbourne Road, LONDON
2 Campsbourne Road, LONDON
2b Campsbourne Road, LONDON
3 Campsbourne Road, LONDON
4 Campsbourne Road, LONDON
5 Campsbourne Road, LONDON
7 Campsbourne Road, LONDON
7b Campsbourne Road, LONDON
9 Campsbourne Road, LONDON
11 Campsbourne Road, LONDON
11a Campsbourne Road, LONDON
13 Campsbourne Road, LONDON
Cavendish Cars, 2a Campsbourne Road, LONDON
The requirement here is to get the first part of the address (which is most likely to be the house number) and then process it as an alphanumeric whole, so that 1a comes before 11. This is quite straight forward because there are many Alphanumeric Sorting algorithms for .NET available. In order to play nicely with Linq and process our data as strings we need a method that is strongly-typed for strings and implements an IComparer interface:
using System; using System.Collections.Generic; using System.Text; namespace Utils { public class AlphaNumericComparer : IComparer<string> { private enum ChunkType { Alphanumeric, Numeric }; private bool InChunk(char ch, char otherCh) { var type = ChunkType.Alphanumeric; if (char.IsDigit(otherCh)) { type = ChunkType.Numeric; } if ((type == ChunkType.Alphanumeric && char.IsDigit(ch)) || (type == ChunkType.Numeric && !char.IsDigit(ch))) { return false; } return true; } public int Compare(string x, string y) { if (x == null || y == null) { return 0; } int thisMarker = 0; int thatMarker = 0; while ((thisMarker < x.Length) || (thatMarker < y.Length)) { if (thisMarker >= x.Length) { return -1; } if (thatMarker >= y.Length) { return 1; } char thisCh = x[thisMarker]; char thatCh = y[thatMarker]; var thisChunk = new StringBuilder(); var thatChunk = new StringBuilder(); while ((thisMarker < x.Length) && (thisChunk.Length == 0 || InChunk(thisCh, thisChunk[0]))) { thisChunk.Append(thisCh); thisMarker++; if (thisMarker < x.Length) { thisCh = x[thisMarker]; } } while ((thatMarker < y.Length) && (thatChunk.Length == 0 || InChunk(thatCh, thatChunk[0]))) { thatChunk.Append(thatCh); thatMarker++; if (thatMarker < y.Length) { thatCh = y[thatMarker]; } } int result = 0; // If both chunks contain numeric characters, sort them numerically if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0])) { int thisNumericChunk = Convert.ToInt32(thisChunk.ToString()); int thatNumericChunk = Convert.ToInt32(thatChunk.ToString()); if (thisNumericChunk < thatNumericChunk) { result = -1; } if (thisNumericChunk > thatNumericChunk) { result = 1; } } else { result = String.Compare(thisChunk.ToString(), thatChunk.ToString(), StringComparison.OrdinalIgnoreCase); } if (result != 0) { return result; } } return 0; } } }
Having got our comparer method, all that we need to do now is get the data, extract the house number if present, call Linq.OrderBy using the comparer, and we’re done.
First the call to get the data:
public IEnumerable<IAddressResult> GetAddressesForPostcode(string postcode) { var results = new List<AddressResult>(); // get the basic service info var serviceCall = GetServiceUrl(); // add to it the data needed for the postcode lookup call serviceCall.Append("&Data=Address"); serviceCall.Append("&Task=Lookup"); serviceCall.Append("&Fields=List"); serviceCall.Append("&Lookup=" + postcode); try { var doc = XDocument.Load(serviceCall.ToString()); var root = doc.Element("AFDPostcodeEverywhere"); var result = root.Element("Result"); if (result != null && Int32.Parse(result.Value) > 0) { results = (from c in doc.Descendants("Item") select new AddressResult() { Name = c.Element("List").Value, Key = c.Element("Key").Value, Postcode = c.Element("Postcode").Value, Country = c.Element("CountryISO").Value, }).ToList<AddressResult>(); } else { XElement error = doc.Element("ErrorText"); if (error != null) throw new Exception(error.Value); } } catch (Exception ex) { // Log Error } return results; }
This method returns a collection of AddressResults, (basically street address plus postcode and country).
The Name property of these items can then be parsed to extract the house number:
private ListItem[] CreateOrderedListOfItems(IEnumerable<IAddressResult> addresses) { var addressResults = addresses as IList<IAddressResult> ?? addresses.ToList(); var addressLines = (from addressResult in addressResults select addressResult.Name.Replace(addressResult.Postcode, "").Trim() into addressLine let sortKey = addressLine.IndexOf(' ') != -1 ? addressLine.Substring(0, addressLine.IndexOf(' ')) : addressLine select new ListItem(addressLine, sortKey)).ToList(); return addressLines.OrderBy(i => i.Value, new AlphaNumericComparer()).ToArray(); }
All that this little Linq expression does is to identify whether the address contains a space character and if so extract it into the Value part of the list item that we are creating for our drop-down list of address lines. If no space is present, the whole address line will be set as the value. The whole address line is then added as the Text for the list item, as this is what we will put into the form edit fields on selection of a line from the drop-down list.
All that remains is to create a function to populate the drop-down list when a postcode has been input into the form and the “Go” button has been clicked:
protected void FindAddress() { // try and find the address by postcode var addresses = _postCodeLookup.GetAddressesForPostcode(PostcodeText).ToList(); if (addresses.Any()) { addressErrorPanel.Visible = false; plcFindAddressResults.Visible = true; drpSelectAddress.Items.Clear(); var items = CreateOrderedListOfItems(addresses); drpSelectAddress.Items.AddRange(items); drpSelectAddress.Items.Insert(0, new ListItem("Please select", "")); drpSelectAddress.AppendDataBoundItems = true; } else { addressErrorPanel.Visible = true; plcAddressEntry.Visible = true; //btnFindAddress.Visible = false; ltlAddressError.Text = "Postcode lookup error"; } }
And there you are. It could be said that it’s quite a lot of code for something very minor (was the original order so terrible?), but hey, it’s all code.