Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import tap from "./tap";
import throwError from "./throwError";
import throwIf from "./throwIf";
import toArray from "./toArray";
import toSorted from "./toSorted";
import unicodeToArray from "./unicodeToArray";
import unless from "./unless";
import when from "./when";
Expand Down Expand Up @@ -139,6 +140,7 @@ export {
throwError,
throwIf,
toArray,
toSorted,
unicodeToArray,
unless,
when,
Expand Down
90 changes: 90 additions & 0 deletions src/toSorted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { isAsyncIterable, isIterable } from "./_internal/utils";
import isArray from "./isArray";
import pipe1 from "./pipe1";
import toArray from "./toArray";
import type IterableInfer from "./types/IterableInfer";
import type ReturnValueType from "./types/ReturnValueType";

/**
* Returns a new array, sorted according to the comparator `f`, which should accept two values.
* Unlike `sort`, this function does not mutate the original array.
*
* @example
* ```ts
* const arr = [3, 4, 1, 2, 5, 2];
* toSorted((a, b) => a > b, arr); // [1, 2, 2, 3, 4, 5]
* arr; // [3, 4, 1, 2, 5, 2] - original array is unchanged
*
* toSorted((a, b) => a.localeCompare(b), "bcdaef"); // ["a", "b", "c", "d", "e", "f"]
*
* // Can be used in a pipeline
* pipe(
* [3, 4, 1, 2, 5, 2],
* filter((a) => a % 2 !== 0),
* toSorted((a, b) => a > b),
* ); // [1, 3, 5]
* ```
*/

function toSorted(f: (a: any, b: any) => unknown, iterable: readonly []): any[];

function toSorted<T>(f: (a: T, b: T) => unknown, iterable: Iterable<T>): T[];

function toSorted<T>(
f: (a: T, b: T) => unknown,
iterable: AsyncIterable<T>,
): Promise<T[]>;

function toSorted<T extends Iterable<unknown> | AsyncIterable<unknown>>(
f: (a: IterableInfer<T>, b: IterableInfer<T>) => unknown,
): (iterable: T) => ReturnValueType<T, IterableInfer<T>[]>;

function toSorted<T extends Iterable<unknown> | AsyncIterable<unknown>>(
f: (a: IterableInfer<T>, b: IterableInfer<T>) => unknown,
iterable?: T,
):
| IterableInfer<T>[]
| Promise<IterableInfer<T>[]>
| ((iterable: T) => ReturnValueType<T, IterableInfer<T>[]>) {
if (iterable === undefined) {
return (iterable: T) => {
// @ts-expect-error - Type narrowing needed for curried function
return toSorted(f, iterable) as ReturnValueType<T, IterableInfer<T>[]>;
};
}

if (isArray(iterable)) {
// Check if native toSorted is available (ES2023+)
// Note: Array.prototype.toSorted is not in TypeScript lib types until TS 5.2+
// @ts-expect-error - toSorted is available in ES2023 but not in current lib types
if (typeof Array.prototype.toSorted === "function") {
// @ts-expect-error - toSorted is available in ES2023 but not in current lib types
return iterable.toSorted(f);
}
// Fallback: create a copy and sort it
const result = Array.from(iterable) as IterableInfer<T>[];
// @ts-expect-error - sort expects (a, b) => number but f returns unknown
return result.sort(f);
}

if (isIterable(iterable)) {
return pipe1(toArray(iterable as Iterable<IterableInfer<T>>), (arr) => {
// @ts-expect-error - sort expects (a, b) => number but f returns unknown
return arr.sort(f);
});
}

if (isAsyncIterable(iterable)) {
return pipe1(
toArray(iterable as AsyncIterable<IterableInfer<T>>),
(arr) => {
// @ts-expect-error - sort expects (a, b) => number but f returns unknown
return arr.sort(f);
},
);
}

throw new TypeError("'iterable' must be type of Iterable or AsyncIterable");
}

export default toSorted;
130 changes: 130 additions & 0 deletions test/toSorted.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { filter, map, pipe, sort, toAsync, toSorted } from "../src";

describe("toSorted", function () {
const sortFn = (a: number | string, b: number | string) => {
if (a === b) {
return 0;
}
if (a > b) {
return 1;
}
return -1;
};

describe("sync", function () {
it.each([
[[], []],
[
[3, 4, 1, 2, 5, 2],
[1, 2, 2, 3, 4, 5],
],
["bcdaef", ["a", "b", "c", "d", "e", "f"]],
])("should sort the elements", function (iterable, result) {
expect(toSorted(sortFn, iterable as Iterable<any>)).toEqual(result);
});

it("should handle empty array", function () {
const result = toSorted(sortFn, []);
expect(result).toEqual([]);
});

it("should handle single element", function () {
const result = toSorted(sortFn, [42]);
expect(result).toEqual([42]);
});

it("should handle array with identical elements", function () {
const result = toSorted(sortFn, [5, 5, 5, 5]);
expect(result).toEqual([5, 5, 5, 5]);
});

it("should be immutable - original array should not be changed", function () {
const original = [3, 4, 1, 2, 5, 2];
const result = toSorted(sortFn, original);
expect(original !== result).toBe(true);
expect(original).toEqual([3, 4, 1, 2, 5, 2]);
});

it("should be immutable - original string should not be changed", function () {
const original = "bcdaef";
const result = toSorted(sortFn, original);
expect(original).toBe("bcdaef");
expect(result).toEqual(["a", "b", "c", "d", "e", "f"]);
});

it("should return the same result as sort but without mutating", function () {
const arr1 = [3, 4, 1, 2, 5, 2];
const arr2 = [3, 4, 1, 2, 5, 2];
const sortedResult = sort(sortFn, arr1);
const toSortedResult = toSorted(sortFn, arr2);

expect(toSortedResult).toEqual(sortedResult);
// arr1 was mutated by sort
expect(arr1).toEqual(sortedResult);
// arr2 was not mutated by toSorted
expect(arr2).toEqual([3, 4, 1, 2, 5, 2]);
});

it("should be able to be used as a curried function in the pipeline", function () {
const res = pipe(
[3, 4, 1, 2, 5, 2],
filter((a) => a % 2 !== 0),
toSorted(sortFn),
);
expect(res).toEqual([1, 3, 5]);
});

it("should work with other functions in pipeline", function () {
const res = pipe(
[3, 4, 1, 2, 5, 2],
map((a) => a * 2),
filter((a) => a > 4),
toSorted(sortFn),
);
expect(res).toEqual([6, 8, 10]);
});

it("should preserve immutability in pipeline", function () {
const original = [3, 4, 1, 2, 5, 2];
const originalCopy = [...original];
const res = pipe(original, toSorted(sortFn));
expect(original).toEqual(originalCopy);
expect(res).toEqual([1, 2, 2, 3, 4, 5]);
});
});

describe("async", function () {
it.each([
[[], []],
[
[3, 4, 1, 2, 5, 2],
[1, 2, 2, 3, 4, 5],
],
["bcdaef", ["a", "b", "c", "d", "e", "f"]],
])("should sort the elements", async function (iterable, result) {
const res = await toSorted(sortFn, toAsync(iterable as Iterable<any>));
expect(res).toEqual(result);
});

it("should be able to be used as a curried function in the pipeline", async function () {
const res = await pipe(
[3, 4, 1, 2, 5, 2],
toAsync,
filter((a) => a % 2 !== 0),
toSorted(sortFn),
);
expect(res).toEqual([1, 3, 5]);
});

it("should work with other functions in async pipeline", async function () {
const res = await pipe(
[3, 4, 1, 2, 5, 2],
toAsync,
map((a) => a * 2),
filter((a) => a > 4),
toSorted(sortFn),
);
expect(res).toEqual([6, 8, 10]);
});
});
});
Loading