diff --git a/src/index.ts b/src/index.ts index d93d5b30..56ff962e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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"; @@ -139,6 +140,7 @@ export { throwError, throwIf, toArray, + toSorted, unicodeToArray, unless, when, diff --git a/src/toSorted.ts b/src/toSorted.ts new file mode 100644 index 00000000..9c4de0e0 --- /dev/null +++ b/src/toSorted.ts @@ -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(f: (a: T, b: T) => unknown, iterable: Iterable): T[]; + +function toSorted( + f: (a: T, b: T) => unknown, + iterable: AsyncIterable, +): Promise; + +function toSorted | AsyncIterable>( + f: (a: IterableInfer, b: IterableInfer) => unknown, +): (iterable: T) => ReturnValueType[]>; + +function toSorted | AsyncIterable>( + f: (a: IterableInfer, b: IterableInfer) => unknown, + iterable?: T, +): + | IterableInfer[] + | Promise[]> + | ((iterable: T) => ReturnValueType[]>) { + if (iterable === undefined) { + return (iterable: T) => { + // @ts-expect-error - Type narrowing needed for curried function + return toSorted(f, iterable) as ReturnValueType[]>; + }; + } + + 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[]; + // @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>), (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>), + (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; diff --git a/test/toSorted.spec.ts b/test/toSorted.spec.ts new file mode 100644 index 00000000..77cf44b9 --- /dev/null +++ b/test/toSorted.spec.ts @@ -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)).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)); + 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]); + }); + }); +});