Repository: remeda/remeda
Pull request #878 (2024-10-06 14:56:11)

feat(mean): Add mean
Related to #874. Please note that I utilized the `sum` utility (not sure if this is something you would prefer to be avoided). I'd be happy to make any further adjustments as needed.

---

Make sure that you:

- [x] Typedoc added for new methods and updated for changed
- [x] Tests added for new methods and updated for changed
- [x] New methods added to `src/index.ts`
- [x] New methods added to `docs/src/content/mapping` for both Lodash and Ramda.

---


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

Latest deployment of this branch, based on commit 36ce371b18e78c38129a649dd3e4be4ae443aa58:

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

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/remeda/remeda/pull/878?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 [(`0420a9e`)](https://app.codecov.io/gh/remeda/remeda/commit/0420a9e877ef9a11e549845bbaa7d0b6c6cf11c1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda) to head [(`36ce371`)](https://app.codecov.io/gh/remeda/remeda/commit/36ce371b18e78c38129a649dd3e4be4ae443aa58?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      #878   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          315       317    +2     
  Lines        10777     10820   +43     
  Branches      2651      2664   +13     
=========================================
+ Hits         10777     10820   +43     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/remeda/remeda/pull/878?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).

grigorischristainas: @cjquines thank you for your feedback! I committed the suggested changes and enhanced tests. For the `Mean` type, I utilized the internal type `NonEmptyArray`, as well as the new `NonEmptyReadonlyArray` type I added, which is the equivalent for readonly arrays. Please let me know if any additional changes are needed.

Patch:
diff --git a/docs/src/content/mapping/lodash/__MISSING.md b/docs/src/content/mapping/lodash/__MISSING.md
index 9a4c21e..08f2234 100644
--- a/docs/src/content/mapping/lodash/__MISSING.md
+++ b/docs/src/content/mapping/lodash/__MISSING.md
@@ -84,10 +84,6 @@ TODO: Go over: https://you-dont-need.github.io/You-Dont-Need-Lodash-Underscore/#
 - toSafeInteger
 - toString
 
-# Math
-
-- mean
-
 # Number
 
 - inRange
diff --git a/docs/src/content/mapping/lodash/mean.md b/docs/src/content/mapping/lodash/mean.md
new file mode 100644
index 0000000..18bd5a9
--- /dev/null
+++ b/docs/src/content/mapping/lodash/mean.md
@@ -0,0 +1,4 @@
+---
+category: Math
+remeda: mean
+---
diff --git a/docs/src/content/mapping/ramda/mean.md b/docs/src/content/mapping/ramda/mean.md
index 3a88b5d..18bd5a9 100644
--- a/docs/src/content/mapping/ramda/mean.md
+++ b/docs/src/content/mapping/ramda/mean.md
@@ -1,14 +1,4 @@
 ---
 category: Math
-remeda: meanBy
+remeda: mean
 ---
