ak-tools
Version:
AK's collections of useful things... a comprehensive utility toolbelt for JavaScript/TypeScript projects
1,735 lines (1,561 loc) • 68.5 kB
JavaScript
// AK's utils
// things to make things ... easier
const IS_NODE = typeof window === 'undefined';
let path, fs, existsSync, mkdirSync, statSync, readdirSync, mkdir, readline, http, os;
if (IS_NODE) {
path = require('path');
fs = require('fs').promises;
({ existsSync, mkdirSync, statSync, readdirSync } = require('fs'));
({ mkdir } = require('fs').promises);
readline = require('readline');
http = require('https');
os = require('os');
} else {
// Provide safe browser fallbacks or empty implementations
path = {};
fs = {};
existsSync = () => false;
mkdirSync = () => undefined;
statSync = () => undefined;
readdirSync = () => [];
mkdir = async () => undefined;
readline = {};
http = {};
os = { platform: () => 'browser' };
}
/*
-------------------
NAMESPACES + TYPES
-------------------
*/
/**
* file management utilities
* @namespace files
*/
/**
* data validation utilities
* @namespace validate
*/
/**
* display, formatting, and other "make it look right" utilities
* @namespace display
*/
/**
* functions for maths, crypto, and maths
* @namespace maths
*/
/**
* object utilities
* @namespace objects
*/
/**
* array utilities
* @namespace arrays
*/
/**
* function utilities
* @namespace functions
*/
/**
* logging, timers and other diagnostic utilities
* @namespace logging
*/
/**
* Generic object with string keys
* @template T
* @typedef {Record<string, T>} Dictionary
*/
/**
* Generic object with any value types
* @typedef {Record<string, unknown>} AnyObject
*/
/**
* Array of objects
* @template T
* @typedef {T[]} ArrayOf
*/
/**
* Constructor function type
* @template T
* @typedef {new (...args: any[]) => T} Constructor
*/
/*
------
FILES
------
*/
/**
* list directory contents
* @example
* await ls('./tmp') // => ['/absolute/path/file1.txt', '/absolute/path/file2.txt']
* await ls('./tmp', true) // => {'file1.txt': '/absolute/path/file1.txt', 'file2.txt': '/absolute/path/file2.txt'}
* @param {string} [dir='./'] - directory to enumerate; default `./`
* @param {boolean} [objectMode=false] - return `{name: path}` instead of `[path]`; default `false`
* @returns {Promise<string[] | Dictionary<string>>} array of paths or object mapping names to paths
* @memberof files
*/
exports.ls = async function listFiles(dir = "./", objectMode = false) {
let fileList = await fs.readdir(dir);
if (!objectMode) {
return fileList.map(fileName => path.resolve(`${dir}/${fileName}`));
}
let results = {};
for (const fileName of fileList) {
// let keyName = fileName.split('.')
results[fileName] = path.resolve(`${dir}/${fileName}`);
}
return results;
};
/**
* remove a file or directory
* @example
* await rm('./myfile.txt') // => undefined (success) or throws error
* @param {string} fileNameOrPath - file or path to be removed
* @param {boolean} [log=true] - whether to log errors
* @param {boolean} [throws=true] - whether to throw errors or return false
* @returns {Promise<void | false>} undefined on success, false on failure (if throws=false)
* @memberof files
*/
exports.rm = async function removeFileOrFolder(fileNameOrPath, log = true, throws = true) {
let fileRemoved;
try {
fileRemoved = await fs.unlink(path.resolve(fileNameOrPath));
} catch (e) {
try {
fileRemoved = await fs.rm(path.resolve(fileNameOrPath), { recursive: true, force: true });
} catch (e) {
if (log) {
console.error(`${fileNameOrPath} not removed!`);
console.error(e);
}
if (throws) {
throw e;
}
return false;
}
}
return fileRemoved;
};
/**
* create a file
* @example
* await touch('newfile.txt', 'hello world') // => '/absolute/path/to/newfile.txt'
* await touch('newfile.json', {foo: 'bar'}, true) // => '/absolute/path/to/newfile.json'
* @param {string} fileNameOrPath - file to create
* @param {string | AnyObject | unknown[]} [data=""] - data to write; default `""`
* @param {boolean} [isJson=false] - whether to JSON.stringify the data; default `false`
* @param {boolean} [log=true] - whether to log errors
* @param {boolean} [throws=true] - whether to throw errors or return false
* @returns {Promise<string | false>} absolute path of created file, or false on failure
* @memberof files
*/
exports.touch = async function addFile(fileNameOrPath, data = "", isJson = false, log = true, throws = true) {
let fileCreated;
let dataToWrite = isJson ? exports.json(data) : data;
try {
//@ts-ignore
fileCreated = await fs.writeFile(path.resolve(fileNameOrPath), dataToWrite, 'utf-8');
} catch (e) {
if (log) {
console.error(`${fileNameOrPath} not created!`);
console.error(e);
}
if (throws) {
throw e;
}
return false;
}
return path.resolve(fileNameOrPath);
};
/**
* load a file into memory
* @example
* await load('myfile.txt') // => 'my file contents'
* await load('myfile.json', true) // => {my: "data"}
* @template T
* @param {string} fileNameOrPath - file to load
* @param {boolean} [isJson=false] - whether to parse as JSON; default `false`
* @param {BufferEncoding} [encoding='utf-8'] - file encoding; default `utf-8`
* @param {boolean} [log=true] - whether to log errors
* @param {boolean} [throws=true] - whether to throw errors or return undefined
* @returns {Promise<string | T | undefined>} file contents as string or parsed JSON
* @memberof files
*/
exports.load = async function loadFile(fileNameOrPath, isJson = false, encoding = 'utf-8', log = true, throws = true) {
let fileLoaded;
try {
// @ts-ignore
fileLoaded = await fs.readFile(path.resolve(fileNameOrPath), encoding);
} catch (e) {
if (log) {
console.error(`${fileNameOrPath} not loaded!`);
console.error(e);
}
if (throws) {
throw e;
}
}
if (isJson) {
// @ts-ignore
fileLoaded = JSON.parse(fileLoaded);
}
return fileLoaded;
};
/**
* make a directory with error handling and confirmation.
* @example
* const myTmpDir = mkdir('./tmp')
* @param {string} [dirPath="./tmp"] - path to create; default `./tmp`
* @returns {string} the absolute path of the directory
* @memberof files
*/
exports.mkdir = function (dirPath = "./tmp") {
let fullPath = path.resolve(dirPath);
if (!existsSync(fullPath)) {
try {
mkdirSync(fullPath, { recursive: true });
// Check if the directory was created
if (!existsSync(fullPath)) {
throw new Error(`Failed to create directory at ${fullPath}`);
}
} catch (error) {
console.error(`Error creating directory at ${fullPath}: ${error.message}`);
throw error; // Rethrow or handle as necessary for your application's error handling strategy
}
}
return fullPath;
};
exports.makeExist = async function (filePath) { // Ensure all directories in the path exist
await mkdir(path.dirname(filePath), { recursive: true });
return true;
};
/**
* check if a file or directory exists
* @param {string} filePath - path to check
* @returns {'directory' | 'file' | false} what is it? and does it exist?
*/
exports.isDirOrFile = function isStringADirOrFile(filePath) {
const resolvedPath = path.resolve(filePath);
try {
const stats = statSync(resolvedPath);
if (stats.isDirectory()) {
return 'directory';
} else if (stats.isFile()) {
return 'file';
} else {
return false;
}
} catch (error) {
return false;
}
};
/**
* Get detailed information about a file or directory, including recursive folder structure
* @param {string} filePath - path to analyze
* @param {Object} options - optional settings
* @param {number} options.maxDepth - maximum recursion depth (default: Infinity)
* @param {Array<string>} options.exclude - patterns to exclude (default: [])
* @returns {Object|false} detailed information about the path
*/
exports.details = function getFileDetails(filePath, options = {}) {
const {
maxDepth = Infinity,
exclude = [],
currentDepth = 0
} = options;
const resolvedPath = path.resolve(filePath);
const type = exports.isDirOrFile(resolvedPath);
if (!type) {
return false;
}
// Base case for files
if (type === 'file') {
const fileDetails = statSync(resolvedPath);
return {
type: 'file',
path: resolvedPath,
size: exports.bytesHuman(fileDetails?.size || 0),
name: path.basename(resolvedPath),
infos: fileDetails
};
}
// Handle directories
if (type === 'directory') {
// Stop recursion if we've reached maxDepth
if (currentDepth >= maxDepth) {
return {
type: 'directory',
path: resolvedPath,
name: path.basename(resolvedPath),
infos: statSync(resolvedPath),
folders: {},
files: []
};
}
const contents = readdirSync(resolvedPath);
const structure = {
type: 'directory',
path: resolvedPath,
name: path.basename(resolvedPath),
infos: statSync(resolvedPath),
folders: {},
files: []
};
// Process each item in the directory
contents.forEach(item => {
const fullPath = path.join(resolvedPath, item);
// Skip excluded patterns
if (exclude.some(pattern => item.includes(pattern))) {
return;
}
const itemType = exports.isDirOrFile(fullPath);
if (itemType === 'directory') {
// Recursively process subdirectories
structure.folders[item] = getFileDetails(fullPath, {
...options,
currentDepth: currentDepth + 1
});
} else if (itemType === 'file') {
// Add files to the files array
const fileDetails = statSync(fullPath);
structure.files.push({
name: item,
path: fullPath,
size: exports.bytesHuman(fileDetails?.size || 0),
infos: fileDetails
});
}
});
return structure;
}
};
/*
-----------
VALIDATION
-----------
*/
/**
* test if string has valid JSON structure
* @example
* isJSONStr('{"foo": "bar"}') // => true
* isJSONStr('not json') // => false
* @param {string} string - string to test
* @returns {boolean} true if string is valid JSON
* @memberof validate
*/
exports.isJSONStr = function hasJsonStructure(string) {
if (typeof string !== 'string') return false;
try {
const result = JSON.parse(string);
const type = Object.prototype.toString.call(result);
return type === '[object Object]' ||
type === '[object Array]';
} catch (err) {
return false;
}
};
/**
* test if data can be stringified as JSON
* @example
* isJSON({foo: "bar"}) // => true
* isJSON(function() {}) // => false
* @param {unknown} data - data to test
* @returns {boolean} true if data can be JSON.stringify'd
* @memberof validate
*/
exports.isJSON = function canBeStringified(data) {
try {
let attempt = JSON.stringify(data);
if (attempt?.startsWith('{') || attempt?.startsWith('[')) {
if (attempt?.endsWith('}') || attempt?.endsWith(']')) {
return true;
}
else {
return false;
}
}
else {
return false;
}
}
catch (e) {
return false;
}
};
/**
* check if a value matches a specific type
* @example
* is(Number, 42) // => true
* is('string', 'hello') // => true
* is(Array, [1,2,3]) // => true
* @template T
* @param {string | Constructor<T>} type - primitive type string or constructor function
* @param {unknown} val - value to check
* @returns {val is T} true if value matches the type
* @memberof validate
*/
exports.is = function isPrimitiveType(type, val) {
if (typeof type === 'string') {
return typeof val === type;
}
return ![, null].includes(val) && val.constructor === type;
};
/**
* check if a value is null or undefined
* @example
* isNil(null) // => true
* isNil(undefined) // => true
* isNil(0) // => false
* @param {unknown} val - value to check
* @returns {val is null | undefined} true if value is null or undefined
* @memberof validate
*/
exports.isNil = function isNullOrUndefined(val) {
return val === undefined || val === null;
};
/**
* check if two objects have similar shape (same keys), recursively
* @example
* similar({a: "foo", b: 1}, {a: "bar", b: 2}) // => true
* similar({a: "foo"}, {a: "bar", b: 2}) // => false
* @param {AnyObject | null} o1 - first object
* @param {AnyObject | null} o2 - second object
* @returns {boolean} true if objects have the same key structure
* @memberof validate
*/
exports.similar = function deepSameKeys(o1, o2) {
// https://stackoverflow.com/a/41802431
// Both nulls = same
if (o1 === null && o2 === null) {
return true;
}
// Get the keys of each object
const o1keys = o1 === null ? new Set() : new Set(Object.keys(o1));
const o2keys = o2 === null ? new Set() : new Set(Object.keys(o2));
if (o1keys.size !== o2keys.size) {
// Different number of own properties = not the same
return false;
}
// Look for differences, recursing as necessary
for (const key of o1keys) {
if (!o2keys.has(key)) {
// Different keys
return false;
}
// Get the values and their types
const v1 = o1[key];
const v2 = o2[key];
const t1 = typeof v1;
const t2 = typeof v2;
if (t1 === "object") {
if (t2 === "object" && !deepSameKeys(v1, v2)) {
return false;
}
} else if (t2 === "object") {
// We know `v1` isn't an object
return false;
}
}
// No differences found
return true;
};
/**
* @typedef {object} GCSUri
* @property {string} uri
* @property {string} bucket
* @property {string} file
*/
/**
* turn a gcs uri into a bucket and file
* @example
* parseGCSUri(`gcs://foo/bar.txt`) // => {uri: "gcs://foo/bar.txt", bucket: "foo", file: "bar.txt"}
* @param {string} uri
* @returns {GCSUri}
* @memberof validate
*/
exports.parseGCSUri = function (uri) {
// ? https://www.npmjs.com/package/google-cloud-storage-uri-parser
let prefix;
if (uri.startsWith("gs://")) prefix = "gs://";
if (uri.startsWith("gcs://")) prefix = "gcs://";
if (!prefix) throw `invalid gcs uri: ${uri}`;
const REG_EXP = new RegExp(`^${prefix}([^/]+)/(.+)$`);
let bucket = uri.replace(REG_EXP, "$1");
let file = uri.replace(REG_EXP, "$2");
if (!file) file = "";
if (!bucket) bucket = uri.split(prefix)[0];
if (bucket.endsWith("/")) bucket = bucket.slice(0, -1);
if (bucket.startsWith(prefix)) bucket = bucket.slice(prefix.length);
if (file === uri) file = "";
return {
uri,
bucket,
file
};
};
/**
* converts various inputs to boolean values
* @example
* toBool('true') // => true
* toBool('yes') // => true
* toBool('1') // => true
* toBool('false') // => false
* toBool(0) // => false
* @param {unknown} input - value to convert to boolean
* @returns {boolean} boolean representation of input
* @memberof validate
*/
exports.toBool = function stringToBoolean(string) {
if (typeof string !== "string") {
return Boolean(string);
}
switch (string.toLowerCase().trim()) {
case "true":
return true;
case "yes":
return true;
case "1":
return true;
case "false":
return false;
case "no":
return false;
case "0":
return false;
case "":
return false;
default:
return Boolean(string);
}
};
/*
-------
DISPLAY
-------
*/
/**
* turn a number into a comma separated (human readable) string
* @example
* comma(1000) // => "1,000"
* @param {(string | number)} num
* @returns {string} formatted number
* @memberof display
*/
exports.comma = function addCommas(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
/**
* truncate a string w/ellipses
* @example
* truncate('foo bar baz', 3) // => 'foo...'
* @param {string} text - text to truncate
* @param {number} [chars=500] - # of max characters
* @param {boolean} [useWordBoundary=true] - don't break words; default `true`
* @returns {string} truncated string
* @memberof display
*
*/
exports.truncate = function intelligentlyTruncate(text, chars = 500, useWordBoundary = true) {
if (!text) {
return "";
}
if (text.length <= chars) {
return text;
}
var subString = text.substring(0, chars - 1);
return (useWordBoundary ?
subString.substring(0, subString.lastIndexOf(' ')) :
subString) + "...";
};
/**
* turn a number (of bytes) into a human readable string
* @example
* bytesHuman(10000000) // => '9.54 MiB'
* @param {number} bytes - number of bytes to convert
* @param {number} [dp=2] - decimal points; default `2`
* @param {boolean} [si=false] - threshold of 1000 or 1024; default `false`
* @returns {string} # of bytes
* @memberof display
*/
exports.bytesHuman = function (bytes, dp = 2, si = true) {
//https://stackoverflow.com/a/14919494
const thresh = si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
const units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u = -1;
const r = 10 ** dp;
do {
bytes /= thresh;
++u;
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
return bytes.toFixed(dp) + ' ' + units[u];
};
/** stringify object to json
* @example
* json({foo: "bar"}) => '{"foo": "bar"}'
* @param {object} data - any serializable object
* @param {number} [padding=2] - padding to use
* @returns {string | false} valid json
* @memberof display
*/
exports.json = function stringifyJSON(data, padding = 2) {
try {
return JSON.stringify(data, null, padding);
}
catch (e) {
return false;
}
};
/**
* strip all `<html>` tags from a string
* @param {string} str string with html tags
* @example
* stripHTML(`<div>i am <br/>text`) // => "i am \n text"
* @returns {string} sanitized string
* @note note: `<br>` tags are replace with `\n`
* @memberof display
*/
exports.stripHTML = function removeHTMLEntities(str) {
return str.replace(/<br\s*[\/]?>/gi, "\n").replace(/<[^>]*>?/gm, '');
};
/**
* find and replace _many_ values in string
* @example
* multiReplace('red fish said', [["red", "blue"],["said"]]) // => "blue fish"
* @param {string} str - string to replace
* @param {Array<Array<string, string>>} [replacePairs=[["|"],["<"],[">"]]] shape: `[ [old, new] ]`
* @returns {string} multi-replaced string
* @memberof display
*/
exports.multiReplace = function (str, replacePairs = [
["|"],
["<"],
[">"]
]) {
let text = str;
for (const pair of replacePairs) {
// @ts-ignore
text = text?.replaceAll(pair[0], pair[1] || " ");
}
//kill multiple spaces
return text.split(" ").filter(x => x).join(" ");
};
/**
* replace all occurrence of `old` with `new`
* @example
* 'foo bar'.replaceAll('foo', 'qux') // => 'qux bar'
* @param {(string | RegExp)} oldVal - old value
* @param {(string)} newVal - new value
* @returns {string} replaced result
* @memberof display
* @note this CAN be called on any string directly
*/
exports.replaceAll = function (oldVal, newVal) {
// If a regex pattern
if (Object.prototype.toString.call(oldVal).toLowerCase() === '[object regexp]') {
return this.replace(oldVal, newVal);
}
// If a string
return this.replace(new RegExp(oldVal, 'g'), newVal);
};
/**
* convert array of arrays to CSV like string
* @example
* toCSV([[1,2],[3,4]], ["foo", "bar"]) // => '"foo","bar"\n"1","2"\n"3","4"'
* @param {Array<String[] | Number[]>} arr - data of the form `[ [], [], [] ]`
* @param {String[]} [headers=[]] - header column
* @param {string} [delimiter=","] - delimiter for cells; default `,`
* @returns {string} a valid CSV
* @memberof display
*/
exports.toCSV = function arrayToCSV(arr, headers = [], delimiter = ',') {
if (!delimiter) {
delimiter = `,`;
}
let body = arr.map(v => v.map(x => `"${x}"`).join(delimiter)).join('\n');
if (headers) {
const topRow = headers.map(x => `"${x}"`).join(delimiter);
body = `${topRow}\n${body}`;
}
return body;
};
/**
* serialize a base64 string
* @example
* unBase64(`eyJmb28iOiAiYmFyIn0=`) => {"foo": "bar"}
* @param {string} b64Str - base64 encoded JSON data
* @returns dict or array of data
* @memberof display
*/
exports.unBase64 = function decodeBase64ToJson(b64Str) {
const data = Buffer.from(b64Str, 'base64').toString('binary');
try {
return JSON.parse(data);
}
catch (e) {
return data;
}
};
/*
-----
MATHS
-----
*/
/**
* random integer between `min` and `max` (inclusive)
* @example
* rand(1,10) // 1 or 2 or 3 ... or 10
* @param {number} min=1 - minimum
* @param {number} max=100 - maximum
* @returns {number} random number
* @note this is not cryptographically safe
* @memberof maths
*/
exports.rand = function generateRandomNumber(min = 1, max = 100) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
};
/**
* calculate average of `...nums`
* @example
* avg(1,2,3) // => 2
* @param {...number} nums - numbers to average
* @returns {number} average
* @memberof maths
*/
exports.avg = function calcAverage(...nums) {
return nums.reduce((acc, val) => acc + val, 0) / nums.length;
};
/**
* calculate the size (on disk)
* @example
* calcSize({foo: "bar"}) // => 13
* @param {(string | generalObject)} data - JSON to estimate
* @returns {number} estimated size in bytes
* @memberof maths
*/
exports.calcSize = function estimateSizeOnDisk(data) {
//calculates size in bytes; assumes utf-8 encoding: https://stackoverflow.com/a/63805778
return Buffer.byteLength(JSON.stringify(data));
};
/**
* round a number to a number of decimal places
* @example
* round(3.14159, 3) // => 3.142
* @param {number} number - number to round
* @param {number} [decimalPlaces=0] - decimal places; default `0`
* @returns {number} rounded number
* @memberof maths
*/
exports.round = function roundsNumbers(number, decimalPlaces = 0) {
//https://gist.github.com/djD-REK/068cba3d430cf7abfddfd32a5d7903c3
// @ts-ignore
return Number(Math.round(number + "e" + decimalPlaces) + "e-" + decimalPlaces);
};
/**
* generate a random uid:
* @example
* uid(4) // => 'AwD9rbntSj'
* @param {number} [length=64] length of id; default `64`
* @returns {string} a uid of specified length
* @note not cryptographically safe
* @memberof maths
*/
exports.uid = function makeUid(length = 64) {
//https://stackoverflow.com/a/1349426/4808195
var result = [];
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result.push(characters.charAt(Math.floor(Math.random() *
charactersLength)));
}
return result.join('');
};
/**
* generated a uuid in v4 format:
* @example
* uuid() // => "f47e2fdf-e387-4a39-9bb9-80b0ed950b48"
* @returns {string} a uuid
* @note not cryptographically safe
* @memberof maths
*/
exports.uuid = function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
/**
* calculate the md5 hash of any data
* @example
* md5({foo: "bar"}) // => "d41d8cd98f00b204e9800998ecf8427e"
* @param {any} data - data to hash
* @returns {string} md5 hash of `data
* @memberof maths
*/
exports.md5 = function calcMd5Hash(data) {
var hc = "0123456789abcdef";
function rh(n) { var j, s = ""; for (j = 0; j <= 3; j++) s += hc.charAt((n >> (j * 8 + 4)) & 0x0F) + hc.charAt((n >> (j * 8)) & 0x0F); return s; }
function ad(x, y) { var l = (x & 0xFFFF) + (y & 0xFFFF); var m = (x >> 16) + (y >> 16) + (l >> 16); return (m << 16) | (l & 0xFFFF); }
function rl(n, c) { return (n << c) | (n >>> (32 - c)); }
function cm(q, a, b, x, s, t) { return ad(rl(ad(ad(a, q), ad(x, t)), s), b); }
function ff(a, b, c, d, x, s, t) { return cm((b & c) | ((~b) & d), a, b, x, s, t); }
function gg(a, b, c, d, x, s, t) { return cm((b & d) | (c & (~d)), a, b, x, s, t); }
function hh(a, b, c, d, x, s, t) { return cm(b ^ c ^ d, a, b, x, s, t); }
function ii(a, b, c, d, x, s, t) { return cm(c ^ (b | (~d)), a, b, x, s, t); }
function sb(x) {
var i; var nblk = ((x.length + 8) >> 6) + 1; var blks = new Array(nblk * 16); for (i = 0; i < nblk * 16; i++) blks[i] = 0;
for (i = 0; i < x.length; i++) blks[i >> 2] |= x.charCodeAt(i) << ((i % 4) * 8);
blks[i >> 2] |= 0x80 << ((i % 4) * 8); blks[nblk * 16 - 2] = x.length * 8; return blks;
}
var i, x = sb(data), a = 1732584193, b = -271733879, c = -1732584194, d = 271733878, olda, oldb, oldc, oldd;
for (i = 0; i < x.length; i += 16) {
olda = a; oldb = b; oldc = c; oldd = d;
a = ff(a, b, c, d, x[i + 0], 7, -680876936); d = ff(d, a, b, c, x[i + 1], 12, -389564586); c = ff(c, d, a, b, x[i + 2], 17, 606105819);
b = ff(b, c, d, a, x[i + 3], 22, -1044525330); a = ff(a, b, c, d, x[i + 4], 7, -176418897); d = ff(d, a, b, c, x[i + 5], 12, 1200080426);
c = ff(c, d, a, b, x[i + 6], 17, -1473231341); b = ff(b, c, d, a, x[i + 7], 22, -45705983); a = ff(a, b, c, d, x[i + 8], 7, 1770035416);
d = ff(d, a, b, c, x[i + 9], 12, -1958414417); c = ff(c, d, a, b, x[i + 10], 17, -42063); b = ff(b, c, d, a, x[i + 11], 22, -1990404162);
a = ff(a, b, c, d, x[i + 12], 7, 1804603682); d = ff(d, a, b, c, x[i + 13], 12, -40341101); c = ff(c, d, a, b, x[i + 14], 17, -1502002290);
b = ff(b, c, d, a, x[i + 15], 22, 1236535329); a = gg(a, b, c, d, x[i + 1], 5, -165796510); d = gg(d, a, b, c, x[i + 6], 9, -1069501632);
c = gg(c, d, a, b, x[i + 11], 14, 643717713); b = gg(b, c, d, a, x[i + 0], 20, -373897302); a = gg(a, b, c, d, x[i + 5], 5, -701558691);
d = gg(d, a, b, c, x[i + 10], 9, 38016083); c = gg(c, d, a, b, x[i + 15], 14, -660478335); b = gg(b, c, d, a, x[i + 4], 20, -405537848);
a = gg(a, b, c, d, x[i + 9], 5, 568446438); d = gg(d, a, b, c, x[i + 14], 9, -1019803690); c = gg(c, d, a, b, x[i + 3], 14, -187363961);
b = gg(b, c, d, a, x[i + 8], 20, 1163531501); a = gg(a, b, c, d, x[i + 13], 5, -1444681467); d = gg(d, a, b, c, x[i + 2], 9, -51403784);
c = gg(c, d, a, b, x[i + 7], 14, 1735328473); b = gg(b, c, d, a, x[i + 12], 20, -1926607734); a = hh(a, b, c, d, x[i + 5], 4, -378558);
d = hh(d, a, b, c, x[i + 8], 11, -2022574463); c = hh(c, d, a, b, x[i + 11], 16, 1839030562); b = hh(b, c, d, a, x[i + 14], 23, -35309556);
a = hh(a, b, c, d, x[i + 1], 4, -1530992060); d = hh(d, a, b, c, x[i + 4], 11, 1272893353); c = hh(c, d, a, b, x[i + 7], 16, -155497632);
b = hh(b, c, d, a, x[i + 10], 23, -1094730640); a = hh(a, b, c, d, x[i + 13], 4, 681279174); d = hh(d, a, b, c, x[i + 0], 11, -358537222);
c = hh(c, d, a, b, x[i + 3], 16, -722521979); b = hh(b, c, d, a, x[i + 6], 23, 76029189); a = hh(a, b, c, d, x[i + 9], 4, -640364487);
d = hh(d, a, b, c, x[i + 12], 11, -421815835); c = hh(c, d, a, b, x[i + 15], 16, 530742520); b = hh(b, c, d, a, x[i + 2], 23, -995338651);
a = ii(a, b, c, d, x[i + 0], 6, -198630844); d = ii(d, a, b, c, x[i + 7], 10, 1126891415); c = ii(c, d, a, b, x[i + 14], 15, -1416354905);
b = ii(b, c, d, a, x[i + 5], 21, -57434055); a = ii(a, b, c, d, x[i + 12], 6, 1700485571); d = ii(d, a, b, c, x[i + 3], 10, -1894986606);
c = ii(c, d, a, b, x[i + 10], 15, -1051523); b = ii(b, c, d, a, x[i + 1], 21, -2054922799); a = ii(a, b, c, d, x[i + 8], 6, 1873313359);
d = ii(d, a, b, c, x[i + 15], 10, -30611744); c = ii(c, d, a, b, x[i + 6], 15, -1560198380); b = ii(b, c, d, a, x[i + 13], 21, 1309151649);
a = ii(a, b, c, d, x[i + 4], 6, -145523070); d = ii(d, a, b, c, x[i + 11], 10, -1120210379); c = ii(c, d, a, b, x[i + 2], 15, 718787259);
b = ii(b, c, d, a, x[i + 9], 21, -343485551); a = ad(a, olda); b = ad(b, oldb); c = ad(c, oldc); d = ad(d, oldd);
}
return rh(a) + rh(b) + rh(c) + rh(d);
};
/**
* generate a random name (adjective + noun + verb + adverb)
* @return {string} a random name
*/
exports.makeName = function generateName(words = 3, separator = "-") {
const adjs = [
"dark", "grim", "swift", "brave", "bold", "fiery", "arcane",
"rugged", "calm", "wild", "brisk", "dusty", "mighty", "sly",
"old", "ghostly", "frosty", "gilded", "murky", "grand", "sly",
"quick", "cruel", "meek", "glum", "drunk", "slick", "bitter",
"nimble", "sweet", "tart", "tough"
];
const nouns = [
"mage", "inn", "imp", "bard", "witch", "drake", "knight", "brew",
"keep", "blade", "beast", "spell", "tome", "crown", "ale", "bard",
"joke", "maid", "elf", "orc", "throne", "quest", "scroll", "fey",
"pixie", "troll", "giant", "vamp", "ogre", "cloak", "gem", "axe",
"armor", "fort", "bow", "lance", "moat", "den"
];
const verbs = [
"cast", "charm", "brawl", "brew", "haunt", "sail", "storm", "quest",
"joust", "feast", "march", "scheme", "raid", "guard", "duel",
"trick", "flee", "prowl", "forge", "explore", "vanish", "summon",
"banish", "bewitch", "sneak", "chase", "ride", "fly", "dream", "dance"
];
const adverbs = [
"boldly", "bravely", "slyly", "wisely", "fiercely", "stealthily", "proudly", "eagerly",
"quietly", "loudly", "heroically", "craftily", "defiantly", "infamously", "cleverly", "dastardly"
];
const continuations = [
"and", "of", "in", "on", "under", "over", "beyond", "within", "while", "during", "after", "before",
"beneath", "beside", "betwixt", "betwain", "because", "despite", "although", "however", "nevertheless"
];
let string;
const cycle = [adjs, nouns, verbs, adverbs, continuations];
for (let i = 0; i < words; i++) {
const index = i % cycle.length;
const word = cycle[index][Math.floor(Math.random() * cycle[index].length)];
if (!string) {
string = word;
} else {
string += separator + word;
}
}
return string;
};
/*
-------
OBJECTS
-------
*/
/**
* rename object keys using a mapping object
* @example
* rnKeys({foo: 'bar', baz: 'qux'}, {foo: 'newFoo'}) // => {newFoo: 'bar', baz: 'qux'}
* @template T
* @param {Record<string, T>} obj - object to rename keys for
* @param {Dictionary<string>} newKeys - mapping of old key to new key
* @returns {Record<string, T>} new object with renamed keys
* @memberof objects
*/
exports.rnKeys = function renameObjectKeys(obj, newKeys) {
//https://stackoverflow.com/a/45287523
const keyValues = Object.keys(obj).map(key => {
const newKey = newKeys[key] || key;
return {
[newKey]: obj[key]
};
});
return Object.assign({}, ...keyValues);
};
/**
* rename object values using a mapping array
* @example
* rnVals({foo: "bar"}, [["bar","baz"]) // => {foo: "baz"}
* @param {generalObject} obj
* @param {Array<Array<string, string>>} pairs `[['old', 'new']]`
* @returns {generalObject} object with renamed values
* @memberof objects
*/
exports.rnVals = function renameValues(obj, pairs) {
return JSON.parse(exports.multiReplace(JSON.stringify(obj), pairs));
};
/**
* @callback filterCallback
* @param {string} keyOrValue object's value or key to test
*/
/**
* filter objects by values or objects by keys; like `map()` for objects
* @example
* const d = {foo: "bar", baz: "qux"}
* objFilter(d, x => x.startsWith('b')) // => {foo: "bar"}
* objFilter(d, x => x.startsWith('f'), 'key') // => {foo: "bar"}
* @param {generalObject} hash - object or array to filter
* @param {filterCallback} test_function - a function which is called on keys/values
* @param {"key" | "value"} [keysOrValues="value"] - test keys or values; default `value`
* @returns {generalObject} filtered object
* @memberof objects
*/
exports.objFilter = function filterObjectKeys(hash, test_function, keysOrValues = "value") {
let key, i;
const iterator = Object.keys(hash);
const filtered = {};
for (i = 0; i < iterator.length; i++) {
key = iterator[i];
if (keysOrValues === 'value') {
if (test_function(hash[key])) {
filtered[key] = hash[key];
}
}
if (keysOrValues === 'key') {
if (test_function(key.toString())) {
filtered[key] = hash[key];
}
}
}
return filtered;
};
/**
* removes the following from deeply nested objects:
* - `null` | `undefined` | `{}` | `[]` | `""`
* @example
* objClean({foo: null, bar: undefined, baz: ""}) // => {}
* @param {generalObject} obj object to clean
* @param {boolean} [clone=true] should produce a new object? default `true`
* @returns {generalObject} cleaned object
* @memberof objects
*/
exports.objClean = function removeFalsyValues(obj, clone = true) {
let target;
//where objects have falsy values, delete those keys
if (clone) target = JSON.parse(JSON.stringify(obj));
if (!clone) target = obj;
function isObject(val) {
if (val === null) { return false; }
return ((typeof val === 'function') || (typeof val === 'object'));
}
const isArray = target instanceof Array;
for (var k in target) {
// falsy values
if (!Boolean(target[k])) {
isArray ? target.splice(k, 1) : delete target[k];
}
//empty strings
if (target[k] === "") {
delete target[k];
}
// empty arrays
if (Array.isArray(target[k]) && target[k]?.length === 0) {
delete target[k];
}
// empty objects
if (isObject(target[k])) {
if (JSON.stringify(target[k]) === '{}') {
delete target[k];
}
}
// recursion
if (isObject(target[k])) {
exports.objClean(target[k]);
}
}
return target;
};
/**
* apply default props to an object; don't override values from source
* @example
* objDefault({foo: "bar"}, {foo: "qux", b: "m"}) // => {foo: 'bar', b: 'm'}
* @param {generalObject} obj - original object
* @param {Object} defs - props to add without overriding
* @returns {generalObject} an object which has `defs` props
* @memberof objects
*/
exports.objDefault = function assignDefaultProps(obj, ...defs) {
return Object.assign({}, obj, ...defs.reverse(), obj);
};
/**
* deep equality match for any two objects
* @example
* objMatch({f: {g: {h: 42}}}, {f: {g: {x: 42}}}) // => false
* @param {Object} obj - object A
* @param {Object} source - object B
* @returns {boolean} do objects A & B (deeply) match?
* @memberof objects
*/
exports.objMatch = function doObjectsMatch(obj, source) {
return Object.keys(source).every(key => obj.hasOwnProperty(key) && obj[key] === source[key]);
};
/**
* efficient object cloning; outperforms `parse(stringify())` by 100x
* @example
* objClone({f: {g: {h : 42}}}) // => { f: { g: { h: 42 } } }
* @param {Object} thing - object to clone
* @param {Object} [opts]
* @returns {Object} deep copy of object
* @memberof objects
*/
exports.objClone = function deepClone(thing, opts) {
var newObject = {};
if (thing instanceof Array) {
return thing.map(function (i) { return exports.clone(i, opts); });
} else if (thing instanceof Date) {
return new Date(thing);
} else if (thing instanceof RegExp) {
return new RegExp(thing);
} else if (thing instanceof Function) {
// @ts-ignore
return opts && opts.newFns ?
new Function('return ' + thing.toString())() :
thing;
} else if (thing instanceof Object) {
Object.keys(thing).forEach(function (key) {
newObject[key] = exports.clone(thing[key], opts);
});
return newObject;
} else if ([undefined, null].indexOf(thing) > -1) {
return thing;
} else {
if (thing.constructor.name === 'Symbol') {
return Symbol(thing.toString()
.replace(/^Symbol\(/, '')
.slice(0, -1));
}
// return _.clone(thing); // If you must use _ ;)
return thing.__proto__.constructor(thing);
}
};
/**
* visit every property of an object; turn "number" values into numbers
* @example
* objTypecast({foo: {bar: '42'}}) // => {foo: {bar: 42}}
* @param {object} obj - object to traverse
* @param {boolean} [isClone=false] - default `false`; if `true` will mutate the passed in object
* @returns {Object} object with all "numbers" as proper numbers
* @memberof objects
*/
exports.objTypecast = function mutateObjValToIntegers(obj, isClone = false) {
//utility function for visiting every single key on an object
let target;
if (isClone) {
target = obj;
} else {
target = exports.clone(obj);
}
Object.keys(target).forEach(key => {
//recursion :(
if (typeof target[key] === 'object') {
exports.typecastInt(target[key], true);
}
//ewww ... mutating the input
else if (typeof target[key] === 'string') {
//check if it can be parsed
const parsed = makeInteger(target[key]);
//if it's NaN, don't changed it
target[key] = isNaN(parsed) ? target[key] : parsed;
}
});
return target;
};
/**
* utility to `await` object values
* @example
* //bar is a promise
* await objAwait({foo: bar()}) // => {foo: "resolved_bar"}
* @param {Object<string, Promise>} obj object
* @returns {Promise<generalObject>} the resolved values of the object's keys
* @memberof objects
*/
exports.objAwait = function resolveObjVals(obj) {
// https://stackoverflow.com/a/53112435
const keys = Object.keys(obj);
const values = Object.values(obj);
return Promise.all(values)
.then(resolved => {
const res = {};
for (let i = 0; i < keys.length; i += 1) {
res[keys[i]] = resolved[i];
}
return res;
});
};
/**
* explicitly remove keys with `null` or `undefined` values
* @example
* removeNulls({foo: "bar", baz: null}) // => {foo: "bar"}
* @param {Object} objWithNullOrUndef - an object with `null` or `undefined` values
* @returns {Object} an object without `null` or `undefined` values
* @note WARNING mutates object
* @memberof objects
*/
exports.removeNulls = function (objWithNullOrUndef) {
for (let key in objWithNullOrUndef) {
if (objWithNullOrUndef[key] === null) {
delete objWithNullOrUndef[key];
}
if (objWithNullOrUndef[key] === undefined) {
delete objWithNullOrUndef[key];
}
}
return objWithNullOrUndef;
};
/**
* check if a value is an integer, if so return it
* @ignore
* @param {string} value - a value to test
* @returns {(number | NaN)} a `number` or `NaN`
*/
function makeInteger(value) {
//the best way to find strings that are integers in disguise
if (/^[-+]?(\d+|Infinity)$/.test(value)) {
return Number(value);
} else {
return NaN;
}
}
/**
* deeply flatten as nested object; use `.` notation for nested keys
* @example
* flatten({foo: {bar: "baz"}}) => {"foo.bar": "baz"}
* @param {Object} obj object to flatten
* @param {Array} roots=[] lineage for recursion
* @param {string} sep='.' separator to use
* @memberof objects
* @return {Object}
*/
exports.flatten = function flattenObjectWithDotNotation(obj, roots = [], sep = '.') {
// ? https://stackoverflow.com/a/61602592
// find props of given object
return Object.keys(obj)
// return an object by iterating props
.reduce((memo, prop) => Object.assign(
// create a new object
{},
// include previously returned object
memo,
Object.prototype.toString.call(obj[prop]) === '[object Object]'
// keep working if value is an object
? exports.flatten(obj[prop], roots.concat([prop]), sep)
// include current prop and value and prefix prop with the roots
: { [roots.concat([prop]).join(sep)]: obj[prop] }
), {});
};
/**
* map over an object's values and return a new object
* @example
* objMap({foo: 2, bar: 4}, val => val * 2) => {foo: 4, bar: 8}
* @param {Object} object object iterate
* @param {function} mapFn function with signature `(val) => {}`
* @return {Object}
* @memberof objects
*/
exports.objMap = function mapOverObjectProps(object, mapFn) {
return Object.keys(object).reduce(function (result, key) {
result[key] = mapFn(object[key]);
return result;
}, {});
};
/**
* find a key in an object that has a particular value
* @example
* getKey({foo: "bar"}, "bar") => "foo"
* @param {Object} object object to search for
* @param {Object} value value withing that object to search for
* @return {string}
* @memberof objects
*/
exports.getKey = function getObjKeysByValue(object, value) {
// ? https://stackoverflow.com/a/28191966
return Object.keys(object).find(key => object[key] === value);
};
/**
* turn an array of objects into a CSV string
* @example
* makeCSV([{foo: "bar"}]) => "foo\nbar\n"
* @param {Array<Object>} data
* @param {number} [charLimit=50_000]
*/
exports.makeCSV = function makeCSVFromData(data, charLimit = 50_000) {
// Handle empty data case
if (!data || data.length === 0) return '';
// Get all unique keys across all objects
const columns = getUniqueKeys(data);
// Create header row
let csvString = columns.join(',') + '\n';
// Process each data item
data.forEach(item => {
const row = columns.map(col => {
// Handle undefined or null values
if (item[col] === undefined || item[col] === null) return '';
// Convert complex types to safe string representations
const value = convertToSafeValue(item[col])
?.toString()
?.trim()
?.slice(0, charLimit);
// Escape CSV-special characters
return `"${value.toString().replace(/"/g, '""')}"`;
}).join(',');
csvString += row + '\n';
});
return csvString;
};
function convertToSafeValue(value) {
// Handle different types of values
if (value === null || value === undefined) return '';
if (typeof value === 'object') {
// For arrays or objects, use JSON.stringify with single quotes
return JSON.stringify(value, null, 0)
.replace(/"/g, "'");
}
// For primitive types, return as is
return value;
}
function getUniqueKeys(data) {
const keysSet = new Set();
data.forEach(item => {
Object.keys(item).forEach(key => keysSet.add(key));
});
return Array.from(keysSet);
};
/*
-------
ARRAYS
--------
*/
/**
* duplicate values within an array N times
* @example
* dupeVals(["a","b","c"]) // => [ 'a', 'b', 'c', 'a', 'b', 'c' ]
* @param {any[]} array - array to duplicate
* @param {number} [times=1] - number of dupes per item; default `1`
* @returns {any[]} duplicated array
* @memberof arrays
*/
exports.dupeVals = function duplicateArrayValues(array, times = 1) {
let dupeArray = [];
for (let i = 0; i < times + 1; i++) {
array.forEach(item => dupeArray.push(item));
}
return dupeArray;
};
/**
* de-dupe array of objects w/Set, stringify, parse
* @memberof arrays
* @param {any} arrayOfThings - array to dedupe
* @returns {any[]} deduped array
*/
exports.dedupe = function deepDeDupe(arrayOfThings) {
// @ts-ignore
return Array.from(new Set(arrayOfThings.map(JSON.stringify))).map(JSON.parse);
};
/**
* de-dupe array of objects by value of specific keys
* @memberof arrays
* @param {any[]} arr - array to dedupe
* @param {string[]} keyNames - key names to dedupe values on
* @returns {any[]} deduped array of objected
*/
exports.dedupeVal = function dedupeByValues(arr, keyNames) {
// https://stackoverflow.com/a/56757215/4808195
return arr.filter((v, i, a) => a.findIndex(v2 => keyNames.every(k => v2[k] === v[k])) === i);
};
/**
* chunk array of objects into array of arrays with each less than or equal to `chunkSize`
* - `[{},{},{},{}]` => `[[{},{}],[{},{}]]`
* @memberof arrays
* @param {any[]} sourceArray - array to batch
* @param {number} chunkSize - max length of each batch
* @returns {any[]} chunked array
*/
exports.chunk = function chunkArray(sourceArray, chunkSize) {
return sourceArray.reduce((resultArray, item, index) => {
const chunkIndex = Math.floor(index / chunkSize);
if (!resultArray[chunkIndex]) {
resultArray[chunkIndex] = []; // start a new chunk
}
resultArray[chunkIndex].push(item);
return resultArray;
}, []);
};
/**
* fisher-yates shuffle of array elements
* @memberof arrays
* @param {any[]} array - array to shuffle
* @param {boolean} [mutate=false] - mutate array in place? default: `false`
* @returns {any[]} shuffled array
*/
exports.shuffle = function shuffleArrayVals(array, mutate = false) {
//https://stackoverflow.com/a/12646864/4808195
let target;
if (mutate) {
target = array;
} else {
target = exports.clone(array);
}
for (let i = target.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[target[i], target[j]] = [target[j], target[i]];
}
return target;
};
/**
* the classic python built-in for generating arrays of integers
* @memberof arrays
* @param {number} min - starting number
* @param {number} max - ending number
* @param {number} [step=1] - step for each interval; default `1`
* @return {number[]} a range of integers
*/
exports.range = function buildRangeArray(min, max, step = 1) {
const result = [];
step = !step ? 1 : step;
max = max / step;
for (var i = min; i <= max; i++) {
result.push(i * step);
}
return result;
};
/**
* recursively and deeply flatten a nested array of objects
* - ex: `[ [ [{},{}], {}], {} ]` => `[{},{},{},{}]`
* @memberof arrays
* @param {any[]} arr - array to flatten
* @returns {any[]} flat array
*/
exports.deepFlat = function deepFlatten(arr) {
return [].concat(...arr.map(v => (Array.isArray(v) ? deepFlatten(v) : v)));
};
/**
* extract words from a string as an array
* - ex `"foo bar baz"` => `['foo','bar','baz']`
* @memberof arrays
* @param {string} str - string to extract from
* @returns {string[]} extracted words
*/
exports.strToArr = function extractWords(str, pattern = /[^a-zA-Z-]+/) {
return str.split(pattern).filter(Boolean);
};
/**
* group array of objects by a key or function
* @example
* groupBy([{name: 'John', age: 25}, {name: 'Jane', age: 25}], 'age')
* // => {'25': [{name: 'John', age: 25}, {name: 'Jane', age: 25}]}
* @template T
* @param {T[]} array - array to group
* @param {string | ((item: T) => string | number)} keyOrFn - key name or function to group by
* @returns {Dictionary<T[]>} object with grouped arrays
* @memberof arrays
*/
exports.groupBy = function groupArrayByKey(array, keyOrFn) {
const getKey = typeof keyOrFn === 'function' ? keyOrFn : (item) => item[keyOrFn];
return array.reduce((groups, item) => {
const key = String(getKey(item));
groups[key] = groups[key] || [];
groups[key].push(item);
return groups;
}, {});
};
/**
* convert grouped object back to flat array
* @example
* ungroupBy({'25': [{name: 'John'}, {name: 'Jane'}]})
* // => [{name: 'John'}, {name: 'Jane'}]
* @template T
* @param {Dictionary<T[]>} groupedObj - grouped object to flatten
* @returns {T[]} flattened array
* @memberof arrays
*/
exports.ungroupBy = function flattenGroupedObject(groupedObj) {
return Object.values(groupedObj).flat();
};
/**
* create an index of array items by a key
* @example
* keyBy([{id: 1, name: 'John'}, {id: 2, name: 'Jane'}], 'id')
* // => {'1': {id: 1, name: 'John'}, '2': {id: 2, name: 'Jane'}}
* @template T
* @param {T[]} array - array to index
* @param {string | ((item: T) => string | number)} keyOrFn - key name or function to index by
* @returns {Dictionary<T>} object with items indexed by key
* @memberof arrays
*/
exports.keyBy = function indexArrayByKey(array, keyOrFn) {
const getKey = typeof keyOrFn === 'function' ? keyOrFn : (item) => item[keyOrFn];
return array.reduce((index, item) => {
const key = String(getKey(item));
index[key] = item;
return index;
}, {});
};
/**
* partition array into two arrays based on predicate
* @example
* partition([1,2,3,4,5], x => x % 2 === 0) // => [[2,4], [1,3,5]]
* @template T
* @param {T[]} array - array to partition
* @param {(item: T, index: number) => boolean} predicate - function to test each item
* @returns {[T[], T[]]} tuple of [matching, nonMatching] arrays
* @memberof arrays
*/
exports.partition = function partitionArray(array, predicate) {
const matching = [];
const nonMatching = [];
array.forEach((item, index) => {
if (predicate(item, index)) {
matching.push(item);
} else {
nonMatching.push(item);
}
});
return [matching, nonMatching];
};
/**
* pick specified properties from an object
* @example
* pick({a: 1, b: 2, c: 3}, ['a', 'c']) // => {a: 1, c: 3}
* @template T, K
* @param {T} obj - object to pick from
* @param {K[]} keys - array of keys to pick
* @returns {Pick<T, K>} new object with only specified keys
* @memberof objects
*/
exports.pick = function pickObjectProperties(obj, keys) {
const result = {};
keys.forEach(key => {
if (key in obj) {
result[key] = obj[key];
}
});
return result;
};
/**
* omit specified properties from an object
* @example
* omit({a: 1, b: 2, c: 3}, ['b']) // => {a: 1, c: 3}
* @template T, K
* @param {T} obj - object to omit from
* @param {K[]} keys - array of keys to omit
* @returns {Omit<T, K>} new object without specified keys
* @memberof objects
*/
exports.omit = function omitObjectProperties(obj, keys) {
const result = { ...obj };
keys.forEach(key => {
delete result[key];
});
return result;
};
/**
* debounce function execution
* @example
* const debouncedSave = debounce(saveData, 500);
* @param {Function} func - function to debounce
* @param {number} wait - milliseconds to wait
* @param {boolean} [immediate=false] - trigger on leading edge instead of trailing
* @returns {Function} debounced function
* @memberof functions
*/
exports.debounce = function debounceFunction(func, wait, immediate = false) {
let timeout;
return function executedFunction(...args) {
const later = () => {
timeout = null;
if (!immediate) func.apply(this, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(this, args);
};
};
/**
* pipe functions left-to-right (opposite of compose)
* @example
* pipe(add1, multiply2, subtract3)(5) // => 9 (((5+1)*2)-3)
* @param {...Function} functions - functions to pipe
* @returns {Function} piped function
* @memberof functions
*/
exports.pipe = function pipeFunctions(...functions) {
return function piped(value) {
return functions.reduce((acc, fn) => fn(acc), value);
};
};
/*
---------
FUNCTIONS
---------
*/
/**
* `try{} catch{}` a function; return results
* @memberof functions
* @param {Function} fn
* @param {...any} args
*/
exports.attempt = async function tryToExec(fn, ...args) {
try {
return await fn(...args);
} catch (e) {
return e instanceof Error ? e : new Error(e);
}
};
/**
* do a function `N` times
* @memberof functions
* @param {number} n - number of times
* @param {Function} iteratee - function to run
*/
exports.times = function doNTimes(n, iteratee, context) {
var accum = Array(Math.max(0, n));
iteratee = optimizeCb(iteratee, context, 1);
for (var i = 0; i < n; i++) accum[i] = iteratee(i);
return accum;
};
/**
* throttle a functions's execution every `N` ms
* @memberof functions
* @param {function} func - function to throttle
* @param {numb