uk_postcode 1.0.1 → 2.0.0.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fd5d3327b3f2fff64453bb32cbf18941c6e668ed
4
- data.tar.gz: 9e8d9284064d002eb6b004ed1fbd0d5779b9a9aa
3
+ metadata.gz: a2918ca3cc4b63e92b62146b63e57897b31166f0
4
+ data.tar.gz: c6689e38f4f8e4583d74f9bf06c5c617a16f66b9
5
5
  SHA512:
6
- metadata.gz: efa1121b17ba18e5d1c33ddd6a8d0ee7a239e1c1baa35a1699400e5e2f3892542636d585fe597bd66be3fa7a4f0148ec824d5a335430a1dd06daf13b6887325e
7
- data.tar.gz: 26f43a092dce9d1f706cf36eee76af39d076b988af4d08ff581ee7ac21c84465f8a2f8d34b570e7021b68038f9e1558bf6c28e21b3c42bd18c221a7526122b31
6
+ metadata.gz: ea95753c01bc600400b6223b5da95957170db8d1e39f10b80f045e5379f248235a33e38b558c64f91b9c3548d616414dfac2f35172cc18b1d29da372418df458
7
+ data.tar.gz: d34d691408d759e967cf626aecec25f92bbff1a10507bff46a082ca96a5ab37e44a2ad185116998a6f4d040c68bcdea1863cbf7487541dbf6928f40ec2b3a8f4
data/COPYING.txt ADDED
@@ -0,0 +1,25 @@
1
+ Copyright (c) 2010-2014 Paul Battley
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
20
+
21
+ The regular expressions in lookup.rb are generated from the ONS Postcode
22
+ Directory under the terms of the Open Government Licence (OGL). The following
23
+ source accreditation is given to fulfil the terms of this licence.
24
+
25
+ http://www.ons.gov.uk/ons/guide-method/geography/products/postcode-directories/-nspp-/index.html
data/README.md CHANGED
@@ -11,6 +11,7 @@ Features:
11
11
  * Allows extraction of fields within postcode.
12
12
  * Validated against 2.5 million postcodes in England, Wales, Scotland, Northern
13
13
  Ireland, the Channel Islands, and the Isle of Man.
14
+ * Finds the country corresponding to a postcode, where possible.
14
15
 
15
16
  ## Usage
16
17
 
@@ -18,52 +19,106 @@ Features:
18
19
  require "uk_postcode"
19
20
  ```
20
21
 
21
- Validate and extract sections of a full postcode:
22
+ Parse and extract sections of a full postcode:
22
23
 
23
24
  ```ruby
24
- pc = UKPostcode.new("W1A 2AB")
25
- pc.valid? #=> true
26
- pc.full? #=> true
27
- pc.outcode #=> "W1A"
28
- pc.incode #=> "2AB"
29
- pc.area #=> "W"
30
- pc.district #=> "1A"
31
- pc.sector #=> "2"
32
- pc.unit #=> "AB"
25
+ pc = UKPostcode.parse("W1A 2AB")
26
+ pc.valid? # => true
27
+ pc.full? # => true
28
+ pc.outcode # => "W1A"
29
+ pc.incode # => "2AB"
30
+ pc.area # => "W"
31
+ pc.district # => "1A"
32
+ pc.sector # => "2"
33
+ pc.unit # => "AB"
33
34
  ```
34
35
 
35
36
  Or of a partial postcode:
36
37
 
37
38
  ```ruby
38
- pc = UKPostcode.new("W1A")
39
- pc.valid? #=> true
40
- pc.full? #=> false
41
- pc.outcode #=> "W1A"
42
- pc.incode #=> nil
43
- pc.area #=> "W"
44
- pc.district #=> "1A"
45
- pc.sector #=> nil
46
- pc.unit #=> nil
39
+ pc = UKPostcode.parse("W1A")
40
+ pc.valid? # => true
41
+ pc.full? # => false
42
+ pc.outcode # => "W1A"
43
+ pc.incode # => nil
44
+ pc.area # => "W"
45
+ pc.district # => "1A"
46
+ pc.sector # => nil
47
+ pc.unit # => nil
47
48
  ```
48
49
 
49
- Normalise postcodes with `normalize` (or just `norm`):
50
+ Postcodes are converted to a normal or canonical form:
50
51
 
51
52
  ```ruby
