Skip to content

Commit

Permalink
Merge f3b0b97 into d64044a
Browse files Browse the repository at this point in the history
  • Loading branch information
Sumolari committed Apr 26, 2019
2 parents d64044a + f3b0b97 commit d48ae4a
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 11 deletions.
10 changes: 10 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import _ from 'lodash'

export function getterFromIteratee<
Item extends any,
ItemKey extends keyof Item
> (iteratee: ItemKey | ((item: Item) => any)): (item: Item) => any {
return _.isFunction(iteratee)
? iteratee
: ((item: Item) => _.get(item, iteratee as ItemKey))
}
2 changes: 1 addition & 1 deletion src/hasTruthyValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function hasTruthyValues<Collection extends object> (collection: Collection): bo
return shortcuttedReduce(
collection,
(accum, value) => accum || !!value,
false
false as boolean
)
}

Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import getTruthyKeys from './getTruthyKeys'
import hasTruthyValues from './hasTruthyValues'
import mapNonNil from './mapNonNil'
import mergeForEach from './mergeForEach'
import mergeJoinWith from './mergeJoinWith'
import mGet from './mGet'
import shortcuttedReduce from './shortcuttedReduce'
import { LoDashStatic } from 'lodash'
Expand All @@ -21,6 +22,7 @@ export default function (_: LoDashStatic) {
hasTruthyValues: hasTruthyValues,
mapNonNil: mapNonNil,
mergeForEach: mergeForEach,
mergeJoinWith: mergeJoinWith,
mGet: mGet,
shortcuttedReduce: shortcuttedReduce
})
Expand Down
10 changes: 1 addition & 9 deletions src/mergeForEach.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import _ from 'lodash'
import { getterFromIteratee } from './common'
import { SORTING_ORDER } from './constants'
import { ValueOf } from './types'

Expand Down Expand Up @@ -125,15 +126,6 @@ function mergeForEach<
rightCallback(sortedRHS[rhsIndex] as RHSItem)
rhsIndex++
}

function getterFromIteratee<
Item extends any,
ItemKey extends keyof Item
> (iteratee: ItemKey | ((item: Item) => any)): (item: Item) => any {
return _.isFunction(iteratee)
? iteratee
: ((item: Item) => _.get(item, iteratee as ItemKey))
}
}

declare module 'lodash' {
Expand Down
107 changes: 107 additions & 0 deletions src/mergeJoinWith.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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
}
1 change: 1 addition & 0 deletions test/exportedMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default [
'hasTruthyValues',
'mapNonNil',
'mergeForEach',
'mergeJoinWith',
'mGet',
'shortcuttedReduce'
]
2 changes: 1 addition & 1 deletion test/unit/mergeForEach.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ describe('mergeForEach', function () {
})
})

