diff --git a/.changeset/minmax-falsy-extremes.md b/.changeset/minmax-falsy-extremes.md new file mode 100644 index 000000000..327ad6403 --- /dev/null +++ b/.changeset/minmax-falsy-extremes.md @@ -0,0 +1,5 @@ +--- +'@tanstack/db-ivm': patch +--- + +Compare min and max aggregates against `undefined` instead of truthiness so `0`, `0n`, and `""` can be the extreme of a group. diff --git a/packages/db-ivm/src/operators/groupBy.ts b/packages/db-ivm/src/operators/groupBy.ts index 435156d25..e2fd19df1 100644 --- a/packages/db-ivm/src/operators/groupBy.ts +++ b/packages/db-ivm/src/operators/groupBy.ts @@ -237,7 +237,7 @@ export function min( reduce: (values) => { let minValue: V | undefined for (const [value, _multiplicity] of values) { - if (!minValue || (value && value < minValue)) { + if (minValue === undefined || value < minValue) { minValue = value } } @@ -267,7 +267,7 @@ export function max( reduce: (values) => { let maxValue: V | undefined for (const [value, _multiplicity] of values) { - if (!maxValue || (value && value > maxValue)) { + if (maxValue === undefined || value > maxValue) { maxValue = value } } diff --git a/packages/db-ivm/tests/operators/groupBy.test.ts b/packages/db-ivm/tests/operators/groupBy.test.ts index 52b0fac65..4f6acf584 100644 --- a/packages/db-ivm/tests/operators/groupBy.test.ts +++ b/packages/db-ivm/tests/operators/groupBy.test.ts @@ -624,6 +624,108 @@ describe(`Operators`, () => { expect(latestMessage.getInner()).toEqual(expectedResult) }) + test(`min and max reduce keep 0, 0n, and empty string as extremes`, () => { + const minNum = min() + const maxNum = max() + const minStr = min() + const minBig = min() + const maxBig = max() + + expect( + minNum.reduce([ + [5, 1], + [0, 1], + ]), + ).toBe(0) + expect( + minNum.reduce([ + [0, 1], + [3, 1], + ]), + ).toBe(0) + expect( + maxNum.reduce([ + [-2, 1], + [0, 1], + [-1, 1], + ]), + ).toBe(0) + expect( + maxNum.reduce([ + [0, 1], + [-1, 1], + ]), + ).toBe(0) + expect( + minStr.reduce([ + [`b`, 1], + [``, 1], + ]), + ).toBe(``) + expect( + minStr.reduce([ + [``, 1], + [`a`, 1], + ]), + ).toBe(``) + expect( + minBig.reduce([ + [5n, 1], + [0n, 1], + ]), + ).toBe(0n) + expect( + maxBig.reduce([ + [-2n, 1], + [0n, 1], + ]), + ).toBe(0n) + }) + + test(`with min and max aggregates including a zero amount`, () => { + const graph = new D2() + const input = graph.newInput<{ + category: string + amount: number + }>() + let latestMessage: any = null + + input.pipe( + groupBy((data) => ({ category: data.category }), { + minimum: min((data) => data.amount), + maximum: max((data) => data.amount), + }), + output((message) => { + latestMessage = message + }), + ) + + graph.finalize() + + input.sendData( + new MultiSet([ + [{ category: `A`, amount: 10 }, 1], + [{ category: `A`, amount: 0 }, 1], + [{ category: `A`, amount: 7 }, 1], + ]), + ) + graph.run() + + expect(latestMessage.getInner()).toEqual([ + [ + [ + serializeValue({ category: `A` }), + { + category: `A`, + minimum: 0, + maximum: 10, + }, + ], + 1, + ], + ]) + }) + test(`with median and mode aggregates`, () => { const graph = new D2() const input = graph.newInput<{