Skip to content

Commit

Permalink
Significant restructuring for version 2
Browse files Browse the repository at this point in the history
Note changes to API as documented in README.md.
  • Loading branch information
threedaymonk committed Dec 14, 2014
1 parent ba71cd6 commit a37c0ee
Show file tree
Hide file tree
Showing 20 changed files with 737 additions and 473 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ ONSPD_URL=http://parlvid.mysociety.org/os/ONSPD_MAY_2014_csv.zip

.PHONY: test clean

test: test/data/postcodes.csv lib/uk_postcode/country/lookup.rb
test: test/data/postcodes.csv lib/uk_postcode/country_lookup.rb
rake

data/onspd.zip:
Expand All @@ -21,7 +21,7 @@ test/data/postcodes.csv: data/postcodes.csv
mkdir -p test/data
cp $< $@

lib/uk_postcode/country/lookup.rb: data/postcodes.csv
lib/uk_postcode/country_lookup.rb: data/postcodes.csv
ruby -I./lib tools/generate_country_lookup.rb $< > $@.tmp && mv $@.tmp $@

clean:
Expand Down
103 changes: 67 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,64 +11,75 @@ Features:
* Allows extraction of fields within postcode.
* Validated against 2.5 million postcodes in England, Wales, Scotland, Northern
Ireland, the Channel Islands, and the Isle of Man.
* Finds the country corresponding to a postcode, where possible.

## Usage

```ruby
require "uk_postcode"
```

Validate and extract sections of a full postcode:
Parse and extract sections of a full postcode:

```ruby
pc = UKPostcode.new("W1A 2AB")
pc.valid? #=> true
pc.full? #=> true
pc.outcode #=> "W1A"
pc.incode #=> "2AB"
pc.area #=> "W"
pc.district #=> "1A"
pc.sector #=> "2"
pc.unit #=> "AB"
pc = UKPostcode.parse("W1A 2AB")
pc.valid? # => true
pc.full? # => true
pc.outcode # => "W1A"
pc.incode # => "2AB"
pc.area # => "W"
pc.district # => "1A"
pc.sector # => "2"
pc.unit # => "AB"
```

Or of a partial postcode:

```ruby
pc = UKPostcode.new("W1A")
pc.valid? #=> true
pc.full? #=> false
pc.outcode #=> "W1A"
pc.incode #=> nil
pc.area #=> "W"
pc.district #=> "1A"
pc.sector #=> nil
pc.unit #=> nil
pc = UKPostcode.parse("W1A")
pc.valid? # => true
pc.full? # => false
pc.outcode # => "W1A"
pc.incode # => nil
pc.area # => "W"
pc.district # => "1A"
pc.sector # => nil
pc.unit # => nil
```

Normalise postcodes with `normalize` (or just `norm`):
Postcodes are converted to a normal or canonical form:

```ruby
UKPostcode.new("w1a1aa").normalize #=> "W1A 1AA"
pc = UKPostcode.parse("w1a1aa")
pc.valid? # => true
pc.area # => "W"
pc.district # => "1A"
pc.sector # => "1"
pc.unit # => "AA
pc.to_s # => "W1A 1AA"
```

Fix mistakes with IO/10:
And mistakes with I/1 and O/0 are corrected:

```ruby
pc = UKPostcode.new("WIA OAA")
pc.outcode #=> "W1A"
pc.incode #=> "0AA"
pc = UKPostcode.parse("WIA OAA")
pc.valid? # => true
pc.area # => "W"
pc.district # => "1A"
pc.sector # => "0"
pc.unit # => "AA
pc.to_s # => "W1A 0AA"
```

