Collection aggregates: x[] and x[].f

An empty [] after a collection reads an aggregate over its members: robots[] is how many there are, robots[].battery is the sum of their battery. Put either in a formula and it stays live.

Kernel: this.me 4.2.1 (npm) ⓘ

Source: the parser AggregateOp / AggregateRef in path-expr.ts, the evaluator evaluateAggregate in aggregate.ts, change tracking aggregate-index.ts. TypeScript reference: Aggregates (TypeScript).

Each block below has a Run button. Run executes the blocks of this page in order on one new ME() from this.me 4.2.1, so later blocks build on earlier ones. The comment after each read is what 4.2.1 returned.

The two forms

Form Name Value
C[] count how many members collection C has
C[].f sum the sum of field f over every member of C (f can be a path: C[].stats.hp)

There are no other aggregates: no avg, min or max. An average is the sum divided by the count.

Read one directly

import { ME } from "this.me";
const me = new ME();

me.robots[1].battery(80); me.robots[1].ok(true);
me.robots[2].battery(50); me.robots[2].ok(true);
me.robots[3].battery(20); me.robots[3].ok(false);
me.robots[4].battery(90); me.robots[4].ok(true);

me("robots[]");            // 4
me("robots[].battery");    // 240
me("robots[].ok");         // 3

true counts as 1 and false as 0, so the sum of a boolean field is how many members have it set.

In a formula

me.fleet["="]("n", "robots[]");
me.fleet["="]("total", "robots[].battery");
me.fleet["="]("avg", "robots[].battery / robots[]");
me.fleet["="]("oks", "robots[].ok");

me("fleet.n");       // 4
me("fleet.total");   // 240
me("fleet.avg");     // 60
me("fleet.oks");     // 3

The collection path is written from the root, even when the formula is declared under fleet. See Limitations.

What explain() shows

An aggregate is one input of the formula, with kind: "aggregate" and an aggregate block:

me.explain("fleet.oks").derivation.inputs[0].aggregate;
me.explain("fleet.oks").meta.dependsOn;    // ["robots[].ok"]

Full result of me.explain("fleet.oks") at this point (timestamp shortened):

{
  "path": "fleet.oks",
  "value": 3,
  "expr": "robots[].ok",
  "derivation": {
    "expression": "robots[].ok",
    "inputs": [
      {
        "label": "robots[].ok",
        "path": "robots[].ok",
        "kind": "aggregate",
        "value": 3,
        "origin": "public",
        "masked": false,
        "status": "resolved",
        "aggregate": {
          "collection": "robots",
          "field": "ok",
          "op": "sum",
          "context": "public-view",
          "coverage": "public-view",
          "status": "resolved",
          "members": 4,
          "terms": 4,
          "domain": "boolean"
        }
      }
    ]
  },
  "meta": {
    "dependsOn": ["robots[].ok"],
    "lastComputedAt": 1792117617602
  }
}
aggregate field Meaning
collection, field, op The reference as parsed: op is "count" or "sum"; field is null for a count.
context, coverage "public-view": the aggregate reads what me.as(null) would see.
status resolved, absent (empty collection), incomplete (a member is missing the field or it is not a number / boolean), deferred, cycle, unsupported, non-finite.
members How many members the collection has.
terms How many of them contributed a value.
domain "boolean" when every term was a boolean, "number" otherwise, null with no terms.
problems Present when not resolved: a count and a sample of the member paths at fault.

It stays live

Writing one member recomputes the formulas that read the collection:

me.robots[3].ok(true);

me("fleet.oks");                            // 4
me.explain("fleet.oks").meta.k;             // 4
me.explain("fleet.oks").meta.recomputed;    // ["fleet.n","fleet.total","fleet.avg","fleet.oks"]
me.explain("fleet.oks").meta.changed;       // ["fleet.oks"]
me.explain("fleet.oks").meta.sourcePath;    // "robots.3.ok"

Every aggregate on robots was recomputed (k is 4), and only fleet.oks changed. See Performance.

Booleans in arithmetic

An aggregate’s value is a number, so it composes with any operator:

me.fleet["="]("okPlusOne", "robots[].ok + 1");
me.fleet["="]("allOk", "robots[].ok == robots[]");

