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.
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