Repository: marpple/FxTS (ts)

Issue:
Add `sortBy` function
# Suggestion

<!--
  Please fill in each section completely. Thank you!
-->

## ⭐ Suggestion

<!-- What would you like to be added? -->

## 💻 Use Cases
```ts
const products = [
  { id: 1, name: 'T-shirt', price: 50 },
  { id: 2, name: 'Phone case', price: 30 },
  { id: 3, name: 'Key ring', price: 15 },
];
sortBy(a => a.price, products);
```

Pull request:
feat: add `sortBy` function
Fixes #74 


Patch:
diff --git a/src/Lazy/compress.ts b/src/Lazy/compress.ts
new file mode 100644
index 00000000..71d0a9fd
--- /dev/null
+++ b/src/Lazy/compress.ts
@@ -0,0 +1,69 @@
+import zip from "./zip";
+import pipe from "../pipe";
+import filter from "./filter";
+import map from "./map";
+import ReturnIterableIteratorType from "../types/ReturnIterableIteratorType";
+
+/**
+ * Returns Iterable/AsyncIterable that filters elements from 'iterable' returning only those that have a corresponding element in 'selectors' that evaluates to 'true'.
+ * Stops when either 'iterable' or 'selectors' has been exhausted.
+ *
+ * @example
+ * ```ts
+ * const iter1 = compress([false,true,false,false,true],  [1, 2, 3, 4, 5]);
+ * iter1.next(); // {value: 2, done:false}
+ * iter1.next(); // {value: 5, done:false}
+ * iter1.next(); // {value: undefined, done:true }
+ *
+ * const iter2 = compress([1,0,0,1,0],  "abcde");
+ * iter2.next(); // {value: "a", done:false}
+ * iter2.next(); // {value: "d", done:false}
+ * iter2.next(); // {value: undefined, done:true }
+ *
+ * // with pipe
+ * pipe(
+ *   [1, 2, 3],
+ *   compress([false, true, true]),
+ *   toArray,
+ * ); // [2, 3]
+ * ```
+ *
+ * see {@link https://fxts.dev/docs/pipe | pipe}, {@link https://fxts.dev/docs/toArray | toArray}
+ */
+function compress<A, B>(
+  selector: Array<A>,
+  iterable: Iterable<B>,
+): IterableIterator<B>;
+
+function compress<A, B>(
+  selector: Array<A>,
+  iterable: AsyncIterable<B>,
+): AsyncIterableIterator<B>;
+
+function compress<A, B extends Iterable<unknown> | AsyncIterable<unknown>>(
+  selector: Array<A>,
+): (iterable: B) => ReturnIterableIteratorType<B>;
+
+function compress<A, B extends Iterable<unknown> | AsyncIterable<unknown>>(
+  selectors: Array<A>,
+  iterable?: B,
+):
+  | ReturnIterableIteratorType<B>
+  | ((iterable: B) => ReturnIterableIteratorType<B>) {
+  if (iterable === undefined) {
+    return (iterable: B) => {
+      return compress(
+        selectors,
+        iterable as any,
+      ) as ReturnIterableIteratorType<B>;
+    };
+  }
+
+  return pipe(
+    zip(selectors, iterable),
+    filter(([selector]) => selector),
+    map(([, value]) => value),
+  ) as ReturnIterableIteratorType<B>;
+}
+
+export default compress;
diff --git a/src/Lazy/index.ts b/src/Lazy/index.ts
index e7dd61ae..fb5fa678 100644
--- a/src/Lazy/index.ts
+++ b/src/Lazy/index.ts
@@ -1,6 +1,7 @@
 import append from "./append";
 import chunk from "./chunk";
 import compact from "./compact";
+import compress from "./compress";
 import concat from "./concat";
 import concurrent from "./concurrent";
 import cycle from "./cycle";
