lib/core/src/utils/utils.ts
[index: string]:
|
Defined in lib/core/src/utils/utils.ts:173
|
export function hasErrors(callback: () => unknown, log_errors = false): boolean {
try{
callback()
return false
} catch(e){
if(log_errors) console.log(e)
return true
}
}
/**
* Checks if callback throws any errors; returns true if not.
*/
export function isErrorFree(callback: () => unknown, log_errors = false): boolean {
return !hasErrors(callback, log_errors)
}
/**
* Use isErrorFree() instead
* @deprecated
*/
export function flattenErrors(callback: () => unknown): boolean {
console.warn("flattenErrors() is deprecated, use isErrorFree() instead.")
try{
callback()
return true
} catch(e){
return false
}
}
/**
* Throws an error and logs extra information.
* Also unlike normal throw it can be used after a return statement:
* ```return throwError(...)```
*/
export function throwError(e: Error|string, offender?: unknown, context?: unknown): never {
const error = (e instanceof Error)
? e
: new Error(e)
// Do not log extra info during tests.
if( (typeof process != 'undefined') && process.env?.NODE_ENV == 'test') throw error
console.groupCollapsed('Error (Details) '+ error.message)
console.info('Offender', offender)
console.info('Context', context)
console.error(error)
console.groupEnd()
throw error
}
export class AssertionError extends Error {
public name = 'AssertionError'
public context : unknown
public assertion : unknown
constructor(message: string, {cause, assertion, context}:{ cause?: string | Error, assertion?: unknown, context?: unknown}){
super(message) //super(message, { cause }) does not work =/
this.cause = cause instanceof Error ? cause : new Error(cause)
this.context = context
this.assertion = assertion
}
}
export function assert(x: unknown, e: string | Error = 'unknown assertion error', context?: unknown, cause?: string|Error): asserts x {
if(x) return;
const error = e instanceof Error
? e
: new AssertionError(e, {assertion: x, context, cause})
throw error
}
/**
* Checks if all keys are property names of given object x. Works as a typeguard
* assertig that properties to all the keys exist.
* Will throw an error if a property is missing -- unlike {@link has}.
*/
export function assertProperty
// type variables:
<PropertyNames extends string[] | string>
// function paramters:
(x:unknown, keys: PropertyNames, message?: string)
// type assertion:
: asserts x is PropertyNames extends string ? {[key in PropertyNames]: unknown }: {[key in PropertyNames[number]]: unknown }
// function body:
{
if(typeof keys == 'string') return assertProperty(x, [keys], message)
keys.forEach(
key => assert(
x && (typeof x == 'object' || typeof x == 'function') && (key in x ),
message || `assertProperty(): missing property: ${key}`,
{x, key},
message ? `assertProperty(): missing property: ${key}` : undefined
)
)
}
export function assertMap<KEY, VALUE>(x: unknown) : asserts x is Map<KEY,VALUE> {
assert(x instanceof Map, "assertMap() x is not an instance of Map.")
}
/**
* Checks if all keys are property names of given object x. Works as a typeguard assertig that properties to all the keys exist.
* Will return false if a proerty is missing-- unlike {@link assertProperties}.
*/
export function has
<PropertyNames extends string[]>
(x:unknown, ...keys: PropertyNames)
: x is PropertyNames extends string ? {[key in PropertyNames]: unknown }: {[key in PropertyNames[number]]: unknown }
{
return isErrorFree( () => assertProperty(x, keys) )
}
export function log(...args: unknown[]): unknown {
console.log(...args)
return args[0]
}
log.info = function(...args: unknown[]){
console.info(...args)
}
export function cycleString(str: string): string{
return str.substr(1)+str[0]
}
export function camel2Underscore(str: string) : string {
return str.replace(/([a-z])([A-Z])/g, '$1_$2')
}
export interface NestedRecord<T> {
[index:string]: NestedRecord<T> | T
}
export function isNestedRecord<T>(x: unknown, type: string | (new (...args:any[]) => T) ): x is NestedRecord<T> {
if(typeof x != 'object') return false
if(typeof x == null) return false
return Object.keys(x).every( key => {
if(!has(x,key)) return false
if(typeof type == 'function' && x[key] instanceof type) return true
if(typeof type == 'string' && typeof x[key] == type) return true
if(isNestedRecord<T>(x[key], type)) return true
return false
})
}
/**
* Needs documentation :D
*
* obj, 'PROP1.PROP2' -> obj.PROP1.PROP2
*/
export function deflateObject<T>(obj: NestedRecord<T> | T | undefined, path = '', recursionCount = 0): NestedRecord<T> | T | undefined {
if(recursionCount > 20) throwError('deflateObject() recursion count greater than 20.', obj, path)
if(path == '') return obj
if(obj === undefined) return undefined
const parts = path.split('.')
const first_part = parts.shift()
const rest = parts.join('.')
const def_obj = (obj as NestedRecord<T> )[first_part]
const def_path = rest
return deflateObject( def_obj, def_path, recursionCount++)
}
/**
* Needs documentation :D
*
* obj, 'PROP1.PROP2' -> { PROP1: { PROP2: obj }}
*/
export function inflateObject<T>(obj: NestedRecord<T> | T, path: string, recursionCount = 0): NestedRecord<T> {
if(recursionCount > 20) throwError('inflateObject() recursion count greater than 20.', obj, path)
assert(typeof path == 'string' && path != '', "inflateObject path argument must be a non-empty string.")
const parts = path.split('.')
const last_part = parts.pop()
const rest = parts.join('.')
const inflated = { [last_part]: obj }
return rest == ''
? inflated
: inflateObject(inflated , rest, recursionCount++)
}
/**
* Needs documentation -.-
*
*/
export function mergeObjects<T> (obj1 : NestedRecord<T> | T, obj2 : NestedRecord<T> | T, recursionCount? : undefined| number) : NestedRecord<T>
export function mergeObjects<T> (obj1 : NestedRecord<T> | T, obj2 : NestedRecord<T> | T, recursionCount? : number) : NestedRecord<T> | T {
recursionCount = recursionCount || 0
assert(recursionCount <= 20, "mergeObjects() recursion count greater than 20.", [obj1, obj2])
const keys1 = obj1 && typeof obj1 == 'object' && Object.keys(obj1)
const keys2 = obj2 && typeof obj2 == 'object' && Object.keys(obj2)
if(!keys1) return obj2
if(!keys2) return obj2
const keys = new Set([...keys1, ...keys2])
const merged : NestedRecord<T> = {}
keys.forEach( key => {
const o1 = has(obj1,key) ? obj1[key] : undefined
const o2 = has(obj2,key) ? obj2[key] : undefined
if(o1 === undefined) return merged[key] = o2
if(o2 === undefined) return merged[key] = o1
merged[key] = mergeObjects( o1, o2, recursionCount++)
})
return merged
}
//Thanks to https://stackoverflow.com/questions/105034/how-to-create-guid-uuid
export function uuidv4(): string {
// @ts-ignore
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) ) as string
}
export function adHocId(): string {
return uuidv4()
}
/**
* Encode a utf-8 string as base64
*/
export function b64EncodeUnicode(str: string): string {
return btoa(
encodeURIComponent(str)
.replace(/%([0-9A-F]{2})/g, (match:string, p1:string) => String.fromCharCode(parseInt(p1, 16)) )
)
}
/**
* Decode base64 string into utf-8
*/
export function b64DecodeUnicode(str: string): string {
return decodeURIComponent(Array.prototype.map.call(atob(str), c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''))
}
export interface pseudoSubject {
next : (value:any) => any
subscribe : (...args:any[]) => any
}
/**
* Sorting function meant to be used in Array.sort().
* Interprets zero and positive values as counting from the start and negative values as counting from the end.
* Non-number values are treated as in between the positive and negative values (their order is considered to be irrelevant).
*
*/
export function biEndSortFn(pos1: unknown, pos2:unknown) : -1 | 0 | 1 {
if(pos1 == pos2) return null
const nonum1 = typeof pos1 != 'number'
const nonum2 = typeof pos2 != 'number'
// non-number values are treated as beeing equally big
if(nonum1 && nonum2) return 0
//non-numbers are treated as beeing bigger than any zero or positive value
if(nonum1 && (pos2 >= 0) ) return +1
if(nonum2 && (pos1 >= 0) ) return -1
//non-numbers are treated as beeing smaller than any negative value
if(nonum1 && (pos2 < 0) ) return -1
if(nonum2 && (pos1 < 0) ) return +1
//negative values are treated as position counted from the end
// < 0 means bigger than any zero or positive value
if( (pos1<0) && pos2>= 0) return +1
if( (pos2<0) && pos1>= 0) return -1
return pos1 > pos2
? +1
: -1
// 3, -1, -3, undefined, 'blub', 0, 0, 5, -10
// becomes:
// 0, 0, 3, 5 undefined, 'blub' , -10, -3, -1
}
export function mapSort( map: (u: unknown) => unknown, sort : (x: unknown, y: unknown) => number ) : (a: unknown, b: unknown) => number {
return (a: unknown, b: unknown) => sort( map(a), map(b) )
}
/**
* Returs a sorting function to be used in Array.sort(). Objects will be sorted by their property mathching the first parameter.
* If a second paramter is given, the property matching it will be used as secondary sort criterium. Values will be sorted by {@link biEndSortFn}.
* If a property turns out to be a function, the function will be called and its return value used for sorting.
*/
export function sortByKeyFn(key: string, secondary_key : string | null = null) : (x:unknown, y:unknown) => -1 | 0 | 1 {
return (item1: unknown, item2: unknown) => {
if(typeof key != 'string') return 0
let pos1 = (item1 as Record<string, unknown>)[key]
let pos2 = (item2 as Record<string, unknown>)[key]
if(typeof pos1 == 'function') pos1 = pos1()
if(typeof pos2 == 'function') pos2 = pos2()
const s = (typeof pos1 == 'number') && (typeof pos2 == 'number')
? biEndSortFn(pos1, pos2)
: pos1 > pos2
? 1
: pos1 < pos2
? -1
: 0
return s == 0
? sortByKeyFn(secondary_key)(item1, item2)
: s
}
}
/**
* Returns a promise that will never resolve, but reject after the given
* number of milliseconds with an optional message. Meant to be used in
* Promise.race in conjunction with other promises, that are prone to never resolve.
* @deprecated
*/
export async function timeoutPromise(ms: number, message? : string) : Promise<void>{
console.warn("timeoutPromise is deprecated, use getTimeoutPromise instead.")
return getTimeoutPromise(ms,message)
}
/**
* Returns a promise, that will resolve after a given number of milliseconds
* with optionally provided data. Meant to be used in
* Promise.race in conjunction with other promises, that are prone to never resolve.
* @deprecated
*/
export async function fallbackPromise<T>(ms: number, data? :T) : Promise<T>{
console.warn("fallbackPromise is deprecated, use getFallbackPromise instead.")
return getFallbackPromise<T>(ms,data)
}
/**
* Returns a promise that will never resolve, but reject after the given
* number of milliseconds with an optional message. Meant to be used in
* Promise.race in conjunction with other promises, that are prone to never resolve.
*/
export async function getTimeoutPromise(ms: number, message? : string) : Promise<void>{
return new Promise( (resolve, reject) => {
setTimeout(() => reject(new Error(message || 'timeout')), ms)
})
}
/**
* Returns a promise, that will resolve after a given number of milliseconds
* with optionally provided data. Meant to be used in
* Promise.race in conjunction with other promises, that are prone to never resolve.
*/
export async function getFallbackPromise<T>(ms: number, data? :T) : Promise<T>{
return new Promise( (resolve) => {
setTimeout(() => resolve(data), ms)
})
}
/**
* Counts the number of decimal places
* @returns Number of decimal places if a single number is provides.
* If an array of number is provided, returns the the maximum of the individual results.
*/
export function getDecimalPlaces( num : number[] | number ): number {
if(Array.isArray(num)) return Math.max(0, ...num.map( n => getDecimalPlaces(n) ) )
return (num.toString().split('.')[1] || '').length
}
/**
* Round a number to a given number of decimal places.
*/
export function round(num : number, decimal_places: number): number {
const scale = Math.pow(10,decimal_places)
return Math.round(num*scale)/scale
}
/**
* Find the greatest common divisor. *
*/
export function gcd(...numbers: number[]): number {
if(numbers.length == 1) return numbers[0]
if(numbers.length > 1) return gcd( gcd(numbers[0], numbers[1]), ...numbers.slice(2) )
const a = numbers[0]
const b = numbers[1]
return b == 0
? a
: gcd(b, a % b)
}