Skip to content

Commit

Permalink
Merge branch 'main' into imp/invariant
Browse files Browse the repository at this point in the history
  • Loading branch information
ssi02014 authored Nov 9, 2024
2 parents bb10377 + 0feabb4 commit ea17632
Show file tree
Hide file tree
Showing 14 changed files with 383 additions and 15 deletions.
10 changes: 10 additions & 0 deletions benchmarks/performance/uniqBy.bench.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { bench, describe } from 'vitest';
import { uniqBy as uniqByToolkit_ } from 'es-toolkit';
import { uniqBy as uniqByToolkitCompat_ } from 'es-toolkit/compat';
import { randomInt } from 'crypto';
import { uniqBy as uniqByLodash_ } from 'lodash';

const uniqByToolkit = uniqByToolkit_;
const uniqByToolkitCompat = uniqByToolkitCompat_;
const uniqByLodash = uniqByLodash_;

describe('uniqBy, small arrays', () => {
bench('es-toolkit/uniqBy', () => {
uniqByToolkit([2.1, 1.2, 2.3], Math.floor);
});

bench('es-toolkit/compat/uniqBy', () => {
uniqByToolkitCompat([2.1, 1.2, 2.3], Math.floor);
});

bench('lodash/uniqBy', () => {
uniqByLodash([2.1, 1.2, 2.3], Math.floor);
});
Expand All @@ -23,6 +29,10 @@ describe('uniqBy, large arrays', () => {
uniqByToolkit(largeArray, Math.floor);
});

bench('es-toolkit/compat/uniqBy', () => {
uniqByToolkitCompat(largeArray, Math.floor);
});

bench('lodash/uniqBy', () => {
uniqByLodash(largeArray, Math.floor);
});
Expand Down
2 changes: 1 addition & 1 deletion docs/ja/reference/compat/predicate/isEmpty.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function isEmpty(): true;
function isEmpty(value: string): value is '';
function isEmpty(value: Map<any, any>): boolean;
function isEmpty(value: Set<any>): boolean;
function isEmpty(value: Array<any>): value is [];
function isEmpty(value: any[]): value is [];
function isEmpty<T extends Record<any, any>>(
value: T | null | undefined
): value is Record<keyof T, never> | null | undefined;
Expand Down
2 changes: 1 addition & 1 deletion docs/ko/reference/compat/predicate/isEmpty.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function isEmpty(): true;
function isEmpty(value: string): value is '';
function isEmpty(value: Map<any, any>): boolean;
function isEmpty(value: Set<any>): boolean;
function isEmpty(value: Array<any>): value is [];
function isEmpty(value: any[]): value is [];
function isEmpty<T extends Record<any, any>>(
value: T | null | undefined
): value is Record<keyof T, never> | null | undefined;
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/compat/predicate/isEmpty.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Checks if a given value is empty.
function isEmpty(value: string): value is '';
function isEmpty(value: Map<any, any>): boolean;
function isEmpty(value: Set<any>): boolean;
function isEmpty(value: Array<any>): value is [];
function isEmpty(value: any[]): value is [];
function isEmpty<T extends Record<any, any>>(
value: T | null | undefined
): value is Record<keyof T, never> | null | undefined;
Expand Down
2 changes: 1 addition & 1 deletion docs/zh_hans/reference/compat/predicate/isEmpty.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function isEmpty(): true;
function isEmpty(value: string): value is '';
function isEmpty(value: Map<any, any>): boolean;
function isEmpty(value: Set<any>): boolean;
function isEmpty(value: Array<any>): value is [];
function isEmpty(value: any[]): value is [];
function isEmpty<T extends Record<any, any>>(
value: T | null | undefined
): value is Record<keyof T, never> | null | undefined;
Expand Down
39 changes: 39 additions & 0 deletions src/compat/_internal/flattenArrayLike.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest';
import { flattenArrayLike } from './flattenArrayLike';

describe('flattenArrayLike', () => {
it('should flatten an array of array-like objects', () => {
const input: Array<ArrayLike<string>> = [
{ length: 3, 0: 'a', 1: 'b', 2: 'c' },
{ length: 2, 0: 'd', 1: 'e' },
{ length: 0 },
];
const expectedOutput = ['a', 'b', 'c', 'd', 'e'];
expect(flattenArrayLike(input)).toEqual(expectedOutput);
});

it('should ignore non-array-like objects', () => {
const input: any[] = [{ length: 2, 0: 'x', 1: 'y' }, 3, { length: 1, 0: 'z' }];
const expectedOutput = ['x', 'y', 'z'];
expect(flattenArrayLike(input)).toEqual(expectedOutput);
});

it('should return an empty array when input is empty', () => {
const input: Array<ArrayLike<any>> = [];
const expectedOutput: any[] = [];
expect(flattenArrayLike(input)).toEqual(expectedOutput);
});

it('should handle nested array-like objects', () => {
const input: Array<ArrayLike<number[]>> = [
{ length: 2, 0: [1, 2], 1: [3, 4] },
{ length: 1, 0: [5, 6] },
];
const expectedOutput = [
[1, 2],
[3, 4],
[5, 6],
];
expect(flattenArrayLike(input)).toEqual(expectedOutput);
});
});
19 changes: 19 additions & 0 deletions src/compat/_internal/flattenArrayLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { isArrayLikeObject } from '../predicate/isArrayLikeObject.ts';

export function flattenArrayLike<T>(values: Array<ArrayLike<T>>): T[] {
const result: T[] = [];

for (let i = 0; i < values.length; i++) {
const arrayLike = values[i];

if (!isArrayLikeObject(arrayLike)) {
continue;
}

for (let j = 0; j < arrayLike.length; j++) {
result.push(arrayLike[j]);
}
}

return result;
}
98 changes: 98 additions & 0 deletions src/compat/array/differenceBy.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { describe, expect, it } from 'vitest';
import { differenceBy } from './differenceBy';
import { range } from '../../math';
import { args } from '../_internal/args';
import { LARGE_ARRAY_SIZE } from '../_internal/LARGE_ARRAY_SIZE';
import { slice } from '../_internal/slice';

describe('differenceBy', () => {
Expand All @@ -15,6 +18,7 @@ describe('differenceBy', () => {
let args: any;

differenceBy([2.1, 1.2], [2.3, 3.4], function () {
// eslint-disable-next-line
args || (args = slice.call(arguments));
});

Expand All @@ -25,4 +29,98 @@ describe('differenceBy', () => {
const actual = differenceBy([2, 1, 2, 3], [3, 4], [3, 2]);
expect(actual).toEqual([1]);
});

/**
* @see https://github.com/lodash/lodash/blob/6a2cc1dfcf7634fea70d1bc5bd22db453df67b42/test/difference-methods.spec.js#L1
*/

it(`should return the difference of two arrays`, () => {
const actual = differenceBy([2, 1], [2, 3]);
expect(actual).toEqual([1]);
});

it(`should return the difference of multiple arrays`, () => {
const actual = differenceBy([2, 1, 2, 3], [3, 4], [3, 2]);
expect(actual).toEqual([1]);
});

it(`should treat \`-0\` as \`0\``, () => {
const array = [-0, 0];

const actual = array.map(value => differenceBy(array, [value]));

expect(actual).toEqual([[], []]);

expect(differenceBy([-0, 1], [1])).toEqual([-0]);
});

it(`should match \`NaN\``, () => {
expect(differenceBy([1, NaN, 3], [NaN, 5, NaN])).toEqual([1, 3]);
});

it(`should work with large arrays`, () => {
const array1: unknown[] = range(LARGE_ARRAY_SIZE + 1);
const array2: unknown[] = range(LARGE_ARRAY_SIZE);

const a = {};
const b = {};
const c = {};

array1.push(a, b, c);
array2.push(b, c, a);

expect(differenceBy(array1, array2)).toEqual([LARGE_ARRAY_SIZE]);
});

it(`should work with large arrays of \`-0\` as \`0\``, () => {
const array = [-0, 0];

const actual = array.map(value => {
const largeArray = Array.from({ length: LARGE_ARRAY_SIZE }).map(() => value);

return differenceBy(array, largeArray);
});

expect(actual).toEqual([[], []]);

const largeArray = Array.from({ length: LARGE_ARRAY_SIZE }).map(() => 1);
expect(differenceBy([-0, 1], largeArray)).toEqual([-0]);
});

it(`should work with large arrays of \`NaN\``, () => {
const largeArray = Array.from({ length: LARGE_ARRAY_SIZE }).map(() => NaN);
expect(differenceBy([1, NaN, 3], largeArray)).toEqual([1, 3]);
});

it(`should work with large arrays of objects`, () => {
const object1 = {};
const object2 = {};
const largeArray = Array.from({ length: LARGE_ARRAY_SIZE }).map(() => ({}));

expect(differenceBy([object1, object2], largeArray)).toEqual([object1, object2]);
});

it(`should work with \`arguments\` objects`, () => {
const array = [0, 1, null, 3];

expect(differenceBy(array, args)).toEqual([0, null]);
expect(differenceBy(args, array)).toEqual([2]);
});

it('should work with arrayLike objects', () => {
const array = { 0: 1, 1: 2, length: 2 };

expect(differenceBy(array, [2, 3])).toEqual([1]);
expect(differenceBy([1, 2, 3], array)).toEqual([3]);
expect(differenceBy([1, 2, 3], array, value => value)).toEqual([3]);
});

it('should return an empty array when the first array is not array-like object', () => {
expect(differenceBy('23', ['2', '3'])).toEqual([]);
});

it('should filter out values that are not arrays or array-like objects', () => {
expect(differenceBy(['2', '3'], '2', ['3'])).toEqual(['2']);
expect(differenceBy(['2', '3'], '2', ['3'], value => value)).toEqual(['2']);
});
});
11 changes: 6 additions & 5 deletions src/compat/array/differenceBy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { flatten } from './flatten.ts';
import { last } from './last.ts';
import { difference as differenceToolkit } from '../../array/difference.ts';
import { differenceBy as differenceByToolkit } from '../../array/differenceBy.ts';
import { flattenArrayLike } from '../_internal/flattenArrayLike.ts';
import { isArrayLikeObject } from '../predicate/isArrayLikeObject.ts';
import { iteratee as createIteratee } from '../util/iteratee.ts';

Expand Down Expand Up @@ -121,16 +121,17 @@ export function differenceBy<T>(array: ArrayLike<T> | null | undefined, ...value
* @param {...any[]} values - Multiple arrays containing elements to be excluded from the primary array.
* @returns {T[]} A new array containing the elements that are present in the primary array but not in the values arrays.
*/
export function differenceBy<T>(arr: ArrayLike<T> | null | undefined, ...values: any[]): T[] {
export function differenceBy<T>(arr: ArrayLike<T> | null | undefined, ..._values: any[]): T[] {
if (!isArrayLikeObject(arr)) {
return [];
}

const iteratee = last(values);
const iteratee = last(_values);
const values = flattenArrayLike<T>(_values);

if (isArrayLikeObject(iteratee)) {
return differenceToolkit(Array.from(arr), flatten(values));
return differenceToolkit(Array.from(arr), values);
}

return differenceByToolkit(Array.from(arr), flatten(values.slice(0, -1)), createIteratee(iteratee));
return differenceByToolkit(Array.from(arr), values, createIteratee(iteratee));
}
95 changes: 95 additions & 0 deletions src/compat/array/uniqBy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, expect, it } from 'vitest';
import { uniqBy } from './uniqBy';
import { LARGE_ARRAY_SIZE } from '../_internal/LARGE_ARRAY_SIZE';

/**
* @see https://github.com/lodash/lodash/blob/6a2cc1dfcf7634fea70d1bc5bd22db453df67b42/test/uniqBy-methods.spec.js
*/
describe('uniqBy', () => {
const objects = [{ a: 2 }, { a: 3 }, { a: 1 }, { a: 2 }, { a: 3 }, { a: 1 }];

it('should work with a `mapper`', () => {
expect(uniqBy([2.1, 1.2, 2.3], Math.floor)).toEqual([2.1, 1.2]);
expect(uniqBy([1.2, 1.5, 2.1, 3.2, 5.7, 5.3, 7.19], Math.floor)).toEqual([1.2, 2.1, 3.2, 5.7, 7.19]);
});

it('should work with an iteratee function', () => {
const expected = objects.slice(0, 3);
const actual = uniqBy(objects, (object: { a: any }) => object.a);
expect(actual).toEqual(expected);
});

it('should work with property shorthands (string)', () => {
const expected = objects.slice(0, 3);
const actual = uniqBy(objects, 'a');
expect(actual).toEqual(expected);
});

it('should work with property shorthands (number)', () => {
const arrays = [[2], [3], [1], [2], [3], [1]];
const expected = arrays.slice(0, 3);
const actual = uniqBy(arrays, 0);
expect(actual).toEqual(expected);
});

it('should work with large arrays', () => {
const largeArray = Array.from({ length: LARGE_ARRAY_SIZE }, () => [1, 2]);
const actual = uniqBy(largeArray, JSON.stringify);
expect(actual.length).toBe(1);
expect(actual[0]).toEqual([1, 2]);
});

it('should provide correct iteratee arguments', () => {
let args: any;
uniqBy(objects, (...params: any[]) => {
if (!args) {
args = params;
}
});
expect(args).toEqual([objects[0]]);
});

it('should return an array with the first element when iteratee returns the same value for all elements', () => {
const actual = uniqBy(objects, () => 'same');
expect(actual).toEqual([objects[0]]);
});

describe('should return an empty array when iteratee returns various types', () => {
const testCases = {
'an array': [0, 'a'],
'an object': { '0': 'a' },
'a number': 0,
'a string': '0',
};

Object.entries(testCases).forEach(([key, value]) => {
it(`should return an empty array when iteratee returns ${key}`, () => {
const actual = uniqBy(objects, () => value);

expect(actual).toEqual([objects[0]]);
});
});
});

it('should return an empty array if the first array is null or undefined', () => {
expect(uniqBy(null as any, 'a')).toEqual([]);
expect(uniqBy(undefined as any, 'a')).toEqual([]);
});

it('should handle empty arrays correctly', () => {
expect(uniqBy([], 'a')).toEqual([]);
expect(uniqBy([], Math.floor)).toEqual([]);
});

it('should handle single-element arrays correctly', () => {
const singleElement = [{ a: 1 }];
expect(uniqBy(singleElement, 'a')).toEqual(singleElement);
expect(uniqBy(singleElement, (obj: { a: any }) => obj.a)).toEqual(singleElement);
});

it('should not mutate the original array', () => {
const original = [...objects];
uniqBy(objects, 'a');
expect(objects).toEqual(original);
});
});
Loading

0 comments on commit ea17632

Please sign in to comment.