uk_postcode 1.0.1 → 2.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
data/lib/uk_postcode.rb CHANGED
@@ -1,112 +1,19 @@
1
- class UKPostcode
2
- MATCH = /\A \s* (?:
3
- ( G[I1]R \s* [0O]AA ) # special postcode
4
- |
5
- ( [A-PR-UWYZ01][A-Z01]? ) # area
6
- ( [0-9IO][0-9A-HJKMNPR-YIO]? ) # district
7
- (?: \s*
8
- ( [0-9IO] ) # sector
9
- ( [ABD-HJLNPQ-Z10]{2} ) # unit
10
- )?
11
- ) \s* \Z/x
12
-
13
- attr_reader :raw
14
-
15
- # Initialise a new UKPostcode instance from the given postcode string
16
- #
17
- def initialize(postcode_as_string)
18
- @raw = postcode_as_string
19
- end
20
-
21
- # Returns true if the postcode is a valid full postcode (e.g. W1A 1AA) or outcode (e.g. W1A)
22
- #
23
- def valid?
24
- !!outcode
25
- end
26
-
27
- # Returns true if the postcode is a valid full postcode (e.g. W1A 1AA)
28
- #
29
- def full?
30
- !!(outcode && incode)
31
- end
32
-
33
- # The left-hand part of the postcode, e.g. W1A 1AA -> W1A
34
- #
35
- def outcode
36
- area && district && [area, district].join
37
- end
38
-
39
- # The right-hand part of the postcode, e.g. W1A 1AA -> 1AA
40
- #
41
- def incode
42
- sector && unit && [sector, unit].join
43
- end
44
-
45
- # The first part of the outcode, e.g. W1A 2AB -> W
46
- #
47
- def area
48
- parts[0]
49
- end
50
-
51
- # The second part of the outcode, e.g. W1A 2AB -> 1A
52
- #
53
- def district
54
- parts[1]
55
- end
56
-
57
- # The first part of the incode, e.g. W1A 2AB -> 2
58
- #
59
- def sector
60
- parts[2]
61
- end
1
+ require "uk_postcode/version"
2
+ require "uk_postcode/geographic_postcode"
3
+ require "uk_postcode/giro_postcode"
4
+ require "uk_postcode/invalid_postcode"
62
5
 
63
- # The second part of the incode, e.g. W1A 2AB -> AB
64
- #
65
- def unit
66
- parts[3]
67
- end
6
+ module UKPostcode
7
+ module_function
68
8
 
69
- # Render the postcode as a normalised string, i.e. in upper case and with spacing.
70
- # Returns an empty string if the postcode is not valid.
9
+ # Attempt to parse the string str as a postcode. Returns an object
10
+ # representing the postcode, or an InvalidPostcode if the string cannot be
11
+ # parsed.
71
12
  #
72
- def norm
73
- [outcode, incode].compact.join(" ")
74
- end
75
- alias_method :normalise, :norm
76
- alias_method :normalize, :norm
77
-
78
- alias_method :to_s, :raw
79
- alias_method :to_str, :raw
80
-
81
- def inspect(*args)
82
- "<#{self.class.to_s} #{raw}>"
83
- end
84
-
85
- private
86
- def parts
87
- return @parts if @matched
88
-
89
- @matched = true
90
- matches = raw.upcase.match(MATCH) || []
91
- if matches[1]
92
- @parts = %w[ G IR 0 AA ]
93
- else
94
- a, b, c, d = (2..5).map{ |i| matches[i] }
95
- if a =~ /^[A-Z][I1]$/
96
- a = a[0, 1]
97
- b = "1" + b
98
- end
99
- @parts = [letters(a), digits(b), digits(c), letters(d)]
13
+ def parse(str)
14
+ [GiroPostcode, GeographicPostcode, InvalidPostcode].each do |klass|
15
+ pc = klass.parse(str)
16
+ return pc if pc
100
17
  end
101
18
  end
102
-
103
- def letters(s)
104
- s && s.tr("10", "IO")
105
- end
106
-
107
- def digits(s)
108
- s && s.tr("IO", "10")
109
- end
110
19
  end
