-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
335 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ export default [ | |
'hasTruthyValues', | ||
'mapNonNil', | ||
'mergeForEach', | ||
'mergeJoinWith', | ||
'mGet', | ||
'shortcuttedReduce' | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
]) | ||
}) | ||
} | ||
}) |