Find the country of a postcode or outcode (if possible: some outcodes span
Find the country of a full or partial postcode (if possible: some outcodes span
countries):

```ruby
UKPostcode.new("W1A 1AA").country #=> :england
UKPostcode.new("BT4").country #=> :northern_ireland
UKPostcode.new("CA6").country #=> :unknown
UKPostcode.new("CA6 5HS").country #=> :scotland
UKPostcode.new("CA6 5HT").country #=> :england
UKPostcode.parse("W1A 1AA").country # => :england
UKPostcode.parse("BT4").country # => :northern_ireland
UKPostcode.parse("CA6").country # => :unknown
UKPostcode.parse("CA6 5HS").country # => :scotland
UKPostcode.parse("CA6 5HT").country # => :england
```

The country returned for a postcode is derived from the [ONS Postcode
Expand All @@ -78,16 +89,36 @@ Directory][onspd] and might not always be correct in a border region:
> assigned to the area where the mean grid reference of all the addresses
> within the postcode falls.
## As a gem
Invalid postcodes:

```sh
$ gem install uk_postcode
```ruby
pc = UKPostcode.parse("Not Valid")
pc.valid? # => false
pc.full? # => false
pc.area # => nil
pc.to_s # => "Not valid"
pc.country # => :unknown
```

or in your `Gemfile`:
## For users of version 1.x

The interface has changed significantly, so code that worked with version 1.x
will not work with version 2.x without changes.

Specifically:

* Use `UKPostcode.parse(str)` where you previously used `UKPostcode.new(str)`.
* `parse` will return either a `GeographicPostcode`, a `GiroPostcode`, or an
`InvalidPostcode`.
* You may prefer to use `GeographicPostcode.parse` directly if you wish to
exclude `GIR 0AA` and invalid postcodes.

## As a gem

In your `Gemfile`:

```ruby
gem "uk_postcode"
gem "uk_postcode", "~> 2.0.0.alpha"
```

## Testing
Expand Down
132 changes: 13 additions & 119 deletions lib/uk_postcode.rb
Original file line number Diff line number Diff line change
@@ -1,125 +1,19 @@
require 'uk_postcode/country'

class UKPostcode
MATCH = /\A \s* (?:
( G[I1]R \s* [0O]AA ) # special postcode
|
( [A-PR-UWYZ01][A-Z01]? ) # area
( [0-9IO][0-9A-HJKMNPR-YIO]? ) # district
(?: \s*
( [0-9IO] ) # sector
( [ABD-HJLNPQ-Z10]{2} ) # unit
)?
) \s* \Z/x

attr_reader :raw

# Initialise a new UKPostcode instance from the given postcode string
#
def initialize(postcode_as_string)
@raw = postcode_as_string
end

# Return the country corresponding to a full or partial postcode
# Note that some outcodes (e.g. CA6) are shared between countries, in which
# case only a full postcode will return the actual country.
#
# Country is one of :england, :scotland, :wales, :northern_ireland,
# :channel_islands, or :isle_of_man.
#
def country
Country.new(self).country
end

# Returns true if the postcode is a valid full postcode (e.g. W1A 1AA) or outcode (e.g. W1A)
#
def valid?
!!outcode
end

# Returns true if the postcode is a valid full postcode (e.g. W1A 1AA)
#
def full?
!!(outcode && incode)
end

# The left-hand part of the postcode, e.g. W1A 1AA -> W1A
#
def outcode
area && district && [area, district].join
end

# The right-hand part of the postcode, e.g. W1A 1AA -> 1AA
#
def incode
sector && unit && [sector, unit].join
end

# The first part of the outcode, e.g. W1A 2AB -> W
#
def area
parts[0]
end

# The second part of the outcode, e.g. W1A 2AB -> 1A
#
def district
parts[1]
end

# The first part of the incode, e.g. W1A 2AB -> 2
#
def sector
parts[2]
end
require "uk_postcode/version"
require "uk_postcode/geographic_postcode"
require "uk_postcode/giro_postcode"
require "uk_postcode/invalid_postcode"

# The second part of the incode, e.g. W1A 2AB -> AB
#
def unit
parts[3]
end
module UKPostcode
module_function

# Render the postcode as a normalised string, i.e. in upper case and with spacing.
# Returns an empty string if the postcode is not valid.
# Attempt to parse the string str as a postcode. Returns an object
# representing the postcode, or an InvalidPostcode if the string cannot be
# parsed.
#
def norm
[outcode, incode].compact.join(" ")
end
alias_method :normalise, :norm
alias_method :normalize, :norm

alias_method :to_s, :raw
alias_method :to_str, :raw

def inspect(*args)
"<#{self.class.to_s} #{raw}>"
end

private
def parts
return @parts if @matched

@matched = true
matches = raw.upcase.match(MATCH) || []
if matches[1]
@parts = %w[ G IR 0 AA ]
else
a, b, c, d = (2..5).map{ |i| matches[i] }
if a =~ /^[A-Z][I1]$/
a = a[0, 1]
b = "1" + b
end
@parts = [letters(a), digits(b), digits(c), letters(d)]
def parse(str)
[GiroPostcode, GeographicPostcode, InvalidPostcode].each do |klass|
pc = klass.parse(str)
return pc if pc
end
end

def letters(s)
s && s.tr("10", "IO")
end

def digits(s)
s && s.tr("IO", "10")
end
end

require "uk_postcode/version"
17 changes: 0 additions & 17 deletions lib/uk_postcode/country.rb

This file was deleted.

12 changes: 0 additions & 12 deletions lib/uk_postcode/country/lookup.rb

This file was deleted.

15 changes: 15 additions & 0 deletions lib/uk_postcode/country_finder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'uk_postcode/country_lookup'

module UKPostcode
module CountryFinder
module_function

def country(postcode)
normalized = [postcode.outcode, postcode.incode].compact.join
COUNTRY_LOOKUP.each do |name, regexp|
return name if normalized.match(regexp)
end
:unknown
end
end
end
Loading

0 comments on commit a37c0ee

Please sign in to comment.