52
- UKPostcode.new("w1a1aa").normalize #=> "W1A 1AA"
53
+ pc = UKPostcode.parse("w1a1aa")
54
+ pc.valid? # => true
55
+ pc.area # => "W"
56
+ pc.district # => "1A"
57
+ pc.sector # => "1"
58
+ pc.unit # => "AA
59
+ pc.to_s # => "W1A 1AA"
53
60
  ```
54
61
 
55
- Fix mistakes with IO/10:
62
+ And mistakes with I/1 and O/0 are corrected:
56
63
 
57
64
  ```ruby
58
- pc = UKPostcode.new("WIA OAA")
59
- pc.outcode #=> "W1A"
60
- pc.incode #=> "0AA"
65
+ pc = UKPostcode.parse("WIA OAA")
66
+ pc.valid? # => true
67
+ pc.area # => "W"
68
+ pc.district # => "1A"
69
+ pc.sector # => "0"
70
+ pc.unit # => "AA
71
+ pc.to_s # => "W1A 0AA"
61
72
  ```
62
73
 
63
- ## Gem?
74
+ Find the country of a full or partial postcode (if possible: some outcodes span
75
+ countries):
64
76
 
65
- ```sh
66
- $ gem install uk_postcode
77
+ ```ruby
78
+ UKPostcode.parse("W1A 1AA").country # => :england
79
+ UKPostcode.parse("BT4").country # => :northern_ireland
80
+ UKPostcode.parse("CA6").country # => :unknown
81
+ UKPostcode.parse("CA6 5HS").country # => :scotland
82
+ UKPostcode.parse("CA6 5HT").country # => :england
83
+ ```
84
+
85
+ The country returned for a postcode is derived from the [ONS Postcode
86
+ Directory][onspd] and might not always be correct in a border region:
87
+
88
+ > Users should note that postcodes that straddle two geographic areas will be
89
+ > assigned to the area where the mean grid reference of all the addresses
90
+ > within the postcode falls.
91
+
92
+ Invalid postcodes:
93
+
94
+ ```ruby
95
+ pc = UKPostcode.parse("Not Valid")
96
+ pc.valid? # => false
97
+ pc.full? # => false
98
+ pc.area # => nil
99
+ pc.to_s # => "Not valid"
100
+ pc.country # => :unknown
101
+ ```
102
+
103
+ ## For users of version 1.x
104
+
105
+ The interface has changed significantly, so code that worked with version 1.x
106
+ will not work with version 2.x without changes.
107
+
108
+ Specifically:
109
+
110
+ * Use `UKPostcode.parse(str)` where you previously used `UKPostcode.new(str)`.
111
+ * `parse` will return either a `GeographicPostcode`, a `GiroPostcode`, or an
112
+ `InvalidPostcode`.
113
+ * You may prefer to use `GeographicPostcode.parse` directly if you wish to
114
+ exclude `GIR 0AA` and invalid postcodes.
115
+
116
+ ## As a gem
117
+
118
+ In your `Gemfile`:
119
+
120
+ ```ruby
121
+ gem "uk_postcode", "~> 2.0.0.alpha"
67
122
  ```
68
123
 
69
124
  ## Testing
@@ -71,44 +126,39 @@ $ gem install uk_postcode
71
126
  To run the test suite:
72
127
 
73
128
  ```sh
74
- $ rake
129
+ $ make
75
130
  ```
76
131
 
77
132
  The full list of UK postcodes is not included in the repository due to its
78
- size.
79
- Additional sample files under `test/samples` will automatically be used.
133
+ size, but will be fetched automatically from [mySociety][mys].
80
134
 
81
- The format of each line in a sample file must be `OOOOIII` or `OOO III` (where
82
- `O` = outcode, `I` = incode), e.g.:
135
+ If you are running an automatic build process, please find a way to cache these
136
+ files and run the tests via Rake instead:
83
137
 
84
138
  ```
85
- AB1 0AA
86
- BT109AH
139
+ $ rake
87
140
  ```
88
141
 
89
- You can obtain lists of postcodes by processing various datasets available
90
- from [mySociety][mys].
142
+ ## Licensing
91
143
 
92
- ### Code-Point Open
144
+ You may use this library according to the terms of the MIT License; see
145
+ COPYING.txt for details.
93
146
 
94
- This does not include postcodes for Northern Ireland, the Channel Islands,
95
- or the Isle of Man.
147
+ The regular expressions in `lookup.rb` are derived from the ONS Postcode
148
+ Directory according to the terms of the [Open Government
149
+ Licence][onspd-lic].
96
150
 
