Passed
Push — feat/issue-2 ( aa255c...edd8dc )
by Yuri
01:34
created

index.ts ➔ hasNullishValues   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
// Constants - make the code self-documenting
2 1
const NULLISH_VALUES = [null, undefined] as const;
3 1
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>;
21
22
type SortOptions = {
23
  ascending: boolean;
24
  sortPrimitiveArrays: boolean;
25
};
26
27 1
export function sort<T>(
28
  data: T,
29
  ascending = true,
30
  sortPrimitiveArrays = false
31
): T {
32 46
  const options: SortOptions = { ascending, sortPrimitiveArrays };
33 46
  return sortRecursively(data, options);
34
}
35
36
function sortRecursively<T>(data: T, options: SortOptions): T {
37 229
  if (Array.isArray(data)) {
38 45
    return sortArray(data, options) as T;
39
  }
40
41 184
  if (isPrimitive(data) || isNonSortableObject(data)) {
42 140
    return data;
43
  }
44
45 44
  if (isObject(data)) {
46 44
    return sortObject(data, options) as T;
47
  }
48
49
  return data;
50
}
51
52
function sortArray<T>(array: T[], options: SortOptions): T[] {
53 45
  if (shouldSortPrimitiveArray(array, options.sortPrimitiveArrays)) {
54 18
    return sortPrimitiveArray(array, options.ascending) as T[];
55
  }
56
57 80
  return array.map((item) => sortRecursively(item, options));
58
}
59
60
function sortObject(obj: ObjectType, options: SortOptions): ObjectType {
61 44
  const entries = collectObjectEntries(obj);
62 44
  const sortedEntries = sortObjectEntries(entries, options.ascending);
63 44
  return createSortedObject(sortedEntries, options);
64
}
65
66
function shouldSortPrimitiveArray(
67
  array: unknown[],
68
  sortPrimitiveArrays: boolean
69
): boolean {
70 45
  return sortPrimitiveArrays && canSortPrimitiveArray(array);
71
}
72
73
function canSortPrimitiveArray(array: unknown[]): boolean {
74 29
  return (
75
    allItemsAreSortablePrimitives(array) && allItemsHaveSameSortableType(array)
76
  );
77
}
78
79
function allItemsAreSortablePrimitives(array: unknown[]): boolean {
80 29
  return array.every(isSortablePrimitive);
81
}
82
83
function sortPrimitiveArray(array: unknown[], ascending: boolean): unknown[] {
84 18
  if (!allItemsHaveSameSortableType(array)) {
85
    return array; // Mixed types maintain original order
86
  }
87
88 78
  return [...array].sort((a, b) => compareSortablePrimitives(a, b, ascending));
89
}
90
91
function allItemsHaveSameSortableType(array: unknown[]): boolean {
92 37
  if (array.length === 0) return true;
93
94
  // Don't sort arrays that contain nullish values
95
  // as they represent absence of value and don't have a natural ordering
96 35
  if (hasNullishValues(array)) {
97
    return false;
98
  }
99
100
  // For arrays with only sortable primitives, check if they have the same type
101 35
  const firstItem = array[0];
102 35
  if (!isSortablePrimitive(firstItem)) return false;
103
104 35
  const expectedType = typeof firstItem;
105 128
  return array.every((item) => typeof item === expectedType);
106
}
107
108
function compareSortablePrimitives(
109
  a: unknown,
110
  b: unknown,
111
  ascending: boolean
112
): number {
113 78
  if (typeof a === 'string' && typeof b === 'string') {
114 9
    return ascending ? a.localeCompare(b) : b.localeCompare(a);
115
  }
116
117 69
  if (typeof a === 'number' && typeof b === 'number') {
118 59
    return compareNumbers(a, b, ascending);
119
  }
120
121 10
  if (typeof a === 'boolean' && typeof b === 'boolean') {
122 10
    return compareBooleans(a, b, ascending);
123
  }
124
125
  return 0; // Maintain order for other primitives
126
}
127
128
function compareNumbers(a: number, b: number, ascending: boolean): number {
129 59
  if (Number.isNaN(a) && Number.isNaN(b)) return 0;
130 57
  if (Number.isNaN(a)) return 1;
131 53
  if (Number.isNaN(b)) return -1;
132
133 47
  return ascending ? a - b : b - a;
134
}
135
136
function compareBooleans(a: boolean, b: boolean, ascending: boolean): number {
137 10
  if (a === b) return 0;
138 6
  if (a) return ascending ? 1 : -1;
139 3
  return ascending ? -1 : 1;
140
}
141
142
// Object entry handling
143
function collectObjectEntries(obj: ObjectType): SortedEntry[] {
144 44
  const stringEntries = Object.entries(obj);
145 44
  const symbolEntries = Object.getOwnPropertySymbols(obj).map(
146 2
    (symbol) => [symbol, obj[symbol]] as SortedEntry
147
  );
148
149 44
  return [...stringEntries, ...symbolEntries];
150
}
151
152
function sortObjectEntries(
153
  entries: SortedEntry[],
154
  ascending: boolean
155
): SortedEntry[] {
156 44
  return entries.sort(([keyA], [keyB]) =>
157 81
    compareObjectKeys(keyA, keyB, ascending)
158
  );
159
}
160
161
function compareObjectKeys(
162
  keyA: string | symbol,
163
  keyB: string | symbol,
164
  ascending: boolean
165
): number {
166 81
  if (typeof keyA === 'symbol' && typeof keyB === 'symbol') return 0;
167 80
  if (typeof keyA === 'symbol') return 1;
168 77
  if (typeof keyB === 'symbol') return -1;
169
170 77
  const stringA = keyA as string;
171 77
  const stringB = keyB as string;
172
173 77
  return ascending
174
    ? stringA.localeCompare(stringB)
175
    : stringB.localeCompare(stringA);
176
}
177
178
function createSortedObject(
179
  entries: SortedEntry[],
180
  options: SortOptions
181
): ObjectType {
182 103
  const sortedEntries = entries.map(([key, value]) => [
183
    key,
184
    sortRecursively(value, options),
185
  ]);
186
187 44
  return Object.fromEntries(sortedEntries);
188
}
189
190
// Type guards - single responsibility, clear naming
191
function isNullish(value: unknown): value is NullishValue {
192 212
  return NULLISH_VALUES.includes(value as NullishValue);
193
}
194
195
function hasNullishValues(array: unknown[]): boolean {
196 35
  return array.some(isNullish);
197
}
198
199
function isSortablePrimitive(
200
  value: unknown
201
): value is string | number | boolean {
202 302
  return SORTABLE_PRIMITIVE_TYPES.includes(
203
    typeof value as SortablePrimitiveType
204
  );
205
}
206
207
function isPrimitive(data: unknown): boolean {
208 184
  return isSortablePrimitive(data) || isNullish(data);
209
}
210
211
function isObject(data: unknown): data is ObjectType {
212 44
  return typeof data === 'object' && data !== null;
213
}
214
215
function isNonSortableObject(obj: unknown): obj is NonSortableType {
216 66
  const nonSortableTypes = [
217
    Date,
218
    RegExp,
219
    Function,
220
    Error,
221
    Map,
222
    Set,
223
    WeakMap,
224
    WeakSet,
225
    Promise,
226
  ];
227
228 66
  return (
229 474
    nonSortableTypes.some((type) => obj instanceof type) ||
230
    Symbol.iterator in Object(obj)
231
  );
232
}
233