UNPKG

simple-pure-utils

Version:

Funciones puras para manipulación de objetos, arreglos, promesas y observables

1,247 lines 124 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.range = exports.reorder = exports.intersectKeys = exports.isSubset = exports.mergeObj = exports.intersect = exports.mapKeys = exports.filterIf = exports.concat = exports.excludeKeys = exports.exclude = exports.unionKey = exports.xor = exports.union = exports.unique = exports.awaitObj = exports.objRxToRxObj = exports.promiseAllObj = exports.upDownItem = exports.moveItem = exports.swapItems = exports.pick = exports.omitUndefined = exports.omit = exports.filterObject = exports.enumKeys = exports.arrayToMap = exports.enumObject = exports.groupByAdjacent = exports.groupBy = exports.last = exports.single = exports.first = exports.flatten = exports.toMap = exports.deepEquals = exports.isArrayLike = exports.canBeArray = exports.toArray = exports.shallowDiff = exports.shallowEquals = exports.setEquals = exports.sequenceEquals = exports.referenceEquals = exports.containsAny = exports.containsAll = exports.contains = exports.any = exports.allEqual = exports.all = void 0; exports.mapObject = exports.shuffle = exports.outOfRange = exports.nextToPromise = exports.sum = exports.combinePath = exports.leftJoin = exports.innerJoin = exports.binarySearch = exports.indicesOf = exports.indexOf = exports.assertUnreachable = exports.delay = exports.formatDateExcel = exports.toIsoDate = exports.toDate = exports.formatDate = exports.nullOrEmpty = exports.formatNumber = exports.formatCurrency = exports.zip = exports.groupByCount = exports.mapMany = exports.runningTotalRight = exports.runningTotal = exports.mapPrevious = exports.mapPreviousRx = exports.isArray = exports.isObservable = exports.duplicatesOnAdd = exports.duplicatesOnEdit = exports.firstMap = exports.skip = exports.take = exports.toObservable = exports.asyncThunkToObservable = exports.promiseToObservable = exports.valToPromise = exports.rxFlatten = exports.min = exports.max = exports.orderByDesc = exports.orderBy = exports.sort = exports.defaultComparer = exports.combineComparers = exports.removeAt = exports.remove = exports.replace = exports.push = void 0; exports.padRight = exports.padLeft = exports.asciiToHex = exports.hexToAscii = exports.mixClasses = exports.lerp = exports.numEqStr = exports.getDecimalCount = exports.parseFormattedNumber = exports.replaceAll = exports.trim = exports.trimRight = exports.trimLeft = exports.obsToPromise = exports.doOnSubscribe = exports.reduxStoreToRx = exports.getListenToRx = exports.treeTraversal = exports.orRx = void 0; const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const rxjs_2 = require("rxjs"); const rxOps = require("rxjs/operators"); const promise_1 = require("./promise"); const pipe_1 = require("./pipe"); /**Devuelve true si todos los elementos de un arreglo encajan con el predicado * @pred Devuelve la condición por cada elemento, si no se usa devuelve el elemento tal cual, es decir que los elementos deben de ser * truthy para pasar */ function all(arr, pred) { pred = pred || (x => !!x); for (const x of arr) { if (!pred(x)) return false; } return true; } exports.all = all; /**Devuelve true si todos los elementos de un arreglo son iguales. Si el arreglo esta vacío devuelve true * @param comparer El comparador de igualdar, por default es @see referenceEquals */ function allEqual(arr, comparer) { if (arr.length == 0) return true; const effectiveComparer = comparer || referenceEquals; const first = arr[0]; for (let i = 1; i < arr.length; i++) { const it = arr[i]; if (!effectiveComparer(first, it)) return false; } return true; } exports.allEqual = allEqual; /**Devuelve true si por lo menos un elemento del arreglo encaja con el predicado, o si existe por lo menos un elemento en caso * de que el predicado este indefinido */ function any(arr, pred) { if (pred) { for (const x of arr) { if (pred(x)) return true; } return false; } else { return arr.length > 0; } } exports.any = any; /**Devuelve true si el valor existe en el arreglo */ function contains(arr, value, comparer) { const effectiveComparer = comparer || referenceEquals; return any(arr, x => effectiveComparer(x, value)); } exports.contains = contains; /**Devuelve true si todos los valores en @see values existen en el arreglo @see arr . Si @see values esta vacío devuelve true */ function containsAll(arr, values, comparer) { return all(values, x => contains(arr, x, comparer)); } exports.containsAll = containsAll; /**Devuelve true si existe algun valor en @see values que exista en @see arr . Si @see values esta vacío devuelve false */ function containsAny(arr, values, comparer) { return any(values, x => contains(arr, x, comparer)); } exports.containsAny = containsAny; /** * Alias para el operador === * @param a * @param b */ function referenceEquals(a, b) { return a === b; } exports.referenceEquals = referenceEquals; /**Compara dos arreglos valor por valor */ function sequenceEquals(a, b, comparer) { if (a === b) return true; if (a == null || b == null) return false; if (a.length != b.length) return false; comparer = comparer || ((a, b) => a === b); for (let i = 0; i < a.length; i++) { if (!comparer(a[i], b[i])) return false; } return true; } exports.sequenceEquals = sequenceEquals; /**Devuelve true si 2 arreglos contienen los mismos valores, sin considerar el orden o la cantidad de veces que el mismo valor esta repetido en el arreglo * @param comparer Función que se usa para comparar los elementos, si no se especifica, se usa el referenceEquals */ function setEquals(a, b, comparer) { for (const aItem of a) { if (!contains(b, aItem, comparer)) return false; } for (const bItem of b) { if (!contains(a, bItem, comparer)) return false; } return true; } exports.setEquals = setEquals; function shallowEqualsCompareByRef(x) { const type = typeof x; const primTypeByRef = type == "string" || type == "boolean" || type == "number" || type == "symbol" || type == "function"; return primTypeByRef || (0, promise_1.isPromiseLike)(x) || isObservable(x); } /**Compara dos objetos propiedad por propiedad */ function shallowEquals(a, b, comparer) { if (typeof (a) == "function" || typeof (b) == "function") { //Las funciones se comparan por igualdad estricta: return a === b; } if ((typeof a) != (typeof b)) return false; if (shallowEqualsCompareByRef(a) || shallowEqualsCompareByRef(b)) { return a === b; } if (a instanceof Date || b instanceof Date) { if (a instanceof Date && b instanceof Date) { return a.valueOf() == b.valueOf(); } else return false; } if (a === b) return true; if (a == null || b == null) return false; if (canBeArray(a) && canBeArray(b)) { return sequenceEquals(toArray(a), toArray(b), comparer); } const aKeys = Object.keys(a).map(x => x); const bKeys = Object.keys(b).map(x => x); if (aKeys.length != bKeys.length) return false; return sequenceEquals(aKeys.map(x => a[x]), aKeys.map(x => b[x]), comparer); } exports.shallowEquals = shallowEquals; /** * Compara 2 objetos propiedad por propiedad, devuelve un objeto con las propiedades que son diferentes asignadas a true * @param a Objeto a * @param b Objecto b * @param comparer Comparador de las propiedades. Se usa por default referenceEquals */ function shallowDiff(a, b, comparer) { const eComp = comparer || referenceEquals; const props = (0, pipe_1.pipe)(union(Object.keys(a), Object.keys(b)), curr => curr.map(x => ({ key: x, value: a[x], otherValue: b[x], refEquals: a[x] === b[x] })), curr => curr.filter(x => !eComp(x.value, x.otherValue)), curr => curr.map(x => x.key), curr => arrayToMap(curr, item => item, item => true)); return props; } exports.shallowDiff = shallowDiff; /**Convierte un ArrayLike o Iterable en un arreglo. Si el valor ya es un arreglo devuelve el valor */ function toArray(arr) { if (arr instanceof Array) { return arr; } const isArrayLike = (x) => x.lenght !== undefined; const ret = []; if (isArrayLike(arr)) { for (let i = 0; i < arr.length; i++) { ret.push(arr[i]); } } else { for (const a of arr) { ret.push(a); } } return ret; } exports.toArray = toArray; /**Devuelve true si un objeeto se puede convertir a un arreglo utilizando la función toArray */ function canBeArray(arr) { return isArrayLike(arr) || hasIterationProtocol(arr); } exports.canBeArray = canBeArray; /**Devuelve true si x es un array o un array like. Note que devuelve true para string. Normalmente es mejor usar la función @see isArray ya que esa si devuelve false para @see string*/ function isArrayLike(x) { return x != null && x.length !== undefined; } exports.isArrayLike = isArrayLike; const hasIterationProtocol = (variable) => variable !== null && Symbol.iterator in Object(variable); function deepEquals(a, b) { const deep = (a, b) => shallowEquals(a, b, deep); return deep(a, b); } exports.deepEquals = deepEquals; /**Convierte un arreglo a un objeto */ function toMap(arr, key, value) { const ret = {}; for (const x of arr) ret[key(x)] = value(x); return ret; } exports.toMap = toMap; /**Aplana una colección de colecciones */ function flatten(arr) { const ret = []; for (const a of arr) ret.push(...a); return ret; } exports.flatten = flatten; /**Devuelve el primer elemento de un arreglo o indefinido si no se encontro ninguno, opcionalmente * filtrado por un predicado */ function first(arr, pred) { for (const a of arr) { if (!pred || pred(a)) return a; } return undefined; } exports.first = first; /** * Devuelve el unico elemento de un arreglo que cumpla con la condición, si no se encontró ninguo o mas de uno devuelve undefined */ function single(arr, pred) { let firstItem = undefined; let first = false; for (const a of arr) { const pass = !pred || pred(a); if (pass) { if (first) { //mas de uno return undefined; } else { firstItem = a; first = true; } } } return firstItem; } exports.single = single; /**Devuelve el ultimo elemento de un arreglo */ function last(arr) { return arr[arr.length - 1]; } exports.last = last; /**Agrupa un arreglo por una llave. Se preserva el orden original de los elementos del arreglo, segun los elementos agrupadores que aparezcan primero, tambien * el orden adentro del grupo es preservado * @param comparer Comparador de la llave por default es un shallowEquals */ function groupBy(arr, keySelector, comparer) { const ret = []; const comparerDefault = comparer || shallowEquals; for (const x of arr) { const key = keySelector(x); const firstItem = first(ret, x => comparerDefault(x.key, key)); if (firstItem === undefined) { ret.push({ key: key, items: [x] }); } else { firstItem.items.push(x); } } return ret; } exports.groupBy = groupBy; /**Agrupa un arreglo por una llave, siempre y cuando los elementos sean contiguos en el arreglo, es decir, si un grupo es interrumpido por un elemento con otra * clave se temrinara ese grupo y comenzará otro con la nueva clave, por lo que pueden existir multiples grupos no contiguos con la misma clave * * En caso de que @param arr este ordenado en funcion de @param keySelector, es equivalente pero mas eficiente que @see groupBy */ function groupByAdjacent(arr, keySelector, comparer) { let ret = []; const comparerDefault = comparer || shallowEquals; for (const x of arr) { const currentKey = ret.length == 0 ? null : { value: ret[ret.length - 1].key }; const itemKey = keySelector(x); if (currentKey == null || comparerDefault(currentKey.value, itemKey) == false) { ret.push({ key: itemKey, items: [x] }); } else { ret[ret.length - 1].items.push(x); } } return ret; } exports.groupByAdjacent = groupByAdjacent; function enumObject(obj, selector) { const defaultSelector = ((key, value) => ({ key, value })); const effectiveSelector = selector || defaultSelector; if (selector) { return Object.keys(obj).map(key => selector(key, obj[key])); } else { return Object.keys(obj).map(key => key).map(key => defaultSelector(key, obj[key])); } } exports.enumObject = enumObject; function arrayToMap(array, keySelector, valueSelector) { const defaultKeySelector = (item) => item.key; const defaultValueSelector = (item) => item.value; const effectiveKeySelector = keySelector || defaultKeySelector; const effectiveValueSelector = valueSelector || defaultValueSelector; const ret = {}; for (let i = 0; i < array.length; i++) { const a = array[i]; const key = effectiveKeySelector(a, i); const value = effectiveValueSelector(a, i); ret[key] = value; } return ret; } exports.arrayToMap = arrayToMap; /**Obtiene los valores numericos de un enum */ function enumKeys(e) { return Object.keys(e).map(x => e[x]).filter(x => typeof (x) == "number"); } exports.enumKeys = enumKeys; /** * Filtra las propiedades de un objeto * @param obj Objeto que se va a filtrar * @param pred Predicado que va a determinar que propiedades si se van a conservar */ function filterObject(obj, pred) { const ret = {}; for (const key in obj) { const value = obj[key]; if (pred(value, key)) { ret[key] = value; } } return ret; } exports.filterObject = filterObject; /** * Quita un conjunto de propiedades de un objeto * @param obj El objeto original * @param keys Las propiedades que se desean quitar */ function omit(obj, keys) { return filterObject(obj, (value, key) => !contains(keys, key)); } exports.omit = omit; /**Quita las propiedades que esten indefinidas en un objeto */ function omitUndefined(obj) { return filterObject(obj, value => value !== undefined); } exports.omitUndefined = omitUndefined; /**Devuelve un objeto son solo ciertas propiedades del objeto incluidas. Si se escoge una propiedad que no existe en el objeto esta no estará incluida en el objeto devuelto */ function pick(obj, ...props) { let ret = {}; for (const key of props) { if (key in obj) { ret[key] = obj[key]; } } return ret; } exports.pick = pick; /**Intercambia 2 elementos de un arreglo, si los indices dados estan afuera del arreglo, lanza una excepción */ function swapItems(array, a, b) { const inside = (x) => x >= 0 && x < array.length; if (!inside(a) || !inside(b)) throw new Error("Indice fuera de rango"); return array.map((x, i, arr) => i == a ? arr[b] : i == b ? arr[a] : arr[i]); } exports.swapItems = swapItems; /**Mueve un elemento del arreglo de un indice a otro, note que no es igual a swapItems ya que al mover un elemento se conserva el orden de todos los de más elemento, esto no ocurre con el swap que * simplemente intercambia de posición 2 elementos. Si los indices estan fuera de rango lanza uan excepción */ function moveItem(array, sourceIndex, destIndex) { const inside = (x) => x >= 0 && x < array.length; if (!inside(sourceIndex) || !inside(destIndex)) throw new Error("Indice fuera de rango"); //Si los valroes son iguales devuelve el arreglo tal cual if (sourceIndex == destIndex) return array; //Dirección del movimiento, puede ser -1 o +1 const dir = Math.sign(destIndex - sourceIndex); const min = Math.min(sourceIndex, destIndex); const max = Math.max(sourceIndex, destIndex); return array.map((x, i, arr) => (i < min || i > max) ? x : (i == destIndex) ? arr[sourceIndex] : arr[i + dir]); } exports.moveItem = moveItem; /**Mueve un elemento hacia array o hacia abajo, si el elemento no se puede mover ya que esta en el borde del arreglo devuelve el arreglo tal cual */ function upDownItem(array, index, direction) { if ((index == 0 && direction == "up") || (index == array.length - 1 && direction == "down")) { return array; } else { return moveItem(array, index, index + (direction == "up" ? -1 : +1)); } } exports.upDownItem = upDownItem; function promiseAllObj(obj) { const keys = Object.keys(obj); const values = keys.map(key => obj[key]); const all = Promise.all(values); const ret = all.then(arr => arr.map((value, index) => ({ key: keys[index], value: value }))).then(x => arrayToMap(x)); return ret; } exports.promiseAllObj = promiseAllObj; /**Convierte un objeto de observables a un observable de objetos, el primer elemento del observable resultante se da cuando todos los observables del objeto lanzan el primer valor. * Es muy similar al @see combineLatest pero en lugar de funcionar con un arreglo funciona con un objeto */ function objRxToRxObj(obj) { const keys = Object.keys(obj); ; const values = keys.map(key => obj[key].pipe((0, operators_1.map)(x => ({ value: x, key: key })))); const combine = (0, rxjs_1.combineLatest)(values).pipe((0, operators_1.map)(x => arrayToMap(x))); return combine; } exports.objRxToRxObj = objRxToRxObj; /**Convierte una promesa de un objeto a un objeto de promesas * @param include Nombres de las propiedades que se desean incluir en el objeto resultante */ function awaitObj(obj, include) { const ret = (0, pipe_1.pipe)(include, inc => filterObject(inc, x => !!x), inc => mapObject(inc, (value, key) => obj.then(x => x[key]))); return ret; } exports.awaitObj = awaitObj; /**Devuelve todos los elementos de un arreglo que no estan repetidos, respetando el orden original en el que aparecen primero. * @param comparer Comparador que determina si 2 elementos son iguales. Se usa el operador === */ function unique(arr, comparer) { return groupBy(arr, x => x, comparer !== null && comparer !== void 0 ? comparer : referenceEquals).map(x => x.key); } exports.unique = unique; /**Devuelve todos los elementos de todos los arreglos que no esten repetidos. * Conserva el orden pero no los elementos repetidos */ function union(...arr) { return unique(concat(...arr)); } exports.union = union; /** * Devuelve todos los elementos en @param a que no estén en @param b, y * todos los elementos en @param b que no estén en @param a. * Es equivalente a la operación XOR / diferencia simétrica de conjuntos */ function xor(a, b) { return union(exclude(a, b), exclude(b, a)); } exports.xor = xor; /** * Devuelve todos los elementos en A que su llave no se encuentre en B las llaves de B y todos los elementos en B, en ese orden * esto equivale a unir los conjuntos A+B, pero dandole prioridad a los elementos en B si es que esa llave se encuentra en los dos conjuntos. * Esto se puede ver como una operacion de INSERT OR UPDATE en A * @param comparer Comparador de la llave. Es @see deepEquals por default */ function unionKey(a, b, getKey, comparer) { const effComparer = comparer || deepEquals; let result = []; //Obtiene todas las claves en B const bKeys = b.map(getKey); //Todos los elementos en A que no esten en B for (const aItem of a) { const aKey = getKey(aItem); if (!contains(bKeys, aKey, effComparer)) { result.push(aItem); } } //Todos los elementos en B result.push(...b); return result; } exports.unionKey = unionKey; /**Devuelve todos los elementos en A que no esten en B. Es la operacion A-B de conjuntos. Conserva el orden y los elementos repetidos de A */ function exclude(a, b, comparer) { const comparerEff = comparer && ((b, a) => comparer(a, b)); return a.filter(aItem => !contains(b, aItem, comparerEff)); } exports.exclude = exclude; /**Devuelve todos los elementos en "items" tal que su key no se encuentre una o mas veces en "keys". Conserva el orden original de "items". * @param keySelector Obtiene la clave de un elemento * @param comparer Comparedor de igualdad. Por default se usa el shallowEquals */ function excludeKeys(items, keys, keySelector, comparer) { const comparerEff = comparer || shallowEquals; return exclude(items, keys, (a, b) => comparerEff(keySelector(a), b)); } exports.excludeKeys = excludeKeys; /**Pega todos los elementos de los arreglos */ function concat(...arr) { return arr.reduce((acum, curr) => [...acum, ...curr], []); } exports.concat = concat; /**Filtra el arreglo sólo si condition == true, si es false devuelve el arreglo tal cual */ function filterIf(arr, predicate, condition) { return condition ? arr.filter(predicate) : arr; } exports.filterIf = filterIf; /**Dado un arreglo de keys, para cada key mapea a el elemento que le corresponde. * Si existen varios elementos con la misma clave, cuando se encuentre esa clave se devolverá el primer elemento en el arreglo values con esa clave * @param keys Claves que se van a mapear * @param values Valores en los que se va a buscar para cada clave, el valor que tiene esa clave * @param keySelector Obtener la clave de un elemento * @param keyComparer Comparador que se usará para determinar si dos claves son iguales. Por default se usa el shallowEquals */ function mapKeys(keys, values, keySelector, keyComparer) { const effectiveKeyComparer = keyComparer || shallowEquals; return keys.map(key => first(values, value => shallowEquals(key, keySelector(value)))); } exports.mapKeys = mapKeys; /**Devuelve todos los elementos en "a" que se encuentren también en "b". Conserva el orden original de "a" * @param comparer Comparedor de igualdad. Por default se usa el referenceEquals */ function intersect(a, b, comparer) { return intersectKeys(a, b, x => x, comparer || referenceEquals); } exports.intersect = intersect; /**Mapea cada una de las propiedades en A que encajen en B y viceversa */ function mergeObj(a, b, merge) { const keys = union(Object.keys(a), Object.keys(b)); const values = keys.map(key => ({ key: key, value: merge(a[key], b[key], key) })); return arrayToMap(values); } exports.mergeObj = mergeObj; /**Devuelve true si SET contiene todos los elementos en SUBSET. Si los conjuntos son iguales devuelve true. * Si los dos arreglos estan vacios devuelve true */ function isSubset(set, subset, comparer) { //Si por lo menos un elemento en subset no existe en set, ya no es un subset return !any(subset, x => !contains(set, x)); } exports.isSubset = isSubset; /**Devuelve todos los elementos en "items" tal que su key se encuentre una o mas veces en "keys". Conserva el orden original de "items". * @param keySelector Obtiene la clave de un elemento * @param comparer Comparador de igualdad. Por default se usa el shallowEquals */ function intersectKeys(items, keys, keySelector, comparer) { return items.filter(item => contains(keys, keySelector(item), comparer || shallowEquals)); } exports.intersectKeys = intersectKeys; /** * Reordena items segun las claves en keys. * Si hay claves repetidas en el arreglo de keys, resulta en apariciones repetidas de ese elemento. * Si hay varios elementos con la misma clave, todos esos elementos aparecen en el orden original al aparecer su clave * Si no hay ningun elemento con cierta clave, esa clave se ignora * @param items Arreglo a reordenar * @param keys Arreglo de claves * @param keySelector Obtiene la clave de un elemento * @param comparer Comparador de igualdad. Por default se usa el shallowEquals */ function reorder(items, keys, keySelector, comparer) { const comparerEff = comparer || shallowEquals; return mapMany(keys, key => items.filter(y => comparerEff(keySelector(y), key))); } exports.reorder = reorder; /**Devuelve un rango de numeros */ function range(start, count, step) { let ret = []; step = step || 1; const end = start + count * step; for (let i = start; i < end; i += step) { ret.push(i); } return ret; } exports.range = range; /** * Devuelve un nuevo arreglo con todo el arreglo original mas el elemento al final */ function push(arr, item) { return [...arr, item]; } exports.push = push; /** * Remplaza todos los valores del arreglo que cumplan con cierta condicion */ function replace(arr, condition, newValue) { return arr.map((x, i) => condition(x, i) ? newValue : x); } exports.replace = replace; /**Elimina un elemento del arreglo */ function remove(arr, item) { return arr.filter(x => x != item); } exports.remove = remove; /**Elimina un elemento del arreglo dado su indice */ function removeAt(arr, index) { return arr.filter((x, i) => i != index); } exports.removeAt = removeAt; /** * Combina varias funciones comparadores que pueden ser usadas para alimentar a la función sort. Se le da prioridad a los primeros comparadores, * si un comparador devuelve 0, entonces se evalue el segundo * @param comparers */ function combineComparers(...comparers) { return (a, b) => { for (const comp of comparers) { const result = comp(a, b); if (result != 0) return result; } return 0; }; } exports.combineComparers = combineComparers; /**Comparador de ordenamiento por default. Si a es mayor que b devuelve 1, si es menor -1 */ function defaultComparer(a, b) { if (a === b) { return 0; } else if (a === null && b === undefined) { return 1; } else if (a === undefined && b === null) { return -1; } else return a > b ? 1 : a < b ? -1 : 0; } exports.defaultComparer = defaultComparer; /**Ordena un arreglo de forma estable, a diferencia de con array.sort el arreglo original no es modificado * @param comparers Comparadores de ordenamiento, se le da precedencia al primero. Si no se especifica ninguno se usará el comparador por default */ function sort(arr, ...comparers) { comparers = comparers.length == 0 ? [defaultComparer] : comparers; const toEffComparer = (func) => (a, b) => func(a.value, b.value); //Comparamos tambien por indice const effComparers = [ ...comparers.map(toEffComparer), (a, b) => a.index - b.index ]; const effectiveComparer = combineComparers(...effComparers); const copy = arr.map((x, i) => ({ value: x, index: i })); copy.sort(effectiveComparer); const ret = copy.map(x => x.value); return ret; } exports.sort = sort; /** * Ordena un arreglo de forma estable segun ciertas claves seleccionadas usando el comparador por default */ function orderBy(arr, ...keySelectors) { const comparers = keySelectors.map(selector => (a, b) => +defaultComparer(selector(a), selector(b))); return sort(arr, ...comparers); } exports.orderBy = orderBy; /**Ordena un arreglo de forma estable y descendiente segun ciertas claves seleccionadas usando el comparador por default */ function orderByDesc(arr, ...keySelectors) { const comparers = keySelectors.map(selector => (a, b) => -defaultComparer(selector(a), selector(b))); return sort(arr, ...comparers); } exports.orderByDesc = orderByDesc; function keysToComparer(keySelectors) { const comparers = (keySelectors.length == 0) ? [defaultComparer] : (keySelectors.map(selector => (a, b) => +defaultComparer(selector(a), selector(b)))); return combineComparers(...comparers); } /**Obtiene el máximo de un arreglo dado un selector de llave */ function max(arr, ...keySelectors) { const comp = keysToComparer(keySelectors); return maxComparer(arr, comp); } exports.max = max; /**Obtiene el mínimo de un arreglo dado un selector de llave */ function min(arr, ...keySelectors) { const comp = keysToComparer(keySelectors); const inv = (a, b) => -comp(a, b); return maxComparer(arr, inv); } exports.min = min; /**Devuelve el valor máximo de un arreglo dado un comparador o undefined si el arreglo está vacío */ function maxComparer(arr, comparer) { let first = true; let max = undefined; for (const x of arr) { if (first || (comparer(x, max) > 0)) { max = x; first = false; } } return max; } /**Convierte un observable de T, de Promise<T> o de Observable<T> a un observable de <T>, efectivamente aplanando un observable anidado en uno desanidado */ function rxFlatten(observable) { const obsOfObs = observable.pipe((0, operators_1.map)(x => toObservable(x))); return obsOfObs.pipe((0, operators_1.concatAll)()); } exports.rxFlatten = rxFlatten; /**Convierte un valor o una promesa a una promesa. No devuelve Promise ya que si el valor es síncrono devuelve un PromiseLike que se resuelve inmediatamente * (el tipo Promise no se resuelve de inmediato) */ function valToPromise(value) { if ((0, promise_1.isPromiseLike)(value)) return value; return (0, promise_1.syncResolve)(value); } exports.valToPromise = valToPromise; /**Convierte una promesa a observable, si la promesa se resuelve las siguientes subscripciones obtienen el valor de la promesa de forma síncorna */ function promiseToObservable(prom) { const sub = new rxjs_1.ReplaySubject(1); prom.then(x => { sub.next(x); sub.complete(); }, err => sub.error(err)); return sub; } exports.promiseToObservable = promiseToObservable; /**Convierte una función que devuelve una promesa a un observable, * la función es llamada sólamente una vez en la primera subscripción del observable, subsecuentes subscripciones al observable no resultan en nuevas * llamadas al thunk. * * Una vez que el thunk se reseulve su valor se almacena y subscripciones posteriores devuelven inmedatamente el valor y se completan. * * @param thunk Función que se va a llamar sólo una vez, en la primera subscripción. */ function asyncThunkToObservable(thunk) { const sub = new rxjs_1.ReplaySubject(1); /**Si es la primera subscripción */ let first = true; return new rxjs_1.Observable(observer => { if (first) { //Llama al thunk sólo en la primera subscripción thunk().then(x => { sub.next(x); sub.complete(); }, err => sub.error(err)); first = false; } return sub.subscribe(observer); }); } exports.asyncThunkToObservable = asyncThunkToObservable; /**Convierte un valor o una promesa a un observable, si el valor ya es un observable lo devuelve tal cual */ function toObservable(value) { if (value instanceof rxjs_1.Observable) { return value; } else if ((0, promise_1.isPromiseLike)(value)) { return promiseToObservable(value); } return (0, rxjs_1.from)([value]); } exports.toObservable = toObservable; /**Toma los primeros N elementos del arreglo */ function take(arr, count) { let ret = []; for (var i = 0; i < Math.min(arr.length, count); i++) { ret.push(arr[i]); } return ret; } exports.take = take; /**Se salta los primeros N elementos del arreglo */ function skip(arr, count) { let ret = []; for (var i = count; i < arr.length; i++) { ret.push(arr[i]); } return ret; } exports.skip = skip; /**Obtiene le primer elemento mapeado de un arreglo o undefined */ function firstMap(arr, predicate, map) { for (let i = 0; i < arr.length; i++) { const x = arr[i]; if (predicate(x, i)) { return map(x, i); } } return undefined; } exports.firstMap = firstMap; /**Devuelve true si existiran duplicados en caso de editar un elemento de un arreglo * @param arr Arreglo * @param oldValueRef Valor anterior del arreglo * @param newValue Nuevo valor del arreglo */ function duplicatesOnEdit(arr, oldValue, newValue, keySelector) { const comparer = (a, b) => shallowEquals(keySelector(a), keySelector(b)); let foundOldValue = false; for (const item of arr) { if (comparer(item, newValue)) { if ((comparer(item, oldValue) && !foundOldValue)) { foundOldValue = true; } else { return true; } } } return false; } exports.duplicatesOnEdit = duplicatesOnEdit; /** * Devuelve true si existirán duplicados en caso de agregar un elemento a un arreglo que es equivalente a saber * si ese elemento esta contenido en el arreglo * @param arr * @param newValue * @param comparer Se usa el shallow equals por default */ function duplicatesOnAdd(arr, newValue, keySelector) { const comparer = (a, b) => shallowEquals(keySelector(a), keySelector(b)); return contains(arr, newValue, comparer); } exports.duplicatesOnAdd = duplicatesOnAdd; /**Devuelve true si x es un observable */ function isObservable(x) { return (0, rxjs_1.isObservable)(x); } exports.isObservable = isObservable; /**Devuelve true si x es un array */ function isArray(x) { return x instanceof Array; } exports.isArray = isArray; /**Mapea el valor actual y el anterior de un observable */ function mapPreviousRx(obs, startWith) { const ret = obs.pipe((0, operators_1.map)(x => ({ prev: startWith, curr: x })), (0, operators_1.scan)((acc, val) => ({ prev: acc.curr, curr: val.curr }))); return ret; } exports.mapPreviousRx = mapPreviousRx; /**Mapea cada elemento de un arreglo tomando en cuenta el elemento anterior */ function mapPrevious(items, map, initial) { let prev = initial; let ret = []; for (const it of items) { ret.push(map(prev, it)); prev = it; } return ret; } exports.mapPrevious = mapPrevious; /**Calcula un agregado corrido para cada elemento de un arreglo */ function runningTotal(items, seed, reduceState, map) { let ret = []; let state = seed; for (const it of items) { const proj = reduceState(state, it); state = proj; const output = map(state, it); ret.push(output); } return ret; } exports.runningTotal = runningTotal; /**Calcula el agregado corrido para cada elemento, recorriendo el arreglo al revez */ function runningTotalRight(items, seed, reduceState, map) { let ret = new Array(items.length); let state = seed; for (let i = items.length - 1; i >= 0; i--) { const it = items[i]; const proj = reduceState(state, it); state = proj; const output = map(state, it); ret[i] = output; } return ret; } exports.runningTotalRight = runningTotalRight; /**Mapea y aplana una colección. Es equivalente a flatten(items.map(map)) */ function mapMany(items, map) { let result = []; for (const x of items) { const mapResult = map(x); result.push(...mapResult); } return result; } exports.mapMany = mapMany; /**Divide @param arr en bloques de longitud máxima @param count, * si @param arr está vacío, devuelve un arreglo vacío */ function groupByCount(arr, count) { if (arr.length == 0) return []; if (count == 0) throw new Error("count debe de ser mayor que 0"); let ret = [[]]; for (let i = 0; i < arr.length; i++) { let curr = ret[ret.length - 1]; if (curr.length == count) { curr = []; ret.push(curr); } curr.push(arr[i]); } return ret; } exports.groupByCount = groupByCount; function zip(data, length) { //Checa que todo los arreglos tengan la misma longitud const lengths = enumObject(data).map(x => x.value.length); if (length == null && !allEqual(lengths)) { throw new Error("Todos los arreglos deben de tener la misma longitud"); } if (lengths.length == 0) return []; const count = length == null ? lengths[0] : length == "min" ? Math.min(...lengths) : Math.max(...lengths); const ret = []; for (let i = 0; i < count; i++) { const fila = mapObject(data, (value, key) => value[i]); ret.push(fila); } return ret; } exports.zip = zip; /**A una cadena que representa un numero entero, aplica el separador de miles */ function aplicarSepMiles(intpart, sep = ",") { if (intpart.length < 4) return intpart; const start = intpart.length % 3; let ret = intpart.substr(0, start); for (var i = start; i < intpart.length; i += 3) { const subpart = intpart.substr(i, 3); ret += i == 0 ? subpart : (sep + subpart); } return ret; } /**Formatea una moneda con 2 decimales y separador de miles */ function formatCurrency(number) { return formatNumber(number, 0, 2, true, "$"); } exports.formatCurrency = formatCurrency; /** * Formatea un número * @param number El numero * @param integer Cantidad de zeros a la izquierda en la parte entera * @param decimals Cantidad de zeros a la derecha en la parte desimal * @param thousep Usar separador de miles. Por default es false * @param prefix Prefijo del numero, ejemplo $ o %. Por default es "" * @param sign True para mostrar signo + si el valor > 0, si no, sólo se muestra si valor < 0 */ function formatNumber(number, integer = 0, decimals = 0, thousep = false, prefix = "", sign = false) { if (number == null) return ""; number = Number(number); if (Number.isNaN(number) || !Number.isFinite(number)) { return number.toString(); } const zeroes = "00000000000000000000"; const numSign = number < 0 ? "-" : (number > 0 && sign) ? "+" : ""; const absX = Math.abs(number); const decInt = Math.pow(10, decimals); const int = Math.trunc(Math.round(absX * decInt * 1000) / decInt / 1000); const intText = "" + int; const intZeroStr = zeroes + intText; const intPartSinSep = intZeroStr.substr(intZeroStr.length - Math.max(integer, intText.length)); const intPart = thousep ? aplicarSepMiles(intPartSinSep) : intPartSinSep; if (decimals == 0) return numSign + prefix + intPart; const frac = absX - int; const fracText = "" + Math.trunc(Math.round(frac * 1000 * Math.pow(10, decimals)) / 1000); const leftFracZeroes = zeroes.substr(0, decimals - fracText.length); const fracZeroStr = leftFracZeroes + fracText + zeroes; const fracPart = fracZeroStr.substring(0, decimals); return numSign + prefix + intPart + "." + fracPart; } exports.formatNumber = formatNumber; function nullOrEmpty(x) { return x == null || x == ""; } exports.nullOrEmpty = nullOrEmpty; const monthNames = ["ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"]; /**Formatea una fecha * @param fullDateTime true o false para indicar si mostrar las horas o no. Por default es undefined e implicar que se mostraran las horas si el valor tiene componente de horas, si no, se mostrará sólo la fecha */ function formatDate(x, fullDateTime) { if (x == null) return ""; x = new Date(x); const year = formatNumber(x.getFullYear(), 4); const month = monthNames[x.getMonth()]; const day = formatNumber(x.getDate(), 2); const hours = formatNumber(x.getHours(), 2); const minutes = formatNumber(x.getMinutes(), 2); //True si se debe de mostrar hora y fecha, si no, solo la fecha const effFull = fullDateTime == null ? (hours != "00" || minutes != "00") : fullDateTime; const dateStr = day + "/" + month + "/" + year; if (effFull) { const hourStr = hours + ":" + minutes; return dateStr + " " + hourStr; } else { return dateStr; } } exports.formatDate = formatDate; function toDate(value) { if (value == null) { return value; } if (typeof value == "string" || typeof (value) == "number") return new Date(value); return value; } exports.toDate = toDate; /**Convierte una fecha al formato ISO 8601 respetando la zona horaria */ function toIsoDate(x) { const year = formatNumber(x.getFullYear(), 4); const month = formatNumber(x.getMonth() + 1, 2); const day = formatNumber(x.getDate(), 2); const hours = formatNumber(x.getHours(), 2); const minutes = formatNumber(x.getMinutes(), 2); const seconds = formatNumber(x.getSeconds(), 2); const offsetMinVal = -x.getTimezoneOffset(); const offsetHours = formatNumber(Math.abs(offsetMinVal / 60), 2); const offsetMin = formatNumber(Math.abs(offsetMinVal) % 60, 2); const offsetSign = offsetMinVal < 0 ? "-" : "+"; const offset = offsetSign + offsetHours + ":" + offsetMin; return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${offset}`; } exports.toIsoDate = toIsoDate; /**Formatea una fecha de tal manera que sea compatible con el excel */ function formatDateExcel(x) { const f = x => formatNumber(x, 2); const f4 = x => formatNumber(x, 4); return `${f4(x.getFullYear())}-${f(x.getMonth() + 1)}-${f(x.getDate())} ${f(x.getHours())}:${f(x.getMinutes())}:${f(x.getSeconds())}`; } exports.formatDateExcel = formatDateExcel; /**Devuelve una promesa que se resuelve en cierto tiempo. Note que si ms == 0 la promesa devuelta no se resuelve síncronamente, ya que un setTimeout(..., 0) no es síncrono*/ function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } exports.delay = delay; /**Una funcion que siempre lanza una excepción al ser llamada. Sirve para implementar cases exhaustivos, tal como esta descrito en https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript*/ function assertUnreachable(x) { throw new Error(`Assert unreachable called with value ${x}`); } exports.assertUnreachable = assertUnreachable; /**Devuelve el indice de la primera aparición de un elemento que cumpla con @see pred */ function indexOf(arr, pred, startIndex = 0) { for (let i = startIndex; i < arr.length; i++) { if (pred(arr[i])) { return i; } } return null; } exports.indexOf = indexOf; /**Devuelve los indices de todas las apariciones de los items que cumplan con @see pred */ function indicesOf(arr, pred, startIndex = 0) { let ret = []; for (let i = startIndex; i < arr.length; i++) { if (pred(arr[i])) { ret.push(i); } } return ret; } exports.indicesOf = indicesOf; /**Realiza una busqueda binaria en un arreglo, devuelve el indice del elemento mas cercano y si fue encontrado o no el elemento. * En caso de que no encaje devuelve el indice del ultimo elemento que es menor que el valor de busqueda. Si ningun elemento del arreglo es menor al valor de busqueda devuelve -1. */ function binarySearch(arr, keySelector, value, comparer) { const effComparer = comparer || defaultComparer; let minIndex = 0; let maxIndex = arr.length - 1; let currentIndex = 0; let currentElement; while (minIndex <= maxIndex) { currentIndex = (minIndex + maxIndex) / 2 | 0; currentElement = keySelector(arr[currentIndex]); const comp = effComparer(currentElement, value); if (comp < 0) { minIndex = currentIndex + 1; } else if (comp > 0) { maxIndex = currentIndex - 1; } else { return { index: currentIndex, match: true }; } } return { index: minIndex - 1, match: false }; } exports.binarySearch = binarySearch; /** * Para todos los pares de elemento en left y right que cumplen con el where, devuelve ese par de elementos * Se respeta el orden del arreglo @see left * Este algoritmo corre con complejidad O^2 o NM donde N es la cantidad de elementos en left y M la cantidad de elementos en right * @param left Arreglo izquierdo * @param right Arreglo derecho * @param where Condicion para filtrar el producto cartesiano */ function innerJoin(left, right, where) { let ret = []; for (const l of left) { for (const r of right) { if (where(l, r)) { ret.push({ left: l, right: r }); } } } return ret; } exports.innerJoin = innerJoin; /** * Para todos los elementos en left y right que cumplen con el where, devuelve ese par de elementos. Si para * cierto elemento en left ningun par de elementos (left, right) cumple con la condicion, devuelve un par solo con el valor (left) asignado y el * (right) en undefined. Esto hace que todos los elementos en left sean incluidos en el resultado final incondicionalmente por lo menos una vez * Este algoritmo corre con complejidad O^2 o NM donde N es la cantidad de elementos en left y M la cantidad de elementos en right * @param left Arreglo izquierdo * @param right Arreglo derecho * @param where Condicion para filtrar el producto cartesiano */ function leftJoin(left, right, where) { let ret = []; for (const l of left) { let anyRight = false; for (const r of right) { if (where(l, r)) { anyRight = true; ret.push({ left: l, right: r }); } } if (!anyRight) { ret.push({ left: l }); } } return ret; } exports.leftJoin = leftJoin; /**Combina dos rutas. Combina correctamente las rutas con ./ y ../ * @param ruta base * @param path Ruta actual * @param pathChar Caracter que divide a la ruta * @param prefix True para iniciar la ruta con el caracter de ruta, false para no iniciar, indefinido para dejarlo tal cual. Por default is true * @param postfix True para terminar la ruta con el caracter de ruta, false para quitarlo, indefinido para dejarlo tal cual */ function combinePath(basePath, path, pathChar = "/", prefix = true, postfix = false) { function trim(s) { const start = s.startsWith(pathChar) ? 1 : 0; const end = s.endsWith(pathChar) ? (s.length - 1) : s.length; return s.substr(start, end - start); } const basePathTrim = trim(basePath); const pathTrim = trim(path); const basePathParts = basePathTrim.split(pathChar); const pathParts = pathTrim.split(pathChar); const relative = pathParts.length > 0 && pathParts[0].startsWith("."); let current = relative ? basePathTrim : pathTrim; let firstTextPart = false; if (relative) { for (const part of pathParts) { if (part == "." || part == "..") { if (firstTextPart) throw new Error("La ruta no puede contener partes con . o .. despues de una parte de texto"); } if (part == ".") { //No hay que hacer nada } else if (part == "..") { const currentParts = current.split("/"); current = currentParts.slice(0, currentParts.length - 1).join(pathChar); } else { firstTextPart = true; current += current == "" ? part : (pathChar + part); } } } const originalStartPathChar = (relative ? basePath : path).startsWith(pathChar); const originalEndsWithPathChar = path.endsWith(pathChar); //Asignar el prefijo y postfijo if (prefix == true || (prefix === undefined && originalStartPathChar)) { current = pathChar + current; } if (postfix == true || (postfix === undefined && originalEndsWithPathChar)) { current = current + pathChar; } return current; } exports.combinePath = combinePath; /**Suma un arreglo de numeros. Si el arreglo esta vacío devuelve 0. * Los valores nulos o indefinidos son ignorados en la suma */ function sum(arr) { return arr.filter(x => x != null).map(x => x).reduce((a, b) => a + b, 0); } exports.sum = sum; /**Convierte un observable a una promesa que se resuelve en el siguiente onNext del observable, esto es diferente a la función * @see Observable.toPromise() que se resueve hasta que el observable es completado. * * Si el observable devuelve un elemento de inmediato, la promesa devuelta se resuelve síncronamiente */ function nextToPromise(obs) { return new promise_1.SyncPromise((resolve, reject) => { let syncResult = null; let subscription = null; let sync = true; /**Llama ya sea a resolve o reject */ const commit = (ev) => { switch (ev.type) { case "resolve": return resolve(ev.value); case "reject": return reject(ev.error); default: assertUnreachable(ev); } }; const onEvent = (ev) => { if (subscription) { subscription.unsubscribe(); commit(ev); return; } syncResult = ev; }; subscription = obs .pipe(rxOps.take(1)) .subscribe(x => onEvent({ type: "resolve", value: x }), x => onEvent({ type: "reject", error: x })); sync = false; if (syncResult) { subscription.unsubscribe(); commit(syncResult); } }); } exports.nextToPromise = nextToPromise; /**Determina si un numero esta afuera del rango, y devuelve de que parte del rango esta afuera */ function outOfRange(value, range) { if (range.min && ((range.min.type == "in" && value < range.min.value) || (range.min.type == "ex" && value <= range.min.value))) return "min"; else if (range.max && ((range.max.type == "in" && value > range.max.value) || (range.max.type == "ex" && value >= range.max.value))) return "max"; return null; } exports.outOfRange = outOfRange; /**Barajea un arreglo */ function shuffle(arr) { function shuffleInPlace(a) { let j, x, i; for (i =