From d146538171866af8216c570da1842e459e750033 Mon Sep 17 00:00:00 2001 From: yceffort Date: Fri, 13 Dec 2024 17:10:30 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[#68]=20=F0=9F=9A=80=20get?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.ts | 1 + package.json | 10 +++++ src/get.bench.ts | 27 +++++++++++++ src/get.test.ts | 103 +++++++++++++++++++++++++++++++++++++++++++++++ src/get.ts | 89 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 230 insertions(+) create mode 100644 src/get.bench.ts create mode 100644 src/get.test.ts create mode 100644 src/get.ts diff --git a/index.ts b/index.ts index 93e1d74..2c071f2 100644 --- a/index.ts +++ b/index.ts @@ -15,6 +15,7 @@ const moduleMap = { first: './src/first.ts', flatten: './src/flatten.ts', flow: './src/flow.ts', + get: './src/get.ts', groupBy: './src/groupBy.ts', gt: './src/gt.ts', has: './src/has.ts', diff --git a/package.json b/package.json index 4373bdf..1c5c85b 100644 --- a/package.json +++ b/package.json @@ -150,6 +150,16 @@ "default": "./flow.js" } }, + "./get": { + "import": { + "types": "./get.d.mts", + "default": "./get.mjs" + }, + "require": { + "types": "./get.d.ts", + "default": "./get.js" + } + }, "./groupBy": { "import": { "types": "./groupBy.d.mts", diff --git a/src/get.bench.ts b/src/get.bench.ts new file mode 100644 index 0000000..3c00bd2 --- /dev/null +++ b/src/get.bench.ts @@ -0,0 +1,27 @@ +import _get from 'lodash/get' +import {describe, bench} from 'vitest' + +import get from './get' + +const obj = {a: [{b: {c: 3}}], x: {y: {z: 'hello'}}} +const PATHS = ['a[0].b.c', ['x', 'y', 'z'], 'a.0.b.c', 'not.exist', ['a', '0', 'b', 'notThere']] + +const ITERATIONS = 10000 + +describe('get performance', () => { + bench('hidash', () => { + for (let i = 0; i < ITERATIONS; i++) { + PATHS.forEach((path) => { + get(obj, path, 'default') + }) + } + }) + + bench('lodash', () => { + for (let i = 0; i < ITERATIONS; i++) { + PATHS.forEach((path) => { + _get(obj, path, 'default') + }) + } + }) +}) diff --git a/src/get.test.ts b/src/get.test.ts new file mode 100644 index 0000000..0c51a57 --- /dev/null +++ b/src/get.test.ts @@ -0,0 +1,103 @@ +import _get from 'lodash/get' +import {describe, it, expect} from 'vitest' + +import get from './get' + +describe('get', () => { + it('should get property by string path', () => { + const obj = {a: {b: {c: 3}}} + expect(get(obj, 'a.b.c')).toBe(_get(obj, 'a.b.c')) + }) + + it('should get property by array path', () => { + const obj = {a: [{b: {c: 3}}]} + expect(get(obj, ['a', 0, 'b', 'c'])).toBe(_get(obj, ['a', '0', 'b', 'c'])) + }) + + it('should return default if not found', () => { + const obj = {a: {b: 2}} + expect(get(obj, 'a.b.c', 'default')).toBe(_get(obj, 'a.b.c', 'default')) + }) + + it('should handle null/undefined object', () => { + expect(get(null, 'a.b.c', 'default')).toBe(_get(null, 'a.b.c', 'default')) + expect(get(undefined, 'x.y', 10)).toBe(_get(undefined, 'x.y', 10)) + }) + + it('should handle non-object', () => { + expect(get(42, 'a', 'not found')).toBe(_get(42, 'a', 'not found')) + }) + + it('should handle symbol keys', () => { + const sym = Symbol('test') + const obj = {[sym]: 'symbolValue'} + expect(get(obj, sym)).toBe(_get(obj, sym)) + }) + + // 복잡한 경로 테스트 + it('should handle complex paths with brackets and quotes', () => { + const obj = { + a: [ + { + b: { + 'complex.key': { + c: 'complexValue', + 'escaped\\"quotes"': 'withEscapedQuotes', + }, + }, + }, + ], + 'weird key with spaces': {x: 123}, + } + + // 경로: a[0].b["complex.key"].c + expect(get(obj, 'a[0].b["complex.key"].c')).toBe(_get(obj, 'a[0].b["complex.key"].c')) + // 경로: a[0].b["complex.key"]["escaped\\\"quotes"] + expect(get(obj, 'a[0].b["complex.key"]["escaped\\"quotes"]')).toBe( + _get(obj, 'a[0].b["complex.key"]["escaped\\"quotes"]'), + ) + + // 공백 포함 키: ["weird key with spaces"].x + expect(get(obj, '["weird key with spaces"].x')).toBe(_get(obj, '["weird key with spaces"].x')) + }) + + it('should handle array indices as strings and negative indices as keys', () => { + const obj = { + a: { + '0': {d: 4}, + '-1': {d: 'negativeIndexKey'}, + }, + } + expect(get(obj, 'a["0"].d')).toBe(_get(obj, 'a["0"].d')) + expect(get(obj, 'a[-1].d')).toBe(_get(obj, 'a[-1].d')) + }) + + it('should handle empty string key', () => { + const obj = { + '': {value: 'emptyKey'}, + a: {'': {nested: 'emptyNested'}}, + } + + expect(get(obj, '[""].value')).toBe(_get(obj, '[""].value')) + expect(get(obj, 'a[""].nested')).toBe(_get(obj, 'a[""].nested')) + }) + + it('should return undefined if intermediate property is missing', () => { + const obj = {a: {b: {c: 3}}} + expect(get(obj, 'a.x.y', 'not found')).toBe(_get(obj, 'a.x.y', 'not found')) + expect(get(obj, 'a.b.x.y', 'defaultVal')).toBe(_get(obj, 'a.b.x.y', 'defaultVal')) + }) + + it('should work with numeric paths as part of a complex path', () => { + const obj = { + arr: [ + { + nested: [{val: 'first'}, {val: 'second'}], + }, + ], + } + + expect(get(obj, 'arr[0].nested[1].val')).toBe(_get(obj, 'arr[0].nested[1].val')) + expect(get(obj, 'arr[0]["nested"][0].val')).toBe(_get(obj, 'arr[0]["nested"][0].val')) + }) +}) diff --git a/src/get.ts b/src/get.ts new file mode 100644 index 0000000..e994234 --- /dev/null +++ b/src/get.ts @@ -0,0 +1,89 @@ +const rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]/g +const reEscapeChar = /\\(\\)?/g +const pathCache = new Map() + +function isObjectLike(value: unknown): value is object { + return value !== null && typeof value === 'object' +} + +function isNumericKey(str: string): boolean { + return /^-?\d+$/.test(str) +} + +function complexStringToPath(path: string): (string | number)[] { + const cached = pathCache.get(path) + if (cached) { + return cached + } + + const result: (string | number)[] = [] + + // 선행 '.' 처리 + if (path[0] === '.') { + result.push('') + } + + path.replace(rePropName, (match, number, quote, subStr) => { + let key: string | number = match + let subString = subStr + if (quote) { + subString = subString ?? '' + key = subString.replace(reEscapeChar, '$1') + } else if (number !== undefined) { + key = Number(number) + } else if (isNumericKey(key)) { + key = Number(key) + } + result.push(key) + return '' + }) + + pathCache.set(path, result) + return result +} + +function castPath(path: string | symbol | (string | number)[]): (string | number | symbol)[] { + if (Array.isArray(path)) { + return path + } + if (typeof path === 'symbol') { + return [path] + } + + if (path.includes('[') || path.includes(']') || path.includes('"') || path.includes("'")) { + return complexStringToPath(path) + } + + return path === '' ? [''] : path.split('.') +} + +function baseGet(obj: unknown, path: (string | number | symbol)[]): unknown { + let current = obj + for (let i = 0; i < path.length; i++) { + if (!isObjectLike(current)) { + return undefined + } + const key = path[i] + if (!Object.prototype.hasOwnProperty.call(current, key)) { + return undefined + } + current = (current as Record)[key] + } + return current +} + +export function get( + object: unknown, + path: string | symbol | (string | number)[], + defaultValue?: T, +): T | undefined { + if (object == null) { + return defaultValue + } + + const arrPath = castPath(path) + const result = baseGet(object, arrPath) + return result === undefined ? defaultValue : (result as T) +} + +export default get From 1855b6d7cd357e4bfcd6f9063feddd275377a114 Mon Sep 17 00:00:00 2001 From: yceffort_naver <158988311+yceffort-naver@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:12:09 +0900 Subject: [PATCH 2/3] Create free-turkeys-sing.md --- .changeset/free-turkeys-sing.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/free-turkeys-sing.md diff --git a/.changeset/free-turkeys-sing.md b/.changeset/free-turkeys-sing.md new file mode 100644 index 0000000..21480ab --- /dev/null +++ b/.changeset/free-turkeys-sing.md @@ -0,0 +1,7 @@ +--- +"@naverpay/hidash": patch +--- + + 🚀 get + +PR: [ 🚀 get](https://github.com/NaverPayDev/hidash/pull/173) From 4fa9d1022fec91fc10044ef0535e0e467cc13ea4 Mon Sep 17 00:00:00 2001 From: yceffort Date: Tue, 17 Dec 2024 09:37:10 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[#68]=20=F0=9F=9A=9A=20isArray?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/get.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/get.ts b/src/get.ts index e994234..1b534d3 100644 --- a/src/get.ts +++ b/src/get.ts @@ -1,3 +1,5 @@ +import isArray from './isArray' + const rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]/g const reEscapeChar = /\\(\\)?/g const pathCache = new Map() @@ -43,7 +45,7 @@ function complexStringToPath(path: string): (string | number)[] { } function castPath(path: string | symbol | (string | number)[]): (string | number | symbol)[] { - if (Array.isArray(path)) { + if (isArray(path)) { return path } if (typeof path === 'symbol') {