mergeJoinWith.ts

import { SORTING_ORDER } from './constants'
import { ValueOf } from './types'

import mergeForEach from './mergeForEach'

export default mergeJoinWith

export type ComparisonResult = number

/**
 * Divide-and-conquer-based join function to merge two arrays into a new one.
 *
 * Both collections must be sortable. They will be sorted ascendently using
 * value returned by the corresponding iteratee.
 *
 * @param lhs A collection of elements.
 * @param rhs A collection of elements.
 * @param options Options for comparison.
 * @param options.lhsIteratee Iteratee used to get the value used to sort `lhs`.
 * Returned value will be used to sort the collection before running the
 * divide-and-conquer algorithm.
 * @param options.rhsIteratee Iteratee used to get the value used to sort `rhs`.
 * Returned value will be used to sort the collection
 * before running the divide-and-conquer algorithm.
 * @param options.getInnerJoinedItem Callback called when there are two matching
 * elements. Boths elements are passed as arguments. Must return the resulting
 * element of merging both parameters.
 * @param options.getLeftJoinedItem Callback called when there are elements in
 * the left-hand-side collection which cannot be matched with any element of the
 * right-hand-side collection. Must return the element to be added to results array.
 * @param options.getRightJoinedItem Callback called when there are elements in
 * the right-hand-side collection which cannot be matched with any element of the
 * left-hand-side collection. Must return the element to be added to results array.
 * @param comparator Function used to compare an item of `lhs` collection against
 * an item of `rhs` collection. Negative values mean that `lhs` item is **before**
 * `rhs` item, positive values that `lhs` item is **after** `rhs` item and `0`
 * that both items are equivalent in terms of sorting. Default implementation is
 * equivalent to `<` operator. Will receive as 3rd and 4th parameters the
 * iteratees used to get sorting value for `lhs` and `rhs`.
 */
function mergeJoinWith<
  L extends any,
  R extends any,
  InnerJoinedItem extends any,
  LeftJoinedItem extends any,
  RightJoinedItem extends any,
  LHSItem extends ValueOf<L>,
  RHSItem extends ValueOf<R>,
  LHSItemKey extends keyof LHSItem,
  RHSItemKey extends keyof RHSItem
> (
  lhs: L | LHSItem[],
  rhs: R | RHSItem[],
  {
    lhsIteratee = (lhsItem) => lhsItem,
    rhsIteratee = (rhsItem) => rhsItem,
    getInnerJoinedItem,
    getLeftJoinedItem = (lhsItem) => lhsItem,
    getRightJoinedItem = (rhsItem) => rhsItem,
    comparator = function ({ lhsItem, rhsItem, getLHSValue, getRHSValue }) {
      const lhsValue = getLHSValue(lhsItem) as any
      const rhsValue = getRHSValue(rhsItem) as any

      if (lhsValue < rhsValue) {
        return SORTING_ORDER.LHS_BEFORE_RHS
      } else if (lhsValue > rhsValue) {
        return SORTING_ORDER.LHS_AFTER_RHS
      } else {
        return SORTING_ORDER.EQUAL
      }
    }
  }: {
    lhsIteratee?: LHSItemKey | ((item: LHSItem) => any),
    rhsIteratee?: RHSItemKey | ((item: RHSItem) => any),
    getInnerJoinedItem: (lhsItem: LHSItem, rhsItem: RHSItem) => InnerJoinedItem,
    getLeftJoinedItem?: (lhsItem: LHSItem) => LeftJoinedItem,
    getRightJoinedItem?: (rhsItem: RHSItem) => RightJoinedItem,
    comparator?: (params: {
      lhsItem: LHSItem,
      rhsItem: RHSItem,
      getLHSValue: (lhsItem: LHSItem) => any,
      getRHSValue: (rhsItem: RHSItem) => any
    }) => ComparisonResult
  }
) {
  const result: (InnerJoinedItem|LeftJoinedItem|RightJoinedItem)[] = []

  mergeForEach(lhs, rhs, {
    lhsIteratee,
    rhsIteratee,
    comparator,
    innerCallback (lhsItem, rhsItem) {
      const joinedItem = getInnerJoinedItem(lhsItem, rhsItem)
      result.push(joinedItem)
    },
    leftCallback (lhsItem) {
      const joinedItem = getLeftJoinedItem(lhsItem)
      result.push(joinedItem)
    },
    rightCallback (rhsItem) {
      const joinedItem = getRightJoinedItem(rhsItem)
      result.push(joinedItem)
    }
  })

  return result
}