me("fleet.okPlusOne");    // 5
me("fleet.allOk");        // true
me.explain("fleet.oks").derivation.inputs[0].aggregate.domain;     // "boolean"
me.explain("fleet.total").derivation.inputs[0].aggregate.domain;   // "number"

Summing a derived field

A per-member formula declared with [i] produces a field on each member. The aggregate sums it:

me.fleet.threshold(60);
me.robots["[i]"]["="]("canTake", "battery > fleet.threshold && ok");
me.fleet["="]("available", "robots[].canTake");
me("fleet.available");    // 2

me.robots[2].battery(75);
me("fleet.available");    // 3
me.explain("fleet.available").meta.recomputed;

The last read returns ["robots.2.canTake","fleet.n","fleet.total","fleet.avg","fleet.oks","fleet.okPlusOne","fleet.allOk","fleet.available"]: the member’s own canTake, then every formula reading robots[].

A member without the field

A sum is all or nothing. A new member without ok makes robots[].ok undefined (never a partial sum), and explain names the member:

me.robots[5].battery(70);

me("fleet.n");        // 5
me("fleet.total");    // 335
me("fleet.avg");      // 67
me("fleet.oks");      // undefined
me.explain("fleet.oks").meta.unresolved;    // {"reason":"incomplete","inputs":["robots[].ok"],"causes":[{"path":"robots[].ok","status":"incomplete"}]}
me.explain("fleet.oks").derivation.inputs[0].aggregate.problems;    // {"count":1,"sample":[{"path":"robots.5.ok","kind":"missing"}]}

me.robots[5].ok(true);
me("fleet.oks");      // 5

Empty collections and unknown fields

me.fleet["="]("drones", "drones[]");
me.fleet["="]("nope", "robots[].nope");

me("fleet.drones");    // undefined
me.explain("fleet.drones").meta.unresolved;    // {"reason":"missing-input","inputs":["drones[]"],"causes":[{"path":"drones[]","status":"absent"}]}
me("fleet.nope");      // undefined

An empty or missing collection is undefined, not 0.

Limitations

Paths are from the root. Inside fleet, squad[].hp does not read fleet.squad; write fleet.squad[].hp:

me.fleet.squad[1].hp(5);
me.fleet.squad[2].hp(7);
me.fleet["="]("hpRel", "squad[].hp");
me.fleet["="]("hpRoot", "fleet.squad[].hp");

me("fleet.hpRel");     // undefined
me("fleet.hpRoot");    // 12

Only []. Function calls, [*], ternaries, (bool) * 1, a selector before [] and a second [] all give undefined:

me.bad["="]("a", "sum(robots[].battery)");
me.bad["="]("b", "count(robots)");
me.bad["="]("c", "robots[*].battery");
me.bad["="]("d", "fleet.n > 2 ? 1 : 0");
me.bad["="]("e", "(robots[1].ok) * 1");
me.bad["="]("f", "robots[battery > 40][]");
me.bad["="]("g", "robots[].cells[]");

me("bad.a");    // undefined
me("bad.b");    // undefined
me("bad.c");    // undefined
me("bad.d");    // undefined
me("bad.e");    // undefined
me("bad.f");    // undefined
me("bad.g");    // undefined
me.explain("bad.f").meta.unresolved;    // {"reason":"evaluation-failed","detail":"rejected-path-form"}

To count members that pass a test, declare the test per member with [i] and sum it (see Summing a derived field).

Other rules from the source:

Performance

A write under a collection recomputes every formula that holds an aggregate of that collection, and each recompute walks every member. The cost of one write grows with the number of members. Measured on Node 20 with this.me 4.2.1, 1000 members, one per-member [i] formula and one r[].hi aggregate:

const m = new ME();
for (let i = 1; i <= 1000; i++) m.r[i].b(50);
m.r["[i]"]["="]("hi", "b > 30");
m.f["="]("c", "r[].hi");
// 200 writes m.r[1 + k].b(k % 2 ? 10 : 60): 783.4 ms, 3.92 ms per write; m("f.c") → 900

Few aggregates on collections of hundreds are fine. For thousands of members written often, keep the aggregate on a smaller collection.

Tests

Run them with npm run test:aggregates in Typescript/.

See also

.me Syntax · .me Operators · me.explain(path) · What is O(k)? · Warehouse Fleet (aggregates in a live demo)