97
- ```sh
98
- $ cut -c 2-8 Data/CSV/*.csv | \
99
- sort -uV > test/samples/large/code_point_open.list
100
- ```
101
-
102
- ### ONS Postcode Directory
151
+ > Under the terms of the Open Government Licence (OGL) […] anyone wishing to
152
+ > use or re-use ONS material, whether commercially or privately, may do so
153
+ > freely without a specific application for a licence, subject to the
154
+ > conditions of the OGL and the Framework. Users reproducing ONS content must
155
+ > include a source accreditation to ONS.
103
156
 
104
- This includes postcodes for the whole UK as well as the Channel Islands and the
105
- Isle of Man.
106
- It also includes some defunct postcodes, most notably the NPT outcode, which
107
- must be filtered out.
108
-
109
- ```sh
110
- $ cut -c 2-8 ONSPD_*.csv | grep '[A-Z]' | grep -v ^NPT | \
111
- sort -uV > test/samples/large/onspd.list
112
- ```
157
+ In order to avoid the restrictive commercial terms of the Northern Ireland
158
+ data in the ONSPD, this is not used to generate the regular expressions.
159
+ Fortunately, Northern Ireland postcodes are very simple: they all start with
160
+ `BT`!
113
161
 
114
162
  [mys]: http://parlvid.mysociety.org/os/