@@ -39,6 +40,7 @@ export {
   append,
   chunk,
   compact,
+  compress,
   concat,
   concurrent,
   cycle,
diff --git a/src/index.ts b/src/index.ts
index d33e4756..398c2397 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -39,6 +39,7 @@ import reduce from "./reduce";
 import size from "./size";
 import some from "./some";
 import sort from "./sort";
+import sortBy from "./sortBy";
 import sum from "./sum";
 import tap from "./tap";
 import toArray from "./toArray";
@@ -87,6 +88,7 @@ export {
   size,
   some,
   sort,
+  sortBy,
   sum,
   tap,
   toArray,
diff --git a/src/sort.ts b/src/sort.ts
index a4fd5f4a..27e3857f 100644
--- a/src/sort.ts
+++ b/src/sort.ts
@@ -6,7 +6,7 @@ import pipe1 from "./pipe1";
 import toArray from "./toArray";
 
 /**
- * Returns an array which is sorted by function `f`.
+ * Returns an array, sorted according to the comparator `f`, which should accept two values
  *
  * @example
  * ```ts
@@ -36,7 +36,7 @@ function sort<T extends Iterable<unknown> | AsyncIterable<unknown>>(
 ): (iterable: T) => ReturnValueType<T, IterableInfer<T>[]>;
 
 function sort<T extends Iterable<unknown> | AsyncIterable<unknown>>(
-  f: (a: T, b: T) => unknown,
+  f: (a: IterableInfer<T>, b: IterableInfer<T>) => unknown,
   iterable?: T,
 ):
   | IterableInfer<T>[]
diff --git a/src/sortBy.ts b/src/sortBy.ts
new file mode 100644
index 00000000..11518c9a
--- /dev/null
+++ b/src/sortBy.ts
@@ -0,0 +1,69 @@
+import isArray from "./isArray";
+import pipe1 from "./pipe1";
+import toArray from "./toArray";
+import IterableInfer from "./types/IterableInfer";
+import ReturnValueType from "./types/ReturnValueType";
+import { isAsyncIterable, isIterable } from "./_internal/utils";
+
+/**
+ * Returns an array which sorted according to the `f`.
+ *
+ * @example
+ * ```ts
+ * sortBy(a => a, [3, 4, 1, 2, 5, 2]);
+ * // [1, 2, 2, 3, 4, 5]
+ * sortBy((a) => a.id, [{id:4,name:'foo'}, {id:2,name:'bar'},{id:3, name:'lee'}]);
+ * // [{id:2,name:'bar'}, {id:3, name:'lee'}, {id:4,name:'foo'}]
+ * ```
+ */
+function sortBy(f: (a: any) => unknown, iterable: readonly []): any[];
+
+function sortBy<T>(f: (a: T) => unknown, iterable: Iterable<T>): T[];
+
+function sortBy<T>(
+  f: (a: T) => unknown,
+  iterable: AsyncIterable<T>,
+): Promise<T[]>;
+
+function sortBy<T extends Iterable<unknown> | AsyncIterable<unknown>>(
+  f: (a: IterableInfer<T>) => unknown,
+): (iterable: T) => ReturnValueType<T, IterableInfer<T>[]>;
+
+function sortBy<T extends Iterable<unknown> | AsyncIterable<unknown>>(
+  f: (a: IterableInfer<T>) => unknown,
+  iterable?: T,
+):
+  | IterableInfer<T>[]
+  | Promise<IterableInfer<T>[]>
+  | ((iterable: T) => ReturnValueType<T, IterableInfer<T>[]>) {
+  if (iterable === undefined) {
+    return (iterable: T) =>
+      sortBy(f, iterable as any) as ReturnValueType<T, IterableInfer<T>[]>;
+  }
+
+  const _sortBy = (a: IterableInfer<T>, b: IterableInfer<T>) => {
+    const aa = f(a) as number;
+    const bb = f(b) as number;
+    return aa < bb ? -1 : aa > bb ? 1 : 0;
+  };
+
+  if (isArray(iterable)) {
+    return iterable.sort(_sortBy);
+  }
+
+  if (isIterable(iterable)) {
+    return pipe1(toArray(iterable as Iterable<IterableInfer<T>>), (arr) =>
+      arr.sort(_sortBy),
+    );
+  }
+
+  if (isAsyncIterable(iterable)) {
+    return pipe1(toArray(iterable as AsyncIterable<IterableInfer<T>>), (arr) =>
+      arr.sort(_sortBy),
+    );
+  }
+
+  throw new TypeError("'iterable' must be type of Iterable or AsyncIterable");
+}
+
+export default sortBy;


Test patch:
diff --git a/test/Lazy/compress.spec.ts b/test/Lazy/compress.spec.ts
new file mode 100644
index 00000000..03213d85
--- /dev/null
+++ b/test/Lazy/compress.spec.ts
@@ -0,0 +1,87 @@
+import { compress, map, pipe, toArray, toAsync } from "../../src";
+import { Concurrent } from "../../src/Lazy/concurrent";
+import { generatorMock } from "../utils";
+
+describe("compress", function () {
+  describe("sync", function () {
+    it.each([
+      [
+        [false, true, false, false, true],
+        [1, 2, 3, 4, 5],
+        [2, 5],
+      ],
+      [
+        [false, true, false, false, true],
+        [1, 2, 3, 4, 5, 6],
+        [2, 5],
+      ],
+      [[false, true, false, false, true], [1, 2, 3, 4], [2]],
+      [[1, 0, 0, 1, 0], "abcde", ["a", "d"]],
+    ])(
+      "should filter elements that have a corresponding element in 'selectors'",
+      function (selectors, iterable, result) {
+        const res = compress(
+          selectors as Array<any>,
+          iterable as Iterable<any>,
+        );
+        expect(toArray(res)).toEqual(result);
+      },
+    );
+
+    it("should be able to be used as a curried function in the pipeline", function () {
+      const res = pipe(
+        [1, 2, 3, 4, 5],
+        compress([false, true, false, false, true]),
+        toArray,
+      );
+
+      expect(res).toEqual([2, 5]);
+    });
+  });
+
+  describe("async", function () {
+    it.each([
+      [
+        [false, true, false, false, true],
+        [1, 2, 3, 4, 5],
+        [2, 5],
+      ],
+      [
+        [false, true, false, false, true],
+        [1, 2, 3, 4, 5, 6],
+        [2, 5],
+      ],
+      [[false, true, false, false, true], [1, 2, 3, 4], [2]],
+      [[1, 0, 0, 1, 0], "abcde", ["a", "d"]],
+    ])(
+      "should filter elements that have a corresponding element in 'selectors'",
+      async function (selectors, iterable, result) {
+        const res = compress(
+          selectors as Array<any>,
+          iterable as Iterable<any>,
+        );
+        expect(await toArray(toAsync(res))).toEqual(result);
+      },
+    );
+
+    it("should be able to be used as a curried function in the pipeline", async function () {
+      const res = await pipe(
+        [1, 2, 3, 4, 5],
+        toAsync,
+        compress([false, true, false, false, true]),
+        toArray,
+      );
+
+      expect(res).toEqual([2, 5]);
+    });
+
+    it("should be passed concurrent object when job works concurrently", async function () {
+      const mock = generatorMock();
+      const iter = map((a) => a, mock);
+      const concurrent = Concurrent.of(2) as any;
+
+      await iter.next(concurrent);
+      expect((mock as any).getConcurrent()).toEqual(concurrent);
+    });
+  });
+});
diff --git a/test/sort.spec.ts b/test/sort.spec.ts
index 8bd6f620..e8654422 100644
--- a/test/sort.spec.ts
+++ b/test/sort.spec.ts
@@ -12,10 +12,15 @@ describe("sort", function () {
   };
 
   describe("sync", function () {
-    it("should return 'false' if given iterable is an empty array", function () {
-      expect(sort(sortFn, [])).toEqual([]);
-      expect(sort(sortFn, [3, 4, 1, 2, 5, 2])).toEqual([1, 2, 2, 3, 4, 5]);
-      expect(sort(sortFn, "bcdaef")).toEqual(["a", "b", "c", "d", "e", "f"]);
+    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(sort(sortFn, iterable as Iterable<any>)).toEqual(result);
     });
 
     it("should be able to be used as a curried function in the pipeline", function () {
@@ -29,29 +34,16 @@ describe("sort", function () {
   });
 
   describe("async", function () {
-    it("should return 'false' if given iterable is an empty array", async function () {
-      const sortFn = (a: number | string, b: number | string) => {
-        if (a === b) {
-          return 0;
-        }
-        if (a > b) {
-          return 1;
-        }
-        return -1;
-      };
-
-      expect(await sort(sortFn, toAsync([]))).toEqual([]);
-      expect(await sort(sortFn, toAsync([3, 4, 1, 2, 5, 2]))).toEqual([
-        1, 2, 2, 3, 4, 5,
-      ]);
-      expect(await sort(sortFn, toAsync("bcdaef"))).toEqual([
-        "a",
-        "b",
-        "c",
-        "d",
-        "e",
-        "f",
-      ]);
+    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 sort(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 () {
diff --git a/test/sortBy.spec.ts b/test/sortBy.spec.ts
new file mode 100644
index 00000000..c3034781
--- /dev/null
+++ b/test/sortBy.spec.ts
@@ -0,0 +1,69 @@
+import { filter, identity, pipe, sortBy, toAsync } from "../src/index";
+
+describe("sortBy", function () {
+  describe("sync", function () {
+    it.each([
+      [identity, [], []],
+      [identity, [3, 4, 1, 2, 5, 2], [1, 2, 2, 3, 4, 5]],
+      [identity, "bcdae", ["a", "b", "c", "d", "e"]],
+      [
+        (a: any) => a.id,
+        [
+          { id: 4, name: "foo" },
+          { id: 2, name: "bar" },
+          { id: 3, name: "lee" },
+        ],
+        [
+          { id: 2, name: "bar" },
+          { id: 3, name: "lee" },
+          { id: 4, name: "foo" },
+        ],
+      ],
+    ])("should sort to elements by 'f'", function (f, iterable, result) {
+      expect(sortBy(f, iterable as Iterable<any>)).toEqual(result);
+    });
+
+    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),
+        sortBy(identity),
+      );
+      expect(res).toEqual([1, 3, 5]);
+    });
+  });
+
+  describe("async", function () {
+    it.each([
+      [identity, [], []],
+      [identity, [3, 4, 1, 2, 5, 2], [1, 2, 2, 3, 4, 5]],
+      [identity, "bcdae", ["a", "b", "c", "d", "e"]],
+      [
+        (a: any) => a.id,
+        [
+          { id: 4, name: "foo" },
+          { id: 2, name: "bar" },
+          { id: 3, name: "lee" },
+        ],
+        [
+          { id: 2, name: "bar" },
+          { id: 3, name: "lee" },
+          { id: 4, name: "foo" },
+        ],
+      ],
+    ])("should sort to elements by 'f'", async function (f, iterable, result) {
+      const res = await sortBy(f, 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),
+        sortBy(identity),
+      );
+      expect(res).toEqual([1, 3, 5]);
+    });
+  });
+});
diff --git a/type-check/Lazy/compress.test.ts b/type-check/Lazy/compress.test.ts
new file mode 100644
index 00000000..2ef34dbb
--- /dev/null
+++ b/type-check/Lazy/compress.test.ts
@@ -0,0 +1,24 @@
+import * as Test from "../../src/types/Test";
+import { toAsync, compress, pipe } from "../../src";
+
+const { checks, check } = Test;
+
+const res1 = compress([], []);
+const res2 = compress([true, false], [1, 0]);
+const res3 = compress([true, false], "ab");
+const res4 = compress([true, 0], [1, 0]);
+const res5 = compress([true, false], toAsync([1, 0]));
+const res6 = pipe([1, 0], compress([true, false]));
+const res7 = pipe(Promise.resolve([1, 2, 3]), compress([true, false]));
+const res8 = pipe(toAsync([1, 2, 3]), compress([true, false]));
+
+checks([
+  check<typeof res1, IterableIterator<never>, Test.Pass>(),
+  check<typeof res2, IterableIterator<number>, Test.Pass>(),
+  check<typeof res3, IterableIterator<string>, Test.Pass>(),
+  check<typeof res4, IterableIterator<number>, Test.Pass>(),
+  check<typeof res5, AsyncIterableIterator<number>, Test.Pass>(),
+  check<typeof res6, IterableIterator<number>, Test.Pass>(),
+  check<typeof res7, Promise<IterableIterator<number>>, Test.Pass>(),
+  check<typeof res8, AsyncIterableIterator<number>, Test.Pass>(),
+]);
diff --git a/type-check/sortBy.test.ts b/type-check/sortBy.test.ts
new file mode 100644
index 00000000..04092901
--- /dev/null
+++ b/type-check/sortBy.test.ts
@@ -0,0 +1,22 @@
+import * as Test from "../src/types/Test";
+import { identity, pipe, sortBy, toAsync } from "../src";
+
+const { checks, check } = Test;
+
+const res0 = sortBy(identity, []);
+const res1 = sortBy(identity, [1, 2, 3]);
+const res2 = sortBy(identity, "abc");
+const res3 = sortBy(identity, toAsync([1, 2, 3]));
+const res4 = sortBy(identity, toAsync("abc"));
+const res5 = pipe([1, 2, 3], sortBy(identity));
+const res6 = pipe([1, 2, 3], toAsync, sortBy(identity));
+
+checks([
+  check<typeof res0, any[], Test.Pass>(),
+  check<typeof res1, number[], Test.Pass>(),
+  check<typeof res2, string[], Test.Pass>(),
+  check<typeof res3, Promise<number[]>, Test.Pass>(),
+  check<typeof res4, Promise<string[]>, Test.Pass>(),
+  check<typeof res5, number[], Test.Pass>(),
+  check<typeof res6, Promise<number[]>, Test.Pass>(),
+]);