-
-Use `meanBy` with `identity` as the mapper.
-
-```ts
-// Ramda
-mean(values);
-
-// Remeda
-meanBy(values, identity());
-```
diff --git a/src/clone.ts b/src/clone.ts
index 94d700f..fa176a3 100644
--- a/src/clone.ts
+++ b/src/clone.ts
@@ -6,9 +6,11 @@
 import { purry } from "./purry";
 
 /**
- * Creates a deep copy of the value. Supported types: `Array`, `Object`,
- * `Number`, `String`, `Boolean`, `Date`, `RegExp`. Functions are assigned by
- * reference rather than copied.
+ * Creates a deep copy of the value. Supported types: [plain objects](#isPlainObject),
+ * `Array`, `number`, `string`, `boolean`, `Date`, and `RegExp`. Functions are
+ * assigned by reference rather than copied. Class instances or any other
+ * built-in type that isn't mentioned above are not supported (but might
+ * work).
  *
  * @param data - The object to clone.
  * @signature
@@ -21,9 +23,11 @@ import { purry } from "./purry";
 export function clone<T>(data: T): T;
 
 /**
- * Creates a deep copy of the value. Supported types: `Array`, `Object`,
- * `Number`, `String`, `Boolean`, `Date`, `RegExp`. Functions are assigned by
- * reference rather than copied.
+ * Creates a deep copy of the value. Supported types: [plain objects](#isPlainObject),
+ * `Array`, `number`, `string`, `boolean`, `Date`, and `RegExp`. Functions are
+ * assigned by reference rather than copied. Class instances or any other
+ * built-in type that isn't mentioned above are not supported (but might
+ * work).
  *
  * @signature
  *   R.clone()(data)
diff --git a/src/index.ts b/src/index.ts
index 60ed80d..a7e5a97 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -77,6 +77,7 @@ export * from "./mapKeys";
 export * from "./mapToObj";
 export * from "./mapValues";
 export * from "./mapWithFeedback";
+export * from "./mean";
 export * from "./meanBy";
 export * from "./merge";
 export * from "./mergeAll";
diff --git a/src/internal/types.ts b/src/internal/types.ts
index 224a724..d53bc56 100644
--- a/src/internal/types.ts
+++ b/src/internal/types.ts
@@ -23,6 +23,8 @@ export type BrandedReturn<F extends (...args: any) => any> = (
 
 export type NonEmptyArray<T> = [T, ...Array<T>];
 
+export type NonEmptyReadonlyArray<T> = readonly [T, ...ReadonlyArray<T>];
+
 export type Mapped<T extends IterableContainer, K> = {
   -readonly [P in keyof T]: K;
 };
diff --git a/src/mean.ts b/src/mean.ts
new file mode 100644
index 0000000..0ccfbd8
--- /dev/null
+++ b/src/mean.ts
@@ -0,0 +1,87 @@
+import type {
+  IterableContainer,
+  NonEmptyArray,
+  NonEmptyReadonlyArray,
+} from "./internal/types";
+import { purry } from "./purry";
+import { sum } from "./sum";
+
+type Mean<T extends IterableContainer<bigint> | IterableContainer<number>> =
+  T extends NonEmptyArray<bigint> | NonEmptyReadonlyArray<bigint>
+    ? bigint
+    : T extends NonEmptyArray<number> | NonEmptyReadonlyArray<number>
+      ? number
+      : T extends [] | readonly []
+        ? undefined
+        : number | undefined;
+
+/**
+ * Returns the mean of the elements of an array.
+ *
+ * Works for both `number` and `bigint` arrays, but not arrays that contain both
+ * types.
+ *
+ * IMPORTANT: The result for empty arrays would be `undefined`, regardless of
+ * the type of the array. This approach improves type-checking and ensures that
+ * cases where `NaN` might occur are handled properly. To avoid adding this to
+ * the return type for cases where the array is known to be non-empty you can use
+ * `hasAtLeast` or `isEmpty` to guard against this case.
+ *
+ * @param data - The array of numbers.
+ * @signature
+ *   R.mean(data);
+ * @example
+ *   R.mean([1, 2, 3]); // => 2
+ *   R.mean([1n, 2n, 3n]); // => 2n
+ *   R.mean([]); // => undefined
+ * @dataFirst
+ * @category Number
+ */
+export function mean<
+  T extends IterableContainer<bigint> | IterableContainer<number>,
+>(data: T): Mean<T>;
+
+/**
+ * Returns the mean of the elements of an array.
+ *
+ * Works for both `number` and `bigint` arrays, but not arrays that contain both
+ * types.
+ *
+ * IMPORTANT: The result for empty arrays would be `undefined`, regardless of
+ * the type of the array. This approach improves type-checking and ensures that
+ * cases where `NaN` might occur are handled properly. To avoid adding this to
+ * the return type for cases where the array is known to be non-empty you can use
+ * `hasAtLeast` or `isEmpty` to guard against this case.
+ *
+ * @signature
+ *   R.mean()(data);
+ * @example
+ *   R.pipe([1, 2, 3], R.mean()); // => 2
+ *   R.pipe([1n, 2n, 3n], R.mean()); // => 2n
+ *   R.pipe([], R.mean()); // => undefined
+ * @dataLast
+ * @category Number
+ */
+export function mean(): <
+  T extends IterableContainer<bigint> | IterableContainer<number>,
+>(
+  data: T,
+) => Mean<T>;
+
+export function mean(...args: ReadonlyArray<unknown>): unknown {
+  return purry(meanImplementation, args);
+}
+
+function meanImplementation<
+  T extends IterableContainer<bigint> | IterableContainer<number>,
+>(data: T): T[number] | undefined {
+  if (data.length === 0) {
+    return undefined;
+  }
+
+  const dataSum = sum(data);
+
+  return typeof dataSum === "bigint"
+    ? dataSum / BigInt(data.length)
+    : dataSum / data.length;
+}


