Passed
Pull Request — main (#3)
by Yuri
03:02 queued 01:35
created

index.ts ➔ compareObjectKeys   A

Complexity

Conditions 5

Size

Total Lines 16
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 15
dl 0
loc 16
ccs 6
cts 6
cp 1
crap 5
rs 9.1832
c 0
b 0
f 0
1
// Type definitions
2
type ObjectType = Record<string | symbol, unknown>;
3
type SortedEntry = [string | symbol, unknown];
4
type NonSortableType =
5
  | Date
6
  | RegExp
7
  | (() => unknown)
8
  | ((...args: unknown[]) => unknown)
9
  | Error
10
  | Map<unknown, unknown>
11
  | Set<unknown>
12
  | WeakMap<object, unknown>
13
  | WeakSet<object>
14
  | Promise<unknown>;
15
16
type SortOptions = {
17
  ascending: boolean;
18
  sortPrimitiveArrays: boolean;
19
};
20
21
// Main public API
22 1
export function sort<T>(
23
  data: T,
24
  ascending = true,
25
  sortPrimitiveArrays = false
26
): T {
27 44
  const options: SortOptions = { ascending, sortPrimitiveArrays };
28 44
  return sortRecursively(data, options);
29
}
30
31
// Core recursive sorting logic
32
function sortRecursively<T>(data: T, options: SortOptions): T {
33 199
  if (isPrimitive(data)) {
34 90
    return data;
35
  }
36
37 109
  if (Array.isArray(data)) {
38 43
    return sortArray(data, options) as T;
39
  }
40
41 66
  if (isObject(data) && !isNonSortableObject(data)) {
42 44
    return sortObject(data, options) as T;
43
  }
44
45 22
  return data;
46
}
47
48
// Array sorting logic
49
function sortArray<T>(array: T[], options: SortOptions): T[] {
50 43
  if (shouldSortPrimitiveArray(array, options.sortPrimitiveArrays)) {
51 22
    return sortPrimitiveArray(array, options.ascending) as T[];
52
  }
53
54 52
  return array.map((item) => sortRecursively(item, options));
55
}
56
57
// Object sorting logic
58
function sortObject(obj: ObjectType, options: SortOptions): ObjectType {
59 44
  const entries = collectObjectEntries(obj);
60 44
  const sortedEntries = sortObjectEntries(entries, options.ascending);
61
62 44
  return createSortedObject(sortedEntries, options);
63
}
64
65
// Primitive array sorting
66
function shouldSortPrimitiveArray(
67
  array: unknown[],
68
  sortPrimitiveArrays: boolean
69
): boolean {
70 43
  return (
71
    sortPrimitiveArrays && array.length > 0 && allItemsArePrimitives(array)
72
  );
73
}
74
75
function allItemsArePrimitives(array: unknown[]): boolean {
76 85
  return array.every((item) => isPrimitive(item));
77
}
78
79
function sortPrimitiveArray(array: unknown[], ascending: boolean): unknown[] {
80 22
  if (!allItemsHaveSameType(array)) {
81 3
    return array; // Mixed types maintain original order
82
  }
83
84 80
  return [...array].sort((a, b) => comparePrimitives(a, b, ascending));
85
}
86
87
function allItemsHaveSameType(array: unknown[]): boolean {
88 22
  if (array.length === 0) return true;
89
90 22
  const firstType = typeof array[0];
91 75
  return array.every((item) => typeof item === firstType);
92
}
93
94
function comparePrimitives(a: unknown, b: unknown, ascending: boolean): number {
95 80
  if (typeof a === 'string' && typeof b === 'string') {
96 9
    return ascending ? a.localeCompare(b) : b.localeCompare(a);
97
  }
98
99 71
  if (typeof a === 'number' && typeof b === 'number') {
100 59
    return compareNumbers(a, b, ascending);
101
  }
102
103 12
  if (typeof a === 'boolean' && typeof b === 'boolean') {
104 10
    return compareBooleans(a, b, ascending);
105
  }
106
107 2
  return 0; // Maintain order for other primitives
108
}
109
110
function compareNumbers(a: number, b: number, ascending: boolean): number {
111 59
  if (Number.isNaN(a) && Number.isNaN(b)) return 0;
112 57
  if (Number.isNaN(a)) return 1;
113 53
  if (Number.isNaN(b)) return -1;
114
115 47
  return ascending ? a - b : b - a;
116
}
117
118
function compareBooleans(a: boolean, b: boolean, ascending: boolean): number {
119 10
  if (a === b) return 0;
120 6
  if (a) return ascending ? 1 : -1;
121 3
  return ascending ? -1 : 1;
122
}
123
124
// Object entry handling
125
function collectObjectEntries(obj: ObjectType): SortedEntry[] {
126 44
  const stringEntries = Object.entries(obj);
127 44
  const symbolEntries = Object.getOwnPropertySymbols(obj).map(
128 2
    (symbol) => [symbol, obj[symbol]] as SortedEntry
129
  );
130
131 44
  return [...stringEntries, ...symbolEntries];
132
}
133
134
function sortObjectEntries(
135
  entries: SortedEntry[],
136
  ascending: boolean
137
): SortedEntry[] {
138 44
  return entries.sort(([keyA], [keyB]) =>
139 81
    compareObjectKeys(keyA, keyB, ascending)
140
  );
141
}
142
143
function compareObjectKeys(
144
  keyA: string | symbol,
145
  keyB: string | symbol,
146
  ascending: boolean
147
): number {
148 81
  if (typeof keyA === 'symbol' && typeof keyB === 'symbol') return 0;
149 80
  if (typeof keyA === 'symbol') return 1;
150 77
  if (typeof keyB === 'symbol') return -1;
151
152 77
  const stringA = keyA as string;
153 77
  const stringB = keyB as string;
154
155 77
  return ascending
156
    ? stringA.localeCompare(stringB)
157
    : stringB.localeCompare(stringA);
158
}
159
160
function createSortedObject(
161
  entries: SortedEntry[],
162
  options: SortOptions
163
): ObjectType {
164 103
  const sortedEntries = entries.map(([key, value]) => [
165
    key,
166
    sortRecursively(value, options),
167
  ]);
168
169 44
  return Object.fromEntries(sortedEntries);
170
}
171
172
// Type guards
173
function isPrimitive(data: unknown): boolean {
174 284
  return (
175
    data === null ||
176
    data === undefined ||
177
    typeof data === 'string' ||
178
    typeof data === 'number' ||
179
    typeof data === 'boolean'
180
  );
181
}
182
183
function isObject(data: unknown): data is ObjectType {
184 66
  return typeof data === 'object' && data !== null;
185
}
186
187
function isNonSortableObject(obj: unknown): obj is NonSortableType {
188 61
  const nonSortableTypes = [
189
    Date,
190
    RegExp,
191
    Function,
192
    Error,
193
    Map,
194
    Set,
195
    WeakMap,
196
    WeakSet,
197
    Promise,
198
  ];
199
200 61
  return (
201 459
    nonSortableTypes.some((type) => obj instanceof type) ||
202
    Symbol.iterator in Object(obj)
203
  );
204
}
205