feat(intersectionBy): Add intersectionBy to compatibility layer (#721)
Some checks are pending
CI / codecov (push) Waiting to run
Release / release (push) Waiting to run

* feat(intersectionBy): Add `intersectionBy` to compatibility layer

* test(intersectionBy): Add test case for array-like objects
This commit is contained in:
Dongho Kim 2024-10-22 22:40:31 +09:00 committed by GitHub
parent 4a8ea44edd
commit 4a1e410dd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 457 additions and 0 deletions

View File

@ -1,8 +1,10 @@
import { bench, describe } from 'vitest';
import { intersectionBy as intersectionByToolkit_ } from 'es-toolkit';
import { intersectionBy as intersectionByCompatToolkit_ } from 'es-toolkit/compat';
import { intersectionBy as intersectionByLodash_ } from 'lodash';
const intersectionByToolkit = intersectionByToolkit_;
const intersectionByCompatToolkit = intersectionByCompatToolkit_;
const intersectionByLodash = intersectionByLodash_;
describe('intersectionBy', () => {
@ -13,6 +15,13 @@ describe('intersectionBy', () => {
intersectionByToolkit(array1, array2, mapper);
});
bench('es-toolkit/compat/intersectionBy', () => {
const array1 = [{ id: 1 }, { id: 2 }, { id: 3 }];
const array2 = [{ id: 2 }, { id: 4 }];
const mapper = item => item.id;
intersectionByCompatToolkit(array1, array2, mapper);
});
bench('lodash/intersectionBy', () => {
const array1 = [{ id: 1 }, { id: 2 }, { id: 3 }];
const array2 = [{ id: 2 }, { id: 4 }];
@ -30,6 +39,10 @@ describe('intersectionBy/largeArrays', () => {
intersectionByToolkit(array1, array2, mapper);
});
bench('es-toolkit/compat/intersectionBy', () => {
intersectionByCompatToolkit(array1, array2, mapper);
});
bench('lodash/intersectionBy', () => {
intersectionByLodash(array1, array2, mapper);
});

View File

@ -31,3 +31,30 @@ const mapper = item => item.id;
const result = intersectionBy(array1, array2, mapper);
// `mapper`で変換したとき、両方の配列に含まれる要素からなる [{ id: 2 }] 値が返されます。
```
## Lodashとの互換性
`es-toolkit/compat`から`intersectionBy`をインポートすると、lodashと互換性があります。
- `intersectionBy`は共通要素を見つけるために複数の配列風オブジェクト(Array-like object)を受け入れます。
- `intersectionBy`はプロパティキーをiterateeとして受け入れます。
```typescript
import { intersectionBy } from 'es-toolkit/compat';
const array1 = [1.2, 2.4, 3.6];
const array2 = [2.5, 3.7];
const array3 = [2.6, 3.8];
const result = intersectionBy(array1, array2, array3, Math.floor);
// 結果は [2.4, 3.6] です。Math.floorを適用した後、共通要素は2と3です。
const array1 = [{ x: 1 }, { x: 2 }, { x: 3 }];
const array2 = [{ x: 2 }, { x: 3 }, { x: 4 }];
const result = intersectionBy(array1, array2, 'x');
// 結果は [{ x: 2 }, { x: 3 }] です。これらの要素は同じ`x`プロパティを持っています。
const arrayLike1 = { 0: 'apple', 1: 'banana', 2: 'cherry', length: 3 };
const arrayLike2 = { 0: 'banana', 1: 'cherry', 2: 'date', length: 3 };
const result = intersectionBy(arrayLike1, arrayLike2);
// 結果は ['banana', 'cherry'] です。これらの要素は両方の配列風オブジェクトに共通しています。
```

View File

@ -31,3 +31,30 @@ const mapper = item => item.id;
const result = intersectionBy(array1, array2, mapper);
// `mapper`로 변환했을 때 두 배열 모두에 포함되는 요소로 이루어진 [{ id: 2 }] 값이 반환되어요.
```
## Lodash와의 호환성
`es-toolkit/compat`에서 `intersectionBy`를 가져오면 lodash와 호환돼요.
- `intersectionBy`는 공통 요소를 찾기 위해 여러 개의 유사 배열 객체를 받을 수 있어요.
- `intersectionBy`는 속성 키를 iteratee로 받을 수 있어요.
```typescript
import { intersectionBy } from 'es-toolkit/compat';
const array1 = [1.2, 2.4, 3.6];
const array2 = [2.5, 3.7];
const array3 = [2.6, 3.8];
const result = intersectionBy(array1, array2, array3, Math.floor);
// 결과는 [2.4, 3.6]이에요. Math.floor를 적용한 후 공통 요소는 2와 3이에요.
const array1 = [{ x: 1 }, { x: 2 }, { x: 3 }];
const array2 = [{ x: 2 }, { x: 3 }, { x: 4 }];
const result = intersectionBy(array1, array2, 'x');
// 결과는 [{ x: 2 }, { x: 3 }]이에요. 이 요소들은 동일한 `x` 속성을 가지고 있어요.
const arrayLike1 = { 0: 'apple', 1: 'banana', 2: 'cherry', length: 3 };
const arrayLike2 = { 0: 'banana', 1: 'cherry', 2: 'date', length: 3 };
const result = intersectionBy(arrayLike1, arrayLike2);
// 결과는 ['banana', 'cherry']예요. 이 요소들은 두 유사 배열 객체에서 공통으로 존재해요.
```

View File

@ -32,3 +32,30 @@ const mapper = item => item.id;
const result = intersectionBy(array1, array2, mapper);
// result will be [{ id: 2 }] since only this element has a matching id in both arrays.
```
## Lodash Compatibility
Import `intersectionBy` from `es-toolkit/compat` for full compatibility with lodash.
- `intersectionBy` can accept multiple array-like objects to find common elements.
- `intersectionBy` can accept a property key as the iteratee.
```typescript
import { intersectionBy } from 'es-toolkit/compat';
const array1 = [1.2, 2.4, 3.6];
const array2 = [2.5, 3.7];
const array3 = [2.6, 3.8];
const result = intersectionBy(array1, array2, array3, Math.floor);
// result will be [2.4, 3.6] since the common elements after applying Math.floor are 2 and 3.
const array1 = [{ x: 1 }, { x: 2 }, { x: 3 }];
const array2 = [{ x: 2 }, { x: 3 }, { x: 4 }];
const result = intersectionBy(array1, array2, 'x');
// result will be [{ x: 2 }, { x: 3 }] since these elements have the same `x` property.
const arrayLike1 = { 0: 'apple', 1: 'banana', 2: 'cherry', length: 3 };
const arrayLike2 = { 0: 'banana', 1: 'cherry', 2: 'date', length: 3 };
const result = intersectionBy(arrayLike1, arrayLike2);
// result will be ['banana', 'cherry'] since these elements are common between the two array-like objects.
```

View File

@ -31,3 +31,30 @@ const mapper = item => item.id;
const result = intersectionBy(array1, array2, mapper);
// 结果将是 [{ id: 2 }] 因为只有这个元素在两个数组中具有匹配的 id。
```
## Lodash 兼容性
`es-toolkit/compat` 导入 `intersectionBy` 以获得与 lodash 的完全兼容性。
- `intersectionBy` 可以接受多个类数组对象来查找公共元素。
- `intersectionBy` 可以将属性键作为迭代器使用。
```typescript
import { intersectionBy } from 'es-toolkit/compat';
const array1 = [1.2, 2.4, 3.6];
const array2 = [2.5, 3.7];
const array3 = [2.6, 3.8];
const result = intersectionBy(array1, array2, array3, Math.floor);
// 结果是 [2.4, 3.6],因为在应用 Math.floor 之后公共元素是2和3。
const array1 = [{ x: 1 }, { x: 2 }, { x: 3 }];
const array2 = [{ x: 2 }, { x: 3 }, { x: 4 }];
const result = intersectionBy(array1, array2, 'x');
// 结果是 [{ x: 2 }, { x: 3 }],因为这些元素有相同的 `x` 属性。
const arrayLike1 = { 0: 'apple', 1: 'banana', 2: 'cherry', length: 3 };
const arrayLike2 = { 0: 'banana', 1: 'cherry', 2: 'date', length: 3 };
const result = intersectionBy(arrayLike1, arrayLike2);
// 结果是 ['banana', 'cherry'],因为这些元素在两个类数组对象中都存在。
```

View File

@ -0,0 +1,138 @@
import { describe, expect, it } from 'vitest';
import { intersection } from './intersection';
import { intersectionBy } from './intersectionBy';
import { range } from '../../math';
import { args } from '../_internal/args';
import { LARGE_ARRAY_SIZE } from '../_internal/LARGE_ARRAY_SIZE';
import { slice } from '../_internal/slice';
import { stubNaN } from '../_internal/stubNaN';
import { constant } from '../util/constant';
import { times } from '../util/times';
import { toString } from '../util/toString';
describe('intersectionBy', () => {
/**
* @see https://github.com/lodash/lodash/blob/afcd5bc1e8801867c31a17566e0e0edebb083d0e/test/intersection-methods.spec.js#L1
*/
it('should return the intersection of two arrays', () => {
const actual = intersection([2, 1], [2, 3]);
expect(actual).toEqual([2]);
});
it('should return the intersection of multiple arrays', () => {
const actual = intersection([2, 1, 2, 3], [3, 4], [3, 2]);
expect(actual).toEqual([3]);
});
it('should return an array of unique values', () => {
const actual = intersection([1, 1, 3, 2, 2], [5, 2, 2, 1, 4], [2, 1, 1]);
expect(actual).toEqual([1, 2]);
});
it('should work with a single array', () => {
const actual = intersection([1, 1, 3, 2, 2]);
expect(actual).toEqual([1, 3, 2]);
});
it('should work with `arguments` objects', () => {
const array = [0, 1, null, 3];
const expected = [1, 3];
expect(intersection(array, args)).toEqual(expected);
expect(intersection(args, array)).toEqual(expected);
});
it('should treat `-0` as `0`', () => {
const values = [-0, 0];
const expected = values.map(constant(['0']));
const actual = values.map(value => intersection(values, [value]).map(toString));
expect(actual).toEqual(expected);
});
it('should match `NaN`', () => {
const actual = intersection([1, NaN, 3], [NaN, 5, NaN]);
expect(actual).toEqual([NaN]);
});
it('should work with large arrays of `-0` as `0`', () => {
const values = [-0, 0];
const expected = values.map(constant(['0']));
const actual = values.map(value => {
const largeArray = times(LARGE_ARRAY_SIZE, constant(value));
return intersection(values, largeArray).map(toString);
});
expect(actual).toEqual(expected);
});
it('should work with large arrays of `NaN`', () => {
const largeArray = times(LARGE_ARRAY_SIZE, stubNaN);
expect(intersection([1, NaN, 3], largeArray)).toEqual([NaN]);
});
it('should work with large arrays of objects', () => {
const object = {};
const largeArray = times(LARGE_ARRAY_SIZE, constant(object));
expect(intersection([object], largeArray)).toEqual([object]);
expect(intersection(range(LARGE_ARRAY_SIZE), [1])).toEqual([1]);
});
it('should treat values that are not arrays or `arguments` objects as empty', () => {
const array = [0, 1, null, 3];
// @ts-ignore
expect(intersection(array, 3, { 0: 1 }, null)).toEqual([]);
expect(intersection(null, array, null, [2, 3])).toEqual([]);
expect(intersection(array, null, args, null)).toEqual([]);
});
/**
* @see https://github.com/lodash/lodash/blob/afcd5bc1e8801867c31a17566e0e0edebb083d0e/test/intersectionBy.spec.js#L1
*/
it('should accept an `iteratee`', () => {
const actual1 = intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor);
expect(actual1).toEqual([2.1]);
const actual2 = intersectionBy([{ x: 1 }], [{ x: 2 }, { x: 1 }], 'x');
expect(actual2).toEqual([{ x: 1 }]);
const actual3 = intersectionBy([2.1, 1.2], [2.3, 3.4], [1.2, 2.4], Math.floor);
expect(actual3).toEqual([2.1]);
const actual4 = intersectionBy([1], [1], [1], [1], [1], [1], [1]);
expect(actual4).toEqual([1]);
});
it('should provide correct `iteratee` arguments', () => {
let args: number[] | undefined;
intersectionBy([2.1, 1.2], [2.3, 3.4], function () {
args || (args = slice.call(arguments));
});
expect(args).toEqual([2.3]);
});
it('should return empty array if no arrays provided', () => {
expect(intersectionBy(null)).toEqual([]);
expect(intersectionBy(undefined)).toEqual([]);
});
it('should return as it is if only one array provided', () => {
expect(intersectionBy([1, 2, 3])).toEqual([1, 2, 3]);
});
it('should return empty array if non array-like object is provided in the middle', () => {
expect(intersectionBy([1, 2, 3], '123', [1, 2])).toEqual([]);
});
it('should support array-like object', () => {
expect(intersectionBy({ 0: 'a', 1: 'b', 2: 'c', length: 3 }, { 0: 'b', 1: 'c', length: 2 })).toEqual(['b', 'c']);
expect(intersectionBy({ 0: 1.1, 1: 2.2, 2: 3.3, length: 3 }, { 0: 1.7, 1: 2.7, length: 2 }, Math.floor)).toEqual([
1.1, 2.2,
]);
});
});

View File

@ -0,0 +1,197 @@
import { intersectionBy as intersectionByToolkit } from '../../array/intersectionBy.ts';
import { last } from '../../array/last.ts';
import { uniq } from '../../array/uniq.ts';
import { identity } from '../_internal/identity.ts';
import { property } from '../object/property.ts';
import { isArrayLikeObject } from '../predicate/isArrayLikeObject.ts';
/**
* Returns the intersection of multiple arrays after applying the iteratee function to their elements.
*
* This function takes multiple arrays and an iteratee function (or property key) to
* compare the elements after transforming them. It returns a new array containing the elements from
* the first array that are present in all subsequent arrays after applying the iteratee to each element.
*
* @template T1, T2
* @param {ArrayLike<T1> | null | undefined} array - The first array to compare.
* @param {ArrayLike<T2>} values - The second array to compare.
* @param {(value: T1 | T2) => unknown | string} iteratee - The iteratee invoked on each element
* for comparison. It can also be a property key to compare based on that property.
* @returns {T1[]} A new array containing the elements from the first array that are present
* in all subsequent arrays after applying the iteratee.
*
* @example
* const array1 = [{ x: 1 }, { x: 2 }, { x: 3 }];
* const array2 = [{ x: 2 }, { x: 3 }, { x: 4 }];
* const result = intersectionBy(array1, array2, 'x');
* // result will be [{ x: 2 }, { x: 3 }] since these elements have the same `x` property.
*
* const array1 = [{ x: 1 }, { x: 2 }, { x: 3 }];
* const array2 = [{ x: 2 }, { x: 3 }, { x: 4 }];
* const result = intersectionBy(array1, array2, value => value.x);
* // result will be [{ x: 2 }, { x: 3 }] since these elements have the same `x` property.
*/
export function intersectionBy<T1, T2>(
array: ArrayLike<T1> | null | undefined,
values: ArrayLike<T2>,
iteratee: ((value: T1 | T2) => unknown) | string
): T1[];
/**
* Returns the intersection of multiple arrays after applying the iteratee function to their elements.
*
* This function takes multiple arrays and an iteratee function (or property key) to
* compare the elements after transforming them. It returns a new array containing the elements from
* the first array that are present in all subsequent arrays after applying the iteratee to each element.
*
* @template T1, T2, T3
* @param {ArrayLike<T1> | null | undefined} array - The first array to compare.
* @param {ArrayLike<T2>} values1 - The second array to compare.
* @param {ArrayLike<T3>} values2 - The third array to compare.
* @param {(value: T1 | T2 | T3) => unknown | string} iteratee - The iteratee invoked on each element
* for comparison. It can also be a property key to compare based on that property.
* @returns {T1[]} A new array containing the elements from the first array that are present
* in all subsequent arrays after applying the iteratee.
*
* @example
* const array1 = [{ x: 1 }, { x: 2 }, { x: 3 }];
* const array2 = [{ x: 2 }, { x: 3 }];
* const array3 = [{ x: 3 }];
* const result = intersectionBy(array1, array2, array3, 'x');
* // result will be [{ x: 3 }] since this element has the same `x` property in all arrays.
*
* const array1 = [{ x: 1 }, { x: 2 }, { x: 3 }];
* const array2 = [{ x: 2 }, { x: 3 }];
* const array3 = [{ x: 3 }];
* const result = intersectionBy(array1, array2, array3, value => value.x);
* // result will be [{ x: 3 }] since this element has the same `x` property in all arrays.
*/
export function intersectionBy<T1, T2, T3>(
array: ArrayLike<T1> | null | undefined,
values1: ArrayLike<T2>,
values2: ArrayLike<T3>,
iteratee: ((value: T1 | T2 | T3) => unknown) | string
): T1[];
/**
* Returns the intersection of multiple arrays after applying the iteratee function to their elements.
*
* This function takes multiple arrays and an iteratee function (or property key) to
* compare the elements after transforming them. It returns a new array containing the elements from
* the first array that are present in all subsequent arrays after applying the iteratee to each element.
*
* @template T1, T2, T3, T4
* @param {ArrayLike<T1> | null | undefined} array - The first array to compare.
* @param {ArrayLike<T2>} values1 - The second array to compare.
* @param {ArrayLike<T3>} values2 - The third array to compare.
* @param {...(ArrayLike<T4> | ((value: T1 | T2 | T3 | T4) => unknown) | string)} values - Additional arrays to compare, or the iteratee function.
* @returns {T1[]} A new array containing the elements from the first array that are present
* in all subsequent arrays after applying the iteratee.
*
* @example
* const array1 = [{ x: 1 }, { x: 2 }, { x: 3 }];
* const array2 = [{ x: 2 }, { x: 3 }];
* const array3 = [{ x: 3 }];
* const array4 = [{ x: 3 }, { x: 4 }];
* const result = intersectionBy(array1, array2, array3, array4, 'x');
* // result will be [{ x: 3 }] since this element has the same `x` property in all arrays.
*
* const array1 = [{ x: 1 }, { x: 2 }, { x: 3 }];
* const array2 = [{ x: 2 }, { x: 3 }];
* const array3 = [{ x: 3 }];
* const array4 = [{ x: 3 }, { x: 4 }];
* const result = intersectionBy(array1, array2, array3, array4, value => value.x);
* // result will be [{ x: 3 }] since this element has the same `x` property in all arrays.
*/
export function intersectionBy<T1, T2, T3, T4>(
array: ArrayLike<T1> | null | undefined,
values1: ArrayLike<T2>,
values2: ArrayLike<T3>,
...values: Array<ArrayLike<T4> | ((value: T1 | T2 | T3 | T4) => unknown) | string>
): T1[];
/**
* Returns the intersection of multiple arrays after applying the iteratee function to their elements.
*
* This function takes multiple arrays and an iteratee function (or property key) to
* compare the elements after transforming them. It returns a new array containing the elements from
* the first array that are present in all subsequent arrays after applying the iteratee to each element.
*
* @template T
* @param {ArrayLike<T> | null | undefined} [array] - The first array to compare.
* @param {...ArrayLike<T>} values - Additional arrays to compare.
* @returns {T[]} A new array containing the elements from the first array that are present
* in all subsequent arrays after applying the iteratee.
*
* @example
* const array1 = [1, 2, 3];
* const array2 = [2, 3];
* const array3 = [3];
* const result = intersectionBy(array1, array2, array3);
* // result will be [3] since these all elements have the same value 3.
*/
export function intersectionBy<T>(array?: ArrayLike<T> | null | undefined, ...values: Array<ArrayLike<T>>): T[];
/**
* Returns the intersection of multiple arrays after applying the iteratee function to their elements.
*
* This function takes multiple arrays and an optional iteratee function (or property key)
* to compare the elements after transforming them. It returns a new array containing the elements from
* the first array that are present in all subsequent arrays after applying the iteratee to each element.
* If no iteratee is provided, the identity function is used.
*
* If the first array is `null` or `undefined`, an empty array is returned.
*
* @template T
* @param {ArrayLike<T> | null | undefined} array - The first array to compare.
* @param {...(ArrayLike<T> | ((value: T) => unknown) | string)} values - The arrays to compare, or the iteratee function.
* @returns {T[]} A new array containing the elements from the first array that are present
* in all subsequent arrays after applying the iteratee.
*
* @example
* const array1 = [{ x: 1 }, { x: 2 }, { x: 3 }];
* const array2 = [{ x: 2 }, { x: 3 }];
* const result = intersectionBy(array1, array2, 'x');
* // result will be [{ x: 2 }, { x: 3 }] since these elements have the same `x` property.
*
* @example
* const array1 = [1.1, 2.2, 3.3];
* const array2 = [2.3, 3.3];
* const result = intersectionBy(array1, array2, Math.floor);
* // result will be [2.3, 3.3] since it shares the same integer part when `Math.floor` is applied.
*/
export function intersectionBy<T>(
array: ArrayLike<T> | null | undefined,
...values: Array<ArrayLike<T> | ((value: T) => unknown) | string>
): T[] {
if (!isArrayLikeObject(array)) {
return [];
}
const lastValue = last(values);
if (lastValue === undefined) {
return Array.from(array);
}
let result = uniq(Array.from(array));
const count = isArrayLikeObject(lastValue) ? values.length : values.length - 1;
for (let i = 0; i < count; ++i) {
const value = values[i];
if (!isArrayLikeObject(value)) {
return [];
}
if (isArrayLikeObject(lastValue)) {
result = intersectionByToolkit(result, Array.from(value), identity);
} else if (typeof lastValue === 'function') {
result = intersectionByToolkit(result, Array.from(value), value => lastValue(value));
} else if (typeof lastValue === 'string') {
result = intersectionByToolkit(result, Array.from(value), property(lastValue));
}
}
return result;
}

View File

@ -47,6 +47,7 @@ export { head as first } from './array/head.ts';
export { includes } from './array/includes.ts';
export { indexOf } from './array/indexOf.ts';
export { intersection } from './array/intersection.ts';
export { intersectionBy } from './array/intersectionBy.ts';
export { join } from './array/join.ts';
export { last } from './array/last.ts';
export { orderBy } from './array/orderBy.ts';