# groupjs_by

> Zero-dependency GROUP BY + sum/avg/min/max/count for arrays of objects. Chainable SQL-style aggregates that Object.groupBy does not provide.

Latest version **2.1.1** (published 2026-08-20) · MIT license · 0 weekly downloads

## Install

```sh
npm install groupjs_by
pnpm add groupjs_by
yarn add groupjs_by
bun add groupjs_by
```

## Health

**Score 75/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; has provenance; recently updated; high maintenance score; high quality score.

Warnings: low downloads.

## Facts

| | |
|---|---|
| Version | 2.1.1 |
| Published | 2026-08-20 |
| First published | 2022-06-21 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Node | >=18 |
| Dependencies | 0 |
| Unpacked size | 63.7 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Provenance | attested (GitHub Actions) |
| GitHub stars | 1 |
| Author | Julio Aguilar |
| Maintainers | t_aguij |
| Keywords | aggregate, aggregation, javascript, sum, min, max, avg, count, groupBy, groupby, group-by-sum, distinctCount, groupObjects, array, data-transform, esm, sql, rollup, reporting, dashboard, object.groupby, lodash-alternative, agrupar, sumar, promedio, contar |

## Links

- npm: https://www.npmjs.com/package/groupjs_by
- Repository: https://github.com/juli04guilar/groupBy
- Homepage: https://github.com/juli04guilar/groupBy#readme
- Issues: https://github.com/juli04guilar/groupBy/issues
- npm.io page: https://npm.io/package/groupjs_by

## Alternatives

- [@libsql/sqlite3](https://npm.io/package/@libsql/sqlite3.md) — 39.8K weekly downloads
- [@fortemi/core](https://npm.io/package/@fortemi/core.md) — 461 weekly downloads
- [cdb-converter](https://npm.io/package/cdb-converter.md) — 341 weekly downloads
- [@uplo/adapter-prisma](https://npm.io/package/@uplo/adapter-prisma.md) — 75 weekly downloads
- [typeorm-aios](https://npm.io/package/typeorm-aios.md) — 30 weekly downloads

## Recent versions

- 2.1.1 (latest) — 2026-08-20
- 2.1.0 — 2026-08-20
- 2.0.6 — 2026-08-17
- 2.0.5 — 2026-08-05
- 2.0.0 — 2025-05-26
- 1.2.3 — 2025-05-26
- 1.2.2 — 2025-05-05
- 1.2.1 — 2025-04-26
- 1.2.0 — 2022-06-30
- 1.1.0 — 2022-06-25
- 1.0.9 — 2022-06-24
- 1.0.8 — 2022-06-24
- 1.0.7 — 2022-06-24
- 1.0.6 — 2022-06-24
- 1.0.5 — 2022-06-24
- … 5 more at https://npm.io/package/groupjs_by/versions

## README

# groupjs_by

[![npm version](https://img.shields.io/npm/v/groupjs_by.svg)](https://www.npmjs.com/package/groupjs_by)
[![npm downloads](https://img.shields.io/npm/dm/groupjs_by.svg)](https://www.npmjs.com/package/groupjs_by)
[![CI](https://github.com/juli04guilar/groupBy/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/juli04guilar/groupBy/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
[![TypeScript](https://img.shields.io/badge/types-included-blue.svg)](./index.d.ts)

Zero-dependency JavaScript library for grouping arrays of objects and computing aggregates — with a chainable, SQL-inspired API.

`Object.groupBy` (ES2024) only buckets rows. **groupjs_by is the `GROUP BY` + `SUM` / `AVG` / `COUNT` step you still write by hand.**

```js
const { groupBy } = require('groupjs_by');

groupBy(orders, 'status')
  .sum('revenue', 'amount')
  .avg('avgOrder', 'amount')
  .count('orders')
  .data;
```

| Native `Object.groupBy` | groupjs_by |
|-------------------------|------------|
| Groups into arrays of items | Groups **and** aggregates in one chain |
| Callback key only | String field, accessor, or `['country', 'status']` |
| You write a second `reduce` for sums | `.sum` / `.avg` / `.count` / `.aggregate()` |
| Empty input → `{}` | Empty input → empty `.data` / `.toArray()` |

---

## Why groupjs_by?

| Need | How groupjs_by helps |
|------|----------------------|
| Group + aggregate in one place | Chain `groupBy` → aggregates → `.data` |
| Multiple dimensions | Pass `['country', 'status']` (or accessors) |
| Chart-friendly rows | Call `.toArray()` for `[{ key, items, …aggs }]` |
| Sorted tables | `.orderBy('revenue', 'desc')` |
| Custom metrics | `.reduce(alias, fn, initial)` |
| Large payloads | `.omitItems()` after aggregating |
| Multiple metrics efficiently | `aggregate()` scans each group once |
| Nested or derived keys | Pass `(item) => …` accessors anywhere a column is expected |
| Typed consumers | Ships with TypeScript declarations |
| Small surface area | No transitive dependencies |

Built for reporting, dashboards, ETL transforms, and any pipeline that looks like `GROUP BY` + `SUM` / `AVG` / `MIN` / `MAX` / `COUNT`.

---

## Install

```bash
npm install groupjs_by
# or
yarn add groupjs_by
```

**CommonJS**

```js
const { groupBy } = require('groupjs_by');
```

**ESM**

```js
import { groupBy } from 'groupjs_by';
// or
import groupjs from 'groupjs_by';
```

Works in Node.js 18+ and any bundler. TypeScript types are included for both `require` and `import`.

See [CHANGELOG.md](./CHANGELOG.md) for release history.

---

## Quick start

```js
const { groupBy } = require('groupjs_by');

const orders = [
  { status: 'paid', amount: 120, sku: 'TEE-BLK' },
  { status: 'paid', amount: 80, sku: 'HAT-RED' },
  { status: 'refunded', amount: 40, sku: 'TEE-BLK' },
];

const byStatus = groupBy(orders, 'status')
  .sum('revenue', 'amount')
  .avg('avgOrder', 'amount')
  .count('orders')
  .data;
```

**Result**

```js
{
  paid: {
    items: [/* … */],
    revenue: 200,
    avgOrder: 100,
    orders: 2,
  },
  refunded: {
    items: [/* … */],
    revenue: 40,
    avgOrder: 40,
    orders: 1,
  },
}
```

---

## Recipes

Copy-paste examples for common reporting jobs.

### Sales report by status

Revenue, average order value, and order count — useful for checkout dashboards.

```js
const { groupBy } = require('groupjs_by');

const orders = [
  { id: 'o1', status: 'paid', amount: 120.5, channel: 'web' },
  { id: 'o2', status: 'paid', amount: 89.0, channel: 'app' },
  { id: 'o3', status: 'pending', amount: 45.0, channel: 'web' },
  { id: 'o4', status: 'refunded', amount: 30.0, channel: 'web' },
  { id: 'o5', status: 'paid', amount: 210.0, channel: 'retail' },
];

// Paid / pending / refunded summary
const salesByStatus = groupBy(orders, 'status').aggregate({
  sum: ['revenue', 'amount'],
  avg: ['avgOrderValue', 'amount', 2],
  min: ['smallestOrder', 'amount'],
  max: ['largestOrder', 'amount'],
  count: ['orderCount'],
}).data;

console.log(salesByStatus.paid.revenue); // 419.5
console.log(salesByStatus.paid.orderCount); // 3

// Only completed revenue (filter first)
const paidOnly = groupBy(orders, 'status')
  .where((order) => order.status === 'paid')
  .aggregate({
    sum: ['revenue', 'amount'],
    distinctCount: ['channels', 'channel'],
    count: ['orderCount'],
  }).data;

console.log(paidOnly.paid);
// { items: […], revenue: 419.5, channels: 3, orderCount: 3 }
```

### Sales by channel (nested fields)

When metrics live under a nested object, use accessors:

```js
const checkoutEvents = [
  { meta: { channel: 'web' }, payment: { total: 50 } },
  { meta: { channel: 'web' }, payment: { total: 75 } },
  { meta: { channel: 'app' }, payment: { total: 120 } },
];

const byChannel = groupBy(checkoutEvents, (row) => row.meta.channel).aggregate({
  sum: ['revenue', (row) => row.payment.total],
  avg: ['avgTicket', (row) => row.payment.total],
  count: ['checkouts'],
}).data;

console.log(byChannel.web.revenue); // 125
console.log(byChannel.app.checkouts); // 1
```

### Request / error logs by day

Roll up API or app logs for daily volume and error rates.

```js
const { groupBy } = require('groupjs_by');

const logs = [
  { ts: '2026-03-01T08:12:00Z', level: 'info', route: '/api/orders', latencyMs: 42 },
  { ts: '2026-03-01T09:04:00Z', level: 'error', route: '/api/orders', latencyMs: 310 },
  { ts: '2026-03-01T18:22:00Z', level: 'info', route: '/api/cart', latencyMs: 28 },
  { ts: '2026-03-02T10:01:00Z', level: 'error', route: '/api/checkout', latencyMs: 900 },
  { ts: '2026-03-02T11:45:00Z', level: 'warn', route: '/api/orders', latencyMs: 120 },
  { ts: '2026-03-02T15:10:00Z', level: 'info', route: '/api/orders', latencyMs: 35 },
];

// Group by calendar day (UTC)
const dayKey = (log) => log.ts.slice(0, 10);

const logsByDay = groupBy(logs, dayKey).aggregate({
  count: ['events'],
  avg: ['avgLatencyMs', 'latencyMs', 1],
  max: ['pWorstLatencyMs', 'latencyMs'],
  distinctCount: ['routesHit', 'route'],
}).data;

console.log(logsByDay['2026-03-01'].events); // 3
console.log(logsByDay['2026-03-02'].avgLatencyMs); // 351.7

// Errors only, still keyed by day
const errorsByDay = groupBy(logs, dayKey)
  .where((log) => log.level === 'error')
  .aggregate({
    count: ['errors'],
    avg: ['avgErrorLatencyMs', 'latencyMs'],
    distinctCount: ['failingRoutes', 'route'],
  }).data;

console.log(errorsByDay['2026-03-01'].errors); // 1
console.log(errorsByDay['2026-03-02'].failingRoutes); // 1
```

### Inventory by SKU

Stock levels, warehouse spread, and movement totals for ops / replenishment views.

```js
const { groupBy } = require('groupjs_by');

const movements = [
  { sku: 'TEE-BLK', warehouse: 'US-EAST', qty: 40, unitCost: 8 },
  { sku: 'TEE-BLK', warehouse: 'US-WEST', qty: 12, unitCost: 8 },
  { sku: 'TEE-BLK', warehouse: 'US-EAST', qty: -5, unitCost: 8 }, // outbound
  { sku: 'HAT-RED', warehouse: 'US-EAST', qty: 20, unitCost: 15 },
  { sku: 'HAT-RED', warehouse: 'EU-CENTRAL', qty: 8, unitCost: 15 },
  { sku: 'MUG-WHT', warehouse: 'US-WEST', qty: 100, unitCost: 4 },
];

const inventoryBySku = groupBy(movements, 'sku').aggregate({
  sum: ['onHand', 'qty'],
  avg: ['avgUnitCost', 'unitCost'],
  distinctCount: ['warehouses', 'warehouse'],
  count: ['ledgerLines'],
}).data;

console.log(inventoryBySku['TEE-BLK'].onHand); // 47  (40 + 12 - 5)
console.log(inventoryBySku['TEE-BLK'].warehouses); // 2
console.log(inventoryBySku['HAT-RED'].onHand); // 28

// Low-stock SKUs only (after aggregating, filter keys you care about)
const lowStock = Object.entries(inventoryBySku)
  .filter(([, row]) => row.onHand < 50)
  .map(([sku, row]) => ({ sku, onHand: row.onHand, warehouses: row.warehouses }));

console.log(lowStock);
// [
//   { sku: 'TEE-BLK', onHand: 47, warehouses: 2 },
//   { sku: 'HAT-RED', onHand: 28, warehouses: 2 },
// ]
```

### Inventory valuation by warehouse

Derive line value with an accessor, then sum it per warehouse:

```js
const lineValue = (row) => row.qty * row.unitCost;

const valuation = groupBy(movements, 'warehouse')
  .where((row) => row.qty > 0) // ignore outbound for on-hand value
  .sum('units', 'qty')
  .sum('inventoryValue', lineValue)
  .distinctCount('skus', 'sku')
  .count('lines')
  .data;

console.log(valuation['US-EAST'].inventoryValue); // 40*8 + 20*15 = 620
console.log(valuation['US-WEST'].skus); // 2
```

### Multi-key groupBy (country × status)

Pass an array of fields (or accessors) to group on more than one dimension:

```js
const { groupBy } = require('groupjs_by');

const orders = [
  { country: 'US', status: 'paid', amount: 100 },
  { country: 'US', status: 'paid', amount: 50 },
  { country: 'US', status: 'refunded', amount: 20 },
  { country: 'MX', status: 'paid', amount: 80 },
];

const byCountryStatus = groupBy(orders, ['country', 'status'])
  .sum('revenue', 'amount')
  .count('orders');

// `.keys` returns composite key arrays
console.log(byCountryStatus.keys);
// [['US', 'paid'], ['US', 'refunded'], ['MX', 'paid']]

// `.data` uses JSON-stringified composite keys as object keys
console.log(byCountryStatus.data[JSON.stringify(['US', 'paid'])].revenue); // 150
```

### Export rows with `.toArray()`

Prefer `.toArray()` for tables, charts, and CSV — each group becomes one object with a `key` field:

```js
const rows = groupBy(orders, ['country', 'status'])
  .sum('revenue', 'amount')
  .count('orders')
  .toArray();

console.log(rows);
// [
//   { key: ['US', 'paid'], items: […], revenue: 150, orders: 2 },
//   { key: ['US', 'refunded'], items: […], revenue: 20, orders: 1 },
//   { key: ['MX', 'paid'], items: […], revenue: 80, orders: 1 },
// ]

// Single-key groups use a string `key`
const byStatus = groupBy(orders, 'status').count('n').toArray();
// [{ key: 'paid', items: […], n: 3 }, { key: 'refunded', items: […], n: 1 }]
```

### Sort, custom metrics, and slim output

```js
const leaderboard = groupBy(orders, 'status')
  .sum('revenue', 'amount')
  .reduce(
    'skus',
    (acc, order) => {
      acc.push(order.sku);
      return acc;
    },
    []
  )
  .orderBy('revenue', 'desc')
  .omitItems() // drop raw rows — call this last
  .toArray();

// [
//   { key: 'paid', revenue: 150, skus: ['A', 'B'] },
//   { key: 'pending', revenue: 80, skus: ['A'] },
//   …
// ]
```

---

## Cookbook

### Single-pass multi-aggregate

Prefer `aggregate()` when you need several metrics — one scan per group instead of one scan per chained method.

```js
groupBy(orders, 'status').aggregate({
  sum: ['revenue', 'amount'],
  avg: ['avgOrder', 'amount'],
  min: ['smallest', 'amount'],
  max: ['largest', 'amount'],
  distinctCount: ['skus', 'sku'],
  count: ['orders'],
}).data;
```

| Spec key         | Entry                              | Notes                          |
|------------------|------------------------------------|--------------------------------|
| `sum`            | `[alias, column]` or a list of those | Numeric sum; skips null / NaN / non-numeric |
| `avg`            | `[alias, column, decimals?]`       | Mean of numeric values; defaults to 2 decimal places |
| `min` / `max`    | `[alias, column]`                  | Empty / no numeric values → `null` |
| `distinctCount`  | `[alias, column]`                  | Unique value count             |
| `count`          | `[alias]`                          | Item count (no column)         |

Pass an array of entries to compute the same op on more than one column in one scan:

```js
groupBy(orders, 'status').aggregate({
  sum: [
    ['revenue', 'amount'],
    ['tax', 'taxAmount'],
  ],
  count: ['orders'],
}).data;
```

### Filter before aggregating

```js
groupBy(orders, 'status')
  .where((order) => order.amount >= 100)
  .sum('revenue', 'amount')
  .count('orders')
  .data;
```

Groups that become empty after `where` are removed. `keys`, `firstGroup`, and `lastGroup` stay in sync.

Empty input is valid — `groupBy([], 'status')` returns empty `.data` / `.toArray()`, matching `Object.groupBy`. Missing grouping keys land in `"undefined"` unless you pass `{ strict: true }`.

### Nested fields & accessors

Every column argument accepts a string key or a function:

```js
groupBy(checkoutEvents, (row) => row.meta.channel)
  .aggregate({
    sum: ['revenue', (row) => row.payment.total],
    count: ['checkouts'],
  })
  .data;
```

Multi-key accessors work the same way:

```js
groupBy(rows, [
  (row) => row.meta.region,
  (row) => row.meta.plan,
]).sum('totalSeats', 'seats').toArray();
```

### TypeScript

```ts
import { groupBy } from 'groupjs_by';

interface Order {
  status: string;
  amount: number;
}

const result = groupBy(orders, 'status')
  .sum('revenue', 'amount')
  .count('orders')
  .data;
```

---

## API

### `groupBy(data, key, options?)`

| Param  | Type | Description |
|--------|------|-------------|
| `data` | `T[]` | Array of objects (empty arrays return an empty result) |
| `key`  | `string \| (item) => any \| Array<string \| (item) => any>` | Single field, accessor, or multi-key list |
| `options.strict` | `boolean` | If `true`, throw when a string key is missing on any object. Default: missing keys group under `"undefined"`. |

Returns a chainable `GroupResult`. Throws if `data` is not an array, or if `key` is missing.

For multi-key grouping, `.data` stores groups under `JSON.stringify(keyParts)`. Prefer `.keys` or `.toArray()` when consuming composite keys.

---

### Aggregates

All aggregate methods (except `count`) take an **alias** (output property name) and a **column** (`string` or accessor). They return `this` for chaining.

| Method | Signature | Empty group |
|--------|-----------|-------------|
| `.sum` | `(alias, column)` | `0` |
| `.avg` | `(alias, column, decimals = 2)` | `null` |
| `.min` | `(alias, column)` | `null` |
| `.max` | `(alias, column)` | `null` |
| `.distinctCount` | `(alias, column)` | `0` |
| `.count` | `(alias)` | `0` |
| `.aggregate` | `(spec)` | Same rules per metric |
| `.reduce` | `(alias, reducer, initial?)` | Same as `Array#reduce` |

`sum` / `avg` / `min` / `max` skip `null`, `undefined`, `''`, and non-finite values so mixed JSON does not poison a group with `NaN`. Numeric strings (e.g. `'4'`) still coerce. `avg` divides by the count of numeric values, not the raw group size.

```js
.aggregate({
  sum: ['total', 'amount'],
  avg: ['mean', 'amount', 2],
  min: ['lo', 'amount'],
  max: ['hi', 'amount'],
  distinctCount: ['skus', 'sku'],
  count: ['n'],
})

// Custom per-group fold (array/object initials are shallow-cloned per group)
.reduce('skus', (acc, row) => { acc.push(row.sku); return acc; }, [])
.reduce('product', (acc, row) => acc * row.amount, 1)
```

---

### `.orderBy(field, direction = 'asc')`

Reorder groups (updates `.data` key order, `.keys`, and `.toArray()`).

| Param | Type | Description |
|-------|------|-------------|
| `field` | `'key' \| string \| (group, key) => any` | Sort by group key, an aggregate alias, or a custom value |
| `direction` | `'asc' \| 'desc'` | Default `'asc'`; nullish values sort last on asc |

```js
.orderBy('revenue', 'desc')
.orderBy('key', 'asc')
.orderBy((group) => group.n, 'desc')
```

---

### `.omitItems()`

Deletes `items` from every group to free memory. Call **after** aggregations / `where` / `reduce`. Further item-based ops throw.

---

### `.where(predicate)`

| Param       | Type                 | Description             |
|-------------|----------------------|-------------------------|
| `predicate` | `(item: T) => boolean` | Keep item when truthy |

Filters items inside each group. Empty groups are deleted from `.data`.

---

### `.toArray()`

Returns an array of group rows (insertion order):

```js
[{ key, items, ...aliases }]
```

- Single-key: `key` is a string (same as `Object.keys` on `.data`)
- Multi-key: `key` is an array of part values, e.g. `['US', 'paid']`

Useful for sorting, mapping to UI tables, or serializing without composite object keys.

---

### Result properties

| Property      | Type       | Description |
|---------------|------------|-------------|
| `.data`       | `object`   | `{ [groupKey]: { items, …aliases } }` |
| `.keys`       | `any[]`    | Current group keys (strings, or arrays when multi-key) |
| `.firstGroup` | `T[]`      | Items in the first group |
| `.lastGroup`  | `T[]`      | Items in the last group |

`keys`, `firstGroup`, and `lastGroup` reflect the latest state after `where`.

---

## Performance notes

- `min` / `max` use tight loops (safe on groups with 100k+ rows — no argument-spread stack limits).
- `distinctCount` uses a `Set` (linear in group size).
- `aggregate()` is the fastest path when computing several metrics together.

Local micro-benchmark (optional, not a CI gate):

```bash
npm run bench
```

---

## Compatibility

- **Runtime:** Node.js 18+ (CommonJS + ESM); browsers via bundlers
- **Entry points:** `require('groupjs_by')` → `index.js`; `import` → `index.mjs`
- **Dependencies:** none
- **Types:** bundled (`index.d.ts` / `index.d.mts`)

---

## Contributing

```bash
git clone https://github.com/juli04guilar/groupBy.git
cd groupBy
yarn install   # or npm install
npm test
```

Pull requests and issues are welcome.

---

## License

[MIT](./LICENSE) © [Julio Aguilar](https://github.com/juli04guilar)

---
_Source: https://npm.io/package/groupjs_by · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
