File

lib/core/src/utils/utils.ts

Indexable

[index: string]: NestedRecord | T
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)

 }

results matching ""

    No results matching ""