diff --git a/benchmarks/performance/findKey.bench.ts b/benchmarks/performance/findKey.bench.ts index 22ed0ef22..4b33118be 100644 --- a/benchmarks/performance/findKey.bench.ts +++ b/benchmarks/performance/findKey.bench.ts @@ -1,5 +1,6 @@ import { bench, describe } from 'vitest'; import { findKey as findKeyToolkit } from 'es-toolkit'; +import { findKey as findKeyCompatToolkit } from 'es-toolkit/compat'; import { findKey as findKeyLodash } from 'lodash'; describe('findKey', () => { @@ -16,6 +17,10 @@ describe('findKey', () => { bench('lodash/findKey', () => { findKeyLodash(users, o => o.age < 40); }); + + bench('es-toolkit/compat/findKey', () => { + findKeyCompatToolkit(users, o => o.age < 40); + }); }); describe('findKey/largeObject', () => { @@ -31,4 +36,8 @@ describe('findKey/largeObject', () => { bench('lodash/findKey', () => { findKeyLodash(largeUsers, o => o.age === 7000); }); + + bench('es-toolkit/compat/findKey', () => { + findKeyCompatToolkit(largeUsers, o => o.age === 7000); + }); }); diff --git a/src/compat/index.ts b/src/compat/index.ts index 15be574b7..1f7bd0d7d 100644 --- a/src/compat/index.ts +++ b/src/compat/index.ts @@ -115,6 +115,7 @@ export { property } from './object/property.ts'; export { set } from './object/set.ts'; export { toDefaulted } from './object/toDefaulted.ts'; export { unset } from './object/unset.ts'; +export { findKey } from './object/findKey.ts' export { conforms } from './predicate/conforms.ts'; export { conformsTo } from './predicate/conformsTo.ts'; diff --git a/src/compat/object/findKey.spec.ts b/src/compat/object/findKey.spec.ts new file mode 100644 index 000000000..624c38f69 --- /dev/null +++ b/src/compat/object/findKey.spec.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest'; +import { findKey } from './findKey'; + +/** + * @see https://lodash.com/docs/4.17.15#findKey + */ +describe('findKey', () => { + const users = { + barney: { age: 36, active: true }, + fred: { age: 40, active: false }, + pebbles: { age: 1, active: true }, + }; + + it('should find key with a function predicate', function () { + const actual = findKey(users, function (o) { + return o.age < 40; + }); + expect(actual).toBe('barney'); + }); + + it('should work with `_.matches` shorthands', function () { + const actual = findKey(users, { age: 1, active: true }); + expect(actual).toBe('pebbles'); + }); + + it('should work with `_.matchesProperty` shorthands', function () { + const actual = findKey(users, ['active', false]); + expect(actual).toBe('fred'); + }); + + it('should work with `_.property` shorthands', function () { + const actual = findKey(users, 'active'); + expect(actual).toBe('barney'); + }); + + it('should return undefined for an empty object', function () { + // @ts-expect-error - invalid argument + const actual = findKey({}, { age: 36 }); + expect(actual).toBeUndefined(); + }); + + it('should return undefined for null input', function () { + const actual = findKey(null, { age: 36 }); + expect(actual).toBeUndefined(); + }); + + it('should return undefined for undefined input', function () { + const actual = findKey(undefined, { age: 36 }); + expect(actual).toBeUndefined(); + }); + + it('should return undefined if no matching key is found', function () { + const actual = findKey(users, { age: 100 }); + expect(actual).toBeUndefined(); + }); + + it('should handle partial matches with `Partial`', function () { + const actual = findKey(users, { active: true }); + expect(actual).toBe('barney'); + }); +}); diff --git a/src/compat/object/findKey.ts b/src/compat/object/findKey.ts new file mode 100644 index 000000000..6db774e1f --- /dev/null +++ b/src/compat/object/findKey.ts @@ -0,0 +1,132 @@ +import { property } from './property'; +import { findKey as findKeyToolkit } from '../../object'; +import { isObject } from '../predicate/isObject'; +import { matches } from '../predicate/matches'; +import { matchesProperty } from '../predicate/matchesProperty'; + +/** + * Finds the key of the first element predicate returns truthy for. + * + * @template T - The type of the object. + * @param {T | null | undefined} obj - The object to inspect. + * @param {(value: T[keyof T], key: keyof T, obj: T) => boolean} conditionToFind - The function invoked per iteration. + * @returns {keyof T | undefined} Returns the key of the matched element, else `undefined`. + * + * @example + * const users = { 'barney': { 'age': 36 }, 'fred': { 'age': 40 } }; + * const result = findKey(users, o => o.age < 40); + * // => 'barney' + */ +export function findKey>( + obj: T | null | undefined, + conditionToFind: (value: T[keyof T], key: keyof T, obj: T) => boolean +): keyof T | undefined; + +/** + * Finds the key of the first element that matches the given object. + * + * @template T - The type of the object. + * @param {T | null | undefined} obj - The object to inspect. + * @param {Partail} objectToFind - The object to match. + * @returns {keyof T | undefined} Returns the key of the matched element, else `undefined`. + * + * @example + * const users = { 'barney': { 'age': 36 }, 'fred': { 'age': 40 } }; + * const result = findKey(users, { 'age': 36 }); + * // => 'barney' + */ +export function findKey>( + obj: T | null | undefined, + objectToFind: Partial +): keyof T | undefined; + +/** + * Finds the key of the first element that matches the given property and value. + * + * @template T - The type of the object. + * @param {T | null | undefined} obj - The object to inspect. + * @param {[keyof T[keyof T], any]} propertyToFind - The property and value to match. + * @returns {keyof T | undefined} Returns the key of the matched element, else `undefined`. + * + * @example + * const users = { 'barney': { 'age': 36 }, 'fred': { 'age': 40 } }; + * const result = findKey(users, ['age', 36]); + * // => 'barney' + */ +export function findKey>( + obj: T | null | undefined, + propertyToFind: [keyof T[keyof T], any] +): keyof T | undefined; + +/** + * Finds the key of the first element that has a truthy value for the given property. + * + * @template T - The type of the object. + * @param {T | null | undefined} obj - The object to inspect. + * @param {keyof T[keyof T]} propertyToFind - The property to check. + * @returns {keyof T | undefined} Returns the key of the matched element, else `undefined`. + * + * @example + * const users = { 'barney': { 'active': true }, 'fred': { 'active': false } }; + * const result = findKey(users, 'active'); + * // => 'barney' + */ +export function findKey>( + obj: T | null | undefined, + propertyToFind: keyof T[keyof T] +): keyof T | undefined; + +/** + * Finds the key of the first element that matches the given predicate. + * + * This function determines the type of the predicate and delegates the search + * to the appropriate helper function. It supports predicates as functions, objects, + * arrays, or strings. + * + * @template T - The type of the object. + * @param {T | null | undefined} obj - The object to inspect. + * @param {(value: T[keyof T], key: keyof T, obj: T) => boolean | Partial | [keyof T[keyof T], any] | keyof T[keyof T]} predicate - The predicate to match. + * @returns {keyof T | undefined} Returns the key of the matched element, else `undefined`. + */ +export function findKey>( + obj: T | null | undefined, + predicate: + | ((value: T[keyof T], key: keyof T, obj: T) => boolean) + | Partial + | [keyof T[keyof T], any] + | keyof T[keyof T] +): keyof T | undefined { + if (!isObject(obj)) { + return undefined; + } + + return findKeyImpl(obj, predicate); +} + +function findKeyImpl>( + obj: T, + predicate: + | ((value: T[keyof T], key: keyof T, obj: T) => boolean) + | Partial + | [keyof T[keyof T], any] + | keyof T[keyof T] +) { + if (typeof predicate === 'function') { + return findKeyToolkit(obj, predicate); + } + + if (typeof predicate === 'object') { + if (Array.isArray(predicate)) { + const key = predicate[0]; + const value = predicate[1]; + + return findKeyToolkit(obj, matchesProperty(key, value)); + } + + return findKeyToolkit(obj, matches(predicate)); + } + + if (typeof predicate === 'string') { + return findKeyToolkit(obj, property(predicate)); + } +}