Test patch:
diff --git a/src/mean.test-d.ts b/src/mean.test-d.ts
new file mode 100644
index 0000000..380de62
--- /dev/null
+++ b/src/mean.test-d.ts
@@ -0,0 +1,116 @@
+import { hasAtLeast } from "./hasAtLeast";
+import { isEmpty } from "./isEmpty";
+import { mean } from "./mean";
+import { pipe } from "./pipe";
+
+test("empty arrays", () => {
+  const result = mean([] as const);
+  expectTypeOf(result).toEqualTypeOf<undefined>();
+});
+
+describe("numbers", () => {
+  test("arbitrary arrays", () => {
+    const result = mean([] as Array<number>);
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+  });
+
+  test("arbitrary readonly arrays", () => {
+    const result = mean([] as ReadonlyArray<number>);
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+  });
+
+  test("arbitrary non-empty arrays", () => {
+    const result = mean([1, 2] as [number, ...Array<number>]);
+    expectTypeOf(result).toEqualTypeOf<number>();
+  });
+
+  test("consts", () => {
+    const result = mean([1, 2, 3] as const);
+    expectTypeOf(result).toEqualTypeOf<number>();
+  });
+
+  test("fixed-size tuples", () => {
+    const result = mean([1, 2] as [number, number]);
+    expectTypeOf(result).toEqualTypeOf<number>();
+  });
+});
+
+describe("bigints", () => {
+  test("arbitrary arrays", () => {
+    const result = mean([] as Array<bigint>);
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+  });
+
+  test("arbitrary readonly arrays", () => {
+    const result = mean([] as ReadonlyArray<bigint>);
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+  });
+
+  test("arbitrary non-empty arrays", () => {
+    const result = mean([1n, 2n] as [bigint, ...Array<bigint>]);
+    expectTypeOf(result).toEqualTypeOf<bigint>();
+  });
+
+  test("consts", () => {
+    const result = mean([1n, 2n, 3n] as const);
+    expectTypeOf(result).toEqualTypeOf<bigint>();
+  });
+
+  test("fixed-size tuples", () => {
+    const result = mean([1n, 2n] as [bigint, bigint]);
+    expectTypeOf(result).toEqualTypeOf<bigint>();
+  });
+});
+
+describe("dataLast", () => {
+  test("numbers", () => {
+    const result = pipe([1, 2, 3] as const, mean());
+    expectTypeOf(result).toEqualTypeOf<number>();
+  });
+
+  test("bigints", () => {
+    const result = pipe([1n, 2n, 3n] as const, mean());
+    expectTypeOf(result).toEqualTypeOf<bigint>();
+  });
+
+  test("arbitrary number arrays", () => {
+    const result = pipe([] as Array<number>, mean());
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+  });
+
+  test("arbitrary readonly number arrays", () => {
+    const result = pipe([] as ReadonlyArray<number>, mean());
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+  });
+
+  test("arbitrary bigint arrays", () => {
+    const result = pipe([] as Array<bigint>, mean());
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+  });
+
+  test("arbitrary readonly bigint arrays", () => {
+    const result = pipe([] as ReadonlyArray<bigint>, mean());
+    expectTypeOf(result).toEqualTypeOf<number | undefined>();
+  });
+});
+
+describe("type-guards", () => {
+  it("narrows to `number` using `hasAtLeast`", () => {
+    const data = [] as Array<number>;
+    if (hasAtLeast(data, 1)) {
+      expectTypeOf(mean(data)).toEqualTypeOf<number>();
+    }
+  });
+
+  it("narrows to `undefined` using `isEmpty`", () => {
+    const data = [] as Array<number>;
+    if (isEmpty(data)) {
+      expectTypeOf(mean(data)).toEqualTypeOf<undefined>();
+    }
+  });
+});
+
+it("doesn't allow mixed arrays", () => {
+  // @ts-expect-error [ts2345] - Can't mean bigints and numbers...
+  mean([1, 2n]);
+});
diff --git a/src/mean.test.ts b/src/mean.test.ts
new file mode 100644
index 0000000..51b0f53
--- /dev/null
+++ b/src/mean.test.ts
@@ -0,0 +1,32 @@
+import { mean } from "./mean";
+import { pipe } from "./pipe";
+
+describe("dataFirst", () => {
+  it("should return the mean of numbers in an array", () => {
+    expect(mean([1, 2, 3])).toBe(2);
+    expect(mean([4, 5, 6])).toBe(5);
+    expect(mean([-1, 0, 1])).toBe(0);
+  });
+
+  it("should return undefined for an empty array", () => {
+    expect(mean([])).toBeUndefined();
+    expect(mean([] as Array<bigint>)).toBeUndefined();
+  });
+
+  it("works on bigints", () => {
+    expect(mean([1n, 2n, 3n])).toBe(2n);
+  });
+});
+
+describe("dataLast", () => {
+  it("should return the mean of numbers in an array", () => {
+    expect(pipe([1, 2, 3], mean())).toBe(2);
+    expect(pipe([4, 5, 6], mean())).toBe(5);
+    expect(pipe([-1, 0, 1], mean())).toBe(0);
+  });
+
+  it("should return undefined for an empty array", () => {
+    expect(pipe([], mean())).toBeUndefined();
+    expect(pipe([] as Array<bigint>, mean())).toBeUndefined();
+  });
+});