111
-
112
- require "uk_postcode/version"
@@ -0,0 +1,33 @@
1
+ require_relative "./test_helper"
2
+ require "uk_postcode"
3
+
4
+ describe UKPostcode do
5
+ it 'should find the country of a full postcode in England' do
6
+ UKPostcode.parse('W1A 1AA').country.must_equal :england
7
+ end
8
+
9
+ it 'should find the country of a full postcode in Scotland' do
10
+ UKPostcode.parse('EH8 8DX').country.must_equal :scotland
11
+ end
12
+
13
+ it 'should find the country of a full postcode in Wales' do
14
+ UKPostcode.parse('CF99 1NA').country.must_equal :wales
15
+ end
16
+
17
+ it 'should find the country of a full postcode in Northern Ireland' do
18
+ UKPostcode.parse('BT4 3XX').country.must_equal :northern_ireland
19
+ end
20
+
21
+ it 'should find the country of a postcode in a border region' do
22
+ UKPostcode.parse('CA6 5HS').country.must_equal :scotland
23
+ UKPostcode.parse('CA6 5HT').country.must_equal :england
24
+ end
25
+
26
+ it 'should find the country of an unambiguous partial postcode' do
27
+ UKPostcode.parse('BT4').country.must_equal :northern_ireland
28
+ end
29
+
30
+ it 'should return :unknown for an ambiguous partial postcode' do
31
+ UKPostcode.parse('DG16').country.must_equal :unknown
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ require_relative "./test_helper"
2
+ require "csv"
3
+ require "uk_postcode"
4
+
5
+ describe "Full set of postcodes" do
6
+ CSV_PATH = File.expand_path("../data/postcodes.csv", __FILE__)
7
+
8
+ COUNTRIES = {
9
+ 'E92000001' => :england,
10
+ 'N92000002' => :northern_ireland,
11
+ 'S92000003' => :scotland,
12
+ 'W92000004' => :wales,
13
+ 'L93000001' => :channel_islands,
14
+ 'M83000003' => :isle_of_man
15
+ }
16
+
17
+ it "should correctly parse and find the country of every extant postcode" do
18
+ if File.exist?(CSV_PATH)
19
+ CSV.foreach(CSV_PATH, headers: [:postcode, :country]) do |row|
20
+ outcode = row[:postcode][0,4].strip
21
+ incode = row[:postcode][4,3].strip
22
+ country = COUNTRIES.fetch(row[:country])
23
+
24
+ postcode = UKPostcode.parse(outcode + incode)
25
+
26
+ postcode.wont_be_nil
27
+ postcode.outcode.must_equal outcode
28
+ postcode.incode.must_equal incode
29
+ postcode.country.must_equal country
30
+ end
31
+ else
32
+ skip "Skipping because #{CSV_PATH} does not exist"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,271 @@
1
+ require_relative "./test_helper"
2
+ require "uk_postcode/geographic_postcode"
3
+
4
+ describe UKPostcode::GeographicPostcode do
5
+ described_class = UKPostcode::GeographicPostcode
6
+
7
+ describe "parse" do
8
+ it "should parse a full postcode" do
9
+ pc = described_class.parse("W1A 1AA")
10
+ pc.must_be_instance_of described_class
11
+ pc.area.must_equal "W"
12
+ pc.district.must_equal "1A"
13
+ pc.sector.must_equal "1"
14
+ pc.unit.must_equal "AA"
15
+ end
16
+
17
+ it "should parse a postcode with no unit" do
18
+ pc = described_class.parse("W1A 1")
19
+ pc.must_be_instance_of described_class
20
+ pc.area.must_equal "W"
21
+ pc.district.must_equal "1A"
22
+ pc.sector.must_equal "1"
23
+ pc.unit.must_be_nil
24
+ end
25
+
26
+ it "should parse an outcode" do
27
+ pc = described_class.parse("W1A")
28
+ pc.must_be_instance_of described_class
29
+ pc.area.must_equal "W"
30
+ pc.district.must_equal "1A"
31
+ pc.sector.must_be_nil
32
+ pc.unit.must_be_nil
33
+ end
34
+
35
+ it "should parse an area" do
36
+ pc = described_class.parse("W")
37
+ pc.must_be_instance_of described_class
38
+ pc.area.must_equal "W"
39
+ pc.district.must_be_nil
40
+ pc.sector.must_be_nil
41
+ pc.unit.must_be_nil
42
+ end
43
+
44
+ it "should handle extra spaces" do
45
+ pc = described_class.parse(" W1A 1AA ")
46
+ pc.must_be_instance_of described_class
47
+ pc.to_s.must_equal "W1A 1AA"
48
+ end
49
+
50
+ it "should handle no spaces" do
51
+ pc = described_class.parse("W1A1AA")
52
+ pc.must_be_instance_of described_class
53
+ pc.to_s.must_equal "W1A 1AA"
54
+ end
55
+
56
+ it "should be case-insensitive" do
57
+ pc = described_class.parse("w1a 1aa")
58
+ pc.must_be_instance_of described_class
59
+ pc.to_s.must_equal "W1A 1AA"
60
+ end
61
+
62
+ it "should return nil if unable to parse" do
63
+ pc = described_class.parse("Can't parse this")
64
+ pc.must_be_nil
65
+ end
66
+
67
+ describe "single-letter area" do
68
+ it "should extract area without trailing I from outcode" do
69
+ pc = described_class.parse("B11")
70
+ pc.area.must_equal "B"
71
+ pc.district.must_equal "11"
72
+ end
73
+
74
+ it "should extract area without trailing I from full postcode with space" do
75
+ pc = described_class.parse("E17 1AA")
76
+ pc.area.must_equal "E"
77
+ pc.district.must_equal "17"
78
+ end
79
+
80
+ it "should extract area without trailing I from full postcode without space" do
81
+ pc = described_class.parse("E171AA")
82
+ pc.area.must_equal "E"
83
+ pc.district.must_equal "17"
84
+ end
85
+ end
86
+
87
+ describe "trailing O in area" do
88
+ it "should extract area with trailing O from outcode" do
89
+ pc = described_class.parse("CO1")
90
+ pc.area.must_equal "CO"
91
+ pc.district.must_equal "1"
92
+ end
93
+
94
+ it "should extract area with trailing O from full postcode with space" do
95
+ pc = described_class.parse("CO1 1BT")
96
+ pc.area.must_equal "CO"
97
+ pc.district.must_equal "1"
98
+ end
99
+
100
+ it "should extract area with trailing O from full postcode without space" do
101
+ pc = described_class.parse("CO11BT")
102
+ pc.area.must_equal "CO"
103
+ pc.district.must_equal "1"
104
+ end
105
+ end
106
+
107
+ describe "tricky postcodes" do
108
+ it "should parse B11 1LL" do
109
+ pc = described_class.parse("B111LL")
110
+ pc.area.must_equal "B"
111
+ pc.district.must_equal "11"
112
+ pc.sector.must_equal "1"
113
+ pc.unit.must_equal "LL"
114
+ end
115
+
116
+ it "should parse BII ILL" do
117
+ pc = described_class.parse("BIIILL")
118
+ pc.area.must_equal "B"
119
+ pc.district.must_equal "11"
120
+ pc.sector.must_equal "1"
121
+ pc.unit.must_equal "LL"
122
+ end
123
+
124
+ it "should parse BB11 1DJ" do
125
+ pc = described_class.parse("BB111DJ")
126
+ pc.area.must_equal "BB"
127
+ pc.district.must_equal "11"
128
+ pc.sector.must_equal "1"
129
+ pc.unit.must_equal "DJ"
130
+ end
131
+
132
+ it "should parse BBII IDJ" do
133
+ pc = described_class.parse("BBIIIDJ")
134
+ pc.area.must_equal "BB"
135
+ pc.district.must_equal "11"
136
+ pc.sector.must_equal "1"
137
+ pc.unit.must_equal "DJ"
138
+ end
139
+
140
+ it "should parse B10 0JP" do
141
+ pc = described_class.parse("B100JP")
142
+ pc.area.must_equal "B"
143
+ pc.district.must_equal "10"
144
+ pc.sector.must_equal "0"
145
+ pc.unit.must_equal "JP"
146
+ end
147
+
148
+ it "should parse BIO OJP" do
149
+ pc = described_class.parse("BIOOJP")
150
+ pc.area.must_equal "B"
151
+ pc.district.must_equal "10"
152
+ pc.sector.must_equal "0"
153
+ pc.unit.must_equal "JP"
154
+ end
155
+ end
156
+ end
157
+
158
+ describe "area" do
159
+ it "should be capitalised" do
160
+ described_class.new("w", "1a", "1", "aa").area.must_equal "W"
161
+ end
162
+
163
+ it "should correct 0 to O" do
164
+ described_class.new("0X", "1", "0", "AB").area.must_equal "OX"
165
+ end
166
+
167
+ it "should correct 1 to I" do
168
+ described_class.new("1G", "1", "1", "AA").area.must_equal "IG"
169
+ end
170
+ end
171
+
172
+ describe "district" do
173
+ it "should be capitalised" do
174
+ described_class.new("w", "1a", "1", "aa").district.must_equal "1A"
175
+ end
176
+
177
+ it "should correct O to 0" do
178
+ described_class.new("B", "2O", "2", "XB").district.must_equal "20"
179
+ end
180
+
181
+ it "should correct I to 1" do
182
+ described_class.new("B", "I", "I", "DF").district.must_equal "1"
183
+ end
184
+ end
185
+
186
+ describe "sector" do
187
+ it "should correct O to 0" do
188
+ described_class.new("AB", "1", "O", "DN").sector.must_equal "0"
189
+ end
190
+
191
+ it "should correct I to 1" do
192
+ described_class.new("W", "1A", "I", "AA").sector.must_equal "1"
193
+ end
194
+ end
195
+
196
+ describe "unit" do
197
+ it "should be capitalised" do
198
+ described_class.new("w", "1a", "1", "aa").unit.must_equal "AA"
199
+ end
200
+
201
+ # Note: neither I nor O appear in units
202
+ end
203
+
204
+ describe "outcode" do
205
+ it "should be generated from area and district" do
206
+ described_class.new("W", "1A").outcode.must_equal "W1A"
207
+ end
208
+
209
+ it "should be nil if missing district" do
210
+ described_class.new("W").outcode.must_be_nil
211
+ end
212
+ end
213
+
214
+ describe "incode" do
215
+ it "should be generated from sector and unit" do
216
+ described_class.new("W", "1A", "1", "AA").incode.must_equal "1AA"
217
+ end
218
+
219
+ it "should be nil if missing sector" do
220
+ described_class.new("W", "1A").incode.must_be_nil
221
+ end
222
+
223
+ it "should be nil if missing unit" do
224
+ described_class.new("W", "1A", "1").incode.must_be_nil
225
+ end
226
+ end
227
+
228
+ describe "to_s" do
229
+ it "should generate a full postcode" do
230
+ described_class.new("W", "1A", "1", "AA").to_s.must_equal "W1A 1AA"
231
+ end
232
+
233
+ it "should generate an outcode" do
234
+ described_class.new("W", "1A").to_s.must_equal "W1A"
235
+ end
236
+
237
+ it "should generate a postcode with no unit" do
238
+ described_class.new("W", "1A", "1").to_s.must_equal "W1A 1"
239
+ end
240
+
241
+ it "should generate an area alone" do
242
+ described_class.new("W").to_s.must_equal "W"
243
+ end
244
+ end
245
+
246
+ describe "full?" do
247
+ it "should be true if outcode and incode are given" do
248
+ described_class.new("W", "1A", "1", "AA").must_be :full?
249
+ end
250
+
251
+ it "should not be true if something is missing" do
252
+ described_class.new("W", "1A", "1").wont_be :full?
253
+ end
254
+ end
255
+
256
+ describe "valid?" do
257
+ it "should be true" do
258
+ described_class.new("W", "1A", "1", "AA").must_be :valid?
259
+ end
260
+ end
261
+
262
+ describe "country" do
263
+ it "should look up the country of a full postcode" do
264
+ described_class.new("EH", "8", "8", "DX").country.must_equal :scotland
265
+ end
266
+
267
+ it "should look up the country of a partial postcode" do
268
+ described_class.new("W", "1A").country.must_equal :england
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,100 @@
1
+ require_relative "./test_helper"
2
+ require "uk_postcode/giro_postcode"
3
+
4
+ describe UKPostcode::GiroPostcode do
5
+ described_class = UKPostcode::GiroPostcode
6
+
7
+ let(:subject) { described_class.instance }
8
+
9
+ describe "parse" do
10
+ it "should parse the canonical form" do
11
+ pc = described_class.parse("GIR 0AA")
12
+ pc.must_be_instance_of described_class
13
+ end
14
+
15
+ it "should parse transcribed 0/O and 1/I" do
16
+ pc = described_class.parse("G1R OAA")
17
+ pc.must_be_instance_of described_class
18
+ end
19
+
20
+ it "should handle extra spaces" do
21
+ pc = described_class.parse(" GIR 0AA ")
22
+ pc.must_be_instance_of described_class
23
+ end
24
+
25
+ it "should handle no spaces" do
26
+ pc = described_class.parse("GIR0AA")
27
+ pc.must_be_instance_of described_class
28
+ end
29
+
30
+ it "should be case-insensitive" do
31
+ pc = described_class.parse("gir 0aa")
32
+ pc.must_be_instance_of described_class
33
+ end
34
+
35
+ it "should return nil if unable to parse" do
36
+ pc = described_class.parse("Can't parse this")
37
+ pc.must_be_nil
38
+ end
39
+ end
40
+
41
+ describe "area" do
42
+ it "should be nil" do
43
+ subject.area.must_be_nil
44
+ end
45
+ end
46
+
47
+ describe "district" do
48
+ it "should be nil" do
49
+ subject.district.must_be_nil
50
+ end
51
+ end
52
+
53
+ describe "sector" do
54
+ it "should be nil" do
55
+ subject.sector.must_be_nil
56
+ end
57
+ end
58
+
59
+ describe "unit" do
60
+ it "should be nil" do
61
+ subject.unit.must_be_nil
62
+ end
63
+ end
64
+
65
+ describe "outcode" do
66
+ it "should be GIR" do
67
+ subject.outcode.must_equal("GIR")
68
+ end
69
+ end
70
+
71
+ describe "incode" do
72
+ it "should be 0AA" do
73
+ subject.incode.must_equal("0AA")
74
+ end
75
+ end
76
+
77
+ describe "to_s" do
78
+ it "should be the canonical form" do
79
+ subject.to_s.must_equal "GIR 0AA"
80
+ end
81
+ end
82
+
83
+ describe "full?" do
84
+ it "should be true" do
85
+ subject.must_be :full?
86
+ end
87
+ end
88
+
89
+ describe "valid?" do
90
+ it "should be true" do
91
+ subject.must_be :valid?
92
+ end
93
+ end
94
+
95
+ describe "country" do
96
+ it "should be England" do
97
+ subject.country.must_equal :england
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,75 @@
1
+ require_relative "./test_helper"
2
+ require "uk_postcode/invalid_postcode"
3
+
4
+ describe UKPostcode::InvalidPostcode do
5
+ described_class = UKPostcode::InvalidPostcode
6
+
7
+ let(:subject) { described_class.new("anything") }
8
+
9
+ describe "parse" do
10
+ it "should parse anything" do
11
+ pc = described_class.parse("Any old junk")
12
+ pc.must_be_instance_of described_class
13
+ end
14
+ end
15
+
16
+ describe "area" do
17
+ it "should be nil" do
18
+ subject.area.must_be_nil
19
+ end
20
+ end
21
+
22
+ describe "district" do
23
+ it "should be nil" do
24
+ subject.district.must_be_nil
25
+ end
26
+ end
27
+
28
+ describe "sector" do
29
+ it "should be nil" do
30
+ subject.sector.must_be_nil
31
+ end
32
+ end
33
+
34
+ describe "unit" do
35
+ it "should be nil" do
36
+ subject.unit.must_be_nil
37
+ end
38
+ end
39
+
40
+ describe "outcode" do
41
+ it "should be nil" do
42
+ subject.outcode.must_be_nil
43
+ end
44
+ end
45
+
46
+ describe "incode" do
47
+ it "should be nil" do
48
+ subject.incode.must_be_nil
49
+ end
50
+ end
51
+
52
+ describe "to_s" do
53
+ it "should return the initialisation string" do
54
+ subject.to_s.must_equal "anything"
55
+ end
56
+ end
57
+
58
+ describe "full?" do
59
+ it "should be false" do
60
+ subject.wont_be :full?
61
+ end
62
+ end
63
+
64
+ describe "valid?" do
65
+ it "should be false" do
66
+ subject.wont_be :valid?
67
+ end
68
+ end
69
+
70
+ describe "country" do
71
+ it "should be unknown" do
72
+ subject.country.must_equal :unknown
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,50 @@
1
+ require_relative "./test_helper"
2
+ require "uk_postcode"
3
+
4
+ describe "Special postcodes" do
5
+ # Special postcodes listed in http://en.wikipedia.org/wiki/UK_postcode
6
+ SPECIAL = %w[
7
+ SW1A 0AA
8
+ SW1A 1AA
9
+ SW1A 2AA
10
+ BS98 1TL
11
+ BX1 1LT
12
+ BX5 5AT
13
+ CF99 1NA
14
+ DH99 1NS
15
+ E16 1XL
16
+ E98 1NW
17
+ E98 1SN
18
+ E98 1ST
19
+ E98 1TT
20
+ EC4Y 0HQ
21
+ EH99 1SP
22
+ EN8 9SL
23
+ G58 1SB
24
+ GIR 0AA
25
+ L30 4GB
26
+ LS98 1FD
27
+ M2 5BE
28
+ N81 1ER
29
+ S2 4SU
30
+ S6 1SW
31
+ SE1 8UJ
32
+ SE9 2UG
33
+ SN38 1NW
34
+ SW1A 0PW
35
+ SW1A 2HQ
36
+ SW1W 0DT
37
+ TS1 3BA
38
+ W1A 1AA
39
+ W1F 9DJ
40
+ ]
41
+
42
+ SPECIAL.each_slice(2) do |outcode, incode|
43
+ it "should correctly handle special postcode #{outcode} #{incode}" do
44
+ postcode = UKPostcode.parse(outcode + incode)
45
+ postcode.wont_be_nil
46
+ postcode.outcode.must_equal outcode
47
+ postcode.incode.must_equal incode
48
+ end
49
+ end
50
+ end