@dra2020/dra-types 1.4.9 → 1.5.11

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.
package/lib/csv.ts ADDED
@@ -0,0 +1,515 @@
1
+ // Public libraries
2
+ import * as Util from '@dra2020/util';
3
+
4
+ // Local library
5
+ import * as VF from './vfeature';
6
+
7
+ // Used internally to index into District Properties Array
8
+ export type BlockMap = { [id: string]: number };
9
+
10
+ // Used more generically and allows string districtIDs
11
+ export type BlockMapping = { [id: string]: string };
12
+
13
+ let reNumeric = /^(\D*)(\d*)(\D*)$/;
14
+ let reDistrictNumber = /^\d+$/;
15
+ let reDistrictNumeric = /^\d/;
16
+
17
+ // Normalize any numeric part to have no padded leading zeros
18
+ export function canonicalDistrictID(districtID: string): string
19
+ {
20
+ let a = reNumeric.exec(districtID);
21
+ if (a && a.length == 4)
22
+ {
23
+ if (a[2].length > 0)
24
+ a[2] = String(Number(a[2]));
25
+ districtID = `${a[1]}${a[2]}${a[3]}`;
26
+ }
27
+ return districtID;
28
+ }
29
+
30
+ // Normalize any numeric part to have four digits with padded leading zeros
31
+ // so alphabetic sorting will result in correct numeric sort for mixed alphanumber
32
+ // district labels.
33
+ export function canonicalSortingDistrictID(districtID: string): string
34
+ {
35
+ let a = reNumeric.exec(districtID);
36
+ if (a && a.length == 4)
37
+ {
38
+ let s = a[2];
39
+ if (s.length > 0)
40
+ {
41
+ switch (s.length)
42
+ {
43
+ case 1: s = `000${s}`; break;
44
+ case 2: s = `00${s}`; break;
45
+ case 3: s = `0${s}`; break;
46
+ }
47
+ a[2] = s;
48
+ }
49
+ districtID = `${a[1]}${a[2]}${a[3]}`;
50
+ }
51
+ return districtID;
52
+ }
53
+
54
+ // Return numeric part of districtID (or -1 if there is none)
55
+ export function canonicalNumericFromDistrictID(districtID: string): number
56
+ {
57
+ let a = reNumeric.exec(districtID);
58
+ if (a && a.length == 4)
59
+ {
60
+ let s = a[2];
61
+ if (s.length > 0)
62
+ return Number(s);
63
+ }
64
+ return -1;
65
+ }
66
+
67
+ export function canonicalDistrictIDFromNumber(districtID: string, n: number): string
68
+ {
69
+ let a = reNumeric.exec(districtID);
70
+ if (a && a.length == 4)
71
+ {
72
+ a[2] = String(n);
73
+ districtID = `${a[1]}${a[2]}${a[3]}`;
74
+ }
75
+ else
76
+ districtID = String(n);
77
+ return districtID;
78
+ }
79
+
80
+ // Numbers start at 1
81
+ export type DistrictOrder = { [districtID: string]: number };
82
+
83
+ // If purely numeric districtIDs and we are missing some number of IDs less than
84
+ export function canonicalDistrictIDGapFill(keys: string[]): string[]
85
+ {
86
+ if (keys == null || keys.length == 0) return keys;
87
+ let nonNumeric = keys.find((s: string) => !reDistrictNumber.test(s)) !== undefined;
88
+ if (nonNumeric) return keys;
89
+ let max = Number(keys[keys.length-1]);
90
+ if (max == keys.length || (max - keys.length) > keys.length) return keys; // no gaps or too many gaps
91
+
92
+ // OK, finally going to fill some gaps
93
+ for (let i: number = 0; i < keys.length; i++)
94
+ {
95
+ let here = Number(keys[i]);
96
+ while (here > i+1)
97
+ {
98
+ keys.splice(i, 0, canonicalSortingDistrictID(String(i+1)));
99
+ i++;
100
+ }
101
+ }
102
+
103
+ return keys;
104
+ }
105
+
106
+ export function canonicalDistrictIDOrdering(order: DistrictOrder): DistrictOrder
107
+ {
108
+ let keys = Object.keys(order);
109
+ let i: number;
110
+ let a: any = [];
111
+ let template: string = undefined;
112
+
113
+ keys = keys.map((s: string) => canonicalSortingDistrictID(s));
114
+ keys = canonicalDistrictIDGapFill(keys);
115
+ keys.sort();
116
+ order = {};
117
+ for (i = 0; i < keys.length; i++)
118
+ order[canonicalDistrictID(keys[i])] = i+1;
119
+
120
+ // Remove water districts
121
+ if (order['ZZZ']) delete order['ZZZ'];
122
+ if (order['ZZ']) delete order['ZZ'];
123
+
124
+ return order;
125
+ }
126
+
127
+ export interface OneCSVLine
128
+ {
129
+ geoid: string;
130
+ districtID: string;
131
+ }
132
+
133
+ let reArray = [
134
+ /^(\d\d[^\s,"']*)[\s]*,[\s]*([^\s'"]+)[\s]*$/,
135
+ /^["'](\d\d[^"']*)["'][\s]*,[\s]*["']([^"']*)["'][\s]*$/,
136
+ /^(\d\d[^\s,]*)[\s]*,[\s]*["']([^"']*)["'][\s]*$/,
137
+ /^["'](\d\d[^"']*)["'][\s]*,[\s]*([^\s]+)[\s]*$/,
138
+ ];
139
+
140
+ export function parseCSVLine(line: string): OneCSVLine
141
+ {
142
+ if (line == null || line == '') return null;
143
+ for (let i: number = 0; i < reArray.length; i++)
144
+ {
145
+ let a = reArray[i].exec(line);
146
+ if (a && a.length === 3)
147
+ return { geoid: a[1], districtID: a[2] };
148
+ }
149
+ return null;
150
+ }
151
+
152
+ export interface ConvertResult
153
+ {
154
+ inBlockMap: BlockMapping;
155
+ inStateMap: BlockMapping;
156
+ outValid: boolean;
157
+ outState: string;
158
+ outMap: BlockMapping;
159
+ outOrder: DistrictOrder;
160
+ outDistrictToSplit: VF.DistrictToSplitBlock;
161
+ }
162
+
163
+ export function blockmapToState(blockMap: BlockMapping): string
164
+ {
165
+ for (var id in blockMap) if (blockMap.hasOwnProperty(id))
166
+ return geoidToState(id);
167
+ return null;
168
+ }
169
+
170
+ // blockToVTD:
171
+ // Take BlockMapping (simple map of GEOID to districtID) and a per-state map of block-level GEOID to VTD
172
+ // and return the output mapping of VTD to districtID, as well a data structure that describes any VTD's
173
+ // that need to be split between districtIDs. Also returns the DistrictOrder structure that defines the
174
+ // districtIDs that were used by the file.
175
+ //
176
+ // The state (as specified by the first two digits of the GEOID) is also determined. If the GEOID's do
177
+ // not all specify the same state, the mapping is considered invalid and the outValid flag is set to false.
178
+ //
179
+
180
+ export function blockmapToVTDmap(blockMap: BlockMapping, stateMap: BlockMapping): ConvertResult
181
+ {
182
+ let res: ConvertResult = {
183
+ inBlockMap: blockMap,
184
+ inStateMap: stateMap,
185
+ outValid: true,
186
+ outState: null,
187
+ outMap: {},
188
+ outOrder: {},
189
+ outDistrictToSplit: {}
190
+ };
191
+
192
+ let bmGather: { [geoid: string]: { [district: string]: { [blockid: string]: boolean } } } = {};
193
+ let revMap: BlockMapping = {};
194
+ let id: string;
195
+
196
+ if (stateMap)
197
+ for (id in stateMap) if (stateMap.hasOwnProperty(id))
198
+ revMap[stateMap[id]] = null;
199
+
200
+ // First aggregate into features across all the blocks
201
+ for (id in blockMap) if (blockMap.hasOwnProperty(id))
202
+ {
203
+ let state = geoidToState(id);
204
+ if (res.outState == null)
205
+ res.outState = state;
206
+ else if (res.outState !== state)
207
+ {
208
+ res.outValid = false;
209
+ break;
210
+ }
211
+
212
+ let districtID: string = canonicalDistrictID(blockMap[id]);
213
+
214
+ // Just ignore ZZZ (water) blocks
215
+ if (districtID === 'ZZZ')
216
+ continue;
217
+
218
+ let n: number = id.length;
219
+ let geoid: string;
220
+
221
+ // Simple test for block id (vs. voting district or block group) id
222
+ if (n >= 15)
223
+ {
224
+ if (stateMap && stateMap[id] !== undefined)
225
+ geoid = stateMap[id];
226
+ else
227
+ {
228
+ geoid = id.substr(0, 12); // heuristic for mapping blockID to blockgroupID
229
+ if (revMap[geoid] === undefined)
230
+ {
231
+ res.outValid = false;
232
+ break;
233
+ }
234
+ }
235
+ }
236
+ else
237
+ geoid = id;
238
+
239
+ if (res.outOrder[districtID] === undefined)
240
+ res.outOrder[districtID] = 0;
241
+
242
+ let districtToBlocks: { [districtID: string]: { [blockid: string]: boolean } } = bmGather[geoid];
243
+ if (districtToBlocks === undefined)
244
+ bmGather[geoid] = { [districtID]: { [id]: true } };
245
+ else
246
+ {
247
+ let thisDistrict: { [blockid: string]: boolean } = districtToBlocks[districtID];
248
+ if (thisDistrict === undefined)
249
+ {
250
+ thisDistrict = { };
251
+ districtToBlocks[districtID] = thisDistrict;
252
+ }
253
+ thisDistrict[id] = true;
254
+ }
255
+ }
256
+
257
+ // Now determine actual mapping of blocks to features, looking for split features
258
+ for (let geoid in bmGather) if (bmGather.hasOwnProperty(geoid))
259
+ {
260
+ let districtToBlocks = bmGather[geoid];
261
+ if (Util.countKeys(districtToBlocks) == 1)
262
+ {
263
+ res.outMap[geoid] = Util.nthKey(districtToBlocks);
264
+ }
265
+ else
266
+ {
267
+ for (let districtID in districtToBlocks) if (districtToBlocks.hasOwnProperty(districtID))
268
+ {
269
+ let split: VF.SplitBlock = { state: '', datasource: '', geoid: geoid, blocks: Object.keys(districtToBlocks[districtID]) };
270
+ let splits = res.outDistrictToSplit[districtID];
271
+ if (splits === undefined)
272
+ {
273
+ splits = [];
274
+ res.outDistrictToSplit[districtID] = splits;
275
+ }
276
+ splits.push(split);
277
+ }
278
+ }
279
+ }
280
+
281
+ res.outOrder = canonicalDistrictIDOrdering(res.outOrder);
282
+
283
+ return res;
284
+ }
285
+
286
+ export const GEOIDToState: any = {
287
+ '01': 'AL',
288
+ '02': 'AK',
289
+ '04': 'AZ',
290
+ '05': 'AR',
291
+ '06': 'CA',
292
+ '08': 'CO',
293
+ '09': 'CT',
294
+ '10': 'DE',
295
+ '12': 'FL',
296
+ '13': 'GA',
297
+ '15': 'HI',
298
+ '16': 'ID',
299
+ '17': 'IL',
300
+ '18': 'IN',
301
+ '19': 'IA',
302
+ '20': 'KS',
303
+ '21': 'KY',
304
+ '22': 'LA',
305
+ '23': 'ME',
306
+ '24': 'MD',
307
+ '25': 'MA',
308
+ '26': 'MI',
309
+ '27': 'MN',
310
+ '28': 'MS',
311
+ '29': 'MO',
312
+ '30': 'MT',
313
+ '31': 'NE',
314
+ '32': 'NV',
315
+ '33': 'NH',
316
+ '34': 'NJ',
317
+ '35': 'NM',
318
+ '36': 'NY',
319
+ '37': 'NC',
320
+ '38': 'ND',
321
+ '39': 'OH',
322
+ '40': 'OK',
323
+ '41': 'OR',
324
+ '42': 'PA',
325
+ '44': 'RI',
326
+ '45': 'SC',
327
+ '46': 'SD',
328
+ '47': 'TN',
329
+ '48': 'TX',
330
+ '49': 'UT',
331
+ '50': 'VT',
332
+ '51': 'VA',
333
+ '53': 'WA',
334
+ '54': 'WV',
335
+ '55': 'WI',
336
+ '56': 'WY',
337
+ };
338
+
339
+ export const StateToGEOID: any = {
340
+ 'AL': '01',
341
+ 'AK': '02',
342
+ 'AZ': '04',
343
+ 'AR': '05',
344
+ 'CA': '06',
345
+ 'CO': '08',
346
+ 'CT': '09',
347
+ 'DE': '10',
348
+ 'FL': '12',
349
+ 'GA': '13',
350
+ 'HI': '15',
351
+ 'ID': '16',
352
+ 'IL': '17',
353
+ 'IN': '18',
354
+ 'IA': '19',
355
+ 'KS': '20',
356
+ 'KY': '21',
357
+ 'LA': '22',
358
+ 'ME': '23',
359
+ 'MD': '24',
360
+ 'MA': '25',
361
+ 'MI': '26',
362
+ 'MN': '27',
363
+ 'MS': '28',
364
+ 'MO': '29',
365
+ 'MT': '30',
366
+ 'NE': '31',
367
+ 'NV': '32',
368
+ 'NH': '33',
369
+ 'NJ': '34',
370
+ 'NM': '35',
371
+ 'NY': '36',
372
+ 'NC': '37',
373
+ 'ND': '38',
374
+ 'OH': '39',
375
+ 'OK': '40',
376
+ 'OR': '41',
377
+ 'PA': '42',
378
+ 'RI': '44',
379
+ 'SC': '45',
380
+ 'SD': '46',
381
+ 'TN': '47',
382
+ 'TX': '48',
383
+ 'UT': '49',
384
+ 'VT': '50',
385
+ 'VA': '51',
386
+ 'WA': '53',
387
+ 'WV': '54',
388
+ 'WI': '55',
389
+ 'WY': '56',
390
+ };
391
+
392
+ export function geoidToState(geoid: string): string
393
+ {
394
+ let re = /^(..).*$/;
395
+
396
+ let a = re.exec(geoid);
397
+ if (a == null || a.length != 2) return null;
398
+ return GEOIDToState[a[1]];
399
+ }
400
+
401
+ export type StateUrls = (
402
+ 'alabama' |
403
+ 'alaska' |
404
+ 'arizona' |
405
+ 'arkansas' |
406
+ 'california' |
407
+ 'colorado' |
408
+ 'connecticut' |
409
+ 'delaware' |
410
+ 'florida' |
411
+ 'georgia' |
412
+ 'hawaii' |
413
+ 'idaho' |
414
+ 'illinois' |
415
+ 'indiana' |
416
+ 'iowa' |
417
+ 'kansas' |
418
+ 'kentucky' |
419
+ 'louisiana' |
420
+ 'maine' |
421
+ 'maryland' |
422
+ 'massachusetts' |
423
+ 'michigan' |
424
+ 'minnesota' |
425
+ 'mississippi' |
426
+ 'missouri' |
427
+ 'montana' |
428
+ 'nebraska' |
429
+ 'nevada' |
430
+ 'new-hampshire' |
431
+ 'new-jersey' |
432
+ 'new-mexico' |
433
+ 'new-york' |
434
+ 'north-carolina' |
435
+ 'north-dakota' |
436
+ 'ohio' |
437
+ 'oklahoma' |
438
+ 'oregon' |
439
+ 'pennsylvania' |
440
+ 'rhode-island' |
441
+ 'south-carolina' |
442
+ 'south-dakota' |
443
+ 'tennessee' |
444
+ 'texas' |
445
+ 'utah' |
446
+ 'vermont' |
447
+ 'virginia' |
448
+ 'washington' |
449
+ 'west-virginia' |
450
+ 'wisconsin' |
451
+ 'wyoming'
452
+ );
453
+
454
+ export type ValidStateUrlsType =
455
+ {
456
+ readonly [stateUrl in StateUrls]: boolean;
457
+ };
458
+
459
+ const ValidStateUrls: ValidStateUrlsType = {
460
+ 'alabama': true,
461
+ 'alaska': true,
462
+ 'arizona': true,
463
+ 'arkansas': true,
464
+ 'california': true,
465
+ 'colorado': true,
466
+ 'connecticut': true,
467
+ 'delaware': true,
468
+ 'florida': true,
469
+ 'georgia': true,
470
+ 'hawaii': true,
471
+ 'idaho': true,
472
+ 'illinois': true,
473
+ 'indiana': true,
474
+ 'iowa': true,
475
+ 'kansas': true,
476
+ 'kentucky': true,
477
+ 'louisiana': true,
478
+ 'maine': true,
479
+ 'maryland': true,
480
+ 'massachusetts': true,
481
+ 'michigan': true,
482
+ 'minnesota': true,
483
+ 'mississippi': true,
484
+ 'missouri': true,
485
+ 'montana': true,
486
+ 'nebraska': true,
487
+ 'nevada': true,
488
+ 'new-hampshire': true,
489
+ 'new-jersey': true,
490
+ 'new-mexico': true,
491
+ 'new-york': true,
492
+ 'north-carolina': true,
493
+ 'north-dakota': true,
494
+ 'ohio': true,
495
+ 'oklahoma': true,
496
+ 'oregon': true,
497
+ 'pennsylvania': true,
498
+ 'rhode-island': true,
499
+ 'south-carolina': true,
500
+ 'south-dakota': true,
501
+ 'tennessee': true,
502
+ 'texas': true,
503
+ 'utah': true,
504
+ 'vermont': true,
505
+ 'virginia': true,
506
+ 'washington': true,
507
+ 'west-virginia': true,
508
+ 'wisconsin': true,
509
+ 'wyoming': true,
510
+ };
511
+
512
+ export function isStateUrl(s: any): s is StateUrls
513
+ {
514
+ return (typeof s === 'string' && s in ValidStateUrls);
515
+ }