Skip to content

Instantly share code, notes, and snippets.

@erikvullings
Last active September 22, 2022 08:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save erikvullings/32b98b3f8221733114292a1a8f2ed82d to your computer and use it in GitHub Desktop.
Save erikvullings/32b98b3f8221733114292a1a8f2ed82d to your computer and use it in GitHub Desktop.
Utility functions
/* A list of useful functions snippets */
/**
* Create a GUID
* @see https://stackoverflow.com/a/2117523/319711
*
* @returns RFC4122 version 4 compliant GUID
*/
export const uuid4 = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
// tslint:disable-next-line:no-bitwise
const r = (Math.random() * 16) | 0;
// tslint:disable-next-line:no-bitwise
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
};
/** Create a reasonably unique ID */
export const uniqueId = () => "id" + Math.random().toString(16).slice(2);
/**
* Retreive a value from an object using a dynamic path.
* If the attribute does not exist, return undefined.
* @param o: object
* @param s: path, e.g. a.b[0].c
* @see https://stackoverflow.com/a/6491621/319711
*/
const getPath = (obj: Record<string, any>, s: string) => {
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
s = s.replace(/^\./, ''); // strip a leading dot
const a = s.split('.');
let o = { ...obj };
for (let i = 0, n = a.length; i < n; ++i) {
const k = a[i];
if (k in o) {
o = o[k];
} else if (o instanceof Array) {
const id = obj[k] || k;
const m = /([A-Z]\w+)/.exec(k); // categoryId => match Id, myNameLabel => NameLabel
const key = (m && m[0][0].toLowerCase() + m[0].substr(1)) || k; // key = id or nameLabel
const found = o.filter((i) => i[key] === id).shift();
if (found) {
o = found;
} else {
return undefined;
}
} else {
return undefined;
}
}
return o as any;
};
/**
* Get a color that is clearly visible against a background color
* @param backgroundColor Background color, e.g. #99AABB
* @returns
*/
export const getTextColorFromBackground = (backgroundColor?: string) => {
if (!backgroundColor) return 'black-text';
const c = backgroundColor.substring(1); // strip #
const rgb = parseInt(c, 16); // convert rrggbb to decimal
const r = (rgb >> 16) & 0xff; // extract red
const g = (rgb >> 8) & 0xff; // extract green
const b = (rgb >> 0) & 0xff; // extract blue
const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709
return luma < 105 ? 'white-text' : 'black-text';
};
const flatten = <T>(arr: T[]) =>
arr.reduce((acc, cur) => (cur instanceof Array ? [...acc, ...cur] : [...acc, cur]), [] as T[]);
/**
* Debounce function wrapper, i.e. between consecutive calls of the wrapped function,
* there will be at least TIMEOUT milliseconds.
* @param func Function to execute
* @param timeout Timeout in milliseconds
* @returns
*/
export const debounce = (func: (...args: any) => void, timeout: number) => {
let timer: number;
return (...args: any) => {
clearTimeout(timer);
timer = window.setTimeout(() => {
func(...args);
}, timeout);
};
};
/**
* Generate a sequence of numbers between from and to with step size: [from, to].
*
* @static
* @param {number} from
* @param {number} to : inclusive
* @param {number} [count=to-from+1]
* @param {number} [step=1]
* @returns
*/
export const range = (
from: number,
to: number,
count: number = to - from + 1,
step: number = 1
) => {
// See here: http://stackoverflow.com/questions/3746725/create-a-javascript-array-containing-1-n
// let a = Array.apply(null, {length: n}).map(Function.call, Math.random);
const a: number[] = new Array(count);
const min = from;
const max = to - (count - 1) * step;
const theRange = max - min;
const x0 = Math.round(from + theRange * Math.random());
for (let i = 0; i < count; i++) {
a[i] = x0 + i * step;
}
return a;
};
/* TEXT UTILITIES */
/**
* Pad the string with padding character.
* @param str Input string or number
* @param length Desired length, @default 2
* @param padding Padding character, @default '0'
*/
export const padLeft = (str: string | number, length = 2, padding = '0'): string => str.toString().length >= length ? str.toString() : padLeft(padding + str, length, padding);
const supRegex = /\^([^_ ]+)(_|$|\s)/g;
const subRegex = /\_([^\^ ]+)(\^|$|\s)/g;
/** Expand markdown notation by converting A_1 to subscript and x^2 to superscript. */
export const subSup = (s: string) =>
s ? s.replace(supRegex, `<sup>$1</sup>`).replace(subRegex, `<sub>$1</sub>`) : s;
/** Remove special characters (e.g. when searching for a text) */
export const cleanUpSpecialChars = (str: string) =>
str
.replace(/[ÀÁÂÃÄÅàáâãäå]/g, 'a')
.replace(/[ÈÉÊËèéêë]/g, 'e')
.replace(/[ÒÓÔÖòóôö]/g, 'o')
.replace(/[ÌÍÎÏìíîï]/g, 'i')
.replace(/[ÙÚÛÜùúûü]/g, 'u')
.replace(/[ç]/g, 'c')
.replace(/[ș]/g, 's')
.replace(/[ț]/g, 't')
.replace(/[\/\-]|&amp;|\s+/g, ' ')
.replace(/[^a-z0-9 ]/gi, ''); // final clean up
/** Join a list of items with a comma, and use AND for the last item in the list. */
export const joinListWithAnd = (arr: string[] = [], and = 'and', prefix = '') =>
arr.length === 0
? ''
: prefix +
(arr.length === 1
? arr[0]
: `${arr.slice(0, arr.length - 1).join(', ')} ${and} ${arr[arr.length - 1]}`);
/** Create a hash for use in keys */
export const hash = (s: string | { [key: string]: any }) => {
if (typeof s !== 'string') {
s = JSON.stringify(s);
}
let hash = 0;
if (s.length === 0) {
return hash;
}
for (var i = 0; i < s.length; i++) {
var char = s.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
};
/* DATE UTILITIES */
/** Format the date YYYY-MM-DD */
export const formatDate = (date: number | Date = new Date()) => {
const d = new Date(date);
return `${d.getFullYear()}-${padLeft(d.getMonth() + 1)}-${padLeft(d.getDate())}`;
};
/**
* Get date of ISO week, i.e. starting on a Monday.
* @param week week number
* @param year year
*/
export const getDateOfISOWeek = (week: number, year?: number) => {
const w = year ? week : week % 100;
if (!year) {
year = Math.round((week - w) / 100);
}
const simple = new Date(year, 0, 1 + (w - 1) * 7);
const dow = simple.getDay();
const ISOweekStart = simple;
if (dow <= 4) {
ISOweekStart.setDate(simple.getDate() - simple.getDay() + 1);
} else {
ISOweekStart.setDate(simple.getDate() + 8 - simple.getDay());
}
return ISOweekStart;
};
/**
* Get the week number from the date
* @param date Date to use
* @returns week number
*/
export const getWeekNumber = (date: Date) => {
const dayNum = date.getUTCDay() || 7;
date.setUTCDate(date.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
return Math.ceil(((date.valueOf() - yearStart.valueOf()) / 86400000 + 1) / 7);
};
/* RANDOMNESS */
/**
* Create a GUID
* @see https://stackoverflow.com/a/2117523/319711
*
* @returns RFC4122 version 4 compliant GUID
*/
export const uuid4 = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
// tslint:disable-next-line:no-bitwise
const r = (Math.random() * 16) | 0;
// tslint:disable-next-line:no-bitwise
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
};
/** Create a reasonably unique ID */
export const uniqueId = () => "id" + Math.random().toString(16).slice(2);
/**
* Returns a random integer between min (inclusive) and max (inclusive), optionally filtered.
* If a filter is supplied, only returns numbers that satisfy the filter.
*
* @param {number} min
* @param {number} max
* @param {Function} filter numbers that do not satisfy the condition
*/
export const random = (
min: number,
max: number,
f?: (n: number, min?: number, max?: number) => boolean
): number => {
const x = min >= max ? min : Math.floor(Math.random() * (max - min + 1)) + min;
return f ? (f(x, min, max) ? x : random(min, max, f)) : x;
};
/**
* Draw N random (unique) numbers between from and to.
*
* @static
* @param {number} from
* @param {number} to
* @param {number} count
* @param {number[]} [existing]
* @param {(n: number) => boolean} [filter] Optional filter to filter out the results
* @returns
*/
export const randomNumbers = (
from: number,
to: number,
count: number,
existing: number[] = [],
filter?: (n: number, existing?: number[]) => boolean
) => {
if (from === to) {
return Array(count).fill(to);
}
if (from > to || count - 1 > to - from) {
throw Error('Outside range error');
}
const result: number[] = [];
do {
const x = random(from, to);
if (existing.indexOf(x) < 0 && result.indexOf(x) < 0) {
if (!filter || filter(x, result)) {
result.push(x);
count--;
} else {
count += result.length;
result.length = 0;
}
}
} while (count > 0);
return result;
};
/**
* Returns n random items from an array
*/
export const randomItems = <T>(arr: T[], count = 5): T[] =>
randomNumbers(0, arr.length - 1, count).map((i) => arr[i]);
/**
* Shuffles array in place. ES6 version
* @param {Array} a items An array containing the items.
*/
export const shuffle = (a: Array<any>) => {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment