Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/tricky-toes-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@naverpay/hidash": minor
---

add sortBy

PR: [add sortBy](https://github.com/NaverPayDev/hidash/pull/282)
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
69 changes: 69 additions & 0 deletions src/sortBy.bench.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
})
117 changes: 117 additions & 0 deletions src/sortBy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import _sortBy from 'lodash/sortBy'
import {describe, it, expect} from 'vitest'

import {sortBy} from './sortBy'

function expectsEqual<T>(...args: Parameters<typeof sortBy<T>>) {
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')
})
})
82 changes: 82 additions & 0 deletions src/sortBy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
type FnType<T> = (item: T) => unknown
type Iteratee<T> = FnType<T> | keyof T

type Iteratees<T> = Iteratee<T> | Iteratee<T>[]

const convertValues = function <T>(iteratees: Iteratees<T>[]) {
let result: Iteratee<T>[] = []
const baseIteratee: FnType<T> = (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<T> => {
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<T>} [collection] The collection to iterate over
* @param {ListIteratee<T | (value: T) => un} [iteratees] Sort by property or function.
* @returns {List<T>} Returns the new sorted array.
*/
export function sortBy<T>(collection: T[], ...iteratees: Iteratees<T>[]): 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
1 change: 1 addition & 0 deletions vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
})