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/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 now = dayjs.utc();
33
- // dates must be in the the last 10 years
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
- try {
37
- const when = chance.integer({ min: 0, max: Math.abs(inTheLast) });
38
- let then;
39
- if (isPast) {
40
- then = now.subtract(when, 'day')
41
- .subtract(integer(0, 23), 'hour')
42
- .subtract(integer(0, 59), 'minute')
43
- .subtract(integer(0, 59), 'second');
44
- }
45
- if (!isPast) {
46
- then = now.add(when, 'day')
47
- .add(integer(0, 23), 'hour')
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
- function day(start, end) {
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(end);
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
- // Box-Muller transform to generate standard normally distributed values
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 = Math.random();
148
- while (v === 0) v = Math.random();
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
- // Apply skewness to the value
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
- function unOptimizedWeightedRange(min, max, size = 100, skew = 1) {
166
- const mean = (max + min) / 2;
167
- const sd = (max - min) / 4;
168
- let array = [];
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
- // Ensure the mapped value is within our min-max range
176
- if (mappedValue >= min && mappedValue <= max) {
177
- array.push(mappedValue);
178
- } else {
179
- i--; // If out of range, redo this iteration
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
- // optimized weighted range
187
- function weightedRange(min, max, size = 100, skew = 1) {
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
- function progress(thing, p) {
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
- readline.cursorTo(process.stdout, 0);
206
- process.stdout.write(`${thing} processed ... ${comma(p)}`);
603
+ const enrichedArray = arr;
604
+
605
+
606
+ enrichedArray.hookPush = transformThenPush;
607
+
608
+
609
+ return enrichedArray;
207
610
  };
208
611
 
209
- function range(a, b, step = 1) {
210
- step = !step ? 1 : step;
211
- b = b / step;
212
- for (var i = a; i <= b; i++) {
213
- this.push(i * step);
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
- return this;
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
- * makes a random-sized array of emojis
240
- * @param {number} max=10
241
- * @param {boolean} array=false
728
+ * build sign waves basically
729
+ * @param {number} [earliestTime]
730
+ * @param {number} [latestTime]
731
+ * @param {number} [peaks=5]
242
732
  */
243
- function generateEmoji(max = 10, array = false) {
244
- return function () {
245
- const emojis = ['😀', '😂', '😍', '😎', '😜', '😇', '😡', '😱', '😭', '😴', '🤢', '🤠', '🤡', '👽', '👻', '💩', '👺', '👹', '👾', '🤖', '🤑', '🤗', '🤓', '🤔', '🤐', '😀', '😂', '😍', '😎', '😜', '😇', '😡', '😱', '😭', '😴', '🤢', '🤠', '🤡', '👽', '👻', '💩', '👺', '👹', '👾', '🤖', '🤑', '🤗', '🤓', '🤔', '🤐', '😈', '👿', '👦', '👧', '👨', '👩', '👴', '👵', '👶', '🧒', '👮', '👷', '💂', '🕵', '👩‍⚕️', '👨‍⚕️', '👩‍🌾', '👨‍🌾', '👩‍🍳', '👨‍🍳', '👩‍🎓', '👨‍🎓', '👩‍🎤', '👨‍🎤', '👩‍🏫', '👨‍🏫', '👩‍🏭', '👨‍🏭', '👩‍💻', '👨‍💻', '👩‍💼', '👨‍💼', '👩‍🔧', '👨‍🔧', '👩‍🔬', '👨‍🔬', '👩‍🎨', '👨‍🎨', '👩‍🚒', '👨‍🚒', '👩‍✈️', '👨‍✈️', '👩‍🚀', '👨‍🚀', '👩‍⚖️', '👨‍⚖️', '🤶', '🎅', '👸', '🤴', '👰', '🤵', '👼', '🤰', '🙇', '💁', '🙅', '🙆', '🙋', '🤦', '🤷', '🙎', '🙍', '💇', '💆', '🕴', '💃', '🕺', '🚶', '🏃', '🤲', '👐', '🙌', '👏', '🤝', '👍', '👎', '👊', '✊', '🤛', '🤜', '🤞', '✌️', '🤟', '🤘', '👌', '👈', '👉', '👆', '👇', '☝️', '✋', '🤚', '🖐', '🖖', '👋', '🤙', '💪', '🖕', '✍️', '🤳', '💅', '👂', '👃', '👣', '👀', '👁', '🧠', '👅', '👄', '💋', '👓', '🕶', '👔', '👕', '👖', '🧣', '🧤', '🧥', '🧦', '👗', '👘', '👙', '👚', '👛', '👜', '👝', '🛍', '🎒', '👞', '👟', '👠', '👡', '👢', '👑', '👒', '🎩', '🎓', '🧢', '⛑', '📿', '💄', '💍', '💎', '🔇', '🔈', '🔉', '🔊', '📢', '📣', '📯', '🔔', '🔕', '🎼', '🎵', '🎶', '🎙', '🎚', '🎛', '🎤', '🎧', '📻', '🎷', '🎸', '🎹', '🎺', '🎻', '🥁', '📱', '📲', '💻', '🖥', '🖨', '🖱', '🖲', '🕹', '🗜', '💽', '💾', '💿', '📀', '📼', '📷', '📸', '📹', '🎥', '📽', '🎞', '📞', '☎️', '📟', '📠', '📺', '📻', '🎙', '📡', '🔍', '🔎', '🔬', '🔭', '📡', '💡', '🔦', '🏮', '📔', '📕', '📖', '📗', '📘', '📙', '📚', '📓', '📒', '📃', '📜', '📄', '📰', '🗞', '📑', '🔖', '🏷', '💰', '💴', '💵', '💶', '💷', '💸', '💳', '🧾', '💹', '💱', '💲', '✉️', '📧', '📨', '📩', '📤', '📥', '📦', '📫', '📪', '📬', '📭', '📮', '🗳', '✏️', '✒️', '🖋', '🖊', '🖌', '🖍', '📝', '💼', '📁', '📂', '🗂', '📅', '📆', '🗒', '🗓', '📇', '📈', '📉', '📊', '📋', '📌', '📍', '📎', '🖇', '📏', '📐', '✂️', '🗃', '🗄', '🗑', '🔒', '🔓', '🔏', '🔐', '🔑', '🗝', '🔨', '⛏', '⚒', '🛠', '🗡', '⚔️', '🔫', '🏹', '🛡', '🔧', '🔩', '⚙️', '🗜', '⚖️', '🔗', '⛓', '🧰', '🧲', '⚗️', '🧪', '🧫', '🧬', '🔬', '🔭', '📡', '💉', '💊', '🛏', '🛋', '🚪', '🚽', '🚿', '🛁', '🧴', '🧷', '🧹', '🧺', '🧻', '🧼', '🧽', '🧯', '🚬', '⚰️', '⚱️', '🗿', '🏺', '🧱', '🎈', '🎏', '🎀', '🎁', '🎊', '🎉', '🎎', '🏮', '🎐', '🧧', '✉️', '📩', '📨', '📧'];
246
- let num = integer(1, max);
247
- let arr = [];
248
- for (let i = 0; i < num; i++) {
249
- arr.push(chance.pickone(emojis));
250
- }
251
- if (array) return arr;
252
- if (!array) return arr.join(', ');
253
- return "🤷";
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 $name = `${first} ${last}`;
271
- const $email = `${first[0]}.${last}@${chance.domain()}.com`;
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 $avatar = avatarPrefix + avPath;
279
- const $created = date(bornDaysAgo, true)();
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
- $name,
284
- $email,
285
- $avatar,
286
- $created,
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
- // Stream each data row
368
- data.forEach(item => {
369
- for (const key in item) {
370
- // Ensure all nested objects are properly stringified
371
- if (typeof item[key] === "object") item[key] = JSON.stringify(item[key]);
372
- }
373
- const row = columns.map(col => item[col] ? `"${item[col].toString().replace(/"/g, '""')}"` : "").join(',');
374
- writeStream.write(row + '\n');
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
- weighList,
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
  };