-
Notifications
You must be signed in to change notification settings - Fork 0
/
util.js
397 lines (395 loc) · 15.9 KB
/
util.js
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
/**
* @namespace util
*/
/**
* Parses csv into a 2-dimensional array. By default, also trims rows and columns left by trailing delimiters.
* @param {string} csvtext - A csv string
* @param {string} [delim='","'] - The value delimiter
* @param {string} [row_delim='\n'] - The row delimiter
* @param {boolean} [trim=true] - Trims empty trailing rows and empty trailing elements in rows
* @returns {Array} - A 2-dimensional array, with each sub-array representing rows.
* @memberof util
*/
function csvArray(csvtext, delim=',',rowdelim='\n',trim=true) {
let arr = csvtext.split(rowdelim); // split on newline or other row delimiter
arr = arr.map( (row) => {
let r = row.split(delim); // split each row into columns
return r;
});
if(trim == true) {
if(csvtext[csvtext.length-1]==rowdelim) {
arr = arr.slice(0,arr.length-1); // trim off bottom row if csv ends in a row delimiter
}
let sum = 1;
arr.forEach( (row)=> sum+= row.length);
let avg = sum/arr.length-1;
arr = arr.map( (row) => row.filter( (el,ind)=> ind<avg )) //trim cols past avg max index
}
return arr;
}
/**
* Chops a row from an array, non mutating.
* @param {Array[]} arr - the 2-dimensional array to operate on
* @param {(number | number[] | RegExp) } find The row index to remove, an array of row indexes to remove, or a regular expression. If a regular expression is passed, all rows that match the regex will be removed. Numbers may be negative to operate from the end.
* @param {number} [regindex=0] The **column** index to search when using regular expressions. Defaults to the first column, index 0, as the typical use case would be to remove rows corresponding to unwanted data categories. If set to -1, chop will search the entire array for the regex, and whenever it finds a match, it will delete the entire row on which it was found.
* @param {boolean} [keepFirst=false] If set to true, Chop will ignore the first row of the .csv, which is often the header row when doing RegEx based searches.
* @returns {Array} - A 2-dimensional array, possibly with some rows removed.
* @memberof util
*/
function chop(arr, find, regIndex = 0, keepFirst=false) {
if(typeof find == 'number') { //remove 1 row
//remove a single row
if(find < 0) {
find = arr.length + find-1;
}
if(find < 0 || find >= arr.length) {
return arr; //index out of bounds
}
let arrFront = arr.slice(0,find);
let arrBack = arr.slice(find+1, arr.length);
return [...arrFront,...arrBack];
}
else if(Array.isArray(find)) { //remove all rows in array
let header = null;
if(keepFirst) {
header = deepCopySimple(arr[0]);
}
find = find.filter( (el)=> typeof el == 'number').sort( (a,b)=> b-a); //remove numbers, sort DESC
find.forEach( (n) => { arr = chop(arr, n)}) //recursion!
if(keepFirst && find[find.length-1] == 0) {
arr.unshift(header);
}
return arr;
}
else if(find instanceof RegExp) { //remove all rows that match regex
let matchrows = [];
if(regIndex < 0) { // match all elements based on regex
arr.forEach( (row,rowind) => {
for(let i = 0; i < row.length; i++) {
if(find.test(row[i])) {
matchrows.push(rowind);
break;
}
}
})
}
else {
arr.forEach( (row, ind) => {
if(find.test(row[regIndex])) { //match only regindex col based on regex
matchrows.push(ind);
}
});
}
return chop(arr, matchrows, regIndex, keepFirst)
}
return arr; //if wrong find type was passed, just give back the array
}
/**
* Chops a column from an array, non mutating.
* @param {*} arr A 2 dimensional array to operate on. Rows should be **equal length**
* @param {(number | number[] | RegExp)} find The column index to remove, an array of column indexes to remove, or a regular expression. If a regular expression is passed _all_ columns that match on the RegIndex row will be removed. Negative numbers will operate from the last column backwards.
* @param {number} [regIndex=0] The **row** index to search when using regular expressions as the find parameter. Defaults to first row, index 0, as the typical use case would be to remove columns representing unwanted data, and column headers are usually located at row 0. If set to -1, chopColumn will search the entire array for a regex, and whenever it finds a match, it will delete the entire column on which it was found.
* @param {boolean} [keepFirst=false] If set to true, chopColumn will ignore the first column of the .csv, which is often the header row when doing RegEx based searches.
* @returns {Array} - A 2-dimensional array with the indicated columns removed
* @memberof util
*/
function chopColumn(arr, find, regIndex = 0, keepFirst = false) {
let height = arr.length;
let width = arr[0].length;
let newArr = [];
if(typeof find == 'number') { //remove 1 column
if(find < 0) { //align index
find = arr[0].length + find-1;
}
if(find < 0 || find>= arr[0].length) {
return arr; //index out of bounds
}
for(let i = 0; i<height; i++) {
let newrow = [];
for(let j = 0; j<width; j++) {
if(j != find) {
newrow.push(arr[i][j]);
}
}
newArr.push(newrow);
}
}
else if(find instanceof Array) { //remove all columns in find array
newArr = arr;
find = find.filter( (el)=> typeof el == 'number').sort( (a,b)=> b-a); //remove numbers, sort DESC
if(keepFirst && find[find.length-1] == 0 ) {
find.pop();
}
find.forEach( (n) => {newArr = chopColumn(newArr,n,regIndex)})
}
else if(find instanceof RegExp) { //remove all columns that match regex
let matchcols = [];
if(regIndex < 0) {
arr.forEach( (row) => {
row.forEach( (el, index)=> {
if(find.test(el)) {
matchcols.push(index);
}
})
})
}
else {
arr[regIndex].forEach( (el,index) => {
if(find.test(el)) {
matchcols.push(index);
}
})
}
newArr = arr;
newArr = chopColumn(newArr,matchcols,regIndex,keepFirst);
}
return newArr;
}
/**
* Clears quotations or another character from elements in a 2D array or portion of that array.
* @param {Array[]} arr - The 2-dimensional array to operate on.
* @param {(string | string[] | RegExp | RegExp[])} [find='"'] - The string, array of strings, or regular expression to remove.
* @param {number} [rowInd=-1] - The row to operate on. If -1 (default), it will operate on the entire 2D array.
* @param {number} [colInd=-1] - The column to operate on. If -1 (default), it will operate on the entire 2D array.
* @memberof util
*/
function clear(arr, find='"', rowInd=-1, colInd=-1) {
arr = deepCopySimple(arr);
if(typeof find == "string") {
find = new RegExp(find, 'g'); //when passed a string, all instances of that string will be replaced, so global flag is set.
}
else if(find instanceof Array) {
find.forEach( (el) => {
arr = clear(arr, el, rowInd, colInd);
})
return arr;
}
if(rowInd < 0) {
arr = arr.map( (row) => mapRow(row, find, colInd) );
}
else if(rowInd < arr.length) {
arr[rowInd] = mapRow(arr[rowInd], find, colInd)
}
return arr;
function mapRow(row, find, colInd) {
if(colInd < 0) {
return row.map( (el) => {
if(typeof el == "string") {
return el.replace(find,'');
}
else{
return el;
}
});
}
else if(colInd < row.length) { //replace only at set column
row[colInd] = row[colInd].replace(find,'');
return row;
}
else {
return row;
}
}
}
/**
* Coverts an element or series of elements in a 2D array to an array by splitting it on a regular expression. Useful for pre-processing before calling chain, which will map an array to nested object properties.
* @param {Array[]} arr - The 2-dimensional array to operate on.
* @param {(string | string[] | RegExp | RegExp[])} [find='!!'] - The string, array of strings, or regular expression to split values on. Defaults to !!, which is used by census data, such as POPULATION!!15 AND OLDER!!, which will becomes [POPULATION,15 AND OLDER]. Note: If passing multiple regexes, all flags will be ignored and they will be set to global. If matching for certain specific characters, like '.', a regex must be used.
* @param {number} [rowInd=-1] - The row index to operate on, default -1, which operates on all.
* @param {number} [colInd=0] - The column index to operate on, default 0. -1 operates on all.
* @todo implement 1 el array options
* @memberof util
*/
function toArray(arr2d, find='!!', rowInd=-1, colInd=0) {
let arr = deepCopySimple(arr2d); //don't mutate original array.
if(typeof find == "string") {
find = new RegExp(find,"g");
}
else if(find instanceof Array) { //join as regex
let finds = []
find.forEach( (el) => {
if(el instanceof RegExp) {
finds.push(el.source);
}
else if(typeof el == "string") {
finds.push(el);
}
})
find = new RegExp( finds.join('|'),'g');
}
if(rowInd < 0) {
arr = arr.map( (row) => {
return splitRow(row, find, colInd)
}); //do for all rows
}
else if(rowInd < arr.length) {
arr[rowInd] = splitRow(arr[rowInd], find, colInd);
}
return arr;
function splitRow(row, find, colInd) {
if(colInd < 0) {
return row.map( (el) => {
return el.split(find)
});
}
else if(colInd < row.length) {
row[colInd] = row[colInd].split(find);
return row;
}
return row;
}
}
/**
* Chains an array into nested object/properties and sets the final property to the value given.
* @param {(string[]|string)} arr1d - an array of strings to be chained into properties
* @param {*} val - the value to set the final object
* @param {obj} [obj={}] - the parent object on which to perform the chaining
* @param {boolean} [mutate=true] - controls whether a semi-deep copy of the object is made before chaining
* @returns - the object with the new properties chain.
* @memberof util
*/
function chainSingle(arr1d, val, obj={}, mutate=true) {
if(!mutate) {obj=deepCopySimple(obj);} //copy the object if we aren't supposed to mutate
let prop = (arr1d instanceof Array) ? arr1d[0] : arr1d;
let hasAlreadyProp = obj.hasOwnProperty(prop);
let hasAlreadyFinalProp = hasAlreadyProp && !(obj[prop] instanceof Object);
let haveMorePropsToAssign = arr1d.length > 1 && typeof arr1d != "string";
if(hasAlreadyProp) {
if(hasAlreadyFinalProp) {
if(haveMorePropsToAssign) { //duplicate and move old prop down - it must be a total type prop
let oldval = obj[prop];
obj[prop] = {};
obj[prop][prop] = oldval;
arr1d = arr1d.slice(1);
obj[prop] = chainSingle(arr1d,val,obj[prop],mutate)
}
else { //reassign if duplicate mapping
obj[prop] = val;
}
}
else{ //move into without re-assigning if we have more to go down
arr1d = arr1d.slice(1);
obj[prop] = chainSingle(arr1d,val,obj[prop],mutate);
}
}
else {
if(haveMorePropsToAssign) { //create new sub-obj if it doesnt exist and we have more to assign, and recurse
arr1d = arr1d.slice(1);
obj[prop] = {};
obj[prop] = chainSingle(arr1d,val,obj[prop],mutate)
}
else { //if we have no more props to assign, assign here
obj[prop] = val;
}
}
return obj;
}
/**
* Creates nested properties in an object based on an array and assigns a value to the last property in the chain.
* @param {Array[]} arr2d - A 2 dimensional array aligned with the vals array. The nested arrays contain strings representing properties to chain.
* @param {Array} vals - A 1 dimensional array aliged with arr1d where vals[x] is the value to be chained with arr1d[x]
* @param {Object} [parentobj={}] - the parent object to mutate
* @param {boolean} [mutate=true] - controls whether a semi-deep copy of parentobj is made before chaining.
* @returns {Object}- the parent object with the new properties chain, or a semi-deep clone of the parent object with the new properties chain if mutate is set to true.
* @memberof util
*/
function chainMultiple(arr2d, vals, parentobj={}, mutate=true) {
if(!mutate) {
parentobj = deepCopySimple(parentobj);
}
arr2d.forEach( (subArray, iteration) => {
parentobj = chainSingle(subArray, vals[iteration], parentobj, mutate)
})
return parentobj;
}
/**
* Gets a column from a 2D array as an array.
* @param {*} arr2d The array to operate on
* @param {*} colIndex The index of the column to get
* @returns {Array[]} An array of the values in the column.
* @memberof util
*/
function getColumn(arr2d, colIndex) {
let arr = [];
arr2d.forEach( (row) => {
arr.push(row[colIndex]);
})
return arr;
}
/**
* Transposes an array so columns become rows
* @param {Array[]} arr2d A 2D array to transpose. Rows should be equal length.
* @returns {Array[]} A transposed array
* @memberof util
*/
function transpose(arr2d) {
let newArr = [];
let startingWidth = arr2d[0].length;
let startingHeight = arr2d.length;
for(let i = 0; i < startingWidth; i++) {
let newrow = [];
for(let j = 0; j < startingHeight; j++) {
newrow.push(arr2d[j][i]);
}
newArr.push(newrow);
}
return newArr;
}
/**
* Goes through arrays and sub-arrays and converts any numeric strings to numbers that it finds
* @param {Array} arr An array of any dimension; numerify will recursively move through sub arrays.
* @returns {Array} An array with numeric strings converted to numbers.
* @memberof util
*/
function numerify(arr) {
return arr.map( (el) => {
if(el instanceof Array) {
return numerify(el);
}
else if(+el){
return +el;
}
else {
return el;
}
})
}
/**
* Uses JSON.parse/stringify to deep-copy an array.
* @private
* @param {(object | array)} obj - The object or array to copy. Should not have nested complex types.
* @returns A clone of the object array
* @memberof util
*/
function deepCopySimple(obj) {
return JSON.parse(JSON.stringify(obj));
}
/**
* Converts array to a CSV style string
* @param {Array[]} arr the array to convert to a string
* @param {string} [colDelim=','] the string to delimit columns
* @param {string} [rowDelim='\n'] the string to delimit rows
* @todo write unit tests?
* @returns {string} a csv-style string
* @memberof util
*/
function convertArrToCSV(arr,colDelim=',',rowDelim='\n') {
let newstring = '';
arr.forEach( (row,iteration) => {
let partialstring = row.join(colDelim);
newstring += partialstring;
newstring += (iteration <= arr.length-1) ? rowDelim : ''
})
return newstring;
}
exports.convertArrToCSV = convertArrToCSV;
exports.chainMultiple = chainMultiple;
exports.csvArray = csvArray;
exports.chop = chop;
exports.chopColumn = chopColumn;
exports.clear = clear;
exports.toArray = toArray;
exports.chainSingle = chainSingle;
exports.getColumn = getColumn;
exports.transpose = transpose;
exports.numerify = numerify;