Skip to content

Commit eceee23

Browse files
committed
Convert existing private methods to static methods
1 parent 6c3229a commit eceee23

2 files changed

Lines changed: 264 additions & 56 deletions

File tree

lib/index.ts

Lines changed: 147 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,136 @@
1+
interface Validator {
2+
(input: string): boolean;
3+
}
4+
5+
interface Parser {
6+
(postcode: string): string | null;
7+
}
8+
9+
/**
10+
* Return first elem of input is RegExpMatchArray or null if input null
11+
*/
12+
const firstOrNull = (match: RegExpMatchArray | null): string | null => {
13+
if (match === null) return null;
14+
return match[0];
15+
};
16+
17+
const SPACE_REGEX = /\s+/gi;
18+
19+
/**
20+
* Drop all spaces and uppercase
21+
*/
22+
const sanitize = (s: string): string => {
23+
return s.replace(SPACE_REGEX, "").toUpperCase();
24+
};
25+
26+
const matchOn = (s: string, regex: RegExp): RegExpMatchArray | null => {
27+
return sanitize(s).match(regex);
28+
};
29+
30+
const incodeRegex = /\d[a-z]{2}$/i;
31+
const validOutcodeRegex = /^[a-z]{1,2}\d[a-z\d]?$/i;
32+
const districtSplitRegex = /^([a-z]{1,2}\d)([a-z])$/i;
33+
const VALIDATION_REGEX = /^[a-z]{1,2}\d[a-z\d]?\s*\d[a-z]{2}$/i;
34+
35+
/**
36+
* Detects a "valid" postcode
37+
* - Starts and ends on a non-space character
38+
* - Any length of intervening space is allowed
39+
* - Must conform to one of following schemas:
40+
* - AA1A 1AA
41+
* - A1A 1AA
42+
* - A1 1AA
43+
* - A99 9AA
44+
* - AA9 9AA
45+
* - AA99 9AA
46+
*/
47+
const isValid: Validator = postcode => {
48+
return postcode.match(VALIDATION_REGEX) !== null;
49+
};
50+
51+
/**
52+
* Returns a normalised postcode string (i.e. uppercased and properly spaced)
53+
*
54+
* Returns null if invalid postcode
55+
*/
56+
const toNormalised: Parser = postcode => {
57+
const outcode = toOutcode(postcode);
58+
if (outcode === null) return null;
59+
const incode = toIncode(postcode);
60+
if (incode === null) return null;
61+
return `${outcode} ${incode}`;
62+
};
63+
64+
/**
65+
* Returns a correctly formatted outcode given a postcode
66+
*
67+
* Returns null if invalid postcode
68+
*/
69+
const toOutcode: Parser = postcode => {
70+
if (!isValid(postcode)) return null;
71+
return sanitize(postcode).replace(incodeRegex, "");
72+
};
73+
74+
/**
75+
* Returns a correctly formatted incode given a postcode
76+
*
77+
* Returns null if invalid postcode
78+
*/
79+
const toIncode: Parser = postcode => {
80+
if (!isValid(postcode)) return null;
81+
const match = matchOn(postcode, incodeRegex);
82+
return firstOrNull(match);
83+
};
84+
85+
const AREA_REGEX = /^[a-z]{1,2}/i;
86+
87+
/**
88+
* Returns a correctly formatted area given a postcode
89+
*
90+
* Returns null if invalid postcode
91+
*/
92+
const toArea: Parser = postcode => {
93+
if (!isValid(postcode)) return null;
94+
const match = matchOn(postcode, AREA_REGEX);
95+
return firstOrNull(match);
96+
};
97+
98+
/**
99+
* Returns a correctly formatted sector given a postcode
100+
*
101+
* Returns null if invalid postcode
102+
*/
103+
const toSector: Parser = postcode => {
104+
const outcode = toOutcode(postcode);
105+
if (outcode === null) return null;
106+
const incode = toIncode(postcode);
107+
if (incode === null) return null;
108+
return `${outcode} ${incode[0]}`;
109+
};
110+
111+
const UNIT_REGEX = /[a-z]{2}$/i;
112+
113+
/**
114+
* Returns a correctly formatted unit given a postcode
115+
*
116+
* Returns null if invalid postcode
117+
*/
118+
const toUnit: Parser = postcode => {
119+
if (!isValid(postcode)) return null;
120+
const match = matchOn(postcode, UNIT_REGEX);
121+
return firstOrNull(match);
122+
};
123+
124+
/**
125+
* Postcode
126+
*
127+
* This wraps an input postcode string and provides instance methods to
128+
* validate, normalise or extract postcode data.
129+
*
130+
* This API is a bit more cumbersome that it needs to be. You should
131+
* favour `Postcode.parse()` or a static method depending on the
132+
* task at hand.
133+
*/
1134
class Postcode {
2135
private _raw: string;
3136
private _valid: boolean;
@@ -11,9 +144,17 @@ class Postcode {
11144

12145
constructor(postcode: string) {
13146
this._raw = postcode;
14-
this._valid = isValidPostcode(postcode);
147+
this._valid = isValid(postcode);
15148
}
16149

150+
static isValid = isValid;
151+
static toNormalised = toNormalised;
152+
static toOutcode = toOutcode;
153+
static toIncode = toIncode;
154+
static toArea = toArea;
155+
static toSector = toSector;
156+
static toUnit = toUnit;
157+
17158
static validOutcode(outcode: string): boolean {
18159
return outcode.match(validOutcodeRegex) !== null;
19160
}
@@ -25,21 +166,21 @@ class Postcode {
25166
incode(): string | null {
26167
if (!this._valid) return null;
27168
if (this._incode) return this._incode;
28-
this._incode = nullOrUpperCase(parseIncode(this._raw));
169+
this._incode = toIncode(this._raw);
29170
return this._incode;
30171
}
31172

32173
outcode(): string | null {
33174
if (!this._valid) return null;
34175
if (this._outcode) return this._outcode;
35-
this._outcode = nullOrUpperCase(parseOutcode(this._raw));
176+
this._outcode = toOutcode(this._raw);
36177
return this._outcode;
37178
}
38179

39180
area(): string | null {
40181
if (!this._valid) return null;
41182
if (this._area) return this._area;
42-
this._area = nullOrUpperCase(parseArea(this._raw));
183+
this._area = toArea(this._raw);
43184
return this._area;
44185
}
45186

@@ -67,14 +208,14 @@ class Postcode {
67208
if (this._sector) return this._sector;
68209
const normalised = this.normalise();
69210
if (normalised === null) return null;
70-
this._sector = parseSector(normalised);
211+
this._sector = toSector(normalised);
71212
return this._sector;
72213
}
73214

74215
unit(): string | null {
75216
if (!this._valid) return null;
76217
if (this._unit) return this._unit;
77-
this._unit = nullOrUpperCase(parseUnit(this._raw));
218+
this._unit = toUnit(this._raw);
78219
return this._unit;
79220
}
80221

@@ -84,54 +225,4 @@ class Postcode {
84225
}
85226
}
86227

87-
const validationRegex = /^[a-z]{1,2}\d[a-z\d]?\s*\d[a-z]{2}$/i;
88-
const incodeRegex = /\d[a-z]{2}$/i;
89-
const validOutcodeRegex = /^[a-z]{1,2}\d[a-z\d]?$/i;
90-
const areaRegex = /^[a-z]{1,2}/i;
91-
const districtSplitRegex = /^([a-z]{1,2}\d)([a-z])$/i;
92-
const sectorRegex = /^[a-z]{1,2}\d[a-z\d]?\s*\d/i;
93-
const unitRegex = /[a-z]{2}$/i;
94-
95-
interface Validator {
96-
(input: string): boolean;
97-
}
98-
99-
const isValidPostcode: Validator = postcode =>
100-
postcode.match(validationRegex) !== null;
101-
102-
interface Parser {
103-
(postcode: string): string | null;
104-
}
105-
const parseOutcode: Parser = postcode => {
106-
return postcode.replace(incodeRegex, "").replace(/\s+/, "");
107-
};
108-
109-
const parseIncode: Parser = postcode => {
110-
const match = postcode.match(incodeRegex);
111-
if (match === null) return null;
112-
return match[0];
113-
};
114-
115-
const parseArea: Parser = postcode => {
116-
const match = postcode.match(areaRegex);
117-
if (match === null) return null;
118-
return match[0];
119-
};
120-
121-
const parseSector: Parser = postcode => {
122-
const match = postcode.match(sectorRegex);
123-
if (match === null) return null;
124-
return match[0];
125-
};
126-
127-
const parseUnit: Parser = postcode => {
128-
const match = postcode.match(unitRegex);
129-
if (match === null) return null;
130-
return match[0];
131-
};
132-
133-
const nullOrUpperCase = (s: string | null): string | null => {
134-
return s === null ? null : s.toUpperCase();
135-
};
136-
137228
export = Postcode;

test/statics.unit.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { assert } from "chai";
2+
import Postcode from "../lib/index";
3+
import { loadFixtures, TestCase } from "./util/helper";
4+
5+
interface StaticMethod {
6+
(postcode: string): string | null;
7+
}
8+
9+
interface TestMethodOptions {
10+
tests: TestCase[];
11+
method: StaticMethod;
12+
}
13+
14+
const testMethod = (options: TestMethodOptions): void => {
15+
const { tests, method } = options;
16+
tests.forEach(({ base, expected }) => {
17+
const result = method.call(null, base);
18+
assert.equal(result, expected);
19+
});
20+
};
21+
22+
describe("Postcode.isValid", async () => {
23+
it("should return true for postcodes that look correct", async () => {
24+
const { tests } = await loadFixtures("validation.json");
25+
tests.forEach(({ base, expected }) => {
26+
assert.equal(Postcode.isValid(base), Boolean(expected));
27+
});
28+
});
29+
});
30+
31+
describe("Postcode.toNormalised", () => {
32+
it("should correctly normalise postcodes", async () => {
33+
const { tests } = await loadFixtures("normalisation.json");
34+
testMethod({ method: Postcode.toNormalised, tests });
35+
});
36+
37+
it("should return null if invalid postcode", () => {
38+
assert.isNull(Postcode.toNormalised("Definitly bogus"));
39+
});
40+
});
41+
42+
describe("Postcode.toIncode", () => {
43+
it("should correctly parse incodes", async () => {
44+
const { tests } = await loadFixtures("incodes.json");
45+
testMethod({ method: Postcode.toIncode, tests });
46+
});
47+
48+
it("should return null if invalid postcode", () => {
49+
assert.isNull(Postcode.toIncode("Definitly bogus"));
50+
});
51+
});
52+
53+
describe("Postcode.toOutcode", () => {
54+
it("should correctly parse outcodes", async () => {
55+
const { tests } = await loadFixtures("outcodes.json");
56+
testMethod({ method: Postcode.toOutcode, tests });
57+
});
58+
59+
it("should return undefined if invalid postcode", () => {
60+
assert.isNull(Postcode.toOutcode("Definitly bogus"));
61+
});
62+
});
63+
64+
describe("Postcode.toArea", () => {
65+
it("should correctly parse areas", async () => {
66+
const { tests } = await loadFixtures("areas.json");
67+
testMethod({ method: Postcode.toArea, tests });
68+
});
69+
70+
it("should return undefined if invalid postcode", () => {
71+
assert.isNull(Postcode.toArea("Definitely bogus"));
72+
});
73+
});
74+
75+
// describe("Postcode.toDistrict", () => {
76+
// it("should correctly parse districts", async () => {
77+
// const { tests } = await loadFixtures("districts.json");
78+
// testMethod({ method: Postcode.toDistrict, tests });
79+
// });
80+
81+
// it("should return undefined if invalid postcode", () => {
82+
// assert.isNull(Postcode.toDistrict("Definitely bogus"));
83+
// });
84+
// });
85+
86+
// describe("Postcode.toSubDistrict", () => {
87+
// it("should correctly parse sub-districts", async () => {
88+
// const { tests } = await loadFixtures("sub-districts.json");
89+
// testMethod({ method: Postcode.toSubDistrict, tests });
90+
// });
91+
92+
// it("should return undefined if invalid postcode", () => {
93+
// assert.isNull(Postcode.toSubDistrict("Definitely bogus"));
94+
// });
95+
// });
96+
97+
describe("Postcode.toSector", () => {
98+
it("should correctly parse sectors", async () => {
99+
const { tests } = await loadFixtures("sectors.json");
100+
testMethod({ method: Postcode.toSector, tests });
101+
});
102+
103+
it("should return undefined if invalid postcode", () => {
104+
assert.isNull(Postcode.toSector("Definitely bogus"));
105+
});
106+
});
107+
108+
describe("Postcode.toUnit", () => {
109+
it("should correctly parse units", async () => {
110+
const { tests } = await loadFixtures("units.json");
111+
testMethod({ method: Postcode.toUnit, tests });
112+
});
113+
114+
it("should return undefined if invalid postcode", () => {
115+
assert.isNull(Postcode.toUnit("Definitely bogus"));
116+
});
117+
});

0 commit comments

Comments
 (0)