collection/mapFind.ts

/** @module Collection */

import { isColl } from './isColl'
import { isObj } from '@object/isObj'
import { isFunc } from '@method/isFunc'
import { exists } from '@ext/exists'
import { validate } from '@validation/validate'

/**
 * Helper for mapFind, handling the array case
 * @private
 * @param {Array} arr
 * @param {Function} mapper
 * @param {Function} testFunc
 * @returns {*}
 */
const mapFindArr = (
  arr: any[],
  mapper: (...params: any[]) => any,
  testFunc: (...params: any[]) => any = exists
) => {
  // iterate over each value in the array,
  // returning when a mapped value is found that passes `testFunc`
  for (let i = 0; i < arr.length; i++) {
    const mappedValue = mapper(arr[i], i, i)
    if (testFunc(mappedValue, i, i)) return mappedValue
  }

  return null
}

/**
 * Helper for mapFind, handling the object case
 * @private
 * @param {Object} obj
 * @param {Function} mapper
 * @param {Function} testFunc
 * @returns {*}
 */
const mapFindObj = (
  obj: Record<any, any>,
  mapper: (...params: any[]) => any,
  testFunc: (...params: any[]) => any = exists
): any => {
  let idx = 0

  // iterate over each property in the object
  // returning when a mapped value is found that passes `testFunc`
  for (let key in obj) {
    if (!obj.hasOwnProperty(key)) continue

    const value = obj[key]
    const mappedValue = mapper(value, key, idx)
    if (testFunc(mappedValue, key, idx)) return mappedValue

    idx++
  }

  return null
}

/**
 * Finds the first element in coll whose mapped value passes the testFunc function, then returns
 * the **mapped** value.
 * It will not map the entire array or object; only the subset needed to find the first passing element.
 * @function
 * @param {Array|Object} coll - Elements to map and find
 * @param {Function} mapper - Mapping function of form: (value, key, idx) -> *. "key" is the index when coll is an array. "idx" is the index of the array value or object entry.
 * @param {Function?} testFunc - Predicate function of form: (mappedValue, key, idx) -> true/false. Defaults to checking if the mapped value is defined. "key" is the index when coll is an array.
 * @returns {*} - The first passing mapped value
 *
 * @example
 * // Find the first file path that can be required from disk
 * const filePaths = [...]
 * const loadedFile = mapFind(filePaths, tryRequireSync)
 *
 * @example
 * // Find the first file path whose required value is an object
 * const filePaths = [...]
 * const loadedFile = mapFind(filePaths, tryRequireSync, isObj)
 *
 * @example
 * // Find the first file path whose required value is an object
 * const filePaths = { document: "foo/bar/doc.txt", image: "foo/bar/pic.img"}
 * const loadedFile = mapFind(filePaths, (value, key) => tryRequireSync(value), isObj)
 */
export const mapFind = (
  coll: Record<any, any> | any[],
  mapper: (...params: any[]) => any,
  testFunc: (...params: any[]) => any = exists
): any => {
  const [valid] = validate(
    { coll, mapper, testFunc },
    { coll: isColl, $default: isFunc }
  )
  if (!valid) return undefined

  return isObj(coll)
    ? mapFindObj(coll, mapper, testFunc)
    : mapFindArr(coll as any[], mapper, testFunc)
}