/** * Various utility functions. */ module TK { /** * Functions that does nothing (useful for default callbacks) */ export function nop(): void { } /** * Identity function (returns the sole argument untouched) */ export function identity(input: T): T { return input; } /** * Check a value for a boolean equivalent */ export function bool(value: T | null | undefined): value is T; export function bool(value: any): boolean { if (!value) { return false; } else if (typeof value == "object") { return Object.keys(value).length > 0; } else { return true; } } /** * Return a default value if the given one is undefined */ export function coalesce(value: string | null | undefined, fallback: string): string; export function coalesce(value: number | null | undefined, fallback: number): number; export function coalesce(value: T | null | undefined, fallback: T): T { if (typeof value == "undefined" || value === null) { return fallback; } else { return value; } } /** * Check for an object being an instance of a type, an returns casted version * * Throws an error on failure to cast */ export function as(classref: { new(...args: any[]): T }, obj: any): T { if (obj instanceof classref) { return obj; } else { console.error("Bad cast", obj, classref); throw new Error("Bad cast"); } } /** * Apply a functor on the result of another function */ export function fmap(g: (arg: U) => V, f: (...args: any[]) => U): (...args: any[]) => V { // TODO variadic typing, as soon as supported by typescript return (...args) => g(f(...args)); } /** * Apply a default value to nulls or undefineds returned by a function */ export function nnf(fallback: T, f: (...args: any[]) => T | null): (...args: any[]) => T { return fmap(val => val === null ? fallback : val, f); } /** * Check if a value if null, throwing an exception if its the case */ export function nn(value: T | null | undefined): T { if (value === null) { throw new Error("Null value"); } else if (typeof value == "undefined") { throw new Error("Undefined value"); } else { return value; } } /** * Remove null values from an array */ export function nna(array: (T | null)[]): T[] { return array.filter(item => item !== null); } /** * Compare operator, that can be used in sort() calls. */ export function cmp(a: any, b: any, reverse = false): number { if (a > b) { return reverse ? -1 : 1; } else if (a < b) { return reverse ? 1 : -1; } else { return 0; } } /** * Clamp a value in a range. */ export function clamp(value: T, min: T, max: T): T { if (value < min) { return min; } else if (value > max) { return max; } else { return value; } } /** * Perform a linear interpolation between two values (factor is between 0 and 1). */ export function lerp(factor: number, min: number, max: number): number { return min + (max - min) * factor; } /** * Make a deep copy of any object. * * Serializer is used for this, and needs a namespace to work. * * Please be aware that contained RObjects will be duplicated, but keep their ID, thus breaking the uniqueness. */ export function duplicate(obj: T, namespace: Object = TK): T { let serializer = new Serializer(namespace); let serialized = serializer.serialize({ dat: obj }); return serializer.unserialize(serialized).dat; } /** * Make a shallow copy of an array. */ export function acopy(input: T[]): T[] { return input.slice(); } /** * Call a function for each member of an array, sorted by a key. */ export function itersorted(input: T[], keyfunc: (item: T) => any, callback: (item: T) => void): void { var array = acopy(input); array.sort((item1, item2) => cmp(keyfunc(item1), keyfunc(item2))); array.forEach(callback); } /** * Capitalize the first letter of an input string. */ export function capitalize(input: string): string { return input.charAt(0).toLocaleUpperCase() + input.slice(1); }; /** * Check if an array contains an item. */ export function contains(array: T[], item: T): boolean { return array.indexOf(item) >= 0; } /** * Produce an n-sized array, with integers counting from 0 */ export function range(n: number): number[] { var result: number[] = []; for (var i = 0; i < n; i++) { result.push(i); } return result; } /** * Produce an array of couples, build from the common length of two arrays */ export function zip(array1: T1[], array2: T2[]): [T1, T2][] { var result: [T1, T2][] = []; var n = (array1.length > array2.length) ? array2.length : array1.length; for (var i = 0; i < n; i++) { result.push([array1[i], array2[i]]); } return result; } /** * Produce two arrays, build from an array of couples */ export function unzip(array: [T1, T2][]): [T1[], T2[]] { return [array.map(x => x[0]), array.map(x => x[1])]; } /** * Partition a list by a predicate, returning the items that pass the predicate, then the ones that don't pass it */ export function binpartition(array: T[], predicate: (item: T) => boolean): [T[], T[]] { let pass: T[] = []; let fail: T[] = []; array.forEach(item => (predicate(item) ? pass : fail).push(item)); return [pass, fail]; } /** * Yields the neighbors tuple list */ export function neighbors(array: T[], wrap = false): [T, T][] { var result: [T, T][] = []; if (array.length > 0) { var previous = array[0]; for (var i = 1; i < array.length; i++) { result.push([previous, array[i]]); previous = array[i]; } if (wrap) { result.push([previous, array[0]]); } return result; } else { return []; } } /** * Type filter, to return a list of instances of a given type */ export function tfilter(array: any[], filter: (item: any) => item is T): T[] { return array.filter(filter); } /** * Class filter, to return a list of instances of a given type */ export function cfilter(array: any[], classref: { new(...args: any[]): T }): T[] { return array.filter((item): item is T => item instanceof classref); } /** * Flatten a list of lists */ export function flatten(array: T[][]): T[] { return array.reduce((a, b) => a.concat(b), []); } /** * Count each element in an array */ export function counter(array: T[], equals: (a: T, b: T) => boolean = (a, b) => a === b): [T, number][] { var result: [T, number][] = []; array.forEach(item => { var found = first(result, iter => equals(iter[0], item)); if (found) { found[1]++; } else { result.push([item, 1]); } }); return result; } /** * Return the first element of the array that matches the predicate, null if not found */ export function first(array: T[], predicate: (item: T) => boolean): T | null { for (var i = 0; i < array.length; i++) { if (predicate(array[i])) { return array[i]; } } return null; } /** * Return whether if any element in the array matches the predicate */ export function any(array: T[], predicate: (item: T) => boolean): boolean { return first(array, predicate) != null; } /** * Return an iterator over an array * * An iterator is a function yielding the next value each time, until the end of array where it yields null. * * For more powerful iterators, see Iterators */ export function iterator(array: T[]): () => T | null { let i = 0; return () => (i < array.length) ? array[i++] : null; } /** * Iterate a list of (key, value) in an object. */ export function iteritems(obj: { [key: string]: T }, func: (key: string, value: T) => void) { for (var key in obj) { if (obj.hasOwnProperty(key)) { func(key, obj[key]); } } } /** * Transform an dictionary object to a list of couples (key, value). */ export function items(obj: { [key: string]: T }): [string, T][] { let result: [string, T][] = []; iteritems(obj, (key, value) => result.push([key, value])); return result; } /** * Return the list of keys from an object. */ export function keys(obj: T): (Extract)[] { var result: (Extract)[] = []; for (var key in obj) { if (obj.hasOwnProperty(key)) { result.push(key); } } return result; } /** * Return the list of values from an object. */ export function values(obj: { [key: string]: T }): T[] { var result: T[] = []; for (var key in obj) { if (obj.hasOwnProperty(key)) { result.push(obj[key]); } } return result; } /** * Iterate an enum values. */ export function iterenum(obj: T, callback: (item: number) => void) { for (var val in obj) { var parsed = parseInt(val, 10); if (!isNaN(parsed)) { callback(parsed); } } } /** * Collect enum values. */ export function enumvalues(obj: T): number[] { let result: number[] = []; iterenum(obj, val => result.push(val)); return result; } /** * Create a dictionary from a list of couples */ export function dict(array: [string, T][]): { [index: string]: T } { let result: { [index: string]: T } = {}; array.forEach(([key, value]) => result[key] = value); return result; } /** * Create a dictionnary index from a list of objects */ export function index(array: T[], keyfunc: (obj: T) => string): { [key: string]: T } { var result: { [key: string]: T } = {}; array.forEach(obj => result[keyfunc(obj)] = obj); return result; } /** * Add an item to the end of a list, only if not already there */ export function add(array: T[], item: T): boolean { if (!contains(array, item)) { array.push(item); return true; } else { return false; } } /** * Remove an item from a list if found. Return true if changed. */ export function remove(array: T[], item: T): boolean { var idx = array.indexOf(item); if (idx >= 0) { array.splice(idx, 1); return true; } else { return false; } } /** * Check if two standard objects are equal. */ export function equals(obj1: { [key: string]: T }, obj2: { [key: string]: T }): boolean { return JSON.stringify(obj1) == JSON.stringify(obj2); } /** * Call a function on any couple formed from combining two arrays. */ export function combicall(array1: T[], array2: T[], callback: (item1: T, item2: T) => void): void { array1.forEach(item1 => array2.forEach(item2 => callback(item1, item2))); } /** * Combinate two filter functions (predicates), with a boolean and. */ export function andfilter(filt1: (item: T) => boolean, filt2: (item: T) => boolean): (item: T) => boolean { return (item: T) => filt1(item) && filt2(item); } /** * Get the class name of an object. */ export function classname(obj: Object): string { return (obj.constructor).name; } /** * Get the lowest item of an array, using a mapping function. */ export function lowest(array: T[], rating: (item: T) => number): T { var rated = array.map((item: T): [T, number] => [item, rating(item)]); rated.sort((a, b) => cmp(a[1], b[1])); return rated[0][0]; } /** * Return a function bound to an object. * * This is useful to pass the bound function as callback directly. */ export function bound(obj: T, func: K): T[K] { let attr = obj[func]; if (attr instanceof Function) { return attr.bind(obj); } else { return (() => attr); } } /** * Return a 0.0-1.0 factor of progress between two limits. */ export function progress(value: number, min: number, max: number) { var result = (value - min) / (max - min); return clamp(result, 0.0, 1.0); } /** * Copy all fields of an object in another (shallow copy) */ export function copyfields(src: Partial, dest: Partial) { for (let key in src) { if (src.hasOwnProperty(key)) { dest[key] = src[key]; } } } /** * Copy an object (only a shallow copy of immediate properties) */ export function copy(object: T): T { let objectCopy = Object.create(object.constructor.prototype); copyfields(object, objectCopy); return objectCopy; } /** * Merge an object into another */ export function merge(base: T, incoming: Partial): T { let result = copy(base); copyfields(incoming, result); return result; } export const STOP_CRAWLING = {}; /** * Recursively crawl through an object, yielding any defined value found along the way * * If *replace* is set to true, the current object is replaced (in array or object attribute) by the result of the callback * * *memo* is used to prevent circular references to be traversed */ export function crawl(obj: any, callback: (item: any) => any, replace = false, memo: any[] = []) { if (obj instanceof Object && !Array.isArray(obj)) { if (memo.indexOf(obj) >= 0) { return obj; } else { memo.push(obj); } } if (obj !== undefined && obj !== null && typeof obj != "function") { let result = callback(obj); if (result === STOP_CRAWLING) { return; } if (Array.isArray(obj)) { let subresult = obj.map(value => crawl(value, callback, replace, memo)); if (replace) { subresult.forEach((value, index) => { obj[index] = value; }); } } else if (obj instanceof Object) { let subresult: any = {}; iteritems(obj, (key, value) => { subresult[key] = crawl(value, callback, replace, memo); }); if (replace) { copyfields(subresult, obj); } } return result; } else { return obj; } } /** * Return the minimal value of an array */ export function min(array: T[]): T { return array.reduce((a, b) => a < b ? a : b); } /** * Return the maximal value of an array */ export function max(array: T[]): T { return array.reduce((a, b) => a > b ? a : b); } /** * Return the sum of an array */ export function sum(array: number[]): number { return array.reduce((a, b) => a + b, 0); } /** * Return the average of an array */ export function avg(array: number[]): number { return sum(array) / array.length; } /** * Return value, with the same sign as base */ export function samesign(value: number, base: number): number { return Math.abs(value) * (base < 0 ? -1 : 1); } /** * Return a copy of the array, sorted by a cmp function (equivalent of javascript sort) */ export function sorted(array: T[], cmpfunc: (v1: T, v2: T) => number): T[] { return acopy(array).sort(cmpfunc); } /** * Return a copy of the array, sorted by the result of a function applied to each item */ export function sortedBy(array: T1[], func: (val: T1) => T2, reverse = false): T1[] { return sorted(array, (a, b) => cmp(func(a), func(b), reverse)); } /** * Return the minimum of an array transformed by a function */ export function minBy(array: T1[], func: (val: T1) => T2): T1 { return array.reduce((a, b) => func(a) < func(b) ? a : b); } /** * Return the maximum of an array transformed by a function */ export function maxBy(array: T1[], func: (val: T1) => T2): T1 { return array.reduce((a, b) => func(a) > func(b) ? a : b); } /** * Return a copy of an array, containing each value only once */ export function unique(array: T[]): T[] { return array.filter((value, index, self) => self.indexOf(value) === index); } /** * Return the union of two arrays (items in either array) */ export function union(array1: T[], array2: T[]): T[] { return array1.concat(difference(array2, array1)); } /** * Return the difference between two arrays (items in the first, but not in the second) */ export function difference(array1: T[], array2: T[]): T[] { return array1.filter(value => !contains(array2, value)); } /** * Return the intersection of two arrays (items in both arrays) */ export function intersection(array1: T[], array2: T[]): T[] { return array1.filter(value => contains(array2, value)); } /** * Return the disjunctive union of two arrays (items not in both arrays) */ export function disjunctunion(array1: T[], array2: T[]): T[] { return difference(union(array1, array2), intersection(array1, array2)); } }