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/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 now = dayjs.utc();
29
- // dates must be in the the last 10 years
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
- try {
33
- const when = chance.integer({ min: 0, max: Math.abs(inTheLast) });
34
- let then;
35
- if (isPast) {
36
- then = now.subtract(when, 'day')
37
- .subtract(integer(0, 23), 'hour')
38
- .subtract(integer(0, 59), 'minute')
39
- .subtract(integer(0, 59), 'second');
40
- }
41
- if (!isPast) {
42
- then = now.add(when, 'day')
43
- .add(integer(0, 23), 'hour')
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
- function day(start, end) {
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(end);
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
- function integer(min, max) {
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
- // Box-Muller transform to generate standard normally distributed values
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 = Math.random();
141
- while (v === 0) v = Math.random();
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
- // Apply skewness to the value
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
- function unOptimizedWeightedRange(min, max, size = 100, skew = 1) {
159
- const mean = (max + min) / 2;
160
- const sd = (max - min) / 4;
161
- let array = [];
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
- // Ensure the mapped value is within our min-max range
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
- // optimized weighted range
180
- function weightedRange(min, max, size = 100, skew = 1) {
181
- const mean = (max + min) / 2;
182
- const sd = (max - min) / 4;
183
- const array = [];
184
- while (array.length < size) {
185
- const normalValue = boxMullerRandom();
186
- const skewedValue = applySkew(normalValue, skew);
187
- const mappedValue = mapToRange(skewedValue, mean, sd);
188
- if (mappedValue >= min && mappedValue <= max) {
189
- array.push(mappedValue);
190
- }
191
- }
192
- return array;
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
- //makes a random-sized array of emojis
230
- function generateEmoji(max = 10, array = false) {
231
- return function () {
232
- const emojis = ['😀', '😂', '😍', '😎', '😜', '😇', '😡', '😱', '😭', '😴', '🤢', '🤠', '🤡', '👽', '👻', '💩', '👺', '👹', '👾', '🤖', '🤑', '🤗', '🤓', '🤔', '🤐', '😀', '😂', '😍', '😎', '😜', '😇', '😡', '😱', '😭', '😴', '🤢', '🤠', '🤡', '👽', '👻', '💩', '👺', '👹', '👾', '🤖', '🤑', '🤗', '🤓', '🤔', '🤐', '😈', '👿', '👦', '👧', '👨', '👩', '👴', '👵', '👶', '🧒', '👮', '👷', '💂', '🕵', '👩‍⚕️', '👨‍⚕️', '👩‍🌾', '👨‍🌾', '👩‍🍳', '👨‍🍳', '👩‍🎓', '👨‍🎓', '👩‍🎤', '👨‍🎤', '👩‍🏫', '👨‍🏫', '👩‍🏭', '👨‍🏭', '👩‍💻', '👨‍💻', '👩‍💼', '👨‍💼', '👩‍🔧', '👨‍🔧', '👩‍🔬', '👨‍🔬', '👩‍🎨', '👨‍🎨', '👩‍🚒', '👨‍🚒', '👩‍✈️', '👨‍✈️', '👩‍🚀', '👨‍🚀', '👩‍⚖️', '👨‍⚖️', '🤶', '🎅', '👸', '🤴', '👰', '🤵', '👼', '🤰', '🙇', '💁', '🙅', '🙆', '🙋', '🤦', '🤷', '🙎', '🙍', '💇', '💆', '🕴', '💃', '🕺', '🚶', '🏃', '🤲', '👐', '🙌', '👏', '🤝', '👍', '👎', '👊', '✊', '🤛', '🤜', '🤞', '✌️', '🤟', '🤘', '👌', '👈', '👉', '👆', '👇', '☝️', '✋', '🤚', '🖐', '🖖', '👋', '🤙', '💪', '🖕', '✍️', '🤳', '💅', '👂', '👃', '👣', '👀', '👁', '🧠', '👅', '👄', '💋', '👓', '🕶', '👔', '👕', '👖', '🧣', '🧤', '🧥', '🧦', '👗', '👘', '👙', '👚', '👛', '👜', '👝', '🛍', '🎒', '👞', '👟', '👠', '👡', '👢', '👑', '👒', '🎩', '🎓', '🧢', '⛑', '📿', '💄', '💍', '💎', '🔇', '🔈', '🔉', '🔊', '📢', '📣', '📯', '🔔', '🔕', '🎼', '🎵', '🎶', '🎙', '🎚', '🎛', '🎤', '🎧', '📻', '🎷', '🎸', '🎹', '🎺', '🎻', '🥁', '📱', '📲', '💻', '🖥', '🖨', '🖱', '🖲', '🕹', '🗜', '💽', '💾', '💿', '📀', '📼', '📷', '📸', '📹', '🎥', '📽', '🎞', '📞', '☎️', '📟', '📠', '📺', '📻', '🎙', '📡', '🔍', '🔎', '🔬', '🔭', '📡', '💡', '🔦', '🏮', '📔', '📕', '📖', '📗', '📘', '📙', '📚', '📓', '📒', '📃', '📜', '📄', '📰', '🗞', '📑', '🔖', '🏷', '💰', '💴', '💵', '💶', '💷', '💸', '💳', '🧾', '💹', '💱', '💲', '✉️', '📧', '📨', '📩', '📤', '📥', '📦', '📫', '📪', '📬', '📭', '📮', '🗳', '✏️', '✒️', '🖋', '🖊', '🖌', '🖍', '📝', '💼', '📁', '📂', '🗂', '📅', '📆', '🗒', '🗓', '📇', '📈', '📉', '📊', '📋', '📌', '📍', '📎', '🖇', '📏', '📐', '✂️', '🗃', '🗄', '🗑', '🔒', '🔓', '🔏', '🔐', '🔑', '🗝', '🔨', '⛏', '⚒', '🛠', '🗡', '⚔️', '🔫', '🏹', '🛡', '🔧', '🔩', '⚙️', '🗜', '⚖️', '🔗', '⛓', '🧰', '🧲', '⚗️', '🧪', '🧫', '🧬', '🔬', '🔭', '📡', '💉', '💊', '🛏', '🛋', '🚪', '🚽', '🚿', '🛁', '🧴', '🧷', '🧹', '🧺', '🧻', '🧼', '🧽', '🧯', '🚬', '⚰️', '⚱️', '🗿', '🏺', '🧱', '🎈', '🎏', '🎀', '🎁', '🎊', '🎉', '🎎', '🏮', '🎐', '🧧', '✉️', '📩', '📨', '📧'];
233
- let num = integer(1, max);
234
- let arr = [];
235
- for (let i = 0; i < num; i++) {
236
- arr.push(chance.pickone(emojis));
237
- }
238
- if (array) return arr;
239
- if (!array) return arr.join(', ');
240
- return "🤷";
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
- const gender = chance.pickone(['male', 'female']);
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 $name = `${first} ${last}`;
251
- const $email = `${first[0]}.${last}@${chance.domain()}.com`;
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 $avatar = avatarPrefix + avPath;
259
- const $created = date(bornDaysAgo, true, null)();
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
- $name,
263
- $email,
264
- $avatar,
265
- $created,
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
- anonymousIds.push(uid(42));
733
+ const anonId = uid(42);
734
+ user.anonymousIds.push(anonId);
274
735
  }
275
- user.anonymousIds = anonymousIds;
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
- sessionIds.push([uid(5), uid(5), uid(5), uid(5)].join("-"));
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
- function weighList(items, mostChosenIndex) {
293
- if (mostChosenIndex > items.length) mostChosenIndex = items.length;
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 weighted = [];
296
- for (let i = 0; i < 10; i++) {
297
- if (chance.bool({ likelihood: integer(10, 35) })) {
298
- if (chance.bool({ likelihood: 50 })) {
299
- weighted.push(items[mostChosenIndex]);
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 weighted;
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
- weighList
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
  };