make-mp-data 1.0.14 โ†’ 1.0.16

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.
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "cSpell.words": [
3
3
  "unparse"
4
- ]
4
+ ],
5
+ "jest.runMode": "on-demand",
6
+ "jest.jestCommandLine": "npm run test --",
7
+ "js/ts.implicitProjectConfig.checkJs": false,
5
8
  }
package/cli.js CHANGED
@@ -25,41 +25,61 @@ DOCS: https://github.com/ak--47/make-mp-data`)
25
25
  .command('$0', 'model mixpanel data', () => { })
26
26
  .option("token", {
27
27
  demandOption: false,
28
+ alias: 't',
28
29
  describe: 'project token; if supplied data will be sent to mixpanel',
29
30
  type: 'string'
30
31
  })
31
32
  .option("seed", {
32
- demandOption: false,
33
+ demandOption: false,
34
+ alias: 's',
33
35
  describe: 'randomness seed; used to create distinct_ids',
34
36
  type: 'string'
35
37
  })
36
38
  .option("format", {
37
39
  demandOption: false,
38
40
  default: 'csv',
41
+ alias: 'f',
39
42
  describe: 'csv or json',
40
43
  type: 'string'
41
44
  })
42
45
  .option("numDays", {
43
46
  demandOption: false,
47
+ alias: 'd',
44
48
  describe: 'number of days in past to model',
45
49
  type: 'number',
46
50
  })
47
51
  .option("numUsers", {
48
52
  demandOption: false,
53
+ alias: 'u',
49
54
  describe: 'number of users to model',
50
55
  type: 'number',
51
56
  })
52
57
  .option("numEvents", {
53
58
  demandOption: false,
59
+ alias: 'e',
54
60
  describe: 'number of events to model',
55
61
  type: 'number',
56
62
  })
57
63
  .option("region", {
58
64
  demandOption: false,
59
65
  default: 'US',
66
+ alias: 'r',
60
67
  describe: 'either US or EU',
61
68
  type: 'string'
62
69
  })
70
+ .option("writeToDisk", {
71
+ demandOption: false,
72
+ default: true,
73
+ describe: 'write data to disk',
74
+ alias: 'w',
75
+ type: 'boolean',
76
+ coerce: (value) => {
77
+ if (typeof value === 'string') {
78
+ return value.toLowerCase() === 'true';
79
+ }
80
+ return value;
81
+ }
82
+ })
63
83
  .help()
64
84
  .wrap(null)
65
85
  .argv;
package/default.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const Chance = require('chance');
2
2
  const chance = new Chance();
3
- const { weightedRange, makeProducts, date } = require('./utils.js');
3
+ const { weightedRange, makeProducts, date, generateEmoji } = require('./utils.js');
4
4
 
