Repository: remeda/remeda
Pull request #826 (2024-08-12 11:47:42)

docs(readme): add contributors
@cjquines, wdyt?

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/826/builds/531133) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit b6a8173cdf96c621f5c728842a5530089d935ade:

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

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/remeda/remeda/pull/826?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`7831fae`)](https://app.codecov.io/gh/remeda/remeda/commit/7831faef235e1cb97ac02b76c5d3b3d2c7c05c46?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda) to head [(`b6a8173`)](https://app.codecov.io/gh/remeda/remeda/commit/b6a8173cdf96c621f5c728842a5530089d935ade?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main      #826   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          309       309           
  Lines        10472     10472           
  Branches      2548      2548           
=========================================
  Hits         10472     10472           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/remeda/remeda/pull/826?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda).


Patch:
diff --git a/README.md b/README.md
index 4e66609..603480a 100644
--- a/README.md
+++ b/README.md
@@ -68,3 +68,11 @@ R.pipe(
 ### Getting help
 
 Questions, bug reports, and feature requests are tracked in [GitHub issues](https://github.com/remeda/remeda/issues).
+
+## Contributors
+
+<a href="https://github.com/remeda/remeda/graphs/contributors">
+  <img src="https://contrib.rocks/image?repo=remeda/remeda" />
+</a>
+
+Made with [contrib.rocks](https://contrib.rocks).
diff --git a/docs/src/content/mapping/lodash/__MISSING.md b/docs/src/content/mapping/lodash/__MISSING.md
index 4f0a738..b7697d1 100644
--- a/docs/src/content/mapping/lodash/__MISSING.md
+++ b/docs/src/content/mapping/lodash/__MISSING.md
@@ -91,7 +91,6 @@ TODO: Go over: https://you-dont-need.github.io/You-Dont-Need-Lodash-Underscore/#
 # Number
 
 - inRange
-- random
 
 # Object
 
diff --git a/docs/src/content/mapping/lodash/random.md b/docs/src/content/mapping/lodash/random.md
new file mode 100644
index 0000000..3f5b815
--- /dev/null
+++ b/docs/src/content/mapping/lodash/random.md
@@ -0,0 +1,53 @@
+---
+category: Number
+remeda: randomInteger
+---
+
+- When Lodash's `random` function is called with a non-integer param it returns
+  any number, not just integers (e.g. 2.5)! Remeda's `randomInteger` function
+  always returns integers, effectively rounding the parameters to fit the range
+  of possible integers.
+- If you want to generate any number in the range (and not just integers) see
+  the solutions provided in [_"You don't need Lodash"_](https://you-dont-need.github.io/You-Dont-Need-Lodash-Underscore/#/?id=_random).
+- Lodash's `random` parameters are optional. In Remeda all parameters are
+  required.
+
+### Two integer params
+
+```ts
+// Lodash
+random(1, 10);
+
+// Remeda
+randomInteger(1, 10);
+```
+
+### Single integer param
+
+```ts
+// Lodash
+random(10);
+
+// Remeda
+randomInteger(0, 10);
+```
+
+### No params
+
+```ts
+// Lodash
+random();
+
+// Remeda
+randomInteger(0, 1);
+```
+
+### Not supported: floating-point numbers
+
+```ts
+random(1.5, 3.5);
+random(1.5);
+random(10, true);
+random(5, 10, true);
+random(true);
+```
diff --git a/src/index.ts b/src/index.ts
index 06f9a20..c5aa285 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -98,6 +98,7 @@ export * from "./product";
 export * from "./prop";
 export * from "./pullObject";
 export * from "./purry";
+export * from "./randomInteger";
 export * from "./randomString";
 export * from "./range";
 export * from "./rankBy";
diff --git a/src/randomInteger.ts b/src/randomInteger.ts
new file mode 100644
index 0000000..87829b7
--- /dev/null
+++ b/src/randomInteger.ts
@@ -0,0 +1,62 @@
+import type {
+  IntRange,
+  GreaterThan,
+  GreaterThanOrEqual,
+  Or,
+  IsNever,
+  NonNegativeInteger,
+  IsEqual,
+} from "type-fest";
+
+type RandomInteger<From extends number, To extends number> =
+  Or<
+    IsNever<NonNegativeInteger<From>>,
+    IsNever<NonNegativeInteger<To>>
+  > extends true
+    ? number
+    : IsEqual<From, To> extends true
+      ? From
+      : GreaterThan<From, To> extends true
+        ? never
+        : GreaterThanOrEqual<To, 1000> extends true
+          ? number
+          : IntRange<From, To> | To;
+
+/**
+ * Generate a random integer between `from` and `to` (inclusive).
+ *
+ * !Important: This function uses [`Math.random()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) under-the-hood, which has two major limitations:
+ * 1. It generates 2^52 possible values, so the bigger the range, the less
+ * uniform the distribution of values would be, and at ranges larger than that
+ * some values would never come up.
+ * 2. It is not cryptographically secure and should not be used for security
+ * scenarios.
+ *
+ * @param from - The minimum value.
+ * @param to - The maximum value.
+ * @returns The random integer.
+ * @signature
+ *   R.randomInteger(from, to)
+ * @example
+ *   R.randomInteger(1, 10) // => 5
+ *   R.randomInteger(1.5, 2.6) // => 2
+ * @dataFirst
+ * @category Number
+ */
+export function randomInteger<From extends number, To extends number>(
+  from: From,
+  to: To,
+): RandomInteger<From, To> {
+  const fromCeiled = Math.ceil(from);
+  const toFloored = Math.floor(to);
+
+  if (toFloored < fromCeiled) {
+    throw new RangeError(
+      `randomInteger: The range [${from},${to}] contains no integer`,
+    );
+  }
+
+  return Math.floor(
+    Math.random() * (toFloored - fromCeiled + 1) + fromCeiled,
+  ) as RandomInteger<From, To>;
+}


Test patch:
diff --git a/src/randomInteger.test-d.ts b/src/randomInteger.test-d.ts
new file mode 100644
index 0000000..c94e6c7
--- /dev/null
+++ b/src/randomInteger.test-d.ts
@@ -0,0 +1,64 @@
+import { randomInteger } from "./randomInteger";
+
+test("returns number when from is float", () => {
+  expectTypeOf(randomInteger(0.5, 1)).toEqualTypeOf<number>();
+});
+
+test("returns number when to is float", () => {
+  expectTypeOf(randomInteger(1, 1.5)).toEqualTypeOf<number>();
+});
+
+test("returns never when from is greater than to", () => {
+  expectTypeOf(randomInteger(10, 1)).toEqualTypeOf<never>();
+});
+
+test("returns the range as union for valid range (0-999)", () => {
+  expectTypeOf(randomInteger(0, 5)).toEqualTypeOf<0 | 1 | 2 | 3 | 4 | 5>();
+  expectTypeOf(randomInteger(998, 999)).toEqualTypeOf<998 | 999>();
+});
+
+test("returns the same value when to equals from", () => {
+  expectTypeOf(randomInteger(1, 1)).toEqualTypeOf<1>();
+});
+
+test("returns the same value when to equals from and out of range", () => {
+  expectTypeOf(randomInteger(10_000, 10_000)).toEqualTypeOf<10_000>();
+});
+
+test("Returns number for large max (to >= 1000)", () => {
+  expectTypeOf(randomInteger(1, 1001)).toEqualTypeOf<number>();
+});
+
+test("Returns number when from is a negative integer", () => {
+  expectTypeOf(randomInteger(-1, 1)).toEqualTypeOf<number>();
+});
+
+test("Returns number when to is a negative integer", () => {
+  expectTypeOf(randomInteger(1, -1)).toEqualTypeOf<number>();
+});
+
+test("Returns number when from is a negative decimal", () => {
+  expectTypeOf(randomInteger(-1.5, 1)).toEqualTypeOf<number>();
+});
+
+test("Returns number when to is a negative decimal", () => {
+  expectTypeOf(randomInteger(1, -1.5)).toEqualTypeOf<number>();
+});
+
+test("Returns number when from is number", () => {
+  expectTypeOf(randomInteger(1 as number, 5)).toEqualTypeOf<number>();
+});
+
+test("Returns number when to is number", () => {
+  expectTypeOf(randomInteger(1, 5 as number)).toEqualTypeOf<number>();
+});
+
+test("Returns number when from and to are number", () => {
+  expectTypeOf(randomInteger(1 as number, 5 as number)).toEqualTypeOf<number>();
+});
+
+test("Returns number when from and to are very big numbers", () => {
+  expectTypeOf(
+    randomInteger(10_000_000_000, 10_000_000_001),
+  ).toEqualTypeOf<number>();
+});
diff --git a/src/randomInteger.test.ts b/src/randomInteger.test.ts
new file mode 100644
index 0000000..8936587
--- /dev/null
+++ b/src/randomInteger.test.ts
@@ -0,0 +1,55 @@
+import { randomInteger } from "./randomInteger";
+
+const ITERATIONS = 1000;
+
+test("from is greater than to", () => {
+  expect(() => randomInteger(10, 0)).toThrow(
+    new RangeError("randomInteger: The range [10,0] contains no integer"),
+  );
+});
+
+test("from and to are decimals with same whole number", () => {
+  expect(() => expect(randomInteger(1.5, 1.6))).toThrow(
+    new RangeError("randomInteger: The range [1.5,1.6] contains no integer"),
+  );
+});
+
+test("non-negative integers", () => {
+  for (const v of randomIntegers(0, 10)) {
+    expect(v).toBeGreaterThanOrEqual(0);
+    expect(v).toBeLessThanOrEqual(10);
+  }
+});
+
+test("negative integers", () => {
+  for (const v of randomIntegers(-10, -5)) {
+    expect(v).toBeGreaterThanOrEqual(-10);
+    expect(v).toBeLessThanOrEqual(-5);
+  }
+});
+
+test("positive decimals", () => {
+  for (const v of randomIntegers(0.1, 10.9)) {
+    expect(v).toBeGreaterThanOrEqual(0.1);
+    expect(v).toBeLessThanOrEqual(10.9);
+  }
+});
+
+test("negative decimals", () => {
+  for (const v of randomIntegers(-10.9, -0.1)) {
+    expect(v).toBeGreaterThanOrEqual(-10.9);
+    expect(v).toBeLessThanOrEqual(-0.1);
+  }
+});
+
+test("integers with same value", () => {
+  for (const v of randomIntegers(10, 10)) {
+    expect(v).toBe(10);
+  }
+});
+
+function* randomIntegers(from: number, to: number): Generator<number> {
+  for (let i = 0; i < ITERATIONS; i += 1) {
+    yield randomInteger(from, to);
+  }
+}