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