Repository: remeda/remeda
Pull request #483 (2024-02-01 14:38:15)

chore(deps): bump the actions group with 1 update
Bumps the actions group with 1 update: [codecov/codecov-action](https://github.com/codecov/codecov-action).

Updates `codecov/codecov-action` from 3 to 4
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/codecov/codecov-action/releases">codecov/codecov-action's releases</a>.</em></p>
<blockquote>
<h2>v4.0.0</h2>
<p>v4 of the Codecov Action uses the <a href="https://docs.codecov.com/docs/the-codecov-cli">CLI</a> as the underlying upload. The CLI has helped to power new features including local upload, the global upload token, and new upcoming features.</p>
<h2>Breaking Changes</h2>
<ul>
<li>The Codecov Action runs as a <code>node20</code> action due to <code>node16</code> deprecation. See <a href="https://github.blog/changelog/2023-09-22-github-actions-transitioning-from-node-16-to-node-20/">this post from GitHub</a> on how to migrate.</li>
<li>Tokenless uploading is unsupported. However, PRs made from forks to the upstream public repos will support tokenless (e.g. contributors to OS projects do not need the upstream repo's Codecov token). This <a href="https://docs.codecov.com/docs/adding-the-codecov-token#github-actions">doc</a> shows instructions on how to add the Codecov token.</li>
<li>OS platforms have been added, though some may not be automatically detected. To see a list of platforms, see our <a href="https://cli.codecov.io">CLI download page</a></li>
<li>Various arguments to the Action have been changed. Please be aware that the arguments match with the CLI's needs</li>
</ul>
<p><code>v3</code> versions and below will not have access to CLI features (e.g. global upload token, ATS).</p>
<h2>What's Changed</h2>
<ul>
<li>build(deps): bump openpgp from 5.8.0 to 5.9.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/985">codecov/codecov-action#985</a></li>
<li>build(deps): bump actions/checkout from 3.0.0 to 3.5.3 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1000">codecov/codecov-action#1000</a></li>
<li>build(deps): bump ossf/scorecard-action from 2.1.3 to 2.2.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1006">codecov/codecov-action#1006</a></li>
<li>build(deps): bump tough-cookie from 4.0.0 to 4.1.3 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1013">codecov/codecov-action#1013</a></li>
<li>build(deps-dev): bump word-wrap from 1.2.3 to 1.2.4 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1024">codecov/codecov-action#1024</a></li>
<li>build(deps): bump node-fetch from 3.3.1 to 3.3.2 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1031">codecov/codecov-action#1031</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.1.4 to 20.4.5 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1032">codecov/codecov-action#1032</a></li>
<li>build(deps): bump github/codeql-action from 1.0.26 to 2.21.2 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1033">codecov/codecov-action#1033</a></li>
<li>build commit,report and upload args based on codecovcli by <a href="https://github.com/dana-yaish"><code>@​dana-yaish</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/943">codecov/codecov-action#943</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.4.5 to 20.5.3 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1055">codecov/codecov-action#1055</a></li>
<li>build(deps): bump github/codeql-action from 2.21.2 to 2.21.4 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1051">codecov/codecov-action#1051</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.5.3 to 20.5.4 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1058">codecov/codecov-action#1058</a></li>
<li>chore(deps): update outdated deps by <a href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1059">codecov/codecov-action#1059</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.5.4 to 20.5.6 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1060">codecov/codecov-action#1060</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from 6.4.1 to 6.5.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1065">codecov/codecov-action#1065</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 6.4.1 to 6.5.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1064">codecov/codecov-action#1064</a></li>
<li>build(deps): bump actions/checkout from 3.5.3 to 3.6.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1063">codecov/codecov-action#1063</a></li>
<li>build(deps-dev): bump eslint from 8.47.0 to 8.48.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1061">codecov/codecov-action#1061</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.5.6 to 20.5.7 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1062">codecov/codecov-action#1062</a></li>
<li>build(deps): bump openpgp from 5.9.0 to 5.10.1 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1066">codecov/codecov-action#1066</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.5.7 to 20.5.9 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1070">codecov/codecov-action#1070</a></li>
<li>build(deps): bump github/codeql-action from 2.21.4 to 2.21.5 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1069">codecov/codecov-action#1069</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 6.5.0 to 6.6.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1072">codecov/codecov-action#1072</a></li>
<li>Update README.md by <a href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1073">codecov/codecov-action#1073</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from 6.5.0 to 6.6.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1071">codecov/codecov-action#1071</a></li>
<li>build(deps-dev): bump <code>@​vercel/ncc</code> from 0.36.1 to 0.38.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1074">codecov/codecov-action#1074</a></li>
<li>build(deps): bump <code>@​actions/core</code> from 1.10.0 to 1.10.1 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1081">codecov/codecov-action#1081</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 6.6.0 to 6.7.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1080">codecov/codecov-action#1080</a></li>
<li>build(deps): bump actions/checkout from 3.6.0 to 4.0.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1078">codecov/codecov-action#1078</a></li>
<li>build(deps): bump actions/upload-artifact from 3.1.2 to 3.1.3 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1077">codecov/codecov-action#1077</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 20.5.9 to 20.6.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1075">codecov/codecov-action#1075</a></li>
<li>build(deps-dev): bump <code>@​typescript-eslint/parser</code> from 6.6.0 to 6.7.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1079">codecov/codecov-action#1079</a></li>
<li>build(deps-dev): bump eslint from 8.48.0 to 8.49.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1076">codecov/codecov-action#1076</a></li>
<li>use cli instead of node uploader by <a href="https://github.com/dana-yaish"><code>@​dana-yaish</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1068">codecov/codecov-action#1068</a></li>
<li>chore(release): 4.0.0-beta.1 by <a href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1084">codecov/codecov-action#1084</a></li>
<li>not adding -n if empty to do-upload command by <a href="https://github.com/dana-yaish"><code>@​dana-yaish</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1085">codecov/codecov-action#1085</a></li>
<li>4.0.0-beta.2 by <a href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1086">codecov/codecov-action#1086</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md">codecov/codecov-action's changelog</a>.</em></p>
<blockquote>
<h2>4.0.0-beta.2</h2>
<h3>Fixes</h3>
<ul>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/1085">#1085</a> not adding -n if empty to do-upload command</li>
</ul>
<h2>4.0.0-beta.1</h2>
<p><code>v4</code> represents a move from the <a href="https://github.com/codecov/uploader">universal uploader</a> to the <a href="https://github.com/codecov/codecov-cli">Codecov CLI</a>. Although this will unlock new features for our users, the CLI is not yet at feature parity with the universal uploader.</p>
<h3>Breaking Changes</h3>
<ul>
<li>No current support for <code>aarch64</code> and <code>alpine</code> architectures.</li>
<li>Tokenless uploading is unsuported</li>
<li>Various arguments to the Action have been removed</li>
</ul>
<h2>3.1.4</h2>
<h3>Fixes</h3>
<ul>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/967">#967</a> Fix typo in README.md</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/971">#971</a> fix: add back in working dir</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/969">#969</a> fix: CLI option names for uploader</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/970">#970</a> build(deps-dev): bump <code>@​types/node</code> from 18.15.12 to 18.16.3</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/979">#979</a> build(deps-dev): bump <code>@​types/node</code> from 20.1.0 to 20.1.2</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/981">#981</a> build(deps-dev): bump <code>@​types/node</code> from 20.1.2 to 20.1.4</li>
</ul>
<h2>3.1.3</h2>
<h3>Fixes</h3>
<ul>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/960">#960</a> fix: allow for aarch64 build</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/957">#957</a> build(deps-dev): bump jest-junit from 15.0.0 to 16.0.0</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/958">#958</a> build(deps): bump openpgp from 5.7.0 to 5.8.0</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/959">#959</a> build(deps-dev): bump <code>@​types/node</code> from 18.15.10 to 18.15.12</li>
</ul>
<h2>3.1.2</h2>
<h3>Fixes</h3>
<ul>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/718">#718</a> Update README.md</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/851">#851</a> Remove unsupported path_to_write_report argument</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/898">#898</a> codeql-analysis.yml</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/901">#901</a> Update README to contain correct information - inputs and negate feature</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/955">#955</a> fix: add in all the extra arguments for uploader</li>
</ul>
<h3>Dependencies</h3>
<ul>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/819">#819</a> build(deps): bump openpgp from 5.4.0 to 5.5.0</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/835">#835</a> build(deps): bump node-fetch from 3.2.4 to 3.2.10</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/840">#840</a> build(deps): bump ossf/scorecard-action from 1.1.1 to 2.0.4</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/841">#841</a> build(deps): bump <code>@​actions/core</code> from 1.9.1 to 1.10.0</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/843">#843</a> build(deps): bump <code>@​actions/github</code> from 5.0.3 to 5.1.1</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/869">#869</a> build(deps): bump node-fetch from 3.2.10 to 3.3.0</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/872">#872</a> build(deps-dev): bump jest-junit from 13.2.0 to 15.0.0</li>
<li><a href="https://redirect.github.com/codecov/codecov-action/issues/879">#879</a> build(deps): bump decode-uri-component from 0.2.0 to 0.2.2</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/codecov/codecov-action/commit/f30e4959ba63075080d4f7f90cacc18d9f3fafd7"><code>f30e495</code></a> fix: update action.yml (<a href="https://redirect.github.com/codecov/codecov-action/issues/1240">#1240</a>)</li>
<li><a href="https://github.com/codecov/codecov-action/commit/a7b945cea47ad44d8340fae2b004cb982191264f"><code>a7b945c</code></a> fix: allow for other archs (<a href="https://redirect.github.com/codecov/codecov-action/issues/1239">#1239</a>)</li>
<li><a href="https://github.com/codecov/codecov-action/commit/98ab2c591b94478f4c3606d68ff73601df85ec43"><code>98ab2c5</code></a> Update package.json (<a href="https://redirect.github.com/codecov/codecov-action/issues/1238">#1238</a>)</li>
<li><a href="https://github.com/codecov/codecov-action/commit/43235cc5aeeafd8aeb836fe7d647599acead161c"><code>43235cc</code></a> Update README.md (<a href="https://redirect.github.com/codecov/codecov-action/issues/1237">#1237</a>)</li>
<li><a href="https://github.com/codecov/codecov-action/commit/0cf8684c821546a4e0c8c9a4cf4f21a7a0c5014b"><code>0cf8684</code></a> chore(ci): bump to node20 (<a href="https://redirect.github.com/codecov/codecov-action/issues/1236">#1236</a>)</li>
<li><a href="https://github.com/codecov/codecov-action/commit/8e1e730371bf82c744e8ca9aa469e2b7011542ce"><code>8e1e730</code></a> build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 6.19.1 to 6.20.0 ...</li>
<li><a href="https://github.com/codecov/codecov-action/commit/61293af0e8288c75266030376a088c781ec81c18"><code>61293af</code></a> build(deps-dev): bump <code>@​typescript-eslint/parser</code> from 6.19.1 to 6.20.0 (<a href="https://redirect.github.com/codecov/codecov-action/issues/1235">#1235</a>)</li>
<li><a href="https://github.com/codecov/codecov-action/commit/7a070cbe5b1002a522fa2d9446be76dd6abb709c"><code>7a070cb</code></a> build(deps): bump github/codeql-action from 3.23.1 to 3.23.2 (<a href="https://redirect.github.com/codecov/codecov-action/issues/1231">#1231</a>)</li>
<li><a href="https://github.com/codecov/codecov-action/commit/909716592cc1daab655af36c98983bea3bca03e8"><code>9097165</code></a> build(deps): bump actions/upload-artifact from 4.2.0 to 4.3.0 (<a href="https://redirect.github.com/codecov/codecov-action/issues/1232">#1232</a>)</li>
<li><a href="https://github.com/codecov/codecov-action/commit/ac042eaf0f05ddb0e0049e8761603887a080adc3"><code>ac042ea</code></a> build(deps-dev): bump <code>@​typescript-eslint/eslint-plugin</code> from 6.19.0 to 6.19.1 ...</li>
<li>Additional commits viewable in <a href="https://github.com/codecov/codecov-action/compare/v3...v4">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=codecov/codecov-action&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions


</details>

codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/remeda/remeda/pr/483/builds/467548) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit d91dcde2db86d309b441b616ac5eaaa6a44b3af5:

|Sandbox| Source |
|--|--|
|[remeda-example-vanilla](https://codesandbox.io/s/remeda-example-vanilla-6q6hg7)| Configuration |


Patch:
diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
index daf342f..8e544ad 100644
--- a/.github/workflows/codecov.yml
+++ b/.github/workflows/codecov.yml
@@ -24,4 +24,4 @@ jobs:
         run: npm run coverage
 
       - name: 🗺️ Upload coverage
-        uses: codecov/codecov-action@v3
+        uses: codecov/codecov-action@v4
diff --git a/mapping.md b/mapping.md
index de663b2..cb56353 100644
--- a/mapping.md
+++ b/mapping.md
@@ -120,13 +120,17 @@ provide the exact functionality of the listed Lodash and Ramda functions.
 Be sure to consult each library's documentation and to check what features
 you are relying on when migrating._
 
-| Lodash     | Ramda    | TypeScript                    |
-| ---------- | -------- | ----------------------------- |
-|            | `append` | `(arr, val) => [...arr, val]` |
-| `constant` | `always` | `x => () => x`                |
-| `nth`      | `nth`    | `n => a[n]`                   |
-|            | `path`   | `a?.b?.c`                     |
-|            | `propEq` | `x => x.a === value`          |
-|            | `propOr` | `x => x.a ?? defaultValue`    |
-| `words`    |          | `str => str.split(/\s+/)`     |
-| `compact`  |          | `R.filter(R.isTruthy)`        |
+| Lodash     | Ramda    | TypeScript                        |
+| ---------- | -------- | --------------------------------- |
+|            | `append` | `(arr, val) => [...arr, val]`     |
+| `constant` | `always` | `x => () => x`                    |
+| `nth`      | `nth`    | `n => a[n]`                       |
+|            | `path`   | `a?.b?.c`                         |
+|            | `propEq` | `x => x.a === value`              |
+|            | `propOr` | `x => x.a ?? defaultValue`        |
+| `words`    |          | `str => str.split(/\s+/)`         |
+| `compact`  |          | `R.filter(R.isTruthy)`            |
+| `max`      | `max`    | `R.firstBy([R.identity, "desc"])` |
+| `maxBy`    | `maxBy`  | `R.firstBy([fn, "desc"])`         |
+| `min`      | `min`    | `R.firstBy(R.identity)`           |
+| `minBy`    | `minBy`  | `R.firstBy(fn)`                   |
diff --git a/src/_heap.ts b/src/_heap.ts
new file mode 100644
index 0000000..17d54aa
--- /dev/null
+++ b/src/_heap.ts
@@ -0,0 +1,96 @@
+/**
+ * Heap related utilities.
+ */
+
+import { swapInPlace } from './_swapInPlace';
+import type { CompareFunction } from './_types';
+
+/**
+ * Mutates an array into a "max"-heap based on `compareFn` so that for any `item` in the heap, `compareFn(heap[0], item) > 0`
+ *
+ * @param heap - The array to be heapified. The array would be mutated!
+ * @param compareFn - The comparator used to order items in the heap. Use the
+ * same function in all calls mutating the same heap otherwise you'd get
+ * unexpected results.
+ * @returns - void, the result is in the provided heap array.
+ */
+export function heapify<T>(
+  heap: Array<T>,
+  compareFn: CompareFunction<T>
+): void {
+  for (let i = Math.floor(heap.length / 2) - 1; i >= 0; i--) {
+    heapSiftDown(heap, i, compareFn);
+  }
+}
+
+/**
+ * Insert an item into a heap if it's "smaller" (in regards to `compareFn`) than
+ * the current head of the heap (which is the "largest" value in the heap). If
+ * the item is inserted, the previous head of the heap is returned, otherwise
+ * `undefined` is returned and the heap is unchanged.
+ *
+ * @param heap - a *mutable* array representing a heap (see `heapify`).
+ * @param compareFn - The comparator used to order items in the heap. Use the
+ * @param item - The item to be inserted into the heap.
+ * @returns `undefined` if the heap is unchanged, or the previous head of the
+ * heap if the item was inserted.
+ */
+export function heapMaybeInsert<T>(
+  heap: Array<T>,
+  compareFn: CompareFunction<T>,
+  item: T
+): T | undefined {
+  const head = heap[0];
+
+  if (compareFn(item, head) >= 0) {
+    // The item shouldn't be inserted into the heap, the heap is unchanged.
+    return;
+  }
+
+  heap[0] = item;
+  heapSiftDown(heap, 0, compareFn);
+  return head;
+}
+
+/**
+ * The main heap operation. Takes a `heap` and an `index` and sifts the item
+ * down the heap until it reaches the correct position based on `compareFn`,
+ * swapping other items in the process.
+ */
+function heapSiftDown<T>(
+  heap: Array<T>,
+  index: number,
+  compareFn: CompareFunction<T>
+): void {
+  let currentIndex = index;
+
+  // The loop continues while the currentIndex has children in the heap.
+  while (currentIndex * 2 + 1 < heap.length) {
+    const firstChildIndex = currentIndex * 2 + 1;
+
+    let swapIndex =
+      compareFn(heap[currentIndex], heap[firstChildIndex]) < 0
+        ? // Is the parent "smaller" (in regards to `compareFn`) to its child?
+          firstChildIndex
+        : currentIndex;
+
+    const secondChildIndex = firstChildIndex + 1;
+    if (
+      secondChildIndex < heap.length &&
+      compareFn(heap[swapIndex], heap[secondChildIndex]) < 0
+    ) {
+      // Is there a second child? Is it the smallest of the three?
+      swapIndex = secondChildIndex;
+    }
+
+    if (swapIndex === currentIndex) {
+      // We assume the array is a heap and the existing order of items satisfies
+      // the compareFn so we can stop here.
+      return;
+    }
+
+    swapInPlace(heap, currentIndex, swapIndex);
+
+    currentIndex = swapIndex;
+  }
+}
diff --git a/src/_purryOrderRules.ts b/src/_purryOrderRules.ts
new file mode 100644
index 0000000..657d338
--- /dev/null
+++ b/src/_purryOrderRules.ts
@@ -0,0 +1,175 @@
+import type { CompareFunction, NonEmptyArray } from './_types';
+
+// We define the comparators in a global const so that they are only
+// instantiated once, and so we can couple a label (string) for them that could
+// be used in runtime to refer to them (e.g. "asc", "desc").
+const COMPARATORS = {
+  asc: <T>(x: T, y: T) => x > y,
+  desc: <T>(x: T, y: T) => x < y,
+} as const;
+
+/**
+ * An order rule defines a projection/extractor that returns a comparable from
+ * the data being compared. It would be run on each item being compared, and a
+ * comparator would then be used on the results to determine the order.
+ *
+ * There are 2 forms of the order rule, a simple one which only provides the
+ * projection function and assumes ordering is ascending, and a 2-tuple where
+ * the first element is the projection function and the second is the direction;
+ * this allows changing the direction without defining a more complex projection
+ * to simply negate the value (e.g. `(x) => -x`).
+ *
+ * We rely on the javascript implementation of `<` and `>` for comparison, which
+ * will attempt to transform both operands into a primitive comparable value via
+ * the built in `valueOf` function (and then `toString`). It's up to the caller
+ * to make sure that the projection is returning a value that makes sense for
+ * this logic.
+ *
+ * It's important to note that there is no built-in caching/memoization of
+ * projection function and therefore no guarantee that it would only be called
+ * once.
+ */
+export type OrderRule<T> =
+  | Projection<T>
+  | readonly [projection: Projection<T>, direction: keyof typeof COMPARATORS];
+
+type Projection<T> = (x: T) => Comparable;
+
+// We define the Comparable based on how JS coerces values into primitives when
+// used with the `<` and `>` operators.
+// @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#type_coercion
+type Comparable =
+  | ComparablePrimitive
+  | { [Symbol.toPrimitive](hint: string): ComparablePrimitive }
+  | { valueOf(): ComparablePrimitive }
+  | { toString(): string };
+
+//  Notice that `boolean` is special in that it is coerced as a number (0 for
+// `false`, 1 for `true`) implicitly.
+type ComparablePrimitive = number | string | boolean;
+
+/**
+ * Allows functions that want to handle a variadic number of order rules a
+ * a simplified API that hides most of the implementation details. The only
+ * thing users of this function need to do is provide a function that would take
+ * the data, and a compare function that can be used to determine the order
+ * between the items of the array.
+ * This functions takes care of the rest; it will parse rules, built the
+ * comparer, and manage the purrying of the input arguments.
+ */
+export function purryOrderRules<T>(
+  func: (data: ReadonlyArray<T>, compareFn: CompareFunction<T>) => unknown,
+  inputArgs: IArguments | ReadonlyArray<unknown>
+): unknown {
+  // We rely on casting blindly here, but we rely on casting blindly everywhere
+  // else when we call purry so it's fine...
+  const [dataOrRule, ...rules] = (
+    Array.isArray(inputArgs) ? inputArgs : Array.from(inputArgs)
+  ) as
+    | [data: ReadonlyArray<T>, ...rules: Readonly<NonEmptyArray<OrderRule<T>>>]
+    | Readonly<NonEmptyArray<OrderRule<T>>>;
+
+  if (!isOrderRule<T>(dataOrRule)) {
+    // dataFirst!
+
+    // @ts-expect-error [ts2556]: Typescript is failing to infer the type of rules
+    // correctly here after the type refinement above, rules should be non-empty
+    // when we get here.
+    const compareFn = orderRuleComparer(...rules);
+    return func(dataOrRule, compareFn);
+  }
+
+  // dataLast!
+
+  // Important: initialize the comparer outside of the returned function so it
+  // it's constructed and shared everywhere (it's stateless so should be safe
+  // if used multiple times).
+  const compareFn = orderRuleComparer(dataOrRule, ...rules);
+  return (data: ReadonlyArray<T>) => func(data, compareFn);
+}
+
+/**
+ * Some functions need an extra number argument, this helps facilitate that.
+ */
+export function purryOrderRulesWithArgument(
+  func: <T>(
+    data: ReadonlyArray<T>,
+    compareFn: CompareFunction<T>,
+    arg: any
+  ) => unknown,
+  inputArgs: IArguments
+): unknown {
+  const [first, second, ...rest] = Array.from(inputArgs);
+
+  // We need to pull the `n` argument out to make it work with purryOrderRules.
+  let arg: unknown;
+  let argRemoved: ReadonlyArray<unknown>;
+  if (isOrderRule(second)) {
+    // dataLast!
+    arg = first;
+    argRemoved = [second, ...rest];
+  } else {
+    // dataFirst!
+    arg = second;
+    argRemoved = [first, ...rest];
+  }
+
+  return purryOrderRules((...args) => func(...args, arg), argRemoved);
+}
+
+function orderRuleComparer<T>(
+  primaryRule: OrderRule<T>,
+  secondaryRule?: OrderRule<T>,
+  ...otherRules: ReadonlyArray<OrderRule<T>>
+): (a: T, b: T) => number {
+  const projector =
+    typeof primaryRule === 'function' ? primaryRule : primaryRule[0];
+
+  const direction = typeof primaryRule === 'function' ? 'asc' : primaryRule[1];
+  const comparator = COMPARATORS[direction];
+
+  const nextComparer =
+    secondaryRule === undefined
+      ? undefined
+      : orderRuleComparer(secondaryRule, ...otherRules);
+
+  return (a, b) => {
+    const projectedA = projector(a);
+    const projectedB = projector(b);
+
+    if (comparator(projectedA, projectedB)) {
+      return 1;
+    }
+
+    if (comparator(projectedB, projectedA)) {
+      return -1;
+    }
+
+    // The elements are equal base on the current comparator and projection. So
+    // we need to check the elements using the next comparer, if one exists,
+    // otherwise we consider them as true equal (returning 0).
+    return nextComparer?.(a, b) ?? 0;
+  };
+}
+
+function isOrderRule<T>(x: unknown): x is OrderRule<T> {
+  if (isProjection(x)) {
+    return true;
+  }
+
+  if (typeof x !== 'object' || !Array.isArray(x)) {
+    return false;
+  }
+
+  const [maybeProjection, maybeDirection, ...rest] = x;
+
+  return (
+    isProjection(maybeProjection) &&
+    maybeDirection in COMPARATORS &&
+    // Has to be a 2-tuple
+    rest.length === 0
+  );
+}
+
+const isProjection = <T>(x: unknown): x is Projection<T> =>
+  typeof x === 'function' && x.length === 1;
diff --git a/src/_quickSelect.ts b/src/_quickSelect.ts
new file mode 100644
index 0000000..79d83a9
--- /dev/null
+++ b/src/_quickSelect.ts
@@ -0,0 +1,88 @@
+/**
+ * A simple implementation of the *QuickSelect* algorithm.
+ * @see https://en.wikipedia.org/wiki/Quickselect
+ */
+
+import { swapInPlace } from './_swapInPlace';
+import type { CompareFunction } from './_types';
+
+/**
+ * Perform QuickSelect on the given data. Notice that the data would be cloned
+ * shallowly so that it could be mutated in-place, and then discarded once the
+ * algorithm is done. This means that running this function multiple times on
+ * the same array might be slower then sorting the array before.
+ * @param data - The data to perform the selection on.
+ * @param index - The index of the item we are looking for.
+ * @param compareFn - The compare function to use for sorting.
+ * @returns The item at the given index, or `undefined` if the index is out-of-
+ * bounds.
+ */
+export const quickSelect = <T>(
+  data: ReadonlyArray<T>,
+  index: number,
+  compareFn: CompareFunction<T>
+): T | undefined =>
+  index < 0 || index >= data.length
+    ? // Quickselect doesn't work with out-of-bound indices
+      undefined
+    : quickSelectImplementation(
+        // We need to clone the array because quickSelect mutates it in-place.
+        [...data],
+        0 /* left */,
+        data.length - 1 /* right */,
+        index,
+        compareFn
+      );
+
+/**
+ * The actual implementation, called recursively.
+ */
+function quickSelectImplementation<T>(
+  data: Array<T>,
+  left: number,
+  right: number,
+  index: number,
+  compareFn: CompareFunction<T>
+): T {
+  if (left === right) {
+    return data[left];
+  }
+
+  const pivotIndex = partition(data, left, right, compareFn);
+
+  return index === pivotIndex
+    ? // Once a pivot is chosen it's location is final, so if it matches the
+      // index we found out item!
+      data[index]
+    : quickSelectImplementation(
+        data,
+        // We continue by recursing into the partition where index would be
+        index < pivotIndex ? left : pivotIndex + 1,
+        index < pivotIndex ? pivotIndex - 1 : right,
+        index,
+        compareFn
+      );
+}
+
+function partition<T>(
+  data: Array<T>,
+  left: number,
+  right: number,
+  compareFn: CompareFunction<T>
+): number {
+  const pivot = data[right];
+
+  let i = left;
+  for (let j = left; j < right; j++) {
+    if (compareFn(data[j], pivot) < 0) {
+      // Move items smaller then the pivot to the start of the array.
+      swapInPlace(data, i, j);
+      i++;
+    }
+  }
+
+  swapInPlace(data, i, right);
+
+  // The location of the pivot by the end of the partition function.
+  return i;
+}
diff --git a/src/_swapInPlace.ts b/src/_swapInPlace.ts
new file mode 100644
index 0000000..0541266
--- /dev/null
+++ b/src/_swapInPlace.ts
@@ -0,0 +1,8 @@
+/**
+ * An efficient hack to swap the values at two indices in an array *in-place*.
+ */
+export function swapInPlace(data: Array<unknown>, i: number, j: number): void {
+  // We use destructuring to perform an in-place swap *without* needing a
+  // temporary variable
+  [data[i], data[j]] = [data[j], data[i]];
+}
diff --git a/src/_types.ts b/src/_types.ts
index 2af773b..ba51ba4 100644
--- a/src/_types.ts
+++ b/src/_types.ts
@@ -56,6 +56,12 @@ type BuildTupleHelper<
   ? readonly [...Rest] // Terminate with readonly array (aka tuple)
   : BuildTupleHelper<Element, Length, [Element, ...Rest]>;
 
+/**
+ * A compare function that is compatible with the native `Array.sort` function.
+ * @returns >0 if `a` should come after `b`, 0 if they are equal, and <0 if `a` should come before `b`.
+ */
+export type CompareFunction<T> = (a: T, b: T) => number;
+
 /**
  * Based on type-fest's IsAny
  * @see https://github.com/sindresorhus/type-fest/blob/main/source/is-any.d.ts
diff --git a/src/dropFirstBy.ts b/src/dropFirstBy.ts
new file mode 100644
index 0000000..bf15717
--- /dev/null
+++ b/src/dropFirstBy.ts
@@ -0,0 +1,77 @@
+import { heapify, heapMaybeInsert } from './_heap';
+import { OrderRule, purryOrderRulesWithArgument } from './_purryOrderRules';
+import type { CompareFunction, NonEmptyArray } from './_types';
+
+/**
+ * Drop the first `n` items from `data` based on the provided ordering criteria. This allows you to avoid sorting the array before dropping the items. The complexity of this function is *O(Nlogn)* where `N` is the length of the array.
+ *
+ * For the opposite operation (to keep `n` elements) see `takeFirstBy`.
+ *
+ * @params data - the input array
+ * @params n - the number of items to drop. If `n` is non-positive no items would be dropped and a *clone* of the input would be returned, if `n` is bigger then data.length no items would be returned.
+ * @param rules - A variadic array of order rules defining the sorting criteria. Each order rule is a projection function that extracts a comparable value from the data. Sorting is based on these extracted values using the native `<` and `>` operators. Earlier rules take precedence over later ones. Use the syntax `[projection, "desc"]` for descending order.
+ * @returns a subset of the input array.
+ * @signature
+ *   R.dropFirstBy(data, n, ...rules);
+ * @example
+ *   R.dropFirstBy(['aa', 'aaaa', 'a', 'aaa'], 2, x => x.length); // => ['aaa', 'aaaa']
+ * @dataFirst
+ * @category Array
+ */
+export function dropFirstBy<T>(
+  data: ReadonlyArray<T>,
+  n: number,
+  ...rules: Readonly<NonEmptyArray<OrderRule<T>>>
+): Array<T>;
+
+/**
+ * Drop the first `n` items from `data` based on the provided ordering criteria. This allows you to avoid sorting the array before dropping the items. The complexity of this function is *O(Nlogn)* where `N` is the length of the array.
+ *
+ * For the opposite operation (to keep `n` elements) see `takeFirstBy`.
+ *
+ * @params data - the input array
+ * @params n - the number of items to drop. If `n` is non-positive no items would be dropped and a *clone* of the input would be returned, if `n` is bigger then data.length no items would be returned.
+ * @param rules - A variadic array of order rules defining the sorting criteria. Each order rule is a projection function that extracts a comparable value from the data. Sorting is based on these extracted values using the native `<` and `>` operators. Earlier rules take precedence over later ones. Use the syntax `[projection, "desc"]` for descending order.
+ * @returns a subset of the input array.
+ * @signature
+ *   R.dropFirstBy(n, ...rules)(data);
+ * @example
+ *   R.pipe(['aa', 'aaaa', 'a', 'aaa'], R.dropFirstBy(2, x => x.length)); // => ['aaa', 'aaaa']
+ * @dataLast
+ * @category Array
+ */
+export function dropFirstBy<T>(
+  n: number,
+  ...rules: Readonly<NonEmptyArray<OrderRule<T>>>
+): (data: ReadonlyArray<T>) => Array<T>;
+
+export function dropFirstBy(): unknown {
+  return purryOrderRulesWithArgument(dropFirstByImplementation, arguments);
+}
+
+function dropFirstByImplementation<T>(
+  data: ReadonlyArray<T>,
+  compareFn: CompareFunction<T>,
+  n: number
+): Array<T> {
+  if (n >= data.length) {
+    return [];
+  }
+
+  if (n <= 0) {
+    return [...data];
+  }
+
+  const heap = data.slice(0, n);
+  heapify(heap, compareFn);
+
+  const out = [];
+
+  const rest = data.slice(n);
+  for (const item of rest) {
+    const previousHead = heapMaybeInsert(heap, compareFn, item);
+    out.push(previousHead ?? item);
+  }
+
+  return out;
+}
diff --git a/src/firstBy.ts b/src/firstBy.ts
new file mode 100644
index 0000000..e581de7
--- /dev/null
+++ b/src/firstBy.ts
@@ -0,0 +1,101 @@
+import { OrderRule, purryOrderRules } from './_purryOrderRules';
+import type {
+  CompareFunction,
+  IterableContainer,
+  NonEmptyArray,
+} from './_types';
+import { hasAtLeast } from './hasAtLeast';
+
+type FirstBy<T extends IterableContainer> =
+  | T[number]
+  | (T extends readonly [unknown, ...ReadonlyArray<unknown>]
+      ? never
+      : T extends readonly [...ReadonlyArray<unknown>, unknown]
+        ? never
+        : undefined);
+
+/**
+ * Find the first element in the array that adheres to the order rules provided. This is a superset of what a typical `maxBy` or `minBy` function would do as it allows defining "tie-breaker" rules when values are equal, and allows comparing items using any logic. This function is equivalent to calling `R.first(R.sortBy(...))` but runs at *O(n)* instead of *O(nlogn)*.
+ *
+ * Use `nthBy` if you need an element other that the first, or `takeFirstBy` if you more than just the first element.
+ *
+ * @param data an array of items
+ * @param rules - A variadic array of order rules defining the sorting criteria. Each order rule is a projection function that extracts a comparable value from the data. Sorting is based on these extracted values using the native `<` and `>` operators. Earlier rules take precedence over later ones. Use the syntax `[projection, "desc"]` for descending order.
+ * @returns the first element by the order criteria, or `undefined` if the array
+ * is empty. (The function provides strong typing if the input type assures the
+ * array isn't empty).
+ * @signature
+ *   R.firstBy(...rules)(data);
+ * @example
+ *   const max = R.pipe([1,2,3], R.firstBy([R.identity, "desc"])); // => 3;
+ *   const min = R.pipe([1,2,3], R.firstBy([1,2,3])); // => 1;
+ *
+ *   const data = [{ a: "a" }, { a: "aa" }, { a: "aaa" }] as const;
+ *   const maxBy = R.pipe(data, R.firstBy([(item) => item.a.length, "desc"])); // => { a: "aaa" };
+ *   const minBy = R.pipe(data, R.firstBy((item) => item.a.length)); // => { a: "a" };
+ *
+ *   const data = [{type: "cat", size: 1}, {type: "cat", size: 2}, {type: "dog", size: 3}] as const;
+ *   const multi = R.pipe(data, R.firstBy(R.prop('type'), [R.prop('size'), 'desc'])); // => {type: "cat", size: 2}
+ * @dataLast
+ * @category Array
+ */
+export function firstBy<T extends IterableContainer>(
+  ...rules: Readonly<NonEmptyArray<OrderRule<T[number]>>>
+): (data: T) => FirstBy<T>;
+
+/**
+ * Find the first element in the array that adheres to the order rules provided. This is a superset of what a typical `maxBy` or `minBy` function would do as it allows defining "tie-breaker" rules when values are equal, and allows comparing items using any logic. This function is equivalent to calling `R.first(R.sortBy(...))` but runs at *O(n)* instead of *O(nlogn)*.
+ *
+ * Use `nthBy` if you need an element other that the first, or `takeFirstBy` if you more than just the first element.
+ *
+ * @param data an array of items
+ * @param rules - A variadic array of order rules defining the sorting criteria. Each order rule is a projection function that extracts a comparable value from the data. Sorting is based on these extracted values using the native `<` and `>` operators. Earlier rules take precedence over later ones. Use the syntax `[projection, "desc"]` for descending order.
+ * @returns the first element by the order criteria, or `undefined` if the array
+ * is empty. (The function provides strong typing if the input type assures the
+ * array isn't empty).
+ * @signature
+ *   R.firstBy(data, ...rules);
+ * @example
+ *   const max = R.firstBy([1,2,3], [R.identity, "desc"]); // => 3;
+ *   const min = R.firstBy([1,2,3], R.identity); // => 1;
+ *
+ *   const data = [{ a: "a" }, { a: "aa" }, { a: "aaa" }] as const;
+ *   const maxBy = R.firstBy(data, [(item) => item.a.length, "desc"]); // => { a: "aaa" };
+ *   const minBy = R.firstBy(data, (item) => item.a.length); // => { a: "a" };
+ *
+ *   const data = [{type: "cat", size: 1}, {type: "cat", size: 2}, {type: "dog", size: 3}] as const;
+ *   const multi = R.firstBy(data, R.prop('type'), [R.prop('size'), 'desc']); // => {type: "cat", size: 2}
+ * @dataFirst
+ * @category Array
+ */
+export function firstBy<T extends IterableContainer>(
+  data: T,
+  ...rules: Readonly<NonEmptyArray<OrderRule<T[number]>>>
+): FirstBy<T>;
+
+export function firstBy(): unknown {
+  return purryOrderRules(firstByImplementation, arguments);
+}
+
+function firstByImplementation<T>(
+  data: ReadonlyArray<T>,
+  compareFn: CompareFunction<T>
+): T | undefined {
+  let [currentFirst] = data;
+
+  if (!hasAtLeast(data, 2)) {
+    // If we have 0 or 1 item we simply return the trivial result.
+    return currentFirst;
+  }
+
+  // Remove the first item, we won't compare it with itself.
+  const [, ...rest] = data;
+  for (const item of rest) {
+    if (compareFn(item, currentFirst) < 0) {
+      // item comes before currentFirst in the order.
+      currentFirst = item;
+    }
+  }
+
+  return currentFirst;
+}
diff --git a/src/index.ts b/src/index.ts
index 5b71ccb..1fc4569 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -15,6 +15,7 @@ export * from './difference';
 export * from './differenceWith';
 export * from './divide';
 export * from './drop';
+export * from './dropFirstBy';
 export * from './dropLast';
 export * from './dropLastWhile';
 export * from './dropWhile';
@@ -25,6 +26,7 @@ export * from './findIndex';
 export * from './findLast';
 export * from './findLastIndex';
 export * from './first';
+export * from './firstBy';
 export * from './flatMap';
 export * from './flatMapToObj';
 export * from './flatten';
@@ -74,6 +76,7 @@ export * from './mergeDeep';
 export * from './minBy';
 export * from './multiply';
 export * from './noop';
+export * from './nthBy';
 export * from './objOf';
 export * from './omit';
 export * from './omitBy';
@@ -87,6 +90,7 @@ export * from './prop';
 export * from './purry';
 export * from './randomString';
 export * from './range';
+export * from './rankBy';
 export * from './reduce';
 export * from './reject';
 export * from './reverse';
@@ -112,6 +116,7 @@ export * from './sumBy';
 export * from './swapIndices';
 export * from './swapProps';
 export * from './take';
+export * from './takeFirstBy';
 export * from './takeLastWhile';
 export * from './takeWhile';
 export * from './tap';
diff --git a/src/maxBy.ts b/src/maxBy.ts
index 4949953..e70674b 100644
--- a/src/maxBy.ts
+++ b/src/maxBy.ts
@@ -19,6 +19,9 @@ const _maxBy =
 
 /**
  * Returns the max element using the provided predicate.
+ *
+ * If you need more control over how "max" is defined, consider using `firstBy` instead. maxBy might be deprecated in the future!
+ *
  * @param fn the predicate
  * @signature
  *    R.maxBy(fn)(array)
diff --git a/src/minBy.ts b/src/minBy.ts
index a94d375..b324beb 100644
--- a/src/minBy.ts
+++ b/src/minBy.ts
@@ -19,6 +19,9 @@ const _minBy =
 
 /**
  * Returns the min element using the provided predicate.
+ *
+ * If you need more control over how "min" is defined, consider using `firstBy` instead. minBy might be deprecated in the future!
+ *
  * @param fn the predicate
  * @signature
  *    R.minBy(fn)(array)
diff --git a/src/nthBy.ts b/src/nthBy.ts
new file mode 100644
index 0000000..a42efa3
--- /dev/null
+++ b/src/nthBy.ts
@@ -0,0 +1,66 @@
+import { OrderRule, purryOrderRulesWithArgument } from './_purryOrderRules';
+import { quickSelect } from './_quickSelect';
+import type {
+  CompareFunction,
+  IterableContainer,
+  NonEmptyArray,
+} from './_types';
+
+/**
+ * Retrieves the element that would be at the given index if the array were sorted according to specified rules. This function uses the *QuickSelect* algorithm running at an average complexity of *O(n)*. Semantically it is equivalent to `sortBy(data, ...rules).at(index)` which would run at *O(nlogn)*.
+ *
+ * See also `firstBy` which provides an even more efficient algorithm and a stricter return type, but only for `index === 0`. See `takeFirstBy` to get all the elements up to and including `index`.
+ *
+ * @param data - The input array.
+ * @param index - The zero-based index for selecting the element in the sorted order. Negative indices count backwards from the end.
+ * @param rules - A variadic array of order rules defining the sorting criteria. Each order rule is a projection function that extracts a comparable value from the data. Sorting is based on these extracted values using the native `<` and `>` operators. Earlier rules take precedence over later ones. Use the syntax `[projection, "desc"]` for descending order.
+ * @returns The element at the specified index in the sorted order, or `undefined` if the index is out of bounds.
+ * @signature
+ *   R.nthBy(data, index, ...rules);
+ * @example
+ *   R.nthBy([2,1,4,5,3,], 2, identity); // => 3
+ * @dataFirst
+ * @category Array
+ */
+export function nthBy<T extends IterableContainer>(
+  data: T,
+  index: number,
+  ...rules: Readonly<NonEmptyArray<OrderRule<T[number]>>>
+): T[number] | undefined;
+
+/**
+ * Retrieves the element that would be at the given index if the array were sorted according to specified rules. This function uses the *QuickSelect* algorithm running at an average complexity of *O(n)*. Semantically it is equivalent to `sortBy(data, ...rules)[index]` which would run at *O(nlogn)*.
+ *
+ * See also `firstBy` which provides an even more efficient algorithm and a stricter return type, but only for `index === 0`. See `takeFirstBy` to get all the elements up to and including `index`.
+ *
+ * @param data - The input array.
+ * @param index - The zero-based index for selecting the element in the sorted order. Negative indices count backwards from the end.
+ * @param rules - A variadic array of order rules defining the sorting criteria. Each order rule is a projection function that extracts a comparable value from the data. Sorting is based on these extracted values using the native `<` and `>` operators. Earlier rules take precedence over later ones. Use the syntax `[projection, "desc"]` for descending order.
+ * @returns The element at the specified index in the sorted order, or `undefined` if the index is out of bounds.
+ * @signature
+ *   R.nthBy(index, ...rules)(data);
+ * @example
+ *   R.pipe([2,1,4,5,3,], R.nthBy(2, identity)); // => 3
+ * @dataLast
+ * @category Array
+ */
+export function nthBy<T extends IterableContainer>(
+  index: number,
+  ...rules: Readonly<NonEmptyArray<OrderRule<T[number]>>>
+): (data: T) => T[number] | undefined;
+
+export function nthBy(): unknown {
+  return purryOrderRulesWithArgument(nthByImplementation, arguments);
+}
+
+const nthByImplementation = <T>(
+  data: ReadonlyArray<T>,
+  compareFn: CompareFunction<T>,
+  index: number
+): T | undefined =>
+  quickSelect(
+    data,
+    // Allow negative indices gracefully
+    index >= 0 ? index : data.length + index,
+    compareFn
+  );
diff --git a/src/rankBy.ts b/src/rankBy.ts
new file mode 100644
index 0000000..3b80cfc
--- /dev/null
+++ b/src/rankBy.ts
@@ -0,0 +1,69 @@
+import { OrderRule, purryOrderRulesWithArgument } from './_purryOrderRules';
+import type { CompareFunction, NonEmptyArray } from './_types';
+
+/**
+ * Calculates the rank of an item in an array based on `rules`. The rank is the position where the item would appear in the sorted array. This function provides an efficient way to determine the rank in *O(n)* time, compared to *O(nlogn)* for the equivalent `sortedIndex(sortBy(data, ...rules), item)`.
+ *
+ * @param data - The input array.
+ * @param item - The item whose rank is to be determined.
+ * @param rules - A variadic array of order rules defining the sorting criteria. Each order rule is a projection function that extracts a comparable value from the data. Sorting is based on these extracted values using the native `<` and `>` operators. Earlier rules take precedence over later ones. Use the syntax `[projection, "desc"]` for descending order.
+ * @returns - The rank of the item in the sorted array in the range [0..data.length]
+ * @signature
+ *   R.rankBy(data, item, ...rules)
+ * @example
+ *   const DATA = [{ a: 5 }, { a: 1 }, { a: 3 }] as const;
+ *   R.rankBy(DATA, 0, R.prop('a')) // => 0
+ *   R.rankBy(DATA, 1, R.prop('a')) // => 1
+ *   R.rankBy(DATA, 2, R.prop('a')) // => 1
+ *   R.rankBy(DATA, 3, R.prop('a')) // => 2
+ * @dataFirst
+ * @category Array
+ */
+export function rankBy<T>(
+  data: ReadonlyArray<T>,
+  item: T,
+  ...rules: Readonly<NonEmptyArray<OrderRule<T>>>
+): number;
+
+/**
+ * Calculates the rank of an item in an array based on `rules`. The rank is the position where the item would appear in the sorted array. This function provides an efficient way to determine the rank in *O(n)* time, compared to *O(nlogn)* for the equivalent `sortedIndex(sortBy(data, ...rules), item)`.
+ *
+ * @param data - The input array.
+ * @param item - The item whose rank is to be determined.
+ * @param rules - A variadic array of order rules defining the sorting criteria. Each order rule is a projection function that extracts a comparable value from the data. Sorting is based on these extracted values using the native `<` and `>` operators. Earlier rules take precedence over later ones. Use the syntax `[projection, "desc"]` for descending order.
+ * @returns - The rank of the item in the sorted array in the range [0..data.length]
+ * @signature
+ *   R.rankBy(item, ...rules)(data)
+ * @example
+ *   const DATA = [{ a: 5 }, { a: 1 }, { a: 3 }] as const;
+ *   R.pipe(DATA, R.rankBy(0, R.prop('a'))) // => 0
+ *   R.pipe(DATA, R.rankBy(1, R.prop('a'))) // => 1
+ *   R.pipe(DATA, R.rankBy(2, R.prop('a'))) // => 1
+ *   R.pipe(DATA, R.rankBy(3, R.prop('a'))) // => 2
+ * @dataLast
+ * @category Array
+ */
+export function rankBy<T>(
+  item: T,
+  ...rules: Readonly<NonEmptyArray<OrderRule<T>>>
+): (data: ReadonlyArray<T>) => number;
+
+export function rankBy(): unknown {
+  return purryOrderRulesWithArgument(rankByImplementation, arguments);
+}
+
+function rankByImplementation<T>(
+  data: ReadonlyArray<T>,
+  compareFn: CompareFunction<T>,
+  targetItem: T
+): number {
+  let rank = 0;
+  for (const item of data) {
+    if (compareFn(targetItem, item) > 0) {
+      // The rank of the item is equivalent to the number of items that would
+      // come before it if the array was sorted. We assume that the
+      rank++;
+    }
+  }
+  return rank;
+}
diff --git a/src/sortBy.ts b/src/sortBy.ts
index 545d175..73147cb 100644
--- a/src/sortBy.ts
+++ b/src/sortBy.ts
@@ -1,37 +1,29 @@
-import type { IterableContainer, NonEmptyArray } from './_types';
-import { purry } from './purry';
-
-const ALL_DIRECTIONS = ['asc', 'desc'] as const;
-type Direction = (typeof ALL_DIRECTIONS)[number];
-
-type ComparablePrimitive = number | string | boolean;
-type Comparable = ComparablePrimitive | { valueOf(): ComparablePrimitive };
-type SortProjection<T> = (x: T) => Comparable;
-type SortPair<T> = readonly [
-  projector: SortProjection<T>,
-  direction: Direction,
-];
-type SortRule<T> = SortProjection<T> | SortPair<T>;
-
-const COMPARATOR = {
-  asc: <T>(x: T, y: T) => x > y,
-  desc: <T>(x: T, y: T) => x < y,
-} as const;
+import { OrderRule, purryOrderRules } from './_purryOrderRules';
+import type {
+  CompareFunction,
+  IterableContainer,
+  NonEmptyArray,
+} from './_types';
 
 /**
- * Sorts the list according to the supplied functions and directions.
- * Sorting is based on a native `sort` function. It's not guaranteed to be stable.
+ * Sorts `data` using the provided ordering rules. The `sort` is done via the native `Array.prototype.sort` but is performed on a shallow copy of the array to avoid mutating the original data.
  *
- * Directions are applied to functions in order and default to ascending if not specified.
+ * To maintain the shape of more complex inputs (like non-empty arrays, tuples, etc...) use the `strict` variant.
  *
- * If the input array is more complex (non-empty array, tuple, etc...) use the
- * strict mode to maintain it's shape.
+ * There are several other functions that take order rules and **bypass** the need to sort the array first (in *O(nlogn)* time):
+ * * `firstBy` === `first(sortBy(data, ...rules))`, O(n).
+ * * `takeFirstBy` === `take(sortBy(data, ...rules), k)`, O(nlogk).
+ * * `dropFirstBy` === `drop(sortBy(data, ...rules), k)`, O(nlogk).
+ * * `nthBy` === `sortBy(data, ...rules).at(k)`, O(n).
+ * * `rankBy` === `sortedIndex(sortBy(data, ...rules), item)`, O(n).
+ * Refer to the docs for more details.
  *
- * @param sortRule main sort rule
- * @param additionalSortRules subsequent sort rules (these are only relevant when two items are equal based on the previous sort rule)
+ * @param data - The input array.
+ * @param rules - A variadic array of order rules defining the sorting criteria. Each order rule is a projection function that extracts a comparable value from the data. Sorting is based on these extracted values using the native `<` and `>` operators. Earlier rules take precedence over later ones. Use the syntax `[projection, "desc"]` for descending order.
+ * @return - A shallow copy of the input array sorted by the provided rules.
  * @signature
- *    R.sortBy(sortRule, ...additionalSortRules)(array)
- *    R.sortBy.strict(sortRule, ...additionalSortRules)(array)
+ *    R.sortBy(...rules)(data)
+ *    R.sortBy.strict(...rules)(data)
  * @example
  *    R.pipe(
  *      [{ a: 1 }, { a: 3 }, { a: 7 }, { a: 2 }],
@@ -46,24 +38,28 @@ const COMPARATOR = {
  * @strict
  */
 export function sortBy<T>(
-  ...sortRules: Readonly<NonEmptyArray<SortRule<T>>>
-): (array: ReadonlyArray<T>) => Array<T>;
+  ...rules: Readonly<NonEmptyArray<OrderRule<T>>>
+): (data: ReadonlyArray<T>) => Array<T>;
 
 /**
- * Sorts the list according to the supplied functions and directions.
- * Sorting is based on a native `sort` function. It's not guaranteed to be stable.
+ * Sorts `data` using the provided ordering rules. The `sort` is done via the native `Array.prototype.sort` but is performed on a shallow copy of the array to avoid mutating the original data.
  *
- * Directions are applied to functions in order and default to ascending if not specified.
+ * To maintain the shape of more complex inputs (like non-empty arrays, tuples, etc...) use the `strict` variant.
  *
- * If the input array is more complex (non-empty array, tuple, etc...) use the
- * strict mode to maintain it's shape.
+ * There are several other functions that take order rules and **bypass** the need to sort the array first (in *O(nlogn)* time):
+ * * `firstBy` === `first(sortBy(data, ...rules))`, O(n).
+ * * `takeFirstBy` === `take(sortBy(data, ...rules), k)`, O(nlogk).
+ * * `dropFirstBy` === `drop(sortBy(data, ...rules), k)`, O(nlogk).
+ * * `nthBy` === `sortBy(data, ...rules).at(k)`, O(n).
+ * * `rankBy` === `sortedIndex(sortBy(data, ...rules), item)`, O(n).
+ * Refer to the docs for more details.
  *
- * @param array the array to sort
- * @param sortRule main sort rule
- * @param additionalSortRules subsequent sort rules (these are only relevant when two items are equal based on the previous sort rule)
+ * @param data - The input array.
+ * @param rules - A variadic array of order rules defining the sorting criteria. Each order rule is a projection function that extracts a comparable value from the data. Sorting is based on these extracted values using the native `<` and `>` operators. Earlier rules take precedence over later ones. Use the syntax `[projection, "desc"]` for descending order.
+ * @return - A shallow copy of the input array sorted by the provided rules.
  * @signature
- *    R.sortBy(array, sortRule, ...additionalSortRules)
- *    R.sortBy.strict(array, sortRule, ...additionalSortRules)
+ *    R.sortBy(data, ...rules)
+ *    R.sortBy.strict(data, ...rules)
  * @example
  *    R.sortBy(
  *      [{ a: 1 }, { a: 3 }, { a: 7 }, { a: 2 }],
@@ -98,95 +94,28 @@ export function sortBy<T>(
  */
 export function sortBy<T>(
   array: ReadonlyArray<T>,
-  ...sortRules: Readonly<NonEmptyArray<SortRule<T>>>
+  ...sortRules: Readonly<NonEmptyArray<OrderRule<T>>>
 ): Array<T>;
 
-export function sortBy<T>(
-  arrayOrSortRule: ReadonlyArray<T> | SortRule<T>,
-  ...sortRules: ReadonlyArray<SortRule<T>>
-): unknown {
-  const args = isSortRule(arrayOrSortRule)
-    ? // *data-last invocation*: put all sort rules into a single array to be
-      // passed as the first param.
-      [[arrayOrSortRule, ...sortRules]]
-    : // *data-first invocation*: put the arrayOrSort (which is array now) as
-      // the first param, and all the sorts (as an array) into the second param.
-      // `purry` would pick the right "flavour" based on the length of the
-      // params tuple.
-      [arrayOrSortRule, sortRules];
-
-  return purry(_sortBy, args);
-}
-
-function isSortRule<T>(x: ReadonlyArray<T> | SortRule<T>): x is SortRule<T> {
-  if (typeof x === 'function') {
-    // must be a SortProjection
-    return true;
-  }
-
-  const [maybeProjection, maybeDirection, ...rest] = x;
-
-  if (rest.length > 0) {
-    // Not a SortPair if we have more stuff in the array
-    return false;
-  }
-
-  return (
-    typeof maybeProjection === 'function' &&
-    ALL_DIRECTIONS.indexOf(maybeDirection as Direction) !== -1
-  );
+export function sortBy(): unknown {
+  return purryOrderRules(_sortBy, arguments);
 }
 
 const _sortBy = <T>(
-  array: ReadonlyArray<T>,
-  sorts: Readonly<NonEmptyArray<SortRule<T>>>
+  data: ReadonlyArray<T>,
+  compareFn: CompareFunction<T>
 ): Array<T> =>
   // Sort is done in-place so we need to copy the array.
-  [...array].sort(comparer(...sorts));
-
-function comparer<T>(
-  primaryRule: SortRule<T>,
-  secondaryRule?: SortRule<T>,
-  ...otherRules: ReadonlyArray<SortRule<T>>
-): (a: T, b: T) => number {
-  const projector =
-    typeof primaryRule === 'function' ? primaryRule : primaryRule[0];
-
-  const direction = typeof primaryRule === 'function' ? 'asc' : primaryRule[1];
-  const comparator = COMPARATOR[direction];
-
-  const nextComparer =
-    secondaryRule === undefined
-      ? undefined
-      : comparer(secondaryRule, ...otherRules);
-
-  return (a, b) => {
-    const projectedA = projector(a);
-    const projectedB = projector(b);
-
-    if (comparator(projectedA, projectedB)) {
-      return 1;
-    }
-
-    if (comparator(projectedB, projectedA)) {
-      return -1;
-    }
-
-    // The elements are equal base on the current comparator and projection. So
-    // we need to check the elements using the next comparer, if one exists,
-    // otherwise we consider them as true equal (returning 0).
-    return nextComparer?.(a, b) ?? 0;
-  };
-}
+  [...data].sort(compareFn);
 
 interface Strict {
   <T extends IterableContainer>(
-    ...sortRules: Readonly<NonEmptyArray<SortRule<T[number]>>>
+    ...sortRules: Readonly<NonEmptyArray<OrderRule<T[number]>>>
   ): (array: T) => SortedBy<T>;
 
   <T extends IterableContainer>(
     array: T,
-    ...sortRules: Readonly<NonEmptyArray<SortRule<T[number]>>>
+    ...sortRules: Readonly<NonEmptyArray<OrderRule<T[number]>>>
   ): SortedBy<T>;
 }
 
diff --git a/src/takeFirstBy.ts b/src/takeFirstBy.ts
new file mode 100644
index 0000000..ffdbf83
--- /dev/null
+++ b/src/takeFirstBy.ts
@@ -0,0 +1,74 @@
+import { heapify, heapMaybeInsert } from './_heap';
+import { OrderRule, purryOrderRulesWithArgument } from './_purryOrderRules';
+import type { CompareFunction, NonEmptyArray } from './_types';
+
+/**
+ * Take the first `n` items from `data` based on the provided ordering criteria. This allows you to avoid sorting the array before taking the items. The complexity of this function is *O(Nlogn)* where `N` is the length of the array.
+ *
+ * For the opposite operation (to drop `n` elements) see `dropFirstBy`.
+ *
+ * @params data - the input array
+ * @params n - the number of items to take. If `n` is non-positive no items would be returned, if `n` is bigger then data.length a *clone* of `data` would be returned.
+ * @param rules - A variadic array of order rules defining the sorting criteria. Each order rule is a projection function that extracts a comparable value from the data. Sorting is based on these extracted values using the native `<` and `>` operators. Earlier rules take precedence over later ones. Use the syntax `[projection, "desc"]` for descending order.
+ * @returns a subset of the input array.
+ * @signature
+ *   R.takeFirstBy(data, n, ...rules);
+ * @example
+ *   R.takeFirstBy(['aa', 'aaaa', 'a', 'aaa'], 2, x => x.length); // => ['a', 'aa']
+ * @dataFirst
+ * @category Array
+ */
+export function takeFirstBy<T>(
+  data: ReadonlyArray<T>,
+  n: number,
+  ...rules: Readonly<NonEmptyArray<OrderRule<T>>>
+): Array<T>;
+
+/**
+ * Take the first `n` items from `data` based on the provided ordering criteria. This allows you to avoid sorting the array before taking the items. The complexity of this function is *O(Nlogn)* where `N` is the length of the array.
+ *
+ * For the opposite operation (to drop `n` elements) see `dropFirstBy`.
+ *
+ * @params data - the input array
+ * @params n - the number of items to take. If `n` is non-positive no items would be returned, if `n` is bigger then data.length a *clone* of `data` would be returned.
+ * @param rules - A variadic array of order rules defining the sorting criteria. Each order rule is a projection function that extracts a comparable value from the data. Sorting is based on these extracted values using the native `<` and `>` operators. Earlier rules take precedence over later ones. Use the syntax `[projection, "desc"]` for descending order.
+ * @returns a subset of the input array.
+ * @signature
+ *   R.takeFirstBy(n, ...rules)(data);
+ * @example
+ *   R.pipe(['aa', 'aaaa', 'a', 'aaa'], R.takeFirstBy(2, x => x.length)); // => ['a', 'aa']
+ * @dataLast
+ * @category Array
+ */
+export function takeFirstBy<T>(
+  n: number,
+  ...rules: Readonly<NonEmptyArray<OrderRule<T>>>
+): (data: ReadonlyArray<T>) => Array<T>;
+
+export function takeFirstBy(): unknown {
+  return purryOrderRulesWithArgument(takeFirstByImplementation, arguments);
+}
+
+function takeFirstByImplementation<T>(
+  data: ReadonlyArray<T>,
+  compareFn: CompareFunction<T>,
+  n: number
+): Array<T> {
+  if (n <= 0) {
+    return [];
+  }
+
+  if (n >= data.length) {
+    return [...data];
+  }
+
+  const heap = data.slice(0, n);
+  heapify(heap, compareFn);
+
+  const rest = data.slice(n);
+  for (const item of rest) {
+    heapMaybeInsert(heap, compareFn, item);
+  }
+
+  return heap;
+}


Test patch:
diff --git a/src/dropFirstBy.test.ts b/src/dropFirstBy.test.ts
new file mode 100644
index 0000000..ad0720c
--- /dev/null
+++ b/src/dropFirstBy.test.ts
@@ -0,0 +1,133 @@
+import { dropFirstBy } from './dropFirstBy';
+import { identity } from './identity';
+import { pipe } from './pipe';
+
+describe('runtime (dataFirst)', () => {
+  it('works', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    expect(dropFirstBy(data, 2, identity)).toEqual([5, 6, 4, 3, 7]);
+  });
+
+  it('handles empty arrays gracefully', () => {
+    const data: Array<number> = [];
+    expect(dropFirstBy(data, 1, identity)).toHaveLength(0);
+  });
+
+  it('handles negative numbers gracefully', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    expect(dropFirstBy(data, -3, identity)).toHaveLength(data.length);
+  });
+
+  it('handles overflowing numbers gracefully', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    expect(dropFirstBy(data, 100, identity)).toHaveLength(0);
+  });
+
+  it('clones the input when needed', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    const result = dropFirstBy(data, 0, identity);
+    expect(result).not.toBe(data);
+    expect(result).toEqual(data);
+  });
+
+  it('works with complex compare rules', () => {
+    const data = [
+      'a',
+      'aaa',
+      'bbbbb',
+      'aa',
+      'bb',
+      'bbb',
+      'bbbb',
+      'aaaa',
+      'b',
+      'aaaaa',
+    ];
+    expect(dropFirstBy(data, 3, x => x.length, identity)).toEqual([
+      'bbbbb',
+      'aaa',
+      'bbb',
+      'bbbb',
+      'aaaa',
+      'bb',
+      'aaaaa',
+    ]);
+  });
+});
+
+describe('runtime (dataLast)', () => {
+  it('works', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    expect(
+      pipe(
+        data,
+        dropFirstBy(2, x => x)
+      )
+    ).toEqual([5, 6, 4, 3, 7]);
+  });
+
+  it('handles empty arrays gracefully', () => {
+    const data: Array<number> = [];
+    expect(
+      pipe(
+        data,
+        dropFirstBy(1, x => x)
+      )
+    ).toHaveLength(0);
+  });
+
+  it('handles negative numbers gracefully', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    expect(
+      pipe(
+        data,
+        dropFirstBy(-3, x => x)
+      )
+    ).toHaveLength(data.length);
+  });
+
+  it('handles overflowing numbers gracefully', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    expect(
+      pipe(
+        data,
+        dropFirstBy(100, x => x)
+      )
+    ).toHaveLength(0);
+  });
+
+  it('clones the data when needed', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    const result = pipe(
+      data,
+      dropFirstBy(0, x => x)
+    );
+    expect(result).not.toBe(data);
+    expect(result).toEqual(data);
+  });
+
+  it('works with complex compare rules', () => {
+    const data = [
+      'a',
+      'aaa',
+      'bbbbb',
+      'aa',
+      'bb',
+      'bbb',
+      'bbbb',
+      'aaaa',
+      'b',
+      'aaaaa',
+    ];
+    expect(
+      pipe(
+        data,
+        dropFirstBy(
+          3,
+          x => x.length,
+          x => x
+        )
+      )
+    ).toEqual(['bbbbb', 'aaa', 'bbb', 'bbbb', 'aaaa', 'bb', 'aaaaa']);
+  });
+});
diff --git a/src/firstBy.test.ts b/src/firstBy.test.ts
new file mode 100644
index 0000000..da53f75
--- /dev/null
+++ b/src/firstBy.test.ts
@@ -0,0 +1,155 @@
+import { NonEmptyArray } from './_types';
+import { firstBy } from './firstBy';
+import { identity } from './identity';
+import { pipe } from './pipe';
+
+describe('runtime (dataFirst)', () => {
+  it('returns undefined on empty', () => {
+    expect(firstBy([], identity)).toBeUndefined();
+  });
+
+  it('returns the item on a single item array', () => {
+    expect(firstBy([1], identity)).toBe(1);
+  });
+
+  it('finds the minimum', () => {
+    expect(firstBy([2, 1, 4, 3, 5], identity)).toBe(1);
+  });
+
+  it('finds the minimum with a non-trivial order rule', () => {
+    expect(firstBy(['aa', 'a', 'aaaa', 'aaa', 'aaaaa'], x => x.length)).toBe(
+      'a'
+    );
+  });
+
+  it("finds the max with 'desc' order rules", () => {
+    expect(firstBy([2, 1, 4, 3, 5], [identity, 'desc'])).toBe(5);
+  });
+
+  it("finds the max with non-trivial 'desc' order rules", () => {
+    expect(
+      firstBy(['aa', 'a', 'aaaa', 'aaa', 'aaaaa'], [identity, 'desc'])
+    ).toBe('aaaaa');
+  });
+
+  it('breaks ties with multiple order rules', () => {
+    const data = ['a', 'bb', 'b', 'aaaa', 'bbb', 'aa', 'aaa', 'bbbb'];
+    expect(firstBy(data, x => x.length, identity)).toBe('a');
+    expect(firstBy(data, [x => x.length, 'desc'], identity)).toBe('aaaa');
+    expect(firstBy(data, x => x.length, [identity, 'desc'])).toBe('b');
+    expect(firstBy(data, [x => x.length, 'desc'], [identity, 'desc'])).toBe(
+      'bbbb'
+    );
+  });
+
+  it('can compare strings', () => {
+    expect(firstBy(['b', 'a', 'c'], identity)).toBe('a');
+  });
+
+  it('can compare numbers', () => {
+    expect(firstBy([2, 1, 3], identity)).toBe(1);
+  });
+
+  it('can compare booleans', () => {
+    expect(firstBy([true, false, true, true, false], identity)).toBe(false);
+  });
+
+  it('can compare valueOfs', () => {
+    expect(firstBy([new Date(), new Date(1), new Date(2)], identity)).toEqual(
+      new Date(1)
+    );
+  });
+});
+
+describe('runtime (dataLast)', () => {
+  it('returns undefined on empty', () => {
+    expect(pipe([], firstBy(identity))).toBeUndefined();
+  });
+
+  it('returns the item on a single item array', () => {
+    expect(pipe([1], firstBy(identity))).toBe(1);
+  });
+
+  it('finds the minimum', () => {
+    expect(pipe([2, 1, 4, 3, 5], firstBy(identity))).toBe(1);
+  });
+
+  it('finds the minimum with a non-trivial order rule', () => {
+    expect(
+      pipe(
+        ['aa', 'a', 'aaaa', 'aaa', 'aaaaa'],
+        firstBy(x => x.length)
+      )
+    ).toBe('a');
+  });
+
+  it("finds the max with 'desc' order rules", () => {
+    expect(pipe([2, 1, 4, 3, 5], firstBy([identity, 'desc']))).toBe(5);
+  });
+
+  it("finds the max with non-trivial 'desc' order rules", () => {
+    expect(
+      pipe(['aa', 'a', 'aaaa', 'aaa', 'aaaaa'], firstBy([identity, 'desc']))
+    ).toBe('aaaaa');
+  });
+
+  it('breaks ties with multiple order rules', () => {
+    const data = ['a', 'bb', 'b', 'aaaa', 'bbb', 'aa', 'aaa', 'bbbb'];
+    expect(
+      pipe(
+        data,
+        firstBy(x => x.length, identity)
+      )
+    ).toBe('a');
+    expect(pipe(data, firstBy([x => x.length, 'desc'], identity))).toBe('aaaa');
+    expect(
+      pipe(
+        data,
+        firstBy(x => x.length, [identity, 'desc'])
+      )
+    ).toBe('b');
+    expect(
+      pipe(data, firstBy([(x: string) => x.length, 'desc'], [identity, 'desc']))
+    ).toBe('bbbb');
+  });
+
+  it('can compare strings', () => {
+    expect(pipe(['b', 'a', 'c'], firstBy(identity))).toBe('a');
+  });
+
+  it('can compare numbers', () => {
+    expect(pipe([2, 1, 3], firstBy(identity))).toBe(1);
+  });
+
+  it('can compare booleans', () => {
+    expect(pipe([true, false, true, true, false], firstBy(identity))).toBe(
+      false
+    );
+  });
+
+  it('can compare valueOfs', () => {
+    expect(
+      pipe([new Date(), new Date(1), new Date(2)], firstBy(identity))
+    ).toEqual(new Date(1));
+  });
+});
+
+describe('typing', () => {
+  it('can return undefined on arrays', () => {
+    const data: ReadonlyArray<number> = [1, 2, 3];
+    const result = firstBy(data, identity);
+    expectTypeOf(result).toBeNullable();
+  });
+
+  it("can't return undefined on non-empty array", () => {
+    const data: NonEmptyArray<number> = [1, 2, 3];
+    const result = firstBy(data, identity);
+    expectTypeOf(result).not.toBeNullable();
+  });
+
+  it('only returns null on the empty array', () => {
+    const data = [] as const;
+    const result = firstBy(data, identity);
+    expectTypeOf(result).toBeUndefined();
+  });
+});
diff --git a/src/nthBy.test.ts b/src/nthBy.test.ts
new file mode 100644
index 0000000..6cfca5f
--- /dev/null
+++ b/src/nthBy.test.ts
@@ -0,0 +1,127 @@
+import { nthBy } from './nthBy';
+import { identity } from './identity';
+import { pipe } from './pipe';
+
+describe('runtime (dataFirst)', () => {
+  it('works', () => {
+    const data = [2, 1, 3];
+    expect(nthBy(data, 0, identity)).toEqual(1);
+    expect(nthBy(data, 1, identity)).toEqual(2);
+    expect(nthBy(data, 2, identity)).toEqual(3);
+  });
+
+  it('handles negative indexes', () => {
+    const data = [2, 1, 3];
+    expect(nthBy(data, -1, identity)).toEqual(3);
+    expect(nthBy(data, -2, identity)).toEqual(2);
+    expect(nthBy(data, -3, identity)).toEqual(1);
+  });
+
+  it('handles overflows gracefully', () => {
+    expect(nthBy([1, 2, 3], 100, identity)).toBeUndefined();
+    expect(nthBy([1, 2, 3], -100, identity)).toBeUndefined();
+  });
+
+  it('works with complex order rules', () => {
+    const data = ['aaaa', 'b', 'bb', 'a', 'aaa', 'bbbb', 'aa', 'bbb'] as const;
+    expect(nthBy(data, 0, a => a.length, identity)).toEqual('a');
+    expect(nthBy(data, 1, a => a.length, identity)).toEqual('b');
+    expect(nthBy(data, 2, a => a.length, identity)).toEqual('aa');
+    expect(nthBy(data, 3, a => a.length, identity)).toEqual('bb');
+    expect(nthBy(data, 4, a => a.length, identity)).toEqual('aaa');
+    expect(nthBy(data, 5, a => a.length, identity)).toEqual('bbb');
+    expect(nthBy(data, 6, a => a.length, identity)).toEqual('aaaa');
+    expect(nthBy(data, 7, a => a.length, identity)).toEqual('bbbb');
+  });
+});
+
+describe('runtime (dataLast)', () => {
+  it('works', () => {
+    const data = [2, 1, 3];
+    expect(pipe(data, nthBy(0, identity))).toEqual(1);
+    expect(pipe(data, nthBy(1, identity))).toEqual(2);
+    expect(pipe(data, nthBy(2, identity))).toEqual(3);
+  });
+
+  it('handles negative indexes', () => {
+    const data = [2, 1, 3];
+    expect(pipe(data, nthBy(-1, identity))).toEqual(3);
+    expect(pipe(data, nthBy(-2, identity))).toEqual(2);
+    expect(pipe(data, nthBy(-3, identity))).toEqual(1);
+  });
+
+  it('handles overflows gracefully', () => {
+    expect(pipe([1, 2, 3], nthBy(100, identity))).toBeUndefined();
+    expect(pipe([1, 2, 3], nthBy(-100, identity))).toBeUndefined();
+  });
+
+  it('works with complex order rules', () => {
+    const data = ['aaaa', 'b', 'bb', 'a', 'aaa', 'bbbb', 'aa', 'bbb'] as const;
+    expect(
+      pipe(
+        data,
+        nthBy(0, a => a.length, identity)
+      )
+    ).toEqual('a');
+    expect(
+      pipe(
+        data,
+        nthBy(1, a => a.length, identity)
+      )
+    ).toEqual('b');
+    expect(
+      pipe(
+        data,
+        nthBy(2, a => a.length, identity)
+      )
+    ).toEqual('aa');
+    expect(
+      pipe(
+        data,
+        nthBy(3, a => a.length, identity)
+      )
+    ).toEqual('bb');
+    expect(
+      pipe(
+        data,
+        nthBy(4, a => a.length, identity)
+      )
+    ).toEqual('aaa');
+    expect(
+      pipe(
+        data,
+        nthBy(5, a => a.length, identity)
+      )
+    ).toEqual('bbb');
+    expect(
+      pipe(
+        data,
+        nthBy(6, a => a.length, identity)
+      )
+    ).toEqual('aaaa');
+    expect(
+      pipe(
+        data,
+        nthBy(7, a => a.length, identity)
+      )
+    ).toEqual('bbbb');
+  });
+});
+
+describe('typing', () => {
+  it('works with regular arrays', () => {
+    const result = nthBy([1, 2, 3], 0, identity);
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+  });
+
+  it('works with negative indices', () => {
+    const result = nthBy([1, 2, 3], -1, identity);
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+  });
+
+  it('works with tuples', () => {
+    const data: [string, boolean, number] = ['a', true, 1];
+    const result = nthBy(data, 1, identity);
+    expectTypeOf(result).toEqualTypeOf<string | number | boolean | undefined>();
+  });
+});
diff --git a/src/rankBy.test.ts b/src/rankBy.test.ts
new file mode 100644
index 0000000..4982d88
--- /dev/null
+++ b/src/rankBy.test.ts
@@ -0,0 +1,28 @@
+import { identity } from './identity';
+import { rankBy } from './rankBy';
+
+describe('runtime (dataFirst)', () => {
+  it('works trivially with empty arrays', () => {
+    expect(rankBy([], 1, identity)).toBe(0);
+  });
+
+  it('maintains the rank for items already in the array', () => {
+    const data = [5, 1, 3] as const;
+    const sorted = [...data].sort();
+    for (let i = 0; i < sorted.length; i += 1) {
+      expect(rankBy(data, sorted[i], identity)).toBe(i);
+    }
+  });
+
+  it('can rank items not in the array', () => {
+    const data = [5, 1, 3] as const;
+    expect(rankBy(data, 0, identity)).toBe(0);
+    expect(rankBy(data, 2, identity)).toBe(1);
+    expect(rankBy(data, 4, identity)).toBe(2);
+  });
+
+  it('finds items ranked at the end of the array', () => {
+    const data = [5, 1, 3] as const;
+    expect(rankBy(data, 6, identity)).toBe(3);
+  });
+});
diff --git a/src/takeFirstBy.test.ts b/src/takeFirstBy.test.ts
new file mode 100644
index 0000000..13f4b21
--- /dev/null
+++ b/src/takeFirstBy.test.ts
@@ -0,0 +1,129 @@
+import { identity } from './identity';
+import { pipe } from './pipe';
+import { takeFirstBy } from './takeFirstBy';
+
+describe('runtime (dataFirst)', () => {
+  it('works', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    expect(takeFirstBy(data, 2, identity)).toEqual([2, 1]);
+  });
+
+  it('handles empty arrays gracefully', () => {
+    const data: Array<number> = [];
+    expect(takeFirstBy(data, 1, identity)).toHaveLength(0);
+  });
+
+  it('handles negative numbers gracefully', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    expect(takeFirstBy(data, -3, identity)).toHaveLength(0);
+  });
+
+  it('handles overflowing numbers gracefully', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    expect(takeFirstBy(data, 100, identity)).toHaveLength(data.length);
+  });
+
+  it('clones the array when needed', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    const result = takeFirstBy(data, 100, identity);
+    expect(result).not.toBe(data);
+    expect(result).toEqual(data);
+  });
+
+  it('works with complex compare rules', () => {
+    const data = [
+      'a',
+      'aaa',
+      'bbbbb',
+      'aa',
+      'bb',
+      'bbb',
+      'bbbb',
+      'aaaa',
+      'b',
+      'aaaaa',
+    ];
+    expect(takeFirstBy(data, 3, x => x.length, identity)).toEqual([
+      'aa',
+      'b',
+      'a',
+    ]);
+  });
+});
+
+describe('runtime (dataLast)', () => {
+  it('works', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    expect(
+      pipe(
+        data,
+        takeFirstBy(2, x => x)
+      )
+    ).toEqual([2, 1]);
+  });
+
+  it('handles empty arrays gracefully', () => {
+    const data: Array<number> = [];
+    expect(
+      pipe(
+        data,
+        takeFirstBy(1, x => x)
+      )
+    ).toHaveLength(0);
+  });
+
+  it('handles negative numbers gracefully', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    expect(
+      pipe(
+        data,
+        takeFirstBy(-3, x => x)
+      )
+    ).toHaveLength(0);
+  });
+
+  it('handles overflowing numbers gracefully', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    expect(
+      pipe(
+        data,
+        takeFirstBy(100, x => x)
+      )
+    ).toHaveLength(data.length);
+  });
+
+  it('clones the array when needed', () => {
+    const data = [4, 5, 1, 6, 2, 3, 7];
+    const result = pipe(
+      data,
+      takeFirstBy(100, x => x)
+    );
+    expect(result).not.toBe(data);
+    expect(result).toEqual(data);
+  });
+
+  it('works with complex compare rules', () => {
+    const data = [
+      'a',
+      'aaa',
+      'bbbbb',
+      'aa',
+      'bb',
+      'bbb',
+      'bbbb',
+      'aaaa',
+      'b',
+      'aaaaa',
+    ];
+    expect(
+      pipe(
+        data,
+        takeFirstBy(
+          3,
+          x => x.length,
+          x => x
+        )
+      )
+    ).toEqual(['aa', 'b', 'a']);
+  });
+});