make-mp-data 1.3.3 → 1.4.0
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/.vscode/launch.json +11 -3
- package/.vscode/settings.json +12 -2
- package/README.md +2 -2
- package/chart.js +180 -0
- package/cli.js +30 -17
- package/index.js +459 -287
- package/package.json +59 -49
- package/{models → schemas}/complex.js +39 -19
- package/schemas/foobar.js +110 -0
- package/schemas/funnels.js +222 -0
- package/{models → schemas}/simple.js +10 -10
- package/scratch.mjs +20 -0
- package/testCases.mjs +229 -0
- package/testSoup.mjs +27 -0
- package/tests/e2e.test.js +27 -20
- package/tests/jest.config.js +30 -0
- package/tests/unit.test.js +360 -19
- package/tmp/.gitkeep +0 -0
- package/tsconfig.json +18 -0
- package/types.d.ts +186 -113
- package/utils.js +634 -124
- package/timesoup.js +0 -92
- /package/{models → schemas}/deepNest.js +0 -0
package/utils.js
CHANGED
|
@@ -1,13 +1,71 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
1
2
|
const Chance = require('chance');
|
|
2
|
-
const chance = new Chance();
|
|
3
3
|
const readline = require('readline');
|
|
4
4
|
const { comma, uid } = require('ak-tools');
|
|
5
5
|
const { spawn } = require('child_process');
|
|
6
6
|
const dayjs = require('dayjs');
|
|
7
7
|
const utc = require('dayjs/plugin/utc');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { mkdir } = require('ak-tools');
|
|
8
10
|
dayjs.extend(utc);
|
|
11
|
+
require('dotenv').config();
|
|
12
|
+
|
|
13
|
+
/** @typedef {import('./types').Config} Config */
|
|
14
|
+
/** @typedef {import('./types').ValueValid} ValueValid */
|
|
15
|
+
/** @typedef {import('./types').EnrichedArray} EnrichArray */
|
|
16
|
+
/** @typedef {import('./types').EnrichArrayOptions} EnrichArrayOptions */
|
|
17
|
+
/** @typedef {import('./types').Person} Person */
|
|
18
|
+
|
|
19
|
+
let globalChance;
|
|
20
|
+
let chanceInitialized = false;
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
----
|
|
24
|
+
RNG
|
|
25
|
+
----
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* the random number generator initialization function
|
|
30
|
+
* @param {string} seed
|
|
31
|
+
*/
|
|
32
|
+
function initChance(seed) {
|
|
33
|
+
if (process.env.SEED) seed = process.env.SEED; // Override seed with environment variable if available
|
|
34
|
+
if (!chanceInitialized) {
|
|
35
|
+
globalChance = new Chance(seed);
|
|
36
|
+
if (global.MP_SIMULATION_CONFIG) global.MP_SIMULATION_CONFIG.chance = globalChance;
|
|
37
|
+
chanceInitialized = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* the random number generator getter function
|
|
43
|
+
* @returns {Chance}
|
|
44
|
+
*/
|
|
45
|
+
function getChance() {
|
|
46
|
+
if (!chanceInitialized) {
|
|
47
|
+
const seed = process.env.SEED || global.MP_SIMULATION_CONFIG?.seed;
|
|
48
|
+
if (!seed) {
|
|
49
|
+
return new Chance();
|
|
50
|
+
}
|
|
51
|
+
initChance(seed);
|
|
52
|
+
return globalChance;
|
|
53
|
+
}
|
|
54
|
+
return globalChance;
|
|
55
|
+
}
|
|
9
56
|
|
|
57
|
+
/*
|
|
58
|
+
----
|
|
59
|
+
PICKERS
|
|
60
|
+
----
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* choose a value from an array or a function
|
|
65
|
+
* @param {ValueValid} items
|
|
66
|
+
*/
|
|
10
67
|
function pick(items) {
|
|
68
|
+
const chance = getChance();
|
|
11
69
|
if (!Array.isArray(items)) {
|
|
12
70
|
if (typeof items === 'function') {
|
|
13
71
|
const selection = items();
|
|
@@ -24,50 +82,60 @@ function pick(items) {
|
|
|
24
82
|
return chance.pickone(items);
|
|
25
83
|
};
|
|
26
84
|
|
|
85
|
+
/**
|
|
86
|
+
* returns a random date in the past or future
|
|
87
|
+
* @param {number} inTheLast=30
|
|
88
|
+
* @param {boolean} isPast=true
|
|
89
|
+
* @param {string} format='YYYY-MM-DD'
|
|
90
|
+
*/
|
|
27
91
|
function date(inTheLast = 30, isPast = true, format = 'YYYY-MM-DD') {
|
|
28
|
-
const
|
|
29
|
-
|
|
92
|
+
const chance = getChance();
|
|
93
|
+
const now = global.NOW ? dayjs.unix(global.NOW) : dayjs();
|
|
30
94
|
if (Math.abs(inTheLast) > 365 * 10) inTheLast = chance.integer({ min: 1, max: 180 });
|
|
31
95
|
return function () {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.add(integer(0, 59), 'minute')
|
|
45
|
-
.add(integer(0, 59), 'second');
|
|
46
|
-
}
|
|
47
|
-
if (format) return then?.format(format);
|
|
48
|
-
if (!format) return then?.toISOString();
|
|
49
|
-
}
|
|
50
|
-
catch (e) {
|
|
51
|
-
if (format) return now?.format(format);
|
|
52
|
-
if (!format) return now?.toISOString();
|
|
96
|
+
const when = chance.integer({ min: 0, max: Math.abs(inTheLast) });
|
|
97
|
+
let then;
|
|
98
|
+
if (isPast) {
|
|
99
|
+
then = now.subtract(when, 'day')
|
|
100
|
+
.subtract(integer(0, 23), 'hour')
|
|
101
|
+
.subtract(integer(0, 59), 'minute')
|
|
102
|
+
.subtract(integer(0, 59), 'second');
|
|
103
|
+
} else {
|
|
104
|
+
then = now.add(when, 'day')
|
|
105
|
+
.add(integer(0, 23), 'hour')
|
|
106
|
+
.add(integer(0, 59), 'minute')
|
|
107
|
+
.add(integer(0, 59), 'second');
|
|
53
108
|
}
|
|
109
|
+
|
|
110
|
+
return format ? then.format(format) : then.toISOString();
|
|
54
111
|
};
|
|
55
|
-
}
|
|
112
|
+
}
|
|
56
113
|
|
|
114
|
+
/**
|
|
115
|
+
* returns pairs of random date in the past or future
|
|
116
|
+
* @param {number} inTheLast=30
|
|
117
|
+
* @param {number} numPairs=5
|
|
118
|
+
* @param {string} format='YYYY-MM-DD'
|
|
119
|
+
*/
|
|
57
120
|
function dates(inTheLast = 30, numPairs = 5, format = 'YYYY-MM-DD') {
|
|
58
121
|
const pairs = [];
|
|
59
122
|
for (let i = 0; i < numPairs; i++) {
|
|
60
|
-
pairs.push([date(inTheLast, format), date(inTheLast, format)]);
|
|
123
|
+
pairs.push([date(inTheLast, true, format), date(inTheLast, true, format)]);
|
|
61
124
|
}
|
|
62
125
|
return pairs;
|
|
63
|
-
|
|
64
126
|
};
|
|
65
127
|
|
|
66
|
-
|
|
128
|
+
/**
|
|
129
|
+
* returns a random date
|
|
130
|
+
* @param {any} start
|
|
131
|
+
* @param {any} end=global.NOW
|
|
132
|
+
*/
|
|
133
|
+
function day(start, end = global.NOW) {
|
|
134
|
+
const chance = getChance();
|
|
67
135
|
const format = 'YYYY-MM-DD';
|
|
68
136
|
return function (min, max) {
|
|
69
137
|
start = dayjs(start);
|
|
70
|
-
end = dayjs(
|
|
138
|
+
end = dayjs.unix(global.NOW);
|
|
71
139
|
const diff = end.diff(start, 'day');
|
|
72
140
|
const delta = chance.integer({ min: min, max: diff });
|
|
73
141
|
const day = start.add(delta, 'day');
|
|
@@ -80,7 +148,12 @@ function day(start, end) {
|
|
|
80
148
|
|
|
81
149
|
};
|
|
82
150
|
|
|
151
|
+
/**
|
|
152
|
+
* similar to pick
|
|
153
|
+
* @param {ValueValid} value
|
|
154
|
+
*/
|
|
83
155
|
function choose(value) {
|
|
156
|
+
const chance = getChance();
|
|
84
157
|
try {
|
|
85
158
|
// Keep resolving the value if it's a function
|
|
86
159
|
while (typeof value === 'function') {
|
|
@@ -96,22 +169,36 @@ function choose(value) {
|
|
|
96
169
|
return value;
|
|
97
170
|
}
|
|
98
171
|
|
|
172
|
+
if (typeof value === 'number') {
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
|
|
99
176
|
// If it's not a function or array, return it as is
|
|
100
177
|
return value;
|
|
101
178
|
}
|
|
102
179
|
catch (e) {
|
|
103
|
-
console.error(`\n\nerror on value: ${value};\n\n`,e, '\n\n');
|
|
180
|
+
console.error(`\n\nerror on value: ${value};\n\n`, e, '\n\n');
|
|
104
181
|
return '';
|
|
105
182
|
}
|
|
106
183
|
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* keeps picking from an array until the array is exhausted
|
|
187
|
+
* @param {Array} arr
|
|
188
|
+
*/
|
|
107
189
|
function exhaust(arr) {
|
|
108
190
|
return function () {
|
|
109
191
|
return arr.shift();
|
|
110
192
|
};
|
|
111
193
|
};
|
|
112
194
|
|
|
113
|
-
|
|
114
|
-
|
|
195
|
+
/**
|
|
196
|
+
* returns a random integer between min and max
|
|
197
|
+
* @param {number} min=1
|
|
198
|
+
* @param {number} max=100
|
|
199
|
+
*/
|
|
200
|
+
function integer(min = 1, max = 100) {
|
|
201
|
+
const chance = getChance();
|
|
115
202
|
if (min === max) {
|
|
116
203
|
return min;
|
|
117
204
|
}
|
|
@@ -134,15 +221,61 @@ function integer(min, max) {
|
|
|
134
221
|
};
|
|
135
222
|
|
|
136
223
|
|
|
137
|
-
|
|
224
|
+
function pickAWinner(items, mostChosenIndex) {
|
|
225
|
+
const chance = getChance();
|
|
226
|
+
if (mostChosenIndex > items.length) mostChosenIndex = items.length;
|
|
227
|
+
return function () {
|
|
228
|
+
const weighted = [];
|
|
229
|
+
for (let i = 0; i < 10; i++) {
|
|
230
|
+
if (chance.bool({ likelihood: integer(10, 35) })) {
|
|
231
|
+
if (chance.bool({ likelihood: 50 })) {
|
|
232
|
+
weighted.push(items[mostChosenIndex]);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
const rand = chance.d10();
|
|
236
|
+
const addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;
|
|
237
|
+
let newIndex = mostChosenIndex + addOrSubtract;
|
|
238
|
+
if (newIndex < 0) newIndex = 0;
|
|
239
|
+
if (newIndex > items.length) newIndex = items.length;
|
|
240
|
+
weighted.push(items[newIndex]);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
weighted.push(chance.pickone(items));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return weighted;
|
|
248
|
+
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
/*
|
|
254
|
+
----
|
|
255
|
+
GENERATORS
|
|
256
|
+
----
|
|
257
|
+
*/
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* returns a random float between 0 and 1
|
|
261
|
+
* a substitute for Math.random
|
|
262
|
+
*/
|
|
138
263
|
function boxMullerRandom() {
|
|
264
|
+
const chance = getChance();
|
|
139
265
|
let u = 0, v = 0;
|
|
140
|
-
while (u === 0) u =
|
|
141
|
-
while (v === 0) v =
|
|
266
|
+
while (u === 0) u = chance.floating({ min: 0, max: 1, fixed: 13 });
|
|
267
|
+
while (v === 0) v = chance.floating({ min: 0, max: 1, fixed: 13 });
|
|
142
268
|
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
|
143
269
|
};
|
|
144
270
|
|
|
145
|
-
|
|
271
|
+
/**
|
|
272
|
+
* applies a skew to a value;
|
|
273
|
+
* Skew=0.5: When the skew is 0.5, the distribution becomes more compressed, with values clustering closer to the mean.
|
|
274
|
+
* Skew=1: With a skew of 1, the distribution remains unchanged, as this is equivalent to applying no skew.
|
|
275
|
+
* Skew=2: When the skew is 2, the distribution spreads out, with values extending further from the mean.
|
|
276
|
+
* @param {number} value
|
|
277
|
+
* @param {number} skew
|
|
278
|
+
*/
|
|
146
279
|
function applySkew(value, skew) {
|
|
147
280
|
if (skew === 1) return value;
|
|
148
281
|
// Adjust the value based on skew
|
|
@@ -155,60 +288,301 @@ function mapToRange(value, mean, sd) {
|
|
|
155
288
|
return Math.round(value * sd + mean);
|
|
156
289
|
};
|
|
157
290
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
291
|
+
/**
|
|
292
|
+
* generate a range of numbers
|
|
293
|
+
* @param {number} a
|
|
294
|
+
* @param {number} b
|
|
295
|
+
* @param {number} step=1
|
|
296
|
+
*/
|
|
297
|
+
function range(a, b, step = 1) {
|
|
298
|
+
step = !step ? 1 : step;
|
|
299
|
+
b = b / step;
|
|
300
|
+
for (var i = a; i <= b; i++) {
|
|
301
|
+
this.push(i * step);
|
|
302
|
+
}
|
|
303
|
+
return this;
|
|
304
|
+
};
|
|
162
305
|
|
|
163
|
-
for (let i = 0; i < size; i++) {
|
|
164
|
-
let normalValue = boxMullerRandom();
|
|
165
|
-
let skewedValue = applySkew(normalValue, skew);
|
|
166
|
-
let mappedValue = mapToRange(skewedValue, mean, sd);
|
|
167
306
|
|
|
168
|
-
|
|
307
|
+
/*
|
|
308
|
+
----
|
|
309
|
+
STREAMERS
|
|
310
|
+
----
|
|
311
|
+
*/
|
|
312
|
+
|
|
313
|
+
function streamJSON(path, data) {
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
const writeStream = fs.createWriteStream(path, { encoding: 'utf8' });
|
|
316
|
+
data.forEach(item => {
|
|
317
|
+
writeStream.write(JSON.stringify(item) + '\n');
|
|
318
|
+
});
|
|
319
|
+
writeStream.end();
|
|
320
|
+
writeStream.on('finish', () => {
|
|
321
|
+
resolve(path);
|
|
322
|
+
});
|
|
323
|
+
writeStream.on('error', reject);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function streamCSV(path, data) {
|
|
328
|
+
return new Promise((resolve, reject) => {
|
|
329
|
+
const writeStream = fs.createWriteStream(path, { encoding: 'utf8' });
|
|
330
|
+
// Extract all unique keys from the data array
|
|
331
|
+
const columns = getUniqueKeys(data); // Assuming getUniqueKeys properly retrieves all keys
|
|
332
|
+
|
|
333
|
+
// Stream the header
|
|
334
|
+
writeStream.write(columns.join(',') + '\n');
|
|
335
|
+
|
|
336
|
+
// Stream each data row
|
|
337
|
+
data.forEach(item => {
|
|
338
|
+
for (const key in item) {
|
|
339
|
+
// Ensure all nested objects are properly stringified
|
|
340
|
+
if (typeof item[key] === "object") item[key] = JSON.stringify(item[key]);
|
|
341
|
+
}
|
|
342
|
+
const row = columns.map(col => item[col] ? `"${item[col].toString().replace(/"/g, '""')}"` : "").join(',');
|
|
343
|
+
writeStream.write(row + '\n');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
writeStream.end();
|
|
347
|
+
writeStream.on('finish', () => {
|
|
348
|
+
resolve(path);
|
|
349
|
+
});
|
|
350
|
+
writeStream.on('error', reject);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
/*
|
|
356
|
+
----
|
|
357
|
+
WEIGHERS
|
|
358
|
+
----
|
|
359
|
+
*/
|
|
360
|
+
|
|
361
|
+
function weighFunnels(acc, funnel) {
|
|
362
|
+
const weight = funnel?.weight || 1;
|
|
363
|
+
for (let i = 0; i < weight; i++) {
|
|
364
|
+
acc.push(funnel);
|
|
365
|
+
}
|
|
366
|
+
return acc;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* a utility function to generate a range of numbers within a given skew
|
|
371
|
+
* Skew = 0.5: The values are more concentrated towards the extremes (both ends of the range) with a noticeable dip in the middle. The distribution appears more "U" shaped. Larger sizes result in smoother distributions but maintain the overall shape.
|
|
372
|
+
* Skew = 1: This represents the default normal distribution without skew. The values are normally distributed around the mean. Larger sizes create a clearer bell-shaped curve.
|
|
373
|
+
* Skew = 2: The values are more concentrated towards the mean, with a steeper drop-off towards the extremes. The distribution appears more peaked, resembling a "sharper" bell curve. Larger sizes enhance the clarity of this peaked distribution.
|
|
374
|
+
* Size represents the size of the pool to choose from; Larger sizes result in smoother distributions but maintain the overall shape.
|
|
375
|
+
* @param {number} min
|
|
376
|
+
* @param {number} max
|
|
377
|
+
* @param {number} skew=1
|
|
378
|
+
* @param {number} size=100
|
|
379
|
+
*/
|
|
380
|
+
function weightedRange(min, max, skew = 1, size = 100) {
|
|
381
|
+
const mean = (max + min) / 2;
|
|
382
|
+
const sd = (max - min) / 4;
|
|
383
|
+
const array = [];
|
|
384
|
+
while (array.length < size) {
|
|
385
|
+
const normalValue = boxMullerRandom();
|
|
386
|
+
const skewedValue = applySkew(normalValue, skew);
|
|
387
|
+
const mappedValue = mapToRange(skewedValue, mean, sd);
|
|
169
388
|
if (mappedValue >= min && mappedValue <= max) {
|
|
170
389
|
array.push(mappedValue);
|
|
171
|
-
} else {
|
|
172
|
-
i--; // If out of range, redo this iteration
|
|
173
390
|
}
|
|
174
391
|
}
|
|
392
|
+
return array;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function weighArray(arr) {
|
|
396
|
+
|
|
397
|
+
// Calculate the upper bound based on the size of the array with added noise
|
|
398
|
+
const maxCopies = arr.length + integer(1, arr.length);
|
|
399
|
+
|
|
400
|
+
// Create an empty array to store the weighted elements
|
|
401
|
+
let weightedArray = [];
|
|
402
|
+
|
|
403
|
+
// Iterate over the input array and copy each element a random number of times
|
|
404
|
+
arr.forEach(element => {
|
|
405
|
+
let copies = integer(1, maxCopies);
|
|
406
|
+
for (let i = 0; i < copies; i++) {
|
|
407
|
+
weightedArray.push(element);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
return weightedArray;
|
|
412
|
+
}
|
|
175
413
|
|
|
414
|
+
/*
|
|
415
|
+
----
|
|
416
|
+
SHUFFLERS
|
|
417
|
+
----
|
|
418
|
+
*/
|
|
419
|
+
|
|
420
|
+
// Function to shuffle array
|
|
421
|
+
function shuffleArray(array) {
|
|
422
|
+
const chance = getChance();
|
|
423
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
424
|
+
const j = chance.integer({ min: 0, max: i });
|
|
425
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
426
|
+
}
|
|
176
427
|
return array;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function shuffleExceptFirst(array) {
|
|
431
|
+
if (array.length <= 1) return array;
|
|
432
|
+
const restShuffled = shuffleArray(array.slice(1));
|
|
433
|
+
return [array[0], ...restShuffled];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function shuffleExceptLast(array) {
|
|
437
|
+
if (array.length <= 1) return array;
|
|
438
|
+
const restShuffled = shuffleArray(array.slice(0, -1));
|
|
439
|
+
return [...restShuffled, array[array.length - 1]];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function fixFirstAndLast(array) {
|
|
443
|
+
if (array.length <= 2) return array;
|
|
444
|
+
const middleShuffled = shuffleArray(array.slice(1, -1));
|
|
445
|
+
return [array[0], ...middleShuffled, array[array.length - 1]];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function shuffleMiddle(array) {
|
|
449
|
+
if (array.length <= 2) return array;
|
|
450
|
+
const middleShuffled = shuffleArray(array.slice(1, -1));
|
|
451
|
+
return [array[0], ...middleShuffled, array[array.length - 1]];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function shuffleOutside(array) {
|
|
455
|
+
if (array.length <= 2) return array;
|
|
456
|
+
const middleFixed = array.slice(1, -1);
|
|
457
|
+
const outsideShuffled = shuffleArray([array[0], array[array.length - 1]]);
|
|
458
|
+
return [outsideShuffled[0], ...middleFixed, outsideShuffled[1]];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/*
|
|
462
|
+
----
|
|
463
|
+
META
|
|
464
|
+
----
|
|
465
|
+
*/
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* our meta programming function which lets you mutate items as they are pushed into an array
|
|
469
|
+
* @param {any[]} arr
|
|
470
|
+
* @param {EnrichArrayOptions} opts
|
|
471
|
+
* @returns {EnrichArray}}
|
|
472
|
+
*/
|
|
473
|
+
function enrichArray(arr = [], opts = {}) {
|
|
474
|
+
const { hook = a => a, type = "", ...rest } = opts;
|
|
475
|
+
|
|
476
|
+
function transformThenPush(item) {
|
|
477
|
+
if (item === null) return 0;
|
|
478
|
+
if (item === undefined) return 0;
|
|
479
|
+
if (typeof item === 'object') {
|
|
480
|
+
if (Object.keys(item).length === 0) return 0;
|
|
481
|
+
}
|
|
482
|
+
if (Array.isArray(item)) {
|
|
483
|
+
for (const i of item) {
|
|
484
|
+
try {
|
|
485
|
+
const enriched = hook(i, type, rest);
|
|
486
|
+
arr.push(enriched);
|
|
487
|
+
}
|
|
488
|
+
catch (e) {
|
|
489
|
+
console.error(`\n\nyour hook had an error\n\n`, e);
|
|
490
|
+
arr.push(i);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
}
|
|
494
|
+
return -1;
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
try {
|
|
498
|
+
const enriched = hook(item, type, rest);
|
|
499
|
+
return arr.push(enriched);
|
|
500
|
+
}
|
|
501
|
+
catch (e) {
|
|
502
|
+
console.error(`\n\nyour hook had an error\n\n`, e);
|
|
503
|
+
return arr.push(item);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** @type {EnrichArray} */
|
|
510
|
+
// @ts-ignore
|
|
511
|
+
const enrichedArray = arr;
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
enrichedArray.hookPush = transformThenPush;
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
return enrichedArray;
|
|
177
518
|
};
|
|
178
519
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
520
|
+
function buildFileNames(config) {
|
|
521
|
+
const { format = "csv", groupKeys = [], lookupTables = [] } = config;
|
|
522
|
+
let extension = "";
|
|
523
|
+
extension = format === "csv" ? "csv" : "json";
|
|
524
|
+
// const current = dayjs.utc().format("MM-DD-HH");
|
|
525
|
+
const simName = config.simulationName;
|
|
526
|
+
let writeDir = "./";
|
|
527
|
+
if (config.writeToDisk) writeDir = mkdir("./data");
|
|
528
|
+
if (typeof writeDir !== "string") throw new Error("writeDir must be a string");
|
|
529
|
+
if (typeof simName !== "string") throw new Error("simName must be a string");
|
|
530
|
+
|
|
531
|
+
const writePaths = {
|
|
532
|
+
eventFiles: [path.join(writeDir, `${simName}-EVENTS.${extension}`)],
|
|
533
|
+
userFiles: [path.join(writeDir, `${simName}-USERS.${extension}`)],
|
|
534
|
+
scdFiles: [],
|
|
535
|
+
mirrorFiles: [],
|
|
536
|
+
groupFiles: [],
|
|
537
|
+
lookupFiles: [],
|
|
538
|
+
folder: writeDir,
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
//add SCD files
|
|
542
|
+
const scdKeys = Object.keys(config?.scdProps || {});
|
|
543
|
+
for (const key of scdKeys) {
|
|
544
|
+
writePaths.scdFiles.push(
|
|
545
|
+
path.join(writeDir, `${simName}-${key}-SCD.${extension}`)
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
//add group files
|
|
550
|
+
for (const groupPair of groupKeys) {
|
|
551
|
+
const groupKey = groupPair[0];
|
|
552
|
+
|
|
553
|
+
writePaths.groupFiles.push(
|
|
554
|
+
path.join(writeDir, `${simName}-${groupKey}-GROUP.${extension}`)
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
//add lookup files
|
|
559
|
+
for (const lookupTable of lookupTables) {
|
|
560
|
+
const { key } = lookupTable;
|
|
561
|
+
writePaths.lookupFiles.push(
|
|
562
|
+
//lookups are always CSVs
|
|
563
|
+
path.join(writeDir, `${simName}-${key}-LOOKUP.csv`)
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
//add mirror files
|
|
568
|
+
const mirrorProps = config?.mirrorProps || {};
|
|
569
|
+
if (Object.keys(mirrorProps).length) {
|
|
570
|
+
writePaths.mirrorFiles.push(
|
|
571
|
+
path.join(writeDir, `${simName}-MIRROR.${extension}`)
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return writePaths;
|
|
193
576
|
}
|
|
194
577
|
|
|
195
578
|
|
|
196
579
|
function progress(thing, p) {
|
|
580
|
+
// @ts-ignore
|
|
197
581
|
readline.cursorTo(process.stdout, 0);
|
|
198
582
|
process.stdout.write(`${thing} processed ... ${comma(p)}`);
|
|
199
583
|
};
|
|
200
584
|
|
|
201
|
-
function range(a, b, step = 1) {
|
|
202
|
-
step = !step ? 1 : step;
|
|
203
|
-
b = b / step;
|
|
204
|
-
for (var i = a; i <= b; i++) {
|
|
205
|
-
this.push(i * step);
|
|
206
|
-
}
|
|
207
|
-
return this;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
585
|
|
|
211
|
-
//helper to open the finder
|
|
212
586
|
function openFinder(path, callback) {
|
|
213
587
|
path = path || '/';
|
|
214
588
|
let p = spawn('open', [path]);
|
|
@@ -226,95 +600,209 @@ function getUniqueKeys(data) {
|
|
|
226
600
|
return Array.from(keysSet);
|
|
227
601
|
};
|
|
228
602
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
/*
|
|
606
|
+
----
|
|
607
|
+
CORE
|
|
608
|
+
----
|
|
609
|
+
*/
|
|
610
|
+
|
|
611
|
+
//the function which generates $distinct_id + $anonymous_ids, $session_ids, and created, skewing towards the present
|
|
612
|
+
function generateUser(user_id, numDays, amplitude = 1, frequency = 1, skew = 1) {
|
|
613
|
+
const chance = getChance();
|
|
614
|
+
// Uniformly distributed `u`, then skew applied
|
|
615
|
+
let u = Math.pow(chance.random(), skew);
|
|
616
|
+
|
|
617
|
+
// Sine function for a smoother curve
|
|
618
|
+
const sineValue = (Math.sin(u * Math.PI * frequency - Math.PI / 2) * amplitude + 1) / 2;
|
|
619
|
+
|
|
620
|
+
// Scale the sineValue to the range of days
|
|
621
|
+
let daysAgoBorn = Math.round(sineValue * (numDays - 1)) + 1;
|
|
622
|
+
|
|
623
|
+
// Clamp values to ensure they are within the desired range
|
|
624
|
+
daysAgoBorn = Math.min(daysAgoBorn, numDays);
|
|
625
|
+
|
|
626
|
+
const user = {
|
|
627
|
+
distinct_id: user_id,
|
|
628
|
+
...person(numDays),
|
|
241
629
|
};
|
|
242
|
-
};
|
|
243
630
|
|
|
244
631
|
|
|
632
|
+
return user;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* build sign waves basically
|
|
637
|
+
* @param {number} [earliestTime]
|
|
638
|
+
* @param {number} [latestTime]
|
|
639
|
+
* @param {number} [peaks=5]
|
|
640
|
+
*/
|
|
641
|
+
function TimeSoup(earliestTime, latestTime, peaks = 5, deviation = 2, mean = 0) {
|
|
642
|
+
if (!earliestTime) earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
|
|
643
|
+
if (!latestTime) latestTime = global.NOW;
|
|
644
|
+
const chance = getChance();
|
|
645
|
+
const totalRange = latestTime - earliestTime;
|
|
646
|
+
const chunkSize = totalRange / peaks;
|
|
647
|
+
|
|
648
|
+
// Select a random chunk based on the number of peaks
|
|
649
|
+
const peakIndex = integer(0, peaks - 1);
|
|
650
|
+
const chunkStart = earliestTime + peakIndex * chunkSize;
|
|
651
|
+
const chunkEnd = chunkStart + chunkSize;
|
|
652
|
+
const chunkMid = (chunkStart + chunkEnd) / 2;
|
|
653
|
+
|
|
654
|
+
// Generate a single timestamp within this chunk using a normal distribution centered at chunkMid
|
|
655
|
+
let offset;
|
|
656
|
+
let iterations = 0;
|
|
657
|
+
let isValidTime = false;
|
|
658
|
+
do {
|
|
659
|
+
iterations++;
|
|
660
|
+
offset = chance.normal({ mean: mean, dev: chunkSize / deviation });
|
|
661
|
+
isValidTime = validateTime(chunkMid + offset, earliestTime, latestTime);
|
|
662
|
+
if (iterations > 10000) throw new Error("Too many iterations");
|
|
663
|
+
} while (chunkMid + offset < chunkStart || chunkMid + offset > chunkEnd);
|
|
664
|
+
|
|
665
|
+
try {
|
|
666
|
+
return dayjs.unix(chunkMid + offset).toISOString();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
catch (e) {
|
|
670
|
+
//escape hatch
|
|
671
|
+
// console.log('BAD TIME', e?.message);
|
|
672
|
+
return dayjs.unix(integer(earliestTime, latestTime)).toISOString();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
function validateTime(chosenTime, earliestTime, latestTime) {
|
|
678
|
+
if (!earliestTime) earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
|
|
679
|
+
if (!latestTime) latestTime = global.NOW;
|
|
680
|
+
|
|
681
|
+
if (typeof chosenTime === 'number') {
|
|
682
|
+
if (chosenTime > 0) {
|
|
683
|
+
if (chosenTime > earliestTime) {
|
|
684
|
+
if (chosenTime < latestTime) {
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* @param {number} bornDaysAgo=30
|
|
697
|
+
* @return {Person}
|
|
698
|
+
*/
|
|
245
699
|
function person(bornDaysAgo = 30) {
|
|
700
|
+
const chance = getChance();
|
|
246
701
|
//names and photos
|
|
247
|
-
|
|
702
|
+
let gender = chance.pickone(['male', 'female']);
|
|
703
|
+
if (!gender) gender = "female";
|
|
704
|
+
// @ts-ignore
|
|
248
705
|
const first = chance.first({ gender });
|
|
249
706
|
const last = chance.last();
|
|
250
|
-
const
|
|
251
|
-
const
|
|
707
|
+
const name = `${first} ${last}`;
|
|
708
|
+
const email = `${first[0]}.${last}@${chance.domain()}.com`;
|
|
252
709
|
const avatarPrefix = `https://randomuser.me/api/portraits`;
|
|
253
710
|
const randomAvatarNumber = chance.integer({
|
|
254
711
|
min: 1,
|
|
255
712
|
max: 99
|
|
256
713
|
});
|
|
257
714
|
const avPath = gender === 'male' ? `/men/${randomAvatarNumber}.jpg` : `/women/${randomAvatarNumber}.jpg`;
|
|
258
|
-
const
|
|
259
|
-
const
|
|
715
|
+
const avatar = avatarPrefix + avPath;
|
|
716
|
+
const created = dayjs.unix(global.NOW).subtract(bornDaysAgo, 'day').format('YYYY-MM-DD');
|
|
717
|
+
// const created = date(bornDaysAgo, true)();
|
|
260
718
|
|
|
719
|
+
/** @type {Person} */
|
|
261
720
|
const user = {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
721
|
+
name,
|
|
722
|
+
email,
|
|
723
|
+
avatar,
|
|
724
|
+
created,
|
|
725
|
+
anonymousIds: [],
|
|
726
|
+
sessionIds: []
|
|
266
727
|
};
|
|
267
728
|
|
|
268
729
|
//anon Ids
|
|
269
730
|
if (global.MP_SIMULATION_CONFIG?.anonIds) {
|
|
270
|
-
const anonymousIds = [];
|
|
271
731
|
const clusterSize = integer(2, 10);
|
|
272
732
|
for (let i = 0; i < clusterSize; i++) {
|
|
273
|
-
|
|
733
|
+
const anonId = uid(42);
|
|
734
|
+
user.anonymousIds.push(anonId);
|
|
274
735
|
}
|
|
275
|
-
|
|
736
|
+
|
|
276
737
|
}
|
|
277
738
|
|
|
278
739
|
//session Ids
|
|
279
740
|
if (global.MP_SIMULATION_CONFIG?.sessionIds) {
|
|
280
|
-
const sessionIds = [];
|
|
281
741
|
const sessionSize = integer(5, 30);
|
|
282
742
|
for (let i = 0; i < sessionSize; i++) {
|
|
283
|
-
|
|
743
|
+
const sessionId = [uid(5), uid(5), uid(5), uid(5)].join("-");
|
|
744
|
+
user.sessionIds.push(sessionId);
|
|
284
745
|
}
|
|
285
|
-
user.sessionIds = sessionIds;
|
|
286
746
|
}
|
|
287
747
|
|
|
288
748
|
return user;
|
|
289
749
|
};
|
|
290
750
|
|
|
291
751
|
|
|
292
|
-
|
|
293
|
-
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
//UNUSED
|
|
755
|
+
|
|
756
|
+
function fixFunkyTime(earliestTime, latestTime) {
|
|
757
|
+
if (!earliestTime) earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
|
|
758
|
+
// if (typeof earliestTime !== "number") {
|
|
759
|
+
// if (parseInt(earliestTime) > 0) earliestTime = parseInt(earliestTime);
|
|
760
|
+
// if (dayjs(earliestTime).isValid()) earliestTime = dayjs(earliestTime).unix();
|
|
761
|
+
// }
|
|
762
|
+
if (typeof earliestTime !== "number") earliestTime = dayjs.unix(earliestTime).unix();
|
|
763
|
+
if (typeof latestTime !== "number") latestTime = global.NOW;
|
|
764
|
+
if (typeof latestTime === "number" && latestTime > global.NOW) latestTime = global.NOW;
|
|
765
|
+
if (earliestTime > latestTime) {
|
|
766
|
+
const tempEarlyTime = earliestTime;
|
|
767
|
+
const tempLateTime = latestTime;
|
|
768
|
+
earliestTime = tempLateTime;
|
|
769
|
+
latestTime = tempEarlyTime;
|
|
770
|
+
}
|
|
771
|
+
if (earliestTime === latestTime) {
|
|
772
|
+
earliestTime = dayjs.unix(earliestTime)
|
|
773
|
+
.subtract(integer(1, 14), "day")
|
|
774
|
+
.subtract(integer(1, 23), "hour")
|
|
775
|
+
.subtract(integer(1, 59), "minute")
|
|
776
|
+
.subtract(integer(1, 59), "second")
|
|
777
|
+
.unix();
|
|
778
|
+
}
|
|
779
|
+
return [earliestTime, latestTime];
|
|
780
|
+
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* makes a random-sized array of emojis
|
|
787
|
+
* @param {number} max=10
|
|
788
|
+
* @param {boolean} array=false
|
|
789
|
+
*/
|
|
790
|
+
function generateEmoji(max = 10, array = false) {
|
|
791
|
+
const chance = getChance();
|
|
294
792
|
return function () {
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
const rand = chance.d10();
|
|
303
|
-
const addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;
|
|
304
|
-
let newIndex = mostChosenIndex + addOrSubtract;
|
|
305
|
-
if (newIndex < 0) newIndex = 0;
|
|
306
|
-
if (newIndex > items.length) newIndex = items.length;
|
|
307
|
-
weighted.push(items[newIndex]);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
weighted.push(chance.pickone(items));
|
|
312
|
-
}
|
|
793
|
+
const emojis = ['😀', '😂', '😍', '😎', '😜', '😇', '😡', '😱', '😭', '😴', '🤢', '🤠', '🤡', '👽', '👻', '💩', '👺', '👹', '👾', '🤖', '🤑', '🤗', '🤓', '🤔', '🤐', '😀', '😂', '😍', '😎', '😜', '😇', '😡', '😱', '😭', '😴', '🤢', '🤠', '🤡', '👽', '👻', '💩', '👺', '👹', '👾', '🤖', '🤑', '🤗', '🤓', '🤔', '🤐', '😈', '👿', '👦', '👧', '👨', '👩', '👴', '👵', '👶', '🧒', '👮', '👷', '💂', '🕵', '👩⚕️', '👨⚕️', '👩🌾', '👨🌾', '👩🍳', '👨🍳', '👩🎓', '👨🎓', '👩🎤', '👨🎤', '👩🏫', '👨🏫', '👩🏭', '👨🏭', '👩💻', '👨💻', '👩💼', '👨💼', '👩🔧', '👨🔧', '👩🔬', '👨🔬', '👩🎨', '👨🎨', '👩🚒', '👨🚒', '👩✈️', '👨✈️', '👩🚀', '👨🚀', '👩⚖️', '👨⚖️', '🤶', '🎅', '👸', '🤴', '👰', '🤵', '👼', '🤰', '🙇', '💁', '🙅', '🙆', '🙋', '🤦', '🤷', '🙎', '🙍', '💇', '💆', '🕴', '💃', '🕺', '🚶', '🏃', '🤲', '👐', '🙌', '👏', '🤝', '👍', '👎', '👊', '✊', '🤛', '🤜', '🤞', '✌️', '🤟', '🤘', '👌', '👈', '👉', '👆', '👇', '☝️', '✋', '🤚', '🖐', '🖖', '👋', '🤙', '💪', '🖕', '✍️', '🤳', '💅', '👂', '👃', '👣', '👀', '👁', '🧠', '👅', '👄', '💋', '👓', '🕶', '👔', '👕', '👖', '🧣', '🧤', '🧥', '🧦', '👗', '👘', '👙', '👚', '👛', '👜', '👝', '🛍', '🎒', '👞', '👟', '👠', '👡', '👢', '👑', '👒', '🎩', '🎓', '🧢', '⛑', '📿', '💄', '💍', '💎', '🔇', '🔈', '🔉', '🔊', '📢', '📣', '📯', '🔔', '🔕', '🎼', '🎵', '🎶', '🎙', '🎚', '🎛', '🎤', '🎧', '📻', '🎷', '🎸', '🎹', '🎺', '🎻', '🥁', '📱', '📲', '💻', '🖥', '🖨', '🖱', '🖲', '🕹', '🗜', '💽', '💾', '💿', '📀', '📼', '📷', '📸', '📹', '🎥', '📽', '🎞', '📞', '☎️', '📟', '📠', '📺', '📻', '🎙', '📡', '🔍', '🔎', '🔬', '🔭', '📡', '💡', '🔦', '🏮', '📔', '📕', '📖', '📗', '📘', '📙', '📚', '📓', '📒', '📃', '📜', '📄', '📰', '🗞', '📑', '🔖', '🏷', '💰', '💴', '💵', '💶', '💷', '💸', '💳', '🧾', '💹', '💱', '💲', '✉️', '📧', '📨', '📩', '📤', '📥', '📦', '📫', '📪', '📬', '📭', '📮', '🗳', '✏️', '✒️', '🖋', '🖊', '🖌', '🖍', '📝', '💼', '📁', '📂', '🗂', '📅', '📆', '🗒', '🗓', '📇', '📈', '📉', '📊', '📋', '📌', '📍', '📎', '🖇', '📏', '📐', '✂️', '🗃', '🗄', '🗑', '🔒', '🔓', '🔏', '🔐', '🔑', '🗝', '🔨', '⛏', '⚒', '🛠', '🗡', '⚔️', '🔫', '🏹', '🛡', '🔧', '🔩', '⚙️', '🗜', '⚖️', '🔗', '⛓', '🧰', '🧲', '⚗️', '🧪', '🧫', '🧬', '🔬', '🔭', '📡', '💉', '💊', '🛏', '🛋', '🚪', '🚽', '🚿', '🛁', '🧴', '🧷', '🧹', '🧺', '🧻', '🧼', '🧽', '🧯', '🚬', '⚰️', '⚱️', '🗿', '🏺', '🧱', '🎈', '🎏', '🎀', '🎁', '🎊', '🎉', '🎎', '🏮', '🎐', '🧧', '✉️', '📩', '📨', '📧'];
|
|
794
|
+
let num = integer(1, max);
|
|
795
|
+
let arr = [];
|
|
796
|
+
for (let i = 0; i < num; i++) {
|
|
797
|
+
arr.push(chance.pickone(emojis));
|
|
313
798
|
}
|
|
314
|
-
return
|
|
315
|
-
|
|
799
|
+
if (array) return arr;
|
|
800
|
+
if (!array) return arr.join(', ');
|
|
801
|
+
return "🤷";
|
|
316
802
|
};
|
|
317
|
-
}
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
|
|
318
806
|
|
|
319
807
|
module.exports = {
|
|
320
808
|
pick,
|
|
@@ -324,6 +812,13 @@ module.exports = {
|
|
|
324
812
|
choose,
|
|
325
813
|
exhaust,
|
|
326
814
|
integer,
|
|
815
|
+
TimeSoup,
|
|
816
|
+
|
|
817
|
+
generateEmoji,
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
initChance,
|
|
821
|
+
getChance,
|
|
327
822
|
|
|
328
823
|
boxMullerRandom,
|
|
329
824
|
applySkew,
|
|
@@ -333,7 +828,22 @@ module.exports = {
|
|
|
333
828
|
range,
|
|
334
829
|
openFinder,
|
|
335
830
|
getUniqueKeys,
|
|
336
|
-
generateEmoji,
|
|
337
831
|
person,
|
|
338
|
-
|
|
832
|
+
pickAWinner,
|
|
833
|
+
weighArray,
|
|
834
|
+
weighFunnels,
|
|
835
|
+
|
|
836
|
+
shuffleArray,
|
|
837
|
+
shuffleExceptFirst,
|
|
838
|
+
shuffleExceptLast,
|
|
839
|
+
fixFirstAndLast,
|
|
840
|
+
shuffleMiddle,
|
|
841
|
+
shuffleOutside,
|
|
842
|
+
|
|
843
|
+
generateUser,
|
|
844
|
+
enrichArray,
|
|
845
|
+
|
|
846
|
+
buildFileNames,
|
|
847
|
+
streamJSON,
|
|
848
|
+
streamCSV
|
|
339
849
|
};
|