Skip to content

Commit 343bd55

Browse files
committed
fix: code review
1 parent f068060 commit 343bd55

File tree

4 files changed

+120
-67
lines changed

4 files changed

+120
-67
lines changed

common/reviews/api/node-core-library.api.md

+7-4
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,12 @@ export interface IRunWithRetriesOptions<TResult> {
608608
retryDelayMs?: number;
609609
}
610610

611+
// @public
612+
export interface ISortKeysOptions {
613+
compare?: (x: string, y: string) => number;
614+
deep?: boolean;
615+
}
616+
611617
// @public
612618
export interface IStringBuilder {
613619
append(text: string): void;
@@ -831,10 +837,7 @@ export class Sort {
831837
static isSorted<T>(collection: Iterable<T>, comparer?: (x: any, y: any) => number): boolean;
832838
static isSortedBy<T>(collection: Iterable<T>, keySelector: (element: T) => any, comparer?: (x: any, y: any) => number): boolean;
833839
static sortBy<T>(array: T[], keySelector: (element: T) => any, comparer?: (x: any, y: any) => number): void;
834-
static sortKeys<T extends Partial<Record<string, unknown>> | unknown[]>(object: T, { deep, compare }?: {
835-
deep?: boolean;
836-
compare?: (x: string, y: string) => number;
837-
}): T;
840+
static sortKeys<T extends Partial<Record<string, unknown>> | unknown[]>(object: T, options?: ISortKeysOptions): T;
838841
static sortMapKeys<K, V>(map: Map<K, V>, keyComparer?: (x: K, y: K) => number): void;
839842
static sortSet<T>(set: Set<T>, comparer?: (x: T, y: T) => number): void;
840843
static sortSetBy<T>(set: Set<T>, keySelector: (element: T) => any, keyComparer?: (x: T, y: T) => number): void;

libraries/node-core-library/src/Sort.ts

+101-61
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,38 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4+
/**
5+
* Options of {@link Sort.sortKeys}
6+
*
7+
* @public
8+
*/
9+
export interface ISortKeysOptions {
10+
/**
11+
* Whether or not to recursively sort keys, both in objects and arrays
12+
* @defaultValue false
13+
*/
14+
deep?: boolean;
15+
/**
16+
* Custom compare function when sorting the keys
17+
*
18+
* @defaultValue Sort.compareByValue
19+
* @param x - Key name
20+
* @param y - Key name
21+
* @returns -1 if `x` is smaller than `y`, 1 if `x` is greater than `y`, or 0 if the values are equal.
22+
*/
23+
compare?: (x: string, y: string) => number;
24+
}
25+
26+
interface ISortKeysContext {
27+
cache: SortKeysCache;
28+
options: ISortKeysOptions;
29+
}
30+
31+
type SortKeysCache = WeakMap<
32+
Partial<Record<string, unknown>> | unknown[],
33+
Partial<Record<string, unknown>> | unknown[]
34+
>;
35+
436
/**
537
* Operations for sorting collections.
638
*
@@ -235,6 +267,9 @@ export class Sort {
235267
/**
236268
* Sort the keys given in an object
237269
*
270+
* @param object - The object to be sorted
271+
* @param options - The options for sort keys
272+
*
238273
* @example
239274
*
240275
* ```ts
@@ -243,80 +278,85 @@ export class Sort {
243278
*/
244279
public static sortKeys<T extends Partial<Record<string, unknown>> | unknown[]>(
245280
object: T,
246-
{ deep, compare }: { deep?: boolean; compare?: (x: string, y: string) => number } = {
281+
options: ISortKeysOptions = {
247282
deep: false,
248283
compare: Sort.compareByValue
249284
}
250285
): T {
251-
function isPlainObject(obj: unknown): obj is object {
252-
return obj !== null && typeof obj === 'object';
253-
}
254286
if (!isPlainObject(object) && !Array.isArray(object)) {
255287
throw new TypeError(`Expected object or array`);
256288
}
289+
const cache: SortKeysCache = new WeakMap();
290+
const context: ISortKeysContext = {
291+
cache,
292+
options
293+
};
257294

258-
const cache: WeakMap<
259-
Partial<Record<string, unknown>> | unknown[],
260-
Partial<Record<string, unknown>> | unknown[]
261-
> = new WeakMap();
295+
return Array.isArray(object)
296+
? (innerSortArray(object, context) as T)
297+
: (innerSortKeys(object, context) as T);
298+
}
299+
}
300+
function isPlainObject(obj: unknown): obj is object {
301+
return obj !== null && typeof obj === 'object';
302+
}
262303

263-
function innerSortArray(arr: unknown[]): unknown[] {
264-
const resultFromCache: undefined | Partial<Record<string, unknown>> | unknown[] = cache.get(arr);
265-
if (resultFromCache !== undefined) {
266-
return resultFromCache as unknown[];
267-
}
268-
const result: unknown[] = [];
269-
cache.set(arr, result);
270-
if (deep) {
271-
result.push(
272-
...arr.map((entry) => {
273-
if (Array.isArray(entry)) {
274-
return innerSortArray(entry);
275-
} else if (isPlainObject(entry)) {
276-
return innerSortKeys(entry);
277-
}
278-
return entry;
279-
})
280-
);
281-
} else {
282-
result.push(...arr);
283-
}
304+
function innerSortArray(arr: unknown[], context: ISortKeysContext): unknown[] {
305+
const resultFromCache: undefined | Partial<Record<string, unknown>> | unknown[] = context.cache.get(arr);
306+
if (resultFromCache !== undefined) {
307+
return resultFromCache as unknown[];
308+
}
309+
const result: unknown[] = [];
310+
context.cache.set(arr, result);
311+
if (context.options.deep) {
312+
result.push(
313+
...arr.map((entry) => {
314+
if (Array.isArray(entry)) {
315+
return innerSortArray(entry, context);
316+
} else if (isPlainObject(entry)) {
317+
return innerSortKeys(entry, context);
318+
}
319+
return entry;
320+
})
321+
);
322+
} else {
323+
result.push(...arr);
324+
}
284325

285-
return result;
286-
}
287-
function innerSortKeys(obj: Partial<Record<string, unknown>>): Partial<Record<string, unknown>> {
288-
const resultFromCache: undefined | Partial<Record<string, unknown>> | unknown[] = cache.get(obj);
289-
if (resultFromCache !== undefined) {
290-
return resultFromCache as Partial<Record<string, unknown>>;
291-
}
292-
const result: Partial<Record<string, unknown>> = {};
293-
const keys: string[] = Object.keys(obj).sort(compare);
326+
return result;
327+
}
328+
function innerSortKeys(
329+
obj: Partial<Record<string, unknown>>,
330+
context: ISortKeysContext
331+
): Partial<Record<string, unknown>> {
332+
const resultFromCache: undefined | Partial<Record<string, unknown>> | unknown[] = context.cache.get(obj);
333+
if (resultFromCache !== undefined) {
334+
return resultFromCache as Partial<Record<string, unknown>>;
335+
}
336+
const result: Partial<Record<string, unknown>> = {};
337+
const keys: string[] = Object.keys(obj).sort(context.options.compare);
294338

295-
cache.set(obj, result);
339+
context.cache.set(obj, result);
296340

297-
for (const key of keys) {
298-
const value: unknown = obj[key];
299-
let newValue: unknown;
300-
if (deep) {
301-
if (Array.isArray(value)) {
302-
newValue = innerSortArray(value);
303-
} else if (isPlainObject(value)) {
304-
newValue = innerSortKeys(value);
305-
} else {
306-
newValue = value;
307-
}
308-
} else {
309-
newValue = value;
310-
}
311-
Object.defineProperty(result, key, {
312-
...Object.getOwnPropertyDescriptor(obj, key),
313-
value: newValue
314-
});
341+
for (const key of keys) {
342+
const value: unknown = obj[key];
343+
let newValue: unknown;
344+
if (context.options.deep) {
345+
if (Array.isArray(value)) {
346+
newValue = innerSortArray(value, context);
347+
} else if (isPlainObject(value)) {
348+
newValue = innerSortKeys(value, context);
349+
} else {
350+
newValue = value;
315351
}
316-
317-
return result;
352+
} else {
353+
newValue = value;
318354
}
319-
320-
return Array.isArray(object) ? (innerSortArray(object) as T) : (innerSortKeys(object) as T);
355+
Object.defineProperty(result, key, {
356+
...Object.getOwnPropertyDescriptor(obj, key),
357+
value: newValue
358+
});
321359
}
360+
361+
return result;
322362
}

libraries/node-core-library/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export {
9393
type IPathFormatConciselyOptions
9494
} from './Path';
9595
export { Encoding, Text, NewlineKind, type IReadLinesFromIterableOptions } from './Text';
96-
export { Sort } from './Sort';
96+
export { Sort, type ISortKeysOptions } from './Sort';
9797
export {
9898
AlreadyExistsBehavior,
9999
FileSystem,

libraries/node-core-library/src/test/Sort.test.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,12 @@ describe('Sort.sortKeys', () => {
115115
}
116116

117117
test('sort the keys of an object', () => {
118-
deepEqualInOrder(Sort.sortKeys({ c: 0, a: 0, b: 0 }), { a: 0, b: 0, c: 0 });
118+
const unsortedObj = { c: 0, a: 0, b: 0 };
119+
const sortedObj = Sort.sortKeys(unsortedObj);
120+
// Assert that it's not sorted in-place
121+
expect(sortedObj).not.toBe(unsortedObj);
122+
deepEqualInOrder(unsortedObj, { c: 0, a: 0, b: 0 });
123+
deepEqualInOrder(sortedObj, { a: 0, b: 0, c: 0 });
119124
});
120125

121126
test('custom compare function', () => {
@@ -142,6 +147,11 @@ describe('Sort.sortKeys', () => {
142147
object.circular = object;
143148
const sortedObject = Sort.sortKeys(object, { deep: true });
144149

150+
// Assert that it's not sorted in-place
151+
expect(sortedObject).not.toBe(object);
152+
expect(Object.keys(object)).toEqual(['z', 'circular']);
153+
154+
// Assert that circular value references the same thing
145155
expect(sortedObject).toBe(sortedObject.circular);
146156
expect(Object.keys(sortedObject)).toEqual(['circular', 'z']);
147157

0 commit comments

Comments
 (0)