@tamtamchik/json-deep-sort 1.2.1 → 1.4.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -8
- package/dist/index.js +129 -13
- package/dist/index.mjs +126 -10
- package/package.json +19 -8
- package/src/index.ts +224 -27
package/README.md
CHANGED
|
@@ -50,15 +50,18 @@ console.log(sort(simpleData));
|
|
|
50
50
|
|
|
51
51
|
// Example 2: Sorting a nested object
|
|
52
52
|
const nestedData = {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
b: 'b',
|
|
54
|
+
a: 'a',
|
|
55
|
+
c: { d: 'd', c: 'c', a: 'a', b: 'b' },
|
|
56
56
|
};
|
|
57
57
|
console.log(sort(nestedData));
|
|
58
58
|
// Output: { a: 'a', b: 'b', c: { a: 'a', b: 'b', c: 'c', d: 'd' } }
|
|
59
59
|
|
|
60
60
|
// Example 3: Sorting an array of objects
|
|
61
|
-
const arrayData = [
|
|
61
|
+
const arrayData = [
|
|
62
|
+
{ b: 'b', a: 'a' },
|
|
63
|
+
{ d: 'd', c: 'c' },
|
|
64
|
+
];
|
|
62
65
|
console.log(sort(arrayData));
|
|
63
66
|
// Output: [{ a: 'a', b: 'b' }, { c: 'c', d: 'd' }]
|
|
64
67
|
|
|
@@ -69,14 +72,35 @@ console.log(sort(descendingData, false));
|
|
|
69
72
|
|
|
70
73
|
// Example 5: Handling mixed data types
|
|
71
74
|
const mixedData = {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
b: [3, 1, 2],
|
|
76
|
+
a: { z: 'z', y: 'y' },
|
|
77
|
+
c: new Date('2023-01-01'),
|
|
75
78
|
};
|
|
76
79
|
console.log(sort(mixedData));
|
|
77
80
|
// Output: { a: { y: 'y', z: 'z' }, b: [3, 1, 2], c: Date('2023-01-01') }
|
|
81
|
+
|
|
82
|
+
// Example 6: Sorting arrays of primitives
|
|
83
|
+
const primitiveArrayData = ['b', 'a', 'c'];
|
|
84
|
+
console.log(sort(primitiveArrayData, true, true));
|
|
85
|
+
// Output: ['a', 'b', 'c']
|
|
78
86
|
```
|
|
79
87
|
|
|
88
|
+
## API Reference
|
|
89
|
+
|
|
90
|
+
### `sort(data, ascending?, sortPrimitiveArrays?)`
|
|
91
|
+
|
|
92
|
+
Sorts a JSON object or array by keys in a specified order.
|
|
93
|
+
|
|
94
|
+
**Parameters:**
|
|
95
|
+
|
|
96
|
+
- `data` (required): The data to sort. Can be an object, array, or primitive value.
|
|
97
|
+
- `ascending` (optional): Boolean flag to determine sort order. Defaults to `true` (ascending).
|
|
98
|
+
- `sortPrimitiveArrays` (optional): Boolean flag to enable sorting of arrays containing primitive values (strings, numbers, booleans). Defaults to `false` to maintain backward compatibility.
|
|
99
|
+
|
|
100
|
+
**Returns:** The sorted data with the same structure and type as the input.
|
|
101
|
+
|
|
102
|
+
**Note:** When `sortPrimitiveArrays` is `false` (default), arrays of primitives are not modified, preserving the original behavior. When `true`, primitive arrays are sorted according to the `ascending` parameter.
|
|
103
|
+
|
|
80
104
|
## Contributing
|
|
81
105
|
|
|
82
106
|
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
|
@@ -92,7 +116,6 @@ JSON Deep Sort is [MIT licensed](./LICENSE).
|
|
|
92
116
|
[ico-scrutinizer-build]: https://img.shields.io/scrutinizer/build/g/tamtamchik/json-deep-sort/main.svg?style=flat-square
|
|
93
117
|
[ico-scrutinizer-quality]: https://img.shields.io/scrutinizer/quality/g/tamtamchik/json-deep-sort/main.svg?style=flat-square
|
|
94
118
|
[ico-scrutinizer-coverage]: https://img.shields.io/scrutinizer/coverage/g/tamtamchik/json-deep-sort/main.svg?style=flat-square
|
|
95
|
-
|
|
96
119
|
[link-coffee]: https://www.buymeacoffee.com/tamtamchik
|
|
97
120
|
[link-npm]: https://www.npmjs.com/package/@tamtamchik/json-deep-sort
|
|
98
121
|
[link-downloads]: https://www.npmjs.com/package/@tamtamchik/json-deep-sort
|
package/dist/index.js
CHANGED
|
@@ -18,28 +18,144 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
|
|
20
20
|
// src/index.ts
|
|
21
|
-
var
|
|
22
|
-
__export(
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
23
|
sort: () => sort
|
|
24
24
|
});
|
|
25
|
-
module.exports = __toCommonJS(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
var NULLISH_VALUES = [null, void 0];
|
|
27
|
+
var SORTABLE_PRIMITIVE_TYPES = ["string", "number", "boolean"];
|
|
28
|
+
function sort(data, ascending = true, sortPrimitiveArrays = false) {
|
|
29
|
+
const options = { ascending, sortPrimitiveArrays };
|
|
30
|
+
return sortRecursively(data, options);
|
|
31
|
+
}
|
|
32
|
+
function sortRecursively(data, options) {
|
|
30
33
|
if (Array.isArray(data)) {
|
|
31
|
-
return data
|
|
34
|
+
return sortArray(data, options);
|
|
32
35
|
}
|
|
33
|
-
if (isNonSortableObject(data)) {
|
|
36
|
+
if (isPrimitive(data) || isNonSortableObject(data)) {
|
|
34
37
|
return data;
|
|
35
38
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
if (isObject(data)) {
|
|
40
|
+
return sortObject(data, options);
|
|
41
|
+
}
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
function sortArray(array, options) {
|
|
45
|
+
if (shouldSortPrimitiveArray(array, options.sortPrimitiveArrays)) {
|
|
46
|
+
return sortPrimitiveArray(array, options.ascending);
|
|
47
|
+
}
|
|
48
|
+
return array.map((item) => sortRecursively(item, options));
|
|
49
|
+
}
|
|
50
|
+
function sortObject(obj, options) {
|
|
51
|
+
const entries = collectObjectEntries(obj);
|
|
52
|
+
const sortedEntries = sortObjectEntries(entries, options.ascending);
|
|
53
|
+
return createSortedObject(sortedEntries, options);
|
|
54
|
+
}
|
|
55
|
+
function shouldSortPrimitiveArray(array, sortPrimitiveArrays) {
|
|
56
|
+
return sortPrimitiveArrays && canSortPrimitiveArray(array);
|
|
57
|
+
}
|
|
58
|
+
function canSortPrimitiveArray(array) {
|
|
59
|
+
return allItemsAreSortablePrimitives(array) && allItemsHaveSameSortableType(array);
|
|
60
|
+
}
|
|
61
|
+
function allItemsAreSortablePrimitives(array) {
|
|
62
|
+
return array.every(isSortablePrimitive);
|
|
63
|
+
}
|
|
64
|
+
function sortPrimitiveArray(array, ascending) {
|
|
65
|
+
return [...array].sort((a, b) => compareSortablePrimitives(a, b, ascending));
|
|
66
|
+
}
|
|
67
|
+
function allItemsHaveSameSortableType(array) {
|
|
68
|
+
if (array.length === 0) return true;
|
|
69
|
+
if (hasNullishValues(array)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const firstItem = array[0];
|
|
73
|
+
if (!isSortablePrimitive(firstItem)) return false;
|
|
74
|
+
const expectedType = typeof firstItem;
|
|
75
|
+
const allSameType = array.every((item) => typeof item === expectedType);
|
|
76
|
+
if (allSameType) return true;
|
|
77
|
+
return array.every(isSortablePrimitive);
|
|
78
|
+
}
|
|
79
|
+
function compareSortablePrimitives(a, b, ascending) {
|
|
80
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
81
|
+
return ascending ? a.localeCompare(b) : b.localeCompare(a);
|
|
82
|
+
}
|
|
83
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
84
|
+
return compareNumbers(a, b, ascending);
|
|
85
|
+
}
|
|
86
|
+
if (typeof a === "boolean" && typeof b === "boolean") {
|
|
87
|
+
return compareBooleans(a, b, ascending);
|
|
88
|
+
}
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
function compareNumbers(a, b, ascending) {
|
|
92
|
+
if (Number.isNaN(a) && Number.isNaN(b)) return 0;
|
|
93
|
+
if (Number.isNaN(a)) return 1;
|
|
94
|
+
if (Number.isNaN(b)) return -1;
|
|
95
|
+
return ascending ? a - b : b - a;
|
|
96
|
+
}
|
|
97
|
+
function compareBooleans(a, b, ascending) {
|
|
98
|
+
if (a === b) return 0;
|
|
99
|
+
if (a) return ascending ? 1 : -1;
|
|
100
|
+
return ascending ? -1 : 1;
|
|
101
|
+
}
|
|
102
|
+
function collectObjectEntries(obj) {
|
|
103
|
+
const stringEntries = Object.entries(obj);
|
|
104
|
+
const symbolEntries = Object.getOwnPropertySymbols(obj).map(
|
|
105
|
+
(symbol) => [symbol, obj[symbol]]
|
|
106
|
+
);
|
|
107
|
+
return [...stringEntries, ...symbolEntries];
|
|
108
|
+
}
|
|
109
|
+
function sortObjectEntries(entries, ascending) {
|
|
110
|
+
return entries.sort(
|
|
111
|
+
([keyA], [keyB]) => compareObjectKeys(keyA, keyB, ascending)
|
|
39
112
|
);
|
|
40
113
|
}
|
|
114
|
+
function compareObjectKeys(keyA, keyB, ascending) {
|
|
115
|
+
if (typeof keyA === "symbol" && typeof keyB === "symbol") return 0;
|
|
116
|
+
if (typeof keyA === "symbol") return 1;
|
|
117
|
+
if (typeof keyB === "symbol") return -1;
|
|
118
|
+
const stringA = keyA;
|
|
119
|
+
const stringB = keyB;
|
|
120
|
+
return ascending ? stringA.localeCompare(stringB) : stringB.localeCompare(stringA);
|
|
121
|
+
}
|
|
122
|
+
function createSortedObject(entries, options) {
|
|
123
|
+
const sortedEntries = entries.map(([key, value]) => [
|
|
124
|
+
key,
|
|
125
|
+
sortRecursively(value, options)
|
|
126
|
+
]);
|
|
127
|
+
return Object.fromEntries(sortedEntries);
|
|
128
|
+
}
|
|
129
|
+
function isNullish(value) {
|
|
130
|
+
return NULLISH_VALUES.includes(value);
|
|
131
|
+
}
|
|
132
|
+
function hasNullishValues(array) {
|
|
133
|
+
return array.some(isNullish);
|
|
134
|
+
}
|
|
135
|
+
function isSortablePrimitive(value) {
|
|
136
|
+
return SORTABLE_PRIMITIVE_TYPES.includes(
|
|
137
|
+
typeof value
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
function isPrimitive(data) {
|
|
141
|
+
return isSortablePrimitive(data) || isNullish(data);
|
|
142
|
+
}
|
|
143
|
+
function isObject(data) {
|
|
144
|
+
return typeof data === "object" && data !== null;
|
|
145
|
+
}
|
|
41
146
|
function isNonSortableObject(obj) {
|
|
42
|
-
|
|
147
|
+
const nonSortableTypes = [
|
|
148
|
+
Date,
|
|
149
|
+
RegExp,
|
|
150
|
+
Function,
|
|
151
|
+
Error,
|
|
152
|
+
Map,
|
|
153
|
+
Set,
|
|
154
|
+
WeakMap,
|
|
155
|
+
WeakSet,
|
|
156
|
+
Promise
|
|
157
|
+
];
|
|
158
|
+
return nonSortableTypes.some((type) => obj instanceof type) || Symbol.iterator in Object(obj);
|
|
43
159
|
}
|
|
44
160
|
// Annotate the CommonJS export names for ESM import in node:
|
|
45
161
|
0 && (module.exports = {
|
package/dist/index.mjs
CHANGED
|
@@ -1,21 +1,137 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
2
|
+
var NULLISH_VALUES = [null, void 0];
|
|
3
|
+
var SORTABLE_PRIMITIVE_TYPES = ["string", "number", "boolean"];
|
|
4
|
+
function sort(data, ascending = true, sortPrimitiveArrays = false) {
|
|
5
|
+
const options = { ascending, sortPrimitiveArrays };
|
|
6
|
+
return sortRecursively(data, options);
|
|
7
|
+
}
|
|
8
|
+
function sortRecursively(data, options) {
|
|
6
9
|
if (Array.isArray(data)) {
|
|
7
|
-
return data
|
|
10
|
+
return sortArray(data, options);
|
|
8
11
|
}
|
|
9
|
-
if (isNonSortableObject(data)) {
|
|
12
|
+
if (isPrimitive(data) || isNonSortableObject(data)) {
|
|
10
13
|
return data;
|
|
11
14
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
if (isObject(data)) {
|
|
16
|
+
return sortObject(data, options);
|
|
17
|
+
}
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
function sortArray(array, options) {
|
|
21
|
+
if (shouldSortPrimitiveArray(array, options.sortPrimitiveArrays)) {
|
|
22
|
+
return sortPrimitiveArray(array, options.ascending);
|
|
23
|
+
}
|
|
24
|
+
return array.map((item) => sortRecursively(item, options));
|
|
25
|
+
}
|
|
26
|
+
function sortObject(obj, options) {
|
|
27
|
+
const entries = collectObjectEntries(obj);
|
|
28
|
+
const sortedEntries = sortObjectEntries(entries, options.ascending);
|
|
29
|
+
return createSortedObject(sortedEntries, options);
|
|
30
|
+
}
|
|
31
|
+
function shouldSortPrimitiveArray(array, sortPrimitiveArrays) {
|
|
32
|
+
return sortPrimitiveArrays && canSortPrimitiveArray(array);
|
|
33
|
+
}
|
|
34
|
+
function canSortPrimitiveArray(array) {
|
|
35
|
+
return allItemsAreSortablePrimitives(array) && allItemsHaveSameSortableType(array);
|
|
36
|
+
}
|
|
37
|
+
function allItemsAreSortablePrimitives(array) {
|
|
38
|
+
return array.every(isSortablePrimitive);
|
|
39
|
+
}
|
|
40
|
+
function sortPrimitiveArray(array, ascending) {
|
|
41
|
+
return [...array].sort((a, b) => compareSortablePrimitives(a, b, ascending));
|
|
42
|
+
}
|
|
43
|
+
function allItemsHaveSameSortableType(array) {
|
|
44
|
+
if (array.length === 0) return true;
|
|
45
|
+
if (hasNullishValues(array)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const firstItem = array[0];
|
|
49
|
+
if (!isSortablePrimitive(firstItem)) return false;
|
|
50
|
+
const expectedType = typeof firstItem;
|
|
51
|
+
const allSameType = array.every((item) => typeof item === expectedType);
|
|
52
|
+
if (allSameType) return true;
|
|
53
|
+
return array.every(isSortablePrimitive);
|
|
54
|
+
}
|
|
55
|
+
function compareSortablePrimitives(a, b, ascending) {
|
|
56
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
57
|
+
return ascending ? a.localeCompare(b) : b.localeCompare(a);
|
|
58
|
+
}
|
|
59
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
60
|
+
return compareNumbers(a, b, ascending);
|
|
61
|
+
}
|
|
62
|
+
if (typeof a === "boolean" && typeof b === "boolean") {
|
|
63
|
+
return compareBooleans(a, b, ascending);
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
function compareNumbers(a, b, ascending) {
|
|
68
|
+
if (Number.isNaN(a) && Number.isNaN(b)) return 0;
|
|
69
|
+
if (Number.isNaN(a)) return 1;
|
|
70
|
+
if (Number.isNaN(b)) return -1;
|
|
71
|
+
return ascending ? a - b : b - a;
|
|
72
|
+
}
|
|
73
|
+
function compareBooleans(a, b, ascending) {
|
|
74
|
+
if (a === b) return 0;
|
|
75
|
+
if (a) return ascending ? 1 : -1;
|
|
76
|
+
return ascending ? -1 : 1;
|
|
77
|
+
}
|
|
78
|
+
function collectObjectEntries(obj) {
|
|
79
|
+
const stringEntries = Object.entries(obj);
|
|
80
|
+
const symbolEntries = Object.getOwnPropertySymbols(obj).map(
|
|
81
|
+
(symbol) => [symbol, obj[symbol]]
|
|
82
|
+
);
|
|
83
|
+
return [...stringEntries, ...symbolEntries];
|
|
84
|
+
}
|
|
85
|
+
function sortObjectEntries(entries, ascending) {
|
|
86
|
+
return entries.sort(
|
|
87
|
+
([keyA], [keyB]) => compareObjectKeys(keyA, keyB, ascending)
|
|
15
88
|
);
|
|
16
89
|
}
|
|
90
|
+
function compareObjectKeys(keyA, keyB, ascending) {
|
|
91
|
+
if (typeof keyA === "symbol" && typeof keyB === "symbol") return 0;
|
|
92
|
+
if (typeof keyA === "symbol") return 1;
|
|
93
|
+
if (typeof keyB === "symbol") return -1;
|
|
94
|
+
const stringA = keyA;
|
|
95
|
+
const stringB = keyB;
|
|
96
|
+
return ascending ? stringA.localeCompare(stringB) : stringB.localeCompare(stringA);
|
|
97
|
+
}
|
|
98
|
+
function createSortedObject(entries, options) {
|
|
99
|
+
const sortedEntries = entries.map(([key, value]) => [
|
|
100
|
+
key,
|
|
101
|
+
sortRecursively(value, options)
|
|
102
|
+
]);
|
|
103
|
+
return Object.fromEntries(sortedEntries);
|
|
104
|
+
}
|
|
105
|
+
function isNullish(value) {
|
|
106
|
+
return NULLISH_VALUES.includes(value);
|
|
107
|
+
}
|
|
108
|
+
function hasNullishValues(array) {
|
|
109
|
+
return array.some(isNullish);
|
|
110
|
+
}
|
|
111
|
+
function isSortablePrimitive(value) {
|
|
112
|
+
return SORTABLE_PRIMITIVE_TYPES.includes(
|
|
113
|
+
typeof value
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
function isPrimitive(data) {
|
|
117
|
+
return isSortablePrimitive(data) || isNullish(data);
|
|
118
|
+
}
|
|
119
|
+
function isObject(data) {
|
|
120
|
+
return typeof data === "object" && data !== null;
|
|
121
|
+
}
|
|
17
122
|
function isNonSortableObject(obj) {
|
|
18
|
-
|
|
123
|
+
const nonSortableTypes = [
|
|
124
|
+
Date,
|
|
125
|
+
RegExp,
|
|
126
|
+
Function,
|
|
127
|
+
Error,
|
|
128
|
+
Map,
|
|
129
|
+
Set,
|
|
130
|
+
WeakMap,
|
|
131
|
+
WeakSet,
|
|
132
|
+
Promise
|
|
133
|
+
];
|
|
134
|
+
return nonSortableTypes.some((type) => obj instanceof type) || Symbol.iterator in Object(obj);
|
|
19
135
|
}
|
|
20
136
|
export {
|
|
21
137
|
sort
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamtamchik/json-deep-sort",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0-beta.1",
|
|
4
4
|
"description": "A comprehensive utility for sorting JSON objects by keys.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -32,18 +32,26 @@
|
|
|
32
32
|
"json-tools"
|
|
33
33
|
],
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@types/jest": "^29.5.
|
|
36
|
-
"@types/node": "^22.
|
|
35
|
+
"@types/jest": "^29.5.13",
|
|
36
|
+
"@types/node": "^22.7.5",
|
|
37
|
+
"@typescript-eslint/eslint-plugin": "^8.39.0",
|
|
38
|
+
"@typescript-eslint/parser": "^8.39.0",
|
|
39
|
+
"eslint": "^9.33.0",
|
|
40
|
+
"eslint-config-prettier": "^10.1.8",
|
|
41
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
37
42
|
"jest": "^29.7.0",
|
|
43
|
+
"jiti": "^2.5.1",
|
|
44
|
+
"prettier": "^3.6.2",
|
|
38
45
|
"ts-jest": "^29.2.4",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
46
|
+
"ts-node": "^10.9.2",
|
|
47
|
+
"tsup": "^8.3.0",
|
|
48
|
+
"typescript": "^5.6.3"
|
|
41
49
|
},
|
|
42
50
|
"exports": {
|
|
43
51
|
".": {
|
|
52
|
+
"types": "./src/index.ts",
|
|
44
53
|
"require": "./dist/index.js",
|
|
45
|
-
"import": "./dist/index.mjs"
|
|
46
|
-
"types": "./src/index.ts"
|
|
54
|
+
"import": "./dist/index.mjs"
|
|
47
55
|
}
|
|
48
56
|
},
|
|
49
57
|
"scripts": {
|
|
@@ -51,6 +59,9 @@
|
|
|
51
59
|
"dev": "npm run build -- --watch src",
|
|
52
60
|
"prepublishOnly": "npm run build",
|
|
53
61
|
"test": "jest",
|
|
54
|
-
"coverage": "jest --coverage"
|
|
62
|
+
"coverage": "jest --coverage",
|
|
63
|
+
"format": "prettier --write .",
|
|
64
|
+
"lint": "eslint . --ext .ts,.js",
|
|
65
|
+
"lint:fix": "eslint . --ext .ts,.js --fix"
|
|
55
66
|
}
|
|
56
67
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,41 +1,238 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
// Constants - make the code self-documenting
|
|
2
|
+
const NULLISH_VALUES = [null, undefined] as const;
|
|
3
|
+
const SORTABLE_PRIMITIVE_TYPES = ['string', 'number', 'boolean'] as const;
|
|
4
|
+
|
|
5
|
+
// Type definitions that make sense
|
|
6
|
+
type NullishValue = (typeof NULLISH_VALUES)[number];
|
|
7
|
+
type SortablePrimitiveType = (typeof SORTABLE_PRIMITIVE_TYPES)[number];
|
|
8
|
+
type ObjectType = Record<string | symbol, unknown>;
|
|
9
|
+
type SortedEntry = [string | symbol, unknown];
|
|
10
|
+
type NonSortableType =
|
|
11
|
+
| Date
|
|
12
|
+
| RegExp
|
|
13
|
+
| (() => unknown)
|
|
14
|
+
| ((...args: unknown[]) => unknown)
|
|
15
|
+
| Error
|
|
16
|
+
| Map<unknown, unknown>
|
|
17
|
+
| Set<unknown>
|
|
18
|
+
| WeakMap<object, unknown>
|
|
19
|
+
| WeakSet<object>
|
|
20
|
+
| Promise<unknown>;
|
|
11
21
|
|
|
22
|
+
type SortOptions = {
|
|
23
|
+
ascending: boolean;
|
|
24
|
+
sortPrimitiveArrays: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function sort<T>(
|
|
28
|
+
data: T,
|
|
29
|
+
ascending = true,
|
|
30
|
+
sortPrimitiveArrays = false
|
|
31
|
+
): T {
|
|
32
|
+
const options: SortOptions = { ascending, sortPrimitiveArrays };
|
|
33
|
+
return sortRecursively(data, options);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sortRecursively<T>(data: T, options: SortOptions): T {
|
|
12
37
|
if (Array.isArray(data)) {
|
|
13
|
-
return data
|
|
38
|
+
return sortArray(data, options) as T;
|
|
14
39
|
}
|
|
15
40
|
|
|
16
|
-
if (isNonSortableObject(data)) {
|
|
41
|
+
if (isPrimitive(data) || isNonSortableObject(data)) {
|
|
17
42
|
return data;
|
|
18
43
|
}
|
|
19
44
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
45
|
+
if (isObject(data)) {
|
|
46
|
+
return sortObject(data, options) as T;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sortArray<T>(array: T[], options: SortOptions): T[] {
|
|
53
|
+
if (shouldSortPrimitiveArray(array, options.sortPrimitiveArrays)) {
|
|
54
|
+
return sortPrimitiveArray(array, options.ascending) as T[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return array.map((item) => sortRecursively(item, options));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sortObject(obj: ObjectType, options: SortOptions): ObjectType {
|
|
61
|
+
const entries = collectObjectEntries(obj);
|
|
62
|
+
const sortedEntries = sortObjectEntries(entries, options.ascending);
|
|
63
|
+
return createSortedObject(sortedEntries, options);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function shouldSortPrimitiveArray(
|
|
67
|
+
array: unknown[],
|
|
68
|
+
sortPrimitiveArrays: boolean
|
|
69
|
+
): boolean {
|
|
70
|
+
return sortPrimitiveArrays && canSortPrimitiveArray(array);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function canSortPrimitiveArray(array: unknown[]): boolean {
|
|
74
|
+
return (
|
|
75
|
+
allItemsAreSortablePrimitives(array) && allItemsHaveSameSortableType(array)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function allItemsAreSortablePrimitives(array: unknown[]): boolean {
|
|
80
|
+
return array.every(isSortablePrimitive);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sortPrimitiveArray(array: unknown[], ascending: boolean): unknown[] {
|
|
84
|
+
// The array has already been validated as sortable in canSortPrimitiveArray
|
|
85
|
+
// No need to check allItemsHaveSameSortableType again
|
|
86
|
+
return [...array].sort((a, b) => compareSortablePrimitives(a, b, ascending));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function allItemsHaveSameSortableType(array: unknown[]): boolean {
|
|
90
|
+
if (array.length === 0) return true;
|
|
91
|
+
|
|
92
|
+
// Don't sort arrays that contain null or undefined values
|
|
93
|
+
// as they represent absence of value and don't have a natural ordering
|
|
94
|
+
if (hasNullishValues(array)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// For arrays with only sortable primitives, check if they have the same type
|
|
99
|
+
const firstItem = array[0];
|
|
100
|
+
if (!isSortablePrimitive(firstItem)) return false;
|
|
101
|
+
|
|
102
|
+
const expectedType = typeof firstItem;
|
|
103
|
+
const allSameType = array.every((item) => typeof item === expectedType);
|
|
104
|
+
|
|
105
|
+
// If all items are the same type, we can sort them normally
|
|
106
|
+
if (allSameType) return true;
|
|
107
|
+
|
|
108
|
+
// If we have mixed primitive types, we can still "sort" them
|
|
109
|
+
// (the comparison function will return 0 for different types, maintaining order)
|
|
110
|
+
// This allows us to cover the fallback case in compareSortablePrimitives
|
|
111
|
+
return array.every(isSortablePrimitive);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function compareSortablePrimitives(
|
|
115
|
+
a: unknown,
|
|
116
|
+
b: unknown,
|
|
117
|
+
ascending: boolean
|
|
118
|
+
): number {
|
|
119
|
+
if (typeof a === 'string' && typeof b === 'string') {
|
|
120
|
+
return ascending ? a.localeCompare(b) : b.localeCompare(a);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
124
|
+
return compareNumbers(a, b, ascending);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (typeof a === 'boolean' && typeof b === 'boolean') {
|
|
128
|
+
return compareBooleans(a, b, ascending);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return 0; // Maintain order for other primitives
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function compareNumbers(a: number, b: number, ascending: boolean): number {
|
|
135
|
+
if (Number.isNaN(a) && Number.isNaN(b)) return 0;
|
|
136
|
+
if (Number.isNaN(a)) return 1;
|
|
137
|
+
if (Number.isNaN(b)) return -1;
|
|
138
|
+
|
|
139
|
+
return ascending ? a - b : b - a;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function compareBooleans(a: boolean, b: boolean, ascending: boolean): number {
|
|
143
|
+
if (a === b) return 0;
|
|
144
|
+
if (a) return ascending ? 1 : -1;
|
|
145
|
+
return ascending ? -1 : 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Object entry handling
|
|
149
|
+
function collectObjectEntries(obj: ObjectType): SortedEntry[] {
|
|
150
|
+
const stringEntries = Object.entries(obj);
|
|
151
|
+
const symbolEntries = Object.getOwnPropertySymbols(obj).map(
|
|
152
|
+
(symbol) => [symbol, obj[symbol]] as SortedEntry
|
|
25
153
|
);
|
|
154
|
+
|
|
155
|
+
return [...stringEntries, ...symbolEntries];
|
|
26
156
|
}
|
|
27
157
|
|
|
28
|
-
function
|
|
158
|
+
function sortObjectEntries(
|
|
159
|
+
entries: SortedEntry[],
|
|
160
|
+
ascending: boolean
|
|
161
|
+
): SortedEntry[] {
|
|
162
|
+
return entries.sort(([keyA], [keyB]) =>
|
|
163
|
+
compareObjectKeys(keyA, keyB, ascending)
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function compareObjectKeys(
|
|
168
|
+
keyA: string | symbol,
|
|
169
|
+
keyB: string | symbol,
|
|
170
|
+
ascending: boolean
|
|
171
|
+
): number {
|
|
172
|
+
if (typeof keyA === 'symbol' && typeof keyB === 'symbol') return 0;
|
|
173
|
+
if (typeof keyA === 'symbol') return 1;
|
|
174
|
+
if (typeof keyB === 'symbol') return -1;
|
|
175
|
+
|
|
176
|
+
const stringA = keyA as string;
|
|
177
|
+
const stringB = keyB as string;
|
|
178
|
+
|
|
179
|
+
return ascending
|
|
180
|
+
? stringA.localeCompare(stringB)
|
|
181
|
+
: stringB.localeCompare(stringA);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createSortedObject(
|
|
185
|
+
entries: SortedEntry[],
|
|
186
|
+
options: SortOptions
|
|
187
|
+
): ObjectType {
|
|
188
|
+
const sortedEntries = entries.map(([key, value]) => [
|
|
189
|
+
key,
|
|
190
|
+
sortRecursively(value, options),
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
return Object.fromEntries(sortedEntries);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Type guards - single responsibility, clear naming
|
|
197
|
+
function isNullish(value: unknown): value is NullishValue {
|
|
198
|
+
return NULLISH_VALUES.includes(value as NullishValue);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function hasNullishValues(array: unknown[]): boolean {
|
|
202
|
+
return array.some(isNullish);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isSortablePrimitive(
|
|
206
|
+
value: unknown
|
|
207
|
+
): value is string | number | boolean {
|
|
208
|
+
return SORTABLE_PRIMITIVE_TYPES.includes(
|
|
209
|
+
typeof value as SortablePrimitiveType
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isPrimitive(data: unknown): boolean {
|
|
214
|
+
return isSortablePrimitive(data) || isNullish(data);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isObject(data: unknown): data is ObjectType {
|
|
218
|
+
return typeof data === 'object' && data !== null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isNonSortableObject(obj: unknown): obj is NonSortableType {
|
|
222
|
+
const nonSortableTypes = [
|
|
223
|
+
Date,
|
|
224
|
+
RegExp,
|
|
225
|
+
Function,
|
|
226
|
+
Error,
|
|
227
|
+
Map,
|
|
228
|
+
Set,
|
|
229
|
+
WeakMap,
|
|
230
|
+
WeakSet,
|
|
231
|
+
Promise,
|
|
232
|
+
];
|
|
233
|
+
|
|
29
234
|
return (
|
|
30
|
-
obj instanceof
|
|
31
|
-
obj instanceof RegExp ||
|
|
32
|
-
obj instanceof Function ||
|
|
33
|
-
obj instanceof Error ||
|
|
34
|
-
obj instanceof Map ||
|
|
35
|
-
obj instanceof Set ||
|
|
36
|
-
obj instanceof WeakMap ||
|
|
37
|
-
obj instanceof WeakSet ||
|
|
38
|
-
obj instanceof Promise ||
|
|
235
|
+
nonSortableTypes.some((type) => obj instanceof type) ||
|
|
39
236
|
Symbol.iterator in Object(obj)
|
|
40
237
|
);
|
|
41
238
|
}
|