diff --git a/.changeset/tricky-toes-clap.md b/.changeset/tricky-toes-clap.md new file mode 100644 index 0000000..20e3cdd --- /dev/null +++ b/.changeset/tricky-toes-clap.md @@ -0,0 +1,7 @@ +--- +"@naverpay/hidash": minor +--- + +add sortBy + +PR: [add sortBy](https://github.com/NaverPayDev/hidash/pull/282) diff --git a/index.ts b/index.ts index 52063fe..c8038d5 100644 --- a/index.ts +++ b/index.ts @@ -60,6 +60,7 @@ const moduleMap = { size: './src/size.ts', sleep: './src/sleep.ts', some: './src/some.ts', + sortBy: './src/sortBy.ts', sum: './src/sum.ts', sumBy: './src/sumBy.ts', throttle: './src/throttle.ts', diff --git a/package.json b/package.json index 0106a53..84c7914 100644 --- a/package.json +++ b/package.json @@ -605,6 +605,16 @@ "default": "./some.js" } }, + "./sortBy": { + "import": { + "types": "./sortBy.d.mts", + "default": "./sortBy.mjs" + }, + "require": { + "types": "./sortBy.d.ts", + "default": "./sortBy.js" + } + }, "./sum": { "import": { "types": "./sum.d.mts", diff --git a/src/sortBy.bench.ts b/src/sortBy.bench.ts new file mode 100644 index 0000000..30199fd --- /dev/null +++ b/src/sortBy.bench.ts @@ -0,0 +1,69 @@ +import _sortBy from 'lodash/sortBy' +import {bench, describe} from 'vitest' + +import {sortBy} from './sortBy' + +const ITERATIONS = 100 + +interface User { + user: string + age: number +} + +const USERS_SMALL: User[] = [ + {user: 'ChengJha', age: 48}, + {user: 'JoelSaad', age: 36}, + {user: 'RaulIsmail', age: 40}, + {user: 'VeraSaeed', age: 45}, + {user: 'RobertoRamos', age: 28}, + {user: 'OscarWatanabe', age: 21}, + {user: 'AnnXiang', age: 25}, +] + +const USERS_BIG: User[] = Array.from({length: 1000}, (_, i) => ({ + user: 'user' + (i % 100), + age: Math.floor(Math.random() * 100), +})) + +const USERS_HUGE: User[] = Array.from({length: 10_000}, (_) => ({ + user: 'randomUser' + Math.floor(Math.random() * 1000), + age: Math.floor(Math.random() * 1000), +})) + +const USERS_MULTI: User[] = [ + {user: 'barney', age: 36}, + {user: 'fred', age: 48}, + {user: 'wilma', age: 40}, + {user: 'wilma', age: 36}, + {user: 'betty', age: 36}, + {user: 'barney', age: 34}, + {user: 'fred', age: 48}, + {user: 'betty', age: 40}, +] as const + +const testCases = [ + {data: USERS_SMALL, iteratees: ['age']}, + {data: USERS_SMALL, iteratees: [(o: User) => o.user]}, + {data: USERS_BIG, iteratees: ['user']}, + {data: USERS_BIG, iteratees: [(o: User) => o.age]}, + {data: USERS_HUGE, iteratees: ['age']}, + {data: USERS_MULTI, iteratees: ['user', 'age']}, + {data: USERS_MULTI, iteratees: ['user', (o: User) => o.age]}, +] as const +describe('sortBy performance', () => { + bench('hidash sortBy', () => { + for (let i = 0; i < ITERATIONS; i++) { + for (const {data, iteratees} of testCases) { + sortBy(data, ...iteratees) + } + } + }) + + bench('lodash sortBy', () => { + for (let i = 0; i < ITERATIONS; i++) { + for (const {data, iteratees} of testCases) { + _sortBy(data, ...iteratees) + } + } + }) +}) diff --git a/src/sortBy.test.ts b/src/sortBy.test.ts new file mode 100644 index 0000000..6299694 --- /dev/null +++ b/src/sortBy.test.ts @@ -0,0 +1,117 @@ +import _sortBy from 'lodash/sortBy' +import {describe, it, expect} from 'vitest' + +import {sortBy} from './sortBy' + +function expectsEqual(...args: Parameters>) { + expect(sortBy(...args)).toEqual(_sortBy(...args)) +} + +describe('sortBy', () => { + it('sorts an array of objects by single property', () => { + const data = [ + {user: 'fred', age: 48}, + {user: 'barney', age: 34}, + {user: 'fred', age: 40}, + {user: 'barney', age: 36}, + ] + expectsEqual(data, 'user') + }) + + it('sorts an array with multiple property iteratees', () => { + const data = [ + {user: 'fred', age: 48}, + {user: 'barney', age: 34}, + {user: 'fred', age: 40}, + {user: 'barney', age: 36}, + ] + expectsEqual(data, ['user', 'age']) + }) + + it('sorts an array with function + property iteratees', () => { + const data = [ + {user: 'fred', age: 48}, + {user: 'barney', age: 36}, + {user: 'fred', age: 40}, + {user: 'barney', age: 36}, + ] + expectsEqual(data, (o) => o.user, 'age') + }) + + it('stably sorts items with the same criteria', () => { + const data = [ + {id: 1, group: 'alpha', order: 2}, + {id: 2, group: 'alpha', order: 1}, + {id: 3, group: 'beta', order: 1}, + {id: 4, group: 'beta', order: 1}, + ] + expectsEqual(data, ['group', 'order']) + + expect( + sortBy(data, ['group', 'order']) + .filter((d) => d.group === 'beta') + .map((d) => d.id), + ).toEqual( + _sortBy(data, ['group', 'order']) + .filter((d) => d.group === 'beta') + .map((d) => d.id), + ) + }) + + it('sorts the values of an object as if it were an array', () => { + const dataObj = { + a: {user: 'fred', age: 48}, + b: {user: 'barney', age: 36}, + c: {user: 'wilma', age: 40}, + } + expectsEqual(Object.values(dataObj), 'user') + }) + + it('handles empty array', () => { + expectsEqual([], 'age') + + expectsEqual([], (x) => x) + }) + + it('returns a new sorted array (does not mutate original)', () => { + const data = [ + {user: 'fred', age: 48}, + {user: 'barney', age: 36}, + ] + const dataClone = [...data] + expect(data).toEqual(dataClone) + expectsEqual(data, 'age') + }) + + it('sorts numbers or strings properly', () => { + const numericData = [10, 2, 5, 2] + expectsEqual(numericData) + + const stringData = ['pear', 'apple', 'banana', 'Apple'] + expectsEqual(stringData) + }) + + it('sorts numbers by value', () => { + expectsEqual([5, 2, 9, 1], (x) => x) + + expectsEqual([3, 1, 2]) + }) + + it('sorts using function iteratee', () => { + const users = [ + {name: 'Alice', age: 32}, + {name: 'Bob', age: 24}, + {name: 'Charlie', age: 29}, + ] + expectsEqual(users, (u) => u.name.length) + }) + + it('handles duplicate values correctly', () => { + expectsEqual([3, 1, 2, 1], (x) => x) + }) + + it('handles null values', () => { + expectsEqual([{val: 2}, {val: undefined}, {val: 1}], 'val') + expectsEqual([{val: null}, {val: 2}, {val: 1}], 'val') + }) +}) diff --git a/src/sortBy.ts b/src/sortBy.ts new file mode 100644 index 0000000..e5ee2a6 --- /dev/null +++ b/src/sortBy.ts @@ -0,0 +1,82 @@ +type FnType = (item: T) => unknown +type Iteratee = FnType | keyof T + +type Iteratees = Iteratee | Iteratee[] + +const convertValues = function (iteratees: Iteratees[]) { + let result: Iteratee[] = [] + const baseIteratee: FnType = (v) => v + const list = iteratees.length ? iteratees : [baseIteratee] + + for (const iteratee of list) { + if (Array.isArray(iteratee)) { + result = result.concat(iteratee) + } else { + result.push(iteratee) + } + } + return result.map((fn): FnType => { + if (typeof fn === 'function') { + return fn + } + return (item: T) => item[fn] + }) +} + +function compareValues(a: unknown, b: unknown): number { + if (!(a == null) && !(b == null)) { + if (a > b) { + return 1 + } + if (a < b) { + return -1 + } + } + if (a == null) { + return 1 + } + if (b == null) { + return -1 + } + + return 0 +} + +/** + * @description + * Sort in ascending order + * + * @template T - The type of the function to restrict. + * @param {List} [collection] The collection to iterate over + * @param {ListIteratee un} [iteratees] Sort by property or function. + * @returns {List} Returns the new sorted array. + */ +export function sortBy(collection: T[], ...iteratees: Iteratees[]): T[] { + if (!collection) { + return [] + } + + const getValues = convertValues(iteratees) + const valuesLength = getValues.length + + return collection + .map((value) => { + return { + origin: value, + values: getValues.map((fn) => fn(value)), + } + }) + .sort((a, b) => { + for (let i = 0; i < valuesLength; i++) { + const c = compareValues(a.values[i], b.values[i]) + if (c !== 0) { + return c + } + } + + return 0 + }) + .map(({origin}) => origin) +} + +export default sortBy diff --git a/vite.config.mts b/vite.config.mts index 405843a..b8750d7 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -23,4 +23,5 @@ export default createViteConfig({ 'es.regexp.flags', // https://github.com/zloirock/core-js/commit/9017066b4cb367c6609e4473d43d6e6dad8031a5#diff-59f90be4cf68f9d13d2dce1818780ae968bf48328da4014b47138adf527ec0fcR1066 'es.array.reverse', // https://bugs.webkit.org/show_bug.cgi?id=188794 ], + skipRequiredPolyfillCheck: ['es.array.sort'], })