Repository: char0n/ramda-adjunct
Pull request #2104 (2021-11-29 14:34:43)

feat(sortByPath): sort list according to paths
Implemented `sortByPath` function.

Issue: #1240 


char0n: Also please rebase this PR on top of current master.
nolleto: @char0n I just updated the branch according to your code review 😃 
char0n: Additional changes before I can merge:

I need you to add
char0n: `
char0n: Additional changes I need you to make before the merge:

Add following code in `types/index.d.ts` on line 1486 right after `sortByProps`.

```ts
        /**
         * Sort a list of objects by a list of paths (if first path value is equivalent, sort by second, etc).
         */
        sortByPaths(props: string[][], list: object[]): object[];
        sortByPaths(props: string[][]): (list: object[]) => object[];
```

Create new file called `sortByPaths.ts`  in `types/tests/` and put following code in it:

```ts
import * as RA from 'ramda-adjunct';

const alice = {
    name: 'Alice',
    address: {
        street: 31,
        zipCode: 97777,
    },
};
const bob = {
    name: 'Bob',
    address: {
        street: 31,
        zipCode: 55555,
    },
};
const clara = {
    name: 'Clara',
    address: {
        street: 32,
        zipCode: 90210,
    },
};
const people = [clara, bob, alice];

RA.sortByPaths([['name']])(people); // $ExpectType object[]
RA.sortByPaths([['address', 'street'], ['address', 'zipCode']])(people); // $ExpectType object[]
RA.sortByPaths([])(people); // $ExpectType object[]

