Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(compat): Implement compat/pickBy #950

Merged
merged 7 commits into from
Mar 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/compat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export { merge } from './object/merge.ts';
export { mergeWith } from './object/mergeWith.ts';
export { omit } from './object/omit.ts';
export { pick } from './object/pick.ts';
export { pickBy } from './object/pickBy.ts';
export { property } from './object/property.ts';
export { propertyOf } from './object/propertyOf.ts';
export { set } from './object/set.ts';
Expand Down
168 changes: 168 additions & 0 deletions src/compat/object/pickBy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { describe, expect, it } from 'vitest';
import * as lodashStable from 'es-toolkit/compat';
import { pickBy } from './pickBy';
import { stubTrue } from '../util/stubTrue';

describe('pickBy', () => {
it('should work with a predicate argument', () => {
const object = { a: 1, b: 2, c: 3, d: 4 };

const actual = pickBy(object, n => n === 1 || n === 3);

expect(actual).toEqual({ a: 1, c: 3 });
});

it('should not treat keys with dots as deep paths', () => {
const object = { 'a.b.c': 1 };
const actual = pickBy(object, stubTrue);

expect(actual).toEqual({ 'a.b.c': 1 });
});

it('should pick properties based on the predicate function', () => {
const obj = { a: 1, b: 'pick', c: 3 };
const shouldPick = (value: string | number) => typeof value === 'string';
const result = pickBy(obj, shouldPick);
expect(result).toEqual({ b: 'pick' });
});

it('should return an empty object if no properties satisfy the predicate', () => {
const obj = { a: 1, b: 2, c: 3 };
const shouldPick = (value: number) => typeof value === 'string';
const result = pickBy(obj, shouldPick);
expect(result).toEqual({});
});

it('should return the same object if all properties satisfy the predicate', () => {
const obj = { a: 'pick', b: 'pick', c: 'pick' };
const shouldPick = (value: string) => typeof value === 'string';
const result = pickBy(obj, shouldPick);
expect(result).toEqual(obj);
});

it('should work with an empty object', () => {
const obj = {};
const shouldPick = (value: never) => value;
const result = pickBy(obj, shouldPick);
expect(result).toEqual({});
});

it('should work with nested objects', () => {
const obj = { a: 1, b: { nested: 'pick' }, c: 3 };
const shouldPick = (value: number | { nested: string }, key: string) => key === 'b';
const result = pickBy(obj, shouldPick);
expect(result).toEqual({ b: { nested: 'pick' } });
});

it('should work with no predicate function', () => {
const obj = { a: 1, b: 'pick', c: 3 };
const result = pickBy(obj);
expect(result).toEqual(obj);
});

it('should return an empty object if the object is null', () => {
const obj = null;
const shouldPick = (value: string) => typeof value === 'string';
const result = pickBy(obj as unknown as object, shouldPick);
expect(result).toEqual({});
});

it('should return an empty object if the object is undefined', () => {
const obj = undefined;
const shouldPick = (value: string) => typeof value === 'string';
const result = pickBy(obj as unknown as object, shouldPick);
expect(result).toEqual({});
});

it(`should provide correct iteratee arguments`, () => {
const array = [1, 2, 3];

let args: any;
const expected: any = [1, 0, array];

// eslint-disable-next-line
// @ts-ignore
pickBy(array, function () {
// eslint-disable-next-line
args || (args = Array.prototype.slice.call(arguments));
});

// eslint-disable-next-line
expected[1] += "";

expect(args).toEqual(expected);
});

it(`should treat sparse arrays as dense`, () => {
const array = [1];
array[2] = 3;

let expected = [
[1, '0', array],
[undefined, '1', array],
[3, '2', array],
];

expected = lodashStable.map(expected, args => {
// eslint-disable-next-line
args[1] += "";
return args;
});

const argsList: any = [];
pickBy(array, function () {
// eslint-disable-next-line
argsList.push(Array.prototype.slice.call(arguments));
return true;
});

expect(argsList).toEqual(expected);
});

it(`iterates over own string keyed properties of objects`, () => {
function Foo(this: any) {
// eslint-disable-next-line
// @ts-ignore
this.a = 1;
}
Foo.prototype.b = 2;

const values: any[] = [];
// eslint-disable-next-line
// @ts-ignore
pickBy(new Foo(), value => {
values.push(value);
});
expect(values).toEqual([1]);
});

it(`should ignore changes to \`length\``, () => {
let count = 0;
const array = [1];

pickBy(array, () => {
if (++count === 1) {
array.push(2);
}
return true;
});

expect(count).toBe(1);
});

it(`should ignore added \`object\` properties`, () => {
let count = 0;
const object = { a: 1 };

pickBy(object, () => {
if (++count === 1) {
// eslint-disable-next-line
// @ts-ignore
object.b = 2;
}
return true;
});

expect(count).toBe(1);
});
});
49 changes: 49 additions & 0 deletions src/compat/object/pickBy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { range } from '../../math/range.ts';
import { isArrayLike } from '../predicate/isArrayLike.ts';

/**
* Creates a new object composed of the properties that satisfy the predicate function.
*
* This function takes an object and a predicate function, and returns a new object that
* includes only the properties for which the predicate function returns true.
*
* @template T - The type of object.
* @param {T} obj - The object to pick properties from.
* @param {(value: T[keyof T], key: keyof T) => boolean} shouldPick - A predicate function that determines
* whether a property should be picked. It takes the property's key and value as arguments and returns `true`
* if the property should be picked, and `false` otherwise.
* @returns {Partial<T>} A new object with the properties that satisfy the predicate function.
*
* @example
* const obj = { a: 1, b: 'pick', c: 3 };
* const shouldPick = (value) => typeof value === 'string';
* const result = pickBy(obj, shouldPick);
* // result will be { b: 'pick' }
*/
export function pickBy<T extends Record<string, any>>(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi!
If you could add small fix here it would be great.

Function doesn't warn you if you pass null as obj, and if you do so, it will crash :)

however, lodash pickBy in that case returns {}
you can look here:
https://github.com/lodash/lodash/blob/4.17.15/lodash.js#L13582

Please add same here, or some warning that obj can't be null.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your suggestion! I fixed this problem. Would you check it?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! Thank you very much)

Maybe add some tests for it? I'm not sure.

And wait for approval from someone from repo)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! I will add some tests :)

obj: T,
shouldPick?: (value: T[keyof T], key: keyof T, obj: T) => boolean
): Partial<T> {
if (obj == null) {
return {};
}

const result: Partial<T> = {};

if (shouldPick == null) {
return obj;
}

const keys = isArrayLike(obj) ? range(0, obj.length) : (Object.keys(obj) as Array<keyof T>);

for (let i = 0; i < keys.length; i++) {
const key = keys[i].toString() as keyof T;
const value = obj[key];

if (shouldPick(value, key, obj)) {
result[key] = value;
}
}

return result;
}