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+ */
1134class 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-
137228export = Postcode ;
0 commit comments