RA.sortByPaths([['name']])(123); // $ExpectError
RA.sortByPaths(123)(people); // $ExpectError
```

Patch:
diff --git a/src/index.js b/src/index.js
index 8ffbf472fe..36096c35fb 100644
--- a/src/index.js
+++ b/src/index.js
@@ -159,6 +159,7 @@ export { default as toArray } from './toArray';
 export { default as allUnique } from './allUnique';
 export { default as notAllUnique } from './notAllUnique';
 export { default as sortByProps } from './sortByProps';
+export { default as sortByPaths } from './sortByPaths';
 export { default as skipTake } from './skipTake';
 export { default as rangeStep } from './rangeStep';
 export { default as findOr } from './findOr';
diff --git a/src/sortByPaths.js b/src/sortByPaths.js
new file mode 100644
index 0000000000..b3656802db
--- /dev/null
+++ b/src/sortByPaths.js
@@ -0,0 +1,55 @@
+import { ascend, identity, map, path, pipe, sortWith, useWith } from 'ramda';
+
+const pathToAscendSort = pipe(path, ascend);
+const mapPathsToAscendSort = map(pathToAscendSort);
+
+/**
+ * Sort a list of objects by a list of paths (if first path value is equivalent, sort by second, etc).
+ *
+ * @func sortByPaths
+ * @memberOf RA
+ * @since {@link https://char0n.github.io/ramda-adjunct/3.1.0|v3.1.0}
+ * @category List
+ * @sig [[k]] -> [{k: v}] -> [{k: v}]
+ * @param {Array.<Array.<string>>} paths A list of paths in the list param to sort by
+ * @param {Array.<object>} list A list of objects to be sorted
+ * @return {Array.<object>} A new list sorted by the paths in the paths param
+ * @example
+ *
+ * const alice = {
+ *   name: 'Alice',
+ *   address: {
+ *     street: 31,
+ *     zipCode: 97777,
+ *   },
+ * };
+ * const bob = {
+ *   name: 'Bob',
+ *   address: {
+ *     street: 31,
+ *     zipCode: 55555,
+ *   },
+ * };
+ * const clara = {
+ *   name: 'Clara',
+ *   address: {
+ *     street: 32,
+ *     zipCode: 90210,
+ *   },
+ * };
+ * const people = [clara, bob, alice]
+ *
+ * RA.sortByPaths([
+ *   ['address', 'street'],
+ *   ['address', 'zipCode'],
+ * ], people); // => [bob, alice, clara]
+ *
+ * RA.sortByPaths([
+ *   ['address', 'zipCode'],
+ *   ['address', 'street'],
+ * ], people); // => [bob, clara, alice]
+ */
+
+const sortByPaths = useWith(sortWith, [mapPathsToAscendSort, identity]);
+
+export default sortByPaths;
diff --git a/types/index.d.ts b/types/index.d.ts
index 658eec98cc..d6b9956dbd 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -1483,6 +1483,12 @@ declare namespace RamdaAdjunct {
         skipTake<T>(n: number, list: T[]): T[];
         skipTake<T>(n: number): (list: T[]) => T[];
 
+        /**
+         * Sort a list of objects by a list of paths (if first path value is equivalent, sort by second, etc).
+         */
+        sortByPaths(props: string[][], list: object[]): object[];
+        sortByPaths(props: string[][]): (list: object[]) => object[];
+
         /**
          * Determine if input value is an indexed data type.
          */


Test patch:
diff --git a/test/sortByPaths.js b/test/sortByPaths.js
new file mode 100644
index 0000000000..399fff0633
--- /dev/null
+++ b/test/sortByPaths.js
@@ -0,0 +1,97 @@
+import { assert } from 'chai';
+
+import * as RA from '../src';
+
+describe('sortByPaths', function () {
+  const alice = {
+    name: 'Alice',
+    address: {
+      street: 31,
+      zipCode: 97777,
+    },
+  };
+  const bob = {
+    name: 'Bob',
+    address: {
+      street: 31,
+      zipCode: 55555,
+    },
+  };
+  const clara = {
+    name: 'Clara',
+    address: {
+      street: 32,
+      zipCode: 90210,
+    },
+  };
+  const people = [clara, bob, alice];
+
+  context('given list of objects', function () {
+    specify('should sort by existing paths', function () {
+      assert.deepEqual(RA.sortByPaths([['name']], people), [alice, bob, clara]);
+    });
+
+    context('given path to sort by does not exist', function () {
+      specify('should return original list', function () {
+        assert.deepEqual(RA.sortByPaths([['p']], people), people);
+      });
+    });
+
+    specify('should sort by multiple paths', function () {
+      assert.deepEqual(
+        RA.sortByPaths(
+          [
+            ['address', 'street'],
+            ['address', 'zipCode'],
+          ],
+          people
+        ),
+        [bob, alice, clara]
+      );
+
+      assert.deepEqual(
+        RA.sortByPaths(
+          [
+            ['address', 'zipCode'],
+            ['address', 'street'],
+          ],
+          people
+        ),
+        [bob, clara, alice]
+      );
+    });
+
+    specify('should ignore paths that do not exist', function () {
+      assert.deepEqual(
+        RA.sortByPaths(
+          [['lastName'], ['p'], ['address', 'street'], ['address', 'zipCode']],
+          people
+        ),
+        [bob, alice, clara]
+      );
+    });
+  });
+
+  context('given list of strings', function () {
+    specify('should return unmodified list of strings', function () {
+      assert.deepEqual(RA.sortByPaths([['name']], ['a', 'b', 'c']), [
+        'a',
+        'b',
+        'c',
+      ]);
+    });
+  });
+
+  context('given empty list', function () {
+    specify('should return empty list', function () {
+      assert.deepEqual(RA.sortByPaths([['name']], []), []);
+    });
+  });
+
+  it('should be curried', function () {
+    assert.deepEqual(
+      RA.sortByPaths([['name']])(people),
+      RA.sortByPaths([['name']], people)
+    );
+  });
+});
diff --git a/types/test/sortByPaths.ts b/types/test/sortByPaths.ts
new file mode 100644
index 0000000000..74501180d8
--- /dev/null
+++ b/types/test/sortByPaths.ts
@@ -0,0 +1,31 @@
+import * as RA from 'ramda-adjunct';
+
+const alice = {
+    name: 'Alice',
+    address: {
+        street: 31,
+        zipCode: 97777,
+    },
+};
+const bob = {
+    name: 'Bob',
+    address: {
+        street: 31,
+        zipCode: 55555,
+    },
+};
+const clara = {
+    name: 'Clara',
+    address: {
+        street: 32,
+        zipCode: 90210,
+    },
+};
+const people = [clara, bob, alice];
+
+RA.sortByPaths([['name']])(people); // $ExpectType object[]
+RA.sortByPaths([['address', 'street'], ['address', 'zipCode']])(people); // $ExpectType object[]
+RA.sortByPaths([])(people); // $ExpectType object[]
+
+RA.sortByPaths([['name']])(123); // $ExpectError
+RA.sortByPaths(123)(people); // $ExpectError