163
+ [onspd]: http://www.ons.gov.uk/ons/guide-method/geography/products/postcode-directories/-nspp-/index.html
164
+ [onspd-lic]: http://www.ons.gov.uk/ons/guide-method/geography/beginner-s-guide/licences/index.html
@@ -0,0 +1,15 @@
1
+ require 'uk_postcode/country_lookup'
2
+
3
+ module UKPostcode
4
+ module CountryFinder
5
+ module_function
6
+
7
+ def country(postcode)
8
+ normalized = [postcode.outcode, postcode.incode].compact.join
9
+ COUNTRY_LOOKUP.each do |name, regexp|
10
+ return name if normalized.match(regexp)
11
+ end
12
+ :unknown
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ module UKPostcode
2
+ COUNTRY_LOOKUP = {
3
+ england: /^(?:AL|B[123456789ABDHLNRS]|C(?:A(?:1|2|3|4|5|6(?:4|5(?:G|H[TU]|L[GHJLNPQRSTUWXYZ]|N|P|Q|R|S|T|U|W|X|Y)|6|7)|7|8|9)|B|H(?:1(?:1|2|3|4(?:A|B|D|E|F|G|H|J|L|N|P|Q[ABDFGHJNPQRSTUWX]|R|S|W|X|Y|Z)|5|6(?:A|B[DEFGHJU]|D[ABEFGHLNPQRSTUWXYZ]|E[ADEFGHJLNPQRSTUWXYZ]|F|G|H[DEFGJLNQSTUWXYZ]|J|L|N|P|Q[AP]|R|W|X|Y|Z)|9)|2(?:1|2|3|4|5|6|7|89(?:A[ABEFGHJLNPQRSTUWXYZ]|B)|99(?:A[BDEFGHJLNPQRSTUWXYZ]|B|D|E))|3(?:5|6|7|8|9|0|1|2|3|49(?:A[ABDEFGHJLNQRSTUWXYZ]|B|D|E|S|T|Z))|4(?:0(?:EJ|W[BDFGHRU]|Y[HJ]|Z[ADEFGHJLPTUWY])|7|8(?:A|B|D|E|H|J|L[BDEFLNPSUXYZ]|N[DEFGLNPQRSTUWXYZ]|P[ADEFGHJLNPQRSTUWXZ]|Q[ADU]|R[AD]|W[ABDEFGHJLPQRSTUWXYZ]|X|Y|Z|0|1|2|3|4|5|6|7|8|9)|9(?:A[DEFG]|BW|D[EFGHJLNPQRSTUW]|E[BDEFGJLNPQRSTUWXYZ]|F|G[ABJ]|H|J|L|N|P[XZ]|Q|R|T|U|W|X[SUY]|Y|Z|0|1|2|3|4|5|6|7|8|9)|1|2|3|4|5|6)|6(?:5[0123456789]|6[0123456789]|9AB|0|1|2|3|4)|708(?:A[ADEGHJLNPQRSTUWXYZ]|B|D)|883|9)|M|O|R|T|V|W)|D(?:A|E|G165(?:H[TUZ]|J[AB])|H|L|N|T|Y)|E[123456789CNX]|FY|G(?:I|L(?:1(?:1|2|3|4|5|9|0|6(?:7|8(?:A|B|D|E|H|J|L|N|P[ADEFGHJLNPQRSTUWXYZ]|Q|R|S|T|W|Y|Z)|9)|7|8)|2|3|4|5|6|7|8|9)|U)|H(?:A|D|G|P|R(?:1|2|3(?:5(?:AL|B[BDERZ]|E[NPRSTUWXYZ]|FE|H|Q[XYZ]|R[ABDFGNQW]|S[TUXY]|T[ABDEFGP]|W[NUX]|XA|Y[ADFJNQRSTWX])|6(?:A|B|D|E[ABDEFGHJLNPQRSTUWZ]|H[EFGHJPQSTUWXYZ]|J[ABDEFGHLNPQRSTZ]|L|N|P|Q|R|S|W|Y[ABDEFHJLNPQU]|Z))|4|5(?:3(?:A|B|D|E[ABDEFGHJLNQRSTUWXYZ]|F|G|H|J|L|N[ABEFGHJLNQ]|P[BFGHQTUXYZ]|QA|R[BDEFGHJLNPQRSTUWXYZ]|S|T|U|W|X|Y|Z)|8)|6|7|8|9)|U|X)|I[GP]|KT|L(?:1|2|3|4|5|6|7|8|9|A|D(?:71(?:DY|EA|N[BDEF]|T[NPRSUWY]|U[ABDEFW]|Y[SUY]|ZJ)|82(?:H[HLNPRSTUWY]|L[EGHLNPRSTUWY]|N[ABD]|P[RS]))|E|L145(?:B[UY]|D[ABDEFGLNPRSUWY]|E[ABDEFHLNW]|L[JP])|N|S|U)|M[123456789EK]|N(?:1|2|3|4|5|6|7|8|9|E|G|N|P(?:167(?:A|B|D|E|F|H|J|L|N[AFGHJLNPQRSTUW]|P|Q|R|WA|Y[BDEFGJLN]|Z[AB])|25(?:3S[RSTUX]|4(?:AS|L[DHJLNPQRTXYZ]|N[ABDE])|5(?:Q[JL]|R[DGHJLNPQRSTWY]))|5(?:3(?:Q[JL]|R[GHJLNPQRSTWY]|S[RSTUX])|4(?:L[HJLNPQRSTUWXYZ]|N[AB]))|6(?:6SG|7(?:A|B|D|E|F|H|J|L|N[AFGJLNPQRSTUW]|P|Q|R|Y|Z))|78(?:EB|HH))|R|W)|O|P[ELOR]|R|S(?:1|2|3|4|5|6|7|8|9|E|G|K|L|M|N|O|P|R|S|T|W|Y(?:1(?:1|2|3(?:A|B|D|E|F|G|H|J|L|N|P|Q|R|S|T|W|X|Y|Z|1|2(?:A|B|D|E|F|G|H|J[ABDEFGL]|L[FGT]|N|P[ADEFGHPQRSTUXYZ]|Q|R|S|T|W|X|Y|Z)|3(?:A[AB]|J[GHJLNPQRSTUWXYZ]|L|N[EFGHJLNPQRSTUWXY]|P[FG]|W[ADFGHJXYZ]|X|Y)|4(?:A|B|D|E|H|J|L|N|P|Q|R[ABDFG]|W)|9)|4(?:A|B|D|E|H|J|L|N|P|Q|R|S|T|U|W|Y|7(?:A[ABDEFGHJLPQRSTUWXY]|B|D|E|H[ABDEFGHJLNPQRSTUWXZ]|J[ABDEFJLNPRSWXY]|N[EFGHJQ]|P|U|W|Y)|8)|9[ABDEFGHJLSTUWX]|0(?:0(?:W[AHJQZ]|Z)|1|7(?:A|B|D|E|G|H|J[ABDPRSUXYZ]|L|N[ABDGHJLNPQRW]|P[BDEHJLNPQRW]|R|S|T|U|W|X|Y|Z)|8|9(?:A|B[ABDEFGHJLNQSTUXYZ]|D|E|F|H[ABDEFGHJLNPQRSTUWXY]|J[ABDEFQ]|L[SUXYZ]|N|P[BDEFGHJLNPQRSTUWXYZ]|Q|R|W|Y))|56(?:A[XY]|B[DEGHJLNPQSTUWXYZ]|D[ABDEFGPR]|E[DEP]|H[TUY]|S[PYZ]|T[RZ]|U[BDEFGHQ]|Z))|2(?:2(?:P|5WD|6(?:E[ABDEFGHJLQ]|H[BD]|JX|L[GLNQUWXYZ]|N[TW]|W[ABEHJXYZ]))|5[ABDEGHJLNPQRSTUWXYZ]|6|18(?:D[LN]|E[PRW]|J[LNPQRSTUWXY]|LB))|3|4|5(?:0(?:A|B|D|E|F|H|J[ABDEFGHPQWXYZ]|L|N|P|Q|R|S|T|U|W|X|Y|Z)|6|7|8|9(?:A[ABDEFGHJLNPQ]|B[XYZ]|D|E[ABDEFGHJLNPQRSTWXYZ]|FA|G[ABDEFGHJLQXYZ]|H|J|L[ABEFGHJLNPQRSTUWXYZ]|N|P|Q|R|SA|W|X|Y|Z))|6|7(?:0|7|8(?:A|B|D|E|H|J|L|N|P[ABDEFGHJLNPQRW]|Q|W)|9)|8|9(?:5(?:A|B|D|E|F|H|J[GHJLNQRSTUXY]|L|N|P|W)|9)))|T(?:A|D(?:1(?:24(?:Q[ABDEFGHJLNPQRSTW]|R|S|T|U|WB|X[ABDFGHJLNQ])|5(?:1(?:A|B[ABEFGHJLNPQSUWXYZ]|D|E|H|J|L|N|P|Q|R|S[ABDEJLNPWXY]|T[ABQUX]|U[ABDENPQRTUXY]|W[FX]|X[XY]|Y|Z)|2|9))|90T[JPRSTW])|F|N|Q|R|S|W)|U|W|Y)/,
4
+ scotland: /^(?:AB|CA65(?:A|B|D|E|H[ABDEFGHJLNPQRSWXYZ]|J|L[ABDE])|D(?:D|G(?:1(?:1|2|3|4|9|0|65(?:A|B|D|E|F|G|H[ABDEFGHJLNPQRSWXY]|J[DEFGHJLNPQRSTUWXYZ]|L|N|Q|U|W|X|Y))|2|3|4|5|6|7|8|9))|EH|FK|G[123459678]|HS|IV|K[AWY]|ML|P[AH]|TD(?:1(?:1|2(?:A|B|D|E|H|J|L|N|P|Q|R|S|T|U|W|Y|4(?:A|B|D|E|F|H|J|L|N|QU|W[ADX]|XE|Y)|9)|3|9|0|4|51(?:BT|S[UZ]|T[DEFGHJLNPRSTW]|U[FGHJLSWZ]|W[YZ]|X[ABGHJLNQTUWZ]))|2|3|4|5|6|7|8|9(?:0(?:A|B|D|E|H|J|L|N|P|Q|R|S|T[ABDEFGHLNQUXY]|W|Y|Z)|1|7|8|9))|Z)/,
5
+ wales: /^(?:C(?:F|H(?:1(?:4QL|6(?:B[ABLPQRSTWXYZ]|D[DJ]|EB|H[ABH]|Q[BEFGQ]))|2(?:89AD|99AA)|349AP|4(?:0(?:A|B|D|E[ABDEFGHLNPQRSTUWXYZ]|F|G|H|J|L|N|P|Q|R|S|T|W[AE]|X|Y[PRSTUXY]|Z[BNQRS])|8(?:G|L[AGHJQRTW]|N[ABH]|P[BY]|Q[BEFGHJLNPQRSTWYZ]|R[BEFGHJLNPQRSTUWXYZ]|S|T|U|WN)|9(?:A[ABHJLNPQRSTUWXYZ]|B[ABDEFGHJLNPQRSTUXYZ]|D[ABDXYZ]|E[AH]|GD|P[ABDFG]|XA))|5|6(?:5[ABDEFGHJLNPQRSTUWXYZ]|6[ABDEFHJLZ]|9(?:A[DEFGJLNPQRSTUWXYZ]|B|D))|7(?:1|2|3|4|5|6|9|08A[BF])|8(?:7|8[ABDEHJLNPQRSTWYZ]|9)))|GL168PB|HR(?:3(?:5(?:A[ABDEFGHJNPQRSTUWXYZ]|B[AGHJLNPQSTUWXY]|D|E[ABDEFGHJLQ]|F[AD]|J|L|N|P|Q[ABDEFGHJLNPQRSTUW]|R[EHJLPRSTUXYZ]|S[ABDEFGHJLNPQRSWZ]|T[HRSTUX]|W[ALPQRSTWYZ]|XZ|Y[BEGHLPU]|Z)|6(?:E[XY]|H[ALN]|J[JUWXY]|YG))|53(?:EP|N[DPRSTUWXYZ]|P[ADEJLNPRW]|Q[BDEFGHJLNPQRSTUWXZ]|RA))|L(?:D(?:1|2|3|4|5|6|7(?:1(?:A|B|D[ABDEFGHJLNPQRSTUW]|E[BDEFGHNPRSTUWY]|H|L|N[AGHJLNPQRSTUWY]|P|R|S|T[ABDET]|U[GHPRTUY]|W|Y[ABDEFGHLNPRTW]|ZY)|9)|82(?:A|B|D|E|G|H[ABDEFG]|L[ABDF]|N[EFGHJLNPQRSTUWXY]|P[ABDEFGHLNPTUWY]|R|S|T|U|W|Y|Z))|L(?:1(?:1|2|3|4(?:1|2|3|4|5(?:A|B[ABDEFGHJLNPQSTWXZ]|D[HJ]|E[PQRSTUYZ]|H|L[ABDEFGHLNQRSTUWY]|N|P|Q|R|S|W|Y|Z)|6)|5|6|7|8|9)|2|3|4|5|6|7))|NP(?:1(?:0|2|4|5|6(?:A|B|D|E|F|G|H|J|L|N|P|Q|R|X|Y|Z|5|6|7(?:NX|W[EJLNPQRSTUWXYZ]|Y[AHPQRSTUWY]|Z[SX])|9)|7|8|9|1|3)|2(?:0|1|2|3|4|5(?:A|B|D|E|H|J|L|N|P|Q|R|X|Y|3(?:A|B|D|E|G|H|J|L|N|P|Q|S[ABDEFGHJLNPQWYZ]|T|U|W|X|Y|Z)|4(?:A[BDEFGHJLNPQRTW]|B|D|E|H|J|L[EGUW]|N[YZ]|P|Q|R|S|T|U|W|X|Y|Z)|5(?:A|B|D|E|F|G|H|J|L|N|P|Q[ABDEFGHQXYZ]|R[ABEFZ]|S|T|U|W|X|Y|Z)|9)|6)|3|4|5(?:1|2|3(?:A|B|D|E|G|H|J|L|N|P|Q[ABDEFGHQXYZ]|R[ABDEFZ]|S[ABDEFGHJLNPQWZ]|T|U|X|Y|Z)|4(?:A|B|D|E|F|H|J|L[ABDEFG]|N[FGHJLNPQRSTUWXYZ]|P|Q|R|S|T|U|X|Y))|6(?:1|2|3|4|5|6(?:A|B|D|E|H|J|L|N|P|Q|R|S[ABDEFHJLNPQRSTUWXYZ]|T|U|X|Y)|7N[HX])|7(?:0|1|5|6|7|8(?:A|B|D|E[ADEFGHLNPQRSTWXY]|H[ABDEFGLNPRSTUWY]|L|N|P|R|S|T|U|W|Y|Z)|9)|8|9)|S(?:A|Y(?:1(?:3(?:2(?:J[HJNPQRSTUWXYZ]|L[ABDEHJLNPQRSUWX]|PB)|3(?:A[DEFGHJLNPQRSTUWXYZ]|B|D|E|G|H|J[AB]|NZ|P[ABDE]|W[BE])|4RE)|47(?:A[NZ]|HY|J[HTUZ]|L|N[ABD])|97|0(?:0(?:A|B|D|E|G|H|J|L|N|P|W[BDEFG])|7(?:J[EFGHJLNQW]|N[EFSTUXYZ]|P[AGSTUXYZ]|Q)|9(?:B[PW]|HZ|J[GHJLNPRSTUWXYZ]|L[ABDEFGHJLNPQRTW]|PA))|56(?:A[ABDEFGHJLNPQRSTUWZ]|B[AB]|D[HJLNQSTUWXYZ]|E[ABFGHJLNQRSTUWXYZ]|H[ABDEFGHJLNPQRSWXZ]|J|L|N|P|Q|R|S[ABDEFGHQRSTUX]|T[ABDEFGHJLNPQWY]|U[AJLNPR]|W|Y)|6|7|8)|2(?:2(?:5(?:A|B|D|E|H|J|L|N|W[ABPZ])|6(?:A|B|D|E[NPRSTUWXYZ]|F|G|H[AEFLNPRSTUWXYZ]|J[ABDEFGHJLNPQRSTWYZ]|L[ABDEFHJPRST]|N[ABDEFGHJLNPQRSXYZ]|P|Q|R|S|T|U|W[DFG]|X|Y))|56|0|1(?:0|1|7|8(?:A|B|D[ABDEFGHJPQRSTUWXYZ]|E[ABDEFGHJLNQSTUXYZ]|F|G|H|J[ABDEFGHJZ]|L[ADEFGHJLNPQRSTUWXYZ]|N|P|Q|R|S|T|W|X|Z)|9)|3|4)|5(?:0J[JLN]|9(?:A[RSTUWXYZ]|B[ABDEFGHJLNPQSTUW]|EU|FB|GN|LD|SB))|78PS|95J[PW])))/,
6
+ channel_islands: /^(?:GY|J)/,
7
+ isle_of_man: /^IM/,
8
+ northern_ireland: /^BT/
9
+ }
10
+ end
@@ -0,0 +1,98 @@
1
+ require "uk_postcode/country_finder"
2
+
3
+ module UKPostcode
4
+
5
+ # GeographicPostcode models the majority of postcodes, containing an area,
6
+ # a district, a sector, and a unit.
7
+ #
8
+ # Despite the name, it also handles non-geographic postcodes that follow the
9
+ # geographic format.
10
+ #
11
+ class GeographicPostcode
12
+ PATTERN = %r{
13
+ \A ( [A-PR-UWYZ01][A-HJ-Z0]? ) # area
14
+ (?: ( [0-9IO][0-9A-HJKMNPR-YIO]? ) # district
15
+ (?: \s* ( [0-9IO] ) # sector
16
+ ( [ABD-HJLNPQ-Z]{2} )? )? )? # unit
17
+ \Z
18
+ }ix
19
+
20
+ # Attempts to parse the postcode given in str, and returns an instance of
21
+ # GeographicPostcode on success, or nil on failure.
22
+ #
23
+ def self.parse(str)
24
+ matched = PATTERN.match(str.strip)
25
+ if matched
26
+ new(*(1..4).map { |i| matched[i] })
27
+ else
28
+ nil
29
+ end
30
+ end
31
+
32
+ # Initialise a new GeographicPostcode instance with the given area,
33
+ # district, sector, and unit. Only area is required.
34
+ #
35
+ def initialize(area, district = nil, sector = nil, unit = nil)
36
+ @area = letters(area)
37
+ @district = digits(district)
38
+ @sector = digits(sector)
39
+ @unit = letters(unit)
40
+ end
41
+
42
+ attr_reader :area, :district, :sector, :unit
43
+
44
+ # The left-hand part of the postcode, e.g. W1A 1AA -> W1A
45
+ #
46
+ def outcode
47
+ return nil unless district
48
+ [area, district].join('')
49
+ end
50
+
51
+ # The right-hand part of the postcode, e.g. W1A 1AA -> 1AA
52
+ #
53
+ def incode
54
+ return nil unless sector && unit
55
+ [sector, unit].join('')
56
+ end
57
+
58
+ # Returns the canonical string representation of the postcode.
59
+ #
60
+ def to_s
61
+ [area, district, " ", sector, unit].compact.join('').strip
62
+ end
63
+
64
+ # Returns true if the postcode is a valid full postcode (e.g. W1A 1AA)
65
+ #
66
+ def full?
67
+ area && district && sector && unit && true
68
+ end
69
+
70
+ # Any geographic postcode is assumed to be valid
71
+ #
72
+ def valid?
73
+ true
74
+ end
75
+
76
+ # Find the country associated with the postcode. Possible values are
77
+ # :england, :scotland, :wales, :northern_ireland, :isle_of_man,
78
+ # :channel_islands, or :unknown.
79
+ #
80
+ # Note that, due to limitations in the underlying data, the country might
81
+ # not always be correct in border regions.
82
+ #
83
+ def country
84
+ CountryFinder.country(self)
85
+ end
86
+
87
+ private
88
+ def letters(str)
89
+ return nil unless str
90
+ str.upcase.tr("10", "IO")
91
+ end
92
+
93
+ def digits(str)
94
+ return nil unless str
95
+ str.upcase.tr("IO", "10")
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,61 @@
1
+ require "singleton"
2
+
3
+ module UKPostcode
4
+
5
+ # GiroPostcode models the single, peculiar GIR 0AA postcode originally used
6
+ # by Girobank.
7
+ #
8
+ # area, district, sector, and unit all return nil, because this postcode
9
+ # does not meaningfully possess these distinctions.
10
+ #
11
+ class GiroPostcode
12
+ include Singleton
13
+
14
+ PATTERN = /\A G[I1]R \s* [0O]AA \z/ix
15
+
16
+ # Returns an instance of GiroPostcode if str represents GIR 0AA, and nil
17
+ # otherwise.
18
+ #
19
+ def self.parse(str)
20
+ PATTERN.match(str.strip) ? instance : nil
21
+ end
22
+
23
+ attr_reader :area, :district, :sector, :unit
24
+
25
+ # The left-hand part of the postcode, always "GIR".
26
+ #
27
+ def outcode
28
+ "GIR"
29
+ end
30
+
31
+ # The right-hand part of the postcode, always "0AA".
32
+ #
33
+ def incode
34
+ "0AA"
35
+ end
36
+
37
+ # The canonical string representation of the postcode, i.e. "GIR 0AA".
38
+ #
39
+ def to_s
40
+ "GIR 0AA"
41
+ end
42
+
43
+ # GIR 0AA is always valid.
44
+ #
45
+ def valid?
46
+ true
47
+ end
48
+
49
+ # GIR 0AA is always full.
50
+ #
51
+ def full?
52
+ true
53
+ end
54
+
55
+ # GIR 0AA is in England. (In Bootle, in fact.)
56
+ #
57
+ def country
58
+ :england
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,38 @@
1
+ module UKPostcode
2
+
3
+ # InvalidPostcode is a singleton null object returned by UKPostcode.parse
4
+ # when it is unable to parse the supplied postcode.
5
+ #
6
+ # The sub-fields of the postcode (outcode, area, etc.) are all nil.
7
+ #
8
+ class InvalidPostcode
9
+ def self.parse(str)
10
+ new(str)
11
+ end
12
+
13
+ def initialize(input)
14
+ @input = input
15
+ end
16
+
17
+ attr_reader :area, :district, :sector, :unit, :incode, :outcode
18
+
19
+ # Returns the literal string supplied at initialisation. This may be
20
+ # helpful when returning erroneous input to the user.
21
+ #
22
+ def to_s
23
+ @input
24
+ end
25
+
26
+ def full?
27
+ false
28
+ end
29
+
30
+ def valid?
31
+ false
32
+ end
33
+
34
+ def country
35
+ :unknown
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,75 @@
1
+ module UKPostcode
2
+ class Tree
3
+ def initialize(tree = {})
4
+ @root = tree
5
+ end
6
+
7
+ def insert(path, value)
8
+ path[0..-2].inject(@root) { |n, p| n[p] ||= {} }[path.last] = value
9
+ end
10
+
11
+ def compress
12
+ self.class.new(compress_node(@root))
13
+ end
14
+
15
+ def filter(leaf_value)
16
+ self.class.new(filter_node(@root, leaf_value))
17
+ end
18
+
19
+ def regexp
20
+ Regexp.new('^' + node_regexp(@root))
21
+ end
22
+
23
+ def to_h
24
+ @root
25
+ end
26
+
27
+ private
28
+
29
+ def leaf?(node)
30
+ node.is_a?(Symbol)
31
+ end
32
+
33
+ def contains_identical_leaves?(node)
34
+ node.values.all? { |t| leaf?(t) } && node.values.uniq.length == 1
35
+ end
36
+
37
+ def compress_node(node)
38
+ if leaf?(node)
39
+ node
40
+ else
41
+ comp = Hash[node.map { |k, v| [k, compress_node(v)] }]
42
+ contains_identical_leaves?(comp) ? comp.values.first : comp
43
+ end
44
+ end
45
+
46
+ def filter_node(node, leaf_value)
47
+ if node == leaf_value
48
+ node
49
+ elsif leaf?(node)
50
+ nil
51
+ else
52
+ h = Hash[node.map { |k, v| [k, filter_node(v, leaf_value)] }.
53
+ select { |_, v| v }]
54
+ h.empty? ? nil : h
55
+ end
56
+ end
57
+
58
+ def node_regexp(node)
59
+ if leaf?(node)
60
+ ''
61
+ else
62
+ segments = node.map { |k, v| k + node_regexp(v) }
63
+ if segments.length > 1
64
+ if segments.all? { |s| s.length == 1 }
65
+ '[' + segments.join('') + ']'
66
+ else
67
+ '(?:' + segments.join('|') + ')'
68
+ end
69
+ else
70
+ segments.first
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,3 +1,3 @@
1
- class UKPostcode
2
- VERSION = "1.0.1"
1
+ module UKPostcode
2
+ VERSION = "2.0.0.alpha"
3
3
  end