let lastUsedId = 0;
export const getUniqueID = () => {
return lastUsedId++;
};
export class Util {
static MinBy<T>(list: T[], fn: (T: T) => number): T | null {
let lowestT: T | null = null;
let lowestValue: number | null = null;
for (const item of list) {
const value = fn(item);
if (lowestValue === null || value < lowestValue) {
lowestT = item;
lowestValue = value;
}
}
return lowestT;
}
static MinByAndValue<T>(
list: T[],
fn: (T: T) => number
): { obj: T; value: number } | null {
let lowestT: T | null = null;
let lowestValue: number | null = null;
for (const item of list) {
const value = fn(item);
if (lowestValue === null || value < lowestValue) {
lowestT = item;
lowestValue = value;
}
}
return lowestT === null || lowestValue === null
? null
: { obj: lowestT, value: lowestValue };
}
static MaxBy<T>(list: T[], fn: (T: T) => number): T | null {
let highestT: T | null = null;
let highestValue: number | null = null;
for (const item of list) {
const value = fn(item);
if (highestValue === null || value > highestValue) {
highestT = item;
highestValue = value;
}
}
return highestT;
}
static RandRange(low: number, high: number): number {
return Math.floor(Math.random() * (high - low) + low);
}
public static SortByKey<T>(array: T[], key: (x: T) => number): T[] {
return array.sort((a, b) => {
return key(a) - key(b);
});
}
public static ReplaceAll(
str: string,
mapObj: { [key: string]: string }
): string {
const re = new RegExp(Object.keys(mapObj).join("|"), "gi");
return str.replace(re, (matched) => {
return mapObj[matched.toLowerCase()];
});
}
public static Debounce<F extends (...args: any[]) => void>(
func: F,
waitMilliseconds = 50,
options = {
isImmediate: false,
}
): F {
let timeoutId: any; // types are different on node vs client, so we have to use any.
const result = (...args: any[]) => {
const doLater = () => {
timeoutId = undefined;
if (!options.isImmediate) {
func.apply(this, args);
}
};
const shouldCallNow = options.isImmediate && timeoutId === undefined;
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(doLater, waitMilliseconds);
if (shouldCallNow) {
func.apply(this, args);
}
};
return result as any;
}
public static FormatDate(d: Date): string {
const monthName = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
][d.getMonth()];
return `${monthName} ${d.getDate()}, ${("00" + d.getHours()).substr(-2)}:${(
"00" + d.getMinutes()
).substr(-2)}:${("00" + d.getSeconds()).substr(-2)}`;
}
public static FlattenByOne<T>(arr: T[][]): T[] {
let result: T[] = [];
for (const obj of arr) {
result = result.concat(obj);
}
return result;
}
public static PadString(
string: string,
length: number,
intersperse = "",
character = " "
) {
return string + intersperse + character.repeat(length - string.length);
}
}
/**
* A deep readonly type - given an object type, all subobjects and their subobjects are also marked as readonly.
*/
export type Const<T> = T extends Function
? T
: {
readonly [P in keyof T]: T[P] extends { [k: string]: any }
? Const<T[P]>
: T[P];
};
const assertOnlyCalledOnceData: { [k: string]: [string, number] } = {};
/**
* Asserts that a function is not called more than twice. Useful for debugging react lifecycle which may be creating more objects than you realize, impacting performance.
* @param id identifier
*/
export function assertOnlyCalledOnce(id: string | number) {
let k = id.toString();
if (assertOnlyCalledOnceData[k] !== undefined) {
if (assertOnlyCalledOnceData[k][1] === 1) {
assertOnlyCalledOnceData[k][1] = 2;
} else {
throw new Error(
"Error, called more than twice with same id: " +
k +
" , callback the first time was : " +
assertOnlyCalledOnceData[k]
);
}
} else {
const stacktrace = new Error().stack!;
assertOnlyCalledOnceData[k] = [stacktrace, 1];
}
}
/**
* Class representing a value which is only computed when used.
*
* Usage: const lazy = new Lazy(() => thingThatReturnsSomething()).
* Then thingThatReturnsSomething() will only get called on the first time lazy.get() is called.
* On the second and subsequent times, lazy.get() will return the same object - the factory method is not called again.
*/
export class Lazy<T> {
private _wasConstructed: boolean = false;
private _value: T | undefined = undefined;
private _factory: () => T;
constructor(
factory: () => T,
// structure?: T extends { [key: string]: any } ? T : void
) {
this._factory = factory;
}
public get(): T {
// T might have undefined as a valid value
if (this._value !== undefined || this._wasConstructed === true) {
return this._value!;
} else {
this._value = this._factory();
this._wasConstructed = true;
return this._value;
}
}
public wasConstructed(): boolean {
return this._wasConstructed;
}
// public async getAsync(): Promise<T> {
// if (this._value !== undefined || this._wasConstructed === true) {
// return Promise.resolve(this._value!);
// } else {
// return new Promise<T>((resolve, reject) => {
// this._value = this._factory();
// this._wasConstructed = true;
// resolve(this._value);
// });
// }
// }
}
export function LazyProxy<
T extends { [key: string]: any } | { [i: number]: any }
>(factory: () => T): Const<T> {
return (new Proxy(new Lazy(factory), {
get: (target, property, receiver) => {
if (property === "toJSON") {
return () => {
if (target.wasConstructed()) {
return target.get();
} else {
return "[Object Lazy]";
}
};
}
const targetValue = target.get();
return Reflect.get(targetValue, property);
},
ownKeys: (target) => {
const targetValue = target.get();
return Reflect.ownKeys(targetValue);
},
getOwnPropertyDescriptor: (target, property) => {
/**
* https://stackoverflow.com/questions/40352613/why-does-object-keys-and-object-getownpropertynames-produce-different-output
*/
return Object.getOwnPropertyDescriptor(target.get(), property);
},
has: (target, property) => {
// This is called when iterating over array i.e. array.forEach()
return property in target.get()
}
}) as unknown) as Const<T>;
}
/**
* Multiplies colors (0xFFFFFF === 1). use for applying tints manually.
* @param color1 A base color
* @param color2 A tint
*/
export function multiplyColor(color1: number, color2: number): number {
let reds = [color1 & 0xff0000, color2 & 0xff0000];
let blues = [color1 & 0x0000ff, color2 & 0x0000ff];
let greens = [color1 & 0x00ff00, color2 & 0x00ff00];
let out = Math.round(((reds[0] / 0x010000) * reds[1]) / 0xffffff) * 0x010000;
out += Math.round(((greens[0] / 0x000100) * greens[1]) / 0x00ff00) * 0x000100;
out += Math.round((blues[0] * blues[1]) / 0x0000ff);
return out;
}
export function enumKeys<T extends string>(enm: { [key in T]: T }): T[] {
return Object.keys(enm) as T[];
}
// export function enumKeys<T extends string>(enm: { [key: string]: string }) : T[] {
// return Object.keys(enm) as T[];
// }