Skip to content

Commit de1ed11

Browse files
committed
feat(fix): Fix common errors
`fix` Attempts to correct and clean up a postcode without validating by replacing commonly misplaced characters (e.g. mixing up `0` and `"O"`, `1` and `"I"`). This method will also uppercase and fix spacing. The original input is returned if it cannot be reliably fixed.
1 parent 4ecba1e commit de1ed11

File tree

4 files changed

+171
-3
lines changed

4 files changed

+171
-3
lines changed

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ isValid("AA1 1AB"); // => true
6060

6161
Pass a string to `parse`. This will return a valid or invalid postcode instance which can be easily destructured.
6262

63-
#### Valid Postcode
63+
#### Valid Postcode
6464

6565
`ValidPostcode` type definition
6666

@@ -80,7 +80,7 @@ const {
8080
} = parse("Sw1A 2aa");
8181
```
8282

83-
#### Invalid Postcode
83+
#### Invalid Postcode
8484

8585
`InvalidPostcode` type definition
8686

@@ -163,6 +163,33 @@ toSector("Sw1A 2aa"); // => "SW1A 2"
163163
toUnit("Sw1A 2aa"); // => "AA"
164164
```
165165

166+
#### Fix
167+
168+
`fix` Attempts to correct and clean up a postcode without validating by replacing commonly misplaced characters (e.g. mixing up `0` and `"O"`, `1` and `"I"`). This method will also uppercase and fix spacing. The original input is returned if it cannot be reliably fixed.
169+
170+
```javascript
171+
fix("SWIA 2AA") => "SW1A 2AA" // Corrects I to 1
172+
fix("SW1A 21A") => "SW1A 2IA" // Corrects 1 to I
173+
fix("SW1A OAA") => "SW1A 0AA" // Corrects O to 0
174+
fix("SW1A 20A") => "SW1A 2OA" // Corrects 0 to O
175+
176+
// Other effects
177+
fix(" SW1A 2AO") => "SW1A 2AO" // Properly spaces
178+
fix("SW1A 2A0") => "SW1A 2AO" // 0 is coerced into "0"
179+
```
180+
181+
Aims to be used in conjunction with parse to make postcode entry more forgiving:
182+
183+
```javascript
184+
const { inward } = parse(fix("SW1A 2A0")); // inward = "2AO"
185+
```
186+
187+
If the input is not deemed fixable, the original string will be returned
188+
189+
```javascript
190+
fix("12a") => "12a"
191+
```
192+
166193
#### Extract & Replace
167194

168195
`match`. Retrieve valid postcodes in a body of text

lib/index.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,3 +357,73 @@ export const replace = (corpus: string, replaceWith = ""): ReplaceResult => ({
357357
match: match(corpus),
358358
result: corpus.replace(POSTCODE_CORPUS_REGEX, replaceWith),
359359
});
360+
361+
export const FIXABLE_REGEX = /^\s*[a-z01]{1,2}[0-9oi][a-z\d]?\s*[0-9oi][a-z01]{2}\s*$/i;
362+
363+
/**
364+
* Attempts to fix and clean a postcode. Specifically:
365+
* - Performs character conversion on obviously wrong and commonly mixed up letters (e.g. O => 0 and vice versa)
366+
* - Trims string
367+
* - Properly adds space between outward and inward codes
368+
*
369+
* If the postcode cannot be coerced into a valid format, the original string is returned
370+
*
371+
* @example
372+
* ```javascript
373+
* fix(" SW1A 2AO") => "SW1A 2AO" // Properly spaces
374+
* fix("SW1A 2A0") => "SW1A 2AO" // 0 is coerced into "0"
375+
* ```
376+
*
377+
* Aims to be used in conjunction with parse to make postcode entry more forgiving:
378+
*
379+
* @example
380+
* ```javascript
381+
* const { inward } = parse(fix("SW1A 2A0")); // inward = "2AO"
382+
* ```
383+
*/
384+
export const fix = (s: string): string => {
385+
const match = s.match(FIXABLE_REGEX);
386+
if (match === null) return s;
387+
s = s.toUpperCase().trim().replace(/\s+/gi, "");
388+
const l = s.length;
389+
const inward = s.slice(l - 3, l);
390+
return `${coerceOutcode(s.slice(0, l - 3))} ${coerce("NLL", inward)}`;
391+
};
392+
393+
const toLetter: Record<string, string> = {
394+
"0": "O",
395+
"1": "I",
396+
};
397+
398+
const toNumber: Record<string, string> = {
399+
O: "0",
400+
I: "1",
401+
};
402+
403+
const coerceOutcode = (i: string): string => {
404+
if (i.length === 2) return coerce("LN", i);
405+
if (i.length === 3) return coerce("L??", i);
406+
if (i.length === 4) return coerce("LLN?", i);
407+
return i;
408+
};
409+
410+
/**
411+
* Given a pattern of letters, numbers and unknowns represented as a sequence
412+
* of L, Ns and ? respectively; coerce them into the correct type given a
413+
* mapping of potentially confused letters
414+
*
415+
* @hidden
416+
*
417+
* @example coerce("LLN", "0O8") => "OO8"
418+
*/
419+
const coerce = (pattern: string, input: string): string =>
420+
input
421+
.split("")
422+
.reduce<string[]>((acc, c, i) => {
423+
const target = pattern.charAt(i);
424+
if (target === "N") acc.push(toNumber[c] || c);
425+
if (target === "L") acc.push(toLetter[c] || c);
426+
if (target === "?") acc.push(c);
427+
return acc;
428+
}, [])
429+
.join("");

test/exhaustive_unit.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import axios from "axios";
55

66
const TIMEOUT = 60000;
77

8-
import { parse } from "../lib/index";
8+
import { parse, fix } from "../lib/index";
99

1010
const url = "https://data.ideal-postcodes.co.uk/postcodes.csv";
1111

@@ -24,6 +24,13 @@ describe("Exhaustive postcode test", () => {
2424
.filter((p: string) => p !== "GIR 0AA");
2525
});
2626

27+
describe("fix", () => {
28+
it("never corrects a valid postcode", function () {
29+
this.timeout(TIMEOUT);
30+
postcodes.forEach((p) => assert.equal(fix(p), p));
31+
});
32+
});
33+
2734
describe(".valid", () => {
2835
it("should all be valid", function () {
2936
this.timeout(TIMEOUT);

test/fix.unit.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { assert } from "chai";
2+
import { fix } from "../lib/index";
3+
4+
describe("fix", () => {
5+
it("trims postcode", () => {
6+
assert.equal(fix(" SW1A 2AA "), "SW1A 2AA");
7+
});
8+
9+
it("upper cases string", () => {
10+
assert.equal(fix(" Sw1A 2aa "), "SW1A 2AA");
11+
});
12+
13+
it("fixes spacing", () => {
14+
assert.equal(fix(" Sw1A2aa "), "SW1A 2AA");
15+
assert.equal(fix(" Sw1A 2aa "), "SW1A 2AA");
16+
});
17+
18+
it("returns original string if not fixable", () => {
19+
assert.equal(fix(" 1A2aa "), " 1A2aa ");
20+
});
21+
22+
describe("outward code", () => {
23+
it("fixes LN format", () => {
24+
assert.equal(fix("01 OAA"), "O1 0AA");
25+
assert.equal(fix("SO OAA"), "S0 0AA");
26+
});
27+
28+
it("fixes L?? format", () => {
29+
assert.equal(fix("0W1 OAA"), "OW1 0AA");
30+
31+
// Too ambiguous
32+
assert.equal(fix("S01 OAA"), "S01 0AA");
33+
assert.equal(fix("SO1 OAA"), "SO1 0AA");
34+
assert.equal(fix("SWO OAA"), "SWO 0AA");
35+
assert.equal(fix("SW0 OAA"), "SW0 0AA");
36+
});
37+
38+
it("fixes LLN? format", () => {
39+
assert.equal(fix("0W1A OAA"), "OW1A 0AA");
40+
assert.equal(fix("S01A OAA"), "SO1A 0AA");
41+
assert.equal(fix("SWOA OAA"), "SW0A 0AA");
42+
// Ambiguous
43+
assert.equal(fix("SW10 OAA"), "SW10 0AA");
44+
assert.equal(fix("SW1O OAA"), "SW1O 0AA");
45+
});
46+
});
47+
48+
describe("inward code", () => {
49+
it("coerces first character", () => {
50+
assert.equal(fix(" SW1A OAA"), "SW1A 0AA");
51+
});
52+
it("coerces second character", () => {
53+
assert.equal(fix("SW1A 20A"), "SW1A 2OA");
54+
});
55+
it("coerces last character", () => {
56+
assert.equal(fix("SW1A 2A0"), "SW1A 2AO");
57+
});
58+
});
59+
60+
it("fixes 1 <=> I", () => {
61+
assert.equal(fix("SWIA 2AA"), "SW1A 2AA");
62+
assert.equal(fix("1W1A 2AA"), "IW1A 2AA");
63+
});
64+
});

0 commit comments

Comments
 (0)