5
5
  const config = {
6
6
  token: "",
@@ -36,7 +36,8 @@ const config = {
36
36
  "event": "page view",
37
37
  "weight": 10,
38
38
  "properties": {
39
- path: ["/", "/", "/help", "/account", "/watch", "/listen", "/product", "/people", "/peace"],
39
+ page: ["/", "/", "/help", "/account", "/watch", "/listen", "/product", "/people", "/peace"],
40
+ utm_source: ["$organic", "$organic", "$organic", "$organic", "google", "google", "google", "facebook", "facebook", "twitter", "linkedin"],
40
41
  }
41
42
  },
42
43
  {
@@ -66,7 +67,8 @@ const config = {
66
67
  }
67
68
  ],
68
69
  superProps: {
69
- platform: ["web", "mobile", "kiosk"],
70
+ platform: ["web", "mobile", "web", "mobile", "web", "kiosk"],
71
+ emotions: generateEmoji(),
70
72
 
71
73
  },
72
74
  /*
@@ -76,17 +78,16 @@ const config = {
76
78
  userProps: {
77
79
  title: chance.profession.bind(chance),
78
80
  luckyNumber: weightedRange(42, 420),
79
- servicesUsed: [["foo"], ["foo", "bar"], ["foo", "bar", "baz"], ["foo", "bar", "baz", "qux"], ["baz", "qux"], ["qux"]],
81
+ vibe: generateEmoji(),
80
82
  spiritAnimal: chance.animal.bind(chance)
81
83
  },
82
84
 
83
85
  scdProps: {
84
- plan: ["free", "free", "free", "basic", "basic", "premium", "enterprise"],
86
+ plan: ["free", "free", "free", "free", "basic", "basic", "basic", "premium", "premium", "enterprise"],
85
87
  MRR: weightedRange(0, 10000, 1000, .15),
86
88
  NPS: weightedRange(0, 10, 150, 2),
87
89
  marketingOptIn: [true, true, false],
88
90
  dateOfRenewal: date(100, false),
89
-
90
91
  },
91
92
 
92
93
  /*
package/e2e.test.js ADDED
@@ -0,0 +1,50 @@
1
+ /* cSpell:disable */
2
+ // @ts-nocheck
3
+ /* eslint-disable no-undef */
4
+ /* eslint-disable no-debugger */
5
+ /* eslint-disable no-unused-vars */
6
+ const { generate } = require('./index.js');
7
+ require('dotenv').config();
8
+ const { execSync } = require("child_process");
9
+ const u = require('ak-tools');
10
+
11
+ const timeout = 60000;
12
+
13
+
14
+ describe('e2e', () => {
15
+
16
+ test('works as module', async () => {
17
+ console.log('MODULE TEST');
18
+ const results = await generate({ writeToDisk: false, numEvents: 1000, numUsers: 100, seed: "deal with it" });
19
+ const { eventData, groupProfilesData, lookupTableData, scdTableData, userProfilesData } = results;
20
+ expect(eventData.length).toBeGreaterThan(900);
21
+ expect(groupProfilesData.length).toBe(0);
22
+ expect(lookupTableData.length).toBe(0);
23
+ expect(scdTableData.length).toBeGreaterThan(200);
24
+ expect(userProfilesData.length).toBe(100);
25
+
26
+ }, timeout);
27
+
28
+ test('works as CLI', async () => {
29
+ console.log('CLI TEST');
30
+ const run = execSync(`node ./index.js --numEvents 1000 --numUsers 100 --seed "deal with it"`);
31
+ expect(run.toString().trim().includes('have a wonderful day :)')).toBe(true);
32
+ const csvs = (await u.ls('./data')).filter(a => a.includes('.csv'));
33
+ expect(csvs.length).toBe(5);
34
+ }, timeout);
35
+
36
+
37
+
38
+ });
39
+
40
+
41
+
42
+ afterEach(() => {
43
+
44
+ });
45
+
46
+ afterAll(() => {
47
+ console.log('clearing...');
48
+ execSync(`npm run prune`);
49
+ console.log('...files cleared ๐Ÿ‘');
50
+ });
package/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #! /usr/bin/env node
2
2
 
3
+
3
4
  /*
4
5
  make fake mixpanel data easily!
5
6
  by AK
6
7
  ak@mixpanel.com
7
8
  */
8
9
 
10
+ const RUNTIME = process.env.RUNTIME || "unspecified";
9
11
  const mp = require("mixpanel-import");
10
12
  const path = require("path");
11
13
  const Chance = require("chance");
@@ -25,23 +27,37 @@ const {
25
27
  range,
26
28
  exhaust,
27
29
  openFinder,
30
+ applySkew,
31
+ boxMullerRandom,
32
+ getUniqueKeys
28
33
  } = require("./utils.js");
29
34
  const dayjs = require("dayjs");
30
35
  const utc = require("dayjs/plugin/utc");
31
- const cliParams = require("./cli.js");
32
-
33
36
  dayjs.extend(utc);
37
+ const cliParams = require("./cli.js");
38
+ // @ts-ignore
34
39
  Array.prototype.pickOne = pick;
35
- const now = dayjs().unix();
36
- const dayInSec = 86400;
40
+ const NOW = dayjs().unix();
41
+
42
+ /** @typedef {import('./types.d.ts').Config} Config */
43
+
37
44
  const PEAK_DAYS = [
38
- dayjs().subtract(1, "day").unix(),
45
+ dayjs().subtract(2, "day").unix(),
46
+ dayjs().subtract(3, "day").unix(),
39
47
  dayjs().subtract(5, "day").unix(),
40
- dayjs().subtract(10, "day").unix(),
41
- dayjs().subtract(15, "day").unix(),
48
+ dayjs().subtract(7, "day").unix(),
49
+ dayjs().subtract(11, "day").unix(),
50
+ dayjs().subtract(13, "day").unix(),
51
+ dayjs().subtract(17, "day").unix(),
52
+ dayjs().subtract(19, "day").unix(),
53
+ dayjs().subtract(23, "day").unix(),
54
+ dayjs().subtract(29, "day").unix(),
42
55
  ];
43
56
 
44
- //our main program
57
+ /**
58
+ * generates fake mixpanel data
59
+ * @param {Config} config
60
+ */
45
61
  async function main(config) {
46
62
  let {
47
63
  seed = "every time a rug is micturated upon in this fair city...",
@@ -63,13 +79,31 @@ async function main(config) {
63
79
  region = "US",
64
80
  writeToDisk = false,
65
81
  } = config;
66
- if (require.main === module) writeToDisk = true;
82
+
83
+ //ensure we have a token or are writing to disk
84
+ if (require.main === module) {
85
+ if (!token) {
86
+ if (!writeToDisk) {
87
+ writeToDisk = true;
88
+ config.writeToDisk = true;
89
+ }
90
+ }
91
+ }
92
+
67
93
  const uuidChance = new Chance(seed);
68
94
 
69
- //the function which generates $distinct_id
95
+ //the function which generates $distinct_id + $created, skewing towards the present
70
96
  function uuid() {
71
97
  const distinct_id = uuidChance.guid();
72
- const daysAgoBorn = chance.integer({ min: 1, max: numDays });
98
+ let z = boxMullerRandom();
99
+ const skew = chance.normal({ mean: 10, dev: 3 });
100
+ z = applySkew(z, skew);
101
+
102
+ // Scale and shift the normally distributed value to fit the range of days
103
+ const maxZ = integer(2, 4);
104
+ const scaledZ = (z / maxZ + 1) / 2;
105
+ const daysAgoBorn = Math.round(scaledZ * (numDays - 1)) + 1;
106
+
73
107
  return {
74
108
  distinct_id,
75
109
  ...person(daysAgoBorn),
@@ -199,7 +233,14 @@ async function main(config) {
199
233
  for (const writeData of datasetsToWrite) {
200
234
  if (format === "csv") {
201
235
  console.log(`writing ${path}`);
202
- const csv = Papa.unparse(writeData, {});
236
+ const columns = getUniqueKeys(writeData);
237
+ //papa parse needs nested JSON stringified
238
+ writeData.forEach((e) => {
239
+ for (const key in e) {
240
+ if (typeof e[key] === "object") e[key] = JSON.stringify(e[key]);
241
+ }
242
+ })
243
+ const csv = Papa.unparse(writeData, { columns });
203
244
  await touch(path, csv);
204
245
  console.log(`\tdone\n`);
205
246
  } else {
@@ -214,13 +255,13 @@ async function main(config) {
214
255
  const creds = { token };
215
256
  /** @type {import('mixpanel-import').Options} */
216
257
  const importOpts = {
258
+ region,
217
259
  fixData: true,
218
260
  verbose: false,
219
261
  forceStream: true,
220
262
  strict: false,
221
263
  dryRun: false,
222
264
  abridged: false,
223
- region,
224
265
  };
225
266
  //send to mixpanel
226
267
  if (token) {
@@ -300,25 +341,18 @@ function makeSCD(props, distinct_id, mutations, $created) {
300
341
  return scdEntries;
301
342
  }
302
343
 
303
- function makeEvent(
304
- distinct_id,
305
- earliestTime,
306
- events,
307
- superProps,
308
- groupKeys,
309
- isFirstEvent = false
310
- ) {
344
+ function makeEvent(distinct_id, earliestTime, events, superProps, groupKeys, isFirstEvent = false) {
311
345
  let chosenEvent = events.pickOne();
312
346
  if (typeof chosenEvent === "string")
313
347
  chosenEvent = { event: chosenEvent, properties: {} };
314
348
  const event = {
315
349
  event: chosenEvent.event,
316
350
  distinct_id,
317
- $source: "AK's fake data generator",
351
+ $source: "AKsTimeSoup",
318
352
  };
319
353
 
320
354
  if (isFirstEvent) event.time = earliestTime;
321
- if (!isFirstEvent) event.time = customTimeDistribution(earliestTime, now, PEAK_DAYS);
355
+ if (!isFirstEvent) event.time = AKsTimeSoup(earliestTime, NOW, PEAK_DAYS);
322
356
 
323
357
  const props = { ...chosenEvent.properties, ...superProps };
324
358
 
@@ -375,16 +409,16 @@ function buildFileNames(config) {
375
409
  }
376
410
 
377
411
  /**
378
- * Generates a random timestamp with higher likelihood on peak days and typical business hours.
412
+ * essentially, a timestamp generator with a twist
379
413
  * @param {number} earliestTime - The earliest timestamp in Unix format.
380
414
  * @param {number} latestTime - The latest timestamp in Unix format.
381
415
  * @param {Array} peakDays - Array of Unix timestamps representing the start of peak days.
382
416
  * @returns {number} - The generated event timestamp in Unix format.
383
417
  */
384
- function customTimeDistribution(earliestTime, latestTime, peakDays) {
418
+ function AKsTimeSoup(earliestTime, latestTime = NOW, peakDays = PEAK_DAYS) {
385
419
  // Define business hours
386
- const peakStartHour = 8; // 8 AM
387
- const peakEndHour = 18; // 6 PM
420
+ const peakStartHour = 4; // 4 AM
421
+ const peakEndHour = 23; // 11 PM
388
422
  const likelihoodOfPeakDay = chance.integer({ min: integer(5, 42), max: integer(43, 69) }); // Randomize likelihood with CHAOS!~~
389
423
 
390
424
  // Select a day, with a preference for peak days
@@ -393,7 +427,7 @@ function customTimeDistribution(earliestTime, latestTime, peakDays) {
393
427
  selectedDay = peakDays.length > 0 ? chance.pickone(peakDays) : integer(earliestTime, latestTime);
394
428
  } else {
395
429
  // Introduce minor peaks by allowing some events to still occur during business hours
396
- selectedDay = chance.bool({ likelihood: 20 }) // 20% chance to simulate a minor peak on a non-peak day
430
+ selectedDay = chance.bool({ likelihood: integer(1, 42) })
397
431
  ? chance.pickone(peakDays)
398
432
  : integer(earliestTime, latestTime);
399
433
  }
@@ -407,12 +441,14 @@ function customTimeDistribution(earliestTime, latestTime, peakDays) {
407
441
  let eventTime;
408
442
  if (selectedDay === peakDays[0]) {
409
443
  // Use a skewed distribution for peak days
410
- eventTime = chance.normal({ mean: (businessEnd + businessStart) / 2, dev: (businessEnd - businessStart) / 8 });
444
+ eventTime = chance.normal({ mean: (businessEnd + businessStart) / integer(1, 4), dev: (businessEnd - businessStart) / integer(2, 8) });
411
445
  } else {
412
446
  // For non-peak days, use a uniform distribution to add noise
413
- eventTime = integer(businessStart, businessEnd);
447
+ eventTime = integer(integer(businessStart, businessEnd), integer(businessStart, businessEnd));
414
448
  }
415
- eventTime = Math.min(Math.max(eventTime, businessStart), businessEnd); // Ensure time is within business hours
449
+
450
+ // usually, ensure the event time is within business hours
451
+ if (chance.bool({ likelihood: 42 })) eventTime = Math.min(Math.max(eventTime, businessStart), businessEnd);
416
452
 
417
453
  return eventTime;
418
454
  }
@@ -422,7 +458,7 @@ function customTimeDistribution(earliestTime, latestTime, peakDays) {
422
458
  // this is for CLI
423
459
  if (require.main === module) {
424
460
  const args = cliParams();
425
- const { token, seed, format, numDays, numUsers, numEvents, region } = args;
461
+ const { token, seed, format, numDays, numUsers, numEvents, region, writeToDisk } = args;
426
462
  const suppliedConfig = args._[0];
427
463
 
428
464
  //if the user specifics an separate config file
@@ -443,6 +479,7 @@ if (require.main === module) {
443
479
  if (numUsers) config.numUsers = numUsers;
444
480
  if (numEvents) config.numEvents = numEvents;
445
481
  if (region) config.region = region;
482
+ if (writeToDisk) config.writeToDisk = writeToDisk;
446
483
 
447
484
  main(config)
448
485
  .then((data) => {
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "make-mp-data",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "builds all mixpanel primitives for a given project",
5
5
  "main": "index.js",
6
+ "types": "types.d.ts",
6
7
  "scripts": {
7
8
  "start": "node index.js",
8
9
  "prune": "rm ./data/*",
9
10
  "go": "sh ./go.sh",
10
- "post": "npm publish"
11
+ "post": "npm publish",
12
+ "test": "jest"
11
13
  },
12
14
  "repository": {
13
15
  "type": "git",
package/types.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ // types.d.ts
2
+ import { Chance } from "chance";
3
+
4
+ export interface Config {
5
+ token?: string;
6
+ seed?: string;
7
+ numDays?: number;
8
+ numEvents?: number;
9
+ numUsers?: number;
10
+ format?: "csv" | "json";
11
+ region?: string;
12
+ events?: EventConfig[];
13
+ superProps?: Record<string, string[]>; // Flexible for any string keys
14
+ userProps?: Record<string, any>; // Could be more specific based on actual usage
15
+ scdProps?: {
16
+ plan?: string[];
17
+ MRR?: number;
18
+ NPS?: number;
19
+ marketingOptIn?: boolean[];
20
+ dateOfRenewal?: Date;
21
+ };
22
+ groupKeys?: [string, number][];
23
+ groupProps?: Record<string, GroupProperty>; // Adjust according to usage
24
+ lookupTables?: LookupTable[];
25
+ writeToDisk?: boolean;
26
+ }
27
+
28
+ interface EventConfig {
29
+ event?: string;
30
+ weight?: number;
31
+ properties?: {
32
+ [key: string]: any; // Consider refining based on actual properties used
33
+ };
34
+ isFirstEvent?: boolean;
35
+ }
36
+
37
+ interface GroupProperty {
38
+ [key?: string]: any;
39
+ }
40
+
41
+ interface LookupTable {
42
+ key?: string;
43
+ entries?: number;
44
+ attributes?: {
45
+ category?: string[];
46
+ demand?: string[];
47
+ supply?: string[];
48
+ manufacturer?: () => string;
49
+ price?: number;
50
+ rating?: number;
51
+ reviews?: number;
52
+ };
53
+ }
package/utils.js CHANGED
@@ -20,12 +20,28 @@ function pick() {
20
20
  function date(inTheLast = 30, isPast = true, format = 'YYYY-MM-DD') {
21
21
  const now = dayjs.utc();
22
22
  return function () {
23
- const when = chance.integer({ min: 0, max: inTheLast });
24
- let then;
25
- if (isPast) then = now.subtract(when, 'day');
26
- if (!isPast) then = now.add(when, 'day');
27
- if (format) return then.format(format);
28
- if (!format) return then.toISOString();
23
+ try {
24
+ const when = chance.integer({ min: 0, max: Math.abs(inTheLast) });
25
+ let then;
26
+ if (isPast) {
27
+ then = now.subtract(when, 'day')
28
+ .subtract(integer(0, 23), 'hour')
29
+ .subtract(integer(0, 59), 'minute')
30
+ .subtract(integer(0, 59), 'second');
31
+ }
32
+ if (!isPast) {
33
+ then = now.add(when, 'day')
34
+ .add(integer(0, 23), 'hour')
35
+ .add(integer(0, 59), 'minute')
36
+ .add(integer(0, 59), 'second');
37
+ }
38
+ if (format) return then?.format(format);
39
+ if (!format) return then?.toISOString();
40
+ }
41
+ catch (e) {
42
+ if (format) return now?.format(format);
43
+ if (!format) return now?.toISOString();
44
+ }
29
45
  };
30
46
  }
31
47
 
@@ -74,6 +90,10 @@ function exhaust(arr) {
74
90
 
75
91
 
76
92
  function integer(min, max) {
93
+ if (min === max) {
94
+ return min;
95
+ }
96
+
77
97
  if (min > max) {
78
98
  return chance.integer({
79
99
  min: max,
@@ -81,10 +101,14 @@ function integer(min, max) {
81
101
  });
82
102
  }
83
103
 
84
- return chance.integer({
85
- min: min,
86
- max: max
87
- });
104
+ if (min < max) {
105
+ return chance.integer({
106
+ min: min,
107
+ max: max
108
+ });
109
+ }
110
+
111
+ return 0;
88
112
  }
89
113
 
90
114
  function makeProducts() {
@@ -168,7 +192,6 @@ function progress(thing, p) {
168
192
  process.stdout.write(`${thing} processed ... ${comma(p)}`);
169
193
  }
170
194
 
171
-
172
195
  function person(bornDaysAgo = 30) {
173
196
  //names and photos
174
197
  const gender = chance.pickone(['male', 'female']);
@@ -213,7 +236,27 @@ function openFinder(path, callback) {
213
236
  });
214
237
  }
215
238
 
239
+ function getUniqueKeys(data) {
240
+ const keysSet = new Set();
241
+ data.forEach(item => {
242
+ Object.keys(item).forEach(key => keysSet.add(key));
243
+ });
244
+ return Array.from(keysSet);
245
+ }
216
246
 
247
+ //makes a random-sized array of emojis
248
+ function generateEmoji(max = 10, array = false) {
249
+ return function () {
250
+ const emojis = ['๐Ÿ˜€', '๐Ÿ˜‚', '๐Ÿ˜', '๐Ÿ˜Ž', '๐Ÿ˜œ', '๐Ÿ˜‡', '๐Ÿ˜ก', '๐Ÿ˜ฑ', '๐Ÿ˜ญ', '๐Ÿ˜ด', '๐Ÿคข', '๐Ÿค ', '๐Ÿคก', '๐Ÿ‘ฝ', '๐Ÿ‘ป', '๐Ÿ’ฉ', '๐Ÿ‘บ', '๐Ÿ‘น', '๐Ÿ‘พ', '๐Ÿค–', '๐Ÿค‘', '๐Ÿค—', '๐Ÿค“', '๐Ÿค”', '๐Ÿค', '๐Ÿ˜€', '๐Ÿ˜‚', '๐Ÿ˜', '๐Ÿ˜Ž', '๐Ÿ˜œ', '๐Ÿ˜‡', '๐Ÿ˜ก', '๐Ÿ˜ฑ', '๐Ÿ˜ญ', '๐Ÿ˜ด', '๐Ÿคข', '๐Ÿค ', '๐Ÿคก', '๐Ÿ‘ฝ', '๐Ÿ‘ป', '๐Ÿ’ฉ', '๐Ÿ‘บ', '๐Ÿ‘น', '๐Ÿ‘พ', '๐Ÿค–', '๐Ÿค‘', '๐Ÿค—', '๐Ÿค“', '๐Ÿค”', '๐Ÿค', '๐Ÿ˜ˆ', '๐Ÿ‘ฟ', '๐Ÿ‘ฆ', '๐Ÿ‘ง', '๐Ÿ‘จ', '๐Ÿ‘ฉ', '๐Ÿ‘ด', '๐Ÿ‘ต', '๐Ÿ‘ถ', '๐Ÿง’', '๐Ÿ‘ฎ', '๐Ÿ‘ท', '๐Ÿ’‚', '๐Ÿ•ต', '๐Ÿ‘ฉโ€โš•๏ธ', '๐Ÿ‘จโ€โš•๏ธ', '๐Ÿ‘ฉโ€๐ŸŒพ', '๐Ÿ‘จโ€๐ŸŒพ', '๐Ÿ‘ฉโ€๐Ÿณ', '๐Ÿ‘จโ€๐Ÿณ', '๐Ÿ‘ฉโ€๐ŸŽ“', '๐Ÿ‘จโ€๐ŸŽ“', '๐Ÿ‘ฉโ€๐ŸŽค', '๐Ÿ‘จโ€๐ŸŽค', '๐Ÿ‘ฉโ€๐Ÿซ', '๐Ÿ‘จโ€๐Ÿซ', '๐Ÿ‘ฉโ€๐Ÿญ', '๐Ÿ‘จโ€๐Ÿญ', '๐Ÿ‘ฉโ€๐Ÿ’ป', '๐Ÿ‘จโ€๐Ÿ’ป', '๐Ÿ‘ฉโ€๐Ÿ’ผ', '๐Ÿ‘จโ€๐Ÿ’ผ', '๐Ÿ‘ฉโ€๐Ÿ”ง', '๐Ÿ‘จโ€๐Ÿ”ง', '๐Ÿ‘ฉโ€๐Ÿ”ฌ', '๐Ÿ‘จโ€๐Ÿ”ฌ', '๐Ÿ‘ฉโ€๐ŸŽจ', '๐Ÿ‘จโ€๐ŸŽจ', '๐Ÿ‘ฉโ€๐Ÿš’', '๐Ÿ‘จโ€๐Ÿš’', '๐Ÿ‘ฉโ€โœˆ๏ธ', '๐Ÿ‘จโ€โœˆ๏ธ', '๐Ÿ‘ฉโ€๐Ÿš€', '๐Ÿ‘จโ€๐Ÿš€', '๐Ÿ‘ฉโ€โš–๏ธ', '๐Ÿ‘จโ€โš–๏ธ', '๐Ÿคถ', '๐ŸŽ…', '๐Ÿ‘ธ', '๐Ÿคด', '๐Ÿ‘ฐ', '๐Ÿคต', '๐Ÿ‘ผ', '๐Ÿคฐ', '๐Ÿ™‡', '๐Ÿ’', '๐Ÿ™…', '๐Ÿ™†', '๐Ÿ™‹', '๐Ÿคฆ', '๐Ÿคท', '๐Ÿ™Ž', '๐Ÿ™', '๐Ÿ’‡', '๐Ÿ’†', '๐Ÿ•ด', '๐Ÿ’ƒ', '๐Ÿ•บ', '๐Ÿšถ', '๐Ÿƒ', '๐Ÿคฒ', '๐Ÿ‘', '๐Ÿ™Œ', '๐Ÿ‘', '๐Ÿค', '๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿ‘Š', 'โœŠ', '๐Ÿค›', '๐Ÿคœ', '๐Ÿคž', 'โœŒ๏ธ', '๐ŸคŸ', '๐Ÿค˜', '๐Ÿ‘Œ', '๐Ÿ‘ˆ', '๐Ÿ‘‰', '๐Ÿ‘†', '๐Ÿ‘‡', 'โ˜๏ธ', 'โœ‹', '๐Ÿคš', '๐Ÿ–', '๐Ÿ––', '๐Ÿ‘‹', '๐Ÿค™', '๐Ÿ’ช', '๐Ÿ–•', 'โœ๏ธ', '๐Ÿคณ', '๐Ÿ’…', '๐Ÿ‘‚', '๐Ÿ‘ƒ', '๐Ÿ‘ฃ', '๐Ÿ‘€', '๐Ÿ‘', '๐Ÿง ', '๐Ÿ‘…', '๐Ÿ‘„', '๐Ÿ’‹', '๐Ÿ‘“', '๐Ÿ•ถ', '๐Ÿ‘”', '๐Ÿ‘•', '๐Ÿ‘–', '๐Ÿงฃ', '๐Ÿงค', '๐Ÿงฅ', '๐Ÿงฆ', '๐Ÿ‘—', '๐Ÿ‘˜', '๐Ÿ‘™', '๐Ÿ‘š', '๐Ÿ‘›', '๐Ÿ‘œ', '๐Ÿ‘', '๐Ÿ›', '๐ŸŽ’', '๐Ÿ‘ž', '๐Ÿ‘Ÿ', '๐Ÿ‘ ', '๐Ÿ‘ก', '๐Ÿ‘ข', '๐Ÿ‘‘', '๐Ÿ‘’', '๐ŸŽฉ', '๐ŸŽ“', '๐Ÿงข', 'โ›‘', '๐Ÿ“ฟ', '๐Ÿ’„', '๐Ÿ’', '๐Ÿ’Ž', '๐Ÿ”‡', '๐Ÿ”ˆ', '๐Ÿ”‰', '๐Ÿ”Š', '๐Ÿ“ข', '๐Ÿ“ฃ', '๐Ÿ“ฏ', '๐Ÿ””', '๐Ÿ”•', '๐ŸŽผ', '๐ŸŽต', '๐ŸŽถ', '๐ŸŽ™', '๐ŸŽš', '๐ŸŽ›', '๐ŸŽค', '๐ŸŽง', '๐Ÿ“ป', '๐ŸŽท', '๐ŸŽธ', '๐ŸŽน', '๐ŸŽบ', '๐ŸŽป', '๐Ÿฅ', '๐Ÿ“ฑ', '๐Ÿ“ฒ', '๐Ÿ’ป', '๐Ÿ–ฅ', '๐Ÿ–จ', '๐Ÿ–ฑ', '๐Ÿ–ฒ', '๐Ÿ•น', '๐Ÿ—œ', '๐Ÿ’ฝ', '๐Ÿ’พ', '๐Ÿ’ฟ', '๐Ÿ“€', '๐Ÿ“ผ', '๐Ÿ“ท', '๐Ÿ“ธ', '๐Ÿ“น', '๐ŸŽฅ', '๐Ÿ“ฝ', '๐ŸŽž', '๐Ÿ“ž', 'โ˜Ž๏ธ', '๐Ÿ“Ÿ', '๐Ÿ“ ', '๐Ÿ“บ', '๐Ÿ“ป', '๐ŸŽ™', '๐Ÿ“ก', '๐Ÿ”', '๐Ÿ”Ž', '๐Ÿ”ฌ', '๐Ÿ”ญ', '๐Ÿ“ก', '๐Ÿ’ก', '๐Ÿ”ฆ', '๐Ÿฎ', '๐Ÿ“”', '๐Ÿ“•', '๐Ÿ“–', '๐Ÿ“—', '๐Ÿ“˜', '๐Ÿ“™', '๐Ÿ“š', '๐Ÿ““', '๐Ÿ“’', '๐Ÿ“ƒ', '๐Ÿ“œ', '๐Ÿ“„', '๐Ÿ“ฐ', '๐Ÿ—ž', '๐Ÿ“‘', '๐Ÿ”–', '๐Ÿท', '๐Ÿ’ฐ', '๐Ÿ’ด', '๐Ÿ’ต', '๐Ÿ’ถ', '๐Ÿ’ท', '๐Ÿ’ธ', '๐Ÿ’ณ', '๐Ÿงพ', '๐Ÿ’น', '๐Ÿ’ฑ', '๐Ÿ’ฒ', 'โœ‰๏ธ', '๐Ÿ“ง', '๐Ÿ“จ', '๐Ÿ“ฉ', '๐Ÿ“ค', '๐Ÿ“ฅ', '๐Ÿ“ฆ', '๐Ÿ“ซ', '๐Ÿ“ช', '๐Ÿ“ฌ', '๐Ÿ“ญ', '๐Ÿ“ฎ', '๐Ÿ—ณ', 'โœ๏ธ', 'โœ’๏ธ', '๐Ÿ–‹', '๐Ÿ–Š', '๐Ÿ–Œ', '๐Ÿ–', '๐Ÿ“', '๐Ÿ’ผ', '๐Ÿ“', '๐Ÿ“‚', '๐Ÿ—‚', '๐Ÿ“…', '๐Ÿ“†', '๐Ÿ—’', '๐Ÿ—“', '๐Ÿ“‡', '๐Ÿ“ˆ', '๐Ÿ“‰', '๐Ÿ“Š', '๐Ÿ“‹', '๐Ÿ“Œ', '๐Ÿ“', '๐Ÿ“Ž', '๐Ÿ–‡', '๐Ÿ“', '๐Ÿ“', 'โœ‚๏ธ', '๐Ÿ—ƒ', '๐Ÿ—„', '๐Ÿ—‘', '๐Ÿ”’', '๐Ÿ”“', '๐Ÿ”', '๐Ÿ”', '๐Ÿ”‘', '๐Ÿ—', '๐Ÿ”จ', 'โ›', 'โš’', '๐Ÿ› ', '๐Ÿ—ก', 'โš”๏ธ', '๐Ÿ”ซ', '๐Ÿน', '๐Ÿ›ก', '๐Ÿ”ง', '๐Ÿ”ฉ', 'โš™๏ธ', '๐Ÿ—œ', 'โš–๏ธ', '๐Ÿ”—', 'โ›“', '๐Ÿงฐ', '๐Ÿงฒ', 'โš—๏ธ', '๐Ÿงช', '๐Ÿงซ', '๐Ÿงฌ', '๐Ÿ”ฌ', '๐Ÿ”ญ', '๐Ÿ“ก', '๐Ÿ’‰', '๐Ÿ’Š', '๐Ÿ›', '๐Ÿ›‹', '๐Ÿšช', '๐Ÿšฝ', '๐Ÿšฟ', '๐Ÿ›', '๐Ÿงด', '๐Ÿงท', '๐Ÿงน', '๐Ÿงบ', '๐Ÿงป', '๐Ÿงผ', '๐Ÿงฝ', '๐Ÿงฏ', '๐Ÿšฌ', 'โšฐ๏ธ', 'โšฑ๏ธ', '๐Ÿ—ฟ', '๐Ÿบ', '๐Ÿงฑ', '๐ŸŽˆ', '๐ŸŽ', '๐ŸŽ€', '๐ŸŽ', '๐ŸŽŠ', '๐ŸŽ‰', '๐ŸŽŽ', '๐Ÿฎ', '๐ŸŽ', '๐Ÿงง', 'โœ‰๏ธ', '๐Ÿ“ฉ', '๐Ÿ“จ', '๐Ÿ“ง'];
251
+ let num = integer(1, max);
252
+ let arr = [];
253
+ for (let i = 0; i < num; i++) {
254
+ arr.push(chance.pickone(emojis));
255
+ }
256
+ if (array) return arr;
257
+ if (!array) return arr.join(', ');
258
+ };
259
+ }
217
260
 
218
261
  module.exports = {
219
262
  weightedRange,
@@ -227,5 +270,9 @@ module.exports = {
227
270
  choose,
228
271
  range,
229
272
  exhaust,
230
- openFinder
273
+ openFinder,
274
+ applySkew,
275
+ boxMullerRandom,
276
+ generateEmoji,
277
+ getUniqueKeys
231
278
  };