make-mp-data 1.3.4 → 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 +11 -1
- package/README.md +2 -2
- package/chart.js +180 -0
- package/index.js +443 -301
- package/package.json +59 -52
- package/{models → schemas}/complex.js +18 -18
- package/{models → schemas}/foobar.js +1 -1
- 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 +346 -14
- package/tmp/.gitkeep +0 -0
- package/tsconfig.json +1 -1
- package/types.d.ts +74 -15
- package/utils.js +590 -151
- package/timesoup.js +0 -92
- /package/{models → schemas}/deepNest.js +0 -0
package/utils.js
CHANGED
|
@@ -1,17 +1,71 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
-
const Papa = require('papaparse');
|
|
3
2
|
const Chance = require('chance');
|
|
4
|
-
const chance = new Chance();
|
|
5
3
|
const readline = require('readline');
|
|
6
4
|
const { comma, uid } = require('ak-tools');
|
|
7
5
|
const { spawn } = require('child_process');
|
|
8
6
|
const dayjs = require('dayjs');
|
|
9
7
|
const utc = require('dayjs/plugin/utc');
|
|
10
|
-
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { mkdir } = require('ak-tools');
|
|
11
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 */
|
|
12
18
|
|
|
19
|
+
let globalChance;
|
|
20
|
+
let chanceInitialized = false;
|
|
13
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
|
+
}
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
----
|
|
59
|
+
PICKERS
|
|
60
|
+
----
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* choose a value from an array or a function
|
|
65
|
+
* @param {ValueValid} items
|
|
66
|
+
*/
|
|
14
67
|
function pick(items) {
|
|
68
|
+
const chance = getChance();
|
|
15
69
|
if (!Array.isArray(items)) {
|
|
16
70
|
if (typeof items === 'function') {
|
|
17
71
|
const selection = items();
|
|
@@ -28,36 +82,41 @@ function pick(items) {
|
|
|
28
82
|
return chance.pickone(items);
|
|
29
83
|
};
|
|
30
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
|
+
*/
|
|
31
91
|
function date(inTheLast = 30, isPast = true, format = 'YYYY-MM-DD') {
|
|
32
|
-
const
|
|
33
|
-
|
|
92
|
+
const chance = getChance();
|
|
93
|
+
const now = global.NOW ? dayjs.unix(global.NOW) : dayjs();
|
|
34
94
|
if (Math.abs(inTheLast) > 365 * 10) inTheLast = chance.integer({ min: 1, max: 180 });
|
|
35
95
|
return function () {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.add(integer(0, 59), 'minute')
|
|
49
|
-
.add(integer(0, 59), 'second');
|
|
50
|
-
}
|
|
51
|
-
if (format) return then?.format(format);
|
|
52
|
-
if (!format) return then?.toISOString();
|
|
53
|
-
}
|
|
54
|
-
catch (e) {
|
|
55
|
-
if (format) return now?.format(format);
|
|
56
|
-
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');
|
|
57
108
|
}
|
|
109
|
+
|
|
110
|
+
return format ? then.format(format) : then.toISOString();
|
|
58
111
|
};
|
|
59
|
-
}
|
|
112
|
+
}
|
|
60
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
|
+
*/
|
|
61
120
|
function dates(inTheLast = 30, numPairs = 5, format = 'YYYY-MM-DD') {
|
|
62
121
|
const pairs = [];
|
|
63
122
|
for (let i = 0; i < numPairs; i++) {
|
|
@@ -66,11 +125,17 @@ function dates(inTheLast = 30, numPairs = 5, format = 'YYYY-MM-DD') {
|
|
|
66
125
|
return pairs;
|
|
67
126
|
};
|
|
68
127
|
|
|
69
|
-
|
|
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();
|
|
70
135
|
const format = 'YYYY-MM-DD';
|
|
71
136
|
return function (min, max) {
|
|
72
137
|
start = dayjs(start);
|
|
73
|
-
end = dayjs(
|
|
138
|
+
end = dayjs.unix(global.NOW);
|
|
74
139
|
const diff = end.diff(start, 'day');
|
|
75
140
|
const delta = chance.integer({ min: min, max: diff });
|
|
76
141
|
const day = start.add(delta, 'day');
|
|
@@ -83,7 +148,12 @@ function day(start, end) {
|
|
|
83
148
|
|
|
84
149
|
};
|
|
85
150
|
|
|
151
|
+
/**
|
|
152
|
+
* similar to pick
|
|
153
|
+
* @param {ValueValid} value
|
|
154
|
+
*/
|
|
86
155
|
function choose(value) {
|
|
156
|
+
const chance = getChance();
|
|
87
157
|
try {
|
|
88
158
|
// Keep resolving the value if it's a function
|
|
89
159
|
while (typeof value === 'function') {
|
|
@@ -111,14 +181,24 @@ function choose(value) {
|
|
|
111
181
|
return '';
|
|
112
182
|
}
|
|
113
183
|
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* keeps picking from an array until the array is exhausted
|
|
187
|
+
* @param {Array} arr
|
|
188
|
+
*/
|
|
114
189
|
function exhaust(arr) {
|
|
115
190
|
return function () {
|
|
116
191
|
return arr.shift();
|
|
117
192
|
};
|
|
118
193
|
};
|
|
119
194
|
|
|
120
|
-
|
|
195
|
+
/**
|
|
196
|
+
* returns a random integer between min and max
|
|
197
|
+
* @param {number} min=1
|
|
198
|
+
* @param {number} max=100
|
|
199
|
+
*/
|
|
121
200
|
function integer(min = 1, max = 100) {
|
|
201
|
+
const chance = getChance();
|
|
122
202
|
if (min === max) {
|
|
123
203
|
return min;
|
|
124
204
|
}
|
|
@@ -141,15 +221,61 @@ function integer(min = 1, max = 100) {
|
|
|
141
221
|
};
|
|
142
222
|
|
|
143
223
|
|
|
144
|
-
|
|
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
|
+
*/
|
|
145
263
|
function boxMullerRandom() {
|
|
264
|
+
const chance = getChance();
|
|
146
265
|
let u = 0, v = 0;
|
|
147
|
-
while (u === 0) u =
|
|
148
|
-
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 });
|
|
149
268
|
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
|
150
269
|
};
|
|
151
270
|
|
|
152
|
-
|
|
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
|
+
*/
|
|
153
279
|
function applySkew(value, skew) {
|
|
154
280
|
if (skew === 1) return value;
|
|
155
281
|
// Adjust the value based on skew
|
|
@@ -162,29 +288,96 @@ function mapToRange(value, mean, sd) {
|
|
|
162
288
|
return Math.round(value * sd + mean);
|
|
163
289
|
};
|
|
164
290
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
};
|
|
169
305
|
|
|
170
|
-
for (let i = 0; i < size; i++) {
|
|
171
|
-
let normalValue = boxMullerRandom();
|
|
172
|
-
let skewedValue = applySkew(normalValue, skew);
|
|
173
|
-
let mappedValue = mapToRange(skewedValue, mean, sd);
|
|
174
306
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
}
|
|
307
|
+
/*
|
|
308
|
+
----
|
|
309
|
+
STREAMERS
|
|
310
|
+
----
|
|
311
|
+
*/
|
|
182
312
|
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
185
332
|
|
|
186
|
-
//
|
|
187
|
-
|
|
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) {
|
|
188
381
|
const mean = (max + min) / 2;
|
|
189
382
|
const sd = (max - min) / 4;
|
|
190
383
|
const array = [];
|
|
@@ -199,24 +392,197 @@ function weightedRange(min, max, size = 100, skew = 1) {
|
|
|
199
392
|
return array;
|
|
200
393
|
}
|
|
201
394
|
|
|
395
|
+
function weighArray(arr) {
|
|
202
396
|
|
|
203
|
-
|
|
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
|
+
}
|
|
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
|
+
}
|
|
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} */
|
|
204
510
|
// @ts-ignore
|
|
205
|
-
|
|
206
|
-
|
|
511
|
+
const enrichedArray = arr;
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
enrichedArray.hookPush = transformThenPush;
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
return enrichedArray;
|
|
207
518
|
};
|
|
208
519
|
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
+
);
|
|
214
547
|
}
|
|
215
|
-
|
|
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;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
function progress(thing, p) {
|
|
580
|
+
// @ts-ignore
|
|
581
|
+
readline.cursorTo(process.stdout, 0);
|
|
582
|
+
process.stdout.write(`${thing} processed ... ${comma(p)}`);
|
|
216
583
|
};
|
|
217
584
|
|
|
218
585
|
|
|
219
|
-
//helper to open the finder
|
|
220
586
|
function openFinder(path, callback) {
|
|
221
587
|
path = path || '/';
|
|
222
588
|
let p = spawn('open', [path]);
|
|
@@ -234,56 +600,128 @@ function getUniqueKeys(data) {
|
|
|
234
600
|
return Array.from(keysSet);
|
|
235
601
|
};
|
|
236
602
|
|
|
237
|
-
|
|
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),
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
return user;
|
|
633
|
+
}
|
|
634
|
+
|
|
238
635
|
/**
|
|
239
|
-
*
|
|
240
|
-
* @param {number}
|
|
241
|
-
* @param {
|
|
636
|
+
* build sign waves basically
|
|
637
|
+
* @param {number} [earliestTime]
|
|
638
|
+
* @param {number} [latestTime]
|
|
639
|
+
* @param {number} [peaks=5]
|
|
242
640
|
*/
|
|
243
|
-
function
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
+
}
|
|
250
689
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
};
|
|
255
|
-
};
|
|
690
|
+
}
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
256
693
|
|
|
257
|
-
/** @typedef {import('./types').Person} Person */
|
|
258
694
|
|
|
259
695
|
/**
|
|
260
696
|
* @param {number} bornDaysAgo=30
|
|
261
697
|
* @return {Person}
|
|
262
698
|
*/
|
|
263
699
|
function person(bornDaysAgo = 30) {
|
|
700
|
+
const chance = getChance();
|
|
264
701
|
//names and photos
|
|
265
702
|
let gender = chance.pickone(['male', 'female']);
|
|
266
703
|
if (!gender) gender = "female";
|
|
267
704
|
// @ts-ignore
|
|
268
705
|
const first = chance.first({ gender });
|
|
269
706
|
const last = chance.last();
|
|
270
|
-
const
|
|
271
|
-
const
|
|
707
|
+
const name = `${first} ${last}`;
|
|
708
|
+
const email = `${first[0]}.${last}@${chance.domain()}.com`;
|
|
272
709
|
const avatarPrefix = `https://randomuser.me/api/portraits`;
|
|
273
710
|
const randomAvatarNumber = chance.integer({
|
|
274
711
|
min: 1,
|
|
275
712
|
max: 99
|
|
276
713
|
});
|
|
277
714
|
const avPath = gender === 'male' ? `/men/${randomAvatarNumber}.jpg` : `/women/${randomAvatarNumber}.jpg`;
|
|
278
|
-
const
|
|
279
|
-
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)();
|
|
280
718
|
|
|
281
719
|
/** @type {Person} */
|
|
282
720
|
const user = {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
721
|
+
name,
|
|
722
|
+
email,
|
|
723
|
+
avatar,
|
|
724
|
+
created,
|
|
287
725
|
anonymousIds: [],
|
|
288
726
|
sessionIds: []
|
|
289
727
|
};
|
|
@@ -311,76 +749,59 @@ function person(bornDaysAgo = 30) {
|
|
|
311
749
|
};
|
|
312
750
|
|
|
313
751
|
|
|
314
|
-
function weighList(items, mostChosenIndex) {
|
|
315
|
-
if (mostChosenIndex > items.length) mostChosenIndex = items.length;
|
|
316
|
-
return function () {
|
|
317
|
-
const weighted = [];
|
|
318
|
-
for (let i = 0; i < 10; i++) {
|
|
319
|
-
if (chance.bool({ likelihood: integer(10, 35) })) {
|
|
320
|
-
if (chance.bool({ likelihood: 50 })) {
|
|
321
|
-
weighted.push(items[mostChosenIndex]);
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
const rand = chance.d10();
|
|
325
|
-
const addOrSubtract = chance.bool({ likelihood: 50 }) ? -rand : rand;
|
|
326
|
-
let newIndex = mostChosenIndex + addOrSubtract;
|
|
327
|
-
if (newIndex < 0) newIndex = 0;
|
|
328
|
-
if (newIndex > items.length) newIndex = items.length;
|
|
329
|
-
weighted.push(items[newIndex]);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
weighted.push(chance.pickone(items));
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return weighted;
|
|
337
|
-
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
752
|
|
|
341
753
|
|
|
754
|
+
//UNUSED
|
|
342
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];
|
|
343
780
|
|
|
344
|
-
function streamJSON(path, data) {
|
|
345
|
-
return new Promise((resolve, reject) => {
|
|
346
|
-
const writeStream = fs.createWriteStream(path, { encoding: 'utf8' });
|
|
347
|
-
data.forEach(item => {
|
|
348
|
-
writeStream.write(JSON.stringify(item) + '\n');
|
|
349
|
-
});
|
|
350
|
-
writeStream.end();
|
|
351
|
-
writeStream.on('finish', () => {
|
|
352
|
-
resolve(path);
|
|
353
|
-
});
|
|
354
|
-
writeStream.on('error', reject);
|
|
355
|
-
});
|
|
356
781
|
}
|
|
357
782
|
|
|
358
|
-
function streamCSV(path, data) {
|
|
359
|
-
return new Promise((resolve, reject) => {
|
|
360
|
-
const writeStream = fs.createWriteStream(path, { encoding: 'utf8' });
|
|
361
|
-
// Extract all unique keys from the data array
|
|
362
|
-
const columns = getUniqueKeys(data); // Assuming getUniqueKeys properly retrieves all keys
|
|
363
783
|
|
|
364
|
-
// Stream the header
|
|
365
|
-
writeStream.write(columns.join(',') + '\n');
|
|
366
784
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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();
|
|
792
|
+
return function () {
|
|
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));
|
|
798
|
+
}
|
|
799
|
+
if (array) return arr;
|
|
800
|
+
if (!array) return arr.join(', ');
|
|
801
|
+
return "🤷";
|
|
802
|
+
};
|
|
803
|
+
};
|
|
376
804
|
|
|
377
|
-
writeStream.end();
|
|
378
|
-
writeStream.on('finish', () => {
|
|
379
|
-
resolve(path);
|
|
380
|
-
});
|
|
381
|
-
writeStream.on('error', reject);
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
805
|
|
|
385
806
|
|
|
386
807
|
module.exports = {
|
|
@@ -391,6 +812,13 @@ module.exports = {
|
|
|
391
812
|
choose,
|
|
392
813
|
exhaust,
|
|
393
814
|
integer,
|
|
815
|
+
TimeSoup,
|
|
816
|
+
|
|
817
|
+
generateEmoji,
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
initChance,
|
|
821
|
+
getChance,
|
|
394
822
|
|
|
395
823
|
boxMullerRandom,
|
|
396
824
|
applySkew,
|
|
@@ -400,11 +828,22 @@ module.exports = {
|
|
|
400
828
|
range,
|
|
401
829
|
openFinder,
|
|
402
830
|
getUniqueKeys,
|
|
403
|
-
generateEmoji,
|
|
404
831
|
person,
|
|
405
|
-
|
|
832
|
+
pickAWinner,
|
|
833
|
+
weighArray,
|
|
834
|
+
weighFunnels,
|
|
835
|
+
|
|
836
|
+
shuffleArray,
|
|
837
|
+
shuffleExceptFirst,
|
|
838
|
+
shuffleExceptLast,
|
|
839
|
+
fixFirstAndLast,
|
|
840
|
+
shuffleMiddle,
|
|
841
|
+
shuffleOutside,
|
|
406
842
|
|
|
843
|
+
generateUser,
|
|
844
|
+
enrichArray,
|
|
407
845
|
|
|
846
|
+
buildFileNames,
|
|
408
847
|
streamJSON,
|
|
409
848
|
streamCSV
|
|
410
849
|
};
|