-
Notifications
You must be signed in to change notification settings - Fork 1
/
load-xml.ts
152 lines (123 loc) · 5.11 KB
/
load-xml.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import parser, { ValidationError } from 'fast-xml-parser';
type XMLString = string;
interface Replacement {
regex: RegExp;
replacement: string;
}
interface Road {
startDate: Date;
endDate: Date;
expectedDelay: string;
description: string;
closureType: string;
centreEasting: string;
centreNorthing: string;
roads: string;
}
const cleanup = (description: string) => {
const replacements: Array<Replacement> = [
{ regex: /jct jct/gi, replacement: 'Junction' },
{ regex: /jct/gi, replacement: 'Junction' },
{ regex: /jn/gi, replacement: 'Junction' },
{ regex: /
/g, replacement: '\n' },
{ regex: /
/g, replacement: '\n' },
{ regex: /
/g, replacement: '\n' },
{ regex: /	/g, replacement: '' },
{ regex: /\n\n\n/g, replacement: '\n' },
{ regex: /\n\n/g, replacement: '\n' },
{ regex: /[ \t][ \t][ \t]/g, replacement: ' ' },
{ regex: /[ \t][ \t]/g, replacement: ' ' },
{ regex: /hardshoulder/gi, replacement: 'hard shoulder' },
{ regex: /\s+&\s+/g, replacement: ' and ' },
{ regex: /\s+&\s+/g, replacement: ' and ' },
{ regex: /\s+southbound\s+/g, replacement: ' Southbound ' },
{ regex: /\s+northbound\s+/g, replacement: ' Northbound ' },
{ regex: /\s+eastbound\s+/g, replacement: ' Eastbound ' },
{ regex: /\s+westbound\s+/g, replacement: ' Westbound ' },
{ regex: /\s+SB\s+/gi, replacement: ' Southbound ' },
{ regex: /\s+NB\s+/gi, replacement: ' Northbound ' },
];
return replacements.reduce((desc, { regex, replacement }) => {
return desc.replace(regex, replacement);
}, description);
};
// The wrinkle in the sorting is that there are entries like 'M27 M3' which
// would need to be sorted between 'M27' and 'M27 M271'. There are actually entries
// that have more than two roads, but they tend to be more or less unique.
// Regex for splitting, thus:
// 'M27 M3' -> [matchdata, '27', 'M', '3']
const numRegex: RegExp = /^[AM](\d{1,4})\s?([AM])?(\d{1,4})?/;
const compare = (a: Road, b: Road): number => {
// First of all, Motorways before A-roads
if (a.roads[0] !== b.roads[0]) return a.roads[0] < b.roads[0] ? 1 : -1;
// Both primaries are Motorway or A-road.
const left = a.roads.match(numRegex);
const right = b.roads.match(numRegex);
// If the road number differs, just return the difference.
if (left[1] !== right[1]) return Number(left[1]) - Number(right[1]);
if (left[2] === undefined) {
// No second road left...
if (right[2] === undefined) return 0; // No second road right means they are equal.
return -1; // Left no second, right has, left is less
} else if (right[2] === undefined) return 1; // Left has second, right not, left is more
// Back to A-road vs M-way on secondary
if (left[2] !== right[2]) return left[2] < right[2] ? 1 : -1;
// Both secondary are A-road or M-way, return difference
return Number(left[3]) - Number(right[3]);
};
// Parse the XML and convert the JSON output which retains the multiple
// levels contained in the XML to a flat representation of each roadwork item.
// Then, sort it into the order mentioned above.
const parserOptions = {
attributeNamePrefix: '', // Don't prefix attributes
ignoreAttributes: false, // Collect attributes
ignoreNameSpace: true, // Throw away the namespaces
allowBooleanAttributes: true, // I'm not sure there are any
parseAttributeValue: true, // Parse out attribute values to Number etc
};
interface XMLSingleRoad {
ROAD_NUMBER: string;
}
type XMLRoadList = Array<XMLSingleRoad>;
type XMLRoad = XMLSingleRoad | XMLRoadList;
export const parseRoadworks = (xmlData: XMLString) => {
const jsonData = parser.parse(xmlData, parserOptions);
const rawData =
jsonData.Report.HE_PLANNED_ROADWORKS.HE_PLANNED_WORKS_Collection.HE_PLANNED_WORKS.reduce(
(works: Array<Road>, cur: any) => {
if (
cur.EASTNORTH &&
cur.EASTNORTH.Report &&
cur.EASTNORTH.Report.EASTINGNORTHING // Sometimes (once?) an empty string
) {
const eastNorth = cur.EASTNORTH.Report.EASTINGNORTHING.EASTNORTH_Collection.EASTNORTH;
let road_data: XMLRoad = cur.ROADS.Report.ROADS.ROAD_Collection.ROAD;
// Turn roads into a string if there is more than one road
const roads: string =
(road_data as XMLSingleRoad).ROAD_NUMBER ||
(road_data as XMLRoadList)
.map(({ ROAD_NUMBER }: XMLSingleRoad) => ROAD_NUMBER)
.join(' ');
// Clean up the description
const description = cleanup(cur.DESCRIPTION);
const item: Road = {
startDate: new Date(cur.SDATE),
endDate: new Date(cur.EDATE),
expectedDelay: cur.EXPDEL,
description,
closureType: cur.CLOSURE_TYPE,
centreEasting: eastNorth.CENTRE_EASTING,
centreNorthing: eastNorth.CENTRE_NORTHING,
roads,
};
return [...works, item];
}
return works;
},
[]
);
return rawData.sort(compare); // Sort the roads
};
export const validate = (xmlData: XMLString): true | ValidationError => {
return parser.validate(xmlData);
};