mirror of
https://github.com/toss/es-toolkit.git
synced 2024-11-24 11:45:26 +03:00
feat(orderBy, isSymbol): add compatibility with lodash (#357)
* Add internal functions * add orderBy with compat * Add docs * Fix typo * Split the code for readability * Change convert to path logic and variable name for readability * Add test cases and js doc in isKey and isSymbol * Add testcase and change logic of isKey * change bench category name * split converToPropertyName for testing * Add case for coverage * fix type * Change using getTage to instanceof * move to predicate * Add doc * Add bench and description in isSymbol * Fix type and test case * Simplify function names * Feat: handle the deep path like keys
This commit is contained in:
parent
4af0662066
commit
bf33cfbca5
@ -1,5 +1,6 @@
|
|||||||
import { bench, describe } from 'vitest';
|
import { bench, describe } from 'vitest';
|
||||||
import { isSymbol as isSymbolToolkit } from 'es-toolkit';
|
import { isSymbol as isSymbolToolkit } from 'es-toolkit';
|
||||||
|
import { isSymbol as isSymbolToolkitCompat } from 'es-toolkit/compat';
|
||||||
import { isSymbol as isSymbolLodash } from 'lodash';
|
import { isSymbol as isSymbolLodash } from 'lodash';
|
||||||
|
|
||||||
describe('isSymbol', () => {
|
describe('isSymbol', () => {
|
||||||
@ -11,6 +12,16 @@ describe('isSymbol', () => {
|
|||||||
isSymbolToolkit({});
|
isSymbolToolkit({});
|
||||||
isSymbolToolkit(123);
|
isSymbolToolkit(123);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bench('es-toolkit/compat/isSymbol', () => {
|
||||||
|
isSymbolToolkitCompat(Symbol('a'));
|
||||||
|
isSymbolToolkitCompat(Symbol.for('a'));
|
||||||
|
isSymbolToolkitCompat(Symbol.iterator);
|
||||||
|
isSymbolToolkitCompat('');
|
||||||
|
isSymbolToolkitCompat({});
|
||||||
|
isSymbolToolkitCompat(123);
|
||||||
|
});
|
||||||
|
|
||||||
bench('lodash/isSymbol', () => {
|
bench('lodash/isSymbol', () => {
|
||||||
isSymbolLodash(Symbol('a'));
|
isSymbolLodash(Symbol('a'));
|
||||||
isSymbolLodash(Symbol.for('a'));
|
isSymbolLodash(Symbol.for('a'));
|
||||||
|
@ -1,25 +1,46 @@
|
|||||||
import { bench, describe } from 'vitest';
|
import { bench, describe } from 'vitest';
|
||||||
import { orderBy as orderByToolkit } from 'es-toolkit';
|
import { orderBy as orderByToolkit } from 'es-toolkit';
|
||||||
|
import { orderBy as orderByToolkitCompat } from 'es-toolkit/compat';
|
||||||
import { orderBy as orderByLodash } from 'lodash';
|
import { orderBy as orderByLodash } from 'lodash';
|
||||||
|
|
||||||
|
const users = [
|
||||||
|
{ user: 'fred', age: 48, nested: { user: 'fred' } },
|
||||||
|
{ user: 'barney', age: 34, nested: { user: 'barney' } },
|
||||||
|
{ user: 'fred', age: 40, nested: { user: 'fred' } },
|
||||||
|
{ user: 'barney', age: 36, nested: { user: 'bar' } },
|
||||||
|
];
|
||||||
|
const keys: Array<keyof (typeof users)[0]> = ['user', 'age'];
|
||||||
|
const orders: Array<'asc' | 'desc'> = ['asc', 'desc'];
|
||||||
|
|
||||||
describe('orderBy', () => {
|
describe('orderBy', () => {
|
||||||
bench('es-toolkit/orderBy', () => {
|
bench('es-toolkit/orderBy', () => {
|
||||||
const users = [
|
orderByToolkit(users, keys, orders);
|
||||||
{ user: 'fred', age: 48 },
|
});
|
||||||
{ user: 'barney', age: 34 },
|
|
||||||
{ user: 'fred', age: 40 },
|
bench('es-toolkit/compat/orderBy', () => {
|
||||||
{ user: 'barney', age: 36 },
|
orderByToolkitCompat(users, keys, orders);
|
||||||
];
|
|
||||||
orderByToolkit(users, ['user', 'age'], ['asc', 'asc']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
bench('lodash/orderBy', () => {
|
bench('lodash/orderBy', () => {
|
||||||
const users = [
|
orderByLodash(users, keys, orders);
|
||||||
{ user: 'fred', age: 48 },
|
});
|
||||||
{ user: 'barney', age: 34 },
|
});
|
||||||
{ user: 'fred', age: 40 },
|
|
||||||
{ user: 'barney', age: 36 },
|
describe('orderBy (nested property names)', () => {
|
||||||
];
|
bench('es-toolkit/compat/orderBy', () => {
|
||||||
orderByLodash(users, ['user', 'age'], ['asc', 'asc']);
|
orderByToolkitCompat(users, [['nested', 'user'], ['age']], orders);
|
||||||
|
});
|
||||||
|
|
||||||
|
bench('lodash/orderBy', () => {
|
||||||
|
orderByLodash(users, [['nested', 'user'], ['age']], orders);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('orderBy (property path)', () => {
|
||||||
|
bench('es-toolkit/compat/orderBy', () => {
|
||||||
|
orderByToolkitCompat(users, ['nested.user', 'age'], orders);
|
||||||
|
});
|
||||||
|
bench('lodash/orderBy', () => {
|
||||||
|
orderByLodash(users, ['nested.user', 'age'], orders);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -135,7 +135,7 @@ Even if a feature is marked "in review," it might already be under review to ens
|
|||||||
| [invokeMap](https://lodash.com/docs/4.17.15#invokeMap) | ❌ |
|
| [invokeMap](https://lodash.com/docs/4.17.15#invokeMap) | ❌ |
|
||||||
| [keyBy](https://lodash.com/docs/4.17.15#keyBy) | 📝 |
|
| [keyBy](https://lodash.com/docs/4.17.15#keyBy) | 📝 |
|
||||||
| [map](https://lodash.com/docs/4.17.15#map) | ❌ |
|
| [map](https://lodash.com/docs/4.17.15#map) | ❌ |
|
||||||
| [orderBy](https://lodash.com/docs/4.17.15#orderBy) | 📝 |
|
| [orderBy](https://lodash.com/docs/4.17.15#orderBy) | ✅ |
|
||||||
| [partition](https://lodash.com/docs/4.17.15#partition) | 📝 |
|
| [partition](https://lodash.com/docs/4.17.15#partition) | 📝 |
|
||||||
| [reduce](https://lodash.com/docs/4.17.15#reduce) | ❌ |
|
| [reduce](https://lodash.com/docs/4.17.15#reduce) | ❌ |
|
||||||
| [reduceRight](https://lodash.com/docs/4.17.15#reduceRight) | ❌ |
|
| [reduceRight](https://lodash.com/docs/4.17.15#reduceRight) | ❌ |
|
||||||
@ -226,7 +226,7 @@ Even if a feature is marked "in review," it might already be under review to ens
|
|||||||
| [isSafeInteger](https://lodash.com/docs/4.17.15#isSafeInteger) | ❌ |
|
| [isSafeInteger](https://lodash.com/docs/4.17.15#isSafeInteger) | ❌ |
|
||||||
| [isSet](https://lodash.com/docs/4.17.15#isSet) | ❌ |
|
| [isSet](https://lodash.com/docs/4.17.15#isSet) | ❌ |
|
||||||
| [isString](https://lodash.com/docs/4.17.15#isString) | ❌ |
|
| [isString](https://lodash.com/docs/4.17.15#isString) | ❌ |
|
||||||
| [isSymbol](https://lodash.com/docs/4.17.15#isSymbol) | ❌ |
|
| [isSymbol](https://lodash.com/docs/4.17.15#isSymbol) | ✅ |
|
||||||
| [isTypedArray](https://lodash.com/docs/4.17.15#isTypedArray) | ✅ |
|
| [isTypedArray](https://lodash.com/docs/4.17.15#isTypedArray) | ✅ |
|
||||||
| [isUndefined](https://lodash.com/docs/4.17.15#isUndefined) | ✅ |
|
| [isUndefined](https://lodash.com/docs/4.17.15#isUndefined) | ✅ |
|
||||||
| [isWeakMap](https://lodash.com/docs/4.17.15#isWeakMap) | ❌ |
|
| [isWeakMap](https://lodash.com/docs/4.17.15#isWeakMap) | ❌ |
|
||||||
|
@ -136,7 +136,7 @@ chunk([1, 2, 3, 4], 0);
|
|||||||
| [invokeMap](https://lodash.com/docs/4.17.15#invokeMap) | ❌ |
|
| [invokeMap](https://lodash.com/docs/4.17.15#invokeMap) | ❌ |
|
||||||
| [keyBy](https://lodash.com/docs/4.17.15#keyBy) | 📝 |
|
| [keyBy](https://lodash.com/docs/4.17.15#keyBy) | 📝 |
|
||||||
| [map](https://lodash.com/docs/4.17.15#map) | ❌ |
|
| [map](https://lodash.com/docs/4.17.15#map) | ❌ |
|
||||||
| [orderBy](https://lodash.com/docs/4.17.15#orderBy) | 📝 |
|
| [orderBy](https://lodash.com/docs/4.17.15#orderBy) | ✅ |
|
||||||
| [partition](https://lodash.com/docs/4.17.15#partition) | 📝 |
|
| [partition](https://lodash.com/docs/4.17.15#partition) | 📝 |
|
||||||
| [reduce](https://lodash.com/docs/4.17.15#reduce) | ❌ |
|
| [reduce](https://lodash.com/docs/4.17.15#reduce) | ❌ |
|
||||||
| [reduceRight](https://lodash.com/docs/4.17.15#reduceRight) | ❌ |
|
| [reduceRight](https://lodash.com/docs/4.17.15#reduceRight) | ❌ |
|
||||||
@ -227,7 +227,7 @@ chunk([1, 2, 3, 4], 0);
|
|||||||
| [isSafeInteger](https://lodash.com/docs/4.17.15#isSafeInteger) | ❌ |
|
| [isSafeInteger](https://lodash.com/docs/4.17.15#isSafeInteger) | ❌ |
|
||||||
| [isSet](https://lodash.com/docs/4.17.15#isSet) | ❌ |
|
| [isSet](https://lodash.com/docs/4.17.15#isSet) | ❌ |
|
||||||
| [isString](https://lodash.com/docs/4.17.15#isString) | ❌ |
|
| [isString](https://lodash.com/docs/4.17.15#isString) | ❌ |
|
||||||
| [isSymbol](https://lodash.com/docs/4.17.15#isSymbol) | ❌ |
|
| [isSymbol](https://lodash.com/docs/4.17.15#isSymbol) | ✅ |
|
||||||
| [isTypedArray](https://lodash.com/docs/4.17.15#isTypedArray) | ✅ |
|
| [isTypedArray](https://lodash.com/docs/4.17.15#isTypedArray) | ✅ |
|
||||||
| [isUndefined](https://lodash.com/docs/4.17.15#isUndefined) | ✅ |
|
| [isUndefined](https://lodash.com/docs/4.17.15#isUndefined) | ✅ |
|
||||||
| [isWeakMap](https://lodash.com/docs/4.17.15#isWeakMap) | ❌ |
|
| [isWeakMap](https://lodash.com/docs/4.17.15#isWeakMap) | ❌ |
|
||||||
|
@ -135,7 +135,7 @@ chunk([1, 2, 3, 4], 0);
|
|||||||
| [invokeMap](https://lodash.com/docs/4.17.15#invokeMap) | ❌ |
|
| [invokeMap](https://lodash.com/docs/4.17.15#invokeMap) | ❌ |
|
||||||
| [keyBy](https://lodash.com/docs/4.17.15#keyBy) | 📝 |
|
| [keyBy](https://lodash.com/docs/4.17.15#keyBy) | 📝 |
|
||||||
| [map](https://lodash.com/docs/4.17.15#map) | ❌ |
|
| [map](https://lodash.com/docs/4.17.15#map) | ❌ |
|
||||||
| [orderBy](https://lodash.com/docs/4.17.15#orderBy) | 📝 |
|
| [orderBy](https://lodash.com/docs/4.17.15#orderBy) | ✅ |
|
||||||
| [partition](https://lodash.com/docs/4.17.15#partition) | 📝 |
|
| [partition](https://lodash.com/docs/4.17.15#partition) | 📝 |
|
||||||
| [reduce](https://lodash.com/docs/4.17.15#reduce) | ❌ |
|
| [reduce](https://lodash.com/docs/4.17.15#reduce) | ❌ |
|
||||||
| [reduceRight](https://lodash.com/docs/4.17.15#reduceRight) | ❌ |
|
| [reduceRight](https://lodash.com/docs/4.17.15#reduceRight) | ❌ |
|
||||||
@ -226,7 +226,7 @@ chunk([1, 2, 3, 4], 0);
|
|||||||
| [isSafeInteger](https://lodash.com/docs/4.17.15#isSafeInteger) | ❌ |
|
| [isSafeInteger](https://lodash.com/docs/4.17.15#isSafeInteger) | ❌ |
|
||||||
| [isSet](https://lodash.com/docs/4.17.15#isSet) | ❌ |
|
| [isSet](https://lodash.com/docs/4.17.15#isSet) | ❌ |
|
||||||
| [isString](https://lodash.com/docs/4.17.15#isString) | ❌ |
|
| [isString](https://lodash.com/docs/4.17.15#isString) | ❌ |
|
||||||
| [isSymbol](https://lodash.com/docs/4.17.15#isSymbol) | ❌ |
|
| [isSymbol](https://lodash.com/docs/4.17.15#isSymbol) | ✅ |
|
||||||
| [isTypedArray](https://lodash.com/docs/4.17.15#isTypedArray) | ✅ |
|
| [isTypedArray](https://lodash.com/docs/4.17.15#isTypedArray) | ✅ |
|
||||||
| [isUndefined](https://lodash.com/docs/4.17.15#isUndefined) | ✅ |
|
| [isUndefined](https://lodash.com/docs/4.17.15#isUndefined) | ✅ |
|
||||||
| [isWeakMap](https://lodash.com/docs/4.17.15#isWeakMap) | ❌ |
|
| [isWeakMap](https://lodash.com/docs/4.17.15#isWeakMap) | ❌ |
|
||||||
|
24
src/compat/_internal/getPath.spec.ts
Normal file
24
src/compat/_internal/getPath.spec.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { getPath } from './getPath';
|
||||||
|
|
||||||
|
describe('getPath function', () => {
|
||||||
|
it('should return a property name from a string', () => {
|
||||||
|
expect(getPath('a', { a: 1 })).toBe('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an array of property names from a deep path string', () => {
|
||||||
|
expect(getPath('a.b.c', { a: { b: { c: 1 } } })).toEqual(['a', 'b', 'c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an array of property names from a string array', () => {
|
||||||
|
expect(getPath(['a', 'b', 'c'], { a: { b: { c: 1 } } })).toEqual(['a', 'b', 'c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an array of property names from a string array with a nested string', () => {
|
||||||
|
expect(getPath(['a', 'b.c', 'd'], { a: { b: { c: { d: 1 } } } })).toEqual(['a', 'b', 'c', 'd']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an array of property names from a deep path like string array', () => {
|
||||||
|
expect(getPath(['a.b.c', 'd'], { 'a.b.c': { d: 1 } })).toEqual(['a.b.c', 'd']);
|
||||||
|
});
|
||||||
|
});
|
35
src/compat/_internal/getPath.ts
Normal file
35
src/compat/_internal/getPath.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { isKey } from './isKey';
|
||||||
|
import { toPath } from './toPath';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the `path` (property name) from the `key` (property name or property path).
|
||||||
|
*
|
||||||
|
* @param {string | string[]} key - The `key` (property name or property path) to convert.
|
||||||
|
* @param {object} object - The object to query.
|
||||||
|
* @returns {string | string[]} The converted key (only property name).
|
||||||
|
*/
|
||||||
|
export function getPath(key: string | string[], object: object): string | string[] {
|
||||||
|
if (Array.isArray(key)) {
|
||||||
|
const path = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < key.length; i++) {
|
||||||
|
const k = key[i];
|
||||||
|
|
||||||
|
if (isKey(k, object)) {
|
||||||
|
object = object[k as keyof typeof object];
|
||||||
|
path.push(k);
|
||||||
|
} else {
|
||||||
|
const keys = toPath(k);
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
object = object[keys[i] as keyof typeof object];
|
||||||
|
path.push(keys[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isKey(key, object) ? key : toPath(key);
|
||||||
|
}
|
50
src/compat/_internal/isKey.spec.ts
Normal file
50
src/compat/_internal/isKey.spec.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { isKey } from './isKey';
|
||||||
|
|
||||||
|
describe('isKey', () => {
|
||||||
|
it('should return `true` for property names', () => {
|
||||||
|
expect(isKey('a')).toBe(true);
|
||||||
|
expect(isKey(false)).toBe(true);
|
||||||
|
expect(isKey(1)).toBe(true);
|
||||||
|
expect(isKey(null)).toBe(true);
|
||||||
|
expect(isKey(undefined)).toBe(true);
|
||||||
|
expect(isKey(-1.1)).toBe(true);
|
||||||
|
expect(isKey(Symbol.iterator)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return `false` for property paths', () => {
|
||||||
|
expect(isKey('a.b')).toBe(false);
|
||||||
|
expect(isKey('a[0]')).toBe(false);
|
||||||
|
expect(isKey('a["b"]')).toBe(false);
|
||||||
|
expect(isKey("a['b']")).toBe(false);
|
||||||
|
expect(isKey('a[0].b')).toBe(false);
|
||||||
|
expect(isKey('a[0]["b"]')).toBe(false);
|
||||||
|
expect(isKey("a[0]['b']")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return `true` for property paths that are in the object', () => {
|
||||||
|
expect(isKey('a', { a: 1 })).toBe(true);
|
||||||
|
expect(isKey('a.b', { 'a.b': 2 })).toBe(true);
|
||||||
|
expect(isKey('a[0]', { 'a[0]': 3 })).toBe(true);
|
||||||
|
expect(isKey('a["b"]', { 'a["b"]': 4 })).toBe(true);
|
||||||
|
expect(isKey('a[0].b', { 'a[0].b': 5 })).toBe(true);
|
||||||
|
expect(isKey("a[0]['b']", { "a[0]['b']": 6 })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return `false` for arrays', () => {
|
||||||
|
expect(isKey([])).toBe(false);
|
||||||
|
expect(isKey([1])).toBe(false);
|
||||||
|
expect(isKey([1, 2])).toBe(false);
|
||||||
|
expect(isKey([1, 2, 3])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for empty string', () => {
|
||||||
|
expect(isKey('')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for non-word characters', () => {
|
||||||
|
expect(isKey('^')).toBe(true);
|
||||||
|
expect(isKey('!')).toBe(true);
|
||||||
|
expect(isKey('@')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
34
src/compat/_internal/isKey.ts
Normal file
34
src/compat/_internal/isKey.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { isSymbol } from '../predicate/isSymbol';
|
||||||
|
|
||||||
|
/** Matches any deep property path. (e.g. `a.b[0].c`)*/
|
||||||
|
const regexIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/;
|
||||||
|
/** Matches any word character (alphanumeric & underscore).*/
|
||||||
|
const regexIsPlainProp = /^\w*$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if `value` is a property name and not a property path. (It's ok that the `value` is not in the keys of the `object`)
|
||||||
|
* @param {unknown} value The value to check.
|
||||||
|
* @param {unknown} object The object to query.
|
||||||
|
* @returns {boolean} Returns `true` if `value` is a property name, else `false`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* isKey('a', { a: 1 });
|
||||||
|
* // => true
|
||||||
|
*
|
||||||
|
* isKey('a.b', { a: { b: 2 } });
|
||||||
|
* // => false
|
||||||
|
*/
|
||||||
|
export function isKey(value?: unknown, object?: unknown): boolean {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'number' || typeof value === 'boolean' || value == null || isSymbol(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
(typeof value === 'string' && (regexIsPlainProp.test(value) || !regexIsDeepProp.test(value))) ||
|
||||||
|
(object != null && Object.hasOwn(object, value as PropertyKey))
|
||||||
|
);
|
||||||
|
}
|
83
src/compat/array/orderBy.spec.ts
Normal file
83
src/compat/array/orderBy.spec.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { orderBy } from './orderBy.ts';
|
||||||
|
import { falsey } from '../_internal/falsey.ts';
|
||||||
|
|
||||||
|
describe('orderBy', () => {
|
||||||
|
const objects = [
|
||||||
|
{ a: 'x', b: 3 },
|
||||||
|
{ a: 'y', b: 4 },
|
||||||
|
{ a: 'x', b: 1 },
|
||||||
|
{ a: 'y', b: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const nestedObj = [
|
||||||
|
{ id: '4', address: { zipCode: 4, streetName: 'Beta' } },
|
||||||
|
{ id: '3', address: { zipCode: 3, streetName: 'Alpha' } },
|
||||||
|
{ id: '1', address: { zipCode: 1, streetName: 'Alpha' } },
|
||||||
|
{ id: '2', address: { zipCode: 2, streetName: 'Alpha' } },
|
||||||
|
{ id: '5', address: { zipCode: 4, streetName: 'Alpha' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should return an empty array when the collection is null or undefined', () => {
|
||||||
|
const actual = [null, undefined].map(value => orderBy(value));
|
||||||
|
const expected = [[], []];
|
||||||
|
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a shallow copy of the collection when no keys are provided', () => {
|
||||||
|
const actual = orderBy(objects);
|
||||||
|
const expected = objects.slice();
|
||||||
|
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return ascending ordered collection when no orders are provided', () => {
|
||||||
|
const actual = orderBy(objects, ['a']);
|
||||||
|
const expected = orderBy(objects, ['a'], ['asc']);
|
||||||
|
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by a single property by a specified order', () => {
|
||||||
|
const actual = orderBy(objects, 'a', 'desc');
|
||||||
|
const expected = [objects[1], objects[3], objects[0], objects[2]];
|
||||||
|
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by nested key in array format', () => {
|
||||||
|
const actual = orderBy(nestedObj, [['address', 'zipCode'], ['address.streetName']], ['asc', 'desc']);
|
||||||
|
const expected = [nestedObj[2], nestedObj[3], nestedObj[1], nestedObj[0], nestedObj[4]];
|
||||||
|
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by multiple properties by specified orders', () => {
|
||||||
|
const actual = orderBy(objects, ['a', 'b'], ['desc', 'asc']);
|
||||||
|
const expected = [objects[3], objects[1], objects[2], objects[0]];
|
||||||
|
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by a property in ascending order when its order is not specified', () => {
|
||||||
|
const actual = orderBy(objects, ['a', 'b']);
|
||||||
|
const expected = [objects[2], objects[0], objects[3], objects[1]];
|
||||||
|
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort by a property in ascending order when its order is not specified and the collection is falsey', () => {
|
||||||
|
const actual = falsey.map((order, index) => orderBy(objects, ['a', 'b'], index ? ['desc', order] : ['desc']));
|
||||||
|
const expected = falsey.map(() => [objects[3], objects[1], objects[2], objects[0]]);
|
||||||
|
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with `orders` specified as string objects', () => {
|
||||||
|
const actual = orderBy(objects, ['a'], [Object('desc')]);
|
||||||
|
const expected = [objects[1], objects[3], objects[0], objects[2]];
|
||||||
|
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
99
src/compat/array/orderBy.ts
Normal file
99
src/compat/array/orderBy.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { getPath } from '../_internal/getPath';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts an array of objects based on multiple properties and their corresponding order directions.
|
||||||
|
*
|
||||||
|
* This function takes an array of objects, an array of keys to sort by, and an array of order directions.
|
||||||
|
* It returns the sorted array, ordering by each key according to its corresponding direction
|
||||||
|
* ('asc' for ascending or 'desc' for descending). If values for a key are equal,string
|
||||||
|
* it moves to the next key to determine the order.
|
||||||
|
*
|
||||||
|
* @template T - The type of elements in the array.
|
||||||
|
* @param {T[] | null} collection - The array of objects to be sorted.
|
||||||
|
* @param {string | Array<string | string[]>} keys - An array of keys (property names or property paths) to sort by.
|
||||||
|
* @param {unknown | unknown[]} orders - An array of order directions ('asc' for ascending or 'desc' for descending).
|
||||||
|
* @returns {T[]} - The sorted array.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Sort an array of objects by 'user' in ascending order and 'age' in descending order.
|
||||||
|
* const users = [
|
||||||
|
* { user: 'fred', age: 48 },
|
||||||
|
* { user: 'barney', age: 34 },
|
||||||
|
* { user: 'fred', age: 40 },
|
||||||
|
* { user: 'barney', age: 36 },
|
||||||
|
* ];
|
||||||
|
* const result = orderBy(users, ['user', 'age'], ['asc', 'desc']);
|
||||||
|
* // result will be:
|
||||||
|
* // [
|
||||||
|
* // { user: 'barney', age: 36 },
|
||||||
|
* // { user: 'barney', age: 34 },
|
||||||
|
* // { user: 'fred', age: 48 },
|
||||||
|
* // { user: 'fred', age: 40 },
|
||||||
|
* // ]
|
||||||
|
*/
|
||||||
|
export function orderBy<T extends object>(
|
||||||
|
collection?: T[] | null,
|
||||||
|
keys?: string | Array<string | string[]>,
|
||||||
|
orders?: unknown | unknown[]
|
||||||
|
): T[] {
|
||||||
|
if (collection == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(keys)) {
|
||||||
|
keys = keys == null ? [] : [keys];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(orders)) {
|
||||||
|
orders = orders == null ? [] : [orders];
|
||||||
|
}
|
||||||
|
|
||||||
|
const compareValues = <V>(a: V, b: V, order: string) => {
|
||||||
|
if (a < b) {
|
||||||
|
return order === 'desc' ? 1 : -1; // Default is ascending order
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a > b) {
|
||||||
|
return order === 'desc' ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getValueByPath = (key: string | string[], obj: T) => {
|
||||||
|
if (Array.isArray(key)) {
|
||||||
|
let value: object = obj;
|
||||||
|
|
||||||
|
for (let i = 0; i < key.length; i++) {
|
||||||
|
value = value[key[i] as keyof typeof value];
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj[key as keyof typeof obj];
|
||||||
|
};
|
||||||
|
|
||||||
|
keys = keys.map(key => getPath(key, collection[0]));
|
||||||
|
|
||||||
|
const shallowCopiedCollection = collection.slice();
|
||||||
|
const orderedCollection = shallowCopiedCollection.sort((a, b) => {
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const key = keys[i];
|
||||||
|
|
||||||
|
const valueA = getValueByPath(key, a);
|
||||||
|
const valueB = getValueByPath(key, b);
|
||||||
|
const order = String((orders as unknown[])[i]); // For Object('desc') case
|
||||||
|
|
||||||
|
const comparedResult = compareValues(valueA, valueB, order);
|
||||||
|
|
||||||
|
if (comparedResult !== 0) {
|
||||||
|
return comparedResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return orderedCollection;
|
||||||
|
}
|
@ -31,6 +31,7 @@ export { fill } from './array/fill.ts';
|
|||||||
export { flatten } from './array/flatten.ts';
|
export { flatten } from './array/flatten.ts';
|
||||||
export { flattenDeep } from './array/flattenDeep.ts';
|
export { flattenDeep } from './array/flattenDeep.ts';
|
||||||
export { flattenDepth } from './array/flattenDepth.ts';
|
export { flattenDepth } from './array/flattenDepth.ts';
|
||||||
|
export { orderBy } from './array/orderBy.ts';
|
||||||
export { size } from './array/size.ts';
|
export { size } from './array/size.ts';
|
||||||
export { zipObjectDeep } from './array/zipObjectDeep.ts';
|
export { zipObjectDeep } from './array/zipObjectDeep.ts';
|
||||||
export { head as first } from '../array/head.ts';
|
export { head as first } from '../array/head.ts';
|
||||||
@ -50,6 +51,7 @@ export { isPlainObject } from './predicate/isPlainObject.ts';
|
|||||||
export { isArray } from './predicate/isArray.ts';
|
export { isArray } from './predicate/isArray.ts';
|
||||||
export { isArguments } from './predicate/isArguments.ts';
|
export { isArguments } from './predicate/isArguments.ts';
|
||||||
export { isArrayLike } from './predicate/isArrayLike.ts';
|
export { isArrayLike } from './predicate/isArrayLike.ts';
|
||||||
|
export { isSymbol } from './predicate/isSymbol.ts';
|
||||||
export { isObjectLike } from './predicate/isObjectLike.ts';
|
export { isObjectLike } from './predicate/isObjectLike.ts';
|
||||||
export { isBoolean } from './predicate/isBoolean.ts';
|
export { isBoolean } from './predicate/isBoolean.ts';
|
||||||
export { isTypedArray } from './predicate/isTypedArray.ts';
|
export { isTypedArray } from './predicate/isTypedArray.ts';
|
||||||
|
31
src/compat/predicate/isSymbol.spec.ts
Normal file
31
src/compat/predicate/isSymbol.spec.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { isSymbol } from './isSymbol';
|
||||||
|
import { falsey } from '../_internal/falsey';
|
||||||
|
import { stubFalse } from '../_internal/stubFalse';
|
||||||
|
import { args } from '../_internal/args';
|
||||||
|
import { slice } from '../_internal/slice';
|
||||||
|
|
||||||
|
describe('isSymbol', () => {
|
||||||
|
it('should return `true` for symbols', () => {
|
||||||
|
expect(isSymbol(Symbol('a'))).toBe(true);
|
||||||
|
expect(isSymbol(Object(Symbol('a')))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return `false` for non-symbols', () => {
|
||||||
|
const expected = falsey.map(stubFalse);
|
||||||
|
const actual = falsey.map(isSymbol);
|
||||||
|
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
|
||||||
|
expect(isSymbol(args)).toBe(false);
|
||||||
|
expect(isSymbol([1, 2, 3])).toBe(false);
|
||||||
|
expect(isSymbol(true)).toBe(false);
|
||||||
|
expect(isSymbol(new Date())).toBe(false);
|
||||||
|
expect(isSymbol(new Error())).toBe(false);
|
||||||
|
expect(isSymbol(slice)).toBe(false);
|
||||||
|
expect(isSymbol({ 0: 1, length: 1 })).toBe(false);
|
||||||
|
expect(isSymbol(1)).toBe(false);
|
||||||
|
expect(isSymbol(/x/)).toBe(false);
|
||||||
|
expect(isSymbol('a')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
17
src/compat/predicate/isSymbol.ts
Normal file
17
src/compat/predicate/isSymbol.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Check whether a value is a symbol.
|
||||||
|
*
|
||||||
|
* This function can also serve as a type predicate in TypeScript, narrowing the type of the argument to `symbol`.
|
||||||
|
*
|
||||||
|
* @param {unknown} value The value to check.
|
||||||
|
* @returns {value is symbol} Returns `true` if `value` is a symbol, else `false`.
|
||||||
|
* @example
|
||||||
|
* isSymbol(Symbol.iterator);
|
||||||
|
* // => true
|
||||||
|
*
|
||||||
|
* isSymbol('abc');
|
||||||
|
* // => false
|
||||||
|
*/
|
||||||
|
export function isSymbol(value?: unknown): value is symbol {
|
||||||
|
return typeof value === 'symbol' || (value != null && value instanceof Symbol);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user