mergeForEach.ts

import * as _ from 'lodash'
import { getterFromIteratee } from './common'
import { SORTING_ORDER } from './constants'
import { ValueOf } from './types'

export default mergeForEach

export type ComparisonResult = number

/**
 * Divide-and-conquer-based for-each function where a different iteratee is
 * called for items in both collections, items in the left one but not in the
 * right one and items in the right one but not in the left one.
 *
 * Can be used to implement efficient algorithms which profit from sorted data
 * like `mergeSort` or `mergeJoinWith`.
 *
 * Both collections must be sortable. They will be sorted ascendantly 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.innerCallback Callback called when there are two matching
 * elements. Boths elements are passed as arguments.
 * @param options.leftCallback 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.
 * @param options.rightCallback 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.
 * @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 mergeForEach<
  L extends any,
  R 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,
    innerCallback = () => {},
    leftCallback = () => {},
    rightCallback = () => {},
    comparator = function ({ lhsItem, rhsItem, getLHSValue, getRHSValue }) {
      const lhsValue = getLHSValue(lhsItem) as any
      const rhsValue = getRHSValue(rhsItem) as any

      if (lhsValue === rhsValue) return SORTING_ORDER.EQUAL
      if (lhsValue < rhsValue) return SORTING_ORDER.LHS_BEFORE_RHS
      if (lhsValue > rhsValue) return SORTING_ORDER.LHS_AFTER_RHS

      throw new Error(`[@geoblink/lodash-mixins#mergeForEach] Found non-comparable values: ${lhsValue} on LHS and ${rhsValue} on RHS`)
    }
  }: {
    lhsIteratee?: LHSItemKey | ((item: LHSItem) => any),
    rhsIteratee?: RHSItemKey | ((item: RHSItem) => any),
    innerCallback?: (lhsItem: LHSItem, rhsItem: RHSItem) => void,
    leftCallback?: (lhsItem: LHSItem) => void,
    rightCallback?: (rhsItem: RHSItem) => void,
    comparator?: (params: {
      lhsItem: LHSItem,
      rhsItem: RHSItem,
      getLHSValue: (lhsItem: LHSItem) => any,
      getRHSValue: (rhsItem: RHSItem) => any
    }) => ComparisonResult
  }
) {
  const sortedLHS = _.sortBy(lhs as any, lhsIteratee)
  const sortedRHS = _.sortBy(rhs as any, rhsIteratee)

  let lhsIndex = 0
  let rhsIndex = 0

  const getLHSValue = getterFromIteratee<LHSItem, any>(lhsIteratee)
  const getRHSValue = getterFromIteratee<RHSItem, any>(rhsIteratee)

  while (lhsIndex < sortedLHS.length && rhsIndex < sortedRHS.length) {
    const lhsItem = sortedLHS[lhsIndex] as LHSItem
    const rhsItem = sortedRHS[rhsIndex] as RHSItem

    const comparisonResult = comparator({
      lhsItem,
      rhsItem,
      getLHSValue,
      getRHSValue
    })

    if (comparisonResult < SORTING_ORDER.EQUAL) {
      leftCallback(lhsItem)
      lhsIndex++
    } else if (comparisonResult > SORTING_ORDER.EQUAL) {
      rightCallback(rhsItem)
      rhsIndex++
    } else {
      innerCallback(lhsItem, rhsItem)
      lhsIndex++
      rhsIndex++
    }
  }

  while (lhsIndex < sortedLHS.length) {
    leftCallback(sortedLHS[lhsIndex] as LHSItem)
    lhsIndex++
  }

  while (rhsIndex < sortedRHS.length) {
    rightCallback(sortedRHS[rhsIndex] as RHSItem)
    rhsIndex++
  }
}

declare module 'lodash' {
  interface LoDashStatic {
    /**
     * Divide-and-conquer-based for-each function where a different iteratee is
     * called for items in both collections, items in the left one but not in
     * the right one and items in the right one but not in the left one.
     *
     * Can be used to implement efficient algorithms which profit from sorted
     * data like `mergeSort` or `mergeJoinWith`.
     *
     * Both collections must be sortable. They will be sorted ascendantly 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.innerCallback Callback called when there are two matching
     * elements. Boths elements are passed as arguments.
     * @param options.leftCallback 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.
     * @param options.rightCallback 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.
     * @param options.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`.
     */
    mergeForEach<
      L extends any,
      R extends any,
      LHSItem extends ValueOf<L>,
      RHSItem extends ValueOf<R>,
      LHSItemKey extends keyof LHSItem,
      RHSItemKey extends keyof RHSItem
    > (
      lhs: L | LHSItem[],
      rhs: R | RHSItem[],
      params: {
        lhsIteratee?: LHSItemKey | ((item: LHSItem) => any),
        rhsIteratee?: RHSItemKey | ((item: RHSItem) => any),
        innerCallback?: (lhsItem: LHSItem, rhsItem: RHSItem) => void,
        leftCallback?: (lhsItem: LHSItem) => void,
        rightCallback?: (rhsItem: RHSItem) => void,
        comparator?: (params: {
          lhsItem: LHSItem,
          rhsItem: RHSItem,
          getLHSValue: (lhsItem: LHSItem) => any,
          getRHSValue: (rhsItem: RHSItem) => any
        }) => ComparisonResult
      }
    ): void
  }

  interface LoDashImplicitWrapper<TValue> {
    /**
     * @see _.hasTruthyValues
     */
    mergeForEach<
      L extends any,
      R extends any,
      LHSItem extends ValueOf<L>,
      RHSItem extends ValueOf<R>,
      LHSItemKey extends keyof LHSItem,
      RHSItemKey extends keyof RHSItem
    >(
      this: LoDashImplicitWrapper<TValue | null | undefined>,
      lhs: L | LHSItem[],
      rhs: R | RHSItem[],
      params: {
        lhsIteratee?: LHSItemKey | ((item: LHSItem) => any),
        rhsIteratee?: RHSItemKey | ((item: RHSItem) => any),
        innerCallback?: (lhsItem: LHSItem, rhsItem: RHSItem) => void,
        leftCallback?: (lhsItem: LHSItem) => void,
        rightCallback?: (rhsItem: RHSItem) => void,
        comparator?: (params: {
          lhsItem: LHSItem,
          rhsItem: RHSItem,
          getLHSValue: (lhsItem: LHSItem) => any,
          getRHSValue: (rhsItem: RHSItem) => any
        }) => ComparisonResult
      }
    ): this
  }

  interface LoDashExplicitWrapper<TValue> {
    /**
     * @see _.hasTruthyValues
     */
    mergeForEach<
      L extends any,
      R extends any,
      LHSItem extends ValueOf<L>,
      RHSItem extends ValueOf<R>,
      LHSItemKey extends keyof LHSItem,
      RHSItemKey extends keyof RHSItem
    >(
      this: LoDashExplicitWrapper<TValue | null | undefined>,
      lhs: L | LHSItem[],
      rhs: R | RHSItem[],
      params: {
        lhsIteratee?: LHSItemKey | ((item: LHSItem) => any),
        rhsIteratee?: RHSItemKey | ((item: RHSItem) => any),
        innerCallback?: (lhsItem: LHSItem, rhsItem: RHSItem) => void,
        leftCallback?: (lhsItem: LHSItem) => void,
        rightCallback?: (rhsItem: RHSItem) => void,
        comparator?: (params: {
          lhsItem: LHSItem,
          rhsItem: RHSItem,
          getLHSValue: (lhsItem: LHSItem) => any,
          getRHSValue: (rhsItem: RHSItem) => any
        }) => ComparisonResult
      }
    ): this
  }
}