it('Should call innerCallback for non-matching values in both collections', function () {
it('Should call innerCallback for matching values in both collections', function () {
mergeForEach(lhs, rhs, {
lhsIteratee,
rhsIteratee,
Expand Down
212 changes: 212 additions & 0 deletions test/unit/mergeJoinWith.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import _ from 'lodash'
import mergeJoinWith, { ComparisonResult } from '../../src/mergeJoinWith'
import { ValueOf } from '../../src/types'
import { SORTING_ORDER } from '../../src/constants'
import { expect, use as chaiUse } from 'chai'
import sinonChai from 'sinon-chai'
import * as sinon from 'sinon'
chaiUse(sinonChai)

describe('mergeJoinWith', function () {
const lhsBarcelona = { id: 0, name: 'Barcelona' }
const lhsMadrid = { id: 1, name: 'Madrid' }

const rhsMadrid = { id_city: 1, city_name: 'Madrid' }
const rhsValencia = { id_city: 2, city_name: 'Valencia' }
const rhsCityWithoutId = { city_name: 'No Id City' }

const getLeftJoinedItem = sinon
.stub()
.callsFake((item) => _.assign({ mocked: true }, item))
const getRightJoinedItem = sinon
.stub()
.callsFake((item) => _.assign({ mocked: true }, item))
const getInnerJoinedItem = sinon
.stub()
.callsFake((lhsItem, rhsItem) => _.assign({ mocked: true }, lhsItem, rhsItem))

beforeEach('Reset stubs', function () {
getLeftJoinedItem.resetHistory()
getRightJoinedItem.resetHistory()
getInnerJoinedItem.resetHistory()
})

describe('When collections are sorted', function () {
buildTestCases({
lhs: [lhsBarcelona, lhsMadrid],
rhs: [rhsMadrid, rhsValencia, rhsCityWithoutId],
lhsIteratee: 'id',
rhsIteratee: 'id_city',
getLeftJoinedItem,
getRightJoinedItem,
getInnerJoinedItem
})
})

describe('When collections are NOT sorted', function () {
buildTestCases({
lhs: [lhsMadrid, lhsBarcelona],
rhs: [rhsValencia, rhsMadrid, rhsCityWithoutId],
lhsIteratee: 'id',
rhsIteratee: 'id_city',
getLeftJoinedItem,
getRightJoinedItem,
getInnerJoinedItem
})
})

describe('When using functions as iteratees', function () {
buildTestCases({
lhs: [lhsMadrid, lhsBarcelona],
rhs: [rhsValencia, rhsMadrid, rhsCityWithoutId],
lhsIteratee (lhsItem) { return lhsItem.id },
rhsIteratee (rhsItem) { return rhsItem.id_city },
getLeftJoinedItem,
getRightJoinedItem,
getInnerJoinedItem
})
})

describe('When using a custom comparison function', function () {
buildTestCases({
lhs: [lhsMadrid, lhsBarcelona],
rhs: [rhsValencia, rhsMadrid, rhsCityWithoutId],
lhsIteratee: 'id',
rhsIteratee: 'id_city',
comparator ({ lhsItem, rhsItem, getLHSValue, getRHSValue }) {
const lhsValue = getLHSValue(lhsItem)
const rhsValue = getRHSValue(rhsItem)

if (lhsValue < rhsValue) {
return SORTING_ORDER.LHS_BEFORE_RHS
} else if (rhsValue > lhsValue) {
return SORTING_ORDER.LHS_AFTER_RHS
} else {
return SORTING_ORDER.EQUAL
}
},
getLeftJoinedItem,
getRightJoinedItem,
getInnerJoinedItem
})
})

function buildTestCases<
L extends any,
R extends any,
LHSItem extends ValueOf<L>,
RHSItem extends ValueOf<R>,
LHSItemKey extends keyof LHSItem,
RHSItemKey extends keyof RHSItem
> ({
lhs, rhs,
lhsIteratee, rhsIteratee, comparator,
getLeftJoinedItem, getInnerJoinedItem, getRightJoinedItem
}: {
lhs: L,
rhs: R,
lhsIteratee: LHSItemKey | ((item: LHSItem) => any),
rhsIteratee: RHSItemKey | ((item: RHSItem) => any),
getInnerJoinedItem: (lhsItem: LHSItem, rhsItem: RHSItem) => LHSItem & RHSItem,
getLeftJoinedItem?: (lhsItem: LHSItem) => LHSItem,
getRightJoinedItem?: (rhsItem: RHSItem) => RHSItem,
comparator?: (params: {
lhsItem: LHSItem,
rhsItem: RHSItem,
getLHSValue: (lhsItem: LHSItem) => any,
getRHSValue: (rhsItem: RHSItem) => any
}) => ComparisonResult
}) {
it('Should call getLeftJoinedItem for non-matching values in lhs', function () {
const result = mergeJoinWith(lhs, rhs, {
lhsIteratee,
rhsIteratee,
comparator,
getInnerJoinedItem,
getLeftJoinedItem,
getRightJoinedItem
})

expect(getLeftJoinedItem).to.have.been.calledWithExactly(lhsBarcelona)
expect(result).to.be.deep.equal([
_.assign({ mocked: true }, lhsBarcelona),
_.assign({ mocked: true }, lhsMadrid, rhsMadrid),
_.assign({ mocked: true }, rhsValencia),
_.assign({ mocked: true }, rhsCityWithoutId)
])
})

it('Should not call getLeftJoinedItem if not provided', function () {
const result = mergeJoinWith(lhs, rhs, {
lhsIteratee,
rhsIteratee,
comparator,
getInnerJoinedItem,
getRightJoinedItem
})

expect(result).to.be.deep.equal([
lhsBarcelona,
_.assign({ mocked: true }, lhsMadrid, rhsMadrid),
_.assign({ mocked: true }, rhsValencia),
_.assign({ mocked: true }, rhsCityWithoutId)
])
})

it('Should call getRightJoinedItem for non-matching values in rhs', function () {
const result = mergeJoinWith(lhs, rhs, {
lhsIteratee,
rhsIteratee,
comparator,
getInnerJoinedItem,
getLeftJoinedItem,
getRightJoinedItem
})

expect(getRightJoinedItem).to.have.been.calledWithExactly(rhsValencia)
expect(getRightJoinedItem).to.have.been.calledWithExactly(rhsCityWithoutId)
expect(result).to.be.deep.equal([
_.assign({ mocked: true }, lhsBarcelona),
_.assign({ mocked: true }, lhsMadrid, rhsMadrid),
_.assign({ mocked: true }, rhsValencia),
_.assign({ mocked: true }, rhsCityWithoutId)
])
})

it('Should not call getRightJoinedItem if not provided', function () {
const result = mergeJoinWith(lhs, rhs, {
lhsIteratee,
rhsIteratee,
comparator,
getInnerJoinedItem,
getLeftJoinedItem
})

expect(result).to.be.deep.equal([
_.assign({ mocked: true }, lhsBarcelona),
_.assign({ mocked: true }, lhsMadrid, rhsMadrid),
rhsValencia,
rhsCityWithoutId
])
})

it('Should call getInnerJoinedItem for matching values in both collections', function () {
const result = mergeJoinWith(lhs, rhs, {
lhsIteratee,
rhsIteratee,
comparator,
getInnerJoinedItem,
getLeftJoinedItem,
getRightJoinedItem
})

expect(getInnerJoinedItem).to.have.been.calledWithExactly(lhsMadrid, rhsMadrid)
expect(result).to.be.deep.equal([
_.assign({ mocked: true }, lhsBarcelona),
_.assign({ mocked: true }, lhsMadrid, rhsMadrid),
_.assign({ mocked: true }, rhsValencia),
_.assign({ mocked: true }, rhsCityWithoutId)
])
})
}
})

0 comments on commit d48ae4a

Please sign in to comment.