make-mp-data 1.3.4 → 1.4.0

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